feat(falukant): add noble title level to election eligibility checks and improve election filtering logic

This commit is contained in:
Torsten Schulz (local)
2026-08-24 11:10:17 +02:00
parent 684880a32e
commit 8e72239358
2 changed files with 86 additions and 44 deletions

View File

@@ -960,7 +960,7 @@ class FalukantService extends BaseService {
include: [ include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', 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) // reputation: für Adel-Voraussetzungen u.a. (checkReputationRequirement nutzt user.character ohne Nachladen)
attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation'] attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation']
@@ -6916,9 +6916,20 @@ class FalukantService extends BaseService {
async getElections(hashedUserId) { async getElections(hashedUserId) {
const user = await this.getFalukantUserByHashedId(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 []; 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( const rows = await sequelize.query(
FalukantService.RECURSIVE_REGION_SEARCH, FalukantService.RECURSIVE_REGION_SEARCH,
{ {
@@ -6983,37 +6994,42 @@ class FalukantService extends BaseService {
] ]
}); });
return rawElections.map(election => { return rawElections
const e = election.get({ plain: true }); .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 { return {
id: cand.id, id: e.id,
title: ch.nobleTitle?.labelTr || null, officeType: { name: e.officeType.name },
name: `${firstname} ${lastname}`.trim(), region: {
age: calcAge(ch.birthdate), name: e.region.name,
gender: ch.gender 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) { async vote(hashedUserId, votes) {

View File

@@ -4650,13 +4650,25 @@ export default {
.exercise-unlock-conditions li { .exercise-unlock-conditions li {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 4px 0; gap: var(--space-3);
padding: 7px 0;
color: var(--color-text-secondary); 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 { .exercise-unlock-conditions li.ok {
color: var(--color-positive, #2a9d2b); color: var(--color-success);
font-weight: 600; font-weight: 600;
} }
.exercise-unlock-conditions li.ok span {
color: var(--color-success);
}
.language-assistant-panel { .language-assistant-panel {
display: grid; display: grid;
@@ -5809,9 +5821,11 @@ export default {
} }
.exercise-lock-note { .exercise-lock-note {
background: #fff3cd; padding-left: 16px;
border: 1px solid #ffc107; background: var(--color-primary-soft);
color: #856404; border: 1px solid var(--color-border-strong);
border-left: 4px solid var(--color-primary);
color: var(--color-text-primary);
} }
.review-priority-note strong, .review-priority-note strong,
@@ -5848,7 +5862,8 @@ export default {
.vocab-trainer-stats { .vocab-trainer-stats {
margin-bottom: 15px; margin-bottom: 15px;
padding: 10px; padding: 10px;
background: #f5f5f5; background: var(--color-surface-accent);
border: 1px solid var(--color-border);
border-radius: 4px; border-radius: 4px;
} }
@@ -5866,15 +5881,17 @@ export default {
.success-rate { .success-rate {
font-weight: bold; font-weight: bold;
color: #28a745; color: var(--color-success);
} }
.mode-badge { .mode-badge {
padding: 5px 10px; padding: 5px 10px;
border-radius: 4px; border-radius: 4px;
font-size: 0.9em; font-size: 0.9em;
background: #e9ecef; background: var(--color-surface-strong);
color: #6c757d; 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 { .mode-badge.mode-clickable {
@@ -5882,7 +5899,15 @@ export default {
} }
.mode-badge.mode-clickable:hover { .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 { .mode-badge.mode-active {
@@ -5892,14 +5917,15 @@ export default {
} }
.mode-badge.mode-completed { .mode-badge.mode-completed {
background: #28a745; background: var(--color-success);
color: white; border-color: var(--color-success);
color: var(--color-text-on-accent);
} }
.btn-stop-trainer { .btn-stop-trainer {
padding: 5px 15px; padding: 5px 15px;
background: rgba(177, 59, 53, 0.92); background: var(--color-danger);
color: white; color: var(--color-text-on-accent);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@@ -5907,7 +5933,7 @@ export default {
} }
.btn-stop-trainer:hover { .btn-stop-trainer:hover {
background: var(--color-danger-hover); background: var(--color-danger-hover, #96312c);
} }
.vocab-question { .vocab-question {