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:
@@ -2,6 +2,7 @@ import AdminService from '../services/adminService.js';
|
||||
import Joi from 'joi';
|
||||
|
||||
class AdminController {
|
||||
// --- Chat Room Admin ---
|
||||
constructor() {
|
||||
this.getOpenInterests = this.getOpenInterests.bind(this);
|
||||
this.changeInterest = this.changeInterest.bind(this);
|
||||
@@ -9,6 +10,15 @@ class AdminController {
|
||||
this.changeTranslation = this.changeTranslation.bind(this);
|
||||
this.getOpenContacts = this.getOpenContacts.bind(this);
|
||||
this.answerContact = this.answerContact.bind(this);
|
||||
this.searchUser = this.searchUser.bind(this);
|
||||
this.getFalukantUserById = this.getFalukantUserById.bind(this);
|
||||
this.changeFalukantUser = this.changeFalukantUser.bind(this);
|
||||
this.getRoomTypes = this.getRoomTypes.bind(this);
|
||||
this.getGenderRestrictions = this.getGenderRestrictions.bind(this);
|
||||
this.getUserRights = this.getUserRights.bind(this);
|
||||
this.getRooms = this.getRooms.bind(this);
|
||||
this.createRoom = this.createRoom.bind(this);
|
||||
this.deleteRoom = this.deleteRoom.bind(this);
|
||||
}
|
||||
|
||||
async getOpenInterests(req, res) {
|
||||
@@ -126,6 +136,66 @@ class AdminController {
|
||||
res.status(403).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRoomTypes(req, res) {
|
||||
try {
|
||||
const types = await AdminService.getRoomTypes();
|
||||
res.status(200).json(types);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getGenderRestrictions(req, res) {
|
||||
try {
|
||||
const restrictions = await AdminService.getGenderRestrictions();
|
||||
res.status(200).json(restrictions);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRights(req, res) {
|
||||
try {
|
||||
const rights = await AdminService.getUserRights();
|
||||
res.status(200).json(rights);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRooms(req, res) {
|
||||
try {
|
||||
const rooms = await AdminService.getRooms();
|
||||
res.status(200).json(rooms);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async createRoom(req, res) {
|
||||
try {
|
||||
const room = await AdminService.createRoom(req.body);
|
||||
res.status(201).json(room);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRoom(req, res) {
|
||||
try {
|
||||
await AdminService.deleteRoom(req.params.id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminController;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -219,6 +219,10 @@ const menuStructure = {
|
||||
visible: ["mainadmin", "forum"],
|
||||
path: "/admin/forum"
|
||||
},
|
||||
chatrooms: {
|
||||
visible: ["mainadmin", "chatrooms"],
|
||||
path: "/admin/chatrooms"
|
||||
},
|
||||
userrights: {
|
||||
visible: ["mainadmin", "rights"],
|
||||
path: "/admin/rights"
|
||||
|
||||
@@ -3,3 +3,4 @@ import { cleanupExpiredSessions } from '../utils/redis.js';
|
||||
setInterval(async () => {
|
||||
await cleanupExpiredSessions();
|
||||
}, 5000);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import RoomType from './chat/room_type.js';
|
||||
import ChatRight from './chat/rights.js';
|
||||
import ChatUserRight from './chat/user_rights.js';
|
||||
import ChatUser from './chat/user.js';
|
||||
import Room from './chat/room.js';
|
||||
import User from './community/user.js';
|
||||
import UserParam from './community/user_param.js';
|
||||
import UserParamType from './type/user_param.js';
|
||||
@@ -92,6 +97,31 @@ import Underground from './falukant/data/underground.js';
|
||||
import UndergroundType from './falukant/type/underground.js';
|
||||
|
||||
export default function setupAssociations() {
|
||||
// RoomType 1:n Room
|
||||
RoomType.hasMany(Room, { foreignKey: 'roomTypeId', as: 'rooms' });
|
||||
Room.belongsTo(RoomType, { foreignKey: 'roomTypeId', as: 'roomType' });
|
||||
// ChatUser <-> ChatRight n:m
|
||||
ChatUser.belongsToMany(ChatRight, {
|
||||
through: ChatUserRight,
|
||||
foreignKey: 'chat_user_id',
|
||||
otherKey: 'chat_right_id',
|
||||
as: 'rights',
|
||||
});
|
||||
ChatRight.belongsToMany(ChatUser, {
|
||||
through: ChatUserRight,
|
||||
foreignKey: 'chat_right_id',
|
||||
otherKey: 'chat_user_id',
|
||||
as: 'users',
|
||||
});
|
||||
// ChatUser zu FalukantUser
|
||||
ChatUser.belongsTo(FalukantUser, { foreignKey: 'falukant_user_id', as: 'falukantUser' });
|
||||
FalukantUser.hasOne(ChatUser, { foreignKey: 'falukant_user_id', as: 'chatUser' });
|
||||
// Chat Room associations
|
||||
Room.belongsTo(User, { foreignKey: 'owner_id', as: 'owner' });
|
||||
User.hasMany(Room, { foreignKey: 'owner_id', as: 'ownedRooms' });
|
||||
|
||||
Room.belongsTo(UserParamValue, { foreignKey: 'gender_restriction_id', as: 'genderRestriction' });
|
||||
UserParamValue.hasMany(Room, { foreignKey: 'gender_restriction_id', as: 'roomsWithGenderRestriction' });
|
||||
// UserParam related associations
|
||||
SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' });
|
||||
UserParamType.belongsTo(SettingsType, { foreignKey: 'settingsId', as: 'settings_type' });
|
||||
|
||||
0
backend/models/chat/index.js
Normal file
0
backend/models/chat/index.js
Normal file
22
backend/models/chat/rights.js
Normal file
22
backend/models/chat/rights.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const ChatRight = sequelize.define('ChatRight', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
tr: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
}, {
|
||||
schema: 'chat',
|
||||
tableName: 'rights',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default ChatRight;
|
||||
69
backend/models/chat/room.js
Normal file
69
backend/models/chat/room.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const Room = sequelize.define('Room', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // kann null sein, wenn system-owned
|
||||
},
|
||||
roomTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
isPublic: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
genderRestrictionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
minAge: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
maxAge: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
passwordHash: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
friendsOfOwnerOnly: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
requiredUserRightId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
schema: 'chat',
|
||||
tableName: 'room',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_chat_room_owner',
|
||||
fields: ['owner_id'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default Room;
|
||||
22
backend/models/chat/room_type.js
Normal file
22
backend/models/chat/room_type.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const RoomType = sequelize.define('RoomType', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
tr: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
}, {
|
||||
schema: 'chat',
|
||||
tableName: 'room_type',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default RoomType;
|
||||
41
backend/models/chat/user.js
Normal file
41
backend/models/chat/user.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const ChatUser = sequelize.define('ChatUser', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
falukant_user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: 'Verknüpfung zu community.falukant_user',
|
||||
},
|
||||
display_name: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: false,
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING(16), // z.B. Hex-Code
|
||||
allowNull: false,
|
||||
defaultValue: '#000000',
|
||||
},
|
||||
show_gender: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
show_age: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
}, {
|
||||
schema: 'chat',
|
||||
tableName: 'user',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default ChatUser;
|
||||
26
backend/models/chat/user_rights.js
Normal file
26
backend/models/chat/user_rights.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import ChatUser from './user.js';
|
||||
import ChatRight from './rights.js';
|
||||
|
||||
const ChatUserRight = sequelize.define('ChatUserRight', {
|
||||
chatUserId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
references: null, // Assoziation wird separat gesetzt
|
||||
},
|
||||
chatRightId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
references: null, // Assoziation wird separat gesetzt
|
||||
},
|
||||
}, {
|
||||
schema: 'chat',
|
||||
tableName: 'user_rights',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default ChatUserRight;
|
||||
@@ -99,6 +99,12 @@ import ElectionHistory from './falukant/log/election_history.js';
|
||||
import UndergroundType from './falukant/type/underground.js';
|
||||
import Underground from './falukant/data/underground.js';
|
||||
|
||||
import Room from './chat/room.js';
|
||||
import ChatUser from './chat/user.js';
|
||||
import ChatRight from './chat/rights.js';
|
||||
import ChatUserRight from './chat/user_rights.js';
|
||||
import RoomType from './chat/room_type.js';
|
||||
|
||||
const models = {
|
||||
SettingsType,
|
||||
UserParamValue,
|
||||
@@ -197,6 +203,11 @@ const models = {
|
||||
ElectionHistory,
|
||||
UndergroundType,
|
||||
Underground,
|
||||
Room,
|
||||
ChatUser,
|
||||
ChatRight,
|
||||
ChatUserRight,
|
||||
RoomType,
|
||||
};
|
||||
|
||||
export default models;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import AdminController from '../controllers/adminController.js';
|
||||
@@ -5,6 +6,14 @@ import AdminController from '../controllers/adminController.js';
|
||||
const router = Router();
|
||||
const adminController = new AdminController();
|
||||
|
||||
// --- Chat Room Admin ---
|
||||
router.get('/chat/room-types', authenticate, adminController.getRoomTypes);
|
||||
router.get('/chat/gender-restrictions', authenticate, adminController.getGenderRestrictions);
|
||||
router.get('/chat/user-rights', authenticate, adminController.getUserRights);
|
||||
router.get('/chat/rooms', authenticate, adminController.getRooms);
|
||||
router.post('/chat/rooms', authenticate, adminController.createRoom);
|
||||
router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom);
|
||||
|
||||
router.get('/interests/open', authenticate, adminController.getOpenInterests);
|
||||
router.post('/interest', authenticate, adminController.changeInterest);
|
||||
router.post('/interest/translation', authenticate, adminController.changeTranslation);
|
||||
|
||||
@@ -41,7 +41,7 @@ router.get('/family/gifts', falukantController.getGifts);
|
||||
router.get('/family/children', falukantController.getChildren);
|
||||
router.post('/family/gift', falukantController.sendGift);
|
||||
router.get('/family', falukantController.getFamily);
|
||||
router.get('/nobility/titels', falukantController.getTitelsOfNobility);
|
||||
router.get('/nobility/titels', falukantController.getTitlesOfNobility);
|
||||
router.get('/houses/types', falukantController.getHouseTypes);
|
||||
router.get('/houses/buyable', falukantController.getBuyableHouses);
|
||||
router.get('/houses', falukantController.getUserHouse);
|
||||
@@ -71,5 +71,9 @@ router.post('/politics/open', falukantController.applyForElections);
|
||||
router.get('/cities', falukantController.getRegions);
|
||||
router.get('/underground/types', falukantController.getUndergroundTypes);
|
||||
router.get('/notifications', falukantController.getNotifications);
|
||||
router.get('/underground/targets', falukantController.getUndergroundTargets);
|
||||
router.post('/underground/activities', falukantController.createUndergroundActivity);
|
||||
router.get('/users/search', falukantController.searchUsers);
|
||||
router.get('/underground/attacks', falukantController.getUndergroundAttacks);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import RoomType from '../models/chat/room_type.js';
|
||||
import ChatRight from '../models/chat/rights.js';
|
||||
import UserRight from "../models/community/user_right.js";
|
||||
import UserRightType from "../models/type/user_right.js";
|
||||
import InterestType from "../models/type/interest.js"
|
||||
import InterestTranslationType from "../models/type/interest_translation.js"
|
||||
import InterestType from "../models/type/interest.js";
|
||||
import InterestTranslationType from "../models/type/interest_translation.js";
|
||||
import User from "../models/community/user.js";
|
||||
import UserParamValue from "../models/type/user_param_value.js";
|
||||
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';
|
||||
@@ -12,6 +15,7 @@ import FalukantUser from "../models/falukant/data/user.js";
|
||||
import FalukantCharacter from "../models/falukant/data/character.js";
|
||||
import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js";
|
||||
import FalukantPredefineLastname from "../models/falukant/predefine/lastname.js";
|
||||
import Room from '../models/chat/room.js';
|
||||
|
||||
class AdminService {
|
||||
async hasUserAccess(userId, section) {
|
||||
@@ -286,6 +290,39 @@ class AdminService {
|
||||
await falukantUser.save();
|
||||
await character.save();
|
||||
}
|
||||
|
||||
// --- Chat Room Admin ---
|
||||
async getRoomTypes() {
|
||||
return await RoomType.findAll();
|
||||
}
|
||||
|
||||
async getGenderRestrictions() {
|
||||
// Find the UserParamType for gender restriction (e.g. description = 'gender')
|
||||
const genderType = await UserParamType.findOne({ where: { description: 'gender' } });
|
||||
if (!genderType) return [];
|
||||
return await UserParamValue.findAll({ where: { userParamTypeId: genderType.id } });
|
||||
}
|
||||
|
||||
async getUserRights() {
|
||||
return await ChatRight.findAll();
|
||||
}
|
||||
|
||||
async getRooms() {
|
||||
return await Room.findAll({
|
||||
include: [
|
||||
{ model: RoomType, as: 'roomType' },
|
||||
{ model: UserParamValue, as: 'genderRestriction' },
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async createRoom(data) {
|
||||
return await Room.create(data);
|
||||
}
|
||||
|
||||
async deleteRoom(id) {
|
||||
return await Room.destroy({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
export default new AdminService();
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
43
backend/utils/initializeChat.js
Normal file
43
backend/utils/initializeChat.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import ChatRight from "../models/chat/rights.js";
|
||||
import RoomType from "../models/chat/room_type.js";
|
||||
|
||||
const initializeChat = async () => {
|
||||
initializeChatRights();
|
||||
initializeRoomTypes();
|
||||
}
|
||||
|
||||
const RoomTypes = [
|
||||
'chat',
|
||||
'dice',
|
||||
'poker',
|
||||
'hangman'
|
||||
];
|
||||
|
||||
const ChatRights = [
|
||||
'talk',
|
||||
'scream',
|
||||
'whisper',
|
||||
'start game',
|
||||
'open room',
|
||||
'systemmessage'
|
||||
];
|
||||
|
||||
const initializeChatRights = async () => {
|
||||
for (const right of ChatRights) {
|
||||
await ChatRight.findOrCreate({
|
||||
where: { tr: right },
|
||||
defaults: { tr: right }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const initializeRoomTypes = async () => {
|
||||
for (const type of RoomTypes) {
|
||||
await RoomType.findOrCreate({
|
||||
where: { tr: type },
|
||||
defaults: { tr: type }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default initializeChat;
|
||||
@@ -21,6 +21,7 @@ const createSchemas = async () => {
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_type');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_predefine');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_log');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS chat');
|
||||
};
|
||||
|
||||
const initializeDatabase = async () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import setupAssociations from '../models/associations.js';
|
||||
import models from '../models/index.js';
|
||||
import { createTriggers } from '../models/trigger.js';
|
||||
import initializeForum from './initializeForum.js';
|
||||
import initializeChat from './initializeChat.js';
|
||||
|
||||
const syncDatabase = async () => {
|
||||
try {
|
||||
@@ -43,6 +44,9 @@ const syncDatabase = async () => {
|
||||
console.log("Creating triggers...");
|
||||
await createTriggers();
|
||||
|
||||
console.log("Initializing chat...");
|
||||
await initializeChat();
|
||||
|
||||
console.log('Database synchronization complete.');
|
||||
} catch (error) {
|
||||
console.error('Unable to synchronize the database:', error);
|
||||
|
||||
191
frontend/src/dialogues/admin/RoomDialog.vue
Normal file
191
frontend/src/dialogues/admin/RoomDialog.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t(room && room.id ? 'admin.chatrooms.edit' : 'admin.chatrooms.create')"
|
||||
:show-close="true" :buttons="buttons" name="RoomDialog" :modal="true" :isTitleTranslated="true"
|
||||
@close="closeDialog">
|
||||
<form class="dialog-form" @submit.prevent="save">
|
||||
<label>
|
||||
{{ $t('admin.chatrooms.roomName') }}
|
||||
<input v-model="localRoom.title" required />
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('admin.chatrooms.type') }}
|
||||
<select v-model="localRoom.roomTypeId" required>
|
||||
<option v-for="type in roomTypes" :key="type.id" :value="type.id">{{ $t(`admin.chatrooms.roomtype.${type.tr}`) }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="localRoom.isPublic" />
|
||||
{{ $t('admin.chatrooms.isPublic') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showGenderRestriction" />
|
||||
{{ $t('admin.chatrooms.genderRestriction.show') }}
|
||||
</label>
|
||||
<label v-if="showGenderRestriction">
|
||||
{{ $t('admin.chatrooms.genderRestriction.label') }}
|
||||
<select v-model="localRoom.genderRestrictionId">
|
||||
<option v-for="g in genderRestrictions" :key="g.id" :value="g.id">{{ $t(`gender.${g.value}`) }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showMinAge" />
|
||||
{{ $t('admin.chatrooms.minAge.show') }}
|
||||
</label>
|
||||
<label v-if="showMinAge">
|
||||
{{ $t('admin.chatrooms.minAge.label') }}
|
||||
<input v-model.number="localRoom.minAge" type="number" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showMaxAge" />
|
||||
{{ $t('admin.chatrooms.maxAge.show') }}
|
||||
</label>
|
||||
<label v-if="showMaxAge">
|
||||
{{ $t('admin.chatrooms.maxAge.label') }}
|
||||
<input v-model.number="localRoom.maxAge" type="number" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showPassword" />
|
||||
{{ $t('admin.chatrooms.password.show') }}
|
||||
</label>
|
||||
<label v-if="showPassword">
|
||||
{{ $t('admin.chatrooms.password.label') }}
|
||||
<input v-model="localRoom.password" type="password" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="localRoom.friendsOfOwnerOnly" />
|
||||
{{ $t('admin.chatrooms.friendsOfOwnerOnly') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showRequiredUserRight" />
|
||||
{{ $t('admin.chatrooms.requiredUserRight.show') }}
|
||||
</label>
|
||||
<label v-if="showRequiredUserRight">
|
||||
{{ $t('admin.chatrooms.requiredUserRight.label') }}
|
||||
<select v-model="localRoom.requiredUserRightId">
|
||||
<option v-for="r in userRights" :key="r.id" :value="r.id">{{ $t(`admin.chatrooms.rights.${r.tr}`) }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import axios from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'RoomDialog',
|
||||
components: { DialogWidget },
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
room: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: null,
|
||||
localRoom: this.room ? { ...this.room } : { title: '', isPublic: true },
|
||||
roomTypes: [],
|
||||
genderRestrictions: [],
|
||||
userRights: [],
|
||||
showGenderRestriction: !!(this.room && this.room.genderRestrictionId),
|
||||
showMinAge: !!(this.room && this.room.minAge),
|
||||
showMaxAge: !!(this.room && this.room.maxAge),
|
||||
showPassword: !!(this.room && this.room.password),
|
||||
showRequiredUserRight: !!(this.room && this.room.requiredUserRightId),
|
||||
buttons: [
|
||||
{ text: 'Ok', action: () => this.save() },
|
||||
{ text: 'Cancel', action: () => this.closeDialog() }
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
room: {
|
||||
handler(newVal) {
|
||||
this.localRoom = newVal ? { ...newVal } : { title: '', isPublic: true };
|
||||
this.showGenderRestriction = !!(newVal && newVal.genderRestrictionId);
|
||||
this.showMinAge = !!(newVal && newVal.minAge);
|
||||
this.showMaxAge = !!(newVal && newVal.maxAge);
|
||||
this.showPassword = !!(newVal && newVal.password);
|
||||
this.showRequiredUserRight = !!(newVal && newVal.requiredUserRightId);
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.dialog = this.$refs.dialog;
|
||||
this.fetchRoomTypes();
|
||||
this.fetchGenderRestrictions();
|
||||
this.fetchUserRights();
|
||||
},
|
||||
methods: {
|
||||
async fetchRoomTypes() {
|
||||
const res = await axios.get('/api/admin/chat/room-types');
|
||||
this.roomTypes = res.data;
|
||||
},
|
||||
async fetchGenderRestrictions() {
|
||||
const res = await axios.get('/api/admin/chat/gender-restrictions');
|
||||
this.genderRestrictions = res.data;
|
||||
},
|
||||
async fetchUserRights() {
|
||||
const res = await axios.get('/api/admin/chat/user-rights');
|
||||
this.userRights = res.data;
|
||||
},
|
||||
async open(roomData) {
|
||||
await Promise.all([
|
||||
this.fetchRoomTypes(),
|
||||
this.fetchGenderRestrictions(),
|
||||
this.fetchUserRights()
|
||||
]);
|
||||
this.localRoom = roomData ? { ...roomData } : { title: '', isPublic: true };
|
||||
this.dialog.open();
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialog.close();
|
||||
},
|
||||
save() {
|
||||
this.$emit('save', this.localRoom);
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.room-dialog-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.dialog-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
.dialog-fields > * {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.save-btn, .cancel-btn {
|
||||
padding: 6px 18px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
.save-btn {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -48,6 +48,50 @@
|
||||
"success": "Die Änderungen wurden gespeichert.",
|
||||
"error": "Die Änderungen konnten nicht gespeichert werden."
|
||||
}
|
||||
},
|
||||
"chatrooms": {
|
||||
"title": "[Admin] - Chaträume verwalten",
|
||||
"roomName": "Raumname",
|
||||
"create": "Chatraum anlegen",
|
||||
"edit": "Chatraum bearbeiten",
|
||||
"type": "Typ",
|
||||
"isPublic": "Öffentlich sichtbar",
|
||||
"actions": "Aktionen",
|
||||
"genderRestriction": {
|
||||
"show": "Geschlechtsbeschränkung aktivieren",
|
||||
"label": "Geschlechtsbeschränkung"
|
||||
},
|
||||
"minAge": {
|
||||
"show": "Mindestalter angeben",
|
||||
"label": "Mindestalter"
|
||||
},
|
||||
"maxAge": {
|
||||
"show": "Höchstalter angeben",
|
||||
"label": "Höchstalter"
|
||||
},
|
||||
"password": {
|
||||
"show": "Passwortschutz aktivieren",
|
||||
"label": "Passwort"
|
||||
},
|
||||
"friendsOfOwnerOnly": "Nur Freunde des Besitzers",
|
||||
"requiredUserRight": {
|
||||
"show": "Benötigtes Benutzerrecht angeben",
|
||||
"label": "Benötigtes Benutzerrecht"
|
||||
},
|
||||
"roomtype": {
|
||||
"chat": "Reden",
|
||||
"dice": "Würfeln",
|
||||
"poker": "Poker",
|
||||
"hangman": "Hangman"
|
||||
},
|
||||
"rights": {
|
||||
"talk": "Reden",
|
||||
"scream": "Schreien",
|
||||
"whisper": "Flüstern",
|
||||
"start game": "Spiel starten",
|
||||
"open room": "Raum öffnen",
|
||||
"systemmessage": "Systemnachricht"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import AdminInterestsView from '../views/admin/InterestsView.vue';
|
||||
import AdminContactsView from '../views/admin/ContactsView.vue';
|
||||
import ChatRoomsView from '../views/admin/ChatRoomsView.vue';
|
||||
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
|
||||
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue'
|
||||
|
||||
@@ -22,6 +23,12 @@ const adminRoutes = [
|
||||
component: ForumAdminView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/chatrooms',
|
||||
name: 'AdminChatRooms',
|
||||
component: ChatRoomsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/falukant/edituser',
|
||||
name: 'AdminFalukantEditUserView',
|
||||
|
||||
109
frontend/src/views/admin/ChatRoomsView.vue
Normal file
109
frontend/src/views/admin/ChatRoomsView.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
<template>
|
||||
<div class="admin-chat-rooms">
|
||||
<h2>{{ $t('admin.chatrooms.title') }}</h2>
|
||||
<button class="create-btn" @click="openCreateDialog">{{ $t('admin.chatrooms.create') }}</button>
|
||||
<table class="rooms-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('admin.chatrooms.roomName') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.type') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.isPublic') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="room in rooms" :key="room.id">
|
||||
<td>{{ room.title }}</td>
|
||||
<td>{{ room.roomTypeTr || room.roomTypeId }}</td>
|
||||
<td>{{ room.isPublic ? $t('common.yes') : $t('common.no') }}</td>
|
||||
<td>
|
||||
<button @click="editRoom(room)">{{ $t('common.edit') }}</button>
|
||||
<button @click="deleteRoom(room)">{{ $t('common.delete') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<RoomDialog ref="roomDialog" :room="selectedRoom" @save="fetchRooms" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RoomDialog from '@/dialogues/admin/RoomDialog.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminChatRoomsView',
|
||||
components: { RoomDialog },
|
||||
data() {
|
||||
return {
|
||||
rooms: [],
|
||||
// dialog: false, // removed, handled by DialogWidget
|
||||
selectedRoom: null,
|
||||
// headers entfernt, da eigene Tabelle
|
||||
// roomTypeTr sollte beim Laden der Räume mitgeliefert werden (API/Backend)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchRooms();
|
||||
},
|
||||
methods: {
|
||||
openCreateDialog() {
|
||||
this.selectedRoom = null;
|
||||
this.$refs.roomDialog.open();
|
||||
},
|
||||
editRoom(room) {
|
||||
this.selectedRoom = { ...room };
|
||||
this.$refs.roomDialog.open(this.selectedRoom);
|
||||
},
|
||||
async deleteRoom(room) {
|
||||
if (!room.id) return;
|
||||
await apiClient.delete(`/api/admin/chat/rooms/${room.id}`);
|
||||
this.fetchRooms();
|
||||
},
|
||||
async fetchRooms() {
|
||||
const res = await apiClient.get('/api/admin/chat/rooms');
|
||||
this.rooms = res.data;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-chat-rooms {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.create-btn {
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 18px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rooms-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.rooms-table th, .rooms-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
.rooms-table th {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.rooms-table td button {
|
||||
margin-right: 6px;
|
||||
padding: 4px 12px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rooms-table td button:hover {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
175
frontend/src/views/admin/RoomDialog.vue
Normal file
175
frontend/src/views/admin/RoomDialog.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t(room && room.id ? 'admin.chatrooms.edit' : 'admin.chatrooms.create')"
|
||||
:show-close="true" :buttons="buttons" name="RoomDialog" :modal="true" :isTitleTranslated="true"
|
||||
@close="closeDialog">
|
||||
<form class="dialog-form" @submit.prevent="save">
|
||||
<label>
|
||||
{{ $t('admin.chatrooms.title') }}
|
||||
<input v-model="localRoom.title" required />
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('admin.chatrooms.type') }}
|
||||
<select v-model="localRoom.roomTypeId" required>
|
||||
<option v-for="type in roomTypes" :key="type.id" :value="type.id">{{ type.tr }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="localRoom.isPublic" />
|
||||
{{ $t('admin.chatrooms.isPublic') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showGenderRestriction" />
|
||||
{{ $t('admin.chatrooms.genderRestriction.show') }}
|
||||
</label>
|
||||
<label v-if="showGenderRestriction">
|
||||
{{ $t('admin.chatrooms.genderRestriction.label') }}
|
||||
<select v-model="localRoom.genderRestrictionId">
|
||||
<option v-for="g in genderRestrictions" :key="g.id" :value="g.id">{{ g.tr }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showMinAge" />
|
||||
{{ $t('admin.chatrooms.minAge.show') }}
|
||||
</label>
|
||||
<label v-if="showMinAge">
|
||||
{{ $t('admin.chatrooms.minAge.label') }}
|
||||
<input v-model.number="localRoom.minAge" type="number" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showMaxAge" />
|
||||
{{ $t('admin.chatrooms.maxAge.show') }}
|
||||
</label>
|
||||
<label v-if="showMaxAge">
|
||||
{{ $t('admin.chatrooms.maxAge.label') }}
|
||||
<input v-model.number="localRoom.maxAge" type="number" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showPassword" />
|
||||
{{ $t('admin.chatrooms.password.show') }}
|
||||
</label>
|
||||
<label v-if="showPassword">
|
||||
{{ $t('admin.chatrooms.password.label') }}
|
||||
<input v-model="localRoom.password" type="password" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="localRoom.friendsOfOwnerOnly" />
|
||||
{{ $t('admin.chatrooms.friendsOfOwnerOnly') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showRequiredUserRight" />
|
||||
{{ $t('admin.chatrooms.requiredUserRight.show') }}
|
||||
</label>
|
||||
<label v-if="showRequiredUserRight">
|
||||
{{ $t('admin.chatrooms.requiredUserRight.label') }}
|
||||
<select v-model="localRoom.requiredUserRightId">
|
||||
<option v-for="r in userRights" :key="r.id" :value="r.id">{{ r.tr }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
|
||||
export default {
|
||||
name: 'RoomDialog',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
room: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: null,
|
||||
localRoom: this.room ? { ...this.room } : { title: '', isPublic: true },
|
||||
roomTypes: [],
|
||||
genderRestrictions: [],
|
||||
userRights: [],
|
||||
showGenderRestriction: !!(this.room && this.room.genderRestrictionId),
|
||||
showMinAge: !!(this.room && this.room.minAge),
|
||||
showMaxAge: !!(this.room && this.room.maxAge),
|
||||
showPassword: !!(this.room && this.room.password),
|
||||
showRequiredUserRight: !!(this.room && this.room.requiredUserRightId),
|
||||
buttons: [
|
||||
{ text: 'common.save', action: this.save },
|
||||
{ text: 'common.cancel', action: this.closeDialog }
|
||||
]
|
||||
},
|
||||
watch: {
|
||||
room: {
|
||||
handler(newVal) {
|
||||
this.localRoom = newVal ? { ...newVal } : { title: '', isPublic: true };
|
||||
this.showGenderRestriction = !!(newVal && newVal.genderRestrictionId);
|
||||
this.showMinAge = !!(newVal && newVal.minAge);
|
||||
this.showMaxAge = !!(newVal && newVal.maxAge);
|
||||
this.showPassword = !!(newVal && newVal.password);
|
||||
this.showRequiredUserRight = !!(newVal && newVal.requiredUserRightId);
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.dialog = this.$refs.dialog;
|
||||
this.fetchRoomTypes();
|
||||
this.fetchGenderRestrictions();
|
||||
this.fetchUserRights();
|
||||
},
|
||||
methods: {
|
||||
async fetchRoomTypes() {
|
||||
// API-Call, z.B. this.roomTypes = await apiClient.get('/api/room-types')
|
||||
},
|
||||
async fetchGenderRestrictions() {
|
||||
// API-Call, z.B. this.genderRestrictions = await apiClient.get('/api/gender-restrictions')
|
||||
},
|
||||
async fetchUserRights() {
|
||||
// API-Call, z.B. this.userRights = await apiClient.get('/api/user-rights')
|
||||
},
|
||||
open(roomData) {
|
||||
this.localRoom = roomData ? { ...roomData } : { title: '', isPublic: true };
|
||||
this.dialog.open();
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialog.close();
|
||||
},
|
||||
save() {
|
||||
this.$emit('save', this.localRoom);
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.room-dialog-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.dialog-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
.dialog-fields > * {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.save-btn, .cancel-btn {
|
||||
padding: 6px 18px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
.save-btn {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class="create-activity">
|
||||
<h3>{{ $t('falukant.underground.activities.create') }}</h3>
|
||||
|
||||
<!-- Typ -->
|
||||
<label class="form-label">
|
||||
{{ $t('falukant.underground.activities.type') }}
|
||||
<select v-model="newActivityTypeId" class="form-control">
|
||||
@@ -23,18 +24,43 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<!-- Victim mit Autocomplete -->
|
||||
<label class="form-label">
|
||||
{{ $t('falukant.underground.activities.victim') }}
|
||||
<input v-model="newVictimUsername" type="text" class="form-control"
|
||||
<input v-model="newVictimUsername" @input="onVictimInput" type="text" class="form-control"
|
||||
:placeholder="$t('falukant.underground.activities.victimPlaceholder')" />
|
||||
</label>
|
||||
<div v-if="victimSuggestions.length" class="suggestions">
|
||||
<ul>
|
||||
<li v-for="s in victimSuggestions" :key="s.username" @click="selectVictim(s)">
|
||||
{{ s.username }} — {{ s.firstname }} {{ s.lastname }}
|
||||
({{ s.town }})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Politische Zielpersonen (Multiselect) -->
|
||||
<label v-if="selectedType && selectedType.tr === 'corrupt_politician'" class="form-label">
|
||||
{{ $t('falukant.underground.activities.targets') }}
|
||||
<select v-model="newPoliticalTargets" multiple size="5" class="form-control">
|
||||
<option v-for="p in politicalTargets" :key="p.id" :value="p.id">
|
||||
{{ $t('falukant.titles.' + p.gender + '.' + p.title) }}
|
||||
{{ p.name }}
|
||||
({{ $t('falukant.politics.offices.' + p.officeType) }})
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<!-- Bei sabotage: Ziel auswählen -->
|
||||
<label v-if="selectedType && selectedType.tr === 'sabotage'" class="form-label">
|
||||
{{ $t('falukant.underground.activities.sabotageTarget') }}
|
||||
<select v-model="newSabotageTarget" class="form-control">
|
||||
<option value="house">{{ $t('falukant.underground.targets.house') }}</option>
|
||||
<option value="storage">{{ $t('falukant.underground.targets.storage') }}</option>
|
||||
<option value="house">
|
||||
{{ $t('falukant.underground.targets.house') }}
|
||||
</option>
|
||||
<option value="storage">
|
||||
{{ $t('falukant.underground.targets.storage') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -42,9 +68,15 @@
|
||||
<label v-if="selectedType && selectedType.tr === 'corrupt_politician'" class="form-label">
|
||||
{{ $t('falukant.underground.activities.corruptGoal') }}
|
||||
<select v-model="newCorruptGoal" class="form-control">
|
||||
<option value="elect">{{ $t('falukant.underground.goals.elect') }}</option>
|
||||
<option value="tax_increase">{{ $t('falukant.underground.goals.taxIncrease') }}</option>
|
||||
<option value="tax_decrease">{{ $t('falukant.underground.goals.taxDecrease') }}</option>
|
||||
<option value="elect">
|
||||
{{ $t('falukant.underground.goals.elect') }}
|
||||
</option>
|
||||
<option value="tax_increase">
|
||||
{{ $t('falukant.underground.goals.taxIncrease') }}
|
||||
</option>
|
||||
<option value="tax_decrease">
|
||||
{{ $t('falukant.underground.goals.taxDecrease') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -69,13 +101,9 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="act in activities" :key="act.id">
|
||||
<!-- Typ -->
|
||||
<td>{{ $t(`falukant.underground.types.${act.type}`) }}</td>
|
||||
<!-- Victim -->
|
||||
<td>{{ act.victimName }}</td>
|
||||
<!-- Cost -->
|
||||
<td>{{ formatCost(act.cost) }}</td>
|
||||
<!-- Zusätzliche Informationen -->
|
||||
<td>
|
||||
<template v-if="act.type === 'sabotage'">
|
||||
{{ $t(`falukant.underground.targets.${act.target}`) }}
|
||||
@@ -145,22 +173,35 @@ export default {
|
||||
activities: [],
|
||||
attacks: [],
|
||||
loading: { activities: false, attacks: false },
|
||||
|
||||
// Neue Activity-Formfelder
|
||||
newActivityTypeId: null,
|
||||
newVictimUsername: '',
|
||||
victimSuggestions: [],
|
||||
victimSearchTimeout: null,
|
||||
newPoliticalTargets: [],
|
||||
newSabotageTarget: 'house',
|
||||
newCorruptGoal: 'elect'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedType() {
|
||||
return this.undergroundTypes.find(t => t.id === this.newActivityTypeId) || null;
|
||||
return this.undergroundTypes.find(
|
||||
t => t.id === this.newActivityTypeId
|
||||
) || null;
|
||||
},
|
||||
canCreate() {
|
||||
if (!this.newActivityTypeId || !this.newVictimUsername.trim()) return false;
|
||||
if (this.selectedType.tr === 'sabotage' && !this.newSabotageTarget) return false;
|
||||
if (this.selectedType.tr === 'corrupt_politician' && !this.newCorruptGoal) return false;
|
||||
if (!this.newActivityTypeId) return false;
|
||||
const hasUser = this.newVictimUsername.trim().length > 0;
|
||||
const hasPol = this.newPoliticalTargets.length > 0;
|
||||
if (!hasUser && !hasPol) return false;
|
||||
if (this.selectedType?.tr === 'sabotage' && !this.newSabotageTarget) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selectedType?.tr === 'corrupt_politician' &&
|
||||
!this.newCorruptGoal
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
@@ -181,34 +222,32 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async loadUndergroundTypes() {
|
||||
const { data } = await apiClient.get('/api/falukant/underground/types');
|
||||
this.undergroundTypes = data;
|
||||
},
|
||||
|
||||
async loadActivities() {
|
||||
this.loading.activities = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/underground/activities');
|
||||
this.activities = data;
|
||||
} catch (err) {
|
||||
console.error('Error loading activities', err);
|
||||
} finally {
|
||||
this.loading.activities = false;
|
||||
onVictimInput() {
|
||||
clearTimeout(this.victimSearchTimeout);
|
||||
const q = this.newVictimUsername.trim();
|
||||
if (q.length >= 3) {
|
||||
this.victimSearchTimeout = setTimeout(() => {
|
||||
this.searchVictims(q);
|
||||
}, 300);
|
||||
} else {
|
||||
this.victimSuggestions = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadAttacks() {
|
||||
this.loading.attacks = true;
|
||||
async searchVictims(q) {
|
||||
console.log('Searching victims for:', q);
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/underground/attacks');
|
||||
this.attacks = data;
|
||||
const { data } = await apiClient.get('/api/falukant/users/search', {
|
||||
params: { q }
|
||||
});
|
||||
this.victimSuggestions = data;
|
||||
} catch (err) {
|
||||
console.error('Error loading attacks', err);
|
||||
} finally {
|
||||
this.loading.attacks = false;
|
||||
console.error('Error searching users', err);
|
||||
}
|
||||
},
|
||||
selectVictim(u) {
|
||||
this.newVictimUsername = u.username;
|
||||
this.victimSuggestions = [];
|
||||
},
|
||||
|
||||
async createActivity() {
|
||||
if (!this.canCreate) return;
|
||||
@@ -216,17 +255,22 @@ export default {
|
||||
typeId: this.newActivityTypeId,
|
||||
victimUsername: this.newVictimUsername.trim()
|
||||
};
|
||||
// je nach Typ noch ergänzen:
|
||||
if (this.selectedType.tr === 'sabotage') {
|
||||
payload.target = this.newSabotageTarget;
|
||||
}
|
||||
if (this.selectedType.tr === 'corrupt_politician') {
|
||||
payload.goal = this.newCorruptGoal;
|
||||
if (this.newPoliticalTargets.length) {
|
||||
payload.politicalTargets = this.newPoliticalTargets;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await apiClient.post('/api/falukant/underground/activities', payload);
|
||||
// zurücksetzen & neu laden
|
||||
await apiClient.post(
|
||||
'/api/falukant/underground/activities',
|
||||
payload
|
||||
);
|
||||
this.newVictimUsername = '';
|
||||
this.newPoliticalTargets = [];
|
||||
this.newSabotageTarget = 'house';
|
||||
this.newCorruptGoal = 'elect';
|
||||
await this.loadActivities();
|
||||
@@ -235,6 +279,38 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async loadUndergroundTypes() {
|
||||
const { data } = await apiClient.get(
|
||||
'/api/falukant/underground/types'
|
||||
);
|
||||
this.undergroundTypes = data;
|
||||
},
|
||||
|
||||
async loadActivities() {
|
||||
return;
|
||||
this.loading.activities = true;
|
||||
try {
|
||||
const { data } = await apiClient.get(
|
||||
'/api/falukant/underground/activities'
|
||||
);
|
||||
this.activities = data;
|
||||
} finally {
|
||||
this.loading.activities = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadAttacks() {
|
||||
this.loading.attacks = true;
|
||||
try {
|
||||
const { data } = await apiClient.get(
|
||||
'/api/falukant/underground/attacks'
|
||||
);
|
||||
this.attacks = data;
|
||||
} finally {
|
||||
this.loading.attacks = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(ts) {
|
||||
return new Date(ts).toLocaleDateString(this.$i18n.locale, {
|
||||
year: 'numeric',
|
||||
@@ -245,26 +321,26 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
formatCost(value) {
|
||||
formatCost(v) {
|
||||
return new Intl.NumberFormat(navigator.language, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
}).format(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.underground-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -279,18 +355,12 @@ h2 {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* --- Create Activity --- */
|
||||
.create-activity {
|
||||
border: 1px solid #ccc;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.create-activity h3 {
|
||||
margin-top: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@@ -310,11 +380,11 @@ h2 {
|
||||
|
||||
.btn-create-activity {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-create-activity:disabled {
|
||||
@@ -322,22 +392,42 @@ h2 {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Activities List --- */
|
||||
.activities-list ul {
|
||||
list-style: disc;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
/* --- Attacks Table --- */
|
||||
.activities-table table,
|
||||
.attacks-list table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.activities-table th,
|
||||
.activities-table td,
|
||||
.attacks-list th,
|
||||
.attacks-list td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
z-index: 10;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestions ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.suggestions li {
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggestions li:hover {
|
||||
background: #eee;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user