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,318 @@
import { Request, Response } from 'express';
import { z } from 'zod';
import equipmentService from '../services/equipment.service';
import { AusruestungStatus } from '../models/equipment.model';
import logger from '../utils/logger';
// ── UUID validation ───────────────────────────────────────────────────────────
function isValidUUID(s: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
}
// ── Zod Validation Schemas ────────────────────────────────────────────────────
const AusruestungStatusEnum = z.enum([
AusruestungStatus.Einsatzbereit,
AusruestungStatus.Beschaedigt,
AusruestungStatus.InWartung,
AusruestungStatus.AusserDienst,
]);
const isoDate = z.string().regex(
/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
'Erwartet ISO-Datum im Format YYYY-MM-DD'
);
const uuidString = z.string().regex(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
'Ungültige UUID'
);
const CreateAusruestungSchema = z.object({
bezeichnung: z.string().min(1).max(200),
kategorie_id: uuidString,
seriennummer: z.string().max(100).optional(),
inventarnummer: z.string().max(50).optional(),
hersteller: z.string().max(150).optional(),
baujahr: z.number().int().min(1950).max(2100).optional(),
status: AusruestungStatusEnum.optional(),
status_bemerkung: z.string().max(2000).optional(),
ist_wichtig: z.boolean().optional(),
fahrzeug_id: uuidString.optional(),
standort: z.string().min(1).max(150).optional(),
pruef_intervall_monate: z.number().int().min(1).optional(),
letzte_pruefung_am: isoDate.optional(),
naechste_pruefung_am: isoDate.optional(),
bemerkung: z.string().max(2000).optional(),
});
const UpdateAusruestungSchema = z.object({
bezeichnung: z.string().min(1).max(200).optional(),
kategorie_id: uuidString.optional(),
seriennummer: z.string().max(100).nullable().optional(),
inventarnummer: z.string().max(50).nullable().optional(),
hersteller: z.string().max(150).nullable().optional(),
baujahr: z.number().int().min(1950).max(2100).nullable().optional(),
status: AusruestungStatusEnum.optional(),
status_bemerkung: z.string().max(2000).nullable().optional(),
ist_wichtig: z.boolean().optional(),
fahrzeug_id: uuidString.nullable().optional(),
standort: z.string().min(1).max(150).optional(),
pruef_intervall_monate: z.number().int().min(1).nullable().optional(),
letzte_pruefung_am: isoDate.nullable().optional(),
naechste_pruefung_am: isoDate.nullable().optional(),
bemerkung: z.string().max(2000).nullable().optional(),
});
const UpdateStatusSchema = z.object({
status: AusruestungStatusEnum,
bemerkung: z.string().max(2000).optional().default(''),
});
const CreateWartungslogSchema = z.object({
datum: isoDate,
art: z.enum(['Prüfung', 'Reparatur', 'Sonstiges']),
beschreibung: z.string().min(1).max(2000),
ergebnis: z.enum(['bestanden', 'bestanden_mit_maengeln', 'nicht_bestanden']).optional(),
kosten: z.number().min(0).optional(),
pruefende_stelle: z.string().max(150).optional(),
dokument_url: z.string().url().max(500).refine(
(url) => /^https?:\/\//i.test(url),
'Nur http/https URLs erlaubt'
).optional(),
});
// ── Helper ────────────────────────────────────────────────────────────────────
function getUserId(req: Request): string {
return req.user!.id;
}
// ── Controller ────────────────────────────────────────────────────────────────
class EquipmentController {
async listEquipment(_req: Request, res: Response): Promise<void> {
try {
const equipment = await equipmentService.getAllEquipment();
res.status(200).json({ success: true, data: equipment });
} catch (error) {
logger.error('listEquipment error', { error });
res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht geladen werden' });
}
}
async getEquipment(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' });
return;
}
const equipment = await equipmentService.getEquipmentById(id);
if (!equipment) {
res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: equipment });
} catch (error) {
logger.error('getEquipment error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht geladen werden' });
}
}
async getByVehicle(req: Request, res: Response): Promise<void> {
try {
const { fahrzeugId } = req.params as Record<string, string>;
if (!isValidUUID(fahrzeugId)) {
res.status(400).json({ success: false, message: 'Ungültige Fahrzeug-ID' });
return;
}
const equipment = await equipmentService.getEquipmentByVehicle(fahrzeugId);
res.status(200).json({ success: true, data: equipment });
} catch (error) {
logger.error('getByVehicle error', { error, fahrzeugId: req.params.fahrzeugId });
res.status(500).json({ success: false, message: 'Ausrüstung für Fahrzeug konnte nicht geladen werden' });
}
}
async getCategories(_req: Request, res: Response): Promise<void> {
try {
const categories = await equipmentService.getCategories();
res.status(200).json({ success: true, data: categories });
} catch (error) {
logger.error('getCategories error', { error });
res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' });
}
}
async getStats(_req: Request, res: Response): Promise<void> {
try {
const stats = await equipmentService.getEquipmentStats();
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' });
}
}
async getAlerts(req: Request, res: Response): Promise<void> {
try {
const raw = parseInt((req.query.daysAhead as string) || '30', 10);
if (isNaN(raw) || raw < 0) {
res.status(400).json({ success: false, message: 'Ungültiger daysAhead-Wert' });
return;
}
const daysAhead = Math.min(raw, 365);
const alerts = await equipmentService.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' });
}
}
async getVehicleWarnings(_req: Request, res: Response): Promise<void> {
try {
const warnings = await equipmentService.getVehicleWarnings();
res.status(200).json({ success: true, data: warnings });
} catch (error) {
logger.error('getVehicleWarnings error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Warnungen konnten nicht geladen werden' });
}
}
async createEquipment(req: Request, res: Response): Promise<void> {
try {
const parsed = CreateAusruestungSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
const equipment = await equipmentService.createEquipment(parsed.data, getUserId(req));
res.status(201).json({ success: true, data: equipment });
} catch (error) {
logger.error('createEquipment error', { error });
res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht erstellt werden' });
}
}
async updateEquipment(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' });
return;
}
const parsed = UpdateAusruestungSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
if (Object.keys(parsed.data).length === 0) {
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
return;
}
const equipment = await equipmentService.updateEquipment(id, parsed.data, getUserId(req));
if (!equipment) {
res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: equipment });
} catch (error: any) {
if (error?.message === 'No fields to update') {
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
return;
}
logger.error('updateEquipment error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht aktualisiert werden' });
}
}
async updateStatus(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' });
return;
}
const parsed = UpdateStatusSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
await equipmentService.updateStatus(
id, parsed.data.status, parsed.data.bemerkung, getUserId(req)
);
res.status(200).json({ success: true, message: 'Status aktualisiert' });
} catch (error: any) {
if (error?.message === 'Equipment not found') {
res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' });
return;
}
logger.error('updateStatus error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' });
}
}
async deleteEquipment(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' });
return;
}
const deleted = await equipmentService.deleteEquipment(id, getUserId(req));
if (!deleted) {
res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Ausrüstung gelöscht' });
} catch (error) {
logger.error('deleteEquipment error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht gelöscht werden' });
}
}
async addWartung(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' });
return;
}
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 equipmentService.addWartungslog(id, parsed.data, getUserId(req));
res.status(201).json({ success: true, data: entry });
} catch (error: any) {
if (error?.message === 'Equipment not found') {
res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' });
return;
}
logger.error('addWartung error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Wartungseintrag konnte nicht gespeichert werden' });
}
}
}
export default new EquipmentController();