calendar download, date input validate, nc talk notification
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import pool from '../config/database';
|
import pool from '../config/database';
|
||||||
import notificationService from '../services/notification.service';
|
import notificationService from '../services/notification.service';
|
||||||
|
import nextcloudService from '../services/nextcloud.service';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
||||||
@@ -28,6 +29,7 @@ export async function runNotificationGeneration(): Promise<void> {
|
|||||||
await generateAtemschutzNotifications();
|
await generateAtemschutzNotifications();
|
||||||
await generateVehicleNotifications();
|
await generateVehicleNotifications();
|
||||||
await generateEquipmentNotifications();
|
await generateEquipmentNotifications();
|
||||||
|
await generateNextcloudTalkNotifications();
|
||||||
await notificationService.deleteOldRead();
|
await notificationService.deleteOldRead();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('NotificationGenerationJob: unexpected 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
|
// Job lifecycle
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
import { trainingApi } from '../services/training';
|
import { trainingApi } from '../services/training';
|
||||||
@@ -1187,6 +1187,27 @@ function VeranstaltungFormDialog({
|
|||||||
notification.showError('Titel ist erforderlich');
|
notification.showError('Titel ist erforderlich');
|
||||||
return;
|
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);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const createPayload: CreateVeranstaltungInput = {
|
const createPayload: CreateVeranstaltungInput = {
|
||||||
@@ -1289,6 +1310,7 @@ function VeranstaltungFormDialog({
|
|||||||
: fromDatetimeLocal(raw);
|
: fromDatetimeLocal(raw);
|
||||||
handleChange('datum_von', iso);
|
handleChange('datum_von', iso);
|
||||||
}}
|
}}
|
||||||
|
helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'}
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
@@ -1307,6 +1329,7 @@ function VeranstaltungFormDialog({
|
|||||||
: fromDatetimeLocal(raw);
|
: fromDatetimeLocal(raw);
|
||||||
handleChange('datum_bis', iso);
|
handleChange('datum_bis', iso);
|
||||||
}}
|
}}
|
||||||
|
helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'}
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
@@ -2190,6 +2213,13 @@ export default function Kalender() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setIcalEventOpen(false)}>Schließen</Button>
|
<Button onClick={() => setIcalEventOpen(false)}>Schließen</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileDownloadIcon />}
|
||||||
|
onClick={() => window.open(icalEventUrl, '_blank')}
|
||||||
|
>
|
||||||
|
Herunterladen
|
||||||
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -2678,6 +2708,13 @@ export default function Kalender() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setIcalBookingOpen(false)}>Schließen</Button>
|
<Button onClick={() => setIcalBookingOpen(false)}>Schließen</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileDownloadIcon />}
|
||||||
|
onClick={() => window.open(icalBookingUrl, '_blank')}
|
||||||
|
>
|
||||||
|
Herunterladen
|
||||||
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user