refine vehicle freatures

This commit is contained in:
Matthias Hochmeister
2026-02-28 17:19:18 +01:00
parent 0e81eabda6
commit e2be29c712
17 changed files with 4071 additions and 117 deletions

View File

@@ -0,0 +1,414 @@
import pool from '../config/database';
import logger from '../utils/logger';
import {
Ausruestung,
AusruestungKategorie,
AusruestungListItem,
AusruestungDetail,
AusruestungWartungslog,
CreateAusruestungData,
UpdateAusruestungData,
CreateAusruestungWartungslogData,
AusruestungStatus,
EquipmentStats,
VehicleEquipmentWarning,
} from '../models/equipment.model';
class EquipmentService {
// =========================================================================
// EQUIPMENT OVERVIEW
// =========================================================================
async getAllEquipment(): Promise<AusruestungListItem[]> {
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<AusruestungDetail | null> {
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<AusruestungListItem[]> {
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<AusruestungKategorie[]> {
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');
}
}
// =========================================================================
// CRUD
// =========================================================================
async createEquipment(data: CreateAusruestungData, createdBy: string): Promise<Ausruestung> {
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<Ausruestung | null> {
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<boolean> {
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<void> {
try {
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');
}
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<AusruestungWartungslog> {
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 });
return entry;
} catch (error) {
logger.error('EquipmentService.addWartungslog failed', { error, equipmentId });
throw error;
}
}
// =========================================================================
// DASHBOARD KPI
// =========================================================================
async getEquipmentStats(): Promise<EquipmentStats> {
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<VehicleEquipmentWarning[]> {
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<AusruestungListItem[]> {
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');
}
}
}
export default new EquipmentService();