fix: add checklisten sidebar sub-items and replace unicode escapes with proper umlauts

This commit is contained in:
Matthias Hochmeister
2026-03-28 16:27:32 +01:00
parent b171c3e921
commit 692093cc85
4 changed files with 62 additions and 51 deletions

View File

@@ -30,7 +30,7 @@ import type { CreateFahrzeugItemPayload } from '../../types/checklist.types';
// ── Helpers ──
const formatDate = (iso?: string) =>
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '\u2013';
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '';
// ══════════════════════════════════════════════════════════════════════════════
// Component
@@ -76,8 +76,8 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
const addItemMutation = useMutation({
mutationFn: (data: CreateFahrzeugItemPayload) => checklistenApi.addVehicleItem(fahrzeugId, data),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-fahrzeug-items', fahrzeugId] }); setNewItem({ bezeichnung: '', pflicht: false, sort_order: 0 }); showSuccess('Item hinzugef\u00fcgt'); },
onError: () => showError('Fehler beim Hinzuf\u00fcgen'),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-fahrzeug-items', fahrzeugId] }); setNewItem({ bezeichnung: '', pflicht: false, sort_order: 0 }); showSuccess('Item hinzugefügt'); },
onError: () => showError('Fehler beim Hinzufügen'),
});
const deleteItemMutation = useMutation({
@@ -120,7 +120,7 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
vehicleItems.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.bezeichnung}</TableCell>
<TableCell align="center">{item.pflicht ? <Chip label="Pflicht" size="small" color="warning" /> : '\u2013'}</TableCell>
<TableCell align="center">{item.pflicht ? <Chip label="Pflicht" size="small" color="warning" /> : ''}</TableCell>
<TableCell align="center">
<Switch
size="small"
@@ -144,7 +144,7 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
<TextField size="small" placeholder="Neues Item..." value={newItem.bezeichnung} onChange={(e) => setNewItem((n) => ({ ...n, bezeichnung: e.target.value }))} sx={{ flexGrow: 1 }} />
<FormControlLabel control={<Switch size="small" checked={newItem.pflicht} onChange={(e) => setNewItem((n) => ({ ...n, pflicht: e.target.checked }))} />} label="Pflicht" />
<Button size="small" variant="outlined" startIcon={<Add />} disabled={!newItem.bezeichnung.trim() || addItemMutation.isPending} onClick={() => addItemMutation.mutate(newItem)}>
Hinzuf\u00fcgen
Hinzufügen
</Button>
</Box>
</>
@@ -159,7 +159,7 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
{templatesLoading ? (
<CircularProgress size={24} />
) : templates.length === 0 ? (
<Typography color="text.secondary">Keine Vorlagen f\u00fcr dieses Fahrzeug.</Typography>
<Typography color="text.secondary">Keine Vorlagen für dieses Fahrzeug.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
@@ -167,7 +167,7 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
<TableRow>
<TableCell>Vorlage</TableCell>
<TableCell>Intervall</TableCell>
<TableCell>N\u00e4chste F\u00e4lligkeit</TableCell>
<TableCell>Nächste lligkeit</TableCell>
<TableCell align="right">Aktion</TableCell>
</TableRow>
</TableHead>
@@ -178,11 +178,11 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
<TableRow key={t.id}>
<TableCell>{t.name}</TableCell>
<TableCell>
{t.intervall === 'weekly' ? 'W\u00f6chentlich' : t.intervall === 'monthly' ? 'Monatlich' : t.intervall === 'yearly' ? 'J\u00e4hrlich' : t.intervall === 'custom' ? `${t.intervall_tage ?? '?'} Tage` : '\u2013'}
{t.intervall === 'weekly' ? 'Wöchentlich' : t.intervall === 'monthly' ? 'Monatlich' : t.intervall === 'yearly' ? 'Jährlich' : t.intervall === 'custom' ? `${t.intervall_tage ?? '?'} Tage` : ''}
</TableCell>
<TableCell>
{due ? (
<Chip icon={<Warning />} label={`F\u00e4llig: ${formatDate(due.naechste_faellig_am)}`} color="error" size="small" />
<Chip icon={<Warning />} label={`Fällig: ${formatDate(due.naechste_faellig_am)}`} color="error" size="small" />
) : (
<Typography variant="body2" color="text.secondary">Aktuell</Typography>
)}
@@ -195,7 +195,7 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
startIcon={<PlayArrow />}
onClick={() => navigate(`/checklisten/ausfuehrung/new?fahrzeug=${fahrzeugId}&vorlage=${t.id}`)}
>
Ausf\u00fchren
Ausführen
</Button>
)}
</TableCell>
@@ -210,12 +210,12 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
{/* Section 3: Recent executions */}
<Box>
<Typography variant="h6" sx={{ mb: 1.5 }}>Letzte Ausf\u00fchrungen</Typography>
<Typography variant="h6" sx={{ mb: 1.5 }}>Letzte Ausführungen</Typography>
{executionsLoading ? (
<CircularProgress size={24} />
) : executions.length === 0 ? (
<Typography color="text.secondary">Noch keine Ausf\u00fchrungen f\u00fcr dieses Fahrzeug.</Typography>
<Typography color="text.secondary">Noch keine Ausführungen für dieses Fahrzeug.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
@@ -224,7 +224,7 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
<TableCell>Datum</TableCell>
<TableCell>Vorlage</TableCell>
<TableCell>Status</TableCell>
<TableCell>Ausgef\u00fchrt von</TableCell>
<TableCell>Ausgeführt von</TableCell>
<TableCell>Freigegeben von</TableCell>
</TableRow>
</TableHead>
@@ -232,12 +232,12 @@ const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId
{executions.slice(0, 20).map((e) => (
<TableRow key={e.id} hover sx={{ cursor: 'pointer' }} onClick={() => navigate(`/checklisten/ausfuehrung/${e.id}`)}>
<TableCell>{formatDate(e.ausgefuehrt_am ?? e.created_at)}</TableCell>
<TableCell>{e.vorlage_name ?? '\u2013'}</TableCell>
<TableCell>{e.vorlage_name ?? ''}</TableCell>
<TableCell>
<Chip label={CHECKLIST_STATUS_LABELS[e.status]} color={CHECKLIST_STATUS_COLORS[e.status]} size="small" />
</TableCell>
<TableCell>{e.ausgefuehrt_von_name ?? '\u2013'}</TableCell>
<TableCell>{e.freigegeben_von_name ?? '\u2013'}</TableCell>
<TableCell>{e.ausgefuehrt_von_name ?? ''}</TableCell>
<TableCell>{e.freigegeben_von_name ?? ''}</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -228,6 +228,16 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
issuesSubItems.push({ text: 'Einstellungen', path: `/issues?tab=${issuesSubItems.length}` });
}
// Build Checklisten sub-items dynamically (tab order must match Checklisten.tsx)
const checklistenSubItems: SubItem[] = [
{ text: 'Übersicht', path: '/checklisten?tab=0' },
];
if (hasPermission('checklisten:manage_templates')) {
checklistenSubItems.push({ text: 'Vorlagen', path: '/checklisten?tab=1' });
checklistenSubItems.push({ text: 'Fahrzeugtypen', path: '/checklisten?tab=2' });
}
checklistenSubItems.push({ text: 'Historie', path: `/checklisten?tab=${checklistenSubItems.length}` });
const items = baseNavigationItems
.map((item) => {
if (item.path === '/fahrzeuge') return fahrzeugeItem;
@@ -239,6 +249,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
return { ...item, subItems: ausruestungSubItems, permission: canSeeAusruestung ? undefined : 'ausruestungsanfrage:view' };
}
if (item.path === '/issues') return { ...item, subItems: issuesSubItems };
if (item.path === '/checklisten') return { ...item, subItems: checklistenSubItems };
return item;
})
.filter((item) => !item.permission || hasPermission(item.permission));

View File

@@ -28,7 +28,7 @@ import type { ChecklistAusfuehrungItem } from '../types/checklist.types';
// ── Helpers ──
const formatDate = (iso?: string) =>
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '\u2013';
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
const ERGEBNIS_ICONS: Record<string, JSX.Element> = {
ok: <CheckCircle fontSize="small" color="success" />,
@@ -110,7 +110,7 @@ export default function ChecklistAusfuehrung() {
queryClient.invalidateQueries({ queryKey: ['checklisten-faellig'] });
showSuccess('Checkliste abgeschlossen');
},
onError: () => showError('Fehler beim Abschlie\u00dfen'),
onError: () => showError('Fehler beim Abschließen'),
});
// ── Approve ──
@@ -145,7 +145,7 @@ export default function ChecklistAusfuehrung() {
return (
<DashboardLayout>
<Alert severity="error">Checkliste konnte nicht geladen werden.</Alert>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/checklisten')} sx={{ mt: 2 }}>Zur\u00fcck</Button>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/checklisten')} sx={{ mt: 2 }}>Zurück</Button>
</DashboardLayout>
);
}
@@ -222,7 +222,7 @@ export default function ChecklistAusfuehrung() {
{execution.vorlage_name ?? 'Checkliste'}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
{execution.fahrzeug_name ?? '\u2013'} &middot; {formatDate(execution.ausgefuehrt_am ?? execution.created_at)}
{execution.fahrzeug_name ?? ''} &middot; {formatDate(execution.ausgefuehrt_am ?? execution.created_at)}
</Typography>
</Box>
<Chip
@@ -233,7 +233,7 @@ export default function ChecklistAusfuehrung() {
{execution.status === 'unvollstaendig' && (
<Alert severity="warning" sx={{ mb: 2 }}>
Diese Checkliste wurde als unvollst\u00e4ndig abgeschlossen. Einige Pflicht-Items wurden nicht mit &quot;OK&quot; bewertet.
Diese Checkliste wurde als unvollständig abgeschlossen. Einige Pflicht-Items wurden nicht mit &quot;OK&quot; bewertet.
</Alert>
)}
@@ -260,7 +260,7 @@ export default function ChecklistAusfuehrung() {
fullWidth
multiline
rows={3}
placeholder="Zus\u00e4tzliche Notizen..."
placeholder="Zusätzliche Notizen..."
value={notizen}
onChange={(e) => setNotizen(e.target.value)}
/>
@@ -277,7 +277,7 @@ export default function ChecklistAusfuehrung() {
disabled={submitMutation.isPending}
startIcon={submitMutation.isPending ? <CircularProgress size={16} /> : undefined}
>
Abschlie\u00dfen
Abschließen
</Button>
)}
@@ -299,7 +299,7 @@ export default function ChecklistAusfuehrung() {
<Paper variant="outlined" sx={{ p: 2, mt: 3 }}>
{execution.ausgefuehrt_von_name && (
<Typography variant="body2" color="text.secondary">
Ausgef\u00fchrt von: {execution.ausgefuehrt_von_name} am {formatDate(execution.ausgefuehrt_am)}
Ausgeführt von: {execution.ausgefuehrt_von_name} am {formatDate(execution.ausgefuehrt_am)}
</Typography>
)}
{execution.freigegeben_von_name && (

View File

@@ -64,12 +64,12 @@ import type {
// ── Helpers ──
const formatDate = (iso?: string) =>
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '\u2013';
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '';
const INTERVALL_LABELS: Record<string, string> = {
weekly: 'W\u00f6chentlich',
weekly: 'Wöchentlich',
monthly: 'Monatlich',
yearly: 'J\u00e4hrlich',
yearly: 'Jährlich',
custom: 'Benutzerdefiniert',
};
@@ -95,7 +95,7 @@ export default function Checklisten() {
const canManageTemplates = hasPermission('checklisten:manage_templates');
const canExecute = hasPermission('checklisten:execute');
// Tabs: 0=\u00dcbersicht, 1=Vorlagen (if perm), 2=Fahrzeugtypen (if perm), 3=Historie
// Tabs: 0=Übersicht, 1=Vorlagen (if perm), 2=Fahrzeugtypen (if perm), 3=Historie
const manageTabs = canManageTemplates ? 2 : 0;
const TAB_COUNT = 2 + manageTabs;
@@ -160,14 +160,14 @@ export default function Checklisten() {
variant="scrollable"
scrollButtons="auto"
>
<Tab label="\u00dcbersicht" />
<Tab label="Übersicht" />
{canManageTemplates && <Tab label="Vorlagen" />}
{canManageTemplates && <Tab label="Fahrzeugtypen" />}
<Tab label="Historie" />
</Tabs>
</Box>
{/* Tab 0: \u00dcbersicht */}
{/* Tab 0: Übersicht */}
<TabPanel value={tab} index={0}>
{vehiclesLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
@@ -185,7 +185,7 @@ export default function Checklisten() {
{vOverdue.length > 0 && (
<Chip
icon={<Warning />}
label={`${vOverdue.length} f\u00e4llig`}
label={`${vOverdue.length} fällig`}
color="error"
size="small"
/>
@@ -200,7 +200,7 @@ export default function Checklisten() {
<Warning fontSize="small" color="error" />
<Typography variant="body2">{f.vorlage_name}</Typography>
<Typography variant="caption" color="error.main">
({days > 0 ? `${days}d \u00fcberf\u00e4llig` : 'heute f\u00e4llig'})
({days > 0 ? `${days}d überfällig` : 'heute fällig'})
</Typography>
</Box>
{canExecute && (
@@ -304,8 +304,8 @@ function VorlagenTab({ vorlagen, loading, fahrzeugTypen, queryClient, showSucces
const deleteMutation = useMutation({
mutationFn: (id: number) => checklistenApi.deleteVorlage(id),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-vorlagen'] }); showSuccess('Vorlage gel\u00f6scht'); },
onError: () => showError('Fehler beim L\u00f6schen der Vorlage'),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-vorlagen'] }); showSuccess('Vorlage gelöscht'); },
onError: () => showError('Fehler beim Löschen der Vorlage'),
});
const openCreate = () => { setEditingVorlage(null); setForm(emptyForm); setDialogOpen(true); };
@@ -353,9 +353,9 @@ function VorlagenTab({ vorlagen, loading, fahrzeugTypen, queryClient, showSucces
<React.Fragment key={v.id}>
<TableRow hover sx={{ cursor: 'pointer' }} onClick={() => setExpandedVorlageId(expandedVorlageId === v.id ? null : v.id)}>
<TableCell>{v.name}</TableCell>
<TableCell>{v.fahrzeug_typ?.name ?? '\u2013'}</TableCell>
<TableCell>{v.fahrzeug_typ?.name ?? ''}</TableCell>
<TableCell>
{v.intervall ? INTERVALL_LABELS[v.intervall] || v.intervall : '\u2013'}
{v.intervall ? INTERVALL_LABELS[v.intervall] || v.intervall : ''}
{v.intervall === 'custom' && v.intervall_tage ? ` (${v.intervall_tage} Tage)` : ''}
</TableCell>
<TableCell>
@@ -396,9 +396,9 @@ function VorlagenTab({ vorlagen, loading, fahrzeugTypen, queryClient, showSucces
<InputLabel>Intervall</InputLabel>
<Select label="Intervall" value={form.intervall ?? ''} onChange={(e) => setForm((f) => ({ ...f, intervall: (e.target.value || undefined) as CreateVorlagePayload['intervall'] }))}>
<MenuItem value="">Kein Intervall</MenuItem>
<MenuItem value="weekly">W\u00f6chentlich</MenuItem>
<MenuItem value="weekly">Wöchentlich</MenuItem>
<MenuItem value="monthly">Monatlich</MenuItem>
<MenuItem value="yearly">J\u00e4hrlich</MenuItem>
<MenuItem value="yearly">Jährlich</MenuItem>
<MenuItem value="custom">Benutzerdefiniert</MenuItem>
</Select>
</FormControl>
@@ -431,8 +431,8 @@ function VorlageItemsSection({ vorlageId, queryClient, showSuccess, showError }:
const addMutation = useMutation({
mutationFn: (data: CreateVorlageItemPayload) => checklistenApi.addVorlageItem(vorlageId, data),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-vorlage-items', vorlageId] }); setNewItem({ bezeichnung: '', pflicht: false, sort_order: 0 }); showSuccess('Item hinzugef\u00fcgt'); },
onError: () => showError('Fehler beim Hinzuf\u00fcgen'),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-vorlage-items', vorlageId] }); setNewItem({ bezeichnung: '', pflicht: false, sort_order: 0 }); showSuccess('Item hinzugefügt'); },
onError: () => showError('Fehler beim Hinzufügen'),
});
const deleteMutation = useMutation({
@@ -458,7 +458,7 @@ function VorlageItemsSection({ vorlageId, queryClient, showSuccess, showError }:
<TextField size="small" placeholder="Neues Item..." value={newItem.bezeichnung} onChange={(e) => setNewItem((n) => ({ ...n, bezeichnung: e.target.value }))} sx={{ flexGrow: 1 }} />
<FormControlLabel control={<Switch size="small" checked={newItem.pflicht} onChange={(e) => setNewItem((n) => ({ ...n, pflicht: e.target.checked }))} />} label="Pflicht" />
<Button size="small" variant="outlined" disabled={!newItem.bezeichnung.trim() || addMutation.isPending} onClick={() => addMutation.mutate(newItem)}>
Hinzuf\u00fcgen
Hinzufügen
</Button>
</Box>
</Box>
@@ -495,8 +495,8 @@ function FahrzeugTypenTab({ fahrzeugTypen, queryClient, showSuccess, showError }
const deleteMutation = useMutation({
mutationFn: (id: number) => fahrzeugTypenApi.delete(id),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fahrzeug-typen'] }); showSuccess('Fahrzeugtyp gel\u00f6scht'); },
onError: () => showError('Fehler beim L\u00f6schen'),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fahrzeug-typen'] }); showSuccess('Fahrzeugtyp gelöscht'); },
onError: () => showError('Fehler beim Löschen'),
});
const openCreate = () => { setEditing(null); setForm({ name: '', beschreibung: '', icon: '' }); setDialogOpen(true); };
@@ -536,8 +536,8 @@ function FahrzeugTypenTab({ fahrzeugTypen, queryClient, showSuccess, showError }
fahrzeugTypen.map((t) => (
<TableRow key={t.id} hover>
<TableCell>{t.name}</TableCell>
<TableCell>{t.beschreibung ?? '\u2013'}</TableCell>
<TableCell>{t.icon ?? '\u2013'}</TableCell>
<TableCell>{t.beschreibung ?? ''}</TableCell>
<TableCell>{t.icon ?? ''}</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => openEdit(t)}><EditIcon fontSize="small" /></IconButton>
<IconButton size="small" color="error" onClick={() => deleteMutation.mutate(t.id)}><DeleteIcon fontSize="small" /></IconButton>
@@ -620,24 +620,24 @@ function HistorieTab({ executions, loading, navigate }: HistorieTabProps) {
<TableCell>Vorlage</TableCell>
<TableCell>Datum</TableCell>
<TableCell>Status</TableCell>
<TableCell>Ausgef\u00fchrt von</TableCell>
<TableCell>Ausgeführt von</TableCell>
<TableCell>Freigegeben von</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filtered.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center">Keine Eintr\u00e4ge</TableCell></TableRow>
<TableRow><TableCell colSpan={6} align="center">Keine Einträge</TableCell></TableRow>
) : (
filtered.map((e) => (
<TableRow key={e.id} hover sx={{ cursor: 'pointer' }} onClick={() => navigate(`/checklisten/ausfuehrung/${e.id}`)}>
<TableCell>{e.fahrzeug_name ?? '\u2013'}</TableCell>
<TableCell>{e.vorlage_name ?? '\u2013'}</TableCell>
<TableCell>{e.fahrzeug_name ?? ''}</TableCell>
<TableCell>{e.vorlage_name ?? ''}</TableCell>
<TableCell>{formatDate(e.ausgefuehrt_am ?? e.created_at)}</TableCell>
<TableCell>
<Chip label={CHECKLIST_STATUS_LABELS[e.status]} color={CHECKLIST_STATUS_COLORS[e.status]} size="small" />
</TableCell>
<TableCell>{e.ausgefuehrt_von_name ?? '\u2013'}</TableCell>
<TableCell>{e.freigegeben_von_name ?? '\u2013'}</TableCell>
<TableCell>{e.ausgefuehrt_von_name ?? ''}</TableCell>
<TableCell>{e.freigegeben_von_name ?? ''}</TableCell>
</TableRow>
))
)}