diff --git a/backend/src/jobs/notification-generation.job.ts b/backend/src/jobs/notification-generation.job.ts index 659a0c1..87b5360 100644 --- a/backend/src/jobs/notification-generation.job.ts +++ b/backend/src/jobs/notification-generation.job.ts @@ -12,6 +12,7 @@ import pool from '../config/database'; import notificationService from '../services/notification.service'; +import nextcloudService from '../services/nextcloud.service'; import logger from '../utils/logger'; const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes @@ -28,6 +29,7 @@ export async function runNotificationGeneration(): Promise { await generateAtemschutzNotifications(); await generateVehicleNotifications(); await generateEquipmentNotifications(); + await generateNextcloudTalkNotifications(); await notificationService.deleteOldRead(); } catch (error) { logger.error('NotificationGenerationJob: unexpected error', { @@ -234,6 +236,56 @@ async function generateEquipmentNotifications(): Promise { } } +// --------------------------------------------------------------------------- +// 4. Nextcloud Talk unread messages → per-user notifications +// --------------------------------------------------------------------------- + +async function generateNextcloudTalkNotifications(): Promise { + const usersResult = await pool.query(` + SELECT id, nextcloud_login_name, nextcloud_app_password + FROM users + WHERE is_active = TRUE + AND nextcloud_login_name IS NOT NULL + AND nextcloud_app_password IS NOT NULL + `); + + for (const user of usersResult.rows) { + try { + const { conversations } = await nextcloudService.getConversations( + user.nextcloud_login_name, + user.nextcloud_app_password, + ); + + for (const conv of conversations) { + if (conv.unreadMessages <= 0) continue; + await notificationService.createNotification({ + user_id: user.id, + typ: 'nextcloud_talk', + titel: conv.displayName, + nachricht: `${conv.unreadMessages} ungelesene Nachrichten`, + schwere: 'info', + link: conv.url, + quell_id: conv.token, + quell_typ: 'nextcloud_talk', + }); + } + } catch (error: any) { + if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { + await pool.query( + `UPDATE users SET nextcloud_login_name = NULL, nextcloud_app_password = NULL WHERE id = $1`, + [user.id], + ); + logger.warn('NotificationGenerationJob: cleared invalid Nextcloud credentials', { userId: user.id }); + continue; + } + logger.error('NotificationGenerationJob: generateNextcloudTalkNotifications failed for user', { + userId: user.id, + error, + }); + } + } +} + // --------------------------------------------------------------------------- // Job lifecycle // --------------------------------------------------------------------------- diff --git a/frontend/src/pages/Kalender.tsx b/frontend/src/pages/Kalender.tsx index dcf1807..90ae8c3 100644 --- a/frontend/src/pages/Kalender.tsx +++ b/frontend/src/pages/Kalender.tsx @@ -68,7 +68,7 @@ import { } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; -import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput'; +import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDate, isValidGermanDateTime } from '../utils/dateInput'; import { useAuth } from '../contexts/AuthContext'; import { useNotification } from '../contexts/NotificationContext'; import { trainingApi } from '../services/training'; @@ -1187,6 +1187,27 @@ function VeranstaltungFormDialog({ notification.showError('Titel ist erforderlich'); return; } + + // Date validation + const vonDate = new Date(form.datum_von); + const bisDate = new Date(form.datum_bis); + if (isNaN(vonDate.getTime())) { + notification.showError(`Ungültiges Datum Von (Format: ${form.ganztaegig ? '01.03.2025' : '01.03.2025 18:00'})`); + return; + } + if (isNaN(bisDate.getTime())) { + notification.showError(`Ungültiges Datum Bis (Format: ${form.ganztaegig ? '01.03.2025' : '01.03.2025 18:00'})`); + return; + } + if (bisDate < vonDate) { + notification.showError('Datum Bis muss nach Datum Von liegen'); + return; + } + if (wiederholungAktiv && wiederholungBis && !isValidGermanDate(wiederholungBis)) { + notification.showError('Ungültiges Datum für Wiederholung Bis (Format: 01.03.2025)'); + return; + } + setLoading(true); try { const createPayload: CreateVeranstaltungInput = { @@ -1289,6 +1310,7 @@ function VeranstaltungFormDialog({ : fromDatetimeLocal(raw); handleChange('datum_von', iso); }} + helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'} InputLabelProps={{ shrink: true }} fullWidth /> @@ -1307,6 +1329,7 @@ function VeranstaltungFormDialog({ : fromDatetimeLocal(raw); handleChange('datum_bis', iso); }} + helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'} InputLabelProps={{ shrink: true }} fullWidth /> @@ -2190,6 +2213,13 @@ export default function Kalender() { + @@ -2678,6 +2708,13 @@ export default function Kalender() { +