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(null); const orderId = Number(id); // ── State ── const [newItem, setNewItem] = useState({ ...emptyItem }); const [editingItemId, setEditingItemId] = useState(null); const [editingItemData, setEditingItemData] = useState>({}); const [statusConfirmOpen, setStatusConfirmOpen] = useState(false); const [deleteItemTarget, setDeleteItemTarget] = useState(null); const [deleteFileTarget, setDeleteFileTarget] = useState(null); const [reminderForm, setReminderForm] = useState({ faellig_am: '', nachricht: '' }); const [reminderFormOpen, setReminderFormOpen] = useState(false); const [deleteReminderTarget, setDeleteReminderTarget] = useState(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 }) => 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) { 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 ( Laden... ); } if (isError || !bestellung) { const is404 = (error as any)?.response?.status === 404 || !bestellung; return ( {is404 ? 'Bestellung nicht gefunden.' : 'Fehler beim Laden der Bestellung.'} {!is404 && ( )} ); } return ( {/* ── Header ── */} navigate('/bestellungen')}> {bestellung.bezeichnung} {/* ── Info Cards ── */} Lieferant {bestellung.lieferant_name || '–'} Besteller {bestellung.besteller_name || '–'} Budget {formatCurrency(bestellung.budget)} Erstellt am {formatDate(bestellung.erstellt_am)} {/* ── Status Action ── */} {canEdit && nextStatus && ( )} {/* ── Delivery Progress ── */} {positionen.length > 0 && ( Lieferfortschritt: {totalReceived} / {totalOrdered} ({receivedPercent}%) )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Positionen */} {/* ══════════════════════════════════════════════════════════════════════ */} Positionen Bezeichnung Artikelnr. Menge Einheit Einzelpreis Gesamt Erhalten {canEdit && Aktionen} {positionen.map((p) => editingItemId === p.id ? ( setEditingItemData((d) => ({ ...d, bezeichnung: e.target.value }))} /> setEditingItemData((d) => ({ ...d, artikelnummer: e.target.value }))} /> setEditingItemData((d) => ({ ...d, menge: Number(e.target.value) }))} /> setEditingItemData((d) => ({ ...d, einheit: e.target.value }))} /> setEditingItemData((d) => ({ ...d, einzelpreis: e.target.value ? Number(e.target.value) : undefined }))} /> {formatCurrency((editingItemData.einzelpreis ?? 0) * (editingItemData.menge ?? 0))} {p.erhalten_menge} setEditingItemId(null)}> ) : ( {p.bezeichnung} {p.artikelnummer || '–'} {p.menge} {p.einheit} {formatCurrency(p.einzelpreis)} {formatCurrency((p.einzelpreis ?? 0) * p.menge)} {canEdit ? ( updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })} /> ) : ( p.erhalten_menge )} {canEdit && ( startEditItem(p)}> setDeleteItemTarget(p.id)}> )} ), )} {/* ── Add Item Row ── */} {canEdit && ( setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} /> setNewItem((f) => ({ ...f, artikelnummer: e.target.value }))} /> setNewItem((f) => ({ ...f, menge: Number(e.target.value) }))} /> setNewItem((f) => ({ ...f, einheit: e.target.value }))} /> setNewItem((f) => ({ ...f, einzelpreis: e.target.value ? Number(e.target.value) : undefined }))} /> {formatCurrency((newItem.einzelpreis ?? 0) * newItem.menge)} )} {/* ── Totals Row ── */} {positionen.length > 0 && ( Gesamtsumme {formatCurrency(totalCost)} )}
{/* ══════════════════════════════════════════════════════════════════════ */} {/* Dateien */} {/* ══════════════════════════════════════════════════════════════════════ */} Dateien {canEdit && ( <> )} {dateien.length === 0 ? ( Keine Dateien vorhanden ) : ( {dateien.map((d) => ( {d.thumbnail_pfad && ( )} {d.dateiname} {formatFileSize(d.dateigroesse)} · {formatDate(d.hochgeladen_am)} {canEdit && ( setDeleteFileTarget(d.id)}> )} ))} )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Erinnerungen */} {/* ══════════════════════════════════════════════════════════════════════ */} Erinnerungen {canEdit && ( )} {erinnerungen.length === 0 && !reminderFormOpen ? ( Keine Erinnerungen ) : ( {erinnerungen.map((r) => ( markReminderDone.mutate(r.id)} size="small" /> {r.nachricht || 'Erinnerung'} Fällig: {formatDate(r.faellig_am)} {canEdit && ( setDeleteReminderTarget(r.id)}> )} ))} )} {/* Inline Add Reminder Form */} {reminderFormOpen && ( setReminderForm((f) => ({ ...f, faellig_am: e.target.value }))} /> setReminderForm((f) => ({ ...f, nachricht: e.target.value }))} /> )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Historie */} {/* ══════════════════════════════════════════════════════════════════════ */} Historie {historie.length === 0 ? ( Keine Einträge ) : ( {historie.map((h) => ( {h.aktion} {h.erstellt_von_name || 'System'} · {formatDateTime(h.erstellt_am)} {h.details && ( {Object.entries(h.details).map(([k, v]) => `${k}: ${v}`).join(', ')} )} ))} )} {/* ── Notizen ── */} {bestellung.notizen && ( Notizen {bestellung.notizen} )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Dialogs */} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Status Confirmation */} setStatusConfirmOpen(false)}> Status ändern Status von {BESTELLUNG_STATUS_LABELS[bestellung.status]} auf{' '} {nextStatus ? BESTELLUNG_STATUS_LABELS[nextStatus] : ''} ändern? {/* Delete Item Confirmation */} setDeleteItemTarget(null)}> Position löschen Soll diese Position wirklich gelöscht werden? {/* Delete File Confirmation */} setDeleteFileTarget(null)}> Datei löschen Soll diese Datei wirklich gelöscht werden? {/* Delete Reminder Confirmation */} setDeleteReminderTarget(null)}> Erinnerung löschen Soll diese Erinnerung wirklich gelöscht werden?
); }