feat(frontend): implement unified design system with 17 reusable template components, skeleton loading states, and golden-ratio-based layouts

This commit is contained in:
Matthias Hochmeister
2026-04-13 10:43:27 +02:00
parent 5acfd7cc4f
commit 43ce1f930c
69 changed files with 3289 additions and 3115 deletions

View File

@@ -14,10 +14,6 @@ import {
TableRow,
TextField,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
Card,
CardContent,
@@ -32,7 +28,6 @@ import {
Tooltip,
} from '@mui/material';
import {
ArrowBack,
Add as AddIcon,
Delete as DeleteIcon,
Edit as EditIcon,
@@ -58,6 +53,7 @@ import { addPdfHeader, addPdfFooter } from '../utils/pdfExport';
import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types';
import type { BestellungStatus, BestellpositionFormData, ErinnerungFormData } from '../types/bestellung.types';
import type { AusruestungArtikel, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types';
import { ConfirmDialog, StatusChip, PageHeader } from '../components/templates';
// ── Helpers ──
@@ -674,40 +670,44 @@ export default function BestellungDetail() {
return (
<DashboardLayout>
{/* ── Header ── */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton onClick={() => navigate('/bestellungen')}>
<ArrowBack />
</IconButton>
<Typography variant="h4" sx={{ flexGrow: 1 }}>{bestellung.bezeichnung}</Typography>
{canExport && !editMode && (
<Tooltip title={bestellung.status === 'entwurf' || bestellung.status === 'wartet_auf_genehmigung' ? 'Export erst nach Genehmigung verfügbar' : 'PDF Export'}>
<span>
<IconButton
onClick={generateBestellungDetailPdf}
color="primary"
disabled={bestellung.status === 'entwurf' || bestellung.status === 'wartet_auf_genehmigung'}
>
<PdfIcon />
</IconButton>
</span>
</Tooltip>
)}
{canCreate && !editMode && (
<Button startIcon={<EditIcon />} onClick={enterEditMode}>Bearbeiten</Button>
)}
{editMode && (
<>
<Button variant="contained" startIcon={<SaveIcon />} onClick={handleSaveAll} disabled={isSavingAll}>
Speichern
</Button>
<Button onClick={cancelEditMode} disabled={isSavingAll}>Abbrechen</Button>
</>
)}
<Chip
label={BESTELLUNG_STATUS_LABELS[bestellung.status]}
color={BESTELLUNG_STATUS_COLORS[bestellung.status]}
/>
</Box>
<PageHeader
title={bestellung.bezeichnung}
backTo="/bestellungen"
actions={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{canExport && !editMode && (
<Tooltip title={bestellung.status === 'entwurf' || bestellung.status === 'wartet_auf_genehmigung' ? 'Export erst nach Genehmigung verfügbar' : 'PDF Export'}>
<span>
<IconButton
onClick={generateBestellungDetailPdf}
color="primary"
disabled={bestellung.status === 'entwurf' || bestellung.status === 'wartet_auf_genehmigung'}
>
<PdfIcon />
</IconButton>
</span>
</Tooltip>
)}
{canCreate && !editMode && (
<Button startIcon={<EditIcon />} onClick={enterEditMode}>Bearbeiten</Button>
)}
{editMode && (
<>
<Button variant="contained" startIcon={<SaveIcon />} onClick={handleSaveAll} disabled={isSavingAll}>
Speichern
</Button>
<Button onClick={cancelEditMode} disabled={isSavingAll}>Abbrechen</Button>
</>
)}
<StatusChip
status={bestellung.status}
labelMap={BESTELLUNG_STATUS_LABELS}
colorMap={BESTELLUNG_STATUS_COLORS}
size="medium"
/>
</Box>
}
/>
{/* ── Info Cards ── */}
{editMode ? (
@@ -1344,73 +1344,68 @@ export default function BestellungDetail() {
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Status Confirmation */}
<Dialog open={statusConfirmTarget != null} onClose={() => { setStatusConfirmTarget(null); setStatusForce(false); }}>
<DialogTitle>Status ändern{statusForce ? ' (manuell)' : ''}</DialogTitle>
<DialogContent>
<Typography>
Status von <strong>{BESTELLUNG_STATUS_LABELS[bestellung.status]}</strong> auf{' '}
<strong>{statusConfirmTarget ? BESTELLUNG_STATUS_LABELS[statusConfirmTarget] : ''}</strong> ändern?
</Typography>
{statusForce && (
<Typography variant="body2" color="warning.main" sx={{ mt: 1 }}>
Dies ist ein manueller Statusübergang außerhalb des normalen Ablaufs.
<ConfirmDialog
open={statusConfirmTarget != null}
onClose={() => { setStatusConfirmTarget(null); setStatusForce(false); }}
onConfirm={() => statusConfirmTarget && updateStatus.mutate({ status: statusConfirmTarget, force: statusForce || undefined })}
title={`Status ändern${statusForce ? ' (manuell)' : ''}`}
message={
<>
<Typography>
Status von <strong>{BESTELLUNG_STATUS_LABELS[bestellung.status]}</strong> auf{' '}
<strong>{statusConfirmTarget ? BESTELLUNG_STATUS_LABELS[statusConfirmTarget] : ''}</strong> ändern?
</Typography>
)}
{statusConfirmTarget && ['lieferung_pruefen', 'abgeschlossen'].includes(statusConfirmTarget) && !allCostsEntered && (
<Alert severity="warning" sx={{ mt: 2 }}>
Nicht alle Positionen haben einen Einzelpreis. Bitte prüfen Sie die Kosten, bevor Sie den Status ändern.
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => { setStatusConfirmTarget(null); setStatusForce(false); }}>Abbrechen</Button>
<Button variant="contained" onClick={() => statusConfirmTarget && updateStatus.mutate({ status: statusConfirmTarget, force: statusForce || undefined })} disabled={updateStatus.isPending}>
Bestätigen
</Button>
</DialogActions>
</Dialog>
{statusForce && (
<Typography variant="body2" color="warning.main" sx={{ mt: 1 }}>
Dies ist ein manueller Statusübergang außerhalb des normalen Ablaufs.
</Typography>
)}
{statusConfirmTarget && ['lieferung_pruefen', 'abgeschlossen'].includes(statusConfirmTarget) && !allCostsEntered && (
<Alert severity="warning" sx={{ mt: 2 }}>
Nicht alle Positionen haben einen Einzelpreis. Bitte prüfen Sie die Kosten, bevor Sie den Status ändern.
</Alert>
)}
</>
}
confirmLabel="Bestätigen"
isLoading={updateStatus.isPending}
/>
{/* Delete Item Confirmation */}
<Dialog open={deleteItemTarget != null} onClose={() => setDeleteItemTarget(null)}>
<DialogTitle>Position löschen</DialogTitle>
<DialogContent>
<Typography>Soll diese Position wirklich gelöscht werden?</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteItemTarget(null)}>Abbrechen</Button>
<Button color="error" variant="contained" onClick={() => deleteItemTarget != null && deleteItem.mutate(deleteItemTarget)} disabled={deleteItem.isPending}>
Löschen
</Button>
</DialogActions>
</Dialog>
<ConfirmDialog
open={deleteItemTarget != null}
onClose={() => setDeleteItemTarget(null)}
onConfirm={() => deleteItemTarget != null && deleteItem.mutate(deleteItemTarget)}
title="Position löschen"
message="Soll diese Position wirklich gelöscht werden?"
confirmLabel="Löschen"
confirmColor="error"
isLoading={deleteItem.isPending}
/>
{/* Delete File Confirmation */}
<Dialog open={deleteFileTarget != null} onClose={() => setDeleteFileTarget(null)}>
<DialogTitle>Datei löschen</DialogTitle>
<DialogContent>
<Typography>Soll diese Datei wirklich gelöscht werden?</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteFileTarget(null)}>Abbrechen</Button>
<Button color="error" variant="contained" onClick={() => deleteFileTarget != null && deleteFile.mutate(deleteFileTarget)} disabled={deleteFile.isPending}>
Löschen
</Button>
</DialogActions>
</Dialog>
<ConfirmDialog
open={deleteFileTarget != null}
onClose={() => setDeleteFileTarget(null)}
onConfirm={() => deleteFileTarget != null && deleteFile.mutate(deleteFileTarget)}
title="Datei löschen"
message="Soll diese Datei wirklich gelöscht werden?"
confirmLabel="Löschen"
confirmColor="error"
isLoading={deleteFile.isPending}
/>
{/* Delete Reminder Confirmation */}
<Dialog open={deleteReminderTarget != null} onClose={() => setDeleteReminderTarget(null)}>
<DialogTitle>Erinnerung löschen</DialogTitle>
<DialogContent>
<Typography>Soll diese Erinnerung wirklich gelöscht werden?</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteReminderTarget(null)}>Abbrechen</Button>
<Button color="error" variant="contained" onClick={() => deleteReminderTarget != null && deleteReminder.mutate(deleteReminderTarget)} disabled={deleteReminder.isPending}>
Löschen
</Button>
</DialogActions>
</Dialog>
<ConfirmDialog
open={deleteReminderTarget != null}
onClose={() => setDeleteReminderTarget(null)}
onConfirm={() => deleteReminderTarget != null && deleteReminder.mutate(deleteReminderTarget)}
title="Erinnerung löschen"
message="Soll diese Erinnerung wirklich gelöscht werden?"
confirmLabel="Löschen"
confirmColor="error"
isLoading={deleteReminder.isPending}
/>
</DashboardLayout>
);
}