This commit is contained in:
Matthias Hochmeister
2026-03-26 09:29:59 +01:00
parent 884397b520
commit d5e5f2d44e
10 changed files with 428 additions and 154 deletions

View File

@@ -28,6 +28,7 @@ import {
AccordionSummary,
AccordionDetails,
Autocomplete,
Tooltip,
} from '@mui/material';
import {
ArrowBack,
@@ -41,6 +42,7 @@ import {
ArrowDropDown,
ExpandMore as ExpandMoreIcon,
Save as SaveIcon,
PictureAsPdf as PdfIcon,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
@@ -48,6 +50,8 @@ import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung';
import { configApi } from '../services/config';
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';
@@ -158,6 +162,7 @@ export default function BestellungDetail() {
const canDelete = hasPermission('bestellungen:delete');
const canManageReminders = hasPermission('bestellungen:manage_reminders');
const canManageOrders = hasPermission('bestellungen:manage_orders');
const canExport = hasPermission('bestellungen:export');
const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : [];
// All statuses except current, for force override
@@ -341,6 +346,92 @@ export default function BestellungDetail() {
// ── Loading / Error ──
// ── PDF Export ──
async function generateBestellungDetailPdf() {
if (!bestellung) return;
const { jsPDF } = await import('jspdf');
const autoTable = (await import('jspdf-autotable')).default;
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
let settings;
try { settings = await configApi.getPdfSettings(); } catch { settings = { pdf_header: '', pdf_footer: '', pdf_logo: '', pdf_org_name: '' }; }
const kennung = bestellung.laufende_nummer
? `${new Date(bestellung.erstellt_am).getFullYear()}/${bestellung.laufende_nummer}`
: String(bestellung.id);
const title = `Bestellung #${kennung}`;
let curY = addPdfHeader(doc, title, settings, 210);
// Metadata block
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
const meta: [string, string][] = [
['Bezeichnung', bestellung.bezeichnung],
['Lieferant', bestellung.lieferant_name || ''],
['Besteller', bestellung.besteller_name || ''],
['Status', BESTELLUNG_STATUS_LABELS[bestellung.status]],
['Bestelldatum', bestellung.bestellt_am ? formatDate(bestellung.bestellt_am) : ''],
['Erstellt am', formatDate(bestellung.erstellt_am)],
];
for (const [label, value] of meta) {
doc.setFont('helvetica', 'bold');
doc.text(`${label}:`, 10, curY);
doc.setFont('helvetica', 'normal');
doc.text(value, 45, curY);
curY += 5;
}
curY += 4;
// Line items table
const steuersatz = (parseFloat(String(bestellung.steuersatz)) || 20) / 100;
const rows = positionen.map((p) => {
const ep = p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined;
const menge = parseFloat(String(p.menge)) || 0;
const gesamt = ep != null ? ep * menge : undefined;
return [
p.bezeichnung,
p.artikelnummer || '',
`${menge} ${p.einheit}`,
ep != null ? formatCurrency(ep) : '',
gesamt != null ? formatCurrency(gesamt) : '',
];
});
// Total
const totalNetto = positionen.reduce((sum, p) => {
const ep = p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : 0;
const m = parseFloat(String(p.menge)) || 0;
return sum + ep * m;
}, 0);
const totalBrutto = totalNetto * (1 + steuersatz);
autoTable(doc, {
head: [['Bezeichnung', 'Art.-Nr.', 'Menge', 'Einzelpreis', 'Gesamt']],
body: rows,
startY: curY,
headStyles: { fillColor: [66, 66, 66], textColor: 255, fontStyle: 'bold' },
alternateRowStyles: { fillColor: [245, 245, 245] },
margin: { left: 10, right: 10 },
styles: { fontSize: 9, cellPadding: 2 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 30 },
2: { cellWidth: 25, halign: 'right' },
3: { cellWidth: 30, halign: 'right' },
4: { cellWidth: 30, halign: 'right' },
},
foot: [
['', '', '', 'Netto:', formatCurrency(totalNetto)],
['', '', '', 'Brutto:', formatCurrency(totalBrutto)],
],
footStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' },
didDrawPage: addPdfFooter(doc, settings),
});
doc.save(`bestellung_${kennung.replace('/', '-')}.pdf`);
}
if (isLoading) {
return (
<DashboardLayout>
@@ -374,6 +465,13 @@ export default function BestellungDetail() {
<ArrowBack />
</IconButton>
<Typography variant="h4" sx={{ flexGrow: 1 }}>{bestellung.bezeichnung}</Typography>
{canExport && !editMode && (
<Tooltip title="PDF Export">
<IconButton onClick={generateBestellungDetailPdf} color="primary">
<PdfIcon />
</IconButton>
</Tooltip>
)}
{canCreate && !editMode && (
<Button startIcon={<EditIcon />} onClick={enterEditMode}>Bearbeiten</Button>
)}