feat(AdminController, AdminService, AdminRouter, localization): add character death cleanup feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s
- Implemented `adminCleanupCharacterDeathArtifacts` method in AdminService to remove stale character-bound data after death/inheritance, including knowledge, debtors prism, and political offices. - Added corresponding route in AdminRouter for triggering the cleanup process via an API endpoint. - Enhanced AdminController to handle requests for the new cleanup feature, ensuring proper error handling and response formatting. - Updated frontend components to include a user interface for initiating the cleanup, with localization support in both English and German for improved user experience.
This commit is contained in:
@@ -16,6 +16,7 @@ class AdminController {
|
|||||||
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
|
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
|
||||||
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
|
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
|
||||||
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
|
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
|
||||||
|
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
|
||||||
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
|
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
|
||||||
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
|
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
|
||||||
this.updateFalukantStock = this.updateFalukantStock.bind(this);
|
this.updateFalukantStock = this.updateFalukantStock.bind(this);
|
||||||
@@ -507,6 +508,21 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async adminCleanupCharacterDeathArtifacts(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { characterId } = req.params;
|
||||||
|
const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId);
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess'
|
||||||
|
? 403
|
||||||
|
: (['invalidCharacter', 'notfound'].includes(error.message) ? 400 : 500);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getFalukantUserBranches(req, res) {
|
async getFalukantUserBranches(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
const { userid: userId } = req.headers;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ router.get('/falukant/character/:characterId/potential-fathers', authenticate, a
|
|||||||
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
|
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
|
||||||
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
|
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
|
||||||
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
|
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
|
||||||
|
router.post('/falukant/character/:characterId/death-cleanup', authenticate, adminController.adminCleanupCharacterDeathArtifacts);
|
||||||
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
|
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
|
||||||
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
|
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
|
||||||
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
|
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UserParamType from "../models/type/user_param.js";
|
|||||||
import ContactMessage from "../models/service/contactmessage.js";
|
import ContactMessage from "../models/service/contactmessage.js";
|
||||||
import ContactService from "./ContactService.js";
|
import ContactService from "./ContactService.js";
|
||||||
import { sendAnswerEmail } from './emailService.js';
|
import { sendAnswerEmail } from './emailService.js';
|
||||||
import { Op, Sequelize } from 'sequelize';
|
import { Op, QueryTypes, Sequelize } from 'sequelize';
|
||||||
import FalukantUser from "../models/falukant/data/user.js";
|
import FalukantUser from "../models/falukant/data/user.js";
|
||||||
import FalukantCharacter from "../models/falukant/data/character.js";
|
import FalukantCharacter from "../models/falukant/data/character.js";
|
||||||
import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js";
|
import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js";
|
||||||
@@ -32,6 +32,10 @@ import ChildRelation from "../models/falukant/data/child_relation.js";
|
|||||||
import Relationship from "../models/falukant/data/relationship.js";
|
import Relationship from "../models/falukant/data/relationship.js";
|
||||||
import RelationshipType from "../models/falukant/type/relationship.js";
|
import RelationshipType from "../models/falukant/type/relationship.js";
|
||||||
import RelationshipState from "../models/falukant/data/relationship_state.js";
|
import RelationshipState from "../models/falukant/data/relationship_state.js";
|
||||||
|
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 { 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';
|
||||||
@@ -1172,6 +1176,255 @@ class AdminService {
|
|||||||
return { success: true, childCharacterId, gender: childGender };
|
return { success: true, childCharacterId, gender: childGender };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async adminCleanupCharacterDeathArtifacts(userId, characterId) {
|
||||||
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||||
|
throw new Error('noaccess');
|
||||||
|
}
|
||||||
|
const parsedCharacterId = Number(characterId);
|
||||||
|
if (!Number.isFinite(parsedCharacterId) || parsedCharacterId <= 0) {
|
||||||
|
throw new Error('invalidCharacter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const character = await FalukantCharacter.findByPk(parsedCharacterId, {
|
||||||
|
attributes: ['id', 'userId']
|
||||||
|
});
|
||||||
|
if (!character) {
|
||||||
|
throw new Error('notfound');
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
characterId: parsedCharacterId,
|
||||||
|
knowledgeDeleted: 0,
|
||||||
|
debtorsPrismDeleted: 0,
|
||||||
|
politicalOfficeArchived: 0,
|
||||||
|
politicalOfficeDeleted: 0,
|
||||||
|
electionCandidateDeleted: 0,
|
||||||
|
characterDeleted: false
|
||||||
|
};
|
||||||
|
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
|
summary.knowledgeDeleted = await Knowledge.destroy({
|
||||||
|
where: { characterId: parsedCharacterId },
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
summary.debtorsPrismDeleted = await DebtorsPrism.destroy({
|
||||||
|
where: { characterId: parsedCharacterId },
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
summary.electionCandidateDeleted = await Candidate.destroy({
|
||||||
|
where: { characterId: parsedCharacterId },
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
|
||||||
|
const archivedRows = await sequelize.query(
|
||||||
|
`
|
||||||
|
INSERT INTO falukant_log.political_office_history (
|
||||||
|
character_id,
|
||||||
|
office_type_id,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
po.character_id,
|
||||||
|
po.office_type_id,
|
||||||
|
COALESCE(po.created_at, NOW()),
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM falukant_data.political_office po
|
||||||
|
WHERE po.character_id = :characterId
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
summary.politicalOfficeArchived = archivedRows.length;
|
||||||
|
summary.politicalOfficeDeleted = await PoliticalOffice.destroy({
|
||||||
|
where: { characterId: parsedCharacterId },
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.occupied_political_office
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.political_benefit_last_tick
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.falukant_character_trait
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.church_application
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
OR supervisor_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.church_office
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
OR supervisor_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.political_appointment
|
||||||
|
WHERE appointer_character_id = :characterId
|
||||||
|
OR target_character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.marriage_proposals
|
||||||
|
WHERE requester_character_id = :characterId
|
||||||
|
OR proposed_character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.child_relation
|
||||||
|
WHERE father_character_id = :characterId
|
||||||
|
OR mother_character_id = :characterId
|
||||||
|
OR child_character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.relationship_state
|
||||||
|
WHERE relationship_id IN (
|
||||||
|
SELECT id FROM falukant_data.relationship
|
||||||
|
WHERE character1_id = :characterId
|
||||||
|
OR character2_id = :characterId
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.relationship
|
||||||
|
WHERE character1_id = :characterId
|
||||||
|
OR character2_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_data.region_tax_history
|
||||||
|
WHERE setter_character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_log.health_activity
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`
|
||||||
|
DELETE FROM falukant_log.political_office_history
|
||||||
|
WHERE character_id = :characterId
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
replacements: { characterId: parsedCharacterId },
|
||||||
|
type: QueryTypes.DELETE,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCharacters = await FalukantCharacter.destroy({
|
||||||
|
where: { id: parsedCharacterId },
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
summary.characterDeleted = deletedCharacters > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (character.userId) {
|
||||||
|
const falukantUser = await FalukantUser.findByPk(character.userId, {
|
||||||
|
include: [{ model: User, as: 'user', attributes: ['hashedId'] }]
|
||||||
|
});
|
||||||
|
const hashedId = falukantUser?.user?.hashedId;
|
||||||
|
if (hashedId) {
|
||||||
|
await notifyUser(hashedId, 'falukantUpdateStatus', {});
|
||||||
|
await notifyUser(hashedId, 'familychanged', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, ...summary };
|
||||||
|
}
|
||||||
|
|
||||||
// --- User Administration ---
|
// --- User Administration ---
|
||||||
async searchUsers(requestingHashedUserId, query) {
|
async searchUsers(requestingHashedUserId, query) {
|
||||||
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
||||||
|
|||||||
@@ -244,6 +244,14 @@
|
|||||||
"force": "Geburt auslösen",
|
"force": "Geburt auslösen",
|
||||||
"success": "Kind wurde angelegt (Taufe ausstehend).",
|
"success": "Kind wurde angelegt (Taufe ausstehend).",
|
||||||
"error": "Geburt konnte nicht ausgelöst werden."
|
"error": "Geburt konnte nicht ausgelöst werden."
|
||||||
|
},
|
||||||
|
"deathCleanup": {
|
||||||
|
"title": "Tod/Erbe-Aufräumen (Admin)",
|
||||||
|
"hint": "Entfernt veraltete charaktergebundene Daten nach Tod/Erbe (Wissen, Schuldnerkartei, politische Ämter inkl. Archiv, Wahlkandidatur).",
|
||||||
|
"action": "Cleanup ausführen",
|
||||||
|
"confirm": "Diesen Cleanup für den aktuellen Charakter jetzt ausführen?",
|
||||||
|
"success": "Cleanup erfolgreich ausgeführt.",
|
||||||
|
"error": "Cleanup konnte nicht ausgeführt werden."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
|
|||||||
@@ -299,6 +299,14 @@
|
|||||||
"force": "Trigger birth",
|
"force": "Trigger birth",
|
||||||
"success": "Child created (baptism pending).",
|
"success": "Child created (baptism pending).",
|
||||||
"error": "Could not trigger birth."
|
"error": "Could not trigger birth."
|
||||||
|
},
|
||||||
|
"deathCleanup": {
|
||||||
|
"title": "Death/inheritance cleanup (admin)",
|
||||||
|
"hint": "Removes stale character-bound data after death/inheritance (knowledge, debtors prism, political offices incl. archive, election candidacy).",
|
||||||
|
"action": "Run cleanup",
|
||||||
|
"confirm": "Run this cleanup for the current character now?",
|
||||||
|
"success": "Cleanup completed successfully.",
|
||||||
|
"error": "Cleanup failed."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createNPC": {
|
"createNPC": {
|
||||||
|
|||||||
@@ -128,6 +128,14 @@
|
|||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button type="button" @click="adminForceBirth">{{ $t('admin.falukant.edituser.birth.force') }}</button>
|
<button type="button" @click="adminForceBirth">{{ $t('admin.falukant.edituser.birth.force') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h4>{{ $t('admin.falukant.edituser.deathCleanup.title') }}</h4>
|
||||||
|
<p class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.deathCleanup.hint') }}</p>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button type="button" class="button-secondary" @click="adminCleanupDeathArtifacts">
|
||||||
|
{{ $t('admin.falukant.edituser.deathCleanup.action') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
@@ -567,6 +575,22 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showApiError(this, error, 'tr:admin.falukant.edituser.birth.error');
|
showApiError(this, error, 'tr:admin.falukant.edituser.birth.error');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async adminCleanupDeathArtifacts() {
|
||||||
|
const characterId = this.editableUser?.falukantData?.[0]?.character?.id;
|
||||||
|
if (!characterId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.confirm(this.$t('admin.falukant.edituser.deathCleanup.confirm'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await apiClient.post(`/api/admin/falukant/character/${characterId}/death-cleanup`);
|
||||||
|
showSuccess(this, 'tr:admin.falukant.edituser.deathCleanup.success');
|
||||||
|
await this.refreshEditableUser();
|
||||||
|
} catch (error) {
|
||||||
|
showApiError(this, error, 'tr:admin.falukant.edituser.deathCleanup.error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user