feat(checklisten): rework checklist scheduling, overview, and execution UI
This commit is contained in:
@@ -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 <Alert severity="info" sx={{ mt: 1 }}>Keine Checklisten zugewiesen</Alert>;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ mb: 1.5 }}>
|
||||
Offene Prüfungen ({openChecks.length})
|
||||
</Typography>
|
||||
<List disablePadding>
|
||||
{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 (
|
||||
<ListItemButton
|
||||
key={`${cl.targetId}-${cl.vorlage_id}-${cl.ausruestung_id ?? ''}`}
|
||||
onClick={() => 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' },
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={primaryText}
|
||||
secondary={secondaryText}
|
||||
primaryTypographyProps={{ variant: 'body2' }}
|
||||
secondaryTypographyProps={{ variant: 'caption' }}
|
||||
/>
|
||||
<Chip
|
||||
label={label}
|
||||
color={color}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ ml: 1, pointerEvents: 'none' }}
|
||||
/>
|
||||
{canExecute && <PlayArrow fontSize="small" color="action" sx={{ ml: 1, opacity: 0.5 }} />}
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 && <PlayArrow fontSize="small" color="action" sx={{ ml: 1, opacity: 0.5 }} />}
|
||||
{canExecute && cl.ist_faellig && <PlayArrow fontSize="small" color="action" sx={{ ml: 1, opacity: 0.5 }} />}
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
@@ -376,16 +314,16 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro
|
||||
{icon} {title}
|
||||
</Typography>
|
||||
{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 (
|
||||
<Accordion key={item.id} variant="outlined" disableGutters>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, width: '100%' }}>
|
||||
<Typography variant="subtitle1">{item.name}</Typography>
|
||||
<Badge
|
||||
badgeContent={totalDue}
|
||||
badgeContent={openCount > 0 ? openCount : undefined}
|
||||
color={badgeColor as any}
|
||||
sx={{ ml: 'auto', mr: 2 }}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user