update
This commit is contained in:
@@ -149,10 +149,17 @@ async function getOrderById(id: number) {
|
||||
const orderResult = await pool.query(
|
||||
`SELECT b.*,
|
||||
l.name AS lieferant_name,
|
||||
COALESCE(u.name, u.preferred_username, u.email) AS besteller_name
|
||||
l.kontakt_name AS lieferant_kontakt_name,
|
||||
l.email AS lieferant_email,
|
||||
l.telefon AS lieferant_telefon,
|
||||
l.adresse AS lieferant_adresse,
|
||||
COALESCE(u.name, u.preferred_username, u.email) AS besteller_name,
|
||||
u.email AS besteller_email,
|
||||
mp.dienstgrad AS besteller_dienstgrad
|
||||
FROM bestellungen b
|
||||
LEFT JOIN lieferanten l ON l.id = b.lieferant_id
|
||||
LEFT JOIN users u ON u.id = b.erstellt_von
|
||||
LEFT JOIN users u ON u.id = COALESCE(b.besteller_id, b.erstellt_von)
|
||||
LEFT JOIN mitglieder_profile mp ON mp.user_id = COALESCE(b.besteller_id, b.erstellt_von)
|
||||
WHERE b.id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
@@ -363,34 +363,73 @@ export default function BestellungDetail() {
|
||||
|
||||
let curY = await addPdfHeader(doc, settings, 210);
|
||||
|
||||
// Document title below header
|
||||
// ── Document title ──
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(title, 10, curY);
|
||||
curY += 10;
|
||||
|
||||
// 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) {
|
||||
// ── Helper: two-column label/value row ──
|
||||
const row = (label: string, value: string, col2X = 70) => {
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`${label}:`, 10, curY);
|
||||
doc.text(label + ':', 10, curY);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(value, 45, curY);
|
||||
doc.text(value, col2X, curY);
|
||||
curY += 5;
|
||||
}
|
||||
curY += 4;
|
||||
};
|
||||
|
||||
// Line items table
|
||||
// ── Vendor block ──
|
||||
if (bestellung.lieferant_name) {
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Lieferant', 10, curY);
|
||||
curY += 5;
|
||||
row('Name', bestellung.lieferant_name);
|
||||
if (bestellung.lieferant_kontakt_name) row('Ansprechpartner', bestellung.lieferant_kontakt_name);
|
||||
if (bestellung.lieferant_adresse) row('Adresse', bestellung.lieferant_adresse);
|
||||
if (bestellung.lieferant_email) row('E-Mail', bestellung.lieferant_email);
|
||||
if (bestellung.lieferant_telefon) row('Telefon', bestellung.lieferant_telefon);
|
||||
curY += 3;
|
||||
}
|
||||
|
||||
// ── Besteller block ──
|
||||
if (bestellung.besteller_name) {
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Besteller', 10, curY);
|
||||
curY += 5;
|
||||
const nameWithRank = bestellung.besteller_dienstgrad
|
||||
? `${bestellung.besteller_dienstgrad} ${bestellung.besteller_name}`
|
||||
: bestellung.besteller_name;
|
||||
row('Name', nameWithRank);
|
||||
if (bestellung.besteller_email) row('E-Mail', bestellung.besteller_email);
|
||||
curY += 3;
|
||||
}
|
||||
|
||||
// ── Order info block ──
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Bestellinformationen', 10, curY);
|
||||
curY += 5;
|
||||
row('Bezeichnung', bestellung.bezeichnung);
|
||||
row('Status', BESTELLUNG_STATUS_LABELS[bestellung.status]);
|
||||
row('Erstellt am', formatDate(bestellung.erstellt_am));
|
||||
if (bestellung.bestellt_am) row('Bestelldatum', formatDate(bestellung.bestellt_am));
|
||||
curY += 5;
|
||||
|
||||
// ── Line items table ──
|
||||
const steuersatz = (parseFloat(String(bestellung.steuersatz)) || 20) / 100;
|
||||
const hasPrices = positionen.some((p) => p.einzelpreis != null);
|
||||
|
||||
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);
|
||||
|
||||
if (hasPrices) {
|
||||
const rows = positionen.map((p) => {
|
||||
const ep = p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined;
|
||||
const menge = parseFloat(String(p.menge)) || 0;
|
||||
@@ -404,36 +443,81 @@ export default function BestellungDetail() {
|
||||
];
|
||||
});
|
||||
|
||||
// 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,
|
||||
theme: 'grid',
|
||||
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 },
|
||||
1: { cellWidth: 28 },
|
||||
2: { cellWidth: 25, halign: 'right' },
|
||||
3: { cellWidth: 30, halign: 'right' },
|
||||
4: { cellWidth: 30, halign: 'right' },
|
||||
},
|
||||
foot: [
|
||||
['', '', '', 'Netto:', formatCurrency(totalNetto)],
|
||||
['', '', '', 'Brutto:', formatCurrency(totalBrutto)],
|
||||
['', '', '', `Brutto (${bestellung.steuersatz ?? 20}% USt.):`, formatCurrency(totalBrutto)],
|
||||
],
|
||||
footStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' },
|
||||
didDrawPage: addPdfFooter(doc, settings),
|
||||
});
|
||||
} else {
|
||||
const rows = positionen.map((p) => {
|
||||
const menge = parseFloat(String(p.menge)) || 0;
|
||||
return [p.bezeichnung, p.artikelnummer || '', `${menge} ${p.einheit}`];
|
||||
});
|
||||
|
||||
autoTable(doc, {
|
||||
head: [['Bezeichnung', 'Art.-Nr.', 'Menge']],
|
||||
body: rows,
|
||||
startY: curY,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [66, 66, 66], textColor: 255, fontStyle: 'bold' },
|
||||
margin: { left: 10, right: 10 },
|
||||
styles: { fontSize: 9, cellPadding: 2 },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 100 },
|
||||
1: { cellWidth: 40 },
|
||||
2: { cellWidth: 33, halign: 'right' },
|
||||
},
|
||||
didDrawPage: addPdfFooter(doc, settings),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Signature section ──
|
||||
const signY = (doc as any).lastAutoTable?.finalY ?? curY;
|
||||
const sigStartY = signY + 15;
|
||||
|
||||
// Ensure there's enough space; add a page if needed
|
||||
if (sigStartY + 25 > doc.internal.pageSize.height - 20) {
|
||||
doc.addPage();
|
||||
curY = 20;
|
||||
} else {
|
||||
curY = sigStartY;
|
||||
}
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
// Signature line
|
||||
doc.setDrawColor(0, 0, 0);
|
||||
doc.setLineWidth(0.4);
|
||||
doc.line(10, curY, 90, curY);
|
||||
|
||||
// Date line (right side)
|
||||
doc.line(120, curY, 200, curY);
|
||||
|
||||
curY += 4;
|
||||
const sigName = bestellung.besteller_dienstgrad
|
||||
? `${bestellung.besteller_dienstgrad} ${bestellung.besteller_name || ''}`
|
||||
: (bestellung.besteller_name || '');
|
||||
doc.text(sigName, 10, curY);
|
||||
doc.text('Ort, Datum', 120, curY);
|
||||
|
||||
doc.save(`bestellung_${kennung.replace('/', '-')}.pdf`);
|
||||
}
|
||||
|
||||
@@ -2056,12 +2056,13 @@ export default function Kalender() {
|
||||
<Tooltip title="Als PDF exportieren">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => generatePdf(
|
||||
viewMonth.year,
|
||||
viewMonth.month,
|
||||
trainingForMonth,
|
||||
eventsForMonth,
|
||||
)}
|
||||
onClick={() => {
|
||||
const start = new Date(viewMonth.year, viewMonth.month, 1);
|
||||
const end = new Date(viewMonth.year, viewMonth.month + 1, 0, 23, 59, 59);
|
||||
const trainToExport = trainingEvents.filter((t) => { const d = new Date(t.datum_von); return d >= start && d <= end; });
|
||||
const eventsToExport = veranstaltungen.filter((e) => { const d = new Date(e.datum_von); return d >= start && d <= end; });
|
||||
generatePdf(viewMonth.year, viewMonth.month, trainToExport, eventsToExport);
|
||||
}}
|
||||
>
|
||||
<PdfIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
@@ -53,8 +53,14 @@ export interface Bestellung {
|
||||
bezeichnung: string;
|
||||
lieferant_id?: number;
|
||||
lieferant_name?: string;
|
||||
lieferant_kontakt_name?: string;
|
||||
lieferant_email?: string;
|
||||
lieferant_telefon?: string;
|
||||
lieferant_adresse?: string;
|
||||
besteller_id?: string;
|
||||
besteller_name?: string;
|
||||
besteller_email?: string;
|
||||
besteller_dienstgrad?: string;
|
||||
status: BestellungStatus;
|
||||
budget?: number;
|
||||
steuersatz?: number;
|
||||
|
||||
@@ -129,16 +129,31 @@ export async function addPdfHeader(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `didDrawPage` callback that renders pdf_footer at the bottom of each page.
|
||||
* Returns a `didDrawPage` callback that renders pdf_footer (left) and
|
||||
* "Seite X/Y" page numbers (bottom right) on every page.
|
||||
*/
|
||||
export function addPdfFooter(
|
||||
doc: jsPDF,
|
||||
settings: PdfSettings,
|
||||
): ((data: any) => void) | undefined {
|
||||
if (!settings.pdf_footer) return undefined;
|
||||
return () => {
|
||||
renderMarkdownText(doc, settings.pdf_footer, 10, doc.internal.pageSize.height - 12, {
|
||||
fontSize: 8,
|
||||
});
|
||||
): (data: any) => void {
|
||||
return (data: any) => {
|
||||
const pageHeight = doc.internal.pageSize.height;
|
||||
const pageWidth = doc.internal.pageSize.width;
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(100, 100, 100);
|
||||
|
||||
if (settings.pdf_footer) {
|
||||
renderMarkdownText(doc, settings.pdf_footer, 10, pageHeight - 8, { fontSize: 8 });
|
||||
}
|
||||
|
||||
const pageNum = data?.pageNumber ?? 1;
|
||||
const pageCount = data?.pageCount ?? 1;
|
||||
const pageText = `Seite ${pageNum}/${pageCount}`;
|
||||
const textWidth = doc.getTextWidth(pageText);
|
||||
doc.text(pageText, pageWidth - 10 - textWidth, pageHeight - 8);
|
||||
|
||||
doc.setTextColor(0, 0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user