feat: add issue kanban/attachments/deadlines, dashboard widget DnD, and checklisten system

This commit is contained in:
Matthias Hochmeister
2026-03-28 15:19:41 +01:00
parent a1cda5be51
commit 0c2ea829aa
42 changed files with 4804 additions and 201 deletions

View File

@@ -0,0 +1,85 @@
import pool from '../config/database';
import notificationService from '../services/notification.service';
import logger from '../utils/logger';
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
let jobInterval: ReturnType<typeof setInterval> | null = null;
let isRunning = false;
async function runChecklistReminderCheck(): Promise<void> {
if (isRunning) {
logger.warn('ChecklistReminderJob: previous run still in progress — skipping');
return;
}
isRunning = true;
try {
// Find overdue checklists
const result = await pool.query(`
SELECT cf.fahrzeug_id, cf.vorlage_id, cf.naechste_faellig_am,
f.bezeichnung AS fahrzeug_name,
v.name AS vorlage_name
FROM checklist_faelligkeit cf
JOIN fahrzeuge f ON f.id = cf.fahrzeug_id AND f.deleted_at IS NULL
JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true
WHERE cf.naechste_faellig_am <= CURRENT_DATE
`);
if (result.rows.length === 0) return;
// Find users who can execute checklists (Zeugmeister, Fahrmeister, Kommandant groups)
const usersResult = await pool.query(`
SELECT id FROM users
WHERE authentik_groups && ARRAY['dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_kommando', 'dashboard_admin']::text[]
`);
const targetUserIds = usersResult.rows.map((r: any) => r.id);
if (targetUserIds.length === 0) return;
for (const row of result.rows) {
const faelligDatum = new Date(row.naechste_faellig_am).toLocaleDateString('de-AT', {
day: '2-digit', month: '2-digit', year: 'numeric',
});
// Notify first responsible user (avoid spam by using quell_id dedup)
for (const userId of targetUserIds) {
await notificationService.createNotification({
user_id: userId,
typ: 'checklist_faellig',
titel: `Checkliste überfällig: ${row.vorlage_name}`,
nachricht: `Die Checkliste "${row.vorlage_name}" für ${row.fahrzeug_name} war fällig am ${faelligDatum}`,
schwere: 'warnung',
link: `/checklisten`,
quell_id: `${row.fahrzeug_id}_${row.vorlage_id}`,
quell_typ: 'checklist_faellig',
});
}
}
logger.info(`ChecklistReminderJob: processed ${result.rows.length} overdue checklists`);
} catch (error) {
logger.error('ChecklistReminderJob: unexpected error', {
error: error instanceof Error ? error.message : String(error),
});
} finally {
isRunning = false;
}
}
export function startChecklistReminderJob(): void {
if (jobInterval !== null) {
logger.warn('Checklist reminder job already running — skipping duplicate start');
return;
}
// Run once after short delay, then repeat
setTimeout(() => runChecklistReminderCheck(), 75 * 1000);
jobInterval = setInterval(() => runChecklistReminderCheck(), INTERVAL_MS);
logger.info('Checklist reminder job scheduled (every 15 minutes)');
}
export function stopChecklistReminderJob(): void {
if (jobInterval !== null) {
clearInterval(jobInterval);
jobInterval = null;
}
logger.info('Checklist reminder job stopped');
}

View File

@@ -0,0 +1,71 @@
import pool from '../config/database';
import notificationService from '../services/notification.service';
import logger from '../utils/logger';
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
let jobInterval: ReturnType<typeof setInterval> | null = null;
let isRunning = false;
async function runIssueReminderCheck(): Promise<void> {
if (isRunning) {
logger.warn('IssueReminderJob: previous run still in progress — skipping');
return;
}
isRunning = true;
try {
// Find overdue issues that are not in a terminal status
const result = await pool.query(`
SELECT i.id, i.titel, i.faellig_am, i.erstellt_von, i.zugewiesen_an
FROM issues i
LEFT JOIN issue_statuses s ON s.schluessel = i.status
WHERE i.faellig_am < NOW()
AND (s.ist_abschluss = false OR s.ist_abschluss IS NULL)
`);
for (const row of result.rows) {
// Notify the assignee, or the creator if no assignee
const targetUserId = row.zugewiesen_an || row.erstellt_von;
if (!targetUserId) continue;
await notificationService.createNotification({
user_id: targetUserId,
typ: 'issue_ueberfaellig',
titel: 'Issue überfällig',
nachricht: `Issue "${row.titel}" ist überfällig`,
schwere: 'warnung',
link: `/issues/${row.id}`,
quell_id: `issue-ueberfaellig-${row.id}`,
quell_typ: 'issue_ueberfaellig',
});
}
if (result.rows.length > 0) {
logger.info(`IssueReminderJob: processed ${result.rows.length} overdue issues`);
}
} catch (error) {
logger.error('IssueReminderJob: unexpected error', {
error: error instanceof Error ? error.message : String(error),
});
} finally {
isRunning = false;
}
}
export function startIssueReminderJob(): void {
if (jobInterval !== null) {
logger.warn('Issue reminder job already running — skipping duplicate start');
return;
}
// Run once after short delay, then repeat
setTimeout(() => runIssueReminderCheck(), 60 * 1000);
jobInterval = setInterval(() => runIssueReminderCheck(), INTERVAL_MS);
logger.info('Issue reminder job scheduled (every 15 minutes)');
}
export function stopIssueReminderJob(): void {
if (jobInterval !== null) {
clearInterval(jobInterval);
jobInterval = null;
}
logger.info('Issue reminder job stopped');
}