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 React, { useEffect, useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Autocomplete,
|
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
Chip,
|
||||||
@@ -61,8 +60,6 @@ import { vehiclesApi } from '../services/vehicles';
|
|||||||
import GermanDateField from '../components/shared/GermanDateField';
|
import GermanDateField from '../components/shared/GermanDateField';
|
||||||
import { fromGermanDate } from '../utils/dateInput';
|
import { fromGermanDate } from '../utils/dateInput';
|
||||||
import { equipmentApi } from '../services/equipment';
|
import { equipmentApi } from '../services/equipment';
|
||||||
import { fahrzeugTypenApi } from '../services/fahrzeugTypen';
|
|
||||||
import type { FahrzeugTyp } from '../types/checklist.types';
|
|
||||||
import {
|
import {
|
||||||
FahrzeugDetail as FahrzeugDetailType,
|
FahrzeugDetail as FahrzeugDetailType,
|
||||||
FahrzeugWartungslog,
|
FahrzeugWartungslog,
|
||||||
@@ -207,43 +204,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
|
|||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
const [overlappingBookings, setOverlappingBookings] = useState<OverlappingBooking[]>([]);
|
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) =>
|
const isAusserDienst = (s: FahrzeugStatus) =>
|
||||||
s === FahrzeugStatus.AusserDienstWartung || s === FahrzeugStatus.AusserDienstSchaden;
|
s === FahrzeugStatus.AusserDienstWartung || s === FahrzeugStatus.AusserDienstSchaden;
|
||||||
|
|
||||||
@@ -383,50 +343,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</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 */}
|
{/* Inspection deadline quick view */}
|
||||||
<Typography variant="h6" sx={{ mt: 3, mb: 1.5 }}>
|
<Typography variant="h6" sx={{ mt: 3, mb: 1.5 }}>
|
||||||
Prüf- und Wartungsfristen
|
Prüf- und Wartungsfristen
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Autocomplete,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
Table,
|
Table,
|
||||||
@@ -31,6 +34,7 @@ import DashboardLayout from '../components/dashboard/DashboardLayout';
|
|||||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
import { fahrzeugTypenApi } from '../services/fahrzeugTypen';
|
import { fahrzeugTypenApi } from '../services/fahrzeugTypen';
|
||||||
|
import { vehiclesApi } from '../services/vehicles';
|
||||||
import type { FahrzeugTyp } from '../types/checklist.types';
|
import type { FahrzeugTyp } from '../types/checklist.types';
|
||||||
|
|
||||||
export default function FahrzeugEinstellungen() {
|
export default function FahrzeugEinstellungen() {
|
||||||
@@ -229,7 +233,121 @@ export default function FahrzeugEinstellungen() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 4 }} />
|
||||||
|
<VehicleTypeAssignment allTypes={fahrzeugTypen} />
|
||||||
</Container>
|
</Container>
|
||||||
</DashboardLayout>
|
</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