615 lines
22 KiB
TypeScript
615 lines
22 KiB
TypeScript
import pool from '../config/database';
|
|
import logger from '../utils/logger';
|
|
import {
|
|
Fahrzeug,
|
|
FahrzeugListItem,
|
|
FahrzeugWithPruefstatus,
|
|
FahrzeugPruefung,
|
|
FahrzeugWartungslog,
|
|
CreateFahrzeugData,
|
|
UpdateFahrzeugData,
|
|
CreatePruefungData,
|
|
CreateWartungslogData,
|
|
FahrzeugStatus,
|
|
PruefungArt,
|
|
PruefungIntervalMonths,
|
|
VehicleStats,
|
|
InspectionAlert,
|
|
} from '../models/vehicle.model';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: add N months to a Date (handles month-end edge cases)
|
|
// ---------------------------------------------------------------------------
|
|
function addMonths(date: Date, months: number): Date {
|
|
const result = new Date(date);
|
|
result.setMonth(result.getMonth() + months);
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: map a flat view row to PruefungStatus sub-object
|
|
// ---------------------------------------------------------------------------
|
|
function mapPruefungStatus(row: any, prefix: string) {
|
|
return {
|
|
pruefung_id: row[`${prefix}_pruefung_id`] ?? null,
|
|
faellig_am: row[`${prefix}_faellig_am`] ?? null,
|
|
tage_bis_faelligkeit: row[`${prefix}_tage_bis_faelligkeit`] != null
|
|
? parseInt(row[`${prefix}_tage_bis_faelligkeit`], 10)
|
|
: null,
|
|
ergebnis: row[`${prefix}_ergebnis`] ?? null,
|
|
};
|
|
}
|
|
|
|
class VehicleService {
|
|
// =========================================================================
|
|
// FLEET OVERVIEW
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Returns all vehicles with their next-due inspection dates per type.
|
|
* Used by the fleet overview grid (FahrzeugListItem[]).
|
|
*/
|
|
async getAllVehicles(): Promise<FahrzeugListItem[]> {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
id,
|
|
bezeichnung,
|
|
kurzname,
|
|
amtliches_kennzeichen,
|
|
baujahr,
|
|
hersteller,
|
|
besatzung_soll,
|
|
status,
|
|
status_bemerkung,
|
|
bild_url,
|
|
paragraph57a_faellig_am,
|
|
paragraph57a_tage_bis_faelligkeit,
|
|
naechste_wartung_am,
|
|
wartung_tage_bis_faelligkeit,
|
|
hu_faellig_am,
|
|
hu_tage_bis_faelligkeit,
|
|
au_faellig_am,
|
|
au_tage_bis_faelligkeit,
|
|
uvv_faellig_am,
|
|
uvv_tage_bis_faelligkeit,
|
|
leiter_faellig_am,
|
|
leiter_tage_bis_faelligkeit,
|
|
naechste_pruefung_tage
|
|
FROM fahrzeuge_mit_pruefstatus
|
|
ORDER BY bezeichnung ASC
|
|
`);
|
|
|
|
return result.rows.map((row) => ({
|
|
...row,
|
|
paragraph57a_tage_bis_faelligkeit: row.paragraph57a_tage_bis_faelligkeit != null
|
|
? parseInt(row.paragraph57a_tage_bis_faelligkeit, 10) : null,
|
|
wartung_tage_bis_faelligkeit: row.wartung_tage_bis_faelligkeit != null
|
|
? parseInt(row.wartung_tage_bis_faelligkeit, 10) : null,
|
|
hu_tage_bis_faelligkeit: row.hu_tage_bis_faelligkeit != null
|
|
? parseInt(row.hu_tage_bis_faelligkeit, 10) : null,
|
|
au_tage_bis_faelligkeit: row.au_tage_bis_faelligkeit != null
|
|
? parseInt(row.au_tage_bis_faelligkeit, 10) : null,
|
|
uvv_tage_bis_faelligkeit: row.uvv_tage_bis_faelligkeit != null
|
|
? parseInt(row.uvv_tage_bis_faelligkeit, 10) : null,
|
|
leiter_tage_bis_faelligkeit: row.leiter_tage_bis_faelligkeit != null
|
|
? parseInt(row.leiter_tage_bis_faelligkeit, 10) : null,
|
|
naechste_pruefung_tage: row.naechste_pruefung_tage != null
|
|
? parseInt(row.naechste_pruefung_tage, 10) : null,
|
|
})) as FahrzeugListItem[];
|
|
} catch (error) {
|
|
logger.error('VehicleService.getAllVehicles failed', { error });
|
|
throw new Error('Failed to fetch vehicles');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// VEHICLE DETAIL
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Returns a single vehicle with full pruefstatus, inspection history,
|
|
* and maintenance log.
|
|
*/
|
|
async getVehicleById(id: string): Promise<FahrzeugWithPruefstatus | null> {
|
|
try {
|
|
// 1) Main record + inspection status from view
|
|
const vehicleResult = await pool.query(
|
|
`SELECT * FROM fahrzeuge_mit_pruefstatus WHERE id = $1`,
|
|
[id]
|
|
);
|
|
|
|
if (vehicleResult.rows.length === 0) return null;
|
|
|
|
const row = vehicleResult.rows[0];
|
|
|
|
// 2) Full inspection history
|
|
const pruefungenResult = await pool.query(
|
|
`SELECT * FROM fahrzeug_pruefungen
|
|
WHERE fahrzeug_id = $1
|
|
ORDER BY faellig_am DESC, created_at DESC`,
|
|
[id]
|
|
);
|
|
|
|
// 3) Maintenance log
|
|
const wartungslogResult = await pool.query(
|
|
`SELECT * FROM fahrzeug_wartungslog
|
|
WHERE fahrzeug_id = $1
|
|
ORDER BY datum DESC, created_at DESC`,
|
|
[id]
|
|
);
|
|
|
|
const vehicle: FahrzeugWithPruefstatus = {
|
|
id: row.id,
|
|
bezeichnung: row.bezeichnung,
|
|
kurzname: row.kurzname,
|
|
amtliches_kennzeichen: row.amtliches_kennzeichen,
|
|
fahrgestellnummer: row.fahrgestellnummer,
|
|
baujahr: row.baujahr,
|
|
hersteller: row.hersteller,
|
|
typ_schluessel: row.typ_schluessel,
|
|
besatzung_soll: row.besatzung_soll,
|
|
status: row.status as FahrzeugStatus,
|
|
status_bemerkung: row.status_bemerkung,
|
|
standort: row.standort,
|
|
bild_url: row.bild_url,
|
|
paragraph57a_faellig_am: row.paragraph57a_faellig_am ?? null,
|
|
naechste_wartung_am: row.naechste_wartung_am ?? null,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at,
|
|
pruefstatus: {
|
|
hu: mapPruefungStatus(row, 'hu'),
|
|
au: mapPruefungStatus(row, 'au'),
|
|
uvv: mapPruefungStatus(row, 'uvv'),
|
|
leiter: mapPruefungStatus(row, 'leiter'),
|
|
},
|
|
paragraph57a_tage_bis_faelligkeit: row.paragraph57a_tage_bis_faelligkeit != null
|
|
? parseInt(row.paragraph57a_tage_bis_faelligkeit, 10) : null,
|
|
wartung_tage_bis_faelligkeit: row.wartung_tage_bis_faelligkeit != null
|
|
? parseInt(row.wartung_tage_bis_faelligkeit, 10) : null,
|
|
naechste_pruefung_tage: row.naechste_pruefung_tage != null
|
|
? parseInt(row.naechste_pruefung_tage, 10) : null,
|
|
pruefungen: pruefungenResult.rows as FahrzeugPruefung[],
|
|
wartungslog: wartungslogResult.rows as FahrzeugWartungslog[],
|
|
};
|
|
|
|
return vehicle;
|
|
} catch (error) {
|
|
logger.error('VehicleService.getVehicleById failed', { error, id });
|
|
throw new Error('Failed to fetch vehicle');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// CRUD
|
|
// =========================================================================
|
|
|
|
async createVehicle(
|
|
data: CreateFahrzeugData,
|
|
createdBy: string
|
|
): Promise<Fahrzeug> {
|
|
try {
|
|
const result = await pool.query(
|
|
`INSERT INTO fahrzeuge (
|
|
bezeichnung, kurzname, amtliches_kennzeichen, fahrgestellnummer,
|
|
baujahr, hersteller, typ_schluessel, besatzung_soll,
|
|
status, status_bemerkung, standort, bild_url,
|
|
paragraph57a_faellig_am, naechste_wartung_am
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
|
|
RETURNING *`,
|
|
[
|
|
data.bezeichnung,
|
|
data.kurzname ?? null,
|
|
data.amtliches_kennzeichen ?? null,
|
|
data.fahrgestellnummer ?? null,
|
|
data.baujahr ?? null,
|
|
data.hersteller ?? null,
|
|
data.typ_schluessel ?? null,
|
|
data.besatzung_soll ?? null,
|
|
data.status ?? FahrzeugStatus.Einsatzbereit,
|
|
data.status_bemerkung ?? null,
|
|
data.standort ?? 'Feuerwehrhaus',
|
|
data.bild_url ?? null,
|
|
data.paragraph57a_faellig_am ?? null,
|
|
data.naechste_wartung_am ?? null,
|
|
]
|
|
);
|
|
|
|
const vehicle = result.rows[0] as Fahrzeug;
|
|
logger.info('Vehicle created', { id: vehicle.id, by: createdBy });
|
|
return vehicle;
|
|
} catch (error) {
|
|
logger.error('VehicleService.createVehicle failed', { error, createdBy });
|
|
throw new Error('Failed to create vehicle');
|
|
}
|
|
}
|
|
|
|
async updateVehicle(
|
|
id: string,
|
|
data: UpdateFahrzeugData,
|
|
updatedBy: string
|
|
): Promise<Fahrzeug> {
|
|
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.kurzname !== undefined) addField('kurzname', data.kurzname);
|
|
if (data.amtliches_kennzeichen !== undefined) addField('amtliches_kennzeichen', data.amtliches_kennzeichen);
|
|
if (data.fahrgestellnummer !== undefined) addField('fahrgestellnummer', data.fahrgestellnummer);
|
|
if (data.baujahr !== undefined) addField('baujahr', data.baujahr);
|
|
if (data.hersteller !== undefined) addField('hersteller', data.hersteller);
|
|
if (data.typ_schluessel !== undefined) addField('typ_schluessel', data.typ_schluessel);
|
|
if (data.besatzung_soll !== undefined) addField('besatzung_soll', data.besatzung_soll);
|
|
if (data.status !== undefined) addField('status', data.status);
|
|
if (data.status_bemerkung !== undefined) addField('status_bemerkung', data.status_bemerkung);
|
|
if (data.standort !== undefined) addField('standort', data.standort);
|
|
if (data.bild_url !== undefined) addField('bild_url', data.bild_url);
|
|
if (data.paragraph57a_faellig_am !== undefined) addField('paragraph57a_faellig_am', data.paragraph57a_faellig_am);
|
|
if (data.naechste_wartung_am !== undefined) addField('naechste_wartung_am', data.naechste_wartung_am);
|
|
|
|
if (fields.length === 0) {
|
|
throw new Error('No fields to update');
|
|
}
|
|
|
|
values.push(id); // for WHERE clause
|
|
const result = await pool.query(
|
|
`UPDATE fahrzeuge SET ${fields.join(', ')} WHERE id = $${p} RETURNING *`,
|
|
values
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Vehicle not found');
|
|
}
|
|
|
|
const vehicle = result.rows[0] as Fahrzeug;
|
|
logger.info('Vehicle updated', { id, by: updatedBy });
|
|
return vehicle;
|
|
} catch (error) {
|
|
logger.error('VehicleService.updateVehicle failed', { error, id, updatedBy });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async deleteVehicle(id: string, deletedBy: string): Promise<void> {
|
|
try {
|
|
const result = await pool.query(
|
|
`DELETE FROM fahrzeuge WHERE id = $1 RETURNING id`,
|
|
[id]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Vehicle not found');
|
|
}
|
|
|
|
logger.info('Vehicle deleted', { id, by: deletedBy });
|
|
} catch (error) {
|
|
logger.error('VehicleService.deleteVehicle failed', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// STATUS MANAGEMENT
|
|
// Socket.io-ready: accepts optional `io` parameter.
|
|
// In Tier 3, pass the real Socket.IO server instance here.
|
|
// The endpoint contract is: PATCH /api/vehicles/:id/status
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Updates vehicle status and optionally broadcasts a Socket.IO event.
|
|
*
|
|
* Socket.IO integration (Tier 3):
|
|
* Pass the live `io` instance from server.ts. When provided, emits:
|
|
* event: 'vehicle:statusChanged'
|
|
* payload: { vehicleId, bezeichnung, oldStatus, newStatus, bemerkung, updatedBy, timestamp }
|
|
* All connected clients on the default namespace receive the update immediately.
|
|
*
|
|
* @param io - Optional Socket.IO server instance (injected from app layer in Tier 3)
|
|
*/
|
|
async updateVehicleStatus(
|
|
id: string,
|
|
status: FahrzeugStatus,
|
|
bemerkung: string,
|
|
updatedBy: string,
|
|
io?: any
|
|
): Promise<void> {
|
|
try {
|
|
// Fetch old status for Socket.IO payload and logging
|
|
const oldResult = await pool.query(
|
|
`SELECT bezeichnung, status FROM fahrzeuge WHERE id = $1`,
|
|
[id]
|
|
);
|
|
|
|
if (oldResult.rows.length === 0) {
|
|
throw new Error('Vehicle not found');
|
|
}
|
|
|
|
const { bezeichnung, status: oldStatus } = oldResult.rows[0];
|
|
|
|
await pool.query(
|
|
`UPDATE fahrzeuge
|
|
SET status = $1, status_bemerkung = $2
|
|
WHERE id = $3`,
|
|
[status, bemerkung || null, id]
|
|
);
|
|
|
|
logger.info('Vehicle status updated', {
|
|
id,
|
|
from: oldStatus,
|
|
to: status,
|
|
by: updatedBy,
|
|
});
|
|
|
|
// ── Socket.IO broadcast (Tier 3 integration point) ──────────────────
|
|
// When `io` is provided (Tier 3), broadcast the status change to all
|
|
// connected dashboard clients so the live status board updates in real time.
|
|
if (io) {
|
|
const payload = {
|
|
vehicleId: id,
|
|
bezeichnung,
|
|
oldStatus,
|
|
newStatus: status,
|
|
bemerkung: bemerkung || null,
|
|
updatedBy,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
io.emit('vehicle:statusChanged', payload);
|
|
logger.debug('Emitted vehicle:statusChanged via Socket.IO', { vehicleId: id });
|
|
}
|
|
} catch (error) {
|
|
logger.error('VehicleService.updateVehicleStatus failed', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// INSPECTIONS
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Records a new inspection entry.
|
|
* Automatically calculates naechste_faelligkeit based on standard intervals
|
|
* when durchgefuehrt_am is provided and the art has a known interval.
|
|
*/
|
|
async addPruefung(
|
|
fahrzeugId: string,
|
|
data: CreatePruefungData,
|
|
createdBy: string
|
|
): Promise<FahrzeugPruefung> {
|
|
try {
|
|
// Auto-calculate naechste_faelligkeit
|
|
let naechsteFaelligkeit: string | null = null;
|
|
|
|
if (data.durchgefuehrt_am) {
|
|
const intervalMonths = PruefungIntervalMonths[data.pruefung_art];
|
|
if (intervalMonths !== undefined) {
|
|
const durchgefuehrt = new Date(data.durchgefuehrt_am);
|
|
naechsteFaelligkeit = addMonths(durchgefuehrt, intervalMonths)
|
|
.toISOString()
|
|
.split('T')[0];
|
|
}
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO fahrzeug_pruefungen (
|
|
fahrzeug_id, pruefung_art, faellig_am, durchgefuehrt_am,
|
|
ergebnis, naechste_faelligkeit, pruefende_stelle,
|
|
kosten, dokument_url, bemerkung, erfasst_von
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
|
RETURNING *`,
|
|
[
|
|
fahrzeugId,
|
|
data.pruefung_art,
|
|
data.faellig_am,
|
|
data.durchgefuehrt_am ?? null,
|
|
data.ergebnis ?? 'ausstehend',
|
|
naechsteFaelligkeit,
|
|
data.pruefende_stelle ?? null,
|
|
data.kosten ?? null,
|
|
data.dokument_url ?? null,
|
|
data.bemerkung ?? null,
|
|
createdBy,
|
|
]
|
|
);
|
|
|
|
const pruefung = result.rows[0] as FahrzeugPruefung;
|
|
logger.info('Pruefung added', {
|
|
pruefungId: pruefung.id,
|
|
fahrzeugId,
|
|
art: data.pruefung_art,
|
|
by: createdBy,
|
|
});
|
|
return pruefung;
|
|
} catch (error) {
|
|
logger.error('VehicleService.addPruefung failed', { error, fahrzeugId });
|
|
throw new Error('Failed to add inspection record');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the full inspection history for a specific vehicle,
|
|
* ordered newest-first.
|
|
*/
|
|
async getPruefungenForVehicle(fahrzeugId: string): Promise<FahrzeugPruefung[]> {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT * FROM fahrzeug_pruefungen
|
|
WHERE fahrzeug_id = $1
|
|
ORDER BY faellig_am DESC, created_at DESC`,
|
|
[fahrzeugId]
|
|
);
|
|
return result.rows.map(r => ({
|
|
...r,
|
|
kosten: r.kosten != null ? Number(r.kosten) : null,
|
|
})) as FahrzeugPruefung[];
|
|
} catch (error) {
|
|
logger.error('VehicleService.getPruefungenForVehicle failed', { error, fahrzeugId });
|
|
throw new Error('Failed to fetch inspection history');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns all upcoming or overdue inspections within the given lookahead window.
|
|
* Used by the dashboard InspectionAlerts panel.
|
|
*
|
|
* @param daysAhead - How many days into the future to look (e.g. 30).
|
|
* Pass a very large number (e.g. 9999) to include all overdue too.
|
|
*/
|
|
async getUpcomingInspections(daysAhead: number): Promise<InspectionAlert[]> {
|
|
try {
|
|
// We include already-overdue inspections (tage < 0) AND upcoming within window.
|
|
// Only open (not yet completed) inspections are relevant.
|
|
const result = await pool.query(
|
|
`SELECT
|
|
p.id AS pruefung_id,
|
|
p.fahrzeug_id,
|
|
p.pruefung_art,
|
|
p.faellig_am,
|
|
(p.faellig_am::date - CURRENT_DATE) AS tage,
|
|
f.bezeichnung,
|
|
f.kurzname
|
|
FROM fahrzeug_pruefungen p
|
|
JOIN fahrzeuge f ON f.id = p.fahrzeug_id
|
|
WHERE
|
|
p.durchgefuehrt_am IS NULL
|
|
AND (p.faellig_am::date - CURRENT_DATE) <= $1
|
|
ORDER BY p.faellig_am ASC`,
|
|
[daysAhead]
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
fahrzeugId: row.fahrzeug_id,
|
|
bezeichnung: row.bezeichnung,
|
|
kurzname: row.kurzname,
|
|
pruefungId: row.pruefung_id,
|
|
pruefungArt: row.pruefung_art as PruefungArt,
|
|
faelligAm: row.faellig_am,
|
|
tage: parseInt(row.tage, 10),
|
|
})) as InspectionAlert[];
|
|
} catch (error) {
|
|
logger.error('VehicleService.getUpcomingInspections failed', { error, daysAhead });
|
|
throw new Error('Failed to fetch inspection alerts');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// MAINTENANCE LOG
|
|
// =========================================================================
|
|
|
|
async addWartungslog(
|
|
fahrzeugId: string,
|
|
data: CreateWartungslogData,
|
|
createdBy: string
|
|
): Promise<FahrzeugWartungslog> {
|
|
try {
|
|
const result = await pool.query(
|
|
`INSERT INTO fahrzeug_wartungslog (
|
|
fahrzeug_id, datum, art, beschreibung,
|
|
km_stand, kraftstoff_liter, kosten, externe_werkstatt, erfasst_von
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
RETURNING *`,
|
|
[
|
|
fahrzeugId,
|
|
data.datum,
|
|
data.art ?? null,
|
|
data.beschreibung,
|
|
data.km_stand ?? null,
|
|
data.kraftstoff_liter ?? null,
|
|
data.kosten ?? null,
|
|
data.externe_werkstatt ?? null,
|
|
createdBy,
|
|
]
|
|
);
|
|
|
|
const entry = result.rows[0] as FahrzeugWartungslog;
|
|
logger.info('Wartungslog entry added', {
|
|
entryId: entry.id,
|
|
fahrzeugId,
|
|
by: createdBy,
|
|
});
|
|
return entry;
|
|
} catch (error) {
|
|
logger.error('VehicleService.addWartungslog failed', { error, fahrzeugId });
|
|
throw new Error('Failed to add maintenance log entry');
|
|
}
|
|
}
|
|
|
|
async getWartungslogForVehicle(fahrzeugId: string): Promise<FahrzeugWartungslog[]> {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT * FROM fahrzeug_wartungslog
|
|
WHERE fahrzeug_id = $1
|
|
ORDER BY datum DESC, created_at DESC`,
|
|
[fahrzeugId]
|
|
);
|
|
return result.rows.map(r => ({
|
|
...r,
|
|
kosten: r.kosten != null ? Number(r.kosten) : null,
|
|
})) as FahrzeugWartungslog[];
|
|
} catch (error) {
|
|
logger.error('VehicleService.getWartungslogForVehicle failed', { error, fahrzeugId });
|
|
throw new Error('Failed to fetch maintenance log');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// DASHBOARD KPI
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Returns aggregate counts for the dashboard stats strip.
|
|
* inspectionsDue = vehicles with at least one inspection due within 30 days
|
|
* inspectionsOverdue = vehicles with at least one inspection already overdue
|
|
*/
|
|
async getVehicleStats(): Promise<VehicleStats> {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE status = 'einsatzbereit') AS einsatzbereit,
|
|
COUNT(*) FILTER (
|
|
WHERE status IN ('ausser_dienst_wartung','ausser_dienst_schaden')
|
|
) AS ausser_dienst,
|
|
COUNT(*) FILTER (WHERE status = 'in_lehrgang') AS in_lehrgang
|
|
FROM fahrzeuge
|
|
`);
|
|
|
|
const alertResult = await pool.query(`
|
|
SELECT
|
|
COUNT(DISTINCT fahrzeug_id) FILTER (
|
|
WHERE (faellig_am::date - CURRENT_DATE) BETWEEN 0 AND 30
|
|
) AS inspections_due,
|
|
COUNT(DISTINCT fahrzeug_id) FILTER (
|
|
WHERE faellig_am::date < CURRENT_DATE
|
|
) AS inspections_overdue
|
|
FROM fahrzeug_pruefungen
|
|
WHERE durchgefuehrt_am IS NULL
|
|
`);
|
|
|
|
const totals = result.rows[0];
|
|
const alerts = alertResult.rows[0];
|
|
|
|
return {
|
|
total: parseInt(totals.total, 10),
|
|
einsatzbereit: parseInt(totals.einsatzbereit, 10),
|
|
ausserDienst: parseInt(totals.ausser_dienst, 10),
|
|
inLehrgang: parseInt(totals.in_lehrgang, 10),
|
|
inspectionsDue: parseInt(alerts.inspections_due, 10),
|
|
inspectionsOverdue: parseInt(alerts.inspections_overdue, 10),
|
|
};
|
|
} catch (error) {
|
|
logger.error('VehicleService.getVehicleStats failed', { error });
|
|
throw new Error('Failed to fetch vehicle stats');
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new VehicleService();
|