update
This commit is contained in:
@@ -25,8 +25,12 @@ function handleZodError(res: Response, err: ZodError): void {
|
||||
}
|
||||
|
||||
function handleConflictError(res: Response, err: Error): boolean {
|
||||
if (err.message?.includes('außer Dienst')) {
|
||||
res.status(409).json({ success: false, message: err.message, reason: 'out_of_service' });
|
||||
return true;
|
||||
}
|
||||
if (err.message?.includes('bereits gebucht')) {
|
||||
res.status(409).json({ success: false, message: err.message });
|
||||
res.status(409).json({ success: false, message: err.message, reason: 'booking_conflict' });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -88,10 +92,27 @@ class BookingController {
|
||||
.json({ success: false, message: 'fahrzeugId, from und to sind erforderlich' });
|
||||
return;
|
||||
}
|
||||
const beginn = new Date(from as string);
|
||||
const ende = new Date(to as string);
|
||||
|
||||
const outOfService = await bookingService.checkOutOfServiceConflict(
|
||||
fahrzeugId as string, beginn, ende
|
||||
);
|
||||
if (outOfService) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
available: false,
|
||||
reason: 'out_of_service',
|
||||
ausserDienstVon: outOfService.ausser_dienst_von.toISOString(),
|
||||
ausserDienstBis: outOfService.ausser_dienst_bis.toISOString(),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasConflict = await bookingService.checkConflict(
|
||||
fahrzeugId as string,
|
||||
new Date(from as string),
|
||||
new Date(to as string)
|
||||
fahrzeugId as string, beginn, ende
|
||||
);
|
||||
res.json({ success: true, data: { available: !hasConflict } });
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,7 +16,6 @@ const FahrzeugStatusEnum = z.enum([
|
||||
FahrzeugStatus.Einsatzbereit,
|
||||
FahrzeugStatus.AusserDienstWartung,
|
||||
FahrzeugStatus.AusserDienstSchaden,
|
||||
FahrzeugStatus.InLehrgang,
|
||||
]);
|
||||
|
||||
const isoDate = z.string().regex(
|
||||
@@ -64,10 +63,27 @@ const UpdateFahrzeugSchema = z.object({
|
||||
naechste_wartung_am: isoDate.nullable().optional(),
|
||||
});
|
||||
|
||||
const isoDatetime = z.string().datetime({ offset: true, message: 'Erwartet ISO-8601 Datum mit Zeitzone' });
|
||||
|
||||
const UpdateStatusSchema = z.object({
|
||||
status: FahrzeugStatusEnum,
|
||||
bemerkung: z.string().max(500).optional().default(''),
|
||||
});
|
||||
status: FahrzeugStatusEnum,
|
||||
bemerkung: z.string().max(500).optional().default(''),
|
||||
ausserDienstVon: isoDatetime.optional(),
|
||||
ausserDienstBis: isoDatetime.optional(),
|
||||
}).refine(
|
||||
(d) => {
|
||||
const isAusserDienst = d.status === FahrzeugStatus.AusserDienstWartung || d.status === FahrzeugStatus.AusserDienstSchaden;
|
||||
if (!isAusserDienst) return true;
|
||||
return !!d.ausserDienstVon && !!d.ausserDienstBis;
|
||||
},
|
||||
{ message: 'Außer-Dienst-Zeitraum (von + bis) ist bei diesem Status erforderlich', path: ['ausserDienstVon'] }
|
||||
).refine(
|
||||
(d) => {
|
||||
if (!d.ausserDienstVon || !d.ausserDienstBis) return true;
|
||||
return new Date(d.ausserDienstBis) > new Date(d.ausserDienstVon);
|
||||
},
|
||||
{ message: 'Enddatum muss nach Startdatum liegen', path: ['ausserDienstBis'] }
|
||||
);
|
||||
|
||||
const CreateWartungslogSchema = z.object({
|
||||
datum: isoDate,
|
||||
@@ -211,10 +227,16 @@ class VehicleController {
|
||||
return;
|
||||
}
|
||||
const io = req.app.get('io') ?? undefined;
|
||||
await vehicleService.updateVehicleStatus(
|
||||
id, parsed.data.status, parsed.data.bemerkung, getUserId(req), io
|
||||
const result = await vehicleService.updateVehicleStatus(
|
||||
id,
|
||||
parsed.data.status,
|
||||
parsed.data.bemerkung,
|
||||
getUserId(req),
|
||||
io,
|
||||
parsed.data.ausserDienstVon ? new Date(parsed.data.ausserDienstVon) : null,
|
||||
parsed.data.ausserDienstBis ? new Date(parsed.data.ausserDienstBis) : null,
|
||||
);
|
||||
res.status(200).json({ success: true, message: 'Status aktualisiert' });
|
||||
res.status(200).json({ success: true, data: result });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'Vehicle not found') {
|
||||
res.status(404).json({ success: false, message: 'Fahrzeug nicht gefunden' });
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
-- =============================================================================
|
||||
-- Migration 029: Vehicle Out-of-Service Timeframe + Lehrgang Booking Type
|
||||
--
|
||||
-- 1. Add ausser_dienst_von / ausser_dienst_bis columns to fahrzeuge
|
||||
-- 2. Add 'lehrgang' value to fahrzeug_buchung_art enum
|
||||
-- 3. Remove 'in_lehrgang' from vehicle status CHECK constraint
|
||||
-- 4. Refresh fahrzeuge_mit_pruefstatus view to include new columns
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. Add out-of-service timeframe columns
|
||||
-- -----------------------------------------------------------------------------
|
||||
ALTER TABLE fahrzeuge
|
||||
ADD COLUMN IF NOT EXISTS ausser_dienst_von TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS ausser_dienst_bis TIMESTAMPTZ;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_name = 'fahrzeuge'
|
||||
AND constraint_name = 'chk_ausser_dienst_zeitraum'
|
||||
) THEN
|
||||
ALTER TABLE fahrzeuge
|
||||
ADD CONSTRAINT chk_ausser_dienst_zeitraum
|
||||
CHECK (
|
||||
ausser_dienst_von IS NULL
|
||||
OR ausser_dienst_bis IS NULL
|
||||
OR ausser_dienst_bis > ausser_dienst_von
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fahrzeuge_ausser_dienst_zeitraum
|
||||
ON fahrzeuge(id, ausser_dienst_von, ausser_dienst_bis)
|
||||
WHERE ausser_dienst_von IS NOT NULL;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. Add 'lehrgang' to fahrzeug_buchung_art enum
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TYPE fahrzeug_buchung_art ADD VALUE IF NOT EXISTS 'lehrgang';
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
IF SQLERRM NOT LIKE '%already exists%' THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. Remove 'in_lehrgang' from vehicle status CHECK
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Migrate existing in_lehrgang rows first
|
||||
UPDATE fahrzeuge SET status = 'einsatzbereit' WHERE status = 'in_lehrgang';
|
||||
|
||||
-- Drop old check constraint (created in migration 005 as fahrzeuge_status_check)
|
||||
ALTER TABLE fahrzeuge DROP CONSTRAINT IF EXISTS fahrzeuge_status_check;
|
||||
|
||||
-- Re-add with only 3 valid values
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_name = 'fahrzeuge'
|
||||
AND constraint_name = 'chk_fahrzeuge_status'
|
||||
) THEN
|
||||
ALTER TABLE fahrzeuge
|
||||
ADD CONSTRAINT chk_fahrzeuge_status
|
||||
CHECK (status IN (
|
||||
'einsatzbereit',
|
||||
'ausser_dienst_wartung',
|
||||
'ausser_dienst_schaden'
|
||||
));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. Refresh fahrzeuge_mit_pruefstatus view to include new columns
|
||||
-- -----------------------------------------------------------------------------
|
||||
DROP VIEW IF EXISTS fahrzeuge_mit_pruefstatus;
|
||||
|
||||
CREATE OR REPLACE VIEW fahrzeuge_mit_pruefstatus AS
|
||||
SELECT
|
||||
f.id,
|
||||
f.bezeichnung,
|
||||
f.kurzname,
|
||||
f.amtliches_kennzeichen,
|
||||
f.fahrgestellnummer,
|
||||
f.baujahr,
|
||||
f.hersteller,
|
||||
f.typ_schluessel,
|
||||
f.besatzung_soll,
|
||||
f.status,
|
||||
f.status_bemerkung,
|
||||
f.ausser_dienst_von,
|
||||
f.ausser_dienst_bis,
|
||||
f.standort,
|
||||
f.bild_url,
|
||||
f.created_at,
|
||||
f.updated_at,
|
||||
f.paragraph57a_faellig_am,
|
||||
CASE
|
||||
WHEN f.paragraph57a_faellig_am IS NOT NULL
|
||||
THEN f.paragraph57a_faellig_am::date - CURRENT_DATE
|
||||
ELSE NULL
|
||||
END AS paragraph57a_tage_bis_faelligkeit,
|
||||
f.naechste_wartung_am,
|
||||
CASE
|
||||
WHEN f.naechste_wartung_am IS NOT NULL
|
||||
THEN f.naechste_wartung_am::date - CURRENT_DATE
|
||||
ELSE NULL
|
||||
END AS wartung_tage_bis_faelligkeit,
|
||||
LEAST(
|
||||
CASE WHEN f.paragraph57a_faellig_am IS NOT NULL
|
||||
THEN f.paragraph57a_faellig_am::date - CURRENT_DATE
|
||||
ELSE NULL END,
|
||||
CASE WHEN f.naechste_wartung_am IS NOT NULL
|
||||
THEN f.naechste_wartung_am::date - CURRENT_DATE
|
||||
ELSE NULL END
|
||||
) AS naechste_pruefung_tage
|
||||
FROM fahrzeuge f
|
||||
WHERE f.deleted_at IS NULL;
|
||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
// Enums
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const BUCHUNGS_ARTEN = ['intern', 'extern', 'wartung', 'reservierung', 'sonstiges'] as const;
|
||||
export const BUCHUNGS_ARTEN = ['intern', 'extern', 'wartung', 'reservierung', 'sonstiges', 'lehrgang'] as const;
|
||||
export type BuchungsArt = (typeof BUCHUNGS_ARTEN)[number];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -8,14 +8,12 @@ export enum FahrzeugStatus {
|
||||
Einsatzbereit = 'einsatzbereit',
|
||||
AusserDienstWartung = 'ausser_dienst_wartung',
|
||||
AusserDienstSchaden = 'ausser_dienst_schaden',
|
||||
InLehrgang = 'in_lehrgang',
|
||||
}
|
||||
|
||||
export const FahrzeugStatusLabel: Record<FahrzeugStatus, string> = {
|
||||
[FahrzeugStatus.Einsatzbereit]: 'Einsatzbereit',
|
||||
[FahrzeugStatus.AusserDienstWartung]: 'Außer Dienst (Wartung)',
|
||||
[FahrzeugStatus.AusserDienstSchaden]: 'Außer Dienst (Schaden)',
|
||||
[FahrzeugStatus.InLehrgang]: 'In Lehrgang',
|
||||
};
|
||||
|
||||
export type WartungslogArt =
|
||||
@@ -25,6 +23,12 @@ export type WartungslogArt =
|
||||
|
||||
// ── Core Entities ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AktiverLehrgang {
|
||||
titel: string;
|
||||
beginn: Date;
|
||||
ende: Date;
|
||||
}
|
||||
|
||||
export interface Fahrzeug {
|
||||
id: string;
|
||||
bezeichnung: string;
|
||||
@@ -37,12 +41,15 @@ export interface Fahrzeug {
|
||||
besatzung_soll: string | null;
|
||||
status: FahrzeugStatus;
|
||||
status_bemerkung: string | null;
|
||||
ausser_dienst_von: Date | null;
|
||||
ausser_dienst_bis: Date | null;
|
||||
standort: string;
|
||||
bild_url: string | null;
|
||||
paragraph57a_faellig_am: Date | null;
|
||||
naechste_wartung_am: Date | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
aktiver_lehrgang?: AktiverLehrgang | null;
|
||||
}
|
||||
|
||||
export interface FahrzeugWartungslog {
|
||||
@@ -71,12 +78,15 @@ export interface FahrzeugListItem {
|
||||
besatzung_soll: string | null;
|
||||
status: FahrzeugStatus;
|
||||
status_bemerkung: string | null;
|
||||
ausser_dienst_von: Date | null;
|
||||
ausser_dienst_bis: Date | null;
|
||||
bild_url: string | null;
|
||||
paragraph57a_faellig_am: Date | null;
|
||||
paragraph57a_tage_bis_faelligkeit: number | null;
|
||||
naechste_wartung_am: Date | null;
|
||||
wartung_tage_bis_faelligkeit: number | null;
|
||||
naechste_pruefung_tage: number | null;
|
||||
aktiver_lehrgang?: AktiverLehrgang | null;
|
||||
}
|
||||
|
||||
// ── Detail View ───────────────────────────────────────────────────────────────
|
||||
@@ -94,11 +104,19 @@ export interface VehicleStats {
|
||||
total: number;
|
||||
einsatzbereit: number;
|
||||
ausserDienst: number;
|
||||
inLehrgang: number;
|
||||
inLehrgang: number; // derived from active lehrgang bookings
|
||||
inspectionsDue: number;
|
||||
inspectionsOverdue: number;
|
||||
}
|
||||
|
||||
export interface OverlappingBooking {
|
||||
id: string;
|
||||
titel: string;
|
||||
beginn: Date;
|
||||
ende: Date;
|
||||
gebucht_von_name: string;
|
||||
}
|
||||
|
||||
// ── Inspection Alert ──────────────────────────────────────────────────────────
|
||||
|
||||
export type InspectionAlertType = '57a' | 'wartung';
|
||||
|
||||
@@ -141,6 +141,35 @@ class BookingService {
|
||||
return rowToBuchung(rows[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a vehicle is out of service during the given interval.
|
||||
* Returns the out-of-service period if there IS a conflict, null otherwise.
|
||||
*/
|
||||
async checkOutOfServiceConflict(
|
||||
fahrzeugId: string,
|
||||
beginn: Date,
|
||||
ende: Date
|
||||
): Promise<{ ausser_dienst_von: Date; ausser_dienst_bis: Date } | null> {
|
||||
const query = `
|
||||
SELECT ausser_dienst_von, ausser_dienst_bis
|
||||
FROM fahrzeuge
|
||||
WHERE id = $1
|
||||
AND 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 ($2::timestamptz, $3::timestamptz) OVERLAPS (ausser_dienst_von, ausser_dienst_bis)
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const { rows } = await pool.query(query, [fahrzeugId, beginn, ende]);
|
||||
if (rows.length === 0) return null;
|
||||
return {
|
||||
ausser_dienst_von: new Date(rows[0].ausser_dienst_von),
|
||||
ausser_dienst_bis: new Date(rows[0].ausser_dienst_bis),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a vehicle is already booked for the given interval.
|
||||
* Returns true if there IS a conflict.
|
||||
@@ -171,8 +200,13 @@ class BookingService {
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/** Creates a new booking. Throws if the vehicle has a conflicting booking. */
|
||||
/** Creates a new booking. Throws if the vehicle has a conflicting booking or is out of service. */
|
||||
async create(data: CreateBuchungData, userId: string): Promise<FahrzeugBuchung> {
|
||||
const outOfService = await this.checkOutOfServiceConflict(data.fahrzeugId, data.beginn, data.ende);
|
||||
if (outOfService) {
|
||||
throw new Error('Fahrzeug ist im gewählten Zeitraum außer Dienst');
|
||||
}
|
||||
|
||||
const hasConflict = await this.checkConflict(
|
||||
data.fahrzeugId,
|
||||
data.beginn,
|
||||
@@ -225,6 +259,15 @@ class BookingService {
|
||||
data.fahrzeugId != null || data.beginn != null || data.ende != null;
|
||||
|
||||
if (timingChanged) {
|
||||
const outOfService = await this.checkOutOfServiceConflict(
|
||||
effectiveFahrzeugId,
|
||||
effectiveBeginn,
|
||||
effectiveEnde
|
||||
);
|
||||
if (outOfService) {
|
||||
throw new Error('Fahrzeug ist im gewählten Zeitraum außer Dienst');
|
||||
}
|
||||
|
||||
const hasConflict = await this.checkConflict(
|
||||
effectiveFahrzeugId,
|
||||
effectiveBeginn,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FahrzeugStatus,
|
||||
VehicleStats,
|
||||
InspectionAlert,
|
||||
OverlappingBooking,
|
||||
} from '../models/vehicle.model';
|
||||
|
||||
class VehicleService {
|
||||
@@ -24,12 +25,30 @@ class VehicleService {
|
||||
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
|
||||
@@ -38,6 +57,7 @@ class VehicleService {
|
||||
? 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 });
|
||||
@@ -79,6 +99,8 @@ class VehicleService {
|
||||
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,
|
||||
@@ -97,6 +119,23 @@ class VehicleService {
|
||||
})) 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 });
|
||||
@@ -224,8 +263,10 @@ class VehicleService {
|
||||
status: FahrzeugStatus,
|
||||
bemerkung: string,
|
||||
updatedBy: string,
|
||||
io?: any
|
||||
): Promise<void> {
|
||||
io?: any,
|
||||
ausserDienstVon?: Date | null,
|
||||
ausserDienstBis?: Date | null,
|
||||
): Promise<{ overlappingBookings: OverlappingBooking[] }> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
@@ -242,26 +283,56 @@ class VehicleService {
|
||||
|
||||
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 WHERE id = $3`,
|
||||
[status, bemerkung || null, id]
|
||||
`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,
|
||||
vehicleId: id,
|
||||
bezeichnung,
|
||||
oldStatus,
|
||||
newStatus: status,
|
||||
bemerkung: bemerkung || null,
|
||||
newStatus: status,
|
||||
bemerkung: bemerkung || null,
|
||||
ausserDienstVon: isAusserDienst ? ausserDienstVon ?? null : null,
|
||||
ausserDienstBis: isAusserDienst ? ausserDienstBis ?? null : null,
|
||||
updatedBy,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return { overlappingBookings };
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
logger.error('VehicleService.updateVehicleStatus failed', { error, id });
|
||||
@@ -347,12 +418,21 @@ class VehicleService {
|
||||
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
|
||||
) 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 (
|
||||
@@ -373,14 +453,15 @@ class VehicleService {
|
||||
WHERE deleted_at IS NULL
|
||||
`);
|
||||
|
||||
const totals = totalsResult.rows[0] ?? { total: '0', einsatzbereit: '0', ausser_dienst: '0', in_lehrgang: '0' };
|
||||
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(totals.in_lehrgang ?? '0', 10),
|
||||
inLehrgang: parseInt(lehrgangs.in_lehrgang ?? '0', 10),
|
||||
inspectionsDue: parseInt(alerts.inspections_due ?? '0', 10),
|
||||
inspectionsOverdue: parseInt(alerts.inspections_overdue ?? '0', 10),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user