refine vehicle freatures
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Fab,
|
||||
@@ -16,11 +17,18 @@ import {
|
||||
Grid,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
Link,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Tab,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tabs,
|
||||
TextField,
|
||||
Tooltip,
|
||||
@@ -32,6 +40,7 @@ import {
|
||||
Assignment,
|
||||
Build,
|
||||
CheckCircle,
|
||||
DeleteOutline,
|
||||
DirectionsCar,
|
||||
Edit,
|
||||
Error as ErrorIcon,
|
||||
@@ -40,12 +49,14 @@ import {
|
||||
PauseCircle,
|
||||
ReportProblem,
|
||||
School,
|
||||
Star,
|
||||
Verified,
|
||||
Warning,
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { vehiclesApi } from '../services/vehicles';
|
||||
import { equipmentApi } from '../services/equipment';
|
||||
import {
|
||||
FahrzeugDetail,
|
||||
FahrzeugWartungslog,
|
||||
@@ -55,7 +66,10 @@ import {
|
||||
UpdateStatusPayload,
|
||||
WartungslogArt,
|
||||
} from '../types/vehicle.types';
|
||||
import type { AusruestungListItem } from '../types/equipment.types';
|
||||
import { AusruestungStatus, AusruestungStatusLabel } from '../types/equipment.types';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
|
||||
// ── Tab Panel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -195,12 +209,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
|
||||
{ label: 'Bezeichnung', value: vehicle.bezeichnung },
|
||||
{ label: 'Kurzname', value: vehicle.kurzname },
|
||||
{ label: 'Kennzeichen', value: vehicle.amtliches_kennzeichen },
|
||||
{ label: 'Fahrgestellnr.', value: vehicle.fahrgestellnummer },
|
||||
{ label: 'Baujahr', value: vehicle.baujahr?.toString() },
|
||||
{ label: 'Hersteller', value: vehicle.hersteller },
|
||||
{ label: 'Typ (DIN 14502)', value: vehicle.typ_schluessel },
|
||||
{ label: 'Besatzung (Soll)', value: vehicle.besatzung_soll },
|
||||
{ label: 'Standort', value: vehicle.standort },
|
||||
].map(({ label, value }) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={label}>
|
||||
<Typography variant="caption" color="text.secondary" textTransform="uppercase">
|
||||
@@ -492,17 +500,153 @@ const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdde
|
||||
);
|
||||
};
|
||||
|
||||
// ── Ausrüstung Tab ───────────────────────────────────────────────────────────
|
||||
|
||||
const EQUIPMENT_STATUS_COLOR: Record<AusruestungStatus, 'success' | 'error' | 'warning' | 'default'> = {
|
||||
[AusruestungStatus.Einsatzbereit]: 'success',
|
||||
[AusruestungStatus.Beschaedigt]: 'error',
|
||||
[AusruestungStatus.InWartung]: 'warning',
|
||||
[AusruestungStatus.AusserDienst]: 'default',
|
||||
};
|
||||
|
||||
function pruefungBadgeColor(tage: number | null): 'success' | 'warning' | 'error' | 'default' {
|
||||
if (tage === null) return 'default';
|
||||
if (tage < 0) return 'error';
|
||||
if (tage <= 30) return 'warning';
|
||||
return 'success';
|
||||
}
|
||||
|
||||
interface AusruestungTabProps {
|
||||
equipment: AusruestungListItem[];
|
||||
vehicleId: string;
|
||||
}
|
||||
|
||||
const AusruestungTab: React.FC<AusruestungTabProps> = ({ equipment, vehicleId }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const hasProblems = equipment.some(
|
||||
(e) => e.status === AusruestungStatus.Beschaedigt || e.status === AusruestungStatus.InWartung
|
||||
);
|
||||
|
||||
if (equipment.length === 0) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Assignment sx={{ fontSize: 64, color: 'text.disabled', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Keine Ausrüstung zugewiesen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.disabled" sx={{ mb: 2 }}>
|
||||
Diesem Fahrzeug ist derzeit keine Ausrüstung zugeordnet.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => navigate('/ausruestung')}
|
||||
>
|
||||
Zur Ausrüstungsverwaltung
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{hasProblems && (
|
||||
<Alert severity="warning" icon={<Warning />} sx={{ mb: 2 }}>
|
||||
<strong>Achtung:</strong> Eine oder mehrere Ausrüstungen dieses Fahrzeugs sind beschädigt oder in Wartung.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Kategorie</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="center">Wichtig</TableCell>
|
||||
<TableCell>Nächste Prüfung</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{equipment.map((item) => {
|
||||
const statusColor = EQUIPMENT_STATUS_COLOR[item.status] ?? 'default';
|
||||
const pruefTage = item.pruefung_tage_bis_faelligkeit;
|
||||
const pruefColor = pruefungBadgeColor(pruefTage);
|
||||
|
||||
return (
|
||||
<TableRow key={item.id} hover>
|
||||
<TableCell>
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
fontWeight={600}
|
||||
underline="hover"
|
||||
onClick={() => navigate(`/ausruestung/${item.id}`)}
|
||||
sx={{ textAlign: 'left' }}
|
||||
>
|
||||
{item.bezeichnung}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={item.kategorie_kurzname} size="small" variant="outlined" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={AusruestungStatusLabel[item.status]}
|
||||
size="small"
|
||||
color={statusColor}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.ist_wichtig && (
|
||||
<Tooltip title="Wichtige Ausrüstung">
|
||||
<Star fontSize="small" color="warning" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.naechste_pruefung_am ? (
|
||||
<Chip
|
||||
size="small"
|
||||
color={pruefColor}
|
||||
variant={pruefColor === 'default' ? 'outlined' : 'filled'}
|
||||
label={
|
||||
pruefTage !== null && pruefTage < 0
|
||||
? `ÜBERFÄLLIG (${fmtDate(item.naechste_pruefung_am)})`
|
||||
: fmtDate(item.naechste_pruefung_am)
|
||||
}
|
||||
icon={pruefTage !== null && pruefTage < 0 ? <Warning fontSize="small" /> : undefined}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.disabled">—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function FahrzeugDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { isAdmin, canChangeStatus } = usePermissions();
|
||||
const notification = useNotification();
|
||||
|
||||
const [vehicle, setVehicle] = useState<FahrzeugDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [vehicleEquipment, setVehicleEquipment] = useState<AusruestungListItem[]>([]);
|
||||
|
||||
const fetchVehicle = useCallback(async () => {
|
||||
if (!id) return;
|
||||
@@ -511,6 +655,13 @@ function FahrzeugDetail() {
|
||||
setError(null);
|
||||
const data = await vehiclesApi.getById(id);
|
||||
setVehicle(data);
|
||||
// Fetch equipment separately — failure must not break the page
|
||||
try {
|
||||
const eq = await equipmentApi.getByVehicle(id);
|
||||
setVehicleEquipment(eq);
|
||||
} catch {
|
||||
setVehicleEquipment([]);
|
||||
}
|
||||
} catch {
|
||||
setError('Fahrzeug konnte nicht geladen werden.');
|
||||
} finally {
|
||||
@@ -520,6 +671,20 @@ function FahrzeugDetail() {
|
||||
|
||||
useEffect(() => { fetchVehicle(); }, [fetchVehicle]);
|
||||
|
||||
const handleDeleteVehicle = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
await vehiclesApi.delete(id);
|
||||
notification.showSuccess('Fahrzeug wurde erfolgreich gelöscht.');
|
||||
navigate('/fahrzeuge');
|
||||
} catch {
|
||||
notification.showError('Fahrzeug konnte nicht gelöscht werden.');
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
@@ -573,7 +738,6 @@ function FahrzeugDetail() {
|
||||
{vehicle.amtliches_kennzeichen && (
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
{vehicle.amtliches_kennzeichen}
|
||||
{vehicle.hersteller && ` · ${vehicle.hersteller}`}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -594,6 +758,18 @@ function FahrzeugDetail() {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Tooltip title="Fahrzeug löschen">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
aria-label="Fahrzeug löschen"
|
||||
>
|
||||
<DeleteOutline />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -614,6 +790,7 @@ function FahrzeugDetail() {
|
||||
}
|
||||
/>
|
||||
<Tab label="Einsätze" />
|
||||
<Tab label={`Ausrüstung${vehicleEquipment.length > 0 ? ` (${vehicleEquipment.length})` : ''}`} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -645,6 +822,38 @@ function FahrzeugDetail() {
|
||||
</Typography>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={3}>
|
||||
<AusruestungTab equipment={vehicleEquipment} vehicleId={vehicle.id} />
|
||||
</TabPanel>
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<Dialog open={deleteDialogOpen} onClose={() => !deleteLoading && setDeleteDialogOpen(false)}>
|
||||
<DialogTitle>Fahrzeug löschen</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Möchten Sie das Fahrzeug '{vehicle.bezeichnung}' wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
disabled={deleteLoading}
|
||||
autoFocus
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={handleDeleteVehicle}
|
||||
disabled={deleteLoading}
|
||||
startIcon={deleteLoading ? <CircularProgress size={16} /> : undefined}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user