From 1e478479beaad94c39762541d0233b5d04d24343 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Sat, 28 Feb 2026 13:57:41 +0100 Subject: [PATCH] rework vehicle handling --- backend/src/controllers/vehicle.controller.ts | 2 +- .../010_simplify_wartungslog_art.sql | 24 +++++++++++++++++++ backend/src/models/vehicle.model.ts | 8 ++----- .../incidents/IncidentStatsChart.tsx | 6 ++--- frontend/src/pages/Dashboard.tsx | 2 +- frontend/src/pages/FahrzeugDetail.tsx | 11 +++++---- frontend/src/pages/MitgliedDetail.tsx | 2 +- frontend/src/pages/Mitglieder.tsx | 2 +- frontend/src/services/incidents.ts | 15 ++++++++++++ frontend/src/services/members.ts | 19 +++++++++++++-- frontend/src/services/vehicles.ts | 12 ++++++++++ frontend/src/types/vehicle.types.ts | 8 ++----- 12 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 backend/src/database/migrations/010_simplify_wartungslog_art.sql diff --git a/backend/src/controllers/vehicle.controller.ts b/backend/src/controllers/vehicle.controller.ts index b4ef654..97f8e6b 100644 --- a/backend/src/controllers/vehicle.controller.ts +++ b/backend/src/controllers/vehicle.controller.ts @@ -71,7 +71,7 @@ const UpdateStatusSchema = z.object({ const CreateWartungslogSchema = z.object({ datum: isoDate, - art: z.enum(['Inspektion', 'Reparatur', 'Kraftstoff', 'Reifenwechsel', 'Hauptuntersuchung', 'Reinigung', 'Sonstiges']).optional(), + art: z.enum(['§57a Prüfung', 'Service', 'Sonstiges']).optional(), beschreibung: z.string().min(1).max(2000), km_stand: z.number().int().min(0).optional(), kraftstoff_liter: z.number().min(0).optional(), diff --git a/backend/src/database/migrations/010_simplify_wartungslog_art.sql b/backend/src/database/migrations/010_simplify_wartungslog_art.sql new file mode 100644 index 0000000..0941c9c --- /dev/null +++ b/backend/src/database/migrations/010_simplify_wartungslog_art.sql @@ -0,0 +1,24 @@ +-- Migration 010: Simplify WartungslogArt from 7 types to 3 +-- Maps existing data to new categories and updates the CHECK constraint. +-- +-- Old types: New type: +-- Inspektion -> Service +-- Reparatur -> Service +-- Reifenwechsel -> Service +-- Reinigung -> Service +-- Hauptuntersuchung -> §57a Prüfung +-- Kraftstoff -> Sonstiges +-- Sonstiges -> Sonstiges (unchanged) + +-- Step 1: Migrate existing data to new type values +UPDATE fahrzeug_wartungslog SET art = 'Service' WHERE art IN ('Inspektion', 'Reparatur', 'Reifenwechsel', 'Reinigung'); +UPDATE fahrzeug_wartungslog SET art = '§57a Prüfung' WHERE art = 'Hauptuntersuchung'; +UPDATE fahrzeug_wartungslog SET art = 'Sonstiges' WHERE art = 'Kraftstoff'; + +-- Step 2: Drop the old CHECK constraint on art +ALTER TABLE fahrzeug_wartungslog DROP CONSTRAINT IF EXISTS fahrzeug_wartungslog_art_check; + +-- Step 3: Add the new CHECK constraint with simplified types +ALTER TABLE fahrzeug_wartungslog + ADD CONSTRAINT fahrzeug_wartungslog_art_check + CHECK (art IS NULL OR art IN ('§57a Prüfung', 'Service', 'Sonstiges')); diff --git a/backend/src/models/vehicle.model.ts b/backend/src/models/vehicle.model.ts index f971497..df63202 100644 --- a/backend/src/models/vehicle.model.ts +++ b/backend/src/models/vehicle.model.ts @@ -19,12 +19,8 @@ export const FahrzeugStatusLabel: Record = { }; export type WartungslogArt = - | 'Inspektion' - | 'Reparatur' - | 'Kraftstoff' - | 'Reifenwechsel' - | 'Hauptuntersuchung' - | 'Reinigung' + | '§57a Prüfung' + | 'Service' | 'Sonstiges'; // ── Core Entities ───────────────────────────────────────────────────────────── diff --git a/frontend/src/components/incidents/IncidentStatsChart.tsx b/frontend/src/components/incidents/IncidentStatsChart.tsx index d71cd42..53fe759 100644 --- a/frontend/src/components/incidents/IncidentStatsChart.tsx +++ b/frontend/src/components/incidents/IncidentStatsChart.tsx @@ -111,12 +111,12 @@ const IncidentStatsChart: React.FC = ({ stats, loading } const monthlyData = buildMonthlyData( - stats.monthly, - stats.prev_year_monthly, + stats.monthly ?? [], + stats.prev_year_monthly ?? [], stats.jahr ); - const pieData = stats.by_art.map((row) => ({ + const pieData = (stats.by_art ?? []).map((row) => ({ name: EINSATZ_ART_LABELS[row.einsatz_art], value: row.anzahl, art: row.einsatz_art, diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 77b586a..ef91630 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -140,7 +140,7 @@ function Dashboard() { title="Fahrzeuge einsatzbereit" value={ vehicleStats - ? `${vehicleStats.einsatzbereit}/${vehicleStats.total}` + ? `${vehicleStats?.einsatzbereit}/${vehicleStats?.total}` : '—' } icon={DirectionsCar} diff --git a/frontend/src/pages/FahrzeugDetail.tsx b/frontend/src/pages/FahrzeugDetail.tsx index 1563319..67e0d3b 100644 --- a/frontend/src/pages/FahrzeugDetail.tsx +++ b/frontend/src/pages/FahrzeugDetail.tsx @@ -35,9 +35,11 @@ import { Edit, Error as ErrorIcon, LocalFireDepartment, + MoreHoriz, PauseCircle, ReportProblem, School, + Verified, Warning, } from '@mui/icons-material'; import { useNavigate, useParams } from 'react-router-dom'; @@ -303,10 +305,9 @@ interface WartungTabProps { } const WARTUNG_ART_ICONS: Record = { - Kraftstoff: , - Reparatur: , - Inspektion: , - Hauptuntersuchung: , + '§57a Prüfung': , + 'Service': , + 'Sonstiges': , default: , }; @@ -417,7 +418,7 @@ const WartungTab: React.FC = ({ fahrzeugId, wartungslog, onAdde onChange={(e) => setForm((f) => ({ ...f, art: (e.target.value || undefined) as WartungslogArt | undefined }))} > — Bitte wählen — - {(['Inspektion', 'Reparatur', 'Kraftstoff', 'Reifenwechsel', 'Hauptuntersuchung', 'Reinigung', 'Sonstiges'] as WartungslogArt[]).map((a) => ( + {(['§57a Prüfung', 'Service', 'Sonstiges'] as WartungslogArt[]).map((a) => ( {a} ))} diff --git a/frontend/src/pages/MitgliedDetail.tsx b/frontend/src/pages/MitgliedDetail.tsx index 3265567..69c744e 100644 --- a/frontend/src/pages/MitgliedDetail.tsx +++ b/frontend/src/pages/MitgliedDetail.tsx @@ -387,7 +387,7 @@ function MitgliedDetail() { )} - {profile && profile.funktion.length > 0 && ( + {profile && Array.isArray(profile.funktion) && profile.funktion.length > 0 && ( {profile.funktion.map((f) => ( diff --git a/frontend/src/pages/Mitglieder.tsx b/frontend/src/pages/Mitglieder.tsx index 400b0e7..74e975a 100644 --- a/frontend/src/pages/Mitglieder.tsx +++ b/frontend/src/pages/Mitglieder.tsx @@ -354,7 +354,7 @@ function Mitglieder() { {/* Funktion(en) */} - {member.funktion.length > 0 + {Array.isArray(member.funktion) && member.funktion.length > 0 ? member.funktion.map((f) => ( )) diff --git a/frontend/src/services/incidents.ts b/frontend/src/services/incidents.ts index b8a55e5..60d7289 100644 --- a/frontend/src/services/incidents.ts +++ b/frontend/src/services/incidents.ts @@ -216,6 +216,9 @@ export const incidentsApi = { const response = await api.get<{ success: boolean; data: IncidentListResponse }>( `/api/incidents?${params.toString()}` ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -227,6 +230,9 @@ export const incidentsApi = { const response = await api.get<{ success: boolean; data: EinsatzStats }>( `/api/incidents/stats${params}` ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -237,6 +243,9 @@ export const incidentsApi = { const response = await api.get<{ success: boolean; data: EinsatzDetail }>( `/api/incidents/${id}` ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -248,6 +257,9 @@ export const incidentsApi = { '/api/incidents', payload ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -259,6 +271,9 @@ export const incidentsApi = { `/api/incidents/${id}`, payload ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, diff --git a/frontend/src/services/members.ts b/frontend/src/services/members.ts index 0e60547..0dab506 100644 --- a/frontend/src/services/members.ts +++ b/frontend/src/services/members.ts @@ -55,10 +55,13 @@ export const membersService = { const response = await api.get>( `/api/members?${params.toString()}` ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return { items: response.data.data, - total: response.data.meta.total, - page: response.data.meta.page, + total: response.data.meta?.total ?? 0, + page: response.data.meta?.page ?? 1, }; }, @@ -69,6 +72,9 @@ export const membersService = { const response = await api.get>( `/api/members/${userId}` ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -84,6 +90,9 @@ export const membersService = { `/api/members/${userId}/profile`, data ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -100,6 +109,9 @@ export const membersService = { `/api/members/${userId}`, data ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -108,6 +120,9 @@ export const membersService = { */ async getMemberStats(): Promise { const response = await api.get>('/api/members/stats'); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, }; diff --git a/frontend/src/services/vehicles.ts b/frontend/src/services/vehicles.ts index b9c8a33..3604921 100644 --- a/frontend/src/services/vehicles.ts +++ b/frontend/src/services/vehicles.ts @@ -15,6 +15,9 @@ async function unwrap( promise: ReturnType> ): Promise { const response = await promise; + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; } @@ -44,6 +47,9 @@ export const vehiclesApi = { '/api/vehicles', payload ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -52,6 +58,9 @@ export const vehiclesApi = { `/api/vehicles/${id}`, payload ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, @@ -74,6 +83,9 @@ export const vehiclesApi = { `/api/vehicles/${id}/wartung`, payload ); + if (!response.data?.data) { + throw new Error('Invalid API response'); + } return response.data.data; }, }; diff --git a/frontend/src/types/vehicle.types.ts b/frontend/src/types/vehicle.types.ts index 952907c..5ca88ac 100644 --- a/frontend/src/types/vehicle.types.ts +++ b/frontend/src/types/vehicle.types.ts @@ -17,12 +17,8 @@ export const FahrzeugStatusLabel: Record = { }; export type WartungslogArt = - | 'Inspektion' - | 'Reparatur' - | 'Kraftstoff' - | 'Reifenwechsel' - | 'Hauptuntersuchung' - | 'Reinigung' + | '§57a Prüfung' + | 'Service' | 'Sonstiges'; // ── API Response Shapes ───────────────────────────────────────────────────────