diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index b7f47ed..6fda4d3 100755 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -960,7 +960,7 @@ class FalukantService extends BaseService { include: [ { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }, - { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id'] } + { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id', 'level'] } ], // reputation: für Adel-Voraussetzungen u.a. (checkReputationRequirement nutzt user.character ohne Nachladen) attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation'] @@ -6916,9 +6916,20 @@ class FalukantService extends BaseService { async getElections(hashedUserId) { const user = await this.getFalukantUserByHashedId(hashedUserId); - if (!user || user.character.nobleTitle.labelTr === 'noncivil') { + const character = user?.character; + if (!character || Number(character.nobleTitle?.level || 0) < 2) { return []; } + + // Wahlberechtigung: Das Einstiegsamt (ID 1, assessor) wird von allen + // Bürgern oder höheren Ständen gewählt. Jedes weitere Amt wird nur + // von den aktuellen Inhabern des unmittelbar vorherigen Amttyps + // gewählt (z. B. Amt 2 von Inhabern von Amt 1). + const heldOffices = await PoliticalOffice.findAll({ + where: { characterId: character.id }, + attributes: ['officeTypeId'] + }); + const heldOfficeTypeIds = new Set(heldOffices.map((office) => Number(office.officeTypeId))); const rows = await sequelize.query( FalukantService.RECURSIVE_REGION_SEARCH, { @@ -6983,37 +6994,42 @@ class FalukantService extends BaseService { ] }); - return rawElections.map(election => { - const e = election.get({ plain: true }); + return rawElections + .filter((election) => { + const officeTypeId = Number(election.officeTypeId); + return officeTypeId === 1 || heldOfficeTypeIds.has(officeTypeId - 1); + }) + .map(election => { + const e = election.get({ plain: true }); + + const voted = Array.isArray(e.votes) && e.votes.length > 0; + const reducedCandidates = (e.candidates || []).map(cand => { + const ch = cand.character || {}; + const firstname = ch.definedFirstName?.name || ''; + const lastname = ch.definedLastName?.name || ''; + return { + id: cand.id, + title: ch.nobleTitle?.labelTr || null, + name: `${firstname} ${lastname}`.trim(), + age: calcAge(ch.birthdate), + gender: ch.gender + }; + }); - const voted = Array.isArray(e.votes) && e.votes.length > 0; - const reducedCandidates = (e.candidates || []).map(cand => { - const ch = cand.character || {}; - const firstname = ch.definedFirstName?.name || ''; - const lastname = ch.definedLastName?.name || ''; return { - id: cand.id, - title: ch.nobleTitle?.labelTr || null, - name: `${firstname} ${lastname}`.trim(), - age: calcAge(ch.birthdate), - gender: ch.gender + id: e.id, + officeType: { name: e.officeType.name }, + region: { + name: e.region.name, + regionType: { labelTr: e.region.regionType.labelTr } + }, + date: e.date, + postsToFill: e.postsToFill, + candidates: reducedCandidates, + voted: voted, + votedFor: voted ? e.votes.map(vote => { return vote.candidateId }) : null, }; }); - - return { - id: e.id, - officeType: { name: e.officeType.name }, - region: { - name: e.region.name, - regionType: { labelTr: e.region.regionType.labelTr } - }, - date: e.date, - postsToFill: e.postsToFill, - candidates: reducedCandidates, - voted: voted, - votedFor: voted ? e.votes.map(vote => { return vote.candidateId }) : null, - }; - }); } async vote(hashedUserId, votes) { diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index a6453a1..b229b6d 100755 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -4650,13 +4650,25 @@ export default { .exercise-unlock-conditions li { display: flex; justify-content: space-between; - padding: 4px 0; + gap: var(--space-3); + padding: 7px 0; color: var(--color-text-secondary); + border-top: 1px solid var(--color-border); +} +.exercise-unlock-conditions li:first-child { + border-top: 0; +} +.exercise-unlock-conditions li span { + color: var(--color-text-primary); + font-variant-numeric: tabular-nums; } .exercise-unlock-conditions li.ok { - color: var(--color-positive, #2a9d2b); + color: var(--color-success); font-weight: 600; } +.exercise-unlock-conditions li.ok span { + color: var(--color-success); +} .language-assistant-panel { display: grid; @@ -5809,9 +5821,11 @@ export default { } .exercise-lock-note { - background: #fff3cd; - border: 1px solid #ffc107; - color: #856404; + padding-left: 16px; + background: var(--color-primary-soft); + border: 1px solid var(--color-border-strong); + border-left: 4px solid var(--color-primary); + color: var(--color-text-primary); } .review-priority-note strong, @@ -5848,7 +5862,8 @@ export default { .vocab-trainer-stats { margin-bottom: 15px; padding: 10px; - background: #f5f5f5; + background: var(--color-surface-accent); + border: 1px solid var(--color-border); border-radius: 4px; } @@ -5866,15 +5881,17 @@ export default { .success-rate { font-weight: bold; - color: #28a745; + color: var(--color-success); } .mode-badge { padding: 5px 10px; border-radius: 4px; font-size: 0.9em; - background: #e9ecef; - color: #6c757d; + background: var(--color-surface-strong); + border: 1px solid var(--color-border); + color: var(--color-text-secondary); + transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease; } .mode-badge.mode-clickable { @@ -5882,7 +5899,15 @@ export default { } .mode-badge.mode-clickable:hover { - filter: brightness(0.98); + background: var(--color-primary-soft); + border-color: var(--color-primary); + color: var(--color-text-primary); +} + +.mode-badge.mode-clickable:focus-visible, +.btn-stop-trainer:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; } .mode-badge.mode-active { @@ -5892,14 +5917,15 @@ export default { } .mode-badge.mode-completed { - background: #28a745; - color: white; + background: var(--color-success); + border-color: var(--color-success); + color: var(--color-text-on-accent); } .btn-stop-trainer { padding: 5px 15px; - background: rgba(177, 59, 53, 0.92); - color: white; + background: var(--color-danger); + color: var(--color-text-on-accent); border: 1px solid transparent; border-radius: 4px; cursor: pointer; @@ -5907,7 +5933,7 @@ export default { } .btn-stop-trainer:hover { - background: var(--color-danger-hover); + background: var(--color-danger-hover, #96312c); } .vocab-question {