feat(politics): implement reconcilePoliticalVacancies endpoint and UI integration for managing political vacancies

This commit is contained in:
Torsten Schulz (local)
2026-08-24 10:40:23 +02:00
parent d0f4e33fc0
commit 684880a32e
10 changed files with 136 additions and 11 deletions

View File

@@ -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');