import pool from '../config/database'; import logger from '../utils/logger'; import { Ausruestung, AusruestungKategorie, AusruestungListItem, AusruestungDetail, AusruestungWartungslog, CreateAusruestungData, UpdateAusruestungData, CreateAusruestungWartungslogData, UpdateAusruestungWartungslogData, AusruestungStatus, EquipmentStats, VehicleEquipmentWarning, } from '../models/equipment.model'; class EquipmentService { // ========================================================================= // EQUIPMENT OVERVIEW // ========================================================================= async getAllEquipment(): Promise { try { const result = await pool.query(` SELECT * FROM ausruestung_mit_pruefstatus ORDER BY kategorie_kurzname, bezeichnung `); return result.rows.map((row) => ({ ...row, pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null ? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null, })) as AusruestungListItem[]; } catch (error) { logger.error('EquipmentService.getAllEquipment failed', { error }); throw new Error('Failed to fetch equipment'); } } // ========================================================================= // EQUIPMENT DETAIL // ========================================================================= async getEquipmentById(id: string): Promise { try { const equipmentResult = await pool.query( `SELECT * FROM ausruestung_mit_pruefstatus WHERE id = $1`, [id] ); if (equipmentResult.rows.length === 0) return null; const row = equipmentResult.rows[0]; const wartungslogResult = await pool.query( `SELECT * FROM ausruestung_wartungslog WHERE ausruestung_id = $1 ORDER BY datum DESC, created_at DESC`, [id] ); const equipment: AusruestungDetail = { ...row, pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null ? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null, wartungslog: wartungslogResult.rows.map(r => ({ ...r, kosten: r.kosten != null ? Number(r.kosten) : null, })) as AusruestungWartungslog[], }; return equipment; } catch (error) { logger.error('EquipmentService.getEquipmentById failed', { error, id }); throw new Error('Failed to fetch equipment'); } } // ========================================================================= // EQUIPMENT BY VEHICLE // ========================================================================= async getEquipmentByVehicle(fahrzeugId: string): Promise { try { const result = await pool.query( `SELECT * FROM ausruestung_mit_pruefstatus WHERE fahrzeug_id = $1 ORDER BY kategorie_kurzname, bezeichnung`, [fahrzeugId] ); return result.rows.map((row) => ({ ...row, pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null ? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null, })) as AusruestungListItem[]; } catch (error) { logger.error('EquipmentService.getEquipmentByVehicle failed', { error, fahrzeugId }); throw new Error('Failed to fetch equipment for vehicle'); } } // ========================================================================= // CATEGORIES // ========================================================================= async getCategories(): Promise { try { const result = await pool.query( `SELECT * FROM ausruestung_kategorien ORDER BY sortierung` ); return result.rows as AusruestungKategorie[]; } catch (error) { logger.error('EquipmentService.getCategories failed', { error }); throw new Error('Failed to fetch equipment categories'); } } async getCategoryById(id: string): Promise { try { const result = await pool.query( `SELECT * FROM ausruestung_kategorien WHERE id = $1`, [id] ); return result.rows.length > 0 ? (result.rows[0] as AusruestungKategorie) : null; } catch (error) { logger.error('EquipmentService.getCategoryById failed', { error, id }); throw new Error('Failed to fetch equipment category'); } } async createCategory(data: { name: string; kurzname: string; sortierung?: number; motorisiert?: boolean }): Promise { try { const result = await pool.query( `INSERT INTO ausruestung_kategorien (id, name, kurzname, sortierung, motorisiert) VALUES (uuid_generate_v4(), $1, $2, COALESCE($3, (SELECT COALESCE(MAX(sortierung),0)+1 FROM ausruestung_kategorien)), COALESCE($4, false)) RETURNING *`, [data.name, data.kurzname, data.sortierung ?? null, data.motorisiert ?? null] ); logger.info('Equipment category created', { id: result.rows[0].id, name: data.name }); return result.rows[0] as AusruestungKategorie; } catch (error) { logger.error('EquipmentService.createCategory failed', { error }); throw new Error('Failed to create equipment category'); } } async updateCategory(id: string, data: { name?: string; kurzname?: string; sortierung?: number; motorisiert?: boolean }): Promise { try { const fields: string[] = []; const values: unknown[] = []; let p = 1; if (data.name !== undefined) { fields.push(`name = $${p++}`); values.push(data.name); } if (data.kurzname !== undefined) { fields.push(`kurzname = $${p++}`); values.push(data.kurzname); } if (data.sortierung !== undefined) { fields.push(`sortierung = $${p++}`); values.push(data.sortierung); } if (data.motorisiert !== undefined) { fields.push(`motorisiert = $${p++}`); values.push(data.motorisiert); } if (fields.length === 0) throw new Error('No fields to update'); values.push(id); const result = await pool.query( `UPDATE ausruestung_kategorien SET ${fields.join(', ')} WHERE id = $${p} RETURNING *`, values ); if (result.rows.length === 0) return null; logger.info('Equipment category updated', { id }); return result.rows[0] as AusruestungKategorie; } catch (error) { logger.error('EquipmentService.updateCategory failed', { error, id }); throw error; } } async deleteCategory(id: string): Promise<{ deleted: boolean; error?: string }> { try { // Check if any equipment items reference this category const usage = await pool.query( `SELECT COUNT(*) AS cnt FROM ausruestung WHERE kategorie_id = $1 AND deleted_at IS NULL`, [id] ); const count = parseInt(usage.rows[0].cnt, 10); if (count > 0) { return { deleted: false, error: `Kategorie wird von ${count} Ausrüstungsgegenständen verwendet und kann nicht gelöscht werden.` }; } const result = await pool.query( `DELETE FROM ausruestung_kategorien WHERE id = $1 RETURNING id`, [id] ); if (result.rows.length === 0) { return { deleted: false, error: 'Kategorie nicht gefunden' }; } logger.info('Equipment category deleted', { id }); return { deleted: true }; } catch (error) { logger.error('EquipmentService.deleteCategory failed', { error, id }); throw new Error('Failed to delete equipment category'); } } // ========================================================================= // CRUD // ========================================================================= async createEquipment(data: CreateAusruestungData, createdBy: string): Promise { try { const result = await pool.query( `INSERT INTO ausruestung ( id, bezeichnung, kategorie_id, seriennummer, inventarnummer, hersteller, baujahr, status, status_bemerkung, ist_wichtig, fahrzeug_id, standort, pruef_intervall_monate, letzte_pruefung_am, naechste_pruefung_am, bemerkung ) VALUES (uuid_generate_v4(),$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING *`, [ data.bezeichnung, data.kategorie_id, data.seriennummer ?? null, data.inventarnummer ?? null, data.hersteller ?? null, data.baujahr ?? null, data.status ?? AusruestungStatus.Einsatzbereit, data.status_bemerkung ?? null, data.ist_wichtig ?? false, data.fahrzeug_id ?? null, data.standort ?? 'Lager', data.pruef_intervall_monate ?? null, data.letzte_pruefung_am ?? null, data.naechste_pruefung_am ?? null, data.bemerkung ?? null, ] ); const equipment = result.rows[0] as Ausruestung; logger.info('Equipment created', { id: equipment.id, by: createdBy }); return equipment; } catch (error) { logger.error('EquipmentService.createEquipment failed', { error, createdBy }); throw new Error('Failed to create equipment'); } } async updateEquipment(id: string, data: UpdateAusruestungData, updatedBy: string): Promise { try { const fields: string[] = []; const values: unknown[] = []; let p = 1; const addField = (col: string, value: unknown) => { fields.push(`${col} = $${p++}`); values.push(value); }; if (data.bezeichnung !== undefined) addField('bezeichnung', data.bezeichnung); if (data.kategorie_id !== undefined) addField('kategorie_id', data.kategorie_id); if (data.seriennummer !== undefined) addField('seriennummer', data.seriennummer); if (data.inventarnummer !== undefined) addField('inventarnummer', data.inventarnummer); if (data.hersteller !== undefined) addField('hersteller', data.hersteller); if (data.baujahr !== undefined) addField('baujahr', data.baujahr); if (data.status !== undefined) addField('status', data.status); if (data.status_bemerkung !== undefined) addField('status_bemerkung', data.status_bemerkung); if (data.ist_wichtig !== undefined) addField('ist_wichtig', data.ist_wichtig); if (data.fahrzeug_id !== undefined) addField('fahrzeug_id', data.fahrzeug_id); if (data.standort !== undefined) addField('standort', data.standort); if (data.pruef_intervall_monate !== undefined) addField('pruef_intervall_monate', data.pruef_intervall_monate); if (data.letzte_pruefung_am !== undefined) addField('letzte_pruefung_am', data.letzte_pruefung_am); if (data.naechste_pruefung_am !== undefined) addField('naechste_pruefung_am', data.naechste_pruefung_am); if (data.bemerkung !== undefined) addField('bemerkung', data.bemerkung); if (fields.length === 0) { throw new Error('No fields to update'); } values.push(id); const result = await pool.query( `UPDATE ausruestung SET ${fields.join(', ')} WHERE id = $${p} AND deleted_at IS NULL RETURNING *`, values ); if (result.rows.length === 0) { return null; } const equipment = result.rows[0] as Ausruestung; logger.info('Equipment updated', { id, by: updatedBy }); return equipment; } catch (error) { logger.error('EquipmentService.updateEquipment failed', { error, id, updatedBy }); throw error; } } async deleteEquipment(id: string, deletedBy: string): Promise { try { const result = await pool.query( `UPDATE ausruestung SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL RETURNING id`, [id] ); if (result.rows.length === 0) { return false; } logger.info('Equipment soft-deleted', { id, by: deletedBy }); return true; } catch (error) { logger.error('EquipmentService.deleteEquipment failed', { error, id }); throw error; } } // ========================================================================= // STATUS MANAGEMENT // ========================================================================= async updateStatus( id: string, status: AusruestungStatus, bemerkung: string, updatedBy: string ): Promise { try { // Get old status for history const oldResult = await pool.query( `SELECT status FROM ausruestung WHERE id = $1 AND deleted_at IS NULL`, [id] ); const oldStatus = oldResult.rows[0]?.status; const result = await pool.query( `UPDATE ausruestung SET status = $1, status_bemerkung = $2, updated_at = NOW() WHERE id = $3 AND deleted_at IS NULL RETURNING id`, [status, bemerkung || null, id] ); if (result.rows.length === 0) { throw new Error('Equipment not found'); } // Record status change history if (oldStatus && oldStatus !== status) { await pool.query( `INSERT INTO ausruestung_status_historie (ausruestung_id, alter_status, neuer_status, bemerkung, geaendert_von) VALUES ($1, $2, $3, $4, $5)`, [id, oldStatus, status, bemerkung || null, updatedBy] ); } logger.info('Equipment status updated', { id, status, by: updatedBy }); } catch (error) { logger.error('EquipmentService.updateStatus failed', { error, id }); throw error; } } // ========================================================================= // MAINTENANCE LOG // ========================================================================= async addWartungslog( equipmentId: string, data: CreateAusruestungWartungslogData, createdBy: string ): Promise { try { const check = await pool.query( `SELECT 1 FROM ausruestung WHERE id = $1 AND deleted_at IS NULL`, [equipmentId] ); if (check.rows.length === 0) { throw new Error('Equipment not found'); } const result = await pool.query( `INSERT INTO ausruestung_wartungslog ( ausruestung_id, datum, art, beschreibung, ergebnis, kosten, pruefende_stelle, dokument_url, erfasst_von ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`, [ equipmentId, data.datum, data.art, data.beschreibung, data.ergebnis ?? null, data.kosten ?? null, data.pruefende_stelle ?? null, data.dokument_url ?? null, createdBy, ] ); const entry = result.rows[0] as AusruestungWartungslog; logger.info('Equipment wartungslog entry added', { entryId: entry.id, equipmentId, by: createdBy }); // Auto-update next inspection date on the equipment when result is 'bestanden' if (data.ergebnis === 'bestanden' && data.naechste_pruefung_am) { await pool.query( `UPDATE ausruestung SET naechste_pruefung_am = $1, letzte_pruefung_am = $2 WHERE id = $3`, [data.naechste_pruefung_am, data.datum, equipmentId] ); } return entry; } catch (error) { logger.error('EquipmentService.addWartungslog failed', { error, equipmentId }); throw error; } } // ========================================================================= // DASHBOARD KPI // ========================================================================= async getEquipmentStats(): Promise { try { const result = await pool.query(` SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE status = 'einsatzbereit') AS einsatzbereit, COUNT(*) FILTER (WHERE status = 'beschaedigt') AS beschaedigt, COUNT(*) FILTER (WHERE status = 'in_wartung') AS in_wartung, COUNT(*) FILTER (WHERE status = 'ausser_dienst') AS ausser_dienst, COUNT(*) FILTER ( WHERE naechste_pruefung_am IS NOT NULL AND naechste_pruefung_am::date - CURRENT_DATE BETWEEN 0 AND 30 ) AS inspections_due, COUNT(*) FILTER ( WHERE naechste_pruefung_am IS NOT NULL AND naechste_pruefung_am::date < CURRENT_DATE ) AS inspections_overdue, COUNT(*) FILTER ( WHERE ist_wichtig = TRUE AND status != 'einsatzbereit' ) AS wichtig_nicht_bereit FROM ausruestung WHERE deleted_at IS NULL `); const row = result.rows[0] ?? {}; return { total: parseInt(row.total ?? '0', 10), einsatzbereit: parseInt(row.einsatzbereit ?? '0', 10), beschaedigt: parseInt(row.beschaedigt ?? '0', 10), inWartung: parseInt(row.in_wartung ?? '0', 10), ausserDienst: parseInt(row.ausser_dienst ?? '0', 10), inspectionsDue: parseInt(row.inspections_due ?? '0', 10), inspectionsOverdue: parseInt(row.inspections_overdue ?? '0', 10), wichtigNichtBereit: parseInt(row.wichtig_nicht_bereit ?? '0', 10), }; } catch (error) { logger.error('EquipmentService.getEquipmentStats failed', { error }); throw new Error('Failed to fetch equipment stats'); } } // ========================================================================= // VEHICLE WARNINGS // ========================================================================= async getVehicleWarnings(): Promise { try { const result = await pool.query(` SELECT a.fahrzeug_id, a.id AS ausruestung_id, a.bezeichnung, a.status, k.name AS kategorie_name FROM ausruestung a JOIN ausruestung_kategorien k ON k.id = a.kategorie_id WHERE a.ist_wichtig = TRUE AND a.fahrzeug_id IS NOT NULL AND a.status != 'einsatzbereit' AND a.deleted_at IS NULL ORDER BY a.fahrzeug_id `); return result.rows as VehicleEquipmentWarning[]; } catch (error) { logger.error('EquipmentService.getVehicleWarnings failed', { error }); throw new Error('Failed to fetch vehicle equipment warnings'); } } // ========================================================================= // UPCOMING INSPECTIONS // ========================================================================= async getUpcomingInspections(daysAhead: number = 30): Promise { try { const result = await pool.query( `SELECT * FROM ausruestung_mit_pruefstatus WHERE pruefung_tage_bis_faelligkeit IS NOT NULL AND pruefung_tage_bis_faelligkeit <= $1 ORDER BY pruefung_tage_bis_faelligkeit ASC`, [daysAhead] ); return result.rows.map((row) => ({ ...row, pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null ? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null, })) as AusruestungListItem[]; } catch (error) { logger.error('EquipmentService.getUpcomingInspections failed', { error, daysAhead }); throw new Error('Failed to fetch upcoming inspections'); } } // ========================================================================= // STATUS HISTORY // ========================================================================= async getStatusHistory(equipmentId: string) { try { const result = await pool.query( `SELECT h.*, COALESCE(u.name, u.preferred_username, u.email) AS geaendert_von_name FROM ausruestung_status_historie h LEFT JOIN users u ON u.id = h.geaendert_von WHERE h.ausruestung_id = $1 ORDER BY h.erstellt_am DESC LIMIT 50`, [equipmentId] ); return result.rows; } catch (error) { logger.error('EquipmentService.getStatusHistory failed', { error, equipmentId }); throw new Error('Status-Historie konnte nicht geladen werden'); } } // ========================================================================= // WARTUNGSLOG UPDATE // ========================================================================= async updateWartungslog( equipmentId: string, wartungId: number, data: UpdateAusruestungWartungslogData, updatedBy: string ): Promise { try { // Verify the wartung entry belongs to this equipment const check = await pool.query( `SELECT id FROM ausruestung_wartungslog WHERE id = $1 AND ausruestung_id = $2`, [wartungId, equipmentId] ); if (check.rows.length === 0) { throw new Error('Wartungseintrag nicht gefunden'); } const fields: string[] = []; const values: unknown[] = []; let p = 1; const addField = (col: string, value: unknown) => { fields.push(`${col} = $${p++}`); values.push(value); }; if (data.datum !== undefined) addField('datum', data.datum); if (data.art !== undefined) addField('art', data.art); if (data.beschreibung !== undefined) addField('beschreibung', data.beschreibung); if (data.ergebnis !== undefined) addField('ergebnis', data.ergebnis); if (data.kosten !== undefined) addField('kosten', data.kosten); if (data.pruefende_stelle !== undefined) addField('pruefende_stelle', data.pruefende_stelle); if (fields.length === 0) { throw new Error('No fields to update'); } values.push(wartungId); const result = await pool.query( `UPDATE ausruestung_wartungslog SET ${fields.join(', ')} WHERE id = $${p} RETURNING *`, values ); const entry = result.rows[0] as AusruestungWartungslog; // Auto-update next inspection date on the equipment when result is 'bestanden' if (data.ergebnis === 'bestanden' && data.naechste_pruefung_am) { await pool.query( `UPDATE ausruestung SET naechste_pruefung_am = $1, letzte_pruefung_am = $2 WHERE id = $3`, [data.naechste_pruefung_am, data.datum ?? entry.datum, equipmentId] ); } logger.info('Equipment wartungslog entry updated', { wartungId, equipmentId, by: updatedBy }); return entry; } catch (error) { logger.error('EquipmentService.updateWartungslog failed', { error, wartungId, equipmentId }); throw error; } } // ========================================================================= // WARTUNGSLOG FILE UPLOAD // ========================================================================= async updateWartungslogFile(wartungId: number, filePath: string) { try { const result = await pool.query( `UPDATE ausruestung_wartungslog SET dokument_url = $1 WHERE id = $2 RETURNING *`, [filePath, wartungId] ); if (result.rows.length === 0) { throw new Error('Wartungseintrag nicht gefunden'); } return result.rows[0]; } catch (error) { logger.error('EquipmentService.updateWartungslogFile failed', { error, wartungId }); throw error; } } } export default new EquipmentService();