Add room creation options endpoint and integrate with chat UI: Implement getRoomCreateOptions in ChatController and ChatService, add corresponding API route, and enhance MultiChatDialog for room creation with localized labels and validation. Update i18n files for new room creation features.

This commit is contained in:
Torsten Schulz (local)
2026-03-04 23:12:54 +01:00
parent 5f4acbea51
commit 2bc34acacf
8 changed files with 251 additions and 35 deletions

View File

@@ -13,6 +13,7 @@ class ChatController {
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this); this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this); this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
this.getRoomList = this.getRoomList.bind(this); this.getRoomList = this.getRoomList.bind(this);
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
} }
async getMessages(req, res) { async getMessages(req, res) {
@@ -175,6 +176,15 @@ class ChatController {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
} }
async getRoomCreateOptions(req, res) {
try {
const options = await chatService.getRoomCreateOptions();
res.status(200).json(options);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
} }
export default ChatController; export default ChatController;

View File

@@ -15,5 +15,6 @@ router.post('/initOneToOne', authenticate, chatController.initOneToOne);
router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht
router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte
router.get('/rooms', chatController.getRoomList); router.get('/rooms', chatController.getRoomList);
router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions);
export default router; export default router;

View File

@@ -148,6 +148,27 @@ class ChatService {
] ]
}); });
} }
async getRoomCreateOptions() {
const { default: UserRightType } = await import('../models/type/user_right.js');
const { default: InterestType } = await import('../models/type/interest.js');
const [rights, interests] = await Promise.all([
UserRightType.findAll({
attributes: ['id', 'title'],
order: [['id', 'ASC']]
}),
InterestType.findAll({
attributes: ['id', 'name'],
order: [['id', 'ASC']]
})
]);
return {
rights: rights.map((r) => ({ id: r.id, title: r.title })),
roomTypes: interests.map((i) => ({ id: i.id, name: i.name }))
};
}
} }
export default new ChatService(); export default new ChatService();

View File

@@ -4,3 +4,8 @@ export const fetchPublicRooms = async () => {
const response = await apiClient.get("/api/chat/rooms"); const response = await apiClient.get("/api/chat/rooms");
return response.data; // expecting array of { id, title, ... } return response.data; // expecting array of { id, title, ... }
}; };
export const fetchRoomCreateOptions = async () => {
const response = await apiClient.get("/api/chat/room-create-options");
return response.data;
};

View File

@@ -8,7 +8,7 @@
<option v-for="room in rooms" :key="room.id" :value="room.id">{{ room.title }}</option> <option v-for="room in rooms" :key="room.id" :value="room.id">{{ room.title }}</option>
</select> </select>
<button class="create-room-toggle-btn" type="button" @click="toggleRoomCreatePanel"> <button class="create-room-toggle-btn" type="button" @click="toggleRoomCreatePanel">
{{ showRoomCreatePanel ? 'Chat anzeigen' : 'Raum anlegen' }} {{ showRoomCreatePanel ? $t('chat.multichat.createRoom.toggleShowChat') : $t('chat.multichat.createRoom.toggleCreateRoom') }}
</button> </button>
</div> </div>
<div class="right-controls"> <div class="right-controls">
@@ -29,7 +29,7 @@
</label> </label>
<div class="opts-divider" v-if="isAdmin"></div> <div class="opts-divider" v-if="isAdmin"></div>
<div class="opts-row" v-if="isAdmin"> <div class="opts-row" v-if="isAdmin">
<button class="opts-btn" type="button" @click="reloadRoomsAdmin">Räume neu laden</button> <button class="opts-btn" type="button" @click="reloadRoomsAdmin">{{ $t('chat.multichat.reloadRooms') }}</button>
</div> </div>
</div> </div>
</div> </div>
@@ -59,73 +59,81 @@
</div> </div>
</div> </div>
<div v-else class="room-create-panel"> <div v-else class="room-create-panel">
<div class="room-create-title">Neuen Raum erstellen</div> <div class="room-create-title">{{ $t('chat.multichat.createRoom.title') }}</div>
<div class="room-create-grid"> <div class="room-create-grid">
<label> <label>
Raumname * {{ $t('chat.multichat.createRoom.labels.roomName') }} *
<input v-model.trim="roomCreateForm.roomName" type="text" placeholder="z. B. Lounge" <input v-model.trim="roomCreateForm.roomName" type="text" :placeholder="$t('chat.multichat.createRoom.placeholders.roomName')"
:class="{ 'invalid-input': roomCreateValidation.roomName }" /> :class="{ 'invalid-input': roomCreateValidation.roomName }" />
<span v-if="roomCreateValidation.roomName" class="room-create-error">{{ roomCreateValidation.roomName }}</span> <span v-if="roomCreateValidation.roomName" class="room-create-error">{{ roomCreateValidation.roomName }}</span>
</label> </label>
<label> <label>
Sichtbarkeit {{ $t('chat.multichat.createRoom.labels.visibility') }}
<select v-model="roomCreateForm.visibility"> <select v-model="roomCreateForm.visibility">
<option value="">(keine)</option> <option value="">{{ $t('chat.multichat.createRoom.options.none') }}</option>
<option value="public">public</option> <option value="public">public</option>
<option value="private">private</option> <option value="private">private</option>
</select> </select>
</label> </label>
<label> <label>
gender {{ $t('chat.multichat.createRoom.labels.gender') }}
<select v-model="roomCreateForm.gender"> <select v-model="roomCreateForm.gender">
<option value="">(keine)</option> <option value="">{{ $t('chat.multichat.createRoom.options.none') }}</option>
<option value="m">m</option> <option value="m">m</option>
<option value="f">f</option> <option value="f">f</option>
<option value="any">any</option> <option value="any">any</option>
</select> </select>
</label> </label>
<label> <label>
min_age {{ $t('chat.multichat.createRoom.labels.minAge') }}
<input v-model.number="roomCreateForm.minAge" type="number" min="0" <input v-model.number="roomCreateForm.minAge" type="number" min="0"
:class="{ 'invalid-input': roomCreateValidation.minAge }" /> :class="{ 'invalid-input': roomCreateValidation.minAge }" />
<span v-if="roomCreateValidation.minAge" class="room-create-error">{{ roomCreateValidation.minAge }}</span> <span v-if="roomCreateValidation.minAge" class="room-create-error">{{ roomCreateValidation.minAge }}</span>
</label> </label>
<label> <label>
max_age {{ $t('chat.multichat.createRoom.labels.maxAge') }}
<input v-model.number="roomCreateForm.maxAge" type="number" min="0" <input v-model.number="roomCreateForm.maxAge" type="number" min="0"
:class="{ 'invalid-input': roomCreateValidation.maxAge }" /> :class="{ 'invalid-input': roomCreateValidation.maxAge }" />
<span v-if="roomCreateValidation.maxAge" class="room-create-error">{{ roomCreateValidation.maxAge }}</span> <span v-if="roomCreateValidation.maxAge" class="room-create-error">{{ roomCreateValidation.maxAge }}</span>
</label> </label>
<label> <label>
Passwort {{ $t('chat.multichat.createRoom.labels.password') }}
<input v-model.trim="roomCreateForm.password" type="text" placeholder="ohne Leerzeichen" <input v-model.trim="roomCreateForm.password" type="text" :placeholder="$t('chat.multichat.createRoom.placeholders.password')"
:class="{ 'invalid-input': roomCreateValidation.password }" /> :class="{ 'invalid-input': roomCreateValidation.password }" />
<span v-if="roomCreateValidation.password" class="room-create-error">{{ roomCreateValidation.password }}</span> <span v-if="roomCreateValidation.password" class="room-create-error">{{ roomCreateValidation.password }}</span>
</label> </label>
<label> <label>
right_id {{ $t('chat.multichat.createRoom.labels.rightId') }}
<input v-model.number="roomCreateForm.rightId" type="number" min="1" <select v-model="roomCreateForm.rightId" :class="{ 'invalid-input': roomCreateValidation.rightId }">
:class="{ 'invalid-input': roomCreateValidation.rightId }" /> <option :value="null">{{ $t('chat.multichat.createRoom.options.none') }}</option>
<option v-for="right in roomCreateRights" :key="`right-${right.id}`" :value="right.id">
{{ getUserRightLabel(right) }}
</option>
</select>
<span v-if="roomCreateValidation.rightId" class="room-create-error">{{ roomCreateValidation.rightId }}</span> <span v-if="roomCreateValidation.rightId" class="room-create-error">{{ roomCreateValidation.rightId }}</span>
</label> </label>
<label> <label>
type_id {{ $t('chat.multichat.createRoom.labels.typeId') }}
<input v-model.number="roomCreateForm.typeId" type="number" min="1" <select v-model="roomCreateForm.typeId" :class="{ 'invalid-input': roomCreateValidation.typeId }">
:class="{ 'invalid-input': roomCreateValidation.typeId }" /> <option :value="null">{{ $t('chat.multichat.createRoom.options.none') }}</option>
<option v-for="roomType in roomCreateTypes" :key="`type-${roomType.id}`" :value="roomType.id">
{{ getRoomTypeLabel(roomType) }}
</option>
</select>
<span v-if="roomCreateValidation.typeId" class="room-create-error">{{ roomCreateValidation.typeId }}</span> <span v-if="roomCreateValidation.typeId" class="room-create-error">{{ roomCreateValidation.typeId }}</span>
</label> </label>
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" v-model="roomCreateForm.friendsOnly" /> <input type="checkbox" v-model="roomCreateForm.friendsOnly" />
friends_only=true {{ $t('chat.multichat.createRoom.labels.friendsOnly') }}
</label> </label>
</div> </div>
<div class="room-create-actions"> <div class="room-create-actions">
<button type="button" class="send-btn" @click="sendCreateRoomCommand" :disabled="!canSendRoomCreate">Raum erstellen</button> <button type="button" class="send-btn" @click="sendCreateRoomCommand" :disabled="!canSendRoomCreate">{{ $t('chat.multichat.createRoom.actions.create') }}</button>
<button type="button" class="create-room-reset-btn" @click="resetRoomCreateForm">Zurücksetzen</button> <button type="button" class="create-room-reset-btn" @click="resetRoomCreateForm">{{ $t('chat.multichat.createRoom.actions.reset') }}</button>
</div> </div>
<div v-if="roomCreateValidation.range" class="room-create-error room-create-error-block">{{ roomCreateValidation.range }}</div> <div v-if="roomCreateValidation.range" class="room-create-error room-create-error-block">{{ roomCreateValidation.range }}</div>
<div class="room-create-preview"> <div class="room-create-preview">
Kommando: <code>{{ buildRoomCreateCommandPreview() || '/cr <raumname>' }}</code> {{ $t('chat.multichat.createRoom.commandPrefix') }}: <code>{{ buildRoomCreateCommandPreview() || '/cr <raumname>' }}</code>
</div> </div>
</div> </div>
<div class="user-list"> <div class="user-list">
@@ -191,7 +199,7 @@
<script> <script>
import DialogWidget from '@/components/DialogWidget.vue'; import DialogWidget from '@/components/DialogWidget.vue';
import { fetchPublicRooms } from '@/api/chatApi.js'; import { fetchPublicRooms, fetchRoomCreateOptions } from '@/api/chatApi.js';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js'; import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
@@ -215,15 +223,15 @@ export default {
const typeId = this.parseOptionalInteger(this.roomCreateForm.typeId); const typeId = this.parseOptionalInteger(this.roomCreateForm.typeId);
const password = this.roomCreateForm.password || ''; const password = this.roomCreateForm.password || '';
if (!name) errors.roomName = 'Raumname ist erforderlich.'; if (!name) errors.roomName = this.$t('chat.multichat.createRoom.validation.roomNameRequired');
if (minAge !== null && minAge < 0) errors.minAge = 'min_age muss >= 0 sein.'; if (minAge !== null && minAge < 0) errors.minAge = this.$t('chat.multichat.createRoom.validation.minAgeInvalid');
if (maxAge !== null && maxAge < 0) errors.maxAge = 'max_age muss >= 0 sein.'; if (maxAge !== null && maxAge < 0) errors.maxAge = this.$t('chat.multichat.createRoom.validation.maxAgeInvalid');
if (minAge !== null && maxAge !== null && minAge > maxAge) { if (minAge !== null && maxAge !== null && minAge > maxAge) {
errors.range = 'min_age darf nicht größer als max_age sein.'; errors.range = this.$t('chat.multichat.createRoom.validation.ageRangeInvalid');
} }
if (password.includes(' ')) errors.password = 'Passwort darf keine Leerzeichen enthalten.'; if (password.includes(' ')) errors.password = this.$t('chat.multichat.createRoom.validation.passwordSpaces');
if (rightId !== null && rightId <= 0) errors.rightId = 'right_id muss > 0 sein.'; if (rightId !== null && rightId <= 0) errors.rightId = this.$t('chat.multichat.createRoom.validation.rightIdInvalid');
if (typeId !== null && typeId <= 0) errors.typeId = 'type_id muss > 0 sein.'; if (typeId !== null && typeId <= 0) errors.typeId = this.$t('chat.multichat.createRoom.validation.typeIdInvalid');
return errors; return errors;
}, },
@@ -283,6 +291,8 @@ export default {
rightId: null, rightId: null,
typeId: null typeId: null
}, },
roomCreateRights: [],
roomCreateTypes: [],
// Palette state // Palette state
paletteWidth: 420, paletteWidth: 420,
paletteHeight: 220, paletteHeight: 220,
@@ -359,6 +369,36 @@ export default {
buildRoomCreateCommandPreview() { buildRoomCreateCommandPreview() {
return this.buildRoomCreateCommand(); return this.buildRoomCreateCommand();
}, },
getUserRightLabel(right) {
const raw = (right?.title || '').trim();
if (!raw) return right?.id ?? '';
const candidates = [
`chat.multichat.createRoom.rights.${raw}`,
`navigation.${raw}`,
`navigation.m-admin.${raw}`
];
for (const key of candidates) {
if (this.$te(key)) return this.$t(key);
}
return raw;
},
getRoomTypeLabel(roomType) {
const raw = (roomType?.name || '').trim();
if (!raw) return roomType?.id ?? '';
const key = `chat.multichat.createRoom.types.${raw}`;
return this.$te(key) ? this.$t(key) : raw;
},
async loadRoomCreateOptions() {
try {
const options = await fetchRoomCreateOptions();
this.roomCreateRights = Array.isArray(options?.rights) ? options.rights : [];
this.roomCreateTypes = Array.isArray(options?.roomTypes) ? options.roomTypes : [];
} catch (e) {
console.error('Failed loading room create options', e);
this.roomCreateRights = [];
this.roomCreateTypes = [];
}
},
parseOptionalInteger(value) { parseOptionalInteger(value) {
if (value === null || value === undefined || value === '') return null; if (value === null || value === undefined || value === '') return null;
const num = Number(value); const num = Number(value);
@@ -406,22 +446,22 @@ export default {
}, },
sendCreateRoomCommand() { sendCreateRoomCommand() {
if (!this.transportConnected) { if (!this.transportConnected) {
this.messages.push({ id: Date.now(), user: 'System', text: 'Keine Verbindung zum Chat-Server.' }); this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.noConnection') });
return; return;
} }
if (!this.canSendRoomCreate) { if (!this.canSendRoomCreate) {
this.messages.push({ id: Date.now(), user: 'System', text: 'Bitte Eingaben im Raum-Formular korrigieren.' }); this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.invalidForm') });
return; return;
} }
const command = this.buildRoomCreateCommand(); const command = this.buildRoomCreateCommand();
if (!command) { if (!command) {
this.messages.push({ id: Date.now(), user: 'System', text: 'Bitte einen Raumnamen angeben.' }); this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.roomNameMissing') });
return; return;
} }
const payload = { type: 'message', message: command }; const payload = { type: 'message', message: command };
if (this.debug) console.log('[Chat WS >>]', payload); if (this.debug) console.log('[Chat WS >>]', payload);
this.sendWithToken(payload); this.sendWithToken(payload);
this.messages.push({ id: Date.now(), user: 'System', text: `Raum-Erstellung gesendet: ${command}` }); this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.sent', { command }) });
this.requestRoomRefreshAfterCreate(); this.requestRoomRefreshAfterCreate();
}, },
selectTargetUser(name) { selectTargetUser(name) {
@@ -520,6 +560,7 @@ export default {
this.input = ''; this.input = '';
this.showOptions = false; this.showOptions = false;
this.showRoomCreatePanel = false; this.showRoomCreatePanel = false;
this.loadRoomCreateOptions();
this.announcedRoomEnter = false; this.announcedRoomEnter = false;
this.$refs.dialog.open(); this.$refs.dialog.open();
// Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird // Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird

View File

@@ -38,6 +38,52 @@
"connected": "Verbunden", "connected": "Verbunden",
"disconnected": "Getrennt", "disconnected": "Getrennt",
"error": "Fehler bei der Verbindung" "error": "Fehler bei der Verbindung"
},
"reloadRooms": "Räume neu laden",
"createRoom": {
"toggleShowChat": "Chat anzeigen",
"toggleCreateRoom": "Raum anlegen",
"title": "Neuen Raum erstellen",
"commandPrefix": "Kommando",
"labels": {
"roomName": "Raumname",
"visibility": "Sichtbarkeit",
"gender": "Geschlecht",
"minAge": "min_age",
"maxAge": "max_age",
"password": "Passwort",
"rightId": "right_id",
"typeId": "type_id",
"friendsOnly": "friends_only=true"
},
"placeholders": {
"roomName": "z. B. Lounge",
"password": "ohne Leerzeichen"
},
"options": {
"none": "(keine)"
},
"actions": {
"create": "Raum erstellen",
"reset": "Zurücksetzen"
},
"validation": {
"roomNameRequired": "Raumname ist erforderlich.",
"minAgeInvalid": "min_age muss >= 0 sein.",
"maxAgeInvalid": "max_age muss >= 0 sein.",
"ageRangeInvalid": "min_age darf nicht größer als max_age sein.",
"passwordSpaces": "Passwort darf keine Leerzeichen enthalten.",
"rightIdInvalid": "right_id muss > 0 sein.",
"typeIdInvalid": "type_id muss > 0 sein."
},
"messages": {
"noConnection": "Keine Verbindung zum Chat-Server.",
"invalidForm": "Bitte Eingaben im Raum-Formular korrigieren.",
"roomNameMissing": "Bitte einen Raumnamen angeben.",
"sent": "Raum-Erstellung gesendet: {command}"
},
"rights": {},
"types": {}
} }
}, },
"randomchat": { "randomchat": {

View File

@@ -38,6 +38,52 @@
"connected": "Connected", "connected": "Connected",
"disconnected": "Disconnected", "disconnected": "Disconnected",
"error": "Connection error" "error": "Connection error"
},
"reloadRooms": "Reload rooms",
"createRoom": {
"toggleShowChat": "Show chat",
"toggleCreateRoom": "Create room",
"title": "Create new room",
"commandPrefix": "Command",
"labels": {
"roomName": "Room name",
"visibility": "Visibility",
"gender": "Gender",
"minAge": "min_age",
"maxAge": "max_age",
"password": "Password",
"rightId": "right_id",
"typeId": "type_id",
"friendsOnly": "friends_only=true"
},
"placeholders": {
"roomName": "e.g. Lounge",
"password": "without spaces"
},
"options": {
"none": "(none)"
},
"actions": {
"create": "Create room",
"reset": "Reset"
},
"validation": {
"roomNameRequired": "Room name is required.",
"minAgeInvalid": "min_age must be >= 0.",
"maxAgeInvalid": "max_age must be >= 0.",
"ageRangeInvalid": "min_age must not be greater than max_age.",
"passwordSpaces": "Password must not contain spaces.",
"rightIdInvalid": "right_id must be > 0.",
"typeIdInvalid": "type_id must be > 0."
},
"messages": {
"noConnection": "No connection to chat server.",
"invalidForm": "Please correct the room form inputs.",
"roomNameMissing": "Please enter a room name.",
"sent": "Room creation sent: {command}"
},
"rights": {},
"types": {}
} }
}, },
"randomchat": { "randomchat": {

View File

@@ -37,6 +37,52 @@
"connected": "Conectado", "connected": "Conectado",
"disconnected": "Desconectado", "disconnected": "Desconectado",
"error": "Error de conexión" "error": "Error de conexión"
},
"reloadRooms": "Recargar salas",
"createRoom": {
"toggleShowChat": "Mostrar chat",
"toggleCreateRoom": "Crear sala",
"title": "Crear nueva sala",
"commandPrefix": "Comando",
"labels": {
"roomName": "Nombre de la sala",
"visibility": "Visibilidad",
"gender": "Género",
"minAge": "min_age",
"maxAge": "max_age",
"password": "Contraseña",
"rightId": "right_id",
"typeId": "type_id",
"friendsOnly": "friends_only=true"
},
"placeholders": {
"roomName": "p. ej. Lounge",
"password": "sin espacios"
},
"options": {
"none": "(ninguno)"
},
"actions": {
"create": "Crear sala",
"reset": "Restablecer"
},
"validation": {
"roomNameRequired": "El nombre de la sala es obligatorio.",
"minAgeInvalid": "min_age debe ser >= 0.",
"maxAgeInvalid": "max_age debe ser >= 0.",
"ageRangeInvalid": "min_age no puede ser mayor que max_age.",
"passwordSpaces": "La contraseña no debe contener espacios.",
"rightIdInvalid": "right_id debe ser > 0.",
"typeIdInvalid": "type_id debe ser > 0."
},
"messages": {
"noConnection": "Sin conexión con el servidor de chat.",
"invalidForm": "Corrige los datos del formulario de sala.",
"roomNameMissing": "Introduce un nombre de sala.",
"sent": "Creación de sala enviada: {command}"
},
"rights": {},
"types": {}
} }
}, },
"randomchat": { "randomchat": {