update
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user