diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index bc54541..c9deabe 100755 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -66,6 +66,7 @@ class AdminController { this.createNPCs = this.createNPCs.bind(this); this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this); this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this); + this.reconcilePoliticalVacancies = this.reconcilePoliticalVacancies.bind(this); } async getOpenInterests(req, res) { @@ -740,6 +741,17 @@ class AdminController { } } + async reconcilePoliticalVacancies(req, res) { + try { + const { userid: userId } = req.headers; + const result = await AdminService.adminReconcilePoliticalVacancies(userId); + res.status(200).json(result); + } catch (error) { + console.error('[reconcilePoliticalVacancies]', error); + res.status(error.message === 'noaccess' ? 403 : 500).json({ error: error.message }); + } + } + async getNPCsCreationStatus(req, res) { try { const { userid: userId } = req.headers; diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index 6bb4426..737ed1b 100755 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -77,6 +77,7 @@ router.post('/moderation/reports/:reportId/status', authenticate, moderationCont router.post('/falukant/npcs/create', authenticate, adminController.createNPCs); router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus); router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility); +router.post('/falukant/politics/reconcile-vacancies', authenticate, adminController.reconcilePoliticalVacancies); // --- Minigames Admin --- router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns); diff --git a/backend/services/adminService.js b/backend/services/adminService.js index 437356a..25c358b 100755 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -40,6 +40,8 @@ import Knowledge from '../models/falukant/data/product_knowledge.js'; import DebtorsPrism from '../models/falukant/data/debtors_prism.js'; import Candidate from '../models/falukant/data/candidate.js'; import PoliticalOffice from '../models/falukant/data/political_office.js'; +import PoliticalOfficeType from '../models/falukant/type/political_office_type.js'; +import Election from '../models/falukant/data/election.js'; import { sequelize } from '../utils/sequelize.js'; import npcCreationJobService from './npcCreationJobService.js'; import VocabService from './vocabService.js'; @@ -918,6 +920,57 @@ class AdminService { return titles; } + /** Erstellt idempotent Ausschreibungen fuer alle fehlenden politischen Soll-Stellen. */ + async adminReconcilePoliticalVacancies(userId) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + + const now = new Date(); + const electionDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); + const [officeTypes, regions, offices, elections] = await Promise.all([ + PoliticalOfficeType.findAll({ attributes: ['id', 'name', 'regionType', 'seatsPerRegion', 'termLength'] }), + RegionData.findAll({ + attributes: ['id', 'name'], + include: [{ model: RegionType, as: 'regionType', attributes: ['labelTr'] }] + }), + PoliticalOffice.findAll({ + attributes: ['officeTypeId', 'regionId', 'createdAt'], + include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['termLength'] }] + }), + Election.findAll({ attributes: ['officeTypeId', 'regionId', 'postsToFill'] }) + ]); + + const activeSeats = new Map(); + for (const office of offices) { + const expiresAt = new Date(office.createdAt).getTime() + Number(office.type?.termLength || 0) * 86400000; + if (expiresAt > now.getTime()) { + const key = `${office.officeTypeId}:${office.regionId}`; + activeSeats.set(key, (activeSeats.get(key) || 0) + 1); + } + } + const reservedSeats = new Map(); + for (const election of elections) { + const key = `${election.officeTypeId}:${election.regionId}`; + reservedSeats.set(key, (reservedSeats.get(key) || 0) + Number(election.postsToFill || 0)); + } + + const records = []; + const created = []; + for (const type of officeTypes) { + for (const region of regions) { + if (region.regionType?.labelTr !== type.regionType) continue; + const key = `${type.id}:${region.id}`; + const postsToFill = Number(type.seatsPerRegion) - (activeSeats.get(key) || 0) - (reservedSeats.get(key) || 0); + if (postsToFill <= 0) continue; + records.push({ officeTypeId: type.id, regionId: region.id, postsToFill, date: electionDate, createdAt: now, updatedAt: now }); + created.push({ officeTypeId: type.id, officeTypeName: type.name, regionId: region.id, regionName: region.name, postsToFill }); + } + } + if (records.length) await Election.bulkCreate(records); + return { created, createdCount: created.length, electionDate }; + } + async updateFalukantRegionMap(userId, regionId, map) { if (!(await this.hasUserAccess(userId, 'falukantusers'))) { throw new Error('noaccess'); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 78d10c9..b7f47ed 100755 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -7125,14 +7125,12 @@ class FalukantService extends BaseService { } ] }); - const titleId = character.titleOfNobility ?? character.nobleTitle?.id; - const allowedOfficeNames = await getAllowedOfficeTypeNamesByTitle(titleId); const result = openPositions .filter(election => { - if (allowedOfficeNames.size > 0 && !allowedOfficeNames.has(election.officeType?.name)) return false; - return true; - }) - .filter(election => { + // Die verbindlichen Zugangsvoraussetzungen stehen bei dem Amt + // selbst (political_office_prerequisite). Die zusätzlich + // gepflegten Titel-Benefits waren unvollständig und haben + // gültige Ausschreibungen (z. B. councillor) verborgen. const prereqs = election.officeType.prerequisites || []; // Wenn es keine Voraussetzungen gibt, ist die Position grundsätzlich wählbar. if (!Array.isArray(prereqs) || prereqs.length === 0) return true; diff --git a/frontend/src/i18n/locales/ceb/admin.json b/frontend/src/i18n/locales/ceb/admin.json index 89f699a..8c9566a 100755 --- a/frontend/src/i18n/locales/ceb/admin.json +++ b/frontend/src/i18n/locales/ceb/admin.json @@ -348,7 +348,13 @@ "nextRun": "Pinakataas nga sunod nga dagan", "remaining": "Nahibilin", "noTasks": "Walay tasks para ani nga worker.", - "missingUserId": "Walay user ID nga anaa (kulang ang setUserId)." + "missingUserId": "Walay user ID nga anaa (kulang ang setUserId).", + "politicsReconcile": "Idugang ang kulang nga politikal nga mga bakante", + "politicsReconcileLoading": "Gidugang ang politikal nga mga bakante …", + "politicsReconcileConfirm": "Idugang karon ang kulang nga politikal nga mga bakante?", + "politicsReconcileSuccess": "Naghimo og {count} ka bakante. Petsa sa eleksiyon: {date}", + "politicsReconcileNone": "Walay kulang nga politikal nga mga bakante nga nakit-an.", + "politicsReconcileError": "Dili madugang ang politikal nga mga bakante." } }, "chatrooms": { diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index fe0f325..389810d 100755 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -383,7 +383,13 @@ "cadence": "Intervall", "nextRun": "Spätester nächster Lauf", "remaining": "Verbleibend", - "noTasks": "Keine Tasks für diesen Worker." + "noTasks": "Keine Tasks für diesen Worker.", + "politicsReconcile": "Fehlende politische Ausschreibungen ergänzen", + "politicsReconcileLoading": "Politische Ausschreibungen werden ergänzt …", + "politicsReconcileConfirm": "Fehlende politische Ausschreibungen jetzt ergänzen?", + "politicsReconcileSuccess": "{count} Ausschreibung(en) angelegt. Wahltermin: {date}", + "politicsReconcileNone": "Keine fehlenden politischen Ausschreibungen gefunden.", + "politicsReconcileError": "Politische Ausschreibungen konnten nicht ergänzt werden." } }, "chatrooms": { diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index b76b698..2f52422 100755 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -438,7 +438,13 @@ "nextRun": "Latest next run", "remaining": "Remaining", "noTasks": "No tasks for this worker.", - "missingUserId": "No user ID available (setUserId missing)." + "missingUserId": "No user ID available (setUserId missing).", + "politicsReconcile": "Add missing political vacancies", + "politicsReconcileLoading": "Adding political vacancies …", + "politicsReconcileConfirm": "Add the missing political vacancies now?", + "politicsReconcileSuccess": "Created {count} vacancy/vacancies. Election date: {date}", + "politicsReconcileNone": "No missing political vacancies found.", + "politicsReconcileError": "Political vacancies could not be added." } }, "chatrooms": { diff --git a/frontend/src/i18n/locales/es/admin.json b/frontend/src/i18n/locales/es/admin.json index 9b74b1b..4274ccd 100755 --- a/frontend/src/i18n/locales/es/admin.json +++ b/frontend/src/i18n/locales/es/admin.json @@ -321,7 +321,13 @@ "nextRun": "Próxima ejecución máxima", "remaining": "Restante", "noTasks": "No hay tareas para este worker.", - "missingUserId": "No hay ID de usuario disponible (falta setUserId)." + "missingUserId": "No hay ID de usuario disponible (falta setUserId).", + "politicsReconcile": "Añadir vacantes políticas pendientes", + "politicsReconcileLoading": "Añadiendo vacantes políticas …", + "politicsReconcileConfirm": "¿Añadir ahora las vacantes políticas pendientes?", + "politicsReconcileSuccess": "Se han creado {count} vacante(s). Fecha de elección: {date}", + "politicsReconcileNone": "No se encontraron vacantes políticas pendientes.", + "politicsReconcileError": "No se pudieron añadir las vacantes políticas." } }, "chatrooms": { diff --git a/frontend/src/i18n/locales/fr/admin.json b/frontend/src/i18n/locales/fr/admin.json index f041db0..94d31ee 100755 --- a/frontend/src/i18n/locales/fr/admin.json +++ b/frontend/src/i18n/locales/fr/admin.json @@ -321,7 +321,13 @@ "nextRun": "Prochaine exécution au plus tard", "remaining": "Restant", "noTasks": "Aucune tâche pour ce worker.", - "missingUserId": "Aucun ID utilisateur disponible (setUserId manquant)." + "missingUserId": "Aucun ID utilisateur disponible (setUserId manquant).", + "politicsReconcile": "Ajouter les postes politiques vacants", + "politicsReconcileLoading": "Ajout des postes politiques vacants …", + "politicsReconcileConfirm": "Ajouter maintenant les postes politiques vacants ?", + "politicsReconcileSuccess": "{count} poste(s) vacant(s) créé(s). Date de l'élection : {date}", + "politicsReconcileNone": "Aucun poste politique vacant manquant n'a été trouvé.", + "politicsReconcileError": "Les postes politiques vacants n'ont pas pu être ajoutés." } }, "chatrooms": { diff --git a/frontend/src/views/admin/falukant/WorkerSchedulesView.vue b/frontend/src/views/admin/falukant/WorkerSchedulesView.vue index 9c7ce77..b87750e 100755 --- a/frontend/src/views/admin/falukant/WorkerSchedulesView.vue +++ b/frontend/src/views/admin/falukant/WorkerSchedulesView.vue @@ -22,8 +22,15 @@ + +

{{ politicsResult }}

+

{{ $t('admin.falukant.workerSchedules.generatedAt') }}: {{ formatTs(generatedAt) }}

@@ -79,6 +86,7 @@