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

@@ -5,17 +5,9 @@ import {
Paper,
Button,
TextField,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
Card,
CardContent,
Skeleton,
} from '@mui/material';
import { ArrowBack, Edit as EditIcon, Delete as DeleteIcon, Save as SaveIcon, Close as CloseIcon } from '@mui/icons-material';
import { Edit as EditIcon, Delete as DeleteIcon, Save as SaveIcon, Close as CloseIcon } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
@@ -23,6 +15,7 @@ import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung';
import type { LieferantFormData } from '../types/bestellung.types';
import { ConfirmDialog, PageHeader, InfoGrid } from '../components/templates';
const emptyForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' };
@@ -151,11 +144,9 @@ export default function LieferantDetail() {
}
return (
<DashboardLayout>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton onClick={() => navigate('/bestellungen?tab=1')}><ArrowBack /></IconButton>
<Skeleton width={300} height={40} />
</Box>
<Paper sx={{ p: 3 }}>
<PageHeader title="" backTo="/bestellungen?tab=1" />
<Skeleton width={300} height={40} />
<Paper sx={{ p: 3, mt: 2 }}>
<Skeleton height={40} />
<Skeleton height={40} />
<Skeleton height={40} />
@@ -169,39 +160,44 @@ export default function LieferantDetail() {
return (
<DashboardLayout>
{/* ── Header ── */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton onClick={() => navigate('/bestellungen?tab=1')}>
<ArrowBack />
</IconButton>
<Typography variant="h4" sx={{ flexGrow: 1 }}>
{isNew ? 'Neuer Lieferant' : vendor!.name}
</Typography>
{!isNew && canManage && !editMode && (
<PageHeader
title={isNew ? 'Neuer Lieferant' : vendor!.name}
breadcrumbs={[
{ label: 'Bestellungen', href: '/bestellungen' },
{ label: 'Lieferanten', href: '/bestellungen?tab=1' },
{ label: isNew ? 'Neu' : vendor!.name },
]}
backTo="/bestellungen?tab=1"
actions={
<>
<Button startIcon={<EditIcon />} onClick={() => setEditMode(true)}>
Bearbeiten
</Button>
<Button startIcon={<DeleteIcon />} color="error" onClick={() => setDeleteDialogOpen(true)}>
Löschen
</Button>
{!isNew && canManage && !editMode && (
<>
<Button startIcon={<EditIcon />} onClick={() => setEditMode(true)}>
Bearbeiten
</Button>
<Button startIcon={<DeleteIcon />} color="error" onClick={() => setDeleteDialogOpen(true)}>
Löschen
</Button>
</>
)}
{editMode && (
<>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={handleSave}
disabled={!form.name.trim() || isSaving}
>
Speichern
</Button>
<Button startIcon={<CloseIcon />} onClick={handleCancel}>
Abbrechen
</Button>
</>
)}
</>
)}
{editMode && (
<>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={handleSave}
disabled={!form.name.trim() || isSaving}
>
Speichern
</Button>
<Button startIcon={<CloseIcon />} onClick={handleCancel}>
Abbrechen
</Button>
</>
)}
</Box>
}
/>
{/* ── Content ── */}
{editMode ? (
@@ -249,73 +245,31 @@ export default function LieferantDetail() {
</Box>
</Paper>
) : (
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Name</Typography>
<Typography>{vendor!.name}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Kontakt</Typography>
<Typography>{vendor!.kontakt_name || ''}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">E-Mail</Typography>
<Typography>
{vendor!.email ? <a href={`mailto:${vendor!.email}`}>{vendor!.email}</a> : ''}
</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Telefon</Typography>
<Typography>{vendor!.telefon || ''}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Website</Typography>
<Typography>
{vendor!.website ? <a href={ensureUrl(vendor!.website)} target="_blank" rel="noopener noreferrer">{vendor!.website}</a> : ''}
</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Adresse</Typography>
<Typography>{vendor!.adresse || ''}</Typography>
</CardContent></Card>
</Grid>
{vendor!.notizen && (
<Grid item xs={12}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Notizen</Typography>
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{vendor!.notizen}</Typography>
</CardContent></Card>
</Grid>
)}
</Grid>
<InfoGrid
columns={2}
fields={[
{ label: 'Name', value: vendor!.name },
{ label: 'Kontakt', value: vendor!.kontakt_name || '' },
{ label: 'E-Mail', value: vendor!.email ? <a href={`mailto:${vendor!.email}`}>{vendor!.email}</a> : '' },
{ label: 'Telefon', value: vendor!.telefon || '' },
{ label: 'Website', value: vendor!.website ? <a href={ensureUrl(vendor!.website)} target="_blank" rel="noopener noreferrer">{vendor!.website}</a> : '' },
{ label: 'Adresse', value: vendor!.adresse || '' },
...(vendor!.notizen ? [{ label: 'Notizen', value: <Typography sx={{ whiteSpace: 'pre-wrap' }}>{vendor!.notizen}</Typography>, fullWidth: true }] : []),
]}
/>
)}
{/* ── Delete Dialog ── */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Lieferant löschen</DialogTitle>
<DialogContent>
<Typography>
Soll der Lieferant <strong>{vendor?.name}</strong> wirklich gelöscht werden?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Abbrechen</Button>
<Button color="error" variant="contained" onClick={() => deleteVendor.mutate()} disabled={deleteVendor.isPending}>
Löschen
</Button>
</DialogActions>
</Dialog>
<ConfirmDialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={() => deleteVendor.mutate()}
title="Lieferant löschen"
message={<Typography>Soll der Lieferant <strong>{vendor?.name}</strong> wirklich gelöscht werden?</Typography>}
confirmLabel="Löschen"
confirmColor="error"
isLoading={deleteVendor.isPending}
/>
</DashboardLayout>
);
}