feat: always show checklists in overview and add quarterly/halfyearly intervals
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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' && (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user