feat: always show checklists in overview and add quarterly/halfyearly intervals

This commit is contained in:
Matthias Hochmeister
2026-03-28 17:46:31 +01:00
parent a52bb2a57c
commit 6091d6c4dd
3 changed files with 69 additions and 44 deletions

View File

@@ -18,6 +18,12 @@ function calculateNextDueDate(intervall: string | null, intervall_tage: number |
case 'monthly': case 'monthly':
now.setMonth(now.getMonth() + 1); now.setMonth(now.getMonth() + 1);
return now; return now;
case 'quarterly':
now.setMonth(now.getMonth() + 3);
return now;
case 'halfyearly':
now.setMonth(now.getMonth() + 6);
return now;
case 'yearly': case 'yearly':
now.setFullYear(now.getFullYear() + 1); now.setFullYear(now.getFullYear() + 1);
return now; return now;
@@ -522,38 +528,57 @@ async function getTemplatesForEquipment(ausruestungId: string) {
async function getOverviewItems() { async function getOverviewItems() {
try { 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(` const vehiclesResult = await pool.query(`
SELECT f.id, f.bezeichnung AS name, f.kurzname, SELECT f.id, COALESCE(f.bezeichnung, f.kurzname) AS name,
json_agg(json_build_object( json_agg(DISTINCT jsonb_build_object(
'vorlage_id', cf.vorlage_id, 'vorlage_id', cv.id,
'vorlage_name', v.name, 'vorlage_name', cv.name,
'intervall', cv.intervall,
'next_due', cf.naechste_faellig_am 'next_due', cf.naechste_faellig_am
) ORDER BY cf.naechste_faellig_am ASC) AS checklists )) AS checklists
FROM checklist_faelligkeit cf FROM fahrzeuge f
JOIN fahrzeuge f ON f.id = cf.fahrzeug_id AND f.deleted_at IS NULL JOIN checklist_vorlagen cv ON cv.aktiv = true
JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true AND cv.ausruestung_id IS NULL
WHERE cf.fahrzeug_id IS NOT NULL AND cv.ausruestung_typ_id IS NULL
AND cf.naechste_faellig_am <= CURRENT_DATE + INTERVAL '7 days' 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 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(` const equipmentResult = await pool.query(`
SELECT a.id, a.name, SELECT a.id, a.bezeichnung AS name,
json_agg(json_build_object( json_agg(DISTINCT jsonb_build_object(
'vorlage_id', cf.vorlage_id, 'vorlage_id', cv.id,
'vorlage_name', v.name, 'vorlage_name', cv.name,
'intervall', cv.intervall,
'next_due', cf.naechste_faellig_am 'next_due', cf.naechste_faellig_am
) ORDER BY cf.naechste_faellig_am ASC) AS checklists )) AS checklists
FROM checklist_faelligkeit cf FROM ausruestung a
JOIN ausruestung a ON a.id = cf.ausruestung_id JOIN checklist_vorlagen cv ON cv.aktiv = true
JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true AND cv.fahrzeug_id IS NULL
WHERE cf.ausruestung_id IS NOT NULL AND cv.fahrzeug_typ_id IS NULL
AND cf.naechste_faellig_am <= CURRENT_DATE + INTERVAL '7 days' AND (
GROUP BY a.id, a.name cv.ausruestung_id = a.id
ORDER BY MIN(cf.naechste_faellig_am) ASC 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 { return {

View File

@@ -83,6 +83,8 @@ const formatDate = (iso?: string) =>
const INTERVALL_LABELS: Record<string, string> = { const INTERVALL_LABELS: Record<string, string> = {
weekly: 'Wöchentlich', weekly: 'Wöchentlich',
monthly: 'Monatlich', monthly: 'Monatlich',
quarterly: 'Quartalsweise',
halfyearly: 'Halbjährlich',
yearly: 'Jährlich', yearly: 'Jährlich',
custom: 'Benutzerdefiniert', custom: 'Benutzerdefiniert',
}; };
@@ -105,16 +107,16 @@ function getAssignmentLabel(v: ChecklistVorlage): string {
return 'Global'; return 'Global';
} }
function getDueColor(nextDue?: string): 'error' | 'warning' | 'success' { function getDueColor(nextDue?: string | null, intervall?: string | null): 'error' | 'warning' | 'success' | 'default' {
if (!nextDue) return 'success'; 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';
if (daysUntil <= 3) return 'warning'; if (daysUntil <= 3) return 'warning';
return 'success'; return 'success';
} }
function getDueLabel(nextDue?: string): string { function getDueLabel(nextDue?: string | null, intervall?: string | null): string {
if (!nextDue) return 'Aktuell'; 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 (daysUntil === 0) return 'Heute fällig'; if (daysUntil === 0) return 'Heute fällig';
@@ -257,12 +259,12 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro
const equipment = overview?.equipment ?? []; const equipment = overview?.equipment ?? [];
if (vehicles.length === 0 && equipment.length === 0) { if (vehicles.length === 0 && equipment.length === 0) {
return <Alert severity="success" sx={{ mt: 1 }}>Keine offenen oder fälligen Checks</Alert>; return <Alert severity="info" sx={{ mt: 1 }}>Keine Checklisten zugewiesen</Alert>;
} }
const renderChecklistRow = (cl: ChecklistOverviewChecklist, itemId: string, type: 'fahrzeug' | 'ausruestung') => { const renderChecklistRow = (cl: ChecklistOverviewChecklist, itemId: string, type: 'fahrzeug' | 'ausruestung') => {
const color = getDueColor(cl.next_due); const color = getDueColor(cl.next_due, cl.intervall);
const label = getDueLabel(cl.next_due); const label = getDueLabel(cl.next_due, cl.intervall);
const param = type === 'fahrzeug' ? `fahrzeug=${itemId}` : `ausruestung=${itemId}`; const param = type === 'fahrzeug' ? `fahrzeug=${itemId}` : `ausruestung=${itemId}`;
return ( return (
<ListItem <ListItem
@@ -311,7 +313,8 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro
</Typography> </Typography>
{items.map((item) => { {items.map((item) => {
const totalDue = item.checklists.length; 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 ( return (
<Accordion key={item.id} variant="outlined" disableGutters> <Accordion key={item.id} variant="outlined" disableGutters>
<AccordionSummary expandIcon={<ExpandMore />}> <AccordionSummary expandIcon={<ExpandMore />}>
@@ -319,7 +322,7 @@ function OverviewTab({ overview, loading, canExecute, navigate }: OverviewTabPro
<Typography variant="subtitle1">{item.name}</Typography> <Typography variant="subtitle1">{item.name}</Typography>
<Badge <Badge
badgeContent={totalDue} badgeContent={totalDue}
color={hasOverdue ? 'error' : 'warning'} color={badgeColor as any}
sx={{ ml: 'auto', mr: 2 }} sx={{ ml: 'auto', mr: 2 }}
/> />
</Box> </Box>
@@ -593,8 +596,10 @@ function VorlagenTab({ vorlagen, loading, fahrzeugTypen, queryClient, showSucces
<MenuItem value="">Kein Intervall</MenuItem> <MenuItem value="">Kein Intervall</MenuItem>
<MenuItem value="weekly">Wöchentlich</MenuItem> <MenuItem value="weekly">Wöchentlich</MenuItem>
<MenuItem value="monthly">Monatlich</MenuItem> <MenuItem value="monthly">Monatlich</MenuItem>
<MenuItem value="quarterly">Quartalsweise (alle 3 Monate)</MenuItem>
<MenuItem value="halfyearly">Halbjährlich (alle 6 Monate)</MenuItem>
<MenuItem value="yearly">Jährlich</MenuItem> <MenuItem value="yearly">Jährlich</MenuItem>
<MenuItem value="custom">Benutzerdefiniert</MenuItem> <MenuItem value="custom">Benutzerdefiniert (Tage)</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
{form.intervall === 'custom' && ( {form.intervall === 'custom' && (

View File

@@ -127,19 +127,13 @@ export interface CreateVorlagePayload {
fahrzeug_id?: string; fahrzeug_id?: string;
ausruestung_typ_id?: number; ausruestung_typ_id?: number;
ausruestung_id?: string; ausruestung_id?: string;
intervall?: 'weekly' | 'monthly' | 'yearly' | 'custom'; intervall?: 'weekly' | 'monthly' | 'quarterly' | 'halfyearly' | 'yearly' | 'custom';
intervall_tage?: number;
beschreibung?: string;
aktiv?: boolean;
}
export interface UpdateVorlagePayload {
name?: string; name?: string;
fahrzeug_typ_id?: number | null; fahrzeug_typ_id?: number | null;
fahrzeug_id?: string | null; fahrzeug_id?: string | null;
ausruestung_typ_id?: number | null; ausruestung_typ_id?: number | null;
ausruestung_id?: string | null; ausruestung_id?: string | null;
intervall?: 'weekly' | 'monthly' | 'yearly' | 'custom' | null; intervall?: 'weekly' | 'monthly' | 'quarterly' | 'halfyearly' | 'yearly' | 'custom' | null;
intervall_tage?: number | null; intervall_tage?: number | null;
beschreibung?: string | null; beschreibung?: string | null;
aktiv?: boolean; aktiv?: boolean;
@@ -187,7 +181,8 @@ export interface ChecklistWidgetSummary {
export interface ChecklistOverviewChecklist { export interface ChecklistOverviewChecklist {
vorlage_id: number; vorlage_id: number;
vorlage_name: string; vorlage_name: string;
next_due?: string; intervall?: string | null;
next_due?: string | null;
} }
export interface ChecklistOverviewItem { export interface ChecklistOverviewItem {