refactor: move vehicle type assignment from detail page to settings page
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
@@ -61,8 +60,6 @@ import { vehiclesApi } from '../services/vehicles';
|
||||
import GermanDateField from '../components/shared/GermanDateField';
|
||||
import { fromGermanDate } from '../utils/dateInput';
|
||||
import { equipmentApi } from '../services/equipment';
|
||||
import { fahrzeugTypenApi } from '../services/fahrzeugTypen';
|
||||
import type { FahrzeugTyp } from '../types/checklist.types';
|
||||
import {
|
||||
FahrzeugDetail as FahrzeugDetailType,
|
||||
FahrzeugWartungslog,
|
||||
@@ -207,43 +204,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [overlappingBookings, setOverlappingBookings] = useState<OverlappingBooking[]>([]);
|
||||
|
||||
// ── Fahrzeugtypen ──
|
||||
const [allTypes, setAllTypes] = useState<FahrzeugTyp[]>([]);
|
||||
const [vehicleTypes, setVehicleTypes] = useState<FahrzeugTyp[]>([]);
|
||||
const [typesLoading, setTypesLoading] = useState(true);
|
||||
const [editingTypes, setEditingTypes] = useState(false);
|
||||
const [selectedTypes, setSelectedTypes] = useState<FahrzeugTyp[]>([]);
|
||||
const [typesSaving, setTypesSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
fahrzeugTypenApi.getAll(),
|
||||
fahrzeugTypenApi.getTypesForVehicle(vehicle.id),
|
||||
])
|
||||
.then(([all, assigned]) => {
|
||||
if (cancelled) return;
|
||||
setAllTypes(all);
|
||||
setVehicleTypes(assigned);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { if (!cancelled) setTypesLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [vehicle.id]);
|
||||
|
||||
const handleSaveTypes = async () => {
|
||||
try {
|
||||
setTypesSaving(true);
|
||||
await fahrzeugTypenApi.setTypesForVehicle(vehicle.id, selectedTypes.map((t) => t.id));
|
||||
setVehicleTypes(selectedTypes);
|
||||
setEditingTypes(false);
|
||||
} catch {
|
||||
// silent — keep dialog open
|
||||
} finally {
|
||||
setTypesSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isAusserDienst = (s: FahrzeugStatus) =>
|
||||
s === FahrzeugStatus.AusserDienstWartung || s === FahrzeugStatus.AusserDienstSchaden;
|
||||
|
||||
@@ -383,50 +343,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Fahrzeugtypen */}
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1.5 }}>
|
||||
Fahrzeugtypen
|
||||
</Typography>
|
||||
{typesLoading ? (
|
||||
<CircularProgress size={20} />
|
||||
) : editingTypes ? (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={allTypes}
|
||||
getOptionLabel={(o) => o.name}
|
||||
value={selectedTypes}
|
||||
onChange={(_e, val) => setSelectedTypes(val)}
|
||||
isOptionEqualToValue={(a, b) => a.id === b.id}
|
||||
renderInput={(params) => <TextField {...params} label="Fahrzeugtypen" size="small" />}
|
||||
sx={{ minWidth: 300, flexGrow: 1 }}
|
||||
/>
|
||||
<Button variant="contained" size="small" onClick={handleSaveTypes} disabled={typesSaving}>
|
||||
{typesSaving ? <CircularProgress size={16} /> : 'Speichern'}
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setEditingTypes(false)}>Abbrechen</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
{vehicleTypes.length === 0 ? (
|
||||
<Typography variant="body2" color="text.disabled">Keine Typen zugewiesen</Typography>
|
||||
) : (
|
||||
vehicleTypes.map((t) => (
|
||||
<Chip key={t.id} label={t.name} size="small" variant="outlined" />
|
||||
))
|
||||
)}
|
||||
{canEdit && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => { setSelectedTypes(vehicleTypes); setEditingTypes(true); }}
|
||||
aria-label="Fahrzeugtypen bearbeiten"
|
||||
>
|
||||
<Edit fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Inspection deadline quick view */}
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1.5 }}>
|
||||
Prüf- und Wartungsfristen
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
Paper,
|
||||
Table,
|
||||
@@ -31,6 +34,7 @@ import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { fahrzeugTypenApi } from '../services/fahrzeugTypen';
|
||||
import { vehiclesApi } from '../services/vehicles';
|
||||
import type { FahrzeugTyp } from '../types/checklist.types';
|
||||
|
||||
export default function FahrzeugEinstellungen() {
|
||||
@@ -229,7 +233,121 @@ export default function FahrzeugEinstellungen() {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Divider sx={{ my: 4 }} />
|
||||
<VehicleTypeAssignment allTypes={fahrzeugTypen} />
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Per-vehicle type assignment ────────────────────────────────────────────────
|
||||
|
||||
function VehicleTypeAssignment({ allTypes }: { allTypes: FahrzeugTyp[] }) {
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const { data: vehicles = [], isLoading } = useQuery({
|
||||
queryKey: ['vehicles'],
|
||||
queryFn: vehiclesApi.getAll,
|
||||
});
|
||||
|
||||
const [assignDialog, setAssignDialog] = useState<{ vehicleId: string; vehicleName: string; current: FahrzeugTyp[] } | null>(null);
|
||||
const [selected, setSelected] = useState<FahrzeugTyp[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// cache of per-vehicle types: vehicleId → FahrzeugTyp[]
|
||||
const [vehicleTypesMap, setVehicleTypesMap] = useState<Record<string, FahrzeugTyp[]>>({});
|
||||
|
||||
const openAssign = async (vehicleId: string, vehicleName: string) => {
|
||||
let current = vehicleTypesMap[vehicleId];
|
||||
if (!current) {
|
||||
try { current = await fahrzeugTypenApi.getTypesForVehicle(vehicleId); }
|
||||
catch { current = []; }
|
||||
setVehicleTypesMap((m) => ({ ...m, [vehicleId]: current }));
|
||||
}
|
||||
setSelected(current);
|
||||
setAssignDialog({ vehicleId, vehicleName, current });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!assignDialog) return;
|
||||
try {
|
||||
setSaving(true);
|
||||
await fahrzeugTypenApi.setTypesForVehicle(assignDialog.vehicleId, selected.map((t) => t.id));
|
||||
setVehicleTypesMap((m) => ({ ...m, [assignDialog.vehicleId]: selected }));
|
||||
setAssignDialog(null);
|
||||
showSuccess('Typen gespeichert');
|
||||
} catch {
|
||||
showError('Fehler beim Speichern');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>Typzuweisung je Fahrzeug</Typography>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={24} />
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fahrzeug</TableCell>
|
||||
<TableCell>Zugewiesene Typen</TableCell>
|
||||
<TableCell align="right">Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{vehicles.map((v) => {
|
||||
const types = vehicleTypesMap[v.id];
|
||||
return (
|
||||
<TableRow key={v.id} hover>
|
||||
<TableCell>{v.bezeichnung ?? v.kurzname}</TableCell>
|
||||
<TableCell>
|
||||
{types === undefined ? (
|
||||
<Typography variant="body2" color="text.disabled">–</Typography>
|
||||
) : types.length === 0 ? (
|
||||
<Typography variant="body2" color="text.disabled">Keine</Typography>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{types.map((t) => <Chip key={t.id} label={t.name} size="small" variant="outlined" />)}
|
||||
</Box>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => openAssign(v.id, v.bezeichnung ?? v.kurzname ?? v.id)}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Dialog open={!!assignDialog} onClose={() => setAssignDialog(null)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Typen für {assignDialog?.vehicleName}</DialogTitle>
|
||||
<DialogContent sx={{ mt: 1 }}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={allTypes}
|
||||
getOptionLabel={(o) => o.name}
|
||||
value={selected}
|
||||
onChange={(_e, val) => setSelected(val)}
|
||||
isOptionEqualToValue={(a, b) => a.id === b.id}
|
||||
renderInput={(params) => <TextField {...params} label="Fahrzeugtypen" />}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setAssignDialog(null)}>Abbrechen</Button>
|
||||
<Button variant="contained" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <CircularProgress size={20} /> : 'Speichern'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user