From 6091d6c4dde9da0462810bb487e79f3e08ccdd66 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Sat, 28 Mar 2026 17:46:31 +0100 Subject: [PATCH] feat: always show checklists in overview and add quarterly/halfyearly intervals --- backend/src/services/checklist.service.ts | 75 +++++++++++++++-------- frontend/src/pages/Checklisten.tsx | 25 +++++--- frontend/src/types/checklist.types.ts | 13 ++-- 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/backend/src/services/checklist.service.ts b/backend/src/services/checklist.service.ts index bc69de4..72d61c3 100644 --- a/backend/src/services/checklist.service.ts +++ b/backend/src/services/checklist.service.ts @@ -18,6 +18,12 @@ function calculateNextDueDate(intervall: string | null, intervall_tage: number | case 'monthly': now.setMonth(now.getMonth() + 1); return now; + case 'quarterly': + now.setMonth(now.getMonth() + 3); + return now; + case 'halfyearly': + now.setMonth(now.getMonth() + 6); + return now; case 'yearly': now.setFullYear(now.getFullYear() + 1); return now; @@ -522,38 +528,57 @@ async function getTemplatesForEquipment(ausruestungId: string) { async function getOverviewItems() { try { - // Vehicles with overdue or upcoming checklists (within 7 days) + // All vehicles with their assigned templates (direct, by type, or global) + // LEFT JOIN faelligkeit so unexecuted templates still appear const vehiclesResult = await pool.query(` - SELECT f.id, f.bezeichnung AS name, f.kurzname, - json_agg(json_build_object( - 'vorlage_id', cf.vorlage_id, - 'vorlage_name', v.name, + SELECT f.id, COALESCE(f.bezeichnung, f.kurzname) AS name, + json_agg(DISTINCT jsonb_build_object( + 'vorlage_id', cv.id, + 'vorlage_name', cv.name, + 'intervall', cv.intervall, 'next_due', cf.naechste_faellig_am - ) ORDER BY cf.naechste_faellig_am ASC) AS checklists - 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.fahrzeug_id IS NOT NULL - AND cf.naechste_faellig_am <= CURRENT_DATE + INTERVAL '7 days' + )) AS checklists + FROM fahrzeuge f + JOIN checklist_vorlagen cv ON cv.aktiv = true + AND cv.ausruestung_id IS NULL + AND cv.ausruestung_typ_id IS NULL + AND ( + cv.fahrzeug_id = f.id + OR cv.fahrzeug_typ_id IN ( + SELECT fahrzeug_typ_id FROM fahrzeug_fahrzeug_typen WHERE fahrzeug_id = f.id + ) + OR (cv.fahrzeug_id IS NULL AND cv.fahrzeug_typ_id IS NULL) + ) + LEFT JOIN checklist_faelligkeit cf ON cf.vorlage_id = cv.id AND cf.fahrzeug_id = f.id + WHERE f.deleted_at IS NULL GROUP BY f.id, f.bezeichnung, f.kurzname - ORDER BY MIN(cf.naechste_faellig_am) ASC + ORDER BY f.bezeichnung ASC, f.kurzname ASC `); - // Equipment with overdue or upcoming checklists (within 7 days) + // All equipment with their assigned templates const equipmentResult = await pool.query(` - SELECT a.id, a.name, - json_agg(json_build_object( - 'vorlage_id', cf.vorlage_id, - 'vorlage_name', v.name, + SELECT a.id, a.bezeichnung AS name, + json_agg(DISTINCT jsonb_build_object( + 'vorlage_id', cv.id, + 'vorlage_name', cv.name, + 'intervall', cv.intervall, 'next_due', cf.naechste_faellig_am - ) ORDER BY cf.naechste_faellig_am ASC) AS checklists - FROM checklist_faelligkeit cf - JOIN ausruestung a ON a.id = cf.ausruestung_id - JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true - WHERE cf.ausruestung_id IS NOT NULL - AND cf.naechste_faellig_am <= CURRENT_DATE + INTERVAL '7 days' - GROUP BY a.id, a.name - ORDER BY MIN(cf.naechste_faellig_am) ASC + )) AS checklists + FROM ausruestung a + JOIN checklist_vorlagen cv ON cv.aktiv = true + AND cv.fahrzeug_id IS NULL + AND cv.fahrzeug_typ_id IS NULL + AND ( + cv.ausruestung_id = a.id + OR cv.ausruestung_typ_id IN ( + SELECT ausruestung_typ_id FROM ausruestung_ausruestung_typen WHERE ausruestung_id = a.id + ) + OR (cv.ausruestung_id IS NULL AND cv.ausruestung_typ_id IS NULL) + ) + LEFT JOIN checklist_faelligkeit cf ON cf.vorlage_id = cv.id AND cf.ausruestung_id = a.id + WHERE a.deleted_at IS NULL + GROUP BY a.id, a.bezeichnung + ORDER BY a.bezeichnung ASC `); return { diff --git a/frontend/src/pages/Checklisten.tsx b/frontend/src/pages/Checklisten.tsx index 03db58b..e77de07 100644 --- a/frontend/src/pages/Checklisten.tsx +++ b/frontend/src/pages/Checklisten.tsx @@ -83,6 +83,8 @@ const formatDate = (iso?: string) => const INTERVALL_LABELS: Record = { weekly: 'Wöchentlich', monthly: 'Monatlich', + quarterly: 'Quartalsweise', + halfyearly: 'Halbjährlich', yearly: 'Jährlich', custom: 'Benutzerdefiniert', }; @@ -105,16 +107,16 @@ function getAssignmentLabel(v: ChecklistVorlage): string { return 'Global'; } -function getDueColor(nextDue?: string): 'error' | 'warning' | 'success' { - if (!nextDue) return 'success'; +function getDueColor(nextDue?: string | null, intervall?: string | null): 'error' | 'warning' | 'success' | 'default' { + if (!nextDue) return intervall ? 'warning' : 'default'; // has recurrence but never run → warn; no recurrence → neutral const daysUntil = Math.ceil((new Date(nextDue).getTime() - Date.now()) / 86400000); if (daysUntil < 0) return 'error'; if (daysUntil <= 3) return 'warning'; return 'success'; } -function getDueLabel(nextDue?: string): string { - if (!nextDue) return 'Aktuell'; +function getDueLabel(nextDue?: string | null, intervall?: string | null): string { + if (!nextDue) return intervall ? 'Noch nie ausgeführt' : 'Manuell'; const daysUntil = Math.ceil((new Date(nextDue).getTime() - Date.now()) / 86400000); if (daysUntil < 0) return `${Math.abs(daysUntil)}d überfällig`; if (daysUntil === 0) return 'Heute fällig'; @@ -257,12 +259,12 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro const equipment = overview?.equipment ?? []; if (vehicles.length === 0 && equipment.length === 0) { - return Keine offenen oder fälligen Checks; + return Keine Checklisten zugewiesen; } const renderChecklistRow = (cl: ChecklistOverviewChecklist, itemId: string, type: 'fahrzeug' | 'ausruestung') => { - const color = getDueColor(cl.next_due); - const label = getDueLabel(cl.next_due); + const color = getDueColor(cl.next_due, cl.intervall); + const label = getDueLabel(cl.next_due, cl.intervall); const param = type === 'fahrzeug' ? `fahrzeug=${itemId}` : `ausruestung=${itemId}`; return ( {items.map((item) => { const totalDue = item.checklists.length; - const hasOverdue = item.checklists.some((cl) => getDueColor(cl.next_due) === 'error'); + const hasOverdue = item.checklists.some((cl) => getDueColor(cl.next_due, cl.intervall) === 'error'); + const badgeColor = hasOverdue ? 'error' : item.checklists.some((cl) => getDueColor(cl.next_due, cl.intervall) === 'warning') ? 'warning' : 'default'; return ( }> @@ -319,7 +322,7 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro {item.name} @@ -593,8 +596,10 @@ function VorlagenTab({ vorlagen, loading, fahrzeugTypen, queryClient, showSucces Kein Intervall Wöchentlich Monatlich + Quartalsweise (alle 3 Monate) + Halbjährlich (alle 6 Monate) Jährlich - Benutzerdefiniert + Benutzerdefiniert (Tage) {form.intervall === 'custom' && ( diff --git a/frontend/src/types/checklist.types.ts b/frontend/src/types/checklist.types.ts index 3cdc973..14f3c09 100644 --- a/frontend/src/types/checklist.types.ts +++ b/frontend/src/types/checklist.types.ts @@ -127,19 +127,13 @@ export interface CreateVorlagePayload { fahrzeug_id?: string; ausruestung_typ_id?: number; ausruestung_id?: string; - intervall?: 'weekly' | 'monthly' | 'yearly' | 'custom'; - intervall_tage?: number; - beschreibung?: string; - aktiv?: boolean; -} - -export interface UpdateVorlagePayload { + intervall?: 'weekly' | 'monthly' | 'quarterly' | 'halfyearly' | 'yearly' | 'custom'; name?: string; fahrzeug_typ_id?: number | null; fahrzeug_id?: string | null; ausruestung_typ_id?: number | null; ausruestung_id?: string | null; - intervall?: 'weekly' | 'monthly' | 'yearly' | 'custom' | null; + intervall?: 'weekly' | 'monthly' | 'quarterly' | 'halfyearly' | 'yearly' | 'custom' | null; intervall_tage?: number | null; beschreibung?: string | null; aktiv?: boolean; @@ -187,7 +181,8 @@ export interface ChecklistWidgetSummary { export interface ChecklistOverviewChecklist { vorlage_id: number; vorlage_name: string; - next_due?: string; + intervall?: string | null; + next_due?: string | null; } export interface ChecklistOverviewItem {