feat(frontend): implement unified design system with 17 reusable template components, skeleton loading states, and golden-ratio-based layouts
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user