rework vehicle handling
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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'));
|
||||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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" />
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user