add features
This commit is contained in:
344
backend/src/controllers/vehicle.controller.ts
Normal file
344
backend/src/controllers/vehicle.controller.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import vehicleService from '../services/vehicle.service';
|
||||
import { FahrzeugStatus, PruefungArt } from '../models/vehicle.model';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
// ── Zod Validation Schemas ────────────────────────────────────────────────────
|
||||
|
||||
const FahrzeugStatusEnum = z.enum([
|
||||
FahrzeugStatus.Einsatzbereit,
|
||||
FahrzeugStatus.AusserDienstWartung,
|
||||
FahrzeugStatus.AusserDienstSchaden,
|
||||
FahrzeugStatus.InLehrgang,
|
||||
]);
|
||||
|
||||
const PruefungArtEnum = z.enum([
|
||||
PruefungArt.HU,
|
||||
PruefungArt.AU,
|
||||
PruefungArt.UVV,
|
||||
PruefungArt.Leiter,
|
||||
PruefungArt.Kran,
|
||||
PruefungArt.Seilwinde,
|
||||
PruefungArt.Sonstiges,
|
||||
]);
|
||||
|
||||
const isoDate = z.string().regex(
|
||||
/^\d{4}-\d{2}-\d{2}$/,
|
||||
'Expected ISO date format YYYY-MM-DD'
|
||||
);
|
||||
|
||||
const CreateFahrzeugSchema = z.object({
|
||||
bezeichnung: z.string().min(1).max(100),
|
||||
kurzname: z.string().max(20).optional(),
|
||||
amtliches_kennzeichen: z.string().max(20).optional(),
|
||||
fahrgestellnummer: z.string().max(50).optional(),
|
||||
baujahr: z.number().int().min(1950).max(2100).optional(),
|
||||
hersteller: z.string().max(100).optional(),
|
||||
typ_schluessel: z.string().max(30).optional(),
|
||||
besatzung_soll: z.string().max(10).optional(),
|
||||
status: FahrzeugStatusEnum.optional(),
|
||||
status_bemerkung: z.string().max(500).optional(),
|
||||
standort: z.string().max(100).optional(),
|
||||
bild_url: z.string().url().max(500).optional(),
|
||||
});
|
||||
|
||||
const UpdateFahrzeugSchema = CreateFahrzeugSchema.partial();
|
||||
|
||||
const UpdateStatusSchema = z.object({
|
||||
status: FahrzeugStatusEnum,
|
||||
bemerkung: z.string().max(500).optional().default(''),
|
||||
});
|
||||
|
||||
const CreatePruefungSchema = z.object({
|
||||
pruefung_art: PruefungArtEnum,
|
||||
faellig_am: isoDate,
|
||||
durchgefuehrt_am: isoDate.optional(),
|
||||
ergebnis: z.enum(['bestanden', 'bestanden_mit_maengeln', 'nicht_bestanden', 'ausstehend']).optional(),
|
||||
pruefende_stelle: z.string().max(150).optional(),
|
||||
kosten: z.number().min(0).optional(),
|
||||
dokument_url: z.string().url().max(500).optional(),
|
||||
bemerkung: z.string().max(1000).optional(),
|
||||
});
|
||||
|
||||
const CreateWartungslogSchema = z.object({
|
||||
datum: isoDate,
|
||||
art: z.enum(['Inspektion', 'Reparatur', 'Kraftstoff', 'Reifenwechsel', 'Hauptuntersuchung', 'Reinigung', 'Sonstiges']).optional(),
|
||||
beschreibung: z.string().min(1).max(2000),
|
||||
km_stand: z.number().int().min(0).optional(),
|
||||
kraftstoff_liter: z.number().min(0).optional(),
|
||||
kosten: z.number().min(0).optional(),
|
||||
externe_werkstatt: z.string().max(150).optional(),
|
||||
});
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function getUserId(req: Request): string {
|
||||
// req.user is guaranteed by the authenticate middleware
|
||||
return req.user!.id;
|
||||
}
|
||||
|
||||
// ── Controller ────────────────────────────────────────────────────────────────
|
||||
|
||||
class VehicleController {
|
||||
/**
|
||||
* GET /api/vehicles
|
||||
* Fleet overview list with per-vehicle inspection badge data.
|
||||
*/
|
||||
async listVehicles(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const vehicles = await vehicleService.getAllVehicles();
|
||||
res.status(200).json({ success: true, data: vehicles });
|
||||
} catch (error) {
|
||||
logger.error('listVehicles error', { error });
|
||||
res.status(500).json({ success: false, message: 'Fahrzeuge konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/vehicles/stats
|
||||
* Aggregated KPI counts for the dashboard strip.
|
||||
*/
|
||||
async getStats(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const stats = await vehicleService.getVehicleStats();
|
||||
res.status(200).json({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
logger.error('getStats error', { error });
|
||||
res.status(500).json({ success: false, message: 'Statistiken konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/vehicles/alerts?daysAhead=30
|
||||
* Upcoming and overdue inspections — used by the InspectionAlerts dashboard panel.
|
||||
* Returns alerts sorted by urgency (most overdue / soonest due first).
|
||||
*/
|
||||
async getAlerts(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const daysAhead = Math.min(
|
||||
parseInt((req.query.daysAhead as string) || '30', 10),
|
||||
365 // hard cap — never expose more than 1 year of lookahead
|
||||
);
|
||||
|
||||
if (isNaN(daysAhead) || daysAhead < 0) {
|
||||
res.status(400).json({ success: false, message: 'Ungültiger daysAhead-Wert' });
|
||||
return;
|
||||
}
|
||||
|
||||
const alerts = await vehicleService.getUpcomingInspections(daysAhead);
|
||||
res.status(200).json({ success: true, data: alerts });
|
||||
} catch (error) {
|
||||
logger.error('getAlerts error', { error });
|
||||
res.status(500).json({ success: false, message: 'Prüfungshinweise konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/vehicles/:id
|
||||
* Full vehicle detail with pruefstatus, inspection history, and wartungslog.
|
||||
*/
|
||||
async getVehicle(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const vehicle = await vehicleService.getVehicleById(id);
|
||||
|
||||
if (!vehicle) {
|
||||
res.status(404).json({ success: false, message: 'Fahrzeug nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, data: vehicle });
|
||||
} catch (error) {
|
||||
logger.error('getVehicle error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Fahrzeug konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/vehicles
|
||||
* Create a new vehicle. Requires vehicles:write permission.
|
||||
*/
|
||||
async createVehicle(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const parsed = CreateFahrzeugSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicle = await vehicleService.createVehicle(parsed.data, getUserId(req));
|
||||
res.status(201).json({ success: true, data: vehicle });
|
||||
} catch (error) {
|
||||
logger.error('createVehicle error', { error });
|
||||
res.status(500).json({ success: false, message: 'Fahrzeug konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/vehicles/:id
|
||||
* Update vehicle fields. Requires vehicles:write permission.
|
||||
*/
|
||||
async updateVehicle(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const parsed = UpdateFahrzeugSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicle = await vehicleService.updateVehicle(id, parsed.data, getUserId(req));
|
||||
res.status(200).json({ success: true, data: vehicle });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'Vehicle not found') {
|
||||
res.status(404).json({ success: false, message: 'Fahrzeug nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
logger.error('updateVehicle error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Fahrzeug konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/vehicles/:id/status
|
||||
* Live status change — the Socket.IO hook point for Tier 3.
|
||||
* Requires vehicles:write permission.
|
||||
*
|
||||
* The `io` instance is attached to req.app in server.ts (Tier 3):
|
||||
* app.set('io', io);
|
||||
* and retrieved here via req.app.get('io').
|
||||
*/
|
||||
async updateVehicleStatus(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const parsed = UpdateStatusSchema.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Tier 3: io will be available via req.app.get('io') once Socket.IO is wired up.
|
||||
// Passing undefined here is safe — the service handles it gracefully.
|
||||
const io = req.app.get('io') ?? undefined;
|
||||
|
||||
await vehicleService.updateVehicleStatus(
|
||||
id,
|
||||
parsed.data.status,
|
||||
parsed.data.bemerkung,
|
||||
getUserId(req),
|
||||
io
|
||||
);
|
||||
|
||||
res.status(200).json({ success: true, message: 'Status aktualisiert' });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'Vehicle not found') {
|
||||
res.status(404).json({ success: false, message: 'Fahrzeug nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
logger.error('updateVehicleStatus error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inspections ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /api/vehicles/:id/pruefungen
|
||||
* Record an inspection (scheduled or completed). Requires vehicles:write.
|
||||
*/
|
||||
async addPruefung(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const parsed = CreatePruefungSchema.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const pruefung = await vehicleService.addPruefung(id, parsed.data, getUserId(req));
|
||||
res.status(201).json({ success: true, data: pruefung });
|
||||
} catch (error) {
|
||||
logger.error('addPruefung error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Prüfung konnte nicht eingetragen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/vehicles/:id/pruefungen
|
||||
* Full inspection history for a vehicle.
|
||||
*/
|
||||
async getPruefungen(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const pruefungen = await vehicleService.getPruefungenForVehicle(id);
|
||||
res.status(200).json({ success: true, data: pruefungen });
|
||||
} catch (error) {
|
||||
logger.error('getPruefungen error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Prüfungshistorie konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Maintenance Log ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /api/vehicles/:id/wartung
|
||||
* Add a maintenance log entry. Requires vehicles:write.
|
||||
*/
|
||||
async addWartung(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const parsed = CreateWartungslogSchema.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = await vehicleService.addWartungslog(id, parsed.data, getUserId(req));
|
||||
res.status(201).json({ success: true, data: entry });
|
||||
} catch (error) {
|
||||
logger.error('addWartung error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Wartungseintrag konnte nicht gespeichert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/vehicles/:id/wartung
|
||||
* Maintenance log for a vehicle.
|
||||
*/
|
||||
async getWartung(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const entries = await vehicleService.getWartungslogForVehicle(id);
|
||||
res.status(200).json({ success: true, data: entries });
|
||||
} catch (error) {
|
||||
logger.error('getWartung error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Wartungslog konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new VehicleController();
|
||||
Reference in New Issue
Block a user