calendar download, date input validate, nc talk notification

This commit is contained in:
Matthias Hochmeister
2026-03-04 14:39:39 +01:00
parent 179bbabd58
commit d27d2931a5
2 changed files with 90 additions and 1 deletions

View File

@@ -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<void> {
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<void> {
}
}
// ---------------------------------------------------------------------------
// 4. Nextcloud Talk unread messages → per-user notifications
// ---------------------------------------------------------------------------
async function generateNextcloudTalkNotifications(): Promise<void> {
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
// ---------------------------------------------------------------------------

View File

@@ -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() {
</DialogContent>
<DialogActions>
<Button onClick={() => setIcalEventOpen(false)}>Schließen</Button>
<Button
variant="outlined"
startIcon={<FileDownloadIcon />}
onClick={() => window.open(icalEventUrl, '_blank')}
>
Herunterladen
</Button>
</DialogActions>
</Dialog>
</Box>
@@ -2678,6 +2708,13 @@ export default function Kalender() {
</DialogContent>
<DialogActions>
<Button onClick={() => setIcalBookingOpen(false)}>Schließen</Button>
<Button
variant="outlined"
startIcon={<FileDownloadIcon />}
onClick={() => window.open(icalBookingUrl, '_blank')}
>
Herunterladen
</Button>
</DialogActions>
</Dialog>
</Box>