From 9410441ce2dc3f5719696fbee54d54c9f4d22052 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Mon, 20 Apr 2026 18:37:00 +0200 Subject: [PATCH] feat(checklisten): rework checklist scheduling, overview, and execution UI --- frontend/src/pages/Checklisten.tsx | 88 +++++------------------------- 1 file changed, 13 insertions(+), 75 deletions(-) diff --git a/frontend/src/pages/Checklisten.tsx b/frontend/src/pages/Checklisten.tsx index bf76b52..b1cce3c 100644 --- a/frontend/src/pages/Checklisten.tsx +++ b/frontend/src/pages/Checklisten.tsx @@ -110,18 +110,20 @@ function getAssignmentLabel(v: ChecklistVorlage): string { return 'Global'; } -function getDueColor(nextDue?: string | null, intervall?: string | null): 'error' | 'warning' | 'success' | 'default' { +function getDueColor(nextDue?: string | null, intervall?: string | null, istFaellig?: boolean): '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 < 0) return 'error'; // overdue + if (!istFaellig) return 'success'; // done for current period if (daysUntil <= 3) return 'warning'; - return 'success'; + return 'default'; } -function getDueLabel(nextDue?: string | null, intervall?: string | null): string { +function getDueLabel(nextDue?: string | null, intervall?: string | null, istFaellig?: boolean): 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 (!istFaellig) return `Erledigt — nächste in ${daysUntil}d`; if (daysUntil === 0) return 'Heute fällig'; return `in ${daysUntil}d fällig`; } @@ -257,73 +259,9 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro return Keine Checklisten zugewiesen; } - // Flatten all checklists and detect open checks - const allChecklists = [ - ...vehicles.flatMap((v) => v.checklists.map((cl) => ({ ...cl, targetName: v.name, targetId: v.id, targetType: 'fahrzeug' as const }))), - ...equipment.flatMap((e) => e.checklists.map((cl) => ({ ...cl, targetName: e.name, targetId: e.id, targetType: 'ausruestung' as const }))), - ]; - const openChecks = allChecklists.filter((cl) => cl.ist_faellig); - const hasOpenChecks = openChecks.length > 0; - - // ── Open checks flat list ── - if (hasOpenChecks) { - return ( - - - Offene Prüfungen ({openChecks.length}) - - - {openChecks.map((cl, idx) => { - const color = getDueColor(cl.next_due, cl.intervall); - const label = getDueLabel(cl.next_due, cl.intervall); - const param = cl.ausruestung_id - ? `ausruestung=${cl.ausruestung_id}` - : cl.targetType === 'ausruestung' - ? `ausruestung=${cl.targetId}` - : `fahrzeug=${cl.targetId}`; - const primaryText = cl.targetName + ' — ' + cl.vorlage_name + (cl.ausruestung_name ? ` (${cl.ausruestung_name})` : ''); - const secondaryText = cl.letzte_ausfuehrung_am - ? `Letzte Prüfung: ${formatDate(cl.letzte_ausfuehrung_am)}` - : 'Noch nie geprüft'; - return ( - canExecute && navigate(`/checklisten/ausfuehrung/new?${param}&vorlage=${cl.vorlage_id}`)} - sx={{ - py: 1, - px: 2, - bgcolor: idx % 2 === 0 ? 'action.hover' : 'transparent', - cursor: canExecute ? 'pointer' : 'default', - '&:hover': canExecute ? undefined : { bgcolor: idx % 2 === 0 ? 'action.hover' : 'transparent' }, - }} - > - - - {canExecute && } - - ); - })} - - - ); - } - - // ── Normal accordion view (no open checks) ── - const renderChecklistRow = (cl: ChecklistOverviewChecklist, itemId: string, type: 'fahrzeug' | 'ausruestung', index: number) => { - const color = getDueColor(cl.next_due, cl.intervall); - const label = getDueLabel(cl.next_due, cl.intervall); + const color = getDueColor(cl.next_due, cl.intervall, cl.ist_faellig); + const label = getDueLabel(cl.next_due, cl.intervall, cl.ist_faellig); const param = cl.ausruestung_id ? `ausruestung=${cl.ausruestung_id}` : type === 'fahrzeug' @@ -358,7 +296,7 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro variant="outlined" sx={{ ml: 1, pointerEvents: 'none' }} /> - {canExecute && } + {canExecute && cl.ist_faellig && } ); }; @@ -376,16 +314,16 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro {icon} {title} {items.map((item) => { - const totalDue = item.checklists.length; - 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'; + const openCount = item.checklists.filter((cl) => cl.ist_faellig).length; + const hasOverdue = item.checklists.some((cl) => getDueColor(cl.next_due, cl.intervall, cl.ist_faellig) === 'error'); + const badgeColor = hasOverdue ? 'error' : openCount > 0 ? 'warning' : 'success'; return ( }> {item.name} 0 ? openCount : undefined} color={badgeColor as any} sx={{ ml: 'auto', mr: 2 }} />