feat: add issue kanban/attachments/deadlines, dashboard widget DnD, and checklisten system
This commit is contained in:
85
backend/src/jobs/checklist-reminder.job.ts
Normal file
85
backend/src/jobs/checklist-reminder.job.ts
Normal 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');
|
||||
}
|
||||
71
backend/src/jobs/issue-reminder.job.ts
Normal file
71
backend/src/jobs/issue-reminder.job.ts
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user