feat(chat): add chat room management functionality

- Created new chat schema in the database.
- Implemented chat room model with necessary fields (title, ownerId, roomTypeId, etc.).
- Added room type model and rights model for chat functionality.
- Developed API endpoints for managing chat rooms, including create, edit, and delete operations.
- Integrated chat room management into the admin interface with a dedicated view and dialog for room creation/editing.
- Added internationalization support for chat room management UI.
- Implemented autocomplete for victim selection in underground activities.
- Enhanced the underground view with new activity types and political target selection.
This commit is contained in:
Torsten Schulz (local)
2025-08-11 23:31:25 +02:00
parent 6062570fe8
commit 23f698d8fd
26 changed files with 1564 additions and 866 deletions

View File

@@ -55,6 +55,8 @@ import PoliticalOfficePrerequisite from '../models/falukant/predefine/political_
import PoliticalOfficeHistory from '../models/falukant/log/political_office_history.js';
import UndergroundType from '../models/falukant/type/underground.js';
import Notification from '../models/falukant/log/notification.js';
import PoliticalOffice from '../models/falukant/data/political_office.js';
import Underground from '../models/falukant/data/underground.js';
function calcAge(birthdate) {
const b = new Date(birthdate); b.setHours(0, 0);
@@ -1066,7 +1068,7 @@ class FalukantService extends BaseService {
});
if (regionUserDirectorProposals.length > 0) {
for (const p of regionUserDirectorProposals) {
await p.destroy();
await p.destroy();
}
}
notifyUser(hashedUserId, 'directorchanged');
@@ -2795,6 +2797,297 @@ class FalukantService extends BaseService {
});
return user.notifications;
}
async getPoliticalOfficeHolders(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: {
userId: user.id
}
});
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
const now = new Date();
const histories = await PoliticalOffice.findAll({
where: {
regionId: {
[Op.in]: relevantRegionIds
},
},
include: [{
model: FalukantCharacter,
as: 'holder',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
],
attributes: ['id', 'gender']
}, {
model: PoliticalOfficeType,
as: 'type',
}]
});
// Unikate nach character.id
const map = new Map();
histories.forEach(h => {
const c = h.holder;
if (c && c.id && !map.has(c.id)) {
map.set(c.id, {
id: c.id,
name: `${c.definedFirstName.name} ${c.definedLastName.name}`,
title: c.nobleTitle.labelTr,
officeType: h.type.name,
gender: c.gender
});
}
});
return Array.from(map.values());
}
async getRegionAndParentIds(regionId) {
const relevantRegionIds = new Set();
let currentRegionId = regionId;
while (currentRegionId !== null) {
relevantRegionIds.add(currentRegionId);
const region = await RegionData.findByPk(currentRegionId, {
attributes: ['parentId']
});
if (region && region.parentId) {
currentRegionId = region.parentId;
} else {
currentRegionId = null; // Keine weitere Elternregion gefunden
}
}
return Array.from(relevantRegionIds);
}
// vorher: async searchUsers(q) {
async searchUsers(hashedUserId, q) {
// User-Prüfung wie bei anderen Methoden
await getFalukantUserOrFail(hashedUserId); // wir brauchen das Ergebnis hier nicht weiter, nur Validierung
const chars = await FalukantCharacter.findAll({
where: {
userId: { [Op.ne]: null },
[Op.or]: [
{ '$user.user.username$': { [Op.iLike]: `%${q}%` } },
{ '$definedFirstName.name$': { [Op.iLike]: `%${q}%` } },
{ '$definedLastName.name$': { [Op.iLike]: `%${q}%` } }
]
},
include: [
{
model: FalukantUser,
as: 'user',
include: [
{
model: User,
as: 'user',
attributes: ['username']
}
]
},
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name']
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name']
},
{
model: RegionData,
as: 'region',
attributes: ['name']
}
],
limit: 50,
raw: false
});
// Debug-Log (optional)
console.log('FalukantService.searchUsers raw result for', q, chars);
const mapped = chars
.map(c => ({
username: c.user?.user?.username || null,
firstname: c.definedFirstName?.name || null,
lastname: c.definedLastName?.name || null,
town: c.region?.name || null
}))
.filter(u => u.username);
console.log('FalukantService.searchUsers mapped result for', q, mapped);
return mapped;
}
async createUndergroundActivity(hashedUserId, payload) {
const { typeId, victimUsername, target, goal, politicalTargets } = payload;
// 1) Performer auflösen
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser || !falukantUser.character) {
throw new Error('Performer not found');
}
const performerChar = falukantUser.character;
// 2) Victim auflösen über Username (inner join)
const victimChar = await FalukantCharacter.findOne({
include: [
{
model: FalukantUser,
as: 'user',
required: true, // inner join
attributes: [],
include: [
{
model: User,
as: 'user',
required: true, // inner join
where: { username: victimUsername },
attributes: []
}
]
}
]
});
if (!victimChar) {
throw new PreconditionError('Victim character not found');
}
// 3) Selbstangriff verhindern
if (victimChar.id === performerChar.id) {
throw new PreconditionError('Cannot target yourself');
}
// 4) Typ-spezifische Validierung
const undergroundType = await UndergroundType.findByPk(typeId);
if (!undergroundType) {
throw new Error('Invalid underground type');
}
if (undergroundType.tr === 'sabotage') {
if (!target) {
throw new PreconditionError('Sabotage target missing');
}
}
if (undergroundType.tr === 'corrupt_politician') {
if (!goal) {
throw new PreconditionError('Corrupt goal missing');
}
// politicalTargets kann optional sein, falls benötigt prüfen
}
// 5) Eintrag anlegen (optional: in Transaction)
const newEntry = await Underground.create({
undergroundTypeId: typeId,
performerId: performerChar.id,
victimId: victimChar.id,
result: null,
parameters: {
target: target || null,
goal: goal || null,
politicalTargets: politicalTargets || null
}
});
return newEntry;
}
async getUndergroundAttacks(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: falukantUser.id }
});
if (!character) throw new Error('Character not found');
const charId = character.id;
const attacks = await Underground.findAll({
where: {
[Op.or]: [
{ performerId: charId },
{ victimId: charId }
]
},
include: [
{
model: FalukantCharacter,
as: 'performer',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{
model: FalukantUser,
as: 'user',
include: [{ model: User, as: 'user', attributes: ['username'] }],
attributes: []
}
],
attributes: ['id', 'gender']
},
{
model: FalukantCharacter,
as: 'victim',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{
model: FalukantUser,
as: 'user',
include: [{ model: User, as: 'user', attributes: ['username'] }],
attributes: []
}
],
attributes: ['id', 'gender']
},
{
model: UndergroundType,
as: 'undergroundType', // hier der korrekte Alias
attributes: ['tr']
}
],
order: [['createdAt', 'DESC']]
});
const formatCharacter = (c) => {
if (!c) return null;
return {
id: c.id,
username: c.user?.user?.username || null,
firstname: c.definedFirstName?.name || null,
lastname: c.definedLastName?.name || null,
gender: c.gender
};
};
const mapped = attacks.map(a => ({
id: a.id,
result: a.result,
createdAt: a.createdAt,
updatedAt: a.updatedAt,
type: a.undergroundType?.tr || null, // angepasst
parameters: a.parameters,
performer: formatCharacter(a.performer),
victim: formatCharacter(a.victim),
success: !!a.result
}));
return {
sent: mapped.filter(a => a.performer?.id === charId),
received: mapped.filter(a => a.victim?.id === charId),
all: mapped
};
}
}
export default new FalukantService();