rework vehicle handling

This commit is contained in:
Matthias Hochmeister
2026-02-28 13:57:41 +01:00
parent 41fc41bee4
commit 1e478479be
12 changed files with 85 additions and 26 deletions

View File

@@ -71,7 +71,7 @@ const UpdateStatusSchema = z.object({
const CreateWartungslogSchema = z.object({ const CreateWartungslogSchema = z.object({
datum: isoDate, 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), beschreibung: z.string().min(1).max(2000),
km_stand: z.number().int().min(0).optional(), km_stand: z.number().int().min(0).optional(),
kraftstoff_liter: z.number().min(0).optional(), kraftstoff_liter: z.number().min(0).optional(),

View File

@@ -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'));

View File

@@ -19,12 +19,8 @@ export const FahrzeugStatusLabel: Record<FahrzeugStatus, string> = {
}; };
export type WartungslogArt = export type WartungslogArt =
| 'Inspektion' | '§57a Prüfung'
| 'Reparatur' | 'Service'
| 'Kraftstoff'
| 'Reifenwechsel'
| 'Hauptuntersuchung'
| 'Reinigung'
| 'Sonstiges'; | 'Sonstiges';
// ── Core Entities ───────────────────────────────────────────────────────────── // ── Core Entities ─────────────────────────────────────────────────────────────

View File

@@ -111,12 +111,12 @@ const IncidentStatsChart: React.FC<IncidentStatsChartProps> = ({ stats, loading
} }
const monthlyData = buildMonthlyData( const monthlyData = buildMonthlyData(
stats.monthly, stats.monthly ?? [],
stats.prev_year_monthly, stats.prev_year_monthly ?? [],
stats.jahr stats.jahr
); );
const pieData = stats.by_art.map((row) => ({ const pieData = (stats.by_art ?? []).map((row) => ({
name: EINSATZ_ART_LABELS[row.einsatz_art], name: EINSATZ_ART_LABELS[row.einsatz_art],
value: row.anzahl, value: row.anzahl,
art: row.einsatz_art, art: row.einsatz_art,

View File

@@ -140,7 +140,7 @@ function Dashboard() {
title="Fahrzeuge einsatzbereit" title="Fahrzeuge einsatzbereit"
value={ value={
vehicleStats vehicleStats
? `${vehicleStats.einsatzbereit}/${vehicleStats.total}` ? `${vehicleStats?.einsatzbereit}/${vehicleStats?.total}`
: '—' : '—'
} }
icon={DirectionsCar} icon={DirectionsCar}

View File

@@ -35,9 +35,11 @@ import {
Edit, Edit,
Error as ErrorIcon, Error as ErrorIcon,
LocalFireDepartment, LocalFireDepartment,
MoreHoriz,
PauseCircle, PauseCircle,
ReportProblem, ReportProblem,
School, School,
Verified,
Warning, Warning,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
@@ -303,10 +305,9 @@ interface WartungTabProps {
} }
const WARTUNG_ART_ICONS: Record<string, React.ReactElement> = { const WARTUNG_ART_ICONS: Record<string, React.ReactElement> = {
Kraftstoff: <LocalFireDepartment color="action" />, '§57a Prüfung': <Verified color="success" />,
Reparatur: <Build color="warning" />, 'Service': <Build color="warning" />,
Inspektion: <Assignment color="primary" />, 'Sonstiges': <MoreHoriz color="action" />,
Hauptuntersuchung: <CheckCircle color="success" />,
default: <Build color="action" />, default: <Build color="action" />,
}; };
@@ -417,7 +418,7 @@ const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdde
onChange={(e) => setForm((f) => ({ ...f, art: (e.target.value || undefined) as WartungslogArt | undefined }))} onChange={(e) => setForm((f) => ({ ...f, art: (e.target.value || undefined) as WartungslogArt | undefined }))}
> >
<MenuItem value=""> Bitte wählen </MenuItem> <MenuItem value=""> Bitte wählen </MenuItem>
{(['Inspektion', 'Reparatur', 'Kraftstoff', 'Reifenwechsel', 'Hauptuntersuchung', 'Reinigung', 'Sonstiges'] as WartungslogArt[]).map((a) => ( {(['§57a Prüfung', 'Service', 'Sonstiges'] as WartungslogArt[]).map((a) => (
<MenuItem key={a} value={a}>{a}</MenuItem> <MenuItem key={a} value={a}>{a}</MenuItem>
))} ))}
</Select> </Select>

View File

@@ -387,7 +387,7 @@ function MitgliedDetail() {
</Typography> </Typography>
)} )}
{profile && profile.funktion.length > 0 && ( {profile && Array.isArray(profile.funktion) && profile.funktion.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, mt: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 0.5, mt: 1, flexWrap: 'wrap' }}>
{profile.funktion.map((f) => ( {profile.funktion.map((f) => (
<Chip key={f} label={f} size="small" color="secondary" variant="outlined" /> <Chip key={f} label={f} size="small" color="secondary" variant="outlined" />

View File

@@ -354,7 +354,7 @@ function Mitglieder() {
{/* Funktion(en) */} {/* Funktion(en) */}
<TableCell> <TableCell>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{member.funktion.length > 0 {Array.isArray(member.funktion) && member.funktion.length > 0
? member.funktion.map((f) => ( ? member.funktion.map((f) => (
<Chip key={f} label={f} size="small" variant="outlined" color="secondary" /> <Chip key={f} label={f} size="small" variant="outlined" color="secondary" />
)) ))

View File

@@ -216,6 +216,9 @@ export const incidentsApi = {
const response = await api.get<{ success: boolean; data: IncidentListResponse }>( const response = await api.get<{ success: boolean; data: IncidentListResponse }>(
`/api/incidents?${params.toString()}` `/api/incidents?${params.toString()}`
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -227,6 +230,9 @@ export const incidentsApi = {
const response = await api.get<{ success: boolean; data: EinsatzStats }>( const response = await api.get<{ success: boolean; data: EinsatzStats }>(
`/api/incidents/stats${params}` `/api/incidents/stats${params}`
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -237,6 +243,9 @@ export const incidentsApi = {
const response = await api.get<{ success: boolean; data: EinsatzDetail }>( const response = await api.get<{ success: boolean; data: EinsatzDetail }>(
`/api/incidents/${id}` `/api/incidents/${id}`
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -248,6 +257,9 @@ export const incidentsApi = {
'/api/incidents', '/api/incidents',
payload payload
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -259,6 +271,9 @@ export const incidentsApi = {
`/api/incidents/${id}`, `/api/incidents/${id}`,
payload payload
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },

View File

@@ -55,10 +55,13 @@ export const membersService = {
const response = await api.get<ApiListResponse<MemberListItem>>( const response = await api.get<ApiListResponse<MemberListItem>>(
`/api/members?${params.toString()}` `/api/members?${params.toString()}`
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return { return {
items: response.data.data, items: response.data.data,
total: response.data.meta.total, total: response.data.meta?.total ?? 0,
page: response.data.meta.page, page: response.data.meta?.page ?? 1,
}; };
}, },
@@ -69,6 +72,9 @@ export const membersService = {
const response = await api.get<ApiItemResponse<MemberWithProfile>>( const response = await api.get<ApiItemResponse<MemberWithProfile>>(
`/api/members/${userId}` `/api/members/${userId}`
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -84,6 +90,9 @@ export const membersService = {
`/api/members/${userId}/profile`, `/api/members/${userId}/profile`,
data data
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -100,6 +109,9 @@ export const membersService = {
`/api/members/${userId}`, `/api/members/${userId}`,
data data
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -108,6 +120,9 @@ export const membersService = {
*/ */
async getMemberStats(): Promise<MemberStats> { async getMemberStats(): Promise<MemberStats> {
const response = await api.get<ApiItemResponse<MemberStats>>('/api/members/stats'); const response = await api.get<ApiItemResponse<MemberStats>>('/api/members/stats');
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
}; };

View File

@@ -15,6 +15,9 @@ async function unwrap<T>(
promise: ReturnType<typeof api.get<{ success: boolean; data: T }>> promise: ReturnType<typeof api.get<{ success: boolean; data: T }>>
): Promise<T> { ): Promise<T> {
const response = await promise; const response = await promise;
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
} }
@@ -44,6 +47,9 @@ export const vehiclesApi = {
'/api/vehicles', '/api/vehicles',
payload payload
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -52,6 +58,9 @@ export const vehiclesApi = {
`/api/vehicles/${id}`, `/api/vehicles/${id}`,
payload payload
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
@@ -74,6 +83,9 @@ export const vehiclesApi = {
`/api/vehicles/${id}/wartung`, `/api/vehicles/${id}/wartung`,
payload payload
); );
if (!response.data?.data) {
throw new Error('Invalid API response');
}
return response.data.data; return response.data.data;
}, },
}; };

View File

@@ -17,12 +17,8 @@ export const FahrzeugStatusLabel: Record<FahrzeugStatus, string> = {
}; };
export type WartungslogArt = export type WartungslogArt =
| 'Inspektion' | '§57a Prüfung'
| 'Reparatur' | 'Service'
| 'Kraftstoff'
| 'Reifenwechsel'
| 'Hauptuntersuchung'
| 'Reinigung'
| 'Sonstiges'; | 'Sonstiges';
// ── API Response Shapes ─────────────────────────────────────────────────────── // ── API Response Shapes ───────────────────────────────────────────────────────