319 lines
13 KiB
TypeScript
319 lines
13 KiB
TypeScript
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();
|