import pool from '../config/database'; import logger from '../utils/logger'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function calculateNextDueDate(intervall: string | null, intervall_tage: number | null): Date | null { const now = new Date(); if (intervall_tage && intervall_tage > 0) { now.setDate(now.getDate() + intervall_tage); return now; } switch (intervall) { case 'weekly': now.setDate(now.getDate() + 7); return now; case 'monthly': now.setMonth(now.getMonth() + 1); return now; case 'quarterly': now.setMonth(now.getMonth() + 3); return now; case 'halfyearly': now.setMonth(now.getMonth() + 6); return now; case 'yearly': now.setFullYear(now.getFullYear() + 1); return now; default: return null; } } // Subquery fragments for junction table arrays const JUNCTION_SUBQUERIES = ` ARRAY(SELECT fahrzeug_typ_id FROM checklist_vorlage_fahrzeug_typen WHERE vorlage_id = v.id) AS fahrzeug_typ_ids, ARRAY(SELECT fahrzeug_id::text FROM checklist_vorlage_fahrzeuge WHERE vorlage_id = v.id) AS fahrzeug_ids, ARRAY(SELECT ausruestung_typ_id FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = v.id) AS ausruestung_typ_ids, ARRAY(SELECT ausruestung_id::text FROM checklist_vorlage_ausruestung WHERE vorlage_id = v.id) AS ausruestung_ids, ARRAY(SELECT ft.name FROM checklist_vorlage_fahrzeug_typen cvft JOIN fahrzeug_typen ft ON ft.id = cvft.fahrzeug_typ_id WHERE cvft.vorlage_id = v.id) AS fahrzeug_typ_names, ARRAY(SELECT f.bezeichnung FROM checklist_vorlage_fahrzeuge cvf JOIN fahrzeuge f ON f.id = cvf.fahrzeug_id WHERE cvf.vorlage_id = v.id) AS fahrzeug_names, ARRAY(SELECT at2.name FROM checklist_vorlage_ausruestung_typen cvat JOIN ausruestung_typen at2 ON at2.id = cvat.ausruestung_typ_id WHERE cvat.vorlage_id = v.id) AS ausruestung_typ_names, ARRAY(SELECT a.bezeichnung FROM checklist_vorlage_ausruestung cva JOIN ausruestung a ON a.id = cva.ausruestung_id WHERE cva.vorlage_id = v.id) AS ausruestung_names `; // Helper: insert rows into junction tables within a transaction client async function insertJunctionRows(client: any, vorlageId: number, data: { fahrzeug_typ_ids?: number[]; fahrzeug_ids?: string[]; ausruestung_typ_ids?: number[]; ausruestung_ids?: string[]; }) { if (data.fahrzeug_typ_ids?.length) { for (const id of data.fahrzeug_typ_ids) { await client.query( `INSERT INTO checklist_vorlage_fahrzeug_typen (vorlage_id, fahrzeug_typ_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [vorlageId, id] ); } } if (data.fahrzeug_ids?.length) { for (const id of data.fahrzeug_ids) { await client.query( `INSERT INTO checklist_vorlage_fahrzeuge (vorlage_id, fahrzeug_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [vorlageId, id] ); } } if (data.ausruestung_typ_ids?.length) { for (const id of data.ausruestung_typ_ids) { await client.query( `INSERT INTO checklist_vorlage_ausruestung_typen (vorlage_id, ausruestung_typ_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [vorlageId, id] ); } } if (data.ausruestung_ids?.length) { for (const id of data.ausruestung_ids) { await client.query( `INSERT INTO checklist_vorlage_ausruestung (vorlage_id, ausruestung_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [vorlageId, id] ); } } } // Helper: delete all junction rows for a vorlage async function deleteJunctionRows(client: any, vorlageId: number) { await client.query(`DELETE FROM checklist_vorlage_fahrzeug_typen WHERE vorlage_id = $1`, [vorlageId]); await client.query(`DELETE FROM checklist_vorlage_fahrzeuge WHERE vorlage_id = $1`, [vorlageId]); await client.query(`DELETE FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = $1`, [vorlageId]); await client.query(`DELETE FROM checklist_vorlage_ausruestung WHERE vorlage_id = $1`, [vorlageId]); } // "Global" template condition: no rows in any junction table const GLOBAL_TEMPLATE_CONDITION = ` NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeug_typen WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeuge WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung WHERE vorlage_id = v.id) `; // --------------------------------------------------------------------------- // Vorlagen (Templates) // --------------------------------------------------------------------------- async function getVorlagen(filter?: { aktiv?: boolean }) { try { const conditions: string[] = []; const values: any[] = []; let idx = 1; if (filter?.aktiv !== undefined) { conditions.push(`v.aktiv = $${idx}`); values.push(filter.aktiv); idx++; } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await pool.query( `SELECT v.*, ${JUNCTION_SUBQUERIES} FROM checklist_vorlagen v ${where} ORDER BY v.name ASC`, values ); return result.rows; } catch (error) { logger.error('ChecklistService.getVorlagen failed', { error }); throw new Error('Vorlagen konnten nicht geladen werden'); } } async function getVorlageById(id: number) { try { const vorlageResult = await pool.query( `SELECT v.*, ${JUNCTION_SUBQUERIES} FROM checklist_vorlagen v WHERE v.id = $1`, [id] ); if (vorlageResult.rows.length === 0) return null; const vorlage = vorlageResult.rows[0]; const itemsResult = await pool.query( `SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`, [id] ); vorlage.items = itemsResult.rows; return vorlage; } catch (error) { logger.error('ChecklistService.getVorlageById failed', { error, id }); throw new Error('Vorlage konnte nicht geladen werden'); } } async function createVorlage(data: { name: string; fahrzeug_typ_ids?: number[]; fahrzeug_ids?: string[]; ausruestung_typ_ids?: number[]; ausruestung_ids?: string[]; intervall?: string | null; intervall_tage?: number | null; beschreibung?: string | null; }) { const client = await pool.connect(); try { await client.query('BEGIN'); const result = await client.query( `INSERT INTO checklist_vorlagen (name, intervall, intervall_tage, beschreibung) VALUES ($1, $2, $3, $4) RETURNING *`, [data.name, data.intervall ?? null, data.intervall_tage ?? null, data.beschreibung ?? null] ); const vorlage = result.rows[0]; await insertJunctionRows(client, vorlage.id, data); await client.query('COMMIT'); return getVorlageById(vorlage.id); } catch (error) { await client.query('ROLLBACK').catch(() => {}); logger.error('ChecklistService.createVorlage failed', { error }); throw new Error('Vorlage konnte nicht erstellt werden'); } finally { client.release(); } } async function updateVorlage(id: number, data: { name?: string; fahrzeug_typ_ids?: number[]; fahrzeug_ids?: string[]; ausruestung_typ_ids?: number[]; ausruestung_ids?: string[]; intervall?: string | null; intervall_tage?: number | null; beschreibung?: string | null; aktiv?: boolean; }) { const client = await pool.connect(); try { await client.query('BEGIN'); // Build SET clauses for scalar fields only const setClauses: string[] = []; const values: any[] = []; let idx = 1; if (data.name !== undefined) { setClauses.push(`name = $${idx}`); values.push(data.name); idx++; } if ('intervall' in data) { setClauses.push(`intervall = $${idx}`); values.push(data.intervall); idx++; } if ('intervall_tage' in data) { setClauses.push(`intervall_tage = $${idx}`); values.push(data.intervall_tage); idx++; } if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; } if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; } if (setClauses.length > 0) { values.push(id); await client.query( `UPDATE checklist_vorlagen SET ${setClauses.join(', ')} WHERE id = $${idx}`, values ); } // Replace junction rows if any array key is present const hasJunctionData = 'fahrzeug_typ_ids' in data || 'fahrzeug_ids' in data || 'ausruestung_typ_ids' in data || 'ausruestung_ids' in data; if (hasJunctionData) { await deleteJunctionRows(client, id); await insertJunctionRows(client, id, { fahrzeug_typ_ids: data.fahrzeug_typ_ids ?? [], fahrzeug_ids: data.fahrzeug_ids ?? [], ausruestung_typ_ids: data.ausruestung_typ_ids ?? [], ausruestung_ids: data.ausruestung_ids ?? [], }); } await client.query('COMMIT'); return getVorlageById(id); } catch (error) { await client.query('ROLLBACK').catch(() => {}); logger.error('ChecklistService.updateVorlage failed', { error, id }); throw new Error('Vorlage konnte nicht aktualisiert werden'); } finally { client.release(); } } async function deleteVorlage(id: number) { try { const result = await pool.query( `DELETE FROM checklist_vorlagen WHERE id = $1 RETURNING *`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.deleteVorlage failed', { error, id }); throw new Error('Vorlage konnte nicht gelöscht werden'); } } // --------------------------------------------------------------------------- // Vorlage Items (Template line items) // --------------------------------------------------------------------------- async function getVorlageItems(vorlageId: number) { try { const result = await pool.query( `SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`, [vorlageId] ); return result.rows; } catch (error) { logger.error('ChecklistService.getVorlageItems failed', { error, vorlageId }); throw new Error('Vorlage-Items konnten nicht geladen werden'); } } async function addVorlageItem(vorlageId: number, data: { bezeichnung: string; beschreibung?: string | null; pflicht?: boolean; sort_order?: number; parent_item_id?: number | null; }) { try { const result = await pool.query( `INSERT INTO checklist_vorlage_items (vorlage_id, bezeichnung, beschreibung, pflicht, sort_order, parent_item_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [vorlageId, data.bezeichnung, data.beschreibung ?? null, data.pflicht ?? true, data.sort_order ?? 0, data.parent_item_id ?? null] ); return result.rows[0]; } catch (error) { logger.error('ChecklistService.addVorlageItem failed', { error, vorlageId }); throw new Error('Vorlage-Item konnte nicht erstellt werden'); } } async function updateVorlageItem(id: number, data: { bezeichnung?: string; beschreibung?: string | null; pflicht?: boolean; sort_order?: number; }) { try { const setClauses: string[] = []; const values: any[] = []; let idx = 1; if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; } if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; } if (data.pflicht !== undefined) { setClauses.push(`pflicht = $${idx}`); values.push(data.pflicht); idx++; } if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; } if (setClauses.length === 0) { const r = await pool.query(`SELECT * FROM checklist_vorlage_items WHERE id = $1`, [id]); return r.rows[0] || null; } values.push(id); const result = await pool.query( `UPDATE checklist_vorlage_items SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`, values ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.updateVorlageItem failed', { error, id }); throw new Error('Vorlage-Item konnte nicht aktualisiert werden'); } } async function deleteVorlageItem(id: number) { try { const result = await pool.query( `DELETE FROM checklist_vorlage_items WHERE id = $1 RETURNING *`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.deleteVorlageItem failed', { error, id }); throw new Error('Vorlage-Item konnte nicht gelöscht werden'); } } // --------------------------------------------------------------------------- // Fahrzeug-spezifische Items // --------------------------------------------------------------------------- async function getVehicleItems(fahrzeugId: string) { try { const result = await pool.query( `SELECT * FROM fahrzeug_checklist_items WHERE fahrzeug_id = $1 AND aktiv = true ORDER BY sort_order ASC, id ASC`, [fahrzeugId] ); return result.rows; } catch (error) { logger.error('ChecklistService.getVehicleItems failed', { error, fahrzeugId }); throw new Error('Fahrzeug-Items konnten nicht geladen werden'); } } async function addVehicleItem(fahrzeugId: string, data: { bezeichnung: string; beschreibung?: string | null; pflicht?: boolean; sort_order?: number; }) { try { const result = await pool.query( `INSERT INTO fahrzeug_checklist_items (fahrzeug_id, bezeichnung, beschreibung, pflicht, sort_order) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [fahrzeugId, data.bezeichnung, data.beschreibung ?? null, data.pflicht ?? true, data.sort_order ?? 0] ); return result.rows[0]; } catch (error) { logger.error('ChecklistService.addVehicleItem failed', { error, fahrzeugId }); throw new Error('Fahrzeug-Item konnte nicht erstellt werden'); } } async function updateVehicleItem(id: number, data: { bezeichnung?: string; beschreibung?: string | null; pflicht?: boolean; sort_order?: number; aktiv?: boolean; }) { try { const setClauses: string[] = []; const values: any[] = []; let idx = 1; if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; } if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; } if (data.pflicht !== undefined) { setClauses.push(`pflicht = $${idx}`); values.push(data.pflicht); idx++; } if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; } if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; } if (setClauses.length === 0) { const r = await pool.query(`SELECT * FROM fahrzeug_checklist_items WHERE id = $1`, [id]); return r.rows[0] || null; } values.push(id); const result = await pool.query( `UPDATE fahrzeug_checklist_items SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`, values ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.updateVehicleItem failed', { error, id }); throw new Error('Fahrzeug-Item konnte nicht aktualisiert werden'); } } async function deleteVehicleItem(id: number) { try { const result = await pool.query( `UPDATE fahrzeug_checklist_items SET aktiv = false WHERE id = $1 RETURNING *`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.deleteVehicleItem failed', { error, id }); throw new Error('Fahrzeug-Item konnte nicht deaktiviert werden'); } } // --------------------------------------------------------------------------- // Ausrüstung-spezifische Items // --------------------------------------------------------------------------- async function getEquipmentItems(ausruestungId: string) { try { const result = await pool.query( `SELECT * FROM ausruestung_checklist_items WHERE ausruestung_id = $1 AND aktiv = true ORDER BY sort_order ASC, id ASC`, [ausruestungId] ); return result.rows; } catch (error) { logger.error('ChecklistService.getEquipmentItems failed', { error, ausruestungId }); throw new Error('Ausrüstungs-Items konnten nicht geladen werden'); } } async function addEquipmentItem(ausruestungId: string, data: { bezeichnung: string; beschreibung?: string | null; pflicht?: boolean; sort_order?: number; }) { try { const result = await pool.query( `INSERT INTO ausruestung_checklist_items (ausruestung_id, bezeichnung, beschreibung, pflicht, sort_order) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [ausruestungId, data.bezeichnung, data.beschreibung ?? null, data.pflicht ?? true, data.sort_order ?? 0] ); return result.rows[0]; } catch (error) { logger.error('ChecklistService.addEquipmentItem failed', { error, ausruestungId }); throw new Error('Ausrüstungs-Item konnte nicht erstellt werden'); } } async function updateEquipmentItem(id: number, data: { bezeichnung?: string; beschreibung?: string | null; pflicht?: boolean; sort_order?: number; aktiv?: boolean; }) { try { const setClauses: string[] = []; const values: any[] = []; let idx = 1; if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; } if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; } if (data.pflicht !== undefined) { setClauses.push(`pflicht = $${idx}`); values.push(data.pflicht); idx++; } if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; } if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; } if (setClauses.length === 0) { const r = await pool.query(`SELECT * FROM ausruestung_checklist_items WHERE id = $1`, [id]); return r.rows[0] || null; } values.push(id); const result = await pool.query( `UPDATE ausruestung_checklist_items SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`, values ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.updateEquipmentItem failed', { error, id }); throw new Error('Ausrüstungs-Item konnte nicht aktualisiert werden'); } } async function deleteEquipmentItem(id: number) { try { const result = await pool.query( `UPDATE ausruestung_checklist_items SET aktiv = false WHERE id = $1 RETURNING *`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('ChecklistService.deleteEquipmentItem failed', { error, id }); throw new Error('Ausrüstungs-Item konnte nicht deaktiviert werden'); } } // --------------------------------------------------------------------------- // Templates for a specific vehicle (via junction tables) // --------------------------------------------------------------------------- async function getTemplatesForVehicle(fahrzeugId: string) { try { const result = await pool.query( `SELECT DISTINCT v.*, ${JUNCTION_SUBQUERIES} FROM checklist_vorlagen v WHERE v.aktiv = true AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung WHERE vorlage_id = v.id) AND ( EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeuge cvf WHERE cvf.vorlage_id = v.id AND cvf.fahrzeug_id = $1) OR EXISTS ( SELECT 1 FROM checklist_vorlage_fahrzeug_typen cvft WHERE cvft.vorlage_id = v.id AND cvft.fahrzeug_typ_id IN (SELECT fahrzeug_typ_id FROM fahrzeug_fahrzeug_typen WHERE fahrzeug_id = $1) ) OR ( NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeug_typen WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeuge WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = v.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung WHERE vorlage_id = v.id) ) ) ORDER BY v.name ASC`, [fahrzeugId] ); // Attach items to each template for (const vorlage of result.rows) { const items = await pool.query( `SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`, [vorlage.id] ); vorlage.items = items.rows; } return result.rows; } catch (error) { logger.error('ChecklistService.getTemplatesForVehicle failed', { error, fahrzeugId }); throw new Error('Checklisten für Fahrzeug konnten nicht geladen werden'); } } // --------------------------------------------------------------------------- // Templates for a specific equipment item (via junction tables) // --------------------------------------------------------------------------- async function getTemplatesForEquipment(ausruestungId: string) { try { const result = await pool.query( `SELECT DISTINCT v.*, ${JUNCTION_SUBQUERIES} FROM checklist_vorlagen v WHERE v.aktiv = true AND ( EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung cva WHERE cva.vorlage_id = v.id AND cva.ausruestung_id = $1) OR EXISTS ( SELECT 1 FROM checklist_vorlage_ausruestung_typen cvat WHERE cvat.vorlage_id = v.id AND cvat.ausruestung_typ_id IN (SELECT ausruestung_typ_id FROM ausruestung_ausruestung_typen WHERE ausruestung_id = $1) ) OR ( ${GLOBAL_TEMPLATE_CONDITION} ) ) ORDER BY v.name ASC`, [ausruestungId] ); // Attach items to each template for (const vorlage of result.rows) { const items = await pool.query( `SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`, [vorlage.id] ); vorlage.items = items.rows; } return result.rows; } catch (error) { logger.error('ChecklistService.getTemplatesForEquipment failed', { error, ausruestungId }); throw new Error('Checklisten für Ausrüstung konnten nicht geladen werden'); } } // --------------------------------------------------------------------------- // Overview Items (vehicles + equipment with open checklists) // --------------------------------------------------------------------------- async function getOverviewItems() { try { // All vehicles with their assigned templates (direct, by type, or global) const vehiclesResult = await pool.query(` SELECT f.id, COALESCE(f.bezeichnung, f.kurzname) AS name, json_agg(DISTINCT jsonb_build_object( 'vorlage_id', cv.id, 'vorlage_name', cv.name, 'intervall', cv.intervall, 'next_due', cf.naechste_faellig_am )) AS checklists FROM fahrzeuge f JOIN checklist_vorlagen cv ON cv.aktiv = true AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung WHERE vorlage_id = cv.id) AND ( EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeuge cvf WHERE cvf.vorlage_id = cv.id AND cvf.fahrzeug_id = f.id) OR EXISTS ( SELECT 1 FROM checklist_vorlage_fahrzeug_typen cvft WHERE cvft.vorlage_id = cv.id AND cvft.fahrzeug_typ_id IN (SELECT fahrzeug_typ_id FROM fahrzeug_fahrzeug_typen WHERE fahrzeug_id = f.id) ) OR ( NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeug_typen WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeuge WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung WHERE vorlage_id = cv.id) ) ) 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 ORDER BY f.bezeichnung ASC, f.kurzname ASC `); // All equipment with their assigned templates const equipmentResult = await pool.query(` SELECT a.id, a.bezeichnung AS name, json_agg(DISTINCT jsonb_build_object( 'vorlage_id', cv.id, 'vorlage_name', cv.name, 'intervall', cv.intervall, 'next_due', cf.naechste_faellig_am )) AS checklists FROM ausruestung a JOIN checklist_vorlagen cv ON cv.aktiv = true AND ( EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung cva WHERE cva.vorlage_id = cv.id AND cva.ausruestung_id = a.id) OR EXISTS ( SELECT 1 FROM checklist_vorlage_ausruestung_typen cvat WHERE cvat.vorlage_id = cv.id AND cvat.ausruestung_typ_id IN (SELECT ausruestung_typ_id FROM ausruestung_ausruestung_typen WHERE ausruestung_id = a.id) ) OR ( NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeug_typen WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_fahrzeuge WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung_typen WHERE vorlage_id = cv.id) AND NOT EXISTS (SELECT 1 FROM checklist_vorlage_ausruestung WHERE vorlage_id = cv.id) ) ) 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 { vehicles: vehiclesResult.rows, equipment: equipmentResult.rows, }; } catch (error) { logger.error('ChecklistService.getOverviewItems failed', { error }); throw new Error('Übersichtsdaten konnten nicht geladen werden'); } } // --------------------------------------------------------------------------- // Ausführungen (Executions) // --------------------------------------------------------------------------- async function startExecution(fahrzeugId: string | null, vorlageId: number, userId: string, ausruestungId?: string | null) { const client = await pool.connect(); try { await client.query('BEGIN'); // Create the execution record const execResult = await client.query( `INSERT INTO checklist_ausfuehrungen (fahrzeug_id, ausruestung_id, vorlage_id, ausgefuehrt_von) VALUES ($1, $2, $3, $4) RETURNING *`, [fahrzeugId || null, ausruestungId || null, vorlageId, userId] ); const execution = execResult.rows[0]; // Copy template items into execution items (two-pass to preserve parent-child hierarchy) const vorlageItems = await client.query( `SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`, [vorlageId] ); // First pass: insert all items, record the vorlage_item_id → ausfuehrung_item_id mapping const vorlageToAusfuehrungId = new Map(); for (const item of vorlageItems.rows) { const inserted = await client.query( `INSERT INTO checklist_ausfuehrung_items (ausfuehrung_id, vorlage_item_id, bezeichnung) VALUES ($1, $2, $3) RETURNING id`, [execution.id, item.id, item.bezeichnung] ); vorlageToAusfuehrungId.set(item.id, inserted.rows[0].id); } // Second pass: set parent_ausfuehrung_item_id for items that have a parent for (const item of vorlageItems.rows) { if (item.parent_item_id != null) { const parentAusfuehrungId = vorlageToAusfuehrungId.get(item.parent_item_id); const childAusfuehrungId = vorlageToAusfuehrungId.get(item.id); if (parentAusfuehrungId && childAusfuehrungId) { await client.query( `UPDATE checklist_ausfuehrung_items SET parent_ausfuehrung_item_id = $1 WHERE id = $2`, [parentAusfuehrungId, childAusfuehrungId] ); } } } // Copy entity-specific items (vehicle or equipment) if (ausruestungId) { const equipmentItems = await client.query( `SELECT * FROM ausruestung_checklist_items WHERE ausruestung_id = $1 AND aktiv = true ORDER BY sort_order ASC, id ASC`, [ausruestungId] ); for (const item of equipmentItems.rows) { await client.query( `INSERT INTO checklist_ausfuehrung_items (ausfuehrung_id, ausruestung_item_id, bezeichnung) VALUES ($1, $2, $3)`, [execution.id, item.id, item.bezeichnung] ); } } else if (fahrzeugId) { const vehicleItems = await client.query( `SELECT * FROM fahrzeug_checklist_items WHERE fahrzeug_id = $1 AND aktiv = true ORDER BY sort_order ASC, id ASC`, [fahrzeugId] ); for (const item of vehicleItems.rows) { await client.query( `INSERT INTO checklist_ausfuehrung_items (ausfuehrung_id, fahrzeug_item_id, bezeichnung) VALUES ($1, $2, $3)`, [execution.id, item.id, item.bezeichnung] ); } } await client.query('COMMIT'); // Fetch the complete execution with items return getExecutionById(execution.id); } catch (error) { await client.query('ROLLBACK').catch(() => {}); logger.error('ChecklistService.startExecution failed', { error, fahrzeugId, vorlageId }); throw new Error('Checklist-Ausführung konnte nicht gestartet werden'); } finally { client.release(); } } async function getExecutionById(id: string) { try { const execResult = await pool.query( `SELECT a.*, f.bezeichnung AS fahrzeug_name, f.kurzname AS fahrzeug_kurzname, ar.bezeichnung AS ausruestung_name, v.name AS vorlage_name, u1.name AS ausgefuehrt_von_name, u2.name AS freigegeben_von_name FROM checklist_ausfuehrungen a LEFT JOIN fahrzeuge f ON f.id = a.fahrzeug_id LEFT JOIN ausruestung ar ON ar.id = a.ausruestung_id LEFT JOIN checklist_vorlagen v ON v.id = a.vorlage_id LEFT JOIN users u1 ON u1.id = a.ausgefuehrt_von LEFT JOIN users u2 ON u2.id = a.freigegeben_von WHERE a.id = $1`, [id] ); if (execResult.rows.length === 0) return null; const execution = execResult.rows[0]; const itemsResult = await pool.query( `SELECT * FROM checklist_ausfuehrung_items WHERE ausfuehrung_id = $1 ORDER BY id ASC`, [id] ); execution.items = itemsResult.rows; return execution; } catch (error) { logger.error('ChecklistService.getExecutionById failed', { error, id }); throw new Error('Ausführung konnte nicht geladen werden'); } } async function submitExecution( id: string, items: Array<{ itemId: number; ergebnis: string; kommentar?: string }>, notizen: string | null, _userId: string, ) { const client = await pool.connect(); try { await client.query('BEGIN'); // Update each item's result for (const item of items) { await client.query( `UPDATE checklist_ausfuehrung_items SET ergebnis = $1, kommentar = $2 WHERE id = $3 AND ausfuehrung_id = $4`, [item.ergebnis, item.kommentar ?? null, item.itemId, id] ); } // Check if all pflicht items have ergebnis = 'ok' // Exclude parent items (those that have subitems) — their children carry the pflicht flag const pflichtCheck = await client.query( `SELECT ai.id, ai.ergebnis, ai.vorlage_item_id, ai.fahrzeug_item_id, ai.ausruestung_item_id FROM checklist_ausfuehrung_items ai LEFT JOIN checklist_vorlage_items vi ON vi.id = ai.vorlage_item_id LEFT JOIN fahrzeug_checklist_items fi ON fi.id = ai.fahrzeug_item_id LEFT JOIN ausruestung_checklist_items aci ON aci.id = ai.ausruestung_item_id WHERE ai.ausfuehrung_id = $1 AND (COALESCE(vi.pflicht, fi.pflicht, aci.pflicht, true) = true) AND NOT EXISTS ( SELECT 1 FROM checklist_ausfuehrung_items child WHERE child.parent_ausfuehrung_item_id = ai.id )`, [id] ); const allPflichtOk = pflichtCheck.rows.every((r: any) => r.ergebnis === 'ok'); const newStatus = allPflichtOk ? 'abgeschlossen' : 'unvollstaendig'; await client.query( `UPDATE checklist_ausfuehrungen SET status = $1, ausgefuehrt_am = NOW(), notizen = $2 WHERE id = $3`, [newStatus, notizen, id] ); // Update checklist_faelligkeit if completed if (allPflichtOk) { const exec = await client.query(`SELECT vorlage_id, fahrzeug_id, ausruestung_id FROM checklist_ausfuehrungen WHERE id = $1`, [id]); if (exec.rows.length > 0) { const { vorlage_id, fahrzeug_id, ausruestung_id } = exec.rows[0]; const vorlage = await client.query(`SELECT intervall, intervall_tage FROM checklist_vorlagen WHERE id = $1`, [vorlage_id]); if (vorlage.rows.length > 0) { const nextDue = calculateNextDueDate(vorlage.rows[0].intervall, vorlage.rows[0].intervall_tage); if (nextDue) { if (ausruestung_id) { await client.query( `INSERT INTO checklist_faelligkeit (ausruestung_id, vorlage_id, naechste_faellig_am, letzte_ausfuehrung_id) VALUES ($1, $2, $3, $4) ON CONFLICT (vorlage_id, ausruestung_id) WHERE ausruestung_id IS NOT NULL AND fahrzeug_id IS NULL DO UPDATE SET naechste_faellig_am = $3, letzte_ausfuehrung_id = $4`, [ausruestung_id, vorlage_id, nextDue, id] ); } else if (fahrzeug_id) { await client.query( `INSERT INTO checklist_faelligkeit (fahrzeug_id, vorlage_id, naechste_faellig_am, letzte_ausfuehrung_id) VALUES ($1, $2, $3, $4) ON CONFLICT (vorlage_id, fahrzeug_id) WHERE fahrzeug_id IS NOT NULL AND ausruestung_id IS NULL DO UPDATE SET naechste_faellig_am = $3, letzte_ausfuehrung_id = $4`, [fahrzeug_id, vorlage_id, nextDue, id] ); } } } } } await client.query('COMMIT'); return getExecutionById(id); } catch (error) { await client.query('ROLLBACK').catch(() => {}); logger.error('ChecklistService.submitExecution failed', { error, id }); throw new Error('Ausführung konnte nicht abgeschlossen werden'); } finally { client.release(); } } async function approveExecution(id: string, userId: string) { try { const result = await pool.query( `UPDATE checklist_ausfuehrungen SET status = 'freigegeben', freigegeben_von = $1, freigegeben_am = NOW() WHERE id = $2 AND status IN ('abgeschlossen', 'unvollstaendig') RETURNING *`, [userId, id] ); if (result.rows.length === 0) return null; return getExecutionById(id); } catch (error) { logger.error('ChecklistService.approveExecution failed', { error, id }); throw new Error('Freigabe konnte nicht erteilt werden'); } } async function getExecutions(filter?: { fahrzeugId?: string; ausruestungId?: string; vorlageId?: number; status?: string }) { try { const conditions: string[] = []; const values: any[] = []; let idx = 1; if (filter?.fahrzeugId) { conditions.push(`a.fahrzeug_id = $${idx}`); values.push(filter.fahrzeugId); idx++; } if (filter?.ausruestungId) { conditions.push(`a.ausruestung_id = $${idx}`); values.push(filter.ausruestungId); idx++; } if (filter?.vorlageId) { conditions.push(`a.vorlage_id = $${idx}`); values.push(filter.vorlageId); idx++; } if (filter?.status) { conditions.push(`a.status = $${idx}`); values.push(filter.status); idx++; } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await pool.query( `SELECT a.*, f.bezeichnung AS fahrzeug_name, f.kurzname AS fahrzeug_kurzname, ar.bezeichnung AS ausruestung_name, v.name AS vorlage_name, u1.name AS ausgefuehrt_von_name, u2.name AS freigegeben_von_name FROM checklist_ausfuehrungen a LEFT JOIN fahrzeuge f ON f.id = a.fahrzeug_id LEFT JOIN ausruestung ar ON ar.id = a.ausruestung_id LEFT JOIN checklist_vorlagen v ON v.id = a.vorlage_id LEFT JOIN users u1 ON u1.id = a.ausgefuehrt_von LEFT JOIN users u2 ON u2.id = a.freigegeben_von ${where} ORDER BY a.created_at DESC`, values ); return result.rows; } catch (error) { logger.error('ChecklistService.getExecutions failed', { error }); throw new Error('Ausführungen konnten nicht geladen werden'); } } // --------------------------------------------------------------------------- // Fälligkeiten (Due dates) // --------------------------------------------------------------------------- async function getOverdueChecklists() { try { const result = await pool.query(` SELECT cf.*, f.bezeichnung AS fahrzeug_name, f.kurzname AS fahrzeug_kurzname, ar.bezeichnung AS ausruestung_name, v.name AS vorlage_name FROM checklist_faelligkeit cf LEFT JOIN fahrzeuge f ON f.id = cf.fahrzeug_id AND f.deleted_at IS NULL LEFT JOIN ausruestung ar ON ar.id = cf.ausruestung_id JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true WHERE cf.naechste_faellig_am <= CURRENT_DATE AND (cf.fahrzeug_id IS NOT NULL OR cf.ausruestung_id IS NOT NULL) AND (cf.fahrzeug_id IS NULL OR f.id IS NOT NULL) ORDER BY cf.naechste_faellig_am ASC `); return result.rows; } catch (error) { logger.error('ChecklistService.getOverdueChecklists failed', { error }); throw new Error('Überfällige Checklisten konnten nicht geladen werden'); } } async function getDueChecklists(fahrzeugId: string) { try { const result = await pool.query( `SELECT cf.*, v.name AS vorlage_name, v.intervall, v.intervall_tage FROM checklist_faelligkeit cf JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true WHERE cf.fahrzeug_id = $1 ORDER BY cf.naechste_faellig_am ASC`, [fahrzeugId] ); return result.rows; } catch (error) { logger.error('ChecklistService.getDueChecklists failed', { error, fahrzeugId }); throw new Error('Fälligkeiten konnten nicht geladen werden'); } } export default { getVorlagen, getVorlageById, createVorlage, updateVorlage, deleteVorlage, getVorlageItems, addVorlageItem, updateVorlageItem, deleteVorlageItem, getVehicleItems, addVehicleItem, updateVehicleItem, deleteVehicleItem, getEquipmentItems, addEquipmentItem, updateEquipmentItem, deleteEquipmentItem, getTemplatesForVehicle, getTemplatesForEquipment, getOverviewItems, startExecution, getExecutionById, submitExecution, approveExecution, getExecutions, getOverdueChecklists, getDueChecklists, };