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.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
|
||||
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
|
||||
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
|
||||
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
|
||||
this.getFalukantUserBranches = this.getFalukantUserBranches.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) {
|
||||
try {
|
||||
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/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
|
||||
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.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
|
||||
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 ContactService from "./ContactService.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 FalukantCharacter from "../models/falukant/data/character.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 RelationshipType from "../models/falukant/type/relationship.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 npcCreationJobService from './npcCreationJobService.js';
|
||||
import VocabService from './vocabService.js';
|
||||
@@ -1172,6 +1176,255 @@ class AdminService {
|
||||
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 ---
|
||||
async searchUsers(requestingHashedUserId, query) {
|
||||
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
||||
|
||||
@@ -244,6 +244,14 @@
|
||||
"force": "Geburt auslösen",
|
||||
"success": "Kind wurde angelegt (Taufe ausstehend).",
|
||||
"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": {
|
||||
|
||||
@@ -299,6 +299,14 @@
|
||||
"force": "Trigger birth",
|
||||
"success": "Child created (baptism pending).",
|
||||
"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": {
|
||||
|
||||
@@ -128,6 +128,14 @@
|
||||
<div class="action-buttons">
|
||||
<button type="button" @click="adminForceBirth">{{ $t('admin.falukant.edituser.birth.force') }}</button>
|
||||
</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 class="action-buttons">
|
||||
@@ -567,6 +575,22 @@ export default {
|
||||
} catch (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