580 lines
21 KiB
TypeScript
580 lines
21 KiB
TypeScript
import pool from '../config/database';
|
|
import logger from '../utils/logger';
|
|
import {
|
|
Fahrzeug,
|
|
FahrzeugListItem,
|
|
FahrzeugDetail,
|
|
FahrzeugWartungslog,
|
|
CreateFahrzeugData,
|
|
UpdateFahrzeugData,
|
|
CreateWartungslogData,
|
|
FahrzeugStatus,
|
|
VehicleStats,
|
|
InspectionAlert,
|
|
OverlappingBooking,
|
|
} from '../models/vehicle.model';
|
|
|
|
class VehicleService {
|
|
// =========================================================================
|
|
// FLEET OVERVIEW
|
|
// =========================================================================
|
|
|
|
async getAllVehicles(): Promise<FahrzeugListItem[]> {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
id, bezeichnung, kurzname, amtliches_kennzeichen,
|
|
baujahr, hersteller, besatzung_soll, status, status_bemerkung,
|
|
ausser_dienst_von, ausser_dienst_bis,
|
|
bild_url, paragraph57a_faellig_am, paragraph57a_tage_bis_faelligkeit,
|
|
naechste_wartung_am, wartung_tage_bis_faelligkeit, naechste_pruefung_tage
|
|
FROM fahrzeuge_mit_pruefstatus
|
|
ORDER BY bezeichnung ASC
|
|
`);
|
|
|
|
// Fetch active lehrgang bookings for all vehicles in one query
|
|
const lehrgangs = await pool.query(`
|
|
SELECT DISTINCT ON (b.fahrzeug_id)
|
|
b.fahrzeug_id, b.titel, b.beginn, b.ende
|
|
FROM fahrzeug_buchungen b
|
|
WHERE b.buchungs_art = 'lehrgang'
|
|
AND b.abgesagt = FALSE
|
|
AND b.beginn <= NOW()
|
|
AND b.ende >= NOW()
|
|
ORDER BY b.fahrzeug_id, b.beginn ASC
|
|
`);
|
|
|
|
const lehrgangsMap = new Map<string, { titel: string; beginn: Date; ende: Date }>();
|
|
for (const r of lehrgangs.rows) {
|
|
lehrgangsMap.set(r.fahrzeug_id, { titel: r.titel, beginn: new Date(r.beginn), ende: new Date(r.ende) });
|
|
}
|
|
|
|
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,
|
|
naechste_pruefung_tage: row.naechste_pruefung_tage != null
|
|
? parseInt(row.naechste_pruefung_tage, 10) : null,
|
|
aktiver_lehrgang: lehrgangsMap.get(row.id) ?? null,
|
|
})) as FahrzeugListItem[];
|
|
} catch (error) {
|
|
logger.error('VehicleService.getAllVehicles failed', { error });
|
|
throw new Error('Failed to fetch vehicles');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// VEHICLE DETAIL
|
|
// =========================================================================
|
|
|
|
async getVehicleById(id: string): Promise<FahrzeugDetail | null> {
|
|
try {
|
|
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];
|
|
|
|
const wartungslogResult = await pool.query(
|
|
`SELECT * FROM fahrzeug_wartungslog
|
|
WHERE fahrzeug_id = $1
|
|
ORDER BY datum DESC, created_at DESC`,
|
|
[id]
|
|
);
|
|
|
|
const vehicle: FahrzeugDetail = {
|
|
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,
|
|
ausser_dienst_von: row.ausser_dienst_von ?? null,
|
|
ausser_dienst_bis: row.ausser_dienst_bis ?? null,
|
|
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,
|
|
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,
|
|
wartungslog: wartungslogResult.rows.map(r => ({
|
|
...r,
|
|
kosten: r.kosten != null ? Number(r.kosten) : null,
|
|
})) as FahrzeugWartungslog[],
|
|
};
|
|
|
|
// Fetch active lehrgang booking if any
|
|
const lehrgang = await pool.query(`
|
|
SELECT titel, beginn, ende
|
|
FROM fahrzeug_buchungen
|
|
WHERE fahrzeug_id = $1
|
|
AND buchungs_art = 'lehrgang'
|
|
AND abgesagt = FALSE
|
|
AND beginn <= NOW()
|
|
AND ende >= NOW()
|
|
ORDER BY beginn ASC
|
|
LIMIT 1
|
|
`, [id]);
|
|
|
|
vehicle.aktiver_lehrgang = lehrgang.rows.length > 0
|
|
? { titel: lehrgang.rows[0].titel, beginn: lehrgang.rows[0].beginn, ende: lehrgang.rows[0].ende }
|
|
: null;
|
|
|
|
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);
|
|
const result = await pool.query(
|
|
`UPDATE fahrzeuge SET ${fields.join(', ')} WHERE id = $${p} AND deleted_at IS NULL 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(
|
|
`UPDATE fahrzeuge
|
|
SET deleted_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
RETURNING id`,
|
|
[id]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Vehicle not found');
|
|
}
|
|
|
|
logger.info('Vehicle soft-deleted', { id, by: deletedBy });
|
|
} catch (error) {
|
|
logger.error('VehicleService.deleteVehicle failed', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// STATUS MANAGEMENT
|
|
// =========================================================================
|
|
|
|
async updateVehicleStatus(
|
|
id: string,
|
|
status: FahrzeugStatus,
|
|
bemerkung: string,
|
|
updatedBy: string,
|
|
io?: any,
|
|
ausserDienstVon?: Date | null,
|
|
ausserDienstBis?: Date | null,
|
|
): Promise<{ overlappingBookings: OverlappingBooking[] }> {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const oldResult = await client.query(
|
|
`SELECT bezeichnung, status FROM fahrzeuge WHERE id = $1 AND deleted_at IS NULL FOR UPDATE`,
|
|
[id]
|
|
);
|
|
|
|
if (oldResult.rows.length === 0) {
|
|
await client.query('ROLLBACK');
|
|
throw new Error('Vehicle not found');
|
|
}
|
|
|
|
const { bezeichnung, status: oldStatus } = oldResult.rows[0];
|
|
|
|
const isAusserDienst = status === FahrzeugStatus.AusserDienstWartung || status === FahrzeugStatus.AusserDienstSchaden;
|
|
|
|
await client.query(
|
|
`UPDATE fahrzeuge
|
|
SET status = $1, status_bemerkung = $2,
|
|
ausser_dienst_von = $3, ausser_dienst_bis = $4
|
|
WHERE id = $5`,
|
|
[
|
|
status,
|
|
bemerkung || null,
|
|
isAusserDienst ? (ausserDienstVon ?? null) : null,
|
|
isAusserDienst ? (ausserDienstBis ?? null) : null,
|
|
id,
|
|
]
|
|
);
|
|
|
|
await client.query('COMMIT');
|
|
|
|
logger.info('Vehicle status updated', { id, from: oldStatus, to: status, by: updatedBy });
|
|
|
|
// Find bookings that overlap with the out-of-service period
|
|
let overlappingBookings: OverlappingBooking[] = [];
|
|
if (isAusserDienst && ausserDienstVon && ausserDienstBis) {
|
|
const overlap = await pool.query(
|
|
`SELECT b.id, b.titel, b.beginn, b.ende, u.name AS gebucht_von_name
|
|
FROM fahrzeug_buchungen b
|
|
JOIN users u ON u.id = b.gebucht_von
|
|
WHERE b.fahrzeug_id = $1
|
|
AND b.abgesagt = FALSE
|
|
AND ($2::timestamptz, $3::timestamptz) OVERLAPS (b.beginn, b.ende)`,
|
|
[id, ausserDienstVon, ausserDienstBis]
|
|
);
|
|
overlappingBookings = overlap.rows;
|
|
}
|
|
|
|
if (io) {
|
|
io.emit('vehicle:statusChanged', {
|
|
vehicleId: id,
|
|
bezeichnung,
|
|
oldStatus,
|
|
newStatus: status,
|
|
bemerkung: bemerkung || null,
|
|
ausserDienstVon: isAusserDienst ? ausserDienstVon ?? null : null,
|
|
ausserDienstBis: isAusserDienst ? ausserDienstBis ?? null : null,
|
|
updatedBy,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
return { overlappingBookings };
|
|
} catch (error) {
|
|
await client.query('ROLLBACK').catch(() => {});
|
|
logger.error('VehicleService.updateVehicleStatus failed', { error, id });
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// MAINTENANCE LOG
|
|
// =========================================================================
|
|
|
|
async addWartungslog(
|
|
fahrzeugId: string,
|
|
data: CreateWartungslogData,
|
|
createdBy: string
|
|
): Promise<FahrzeugWartungslog> {
|
|
try {
|
|
const check = await pool.query(
|
|
`SELECT 1 FROM fahrzeuge WHERE id = $1 AND deleted_at IS NULL`,
|
|
[fahrzeugId]
|
|
);
|
|
if (check.rows.length === 0) {
|
|
throw new Error('Vehicle not found');
|
|
}
|
|
|
|
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 error;
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// MAINTENANCE WINDOWS (for booking calendar overlay)
|
|
// =========================================================================
|
|
|
|
async getMaintenanceWindows(
|
|
from: Date,
|
|
to: Date
|
|
): Promise<
|
|
{
|
|
id: string;
|
|
bezeichnung: string;
|
|
kurzname: string | null;
|
|
status: string;
|
|
status_bemerkung: string | null;
|
|
ausser_dienst_von: string;
|
|
ausser_dienst_bis: string;
|
|
}[]
|
|
> {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT id, bezeichnung, kurzname, status, status_bemerkung,
|
|
ausser_dienst_von, ausser_dienst_bis
|
|
FROM fahrzeuge
|
|
WHERE deleted_at IS NULL
|
|
AND status IN ('ausser_dienst_wartung', 'ausser_dienst_schaden')
|
|
AND ausser_dienst_von IS NOT NULL
|
|
AND ausser_dienst_bis IS NOT NULL
|
|
AND (ausser_dienst_von, ausser_dienst_bis) OVERLAPS ($1, $2)`,
|
|
[from, to]
|
|
);
|
|
return result.rows;
|
|
} catch (error) {
|
|
logger.error('VehicleService.getMaintenanceWindows failed', { error });
|
|
throw new Error('Failed to fetch maintenance windows');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// DASHBOARD KPI
|
|
// =========================================================================
|
|
|
|
async getVehicleStats(): Promise<VehicleStats> {
|
|
try {
|
|
const totalsResult = 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
|
|
FROM fahrzeuge
|
|
WHERE deleted_at IS NULL
|
|
`);
|
|
|
|
// Count vehicles with an active lehrgang booking
|
|
const lehrgangsResult = await pool.query(`
|
|
SELECT COUNT(DISTINCT b.fahrzeug_id) AS in_lehrgang
|
|
FROM fahrzeug_buchungen b
|
|
WHERE b.buchungs_art = 'lehrgang'
|
|
AND b.abgesagt = FALSE
|
|
AND b.beginn <= NOW()
|
|
AND b.ende >= NOW()
|
|
`);
|
|
|
|
const alertResult = await pool.query(`
|
|
SELECT
|
|
COUNT(*) FILTER (
|
|
WHERE (
|
|
(paragraph57a_faellig_am IS NOT NULL AND paragraph57a_faellig_am::date - CURRENT_DATE BETWEEN 0 AND 30)
|
|
OR
|
|
(naechste_wartung_am IS NOT NULL AND naechste_wartung_am::date - CURRENT_DATE BETWEEN 0 AND 30)
|
|
)
|
|
) AS inspections_due,
|
|
COUNT(*) FILTER (
|
|
WHERE (
|
|
(paragraph57a_faellig_am IS NOT NULL AND paragraph57a_faellig_am::date < CURRENT_DATE)
|
|
OR
|
|
(naechste_wartung_am IS NOT NULL AND naechste_wartung_am::date < CURRENT_DATE)
|
|
)
|
|
) AS inspections_overdue
|
|
FROM fahrzeuge
|
|
WHERE deleted_at IS NULL
|
|
`);
|
|
|
|
const totals = totalsResult.rows[0] ?? { total: '0', einsatzbereit: '0', ausser_dienst: '0' };
|
|
const lehrgangs = lehrgangsResult.rows[0] ?? { in_lehrgang: '0' };
|
|
const alerts = alertResult.rows[0] ?? { inspections_due: '0', inspections_overdue: '0' };
|
|
|
|
return {
|
|
total: parseInt(totals.total ?? '0', 10),
|
|
einsatzbereit: parseInt(totals.einsatzbereit ?? '0', 10),
|
|
ausserDienst: parseInt(totals.ausser_dienst ?? '0', 10),
|
|
inLehrgang: parseInt(lehrgangs.in_lehrgang ?? '0', 10),
|
|
inspectionsDue: parseInt(alerts.inspections_due ?? '0', 10),
|
|
inspectionsOverdue: parseInt(alerts.inspections_overdue ?? '0', 10),
|
|
};
|
|
} catch (error) {
|
|
logger.error('VehicleService.getVehicleStats failed', { error });
|
|
throw new Error('Failed to fetch vehicle stats');
|
|
}
|
|
}
|
|
|
|
async getUpcomingInspections(daysAhead: number): Promise<InspectionAlert[]> {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT
|
|
id AS fahrzeug_id,
|
|
bezeichnung,
|
|
kurzname,
|
|
paragraph57a_faellig_am,
|
|
paragraph57a_faellig_am::date - CURRENT_DATE AS paragraph57a_tage,
|
|
naechste_wartung_am,
|
|
naechste_wartung_am::date - CURRENT_DATE AS wartung_tage
|
|
FROM fahrzeuge
|
|
WHERE
|
|
deleted_at IS NULL
|
|
AND (
|
|
(paragraph57a_faellig_am IS NOT NULL AND paragraph57a_faellig_am::date - CURRENT_DATE <= $1)
|
|
OR
|
|
(naechste_wartung_am IS NOT NULL AND naechste_wartung_am::date - CURRENT_DATE <= $1)
|
|
)
|
|
ORDER BY LEAST(
|
|
CASE WHEN paragraph57a_faellig_am IS NOT NULL
|
|
THEN paragraph57a_faellig_am::date - CURRENT_DATE END,
|
|
CASE WHEN naechste_wartung_am IS NOT NULL
|
|
THEN naechste_wartung_am::date - CURRENT_DATE END
|
|
) ASC NULLS LAST`,
|
|
[daysAhead]
|
|
);
|
|
|
|
const alerts: InspectionAlert[] = [];
|
|
|
|
for (const row of result.rows) {
|
|
if (row.paragraph57a_faellig_am !== null && row.paragraph57a_tage !== null) {
|
|
const tage = parseInt(row.paragraph57a_tage, 10);
|
|
if (tage <= daysAhead) {
|
|
alerts.push({
|
|
fahrzeugId: row.fahrzeug_id,
|
|
bezeichnung: row.bezeichnung,
|
|
kurzname: row.kurzname,
|
|
type: '57a',
|
|
faelligAm: row.paragraph57a_faellig_am,
|
|
tage,
|
|
});
|
|
}
|
|
}
|
|
if (row.naechste_wartung_am !== null && row.wartung_tage !== null) {
|
|
const tage = parseInt(row.wartung_tage, 10);
|
|
if (tage <= daysAhead) {
|
|
alerts.push({
|
|
fahrzeugId: row.fahrzeug_id,
|
|
bezeichnung: row.bezeichnung,
|
|
kurzname: row.kurzname,
|
|
type: 'wartung',
|
|
faelligAm: row.naechste_wartung_am,
|
|
tage,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
alerts.sort((a, b) => a.tage - b.tage);
|
|
return alerts;
|
|
} catch (error) {
|
|
logger.error('VehicleService.getUpcomingInspections failed', { error, daysAhead });
|
|
throw new Error('Failed to fetch inspection alerts');
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new VehicleService();
|