feat(frontend): implement unified design system with 17 reusable template components, skeleton loading states, and golden-ratio-based layouts
This commit is contained in:
@@ -21,14 +21,12 @@ import {
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Tab,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tabs,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
@@ -40,7 +38,6 @@ import {
|
||||
Build,
|
||||
CheckCircle,
|
||||
DeleteOutline,
|
||||
DirectionsCar,
|
||||
Edit,
|
||||
Error as ErrorIcon,
|
||||
History,
|
||||
@@ -55,6 +52,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { DetailLayout } from '../components/templates';
|
||||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||||
import { vehiclesApi } from '../services/vehicles';
|
||||
import GermanDateField from '../components/shared/GermanDateField';
|
||||
@@ -81,20 +79,6 @@ import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import FahrzeugChecklistTab from '../components/fahrzeuge/FahrzeugChecklistTab';
|
||||
|
||||
// ── Tab Panel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => (
|
||||
<div role="tabpanel" hidden={value !== index}>
|
||||
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Status config ─────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_ICONS: Record<FahrzeugStatus, React.ReactElement> = {
|
||||
@@ -190,7 +174,7 @@ interface UebersichtTabProps {
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated, canChangeStatus, canEdit }) => {
|
||||
const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated, canChangeStatus, canEdit: _canEdit }) => {
|
||||
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
|
||||
const [newStatus, setNewStatus] = useState<FahrzeugStatus>(vehicle.status);
|
||||
const [bemerkung, setBemerkung] = useState(vehicle.status_bemerkung ?? '');
|
||||
@@ -889,7 +873,6 @@ function FahrzeugDetail() {
|
||||
const [vehicle, setVehicle] = useState<FahrzeugDetailType | 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[]>([]);
|
||||
@@ -958,130 +941,105 @@ function FahrzeugDetail() {
|
||||
(vehicle.paragraph57a_tage_bis_faelligkeit !== null && vehicle.paragraph57a_tage_bis_faelligkeit < 0) ||
|
||||
(vehicle.wartung_tage_bis_faelligkeit !== null && vehicle.wartung_tage_bis_faelligkeit < 0);
|
||||
|
||||
const titleText = vehicle.kurzname
|
||||
? `${vehicle.bezeichnung} ${vehicle.kurzname}`
|
||||
: vehicle.bezeichnung;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Übersicht',
|
||||
content: (
|
||||
<UebersichtTab
|
||||
vehicle={vehicle}
|
||||
onStatusUpdated={fetchVehicle}
|
||||
canChangeStatus={canChangeStatus}
|
||||
canEdit={hasPermission('fahrzeuge:edit')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: hasOverdue
|
||||
? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
Wartung <Warning color="error" fontSize="small" />
|
||||
</Box>
|
||||
)
|
||||
: 'Wartung',
|
||||
content: (
|
||||
<WartungTab
|
||||
fahrzeugId={vehicle.id}
|
||||
wartungslog={vehicle.wartungslog ?? []}
|
||||
onAdded={fetchVehicle}
|
||||
canWrite={canManageMaintenance}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Einsätze',
|
||||
content: (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<LocalFireDepartment sx={{ fontSize: 64, color: 'text.disabled', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Einsatzhistorie
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.disabled">
|
||||
Die Einsatzverknüpfung wird in Tier 2 (Einsatzverwaltung) implementiert.
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Ausrüstung${vehicleEquipment.length > 0 ? ` (${vehicleEquipment.length})` : ''}`,
|
||||
content: <AusruestungTab equipment={vehicleEquipment} vehicleId={vehicle.id} />,
|
||||
},
|
||||
...(hasPermission('checklisten:view')
|
||||
? [{
|
||||
label: 'Checklisten',
|
||||
content: <FahrzeugChecklistTab fahrzeugId={vehicle.id} />,
|
||||
}]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
<Button
|
||||
startIcon={<ArrowBack />}
|
||||
onClick={() => navigate('/fahrzeuge')}
|
||||
sx={{ mb: 2 }}
|
||||
size="small"
|
||||
>
|
||||
Fahrzeugübersicht
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
||||
<DirectionsCar sx={{ fontSize: 36, color: 'text.secondary' }} />
|
||||
<Box>
|
||||
<Typography variant="h4" component="h1">
|
||||
{vehicle.bezeichnung}
|
||||
{vehicle.kurzname && (
|
||||
<Typography component="span" variant="h5" color="text.secondary" sx={{ ml: 1 }}>
|
||||
{vehicle.kurzname}
|
||||
</Typography>
|
||||
<DetailLayout
|
||||
title={titleText}
|
||||
backTo="/fahrzeuge"
|
||||
tabs={tabs}
|
||||
actions={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
icon={STATUS_ICONS[vehicle.status]}
|
||||
label={FahrzeugStatusLabel[vehicle.status]}
|
||||
color={STATUS_CHIP_COLOR[vehicle.status]}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<Tooltip title="Fahrzeug bearbeiten">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => navigate(`/fahrzeuge/${vehicle.id}/bearbeiten`)}
|
||||
aria-label="Fahrzeug bearbeiten"
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography>
|
||||
{vehicle.amtliches_kennzeichen && (
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
{vehicle.amtliches_kennzeichen}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
icon={STATUS_ICONS[vehicle.status]}
|
||||
label={FahrzeugStatusLabel[vehicle.status]}
|
||||
color={STATUS_CHIP_COLOR[vehicle.status]}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<Tooltip title="Fahrzeug bearbeiten">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => navigate(`/fahrzeuge/${vehicle.id}/bearbeiten`)}
|
||||
aria-label="Fahrzeug bearbeiten"
|
||||
>
|
||||
<Edit />
|
||||
</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>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mt: 2 }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, v) => setActiveTab(v)}
|
||||
aria-label="Fahrzeug Detailansicht"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
>
|
||||
<Tab label="Übersicht" />
|
||||
<Tab
|
||||
label={
|
||||
hasOverdue
|
||||
? <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
Wartung <Warning color="error" fontSize="small" />
|
||||
</Box>
|
||||
: 'Wartung'
|
||||
}
|
||||
/>
|
||||
<Tab label="Einsätze" />
|
||||
<Tab label={`Ausrüstung${vehicleEquipment.length > 0 ? ` (${vehicleEquipment.length})` : ''}`} />
|
||||
{hasPermission('checklisten:view') && <Tab label="Checklisten" />}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel value={activeTab} index={0}>
|
||||
<UebersichtTab
|
||||
vehicle={vehicle}
|
||||
onStatusUpdated={fetchVehicle}
|
||||
canChangeStatus={canChangeStatus}
|
||||
canEdit={hasPermission('fahrzeuge:edit')}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
<WartungTab
|
||||
fahrzeugId={vehicle.id}
|
||||
wartungslog={vehicle.wartungslog ?? []}
|
||||
onAdded={fetchVehicle}
|
||||
canWrite={canManageMaintenance}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={2}>
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<LocalFireDepartment sx={{ fontSize: 64, color: 'text.disabled', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Einsatzhistorie
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.disabled">
|
||||
Die Einsatzverknüpfung wird in Tier 2 (Einsatzverwaltung) implementiert.
|
||||
</Typography>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={3}>
|
||||
<AusruestungTab equipment={vehicleEquipment} vehicleId={vehicle.id} />
|
||||
</TabPanel>
|
||||
|
||||
{hasPermission('checklisten:view') && (
|
||||
<TabPanel value={activeTab} index={4}>
|
||||
<FahrzeugChecklistTab fahrzeugId={vehicle.id} />
|
||||
</TabPanel>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Tooltip title="Fahrzeug löschen">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
aria-label="Fahrzeug löschen"
|
||||
>
|
||||
<DeleteOutline />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<Dialog open={deleteDialogOpen} onClose={() => !deleteLoading && setDeleteDialogOpen(false)}>
|
||||
|
||||
Reference in New Issue
Block a user