Files
dashboard/frontend/src/pages/BestellungDetail.tsx
Matthias Hochmeister 1b13e4f89e new features
2026-03-23 18:43:30 +01:00

693 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef } from 'react';
import {
Box,
Typography,
Paper,
Button,
Chip,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
Card,
CardContent,
LinearProgress,
Checkbox,
} from '@mui/material';
import {
ArrowBack,
Add as AddIcon,
Delete as DeleteIcon,
Edit as EditIcon,
Check as CheckIcon,
Close as CloseIcon,
AttachFile,
Alarm,
History,
Upload as UploadIcon,
} 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';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung';
import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types';
import type { BestellungStatus, BestellpositionFormData, ErinnerungFormData, Bestellposition } from '../types/bestellung.types';
// ── Helpers ──
const formatCurrency = (value?: number) =>
value != null ? new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value) : '';
const formatDate = (iso?: string) =>
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '';
const formatDateTime = (iso?: string) =>
iso ? new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
const formatFileSize = (bytes?: number) => {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// Status flow
const STATUS_FLOW: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'teillieferung', 'vollstaendig', 'abgeschlossen'];
function getNextStatus(current: BestellungStatus): BestellungStatus | null {
const idx = STATUS_FLOW.indexOf(current);
return idx >= 0 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
}
// Empty line item form
const emptyItem: BestellpositionFormData = { bezeichnung: '', artikelnummer: '', menge: 1, einheit: 'Stk', einzelpreis: undefined };
// ══════════════════════════════════════════════════════════════════════════════
// Component
// ══════════════════════════════════════════════════════════════════════════════
export default function BestellungDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const { hasPermission } = usePermissionContext();
const fileInputRef = useRef<HTMLInputElement>(null);
const orderId = Number(id);
// ── State ──
const [newItem, setNewItem] = useState<BestellpositionFormData>({ ...emptyItem });
const [editingItemId, setEditingItemId] = useState<number | null>(null);
const [editingItemData, setEditingItemData] = useState<Partial<BestellpositionFormData>>({});
const [statusConfirmOpen, setStatusConfirmOpen] = useState(false);
const [deleteItemTarget, setDeleteItemTarget] = useState<number | null>(null);
const [deleteFileTarget, setDeleteFileTarget] = useState<number | null>(null);
const [reminderForm, setReminderForm] = useState<ErinnerungFormData>({ faellig_am: '', nachricht: '' });
const [reminderFormOpen, setReminderFormOpen] = useState(false);
const [deleteReminderTarget, setDeleteReminderTarget] = useState<number | null>(null);
// ── Query ──
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['bestellung', orderId],
queryFn: () => bestellungApi.getOrder(orderId),
enabled: !!orderId,
});
const bestellung = data?.bestellung;
const positionen = data?.positionen ?? [];
const dateien = data?.dateien ?? [];
const erinnerungen = data?.erinnerungen ?? [];
const historie = data?.historie ?? [];
const canEdit = hasPermission('bestellungen:edit');
const nextStatus = bestellung ? getNextStatus(bestellung.status) : null;
// ── Mutations ──
const updateStatus = useMutation({
mutationFn: (status: string) => bestellungApi.updateStatus(orderId, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
showSuccess('Status aktualisiert');
setStatusConfirmOpen(false);
},
onError: () => showError('Fehler beim Aktualisieren des Status'),
});
const addItem = useMutation({
mutationFn: (data: BestellpositionFormData) => bestellungApi.addLineItem(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setNewItem({ ...emptyItem });
showSuccess('Position hinzugefügt');
},
onError: () => showError('Fehler beim Hinzufügen der Position'),
});
const updateItem = useMutation({
mutationFn: ({ itemId, data }: { itemId: number; data: Partial<BestellpositionFormData> }) =>
bestellungApi.updateLineItem(itemId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setEditingItemId(null);
showSuccess('Position aktualisiert');
},
onError: () => showError('Fehler beim Aktualisieren der Position'),
});
const deleteItem = useMutation({
mutationFn: (itemId: number) => bestellungApi.deleteLineItem(itemId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setDeleteItemTarget(null);
showSuccess('Position gelöscht');
},
onError: () => showError('Fehler beim Löschen der Position'),
});
const updateReceived = useMutation({
mutationFn: ({ itemId, menge }: { itemId: number; menge: number }) =>
bestellungApi.updateReceivedQty(itemId, menge),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const uploadFile = useMutation({
mutationFn: (file: File) => bestellungApi.uploadFile(orderId, file),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
showSuccess('Datei hochgeladen');
},
onError: () => showError('Fehler beim Hochladen der Datei'),
});
const deleteFile = useMutation({
mutationFn: (fileId: number) => bestellungApi.deleteFile(fileId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setDeleteFileTarget(null);
showSuccess('Datei gelöscht');
},
onError: () => showError('Fehler beim Löschen der Datei'),
});
const addReminder = useMutation({
mutationFn: (data: ErinnerungFormData) => bestellungApi.addReminder(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setReminderForm({ faellig_am: '', nachricht: '' });
setReminderFormOpen(false);
showSuccess('Erinnerung erstellt');
},
onError: () => showError('Fehler beim Erstellen der Erinnerung'),
});
const markReminderDone = useMutation({
mutationFn: (reminderId: number) => bestellungApi.markReminderDone(reminderId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const deleteReminder = useMutation({
mutationFn: (reminderId: number) => bestellungApi.deleteReminder(reminderId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setDeleteReminderTarget(null);
showSuccess('Erinnerung gelöscht');
},
onError: () => showError('Fehler beim Löschen'),
});
// ── Handlers ──
function startEditItem(item: Bestellposition) {
setEditingItemId(item.id);
setEditingItemData({
bezeichnung: item.bezeichnung,
artikelnummer: item.artikelnummer || '',
menge: item.menge,
einheit: item.einheit,
einzelpreis: item.einzelpreis,
});
}
function saveEditItem() {
if (editingItemId == null) return;
updateItem.mutate({ itemId: editingItemId, data: editingItemData });
}
function handleAddItem() {
if (!newItem.bezeichnung.trim()) return;
addItem.mutate(newItem);
}
function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (file) uploadFile.mutate(file);
e.target.value = '';
}
// Compute totals
const totalCost = positionen.reduce((sum, p) => sum + (p.einzelpreis ?? 0) * p.menge, 0);
const totalReceived = positionen.length > 0
? positionen.reduce((sum, p) => sum + p.erhalten_menge, 0)
: 0;
const totalOrdered = positionen.reduce((sum, p) => sum + p.menge, 0);
const receivedPercent = totalOrdered > 0 ? Math.round((totalReceived / totalOrdered) * 100) : 0;
// ── Loading / Error ──
if (isLoading) {
return (
<DashboardLayout>
<Box sx={{ p: 4, textAlign: 'center' }}><Typography>Laden...</Typography></Box>
</DashboardLayout>
);
}
if (isError || !bestellung) {
const is404 = (error as any)?.response?.status === 404 || !bestellung;
return (
<DashboardLayout>
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography color="error">
{is404 ? 'Bestellung nicht gefunden.' : 'Fehler beim Laden der Bestellung.'}
</Typography>
{!is404 && (
<Button sx={{ mt: 2 }} variant="outlined" onClick={() => refetch()}>Erneut versuchen</Button>
)}
<Button sx={{ mt: 2, ml: !is404 ? 1 : 0 }} onClick={() => navigate('/bestellungen')}>Zurück</Button>
</Box>
</DashboardLayout>
);
}
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>
<Chip
label={BESTELLUNG_STATUS_LABELS[bestellung.status]}
color={BESTELLUNG_STATUS_COLORS[bestellung.status]}
/>
</Box>
{/* ── Info Cards ── */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} md={3}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Lieferant</Typography>
<Typography>{bestellung.lieferant_name || ''}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Besteller</Typography>
<Typography>{bestellung.besteller_name || ''}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Budget</Typography>
<Typography>{formatCurrency(bestellung.budget)}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Erstellt am</Typography>
<Typography>{formatDate(bestellung.erstellt_am)}</Typography>
</CardContent></Card>
</Grid>
</Grid>
{/* ── Status Action ── */}
{canEdit && nextStatus && (
<Box sx={{ mb: 3 }}>
<Button variant="contained" onClick={() => setStatusConfirmOpen(true)}>
Status ändern: {BESTELLUNG_STATUS_LABELS[nextStatus]}
</Button>
</Box>
)}
{/* ── Delivery Progress ── */}
{positionen.length > 0 && (
<Box sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
Lieferfortschritt: {totalReceived} / {totalOrdered} ({receivedPercent}%)
</Typography>
<LinearProgress variant="determinate" value={receivedPercent} sx={{ height: 8, borderRadius: 4 }} />
</Box>
)}
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Positionen */}
{/* ══════════════════════════════════════════════════════════════════════ */}
<Paper sx={{ p: 2, mb: 3 }}>
<Typography variant="h6" sx={{ mb: 2 }}>Positionen</Typography>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Bezeichnung</TableCell>
<TableCell>Artikelnr.</TableCell>
<TableCell align="right">Menge</TableCell>
<TableCell>Einheit</TableCell>
<TableCell align="right">Einzelpreis</TableCell>
<TableCell align="right">Gesamt</TableCell>
<TableCell align="right">Erhalten</TableCell>
{canEdit && <TableCell align="right">Aktionen</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{positionen.map((p) =>
editingItemId === p.id ? (
<TableRow key={p.id}>
<TableCell>
<TextField size="small" value={editingItemData.bezeichnung || ''} onChange={(e) => setEditingItemData((d) => ({ ...d, bezeichnung: e.target.value }))} />
</TableCell>
<TableCell>
<TextField size="small" value={editingItemData.artikelnummer || ''} onChange={(e) => setEditingItemData((d) => ({ ...d, artikelnummer: e.target.value }))} />
</TableCell>
<TableCell>
<TextField size="small" type="number" sx={{ width: 80 }} value={editingItemData.menge ?? ''} onChange={(e) => setEditingItemData((d) => ({ ...d, menge: Number(e.target.value) }))} />
</TableCell>
<TableCell>
<TextField size="small" sx={{ width: 80 }} value={editingItemData.einheit || ''} onChange={(e) => setEditingItemData((d) => ({ ...d, einheit: e.target.value }))} />
</TableCell>
<TableCell>
<TextField size="small" type="number" sx={{ width: 100 }} value={editingItemData.einzelpreis ?? ''} onChange={(e) => setEditingItemData((d) => ({ ...d, einzelpreis: e.target.value ? Number(e.target.value) : undefined }))} />
</TableCell>
<TableCell align="right">{formatCurrency((editingItemData.einzelpreis ?? 0) * (editingItemData.menge ?? 0))}</TableCell>
<TableCell align="right">{p.erhalten_menge}</TableCell>
<TableCell align="right">
<IconButton size="small" color="primary" onClick={saveEditItem}><CheckIcon fontSize="small" /></IconButton>
<IconButton size="small" onClick={() => setEditingItemId(null)}><CloseIcon fontSize="small" /></IconButton>
</TableCell>
</TableRow>
) : (
<TableRow key={p.id}>
<TableCell>{p.bezeichnung}</TableCell>
<TableCell>{p.artikelnummer || ''}</TableCell>
<TableCell align="right">{p.menge}</TableCell>
<TableCell>{p.einheit}</TableCell>
<TableCell align="right">{formatCurrency(p.einzelpreis)}</TableCell>
<TableCell align="right">{formatCurrency((p.einzelpreis ?? 0) * p.menge)}</TableCell>
<TableCell align="right">
{canEdit ? (
<TextField
size="small"
type="number"
sx={{ width: 70 }}
value={p.erhalten_menge}
inputProps={{ min: 0, max: p.menge }}
onChange={(e) => updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })}
/>
) : (
p.erhalten_menge
)}
</TableCell>
{canEdit && (
<TableCell align="right">
<IconButton size="small" onClick={() => startEditItem(p)}><EditIcon fontSize="small" /></IconButton>
<IconButton size="small" color="error" onClick={() => setDeleteItemTarget(p.id)}><DeleteIcon fontSize="small" /></IconButton>
</TableCell>
)}
</TableRow>
),
)}
{/* ── Add Item Row ── */}
{canEdit && (
<TableRow>
<TableCell>
<TextField size="small" placeholder="Bezeichnung" value={newItem.bezeichnung} onChange={(e) => setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} />
</TableCell>
<TableCell>
<TextField size="small" placeholder="Artikelnr." value={newItem.artikelnummer || ''} onChange={(e) => setNewItem((f) => ({ ...f, artikelnummer: e.target.value }))} />
</TableCell>
<TableCell>
<TextField size="small" type="number" sx={{ width: 80 }} value={newItem.menge} onChange={(e) => setNewItem((f) => ({ ...f, menge: Number(e.target.value) }))} />
</TableCell>
<TableCell>
<TextField size="small" sx={{ width: 80 }} value={newItem.einheit || 'Stk'} onChange={(e) => setNewItem((f) => ({ ...f, einheit: e.target.value }))} />
</TableCell>
<TableCell>
<TextField size="small" type="number" sx={{ width: 100 }} placeholder="Preis" value={newItem.einzelpreis ?? ''} onChange={(e) => setNewItem((f) => ({ ...f, einzelpreis: e.target.value ? Number(e.target.value) : undefined }))} />
</TableCell>
<TableCell align="right">{formatCurrency((newItem.einzelpreis ?? 0) * newItem.menge)}</TableCell>
<TableCell />
<TableCell align="right">
<IconButton size="small" color="primary" onClick={handleAddItem} disabled={!newItem.bezeichnung.trim() || addItem.isPending}>
<AddIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
)}
{/* ── Totals Row ── */}
{positionen.length > 0 && (
<TableRow>
<TableCell colSpan={5} align="right"><strong>Gesamtsumme</strong></TableCell>
<TableCell align="right"><strong>{formatCurrency(totalCost)}</strong></TableCell>
<TableCell colSpan={canEdit ? 2 : 1} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Dateien */}
{/* ══════════════════════════════════════════════════════════════════════ */}
<Paper sx={{ p: 2, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<AttachFile sx={{ mr: 1 }} />
<Typography variant="h6" sx={{ flexGrow: 1 }}>Dateien</Typography>
{canEdit && (
<>
<input ref={fileInputRef} type="file" hidden onChange={handleFileSelect} />
<Button size="small" startIcon={<UploadIcon />} onClick={() => fileInputRef.current?.click()} disabled={uploadFile.isPending}>
Hochladen
</Button>
</>
)}
</Box>
{dateien.length === 0 ? (
<Typography variant="body2" color="text.secondary">Keine Dateien vorhanden</Typography>
) : (
<Grid container spacing={2}>
{dateien.map((d) => (
<Grid item xs={12} sm={6} md={4} key={d.id}>
<Card variant="outlined">
{d.thumbnail_pfad && (
<Box
component="img"
src={`/api/bestellungen/files/${d.id}/thumbnail`}
alt={d.dateiname}
sx={{ width: '100%', height: 120, objectFit: 'cover' }}
/>
)}
<CardContent sx={{ py: 1, '&:last-child': { pb: 1 } }}>
<Typography variant="body2" noWrap>{d.dateiname}</Typography>
<Typography variant="caption" color="text.secondary">
{formatFileSize(d.dateigroesse)} &middot; {formatDate(d.hochgeladen_am)}
</Typography>
{canEdit && (
<Box sx={{ mt: 0.5 }}>
<IconButton size="small" color="error" onClick={() => setDeleteFileTarget(d.id)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Erinnerungen */}
{/* ══════════════════════════════════════════════════════════════════════ */}
<Paper sx={{ p: 2, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Alarm sx={{ mr: 1 }} />
<Typography variant="h6" sx={{ flexGrow: 1 }}>Erinnerungen</Typography>
{canEdit && (
<Button size="small" startIcon={<AddIcon />} onClick={() => setReminderFormOpen(true)}>
Neue Erinnerung
</Button>
)}
</Box>
{erinnerungen.length === 0 && !reminderFormOpen ? (
<Typography variant="body2" color="text.secondary">Keine Erinnerungen</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{erinnerungen.map((r) => (
<Box key={r.id} sx={{ display: 'flex', alignItems: 'center', gap: 1, opacity: r.erledigt ? 0.5 : 1 }}>
<Checkbox
checked={r.erledigt}
disabled={r.erledigt || !canEdit}
onChange={() => markReminderDone.mutate(r.id)}
size="small"
/>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="body2" sx={{ textDecoration: r.erledigt ? 'line-through' : 'none' }}>
{r.nachricht || 'Erinnerung'}
</Typography>
<Typography variant="caption" color="text.secondary">
Fällig: {formatDate(r.faellig_am)}
</Typography>
</Box>
{canEdit && (
<IconButton size="small" color="error" onClick={() => setDeleteReminderTarget(r.id)}>
<DeleteIcon fontSize="small" />
</IconButton>
)}
</Box>
))}
</Box>
)}
{/* Inline Add Reminder Form */}
{reminderFormOpen && (
<Box sx={{ display: 'flex', gap: 1, mt: 2, alignItems: 'flex-end' }}>
<TextField
size="small"
type="date"
label="Fällig am"
InputLabelProps={{ shrink: true }}
value={reminderForm.faellig_am}
onChange={(e) => setReminderForm((f) => ({ ...f, faellig_am: e.target.value }))}
/>
<TextField
size="small"
label="Nachricht"
sx={{ flexGrow: 1 }}
value={reminderForm.nachricht || ''}
onChange={(e) => setReminderForm((f) => ({ ...f, nachricht: e.target.value }))}
/>
<Button
size="small"
variant="contained"
disabled={!reminderForm.faellig_am || addReminder.isPending}
onClick={() => addReminder.mutate(reminderForm)}
>
Speichern
</Button>
<Button size="small" onClick={() => { setReminderFormOpen(false); setReminderForm({ faellig_am: '', nachricht: '' }); }}>
Abbrechen
</Button>
</Box>
)}
</Paper>
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Historie */}
{/* ══════════════════════════════════════════════════════════════════════ */}
<Paper sx={{ p: 2, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<History sx={{ mr: 1 }} />
<Typography variant="h6">Historie</Typography>
</Box>
{historie.length === 0 ? (
<Typography variant="body2" color="text.secondary">Keine Einträge</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{historie.map((h) => (
<Box key={h.id} sx={{ display: 'flex', gap: 1 }}>
<Box sx={{ width: 6, minHeight: '100%', borderRadius: 3, bgcolor: 'divider', flexShrink: 0 }} />
<Box>
<Typography variant="body2">{h.aktion}</Typography>
<Typography variant="caption" color="text.secondary">
{h.erstellt_von_name || 'System'} &middot; {formatDateTime(h.erstellt_am)}
</Typography>
{h.details && (
<Typography variant="caption" display="block" color="text.secondary">
{Object.entries(h.details).map(([k, v]) => `${k}: ${v}`).join(', ')}
</Typography>
)}
</Box>
</Box>
))}
</Box>
)}
</Paper>
{/* ── Notizen ── */}
{bestellung.notizen && (
<Paper sx={{ p: 2, mb: 3 }}>
<Typography variant="h6" sx={{ mb: 1 }}>Notizen</Typography>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{bestellung.notizen}</Typography>
</Paper>
)}
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Dialogs */}
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Status Confirmation */}
<Dialog open={statusConfirmOpen} onClose={() => setStatusConfirmOpen(false)}>
<DialogTitle>Status ändern</DialogTitle>
<DialogContent>
<Typography>
Status von <strong>{BESTELLUNG_STATUS_LABELS[bestellung.status]}</strong> auf{' '}
<strong>{nextStatus ? BESTELLUNG_STATUS_LABELS[nextStatus] : ''}</strong> ändern?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setStatusConfirmOpen(false)}>Abbrechen</Button>
<Button variant="contained" onClick={() => nextStatus && updateStatus.mutate(nextStatus)} disabled={updateStatus.isPending}>
Bestätigen
</Button>
</DialogActions>
</Dialog>
{/* 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>
{/* 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>
{/* 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>
</DashboardLayout>
);
}