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';
|
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
|
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);
|
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';
|
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';
|
if (!nextDue) return intervall ? 'Noch nie ausgeführt' : 'Manuell';
|
||||||
const daysUntil = Math.ceil((new Date(nextDue).getTime() - Date.now()) / 86400000);
|
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 `${Math.abs(daysUntil)}d überfällig`;
|
||||||
|
if (!istFaellig) return `Erledigt — nächste in ${daysUntil}d`;
|
||||||
if (daysUntil === 0) return 'Heute fällig';
|
if (daysUntil === 0) return 'Heute fällig';
|
||||||
return `in ${daysUntil}d 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>;
|
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 renderChecklistRow = (cl: ChecklistOverviewChecklist, itemId: string, type: 'fahrzeug' | 'ausruestung', index: number) => {
|
||||||
const color = getDueColor(cl.next_due, cl.intervall);
|
const color = getDueColor(cl.next_due, cl.intervall, cl.ist_faellig);
|
||||||
const label = getDueLabel(cl.next_due, cl.intervall);
|
const label = getDueLabel(cl.next_due, cl.intervall, cl.ist_faellig);
|
||||||
const param = cl.ausruestung_id
|
const param = cl.ausruestung_id
|
||||||
? `ausruestung=${cl.ausruestung_id}`
|
? `ausruestung=${cl.ausruestung_id}`
|
||||||
: type === 'fahrzeug'
|
: type === 'fahrzeug'
|
||||||
@@ -358,7 +296,7 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ ml: 1, pointerEvents: 'none' }}
|
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>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -376,16 +314,16 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro
|
|||||||
{icon} {title}
|
{icon} {title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const totalDue = item.checklists.length;
|
const openCount = item.checklists.filter((cl) => cl.ist_faellig).length;
|
||||||
const hasOverdue = item.checklists.some((cl) => getDueColor(cl.next_due, cl.intervall) === 'error');
|
const hasOverdue = item.checklists.some((cl) => getDueColor(cl.next_due, cl.intervall, cl.ist_faellig) === 'error');
|
||||||
const badgeColor = hasOverdue ? 'error' : item.checklists.some((cl) => getDueColor(cl.next_due, cl.intervall) === 'warning') ? 'warning' : 'default';
|
const badgeColor = hasOverdue ? 'error' : openCount > 0 ? 'warning' : 'success';
|
||||||
return (
|
return (
|
||||||
<Accordion key={item.id} variant="outlined" disableGutters>
|
<Accordion key={item.id} variant="outlined" disableGutters>
|
||||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, width: '100%' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, width: '100%' }}>
|
||||||
<Typography variant="subtitle1">{item.name}</Typography>
|
<Typography variant="subtitle1">{item.name}</Typography>
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={totalDue}
|
badgeContent={openCount > 0 ? openCount : undefined}
|
||||||
color={badgeColor as any}
|
color={badgeColor as any}
|
||||||
sx={{ ml: 'auto', mr: 2 }}
|
sx={{ ml: 'auto', mr: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user