featur add fahrmeister

This commit is contained in:
Matthias Hochmeister
2026-02-27 21:46:50 +01:00
parent da4a56ba6b
commit dbe4f52871
17 changed files with 426 additions and 152 deletions

View File

@@ -0,0 +1,12 @@
import { useAuth } from '../contexts/AuthContext';
export function usePermissions() {
const { user } = useAuth();
const groups = user?.groups ?? [];
return {
isAdmin: groups.includes('dashboard_admin'),
canChangeStatus: groups.includes('dashboard_admin') || groups.includes('dashboard_fahrmeister'),
groups,
};
}

View File

@@ -62,6 +62,7 @@ import {
WartungslogArt,
PruefungErgebnis,
} from '../types/vehicle.types';
import { usePermissions } from '../hooks/usePermissions';
// ── Tab Panel ─────────────────────────────────────────────────────────────────
@@ -114,9 +115,10 @@ function inspectionBadgeColor(tage: number | null): 'success' | 'warning' | 'err
interface UebersichtTabProps {
vehicle: FahrzeugDetail;
onStatusUpdated: () => void;
canChangeStatus: boolean;
}
const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated }) => {
const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated, canChangeStatus }) => {
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
const [newStatus, setNewStatus] = useState<FahrzeugStatus>(vehicle.status);
const [bemerkung, setBemerkung] = useState(vehicle.status_bemerkung ?? '');
@@ -177,6 +179,7 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated
setBemerkung(vehicle.status_bemerkung ?? '');
setStatusDialogOpen(true);
}}
sx={{ display: canChangeStatus ? undefined : 'none' }}
>
Status ändern
</Button>
@@ -195,6 +198,8 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated
{ label: 'Typ (DIN 14502)', value: vehicle.typ_schluessel },
{ label: 'Besatzung (Soll)', value: vehicle.besatzung_soll },
{ label: 'Standort', value: vehicle.standort },
{ label: '§57a fällig am', value: fmtDate(vehicle.paragraph57a_faellig_am) !== '—' ? fmtDate(vehicle.paragraph57a_faellig_am) : null },
{ label: 'Nächste Wartung', value: fmtDate(vehicle.naechste_wartung_am) !== '—' ? fmtDate(vehicle.naechste_wartung_am) : null },
].map(({ label, value }) => (
<Grid item xs={12} sm={6} md={4} key={label}>
<Typography variant="caption" color="text.secondary" textTransform="uppercase">
@@ -311,6 +316,7 @@ interface PruefungenTabProps {
fahrzeugId: string;
pruefungen: FahrzeugPruefung[];
onAdded: () => void;
canWrite: boolean;
}
const ERGEBNIS_LABELS: Record<PruefungErgebnis, string> = {
@@ -327,7 +333,7 @@ const ERGEBNIS_COLORS: Record<PruefungErgebnis, 'success' | 'warning' | 'error'
ausstehend: 'default',
};
const PruefungenTab: React.FC<PruefungenTabProps> = ({ fahrzeugId, pruefungen, onAdded }) => {
const PruefungenTab: React.FC<PruefungenTabProps> = ({ fahrzeugId, pruefungen, onAdded, canWrite }) => {
const [dialogOpen, setDialogOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
@@ -431,15 +437,17 @@ const PruefungenTab: React.FC<PruefungenTabProps> = ({ fahrzeugId, pruefungen, o
)}
{/* FAB */}
<Fab
color="primary"
size="small"
aria-label="Prüfung hinzufügen"
sx={{ position: 'fixed', bottom: 32, right: 32 }}
onClick={() => { setForm(emptyForm); setDialogOpen(true); }}
>
<Add />
</Fab>
{canWrite && (
<Fab
color="primary"
size="small"
aria-label="Prüfung hinzufügen"
sx={{ position: 'fixed', bottom: 32, right: 32 }}
onClick={() => { setForm(emptyForm); setDialogOpen(true); }}
>
<Add />
</Fab>
)}
{/* Add inspection dialog */}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
@@ -549,6 +557,7 @@ interface WartungTabProps {
fahrzeugId: string;
wartungslog: FahrzeugWartungslog[];
onAdded: () => void;
canWrite: boolean;
}
const WARTUNG_ART_ICONS: Record<string, React.ReactElement> = {
@@ -559,7 +568,7 @@ const WARTUNG_ART_ICONS: Record<string, React.ReactElement> = {
default: <Build color="action" />,
};
const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdded }) => {
const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdded, canWrite }) => {
const [dialogOpen, setDialogOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
@@ -634,15 +643,17 @@ const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdde
</Stack>
)}
<Fab
color="primary"
size="small"
aria-label="Wartung eintragen"
sx={{ position: 'fixed', bottom: 32, right: 32 }}
onClick={() => { setForm(emptyForm); setDialogOpen(true); }}
>
<Add />
</Fab>
{canWrite && (
<Fab
color="primary"
size="small"
aria-label="Wartung eintragen"
sx={{ position: 'fixed', bottom: 32, right: 32 }}
onClick={() => { setForm(emptyForm); setDialogOpen(true); }}
>
<Add />
</Fab>
)}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Wartung / Service eintragen</DialogTitle>
@@ -746,6 +757,7 @@ const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdde
function FahrzeugDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { isAdmin, canChangeStatus } = usePermissions();
const [vehicle, setVehicle] = useState<FahrzeugDetail | null>(null);
const [loading, setLoading] = useState(true);
@@ -860,7 +872,7 @@ function FahrzeugDetail() {
{/* Tab content */}
<TabPanel value={activeTab} index={0}>
<UebersichtTab vehicle={vehicle} onStatusUpdated={fetchVehicle} />
<UebersichtTab vehicle={vehicle} onStatusUpdated={fetchVehicle} canChangeStatus={canChangeStatus} />
</TabPanel>
<TabPanel value={activeTab} index={1}>
@@ -868,6 +880,7 @@ function FahrzeugDetail() {
fahrzeugId={vehicle.id}
pruefungen={vehicle.pruefungen}
onAdded={fetchVehicle}
canWrite={isAdmin}
/>
</TabPanel>
@@ -876,6 +889,7 @@ function FahrzeugDetail() {
fahrzeugId={vehicle.id}
wartungslog={vehicle.wartungslog}
onAdded={fetchVehicle}
canWrite={isAdmin}
/>
</TabPanel>

View File

@@ -38,6 +38,7 @@ import {
PruefungArt,
PruefungArtLabel,
} from '../types/vehicle.types';
import { usePermissions } from '../hooks/usePermissions';
// ── Status chip config ────────────────────────────────────────────────────────
@@ -87,10 +88,8 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
// Collect inspection badges (only for types where a faellig_am exists)
const inspBadges: { art: string; tage: number | null; faelligAm: string | null }[] = [
{ art: 'HU', tage: vehicle.hu_tage_bis_faelligkeit, faelligAm: vehicle.hu_faellig_am },
{ art: 'AU', tage: vehicle.au_tage_bis_faelligkeit, faelligAm: vehicle.au_faellig_am },
{ art: 'UVV', tage: vehicle.uvv_tage_bis_faelligkeit, faelligAm: vehicle.uvv_faellig_am },
{ art: 'Leiter', tage: vehicle.leiter_tage_bis_faelligkeit, faelligAm: vehicle.leiter_faellig_am },
{ art: '§57a', tage: vehicle.paragraph57a_tage_bis_faelligkeit, faelligAm: vehicle.paragraph57a_faellig_am },
{ art: 'Wartung', tage: vehicle.wartung_tage_bis_faelligkeit, faelligAm: vehicle.naechste_wartung_am },
].filter((b) => b.faelligAm !== null);
return (
@@ -218,6 +217,7 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
function Fahrzeuge() {
const navigate = useNavigate();
const { isAdmin } = usePermissions();
const [vehicles, setVehicles] = useState<FahrzeugListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -347,14 +347,16 @@ function Fahrzeuge() {
)}
{/* FAB — add vehicle (shown to write-role users only; role check done server-side) */}
<Fab
color="primary"
aria-label="Fahrzeug hinzufügen"
sx={{ position: 'fixed', bottom: 32, right: 32 }}
onClick={() => navigate('/fahrzeuge/neu')}
>
<Add />
</Fab>
{isAdmin && (
<Fab
color="primary"
aria-label="Fahrzeug hinzufügen"
sx={{ position: 'fixed', bottom: 32, right: 32 }}
onClick={() => navigate('/fahrzeuge/neu')}
>
<Add />
</Fab>
)}
</Container>
</DashboardLayout>
);

View File

@@ -76,6 +76,10 @@ export const vehiclesApi = {
return response.data.data;
},
async delete(id: string): Promise<void> {
await api.delete(`/api/vehicles/${id}`);
},
/** Live status change — Socket.IO event is emitted server-side in Tier 3 */
async updateStatus(id: string, payload: UpdateStatusPayload): Promise<void> {
await api.patch(`/api/vehicles/${id}/status`, payload);

View File

@@ -71,9 +71,13 @@ export interface FahrzeugListItem {
au_tage_bis_faelligkeit: number | null;
uvv_faellig_am: string | null;
uvv_tage_bis_faelligkeit: number | null;
leiter_faellig_am: string | null;
leiter_tage_bis_faelligkeit: number | null;
naechste_pruefung_tage: number | null;
leiter_faellig_am: string | null;
leiter_tage_bis_faelligkeit: number | null;
paragraph57a_faellig_am: string | null;
paragraph57a_tage_bis_faelligkeit: number | null;
naechste_wartung_am: string | null;
wartung_tage_bis_faelligkeit: number | null;
naechste_pruefung_tage: number | null;
}
export interface PruefungStatus {
@@ -129,6 +133,10 @@ export interface FahrzeugDetail {
bild_url: string | null;
created_at: string;
updated_at: string;
paragraph57a_faellig_am: string | null;
paragraph57a_tage_bis_faelligkeit: number | null;
naechste_wartung_am: string | null;
wartung_tage_bis_faelligkeit: number | null;
pruefstatus: {
hu: PruefungStatus;
au: PruefungStatus;
@@ -174,6 +182,8 @@ export interface CreateFahrzeugPayload {
status_bemerkung?: string;
standort?: string;
bild_url?: string;
paragraph57a_faellig_am?: string;
naechste_wartung_am?: string;
}
export type UpdateFahrzeugPayload = Partial<CreateFahrzeugPayload>;