calendar download, date input validate, nc talk notification
This commit is contained in:
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user