feat(politics): implement reconcilePoliticalVacancies endpoint and UI integration for managing political vacancies
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user