refine vehicle freatures
This commit is contained in:
@@ -60,6 +60,7 @@ import adminRoutes from './routes/admin.routes';
|
||||
import trainingRoutes from './routes/training.routes';
|
||||
import vehicleRoutes from './routes/vehicle.routes';
|
||||
import incidentRoutes from './routes/incident.routes';
|
||||
import equipmentRoutes from './routes/equipment.routes';
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
@@ -68,6 +69,7 @@ app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/training', trainingRoutes);
|
||||
app.use('/api/vehicles', vehicleRoutes);
|
||||
app.use('/api/incidents', incidentRoutes);
|
||||
app.use('/api/equipment', equipmentRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use(notFoundHandler);
|
||||
|
||||
318
backend/src/controllers/equipment.controller.ts
Normal file
318
backend/src/controllers/equipment.controller.ts
Normal 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();
|
||||
143
backend/src/database/migrations/011_create_ausruestung.sql
Normal file
143
backend/src/database/migrations/011_create_ausruestung.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
-- Migration 011: Ausrüstungsverwaltung (Equipment Management)
|
||||
-- Depends on: 001_create_users_table.sql (uuid-ossp extension, users table)
|
||||
-- 005_create_fahrzeuge.sql (fahrzeuge table)
|
||||
-- 009_vehicle_soft_delete.sql (soft-delete pattern)
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: ausruestung_kategorien (Equipment Categories — lookup)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS ausruestung_kategorien (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name VARCHAR(100) NOT NULL UNIQUE, -- e.g. 'Atemschutzgeräte'
|
||||
kurzname VARCHAR(30) NOT NULL UNIQUE, -- e.g. 'PA'
|
||||
sortierung INTEGER NOT NULL DEFAULT 0, -- display order
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: ausruestung (Core Equipment Inventory)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS ausruestung (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
bezeichnung VARCHAR(200) NOT NULL, -- e.g. 'Dräger PSS 5000'
|
||||
kategorie_id UUID NOT NULL REFERENCES ausruestung_kategorien(id),
|
||||
seriennummer VARCHAR(100), -- manufacturer serial
|
||||
inventarnummer VARCHAR(50), -- internal inventory number
|
||||
hersteller VARCHAR(150), -- manufacturer
|
||||
baujahr INTEGER CHECK (baujahr >= 1950 AND baujahr <= 2100),
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'einsatzbereit'
|
||||
CHECK (status IN (
|
||||
'einsatzbereit',
|
||||
'beschaedigt',
|
||||
'in_wartung',
|
||||
'ausser_dienst'
|
||||
)),
|
||||
status_bemerkung TEXT, -- free-text status note
|
||||
ist_wichtig BOOLEAN NOT NULL DEFAULT FALSE, -- drives vehicle card warnings
|
||||
fahrzeug_id UUID REFERENCES fahrzeuge(id) ON DELETE SET NULL, -- nullable
|
||||
standort VARCHAR(150) NOT NULL DEFAULT 'Lager', -- used when no fahrzeug
|
||||
pruef_intervall_monate INTEGER CHECK (pruef_intervall_monate > 0), -- nullable
|
||||
letzte_pruefung_am DATE,
|
||||
naechste_pruefung_am DATE,
|
||||
bemerkung TEXT, -- general notes
|
||||
deleted_at TIMESTAMPTZ, -- soft-delete
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_status
|
||||
ON ausruestung(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_kategorie
|
||||
ON ausruestung(kategorie_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_fahrzeug
|
||||
ON ausruestung(fahrzeug_id)
|
||||
WHERE fahrzeug_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_active
|
||||
ON ausruestung(id)
|
||||
WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_pruefung
|
||||
ON ausruestung(naechste_pruefung_am)
|
||||
WHERE naechste_pruefung_am IS NOT NULL AND deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_wichtig
|
||||
ON ausruestung(fahrzeug_id, status)
|
||||
WHERE ist_wichtig = TRUE AND deleted_at IS NULL;
|
||||
|
||||
-- Auto-update updated_at (reuses function from migration 001)
|
||||
CREATE TRIGGER update_ausruestung_updated_at
|
||||
BEFORE UPDATE ON ausruestung
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: ausruestung_wartungslog (Service/Inspection History)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS ausruestung_wartungslog (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
ausruestung_id UUID NOT NULL REFERENCES ausruestung(id) ON DELETE CASCADE,
|
||||
datum DATE NOT NULL,
|
||||
art VARCHAR(30) NOT NULL
|
||||
CHECK (art IN (
|
||||
'Prüfung',
|
||||
'Reparatur',
|
||||
'Sonstiges'
|
||||
)),
|
||||
beschreibung TEXT NOT NULL,
|
||||
ergebnis VARCHAR(30)
|
||||
CHECK (ergebnis IS NULL OR ergebnis IN (
|
||||
'bestanden',
|
||||
'bestanden_mit_maengeln',
|
||||
'nicht_bestanden'
|
||||
)),
|
||||
kosten DECIMAL(8,2) CHECK (kosten >= 0),
|
||||
pruefende_stelle VARCHAR(150), -- e.g. 'Atemschutzwerkstatt Bezirk'
|
||||
dokument_url VARCHAR(500), -- link to scan/PDF
|
||||
erfasst_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_wartung_item
|
||||
ON ausruestung_wartungslog(ausruestung_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ausruestung_wartung_datum
|
||||
ON ausruestung_wartungslog(datum DESC);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- VIEW: ausruestung_mit_pruefstatus
|
||||
-- For each active equipment item, joins category and vehicle
|
||||
-- and computes pruefung_tage_bis_faelligkeit (negative = overdue).
|
||||
-- The dashboard equipment panel and fleet overview query this view.
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE VIEW ausruestung_mit_pruefstatus AS
|
||||
SELECT
|
||||
a.*,
|
||||
k.name AS kategorie_name,
|
||||
k.kurzname AS kategorie_kurzname,
|
||||
f.bezeichnung AS fahrzeug_bezeichnung,
|
||||
f.kurzname AS fahrzeug_kurzname,
|
||||
CASE
|
||||
WHEN a.naechste_pruefung_am IS NOT NULL
|
||||
THEN a.naechste_pruefung_am::date - CURRENT_DATE
|
||||
ELSE NULL
|
||||
END AS pruefung_tage_bis_faelligkeit
|
||||
FROM ausruestung a
|
||||
JOIN ausruestung_kategorien k ON k.id = a.kategorie_id
|
||||
LEFT JOIN fahrzeuge f ON f.id = a.fahrzeug_id AND f.deleted_at IS NULL
|
||||
WHERE a.deleted_at IS NULL;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- SEED DATA: Equipment Categories
|
||||
-- ============================================================
|
||||
INSERT INTO ausruestung_kategorien (name, kurzname, sortierung) VALUES
|
||||
('Atemschutzgeräte', 'PA', 1),
|
||||
('Pumpen', 'Pumpe', 2),
|
||||
('Schläuche', 'SL', 3),
|
||||
('Leitern', 'Leiter', 4),
|
||||
('Rettungsgeräte', 'RG', 5),
|
||||
('Messgeräte', 'MG', 6),
|
||||
('Persönliche Schutzausrüstung', 'PSA', 7),
|
||||
('Kommunikation', 'Funk', 8),
|
||||
('Beleuchtung', 'Licht', 9),
|
||||
('Sonstige', 'Sonst.', 10)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
175
backend/src/models/equipment.model.ts
Normal file
175
backend/src/models/equipment.model.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
// =============================================================================
|
||||
// Equipment Management — Domain Model
|
||||
// =============================================================================
|
||||
|
||||
// ── Enums ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export enum AusruestungStatus {
|
||||
Einsatzbereit = 'einsatzbereit',
|
||||
Beschaedigt = 'beschaedigt',
|
||||
InWartung = 'in_wartung',
|
||||
AusserDienst = 'ausser_dienst',
|
||||
}
|
||||
|
||||
export const AusruestungStatusLabel: Record<AusruestungStatus, string> = {
|
||||
[AusruestungStatus.Einsatzbereit]: 'Einsatzbereit',
|
||||
[AusruestungStatus.Beschaedigt]: 'Beschädigt',
|
||||
[AusruestungStatus.InWartung]: 'In Wartung',
|
||||
[AusruestungStatus.AusserDienst]: 'Außer Dienst',
|
||||
};
|
||||
|
||||
export type AusruestungWartungslogArt = 'Prüfung' | 'Reparatur' | 'Sonstiges';
|
||||
|
||||
// ── Lookup Entity ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AusruestungKategorie {
|
||||
id: string;
|
||||
name: string;
|
||||
kurzname: string;
|
||||
sortierung: number;
|
||||
}
|
||||
|
||||
// ── Core Entity ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Ausruestung {
|
||||
id: string;
|
||||
bezeichnung: string;
|
||||
kategorie_id: string;
|
||||
seriennummer: string | null;
|
||||
inventarnummer: string | null;
|
||||
hersteller: string | null;
|
||||
baujahr: number | null;
|
||||
status: AusruestungStatus;
|
||||
status_bemerkung: string | null;
|
||||
ist_wichtig: boolean;
|
||||
fahrzeug_id: string | null;
|
||||
standort: string;
|
||||
pruef_intervall_monate: number | null;
|
||||
letzte_pruefung_am: Date | null;
|
||||
naechste_pruefung_am: Date | null;
|
||||
bemerkung: string | null;
|
||||
deleted_at: Date | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
// ── List Item (Grid / Card view) ──────────────────────────────────────────────
|
||||
|
||||
export interface AusruestungListItem {
|
||||
id: string;
|
||||
bezeichnung: string;
|
||||
kategorie_id: string;
|
||||
seriennummer: string | null;
|
||||
inventarnummer: string | null;
|
||||
hersteller: string | null;
|
||||
baujahr: number | null;
|
||||
status: AusruestungStatus;
|
||||
status_bemerkung: string | null;
|
||||
ist_wichtig: boolean;
|
||||
fahrzeug_id: string | null;
|
||||
standort: string;
|
||||
pruef_intervall_monate: number | null;
|
||||
letzte_pruefung_am: Date | null;
|
||||
naechste_pruefung_am: Date | null;
|
||||
bemerkung: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
kategorie_name: string;
|
||||
kategorie_kurzname: string;
|
||||
fahrzeug_bezeichnung: string | null;
|
||||
fahrzeug_kurzname: string | null;
|
||||
pruefung_tage_bis_faelligkeit: number | null;
|
||||
}
|
||||
|
||||
// ── Detail View ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AusruestungDetail extends AusruestungListItem {
|
||||
wartungslog: AusruestungWartungslog[];
|
||||
}
|
||||
|
||||
// ── Wartungslog Entity ────────────────────────────────────────────────────────
|
||||
|
||||
export interface AusruestungWartungslog {
|
||||
id: string;
|
||||
ausruestung_id: string;
|
||||
datum: Date;
|
||||
art: AusruestungWartungslogArt;
|
||||
beschreibung: string;
|
||||
ergebnis: string | null;
|
||||
kosten: number | null;
|
||||
pruefende_stelle: string | null;
|
||||
dokument_url: string | null;
|
||||
erfasst_von: string | null;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
// ── Dashboard KPI ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface EquipmentStats {
|
||||
total: number;
|
||||
einsatzbereit: number;
|
||||
beschaedigt: number;
|
||||
inWartung: number;
|
||||
ausserDienst: number;
|
||||
inspectionsDue: number;
|
||||
inspectionsOverdue: number;
|
||||
wichtigNichtBereit: number;
|
||||
}
|
||||
|
||||
// ── Vehicle Equipment Warning ─────────────────────────────────────────────────
|
||||
|
||||
export interface VehicleEquipmentWarning {
|
||||
fahrzeug_id: string;
|
||||
ausruestung_id: string;
|
||||
bezeichnung: string;
|
||||
status: AusruestungStatus;
|
||||
kategorie_name: string;
|
||||
}
|
||||
|
||||
// ── Create / Update DTOs ──────────────────────────────────────────────────────
|
||||
|
||||
export interface CreateAusruestungData {
|
||||
bezeichnung: string;
|
||||
kategorie_id: string;
|
||||
seriennummer?: string;
|
||||
inventarnummer?: string;
|
||||
hersteller?: string;
|
||||
baujahr?: number;
|
||||
status?: AusruestungStatus;
|
||||
status_bemerkung?: string;
|
||||
ist_wichtig?: boolean;
|
||||
fahrzeug_id?: string;
|
||||
standort?: string;
|
||||
pruef_intervall_monate?: number;
|
||||
letzte_pruefung_am?: string;
|
||||
naechste_pruefung_am?: string;
|
||||
bemerkung?: string;
|
||||
}
|
||||
|
||||
export interface UpdateAusruestungData {
|
||||
bezeichnung?: string;
|
||||
kategorie_id?: string;
|
||||
seriennummer?: string | null;
|
||||
inventarnummer?: string | null;
|
||||
hersteller?: string | null;
|
||||
baujahr?: number | null;
|
||||
status?: AusruestungStatus;
|
||||
status_bemerkung?: string | null;
|
||||
ist_wichtig?: boolean;
|
||||
fahrzeug_id?: string | null;
|
||||
standort?: string;
|
||||
pruef_intervall_monate?: number | null;
|
||||
letzte_pruefung_am?: string | null;
|
||||
naechste_pruefung_am?: string | null;
|
||||
bemerkung?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateAusruestungWartungslogData {
|
||||
datum: string;
|
||||
art: AusruestungWartungslogArt;
|
||||
beschreibung: string;
|
||||
ergebnis?: string;
|
||||
kosten?: number;
|
||||
pruefende_stelle?: string;
|
||||
dokument_url?: string;
|
||||
}
|
||||
32
backend/src/routes/equipment.routes.ts
Normal file
32
backend/src/routes/equipment.routes.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Router } from 'express';
|
||||
import equipmentController from '../controllers/equipment.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requireGroups } from '../middleware/rbac.middleware';
|
||||
|
||||
const ADMIN_GROUPS = ['dashboard_admin'];
|
||||
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister'];
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ── Read-only (any authenticated user) ───────────────────────────────────────
|
||||
|
||||
router.get('/', authenticate, equipmentController.listEquipment.bind(equipmentController));
|
||||
router.get('/stats', authenticate, equipmentController.getStats.bind(equipmentController));
|
||||
router.get('/alerts', authenticate, equipmentController.getAlerts.bind(equipmentController));
|
||||
router.get('/categories', authenticate, equipmentController.getCategories.bind(equipmentController));
|
||||
router.get('/vehicle-warnings', authenticate, equipmentController.getVehicleWarnings.bind(equipmentController));
|
||||
router.get('/vehicle/:fahrzeugId', authenticate, equipmentController.getByVehicle.bind(equipmentController));
|
||||
router.get('/:id', authenticate, equipmentController.getEquipment.bind(equipmentController));
|
||||
|
||||
// ── Write — admin + fahrmeister ──────────────────────────────────────────────
|
||||
|
||||
router.post('/', authenticate, requireGroups(WRITE_GROUPS), equipmentController.createEquipment.bind(equipmentController));
|
||||
router.patch('/:id', authenticate, requireGroups(WRITE_GROUPS), equipmentController.updateEquipment.bind(equipmentController));
|
||||
router.patch('/:id/status', authenticate, requireGroups(WRITE_GROUPS), equipmentController.updateStatus.bind(equipmentController));
|
||||
router.post('/:id/wartung', authenticate, requireGroups(WRITE_GROUPS), equipmentController.addWartung.bind(equipmentController));
|
||||
|
||||
// ── Delete — admin only ──────────────────────────────────────────────────────
|
||||
|
||||
router.delete('/:id', authenticate, requireGroups(ADMIN_GROUPS), equipmentController.deleteEquipment.bind(equipmentController));
|
||||
|
||||
export default router;
|
||||
414
backend/src/services/equipment.service.ts
Normal file
414
backend/src/services/equipment.service.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
import {
|
||||
Ausruestung,
|
||||
AusruestungKategorie,
|
||||
AusruestungListItem,
|
||||
AusruestungDetail,
|
||||
AusruestungWartungslog,
|
||||
CreateAusruestungData,
|
||||
UpdateAusruestungData,
|
||||
CreateAusruestungWartungslogData,
|
||||
AusruestungStatus,
|
||||
EquipmentStats,
|
||||
VehicleEquipmentWarning,
|
||||
} from '../models/equipment.model';
|
||||
|
||||
class EquipmentService {
|
||||
// =========================================================================
|
||||
// EQUIPMENT OVERVIEW
|
||||
// =========================================================================
|
||||
|
||||
async getAllEquipment(): Promise<AusruestungListItem[]> {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT *
|
||||
FROM ausruestung_mit_pruefstatus
|
||||
ORDER BY kategorie_kurzname, bezeichnung
|
||||
`);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
...row,
|
||||
pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null
|
||||
? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null,
|
||||
})) as AusruestungListItem[];
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getAllEquipment failed', { error });
|
||||
throw new Error('Failed to fetch equipment');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// EQUIPMENT DETAIL
|
||||
// =========================================================================
|
||||
|
||||
async getEquipmentById(id: string): Promise<AusruestungDetail | null> {
|
||||
try {
|
||||
const equipmentResult = await pool.query(
|
||||
`SELECT * FROM ausruestung_mit_pruefstatus WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (equipmentResult.rows.length === 0) return null;
|
||||
|
||||
const row = equipmentResult.rows[0];
|
||||
|
||||
const wartungslogResult = await pool.query(
|
||||
`SELECT * FROM ausruestung_wartungslog
|
||||
WHERE ausruestung_id = $1
|
||||
ORDER BY datum DESC, created_at DESC`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const equipment: AusruestungDetail = {
|
||||
...row,
|
||||
pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null
|
||||
? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null,
|
||||
wartungslog: wartungslogResult.rows.map(r => ({
|
||||
...r,
|
||||
kosten: r.kosten != null ? Number(r.kosten) : null,
|
||||
})) as AusruestungWartungslog[],
|
||||
};
|
||||
|
||||
return equipment;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getEquipmentById failed', { error, id });
|
||||
throw new Error('Failed to fetch equipment');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// EQUIPMENT BY VEHICLE
|
||||
// =========================================================================
|
||||
|
||||
async getEquipmentByVehicle(fahrzeugId: string): Promise<AusruestungListItem[]> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT *
|
||||
FROM ausruestung_mit_pruefstatus
|
||||
WHERE fahrzeug_id = $1
|
||||
ORDER BY kategorie_kurzname, bezeichnung`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
...row,
|
||||
pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null
|
||||
? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null,
|
||||
})) as AusruestungListItem[];
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getEquipmentByVehicle failed', { error, fahrzeugId });
|
||||
throw new Error('Failed to fetch equipment for vehicle');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CATEGORIES
|
||||
// =========================================================================
|
||||
|
||||
async getCategories(): Promise<AusruestungKategorie[]> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM ausruestung_kategorien ORDER BY sortierung`
|
||||
);
|
||||
return result.rows as AusruestungKategorie[];
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getCategories failed', { error });
|
||||
throw new Error('Failed to fetch equipment categories');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CRUD
|
||||
// =========================================================================
|
||||
|
||||
async createEquipment(data: CreateAusruestungData, createdBy: string): Promise<Ausruestung> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO ausruestung (
|
||||
id, bezeichnung, kategorie_id, seriennummer, inventarnummer,
|
||||
hersteller, baujahr, status, status_bemerkung, ist_wichtig,
|
||||
fahrzeug_id, standort, pruef_intervall_monate,
|
||||
letzte_pruefung_am, naechste_pruefung_am, bemerkung
|
||||
) VALUES (uuid_generate_v4(),$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.bezeichnung,
|
||||
data.kategorie_id,
|
||||
data.seriennummer ?? null,
|
||||
data.inventarnummer ?? null,
|
||||
data.hersteller ?? null,
|
||||
data.baujahr ?? null,
|
||||
data.status ?? AusruestungStatus.Einsatzbereit,
|
||||
data.status_bemerkung ?? null,
|
||||
data.ist_wichtig ?? false,
|
||||
data.fahrzeug_id ?? null,
|
||||
data.standort ?? 'Lager',
|
||||
data.pruef_intervall_monate ?? null,
|
||||
data.letzte_pruefung_am ?? null,
|
||||
data.naechste_pruefung_am ?? null,
|
||||
data.bemerkung ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
const equipment = result.rows[0] as Ausruestung;
|
||||
logger.info('Equipment created', { id: equipment.id, by: createdBy });
|
||||
return equipment;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.createEquipment failed', { error, createdBy });
|
||||
throw new Error('Failed to create equipment');
|
||||
}
|
||||
}
|
||||
|
||||
async updateEquipment(id: string, data: UpdateAusruestungData, updatedBy: string): Promise<Ausruestung | null> {
|
||||
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.kategorie_id !== undefined) addField('kategorie_id', data.kategorie_id);
|
||||
if (data.seriennummer !== undefined) addField('seriennummer', data.seriennummer);
|
||||
if (data.inventarnummer !== undefined) addField('inventarnummer', data.inventarnummer);
|
||||
if (data.hersteller !== undefined) addField('hersteller', data.hersteller);
|
||||
if (data.baujahr !== undefined) addField('baujahr', data.baujahr);
|
||||
if (data.status !== undefined) addField('status', data.status);
|
||||
if (data.status_bemerkung !== undefined) addField('status_bemerkung', data.status_bemerkung);
|
||||
if (data.ist_wichtig !== undefined) addField('ist_wichtig', data.ist_wichtig);
|
||||
if (data.fahrzeug_id !== undefined) addField('fahrzeug_id', data.fahrzeug_id);
|
||||
if (data.standort !== undefined) addField('standort', data.standort);
|
||||
if (data.pruef_intervall_monate !== undefined) addField('pruef_intervall_monate', data.pruef_intervall_monate);
|
||||
if (data.letzte_pruefung_am !== undefined) addField('letzte_pruefung_am', data.letzte_pruefung_am);
|
||||
if (data.naechste_pruefung_am !== undefined) addField('naechste_pruefung_am', data.naechste_pruefung_am);
|
||||
if (data.bemerkung !== undefined) addField('bemerkung', data.bemerkung);
|
||||
|
||||
if (fields.length === 0) {
|
||||
throw new Error('No fields to update');
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE ausruestung SET ${fields.join(', ')} WHERE id = $${p} AND deleted_at IS NULL RETURNING *`,
|
||||
values
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const equipment = result.rows[0] as Ausruestung;
|
||||
logger.info('Equipment updated', { id, by: updatedBy });
|
||||
return equipment;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.updateEquipment failed', { error, id, updatedBy });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEquipment(id: string, deletedBy: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE ausruestung
|
||||
SET deleted_at = NOW()
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING id`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('Equipment soft-deleted', { id, by: deletedBy });
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.deleteEquipment failed', { error, id });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// STATUS MANAGEMENT
|
||||
// =========================================================================
|
||||
|
||||
async updateStatus(
|
||||
id: string,
|
||||
status: AusruestungStatus,
|
||||
bemerkung: string,
|
||||
updatedBy: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE ausruestung
|
||||
SET status = $1, status_bemerkung = $2, updated_at = NOW()
|
||||
WHERE id = $3 AND deleted_at IS NULL
|
||||
RETURNING id`,
|
||||
[status, bemerkung || null, id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Equipment not found');
|
||||
}
|
||||
|
||||
logger.info('Equipment status updated', { id, status, by: updatedBy });
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.updateStatus failed', { error, id });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MAINTENANCE LOG
|
||||
// =========================================================================
|
||||
|
||||
async addWartungslog(
|
||||
equipmentId: string,
|
||||
data: CreateAusruestungWartungslogData,
|
||||
createdBy: string
|
||||
): Promise<AusruestungWartungslog> {
|
||||
try {
|
||||
const check = await pool.query(
|
||||
`SELECT 1 FROM ausruestung WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[equipmentId]
|
||||
);
|
||||
if (check.rows.length === 0) {
|
||||
throw new Error('Equipment not found');
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO ausruestung_wartungslog (
|
||||
ausruestung_id, datum, art, beschreibung,
|
||||
ergebnis, kosten, pruefende_stelle, dokument_url, erfasst_von
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
RETURNING *`,
|
||||
[
|
||||
equipmentId,
|
||||
data.datum,
|
||||
data.art,
|
||||
data.beschreibung,
|
||||
data.ergebnis ?? null,
|
||||
data.kosten ?? null,
|
||||
data.pruefende_stelle ?? null,
|
||||
data.dokument_url ?? null,
|
||||
createdBy,
|
||||
]
|
||||
);
|
||||
|
||||
const entry = result.rows[0] as AusruestungWartungslog;
|
||||
logger.info('Equipment wartungslog entry added', { entryId: entry.id, equipmentId, by: createdBy });
|
||||
return entry;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.addWartungslog failed', { error, equipmentId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DASHBOARD KPI
|
||||
// =========================================================================
|
||||
|
||||
async getEquipmentStats(): Promise<EquipmentStats> {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE status = 'einsatzbereit') AS einsatzbereit,
|
||||
COUNT(*) FILTER (WHERE status = 'beschaedigt') AS beschaedigt,
|
||||
COUNT(*) FILTER (WHERE status = 'in_wartung') AS in_wartung,
|
||||
COUNT(*) FILTER (WHERE status = 'ausser_dienst') AS ausser_dienst,
|
||||
COUNT(*) FILTER (
|
||||
WHERE naechste_pruefung_am IS NOT NULL
|
||||
AND naechste_pruefung_am::date - CURRENT_DATE BETWEEN 0 AND 30
|
||||
) AS inspections_due,
|
||||
COUNT(*) FILTER (
|
||||
WHERE naechste_pruefung_am IS NOT NULL
|
||||
AND naechste_pruefung_am::date < CURRENT_DATE
|
||||
) AS inspections_overdue,
|
||||
COUNT(*) FILTER (
|
||||
WHERE ist_wichtig = TRUE
|
||||
AND status != 'einsatzbereit'
|
||||
) AS wichtig_nicht_bereit
|
||||
FROM ausruestung
|
||||
WHERE deleted_at IS NULL
|
||||
`);
|
||||
|
||||
const row = result.rows[0] ?? {};
|
||||
|
||||
return {
|
||||
total: parseInt(row.total ?? '0', 10),
|
||||
einsatzbereit: parseInt(row.einsatzbereit ?? '0', 10),
|
||||
beschaedigt: parseInt(row.beschaedigt ?? '0', 10),
|
||||
inWartung: parseInt(row.in_wartung ?? '0', 10),
|
||||
ausserDienst: parseInt(row.ausser_dienst ?? '0', 10),
|
||||
inspectionsDue: parseInt(row.inspections_due ?? '0', 10),
|
||||
inspectionsOverdue: parseInt(row.inspections_overdue ?? '0', 10),
|
||||
wichtigNichtBereit: parseInt(row.wichtig_nicht_bereit ?? '0', 10),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getEquipmentStats failed', { error });
|
||||
throw new Error('Failed to fetch equipment stats');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// VEHICLE WARNINGS
|
||||
// =========================================================================
|
||||
|
||||
async getVehicleWarnings(): Promise<VehicleEquipmentWarning[]> {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.fahrzeug_id,
|
||||
a.id AS ausruestung_id,
|
||||
a.bezeichnung,
|
||||
a.status,
|
||||
k.name AS kategorie_name
|
||||
FROM ausruestung a
|
||||
JOIN ausruestung_kategorien k ON k.id = a.kategorie_id
|
||||
WHERE a.ist_wichtig = TRUE
|
||||
AND a.fahrzeug_id IS NOT NULL
|
||||
AND a.status != 'einsatzbereit'
|
||||
AND a.deleted_at IS NULL
|
||||
ORDER BY a.fahrzeug_id
|
||||
`);
|
||||
|
||||
return result.rows as VehicleEquipmentWarning[];
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getVehicleWarnings failed', { error });
|
||||
throw new Error('Failed to fetch vehicle equipment warnings');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// UPCOMING INSPECTIONS
|
||||
// =========================================================================
|
||||
|
||||
async getUpcomingInspections(daysAhead: number = 30): Promise<AusruestungListItem[]> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT *
|
||||
FROM ausruestung_mit_pruefstatus
|
||||
WHERE pruefung_tage_bis_faelligkeit IS NOT NULL
|
||||
AND pruefung_tage_bis_faelligkeit <= $1
|
||||
ORDER BY pruefung_tage_bis_faelligkeit ASC`,
|
||||
[daysAhead]
|
||||
);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
...row,
|
||||
pruefung_tage_bis_faelligkeit: row.pruefung_tage_bis_faelligkeit != null
|
||||
? parseInt(row.pruefung_tage_bis_faelligkeit, 10) : null,
|
||||
})) as AusruestungListItem[];
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.getUpcomingInspections failed', { error, daysAhead });
|
||||
throw new Error('Failed to fetch upcoming inspections');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new EquipmentService();
|
||||
Reference in New Issue
Block a user