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.createNPCs = this.createNPCs.bind(this);
|
||||||
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
||||||
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
|
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
|
||||||
|
this.reconcilePoliticalVacancies = this.reconcilePoliticalVacancies.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenInterests(req, res) {
|
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) {
|
async getNPCsCreationStatus(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
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.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
||||||
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
||||||
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
||||||
|
router.post('/falukant/politics/reconcile-vacancies', authenticate, adminController.reconcilePoliticalVacancies);
|
||||||
|
|
||||||
// --- Minigames Admin ---
|
// --- Minigames Admin ---
|
||||||
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);
|
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 DebtorsPrism from '../models/falukant/data/debtors_prism.js';
|
||||||
import Candidate from '../models/falukant/data/candidate.js';
|
import Candidate from '../models/falukant/data/candidate.js';
|
||||||
import PoliticalOffice from '../models/falukant/data/political_office.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 { sequelize } from '../utils/sequelize.js';
|
||||||
import npcCreationJobService from './npcCreationJobService.js';
|
import npcCreationJobService from './npcCreationJobService.js';
|
||||||
import VocabService from './vocabService.js';
|
import VocabService from './vocabService.js';
|
||||||
@@ -918,6 +920,57 @@ class AdminService {
|
|||||||
return titles;
|
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) {
|
async updateFalukantRegionMap(userId, regionId, map) {
|
||||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||||
throw new Error('noaccess');
|
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
|
const result = openPositions
|
||||||
.filter(election => {
|
.filter(election => {
|
||||||
if (allowedOfficeNames.size > 0 && !allowedOfficeNames.has(election.officeType?.name)) return false;
|
// Die verbindlichen Zugangsvoraussetzungen stehen bei dem Amt
|
||||||
return true;
|
// selbst (political_office_prerequisite). Die zusätzlich
|
||||||
})
|
// gepflegten Titel-Benefits waren unvollständig und haben
|
||||||
.filter(election => {
|
// gültige Ausschreibungen (z. B. councillor) verborgen.
|
||||||
const prereqs = election.officeType.prerequisites || [];
|
const prereqs = election.officeType.prerequisites || [];
|
||||||
// Wenn es keine Voraussetzungen gibt, ist die Position grundsätzlich wählbar.
|
// Wenn es keine Voraussetzungen gibt, ist die Position grundsätzlich wählbar.
|
||||||
if (!Array.isArray(prereqs) || prereqs.length === 0) return true;
|
if (!Array.isArray(prereqs) || prereqs.length === 0) return true;
|
||||||
|
|||||||
@@ -348,7 +348,13 @@
|
|||||||
"nextRun": "Pinakataas nga sunod nga dagan",
|
"nextRun": "Pinakataas nga sunod nga dagan",
|
||||||
"remaining": "Nahibilin",
|
"remaining": "Nahibilin",
|
||||||
"noTasks": "Walay tasks para ani nga worker.",
|
"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": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -383,7 +383,13 @@
|
|||||||
"cadence": "Intervall",
|
"cadence": "Intervall",
|
||||||
"nextRun": "Spätester nächster Lauf",
|
"nextRun": "Spätester nächster Lauf",
|
||||||
"remaining": "Verbleibend",
|
"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": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -438,7 +438,13 @@
|
|||||||
"nextRun": "Latest next run",
|
"nextRun": "Latest next run",
|
||||||
"remaining": "Remaining",
|
"remaining": "Remaining",
|
||||||
"noTasks": "No tasks for this worker.",
|
"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": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -321,7 +321,13 @@
|
|||||||
"nextRun": "Próxima ejecución máxima",
|
"nextRun": "Próxima ejecución máxima",
|
||||||
"remaining": "Restante",
|
"remaining": "Restante",
|
||||||
"noTasks": "No hay tareas para este worker.",
|
"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": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -321,7 +321,13 @@
|
|||||||
"nextRun": "Prochaine exécution au plus tard",
|
"nextRun": "Prochaine exécution au plus tard",
|
||||||
"remaining": "Restant",
|
"remaining": "Restant",
|
||||||
"noTasks": "Aucune tâche pour ce worker.",
|
"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": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -22,8 +22,15 @@
|
|||||||
<button type="button" @click="requestSchedules">
|
<button type="button" @click="requestSchedules">
|
||||||
{{ $t('admin.falukant.workerSchedules.refresh') }}
|
{{ $t('admin.falukant.workerSchedules.refresh') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" :disabled="reconcilingPolitics" @click="reconcilePoliticalVacancies">
|
||||||
|
{{ $t(reconcilingPolitics
|
||||||
|
? 'admin.falukant.workerSchedules.politicsReconcileLoading'
|
||||||
|
: 'admin.falukant.workerSchedules.politicsReconcile') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="politicsResult" class="politics-result">{{ politicsResult }}</p>
|
||||||
|
|
||||||
<p v-if="generatedAt" class="generated-at">
|
<p v-if="generatedAt" class="generated-at">
|
||||||
{{ $t('admin.falukant.workerSchedules.generatedAt') }}: {{ formatTs(generatedAt) }}
|
{{ $t('admin.falukant.workerSchedules.generatedAt') }}: {{ formatTs(generatedAt) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -79,6 +86,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AdminFalukantWorkerSchedulesView',
|
name: 'AdminFalukantWorkerSchedulesView',
|
||||||
@@ -91,6 +99,8 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
timer: null,
|
timer: null,
|
||||||
|
reconcilingPolitics: false,
|
||||||
|
politicsResult: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -177,6 +187,26 @@ export default {
|
|||||||
this.error = this.$t('admin.falukant.workerSchedules.sendError');
|
this.error = this.$t('admin.falukant.workerSchedules.sendError');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async reconcilePoliticalVacancies() {
|
||||||
|
if (!window.confirm(this.$t('admin.falukant.workerSchedules.politicsReconcileConfirm'))) return;
|
||||||
|
this.reconcilingPolitics = true;
|
||||||
|
this.politicsResult = null;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.post('/api/admin/falukant/politics/reconcile-vacancies');
|
||||||
|
const count = Number(data?.createdCount || 0);
|
||||||
|
this.politicsResult = count
|
||||||
|
? this.$t('admin.falukant.workerSchedules.politicsReconcileSuccess', {
|
||||||
|
count,
|
||||||
|
date: new Date(data.electionDate).toLocaleString()
|
||||||
|
})
|
||||||
|
: this.$t('admin.falukant.workerSchedules.politicsReconcileNone');
|
||||||
|
} catch (err) {
|
||||||
|
this.politicsResult = err?.response?.data?.error
|
||||||
|
|| this.$t('admin.falukant.workerSchedules.politicsReconcileError');
|
||||||
|
} finally {
|
||||||
|
this.reconcilingPolitics = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
handleDaemonMessage(evt) {
|
handleDaemonMessage(evt) {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(evt.data);
|
const payload = JSON.parse(evt.data);
|
||||||
@@ -214,6 +244,7 @@ export default {
|
|||||||
.toolbar { display: flex; gap: 12px; align-items: center; margin: 12px 0; flex-wrap: wrap; }
|
.toolbar { display: flex; gap: 12px; align-items: center; margin: 12px 0; flex-wrap: wrap; }
|
||||||
.toggle { display: inline-flex; gap: 6px; align-items: center; }
|
.toggle { display: inline-flex; gap: 6px; align-items: center; }
|
||||||
.generated-at { color: #666; margin-bottom: 8px; }
|
.generated-at { color: #666; margin-bottom: 8px; }
|
||||||
|
.politics-result { margin: 8px 0; color: #216b2f; }
|
||||||
.workers { display: grid; gap: 12px; }
|
.workers { display: grid; gap: 12px; }
|
||||||
.worker-card { border: 1px solid #ddd; border-radius: 8px; padding: 12px; background: #fff; }
|
.worker-card { border: 1px solid #ddd; border-radius: 8px; padding: 12px; background: #fff; }
|
||||||
.worker-card__header { display: flex; justify-content: space-between; gap: 12px; align-items: center; flex-wrap: wrap; }
|
.worker-card__header { display: flex; justify-content: space-between; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||||
|
|||||||
Reference in New Issue
Block a user