Logo auf hellem Hintergrund + Export als echtes VenDoc-Aufmaßblatt (xlsm)
Build and Deploy / version (push) Successful in 1s
Build and Deploy / build (push) Successful in 21s
Build and Deploy / deploy (push) Successful in 2s

Login-Logo war im hellen Modus weiß-auf-weiß unsichtbar (Karte wechselt mit
dem Theme, Logo war hart auf die dunkle Variante codiert) - zeigt jetzt je
nach Theme die passende Logo-Tinte, gleiches Problem beim "Willkommen"-Titel
behoben. PDF-Export verlor den schwarzen Banner beim Drucken, da Browser
Hintergrundfarben standardmäßig nicht drucken - jetzt über
print-color-adjust erzwungen.

Export ist jetzt eine echte .xlsm-Datei statt CSV, mit Spaltenlayout 1:1 aus
einem echten VenDoc-Aufmaßblatt-Export des Nutzers übernommen (Blatt
"Aufmaß", Position/Aufmaß/EH/Positionstext in den exakt gleichen Spalten).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fidelis Huber
2026-09-11 08:45:31 +02:00
co-authored by Claude Sonnet 5
parent dceddeef1f
commit 61a39682de
4 changed files with 51 additions and 30 deletions
+31 -28
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import * as XLSX from 'xlsx';
import { api } from '../lib/api';
import type { MaterialItem, Projekt } from '../lib/types';
import { kabelInfoForBez } from '../data/kabelData';
@@ -217,37 +218,39 @@ export default function ProjektDetail() {
setRetourBusy(false);
}
// ── CSV-Export (für Import in VenDoc als Auftragspositionen) ──
// Semikolon-getrennt, Komma als Dezimaltrenner, UTF-8 mit BOM – Standardkonvention
// für deutschsprachiges Excel/ERP. Spaltennamen (Position/EH/Positionstext/Aufmaß)
// richten sich nach VenDocs eigenem Aufmaßblatt-Export (Auftrag A00079/2026), das
// keine eigene Artikelnummer-Spalte führt – die Art.-Nr. wird daher wie dort in den
// Positionstext eingebettet. Ob das exakte Spaltenformat vom Universalimport 1:1
// übernommen wird, ist ungeprüft – ggf. anhand des ersten Testimports nachschärfen.
// ── XLSM-Export (für Import in VenDoc als Auftragspositionen) ──
// Spaltenlayout und -namen sind 1:1 aus einem echten VenDoc-Aufmaßblatt-Export des
// Nutzers (Auftrag A00079/2026, Blatt "Aufmaß") übernommen: Position in Spalte A,
// Aufmaß (Menge) in H, EH (Einheit) in I, Positionstext in K. VenDoc führt dort keine
// eigene Artikelnummer-Spalte – die Art.-Nr. wird daher wie im Original in den
// Positionstext eingebettet. Oid/PosTyp/Lohn/Material bleiben leer, da das echte
// VenDoc-Aufmaßblatt bestehende Positionen per interner Oid referenziert, die wir
// nicht kennen/vergeben können.
const EH_MAP: Record<string, string> = { Stk: 'ST', m: 'M' }; // aus VenDoc-Beispiel bestätigt
function csvEscape(v: string): string {
if (/[;"\n\r]/.test(v)) return '"' + v.replace(/"/g, '""') + '"';
return v;
}
function exportCSV() {
function exportXlsm() {
if (!projekt) return;
const header = ['Position', 'EH', 'Positionstext', 'Aufmaß'];
const rows = matList.map((r, i) => [
String(i + 1),
EH_MAP[r.einheit] ?? r.einheit,
r.artNr ? `${r.bezeichnung} (${r.artNr})` : r.bezeichnung,
fmt1(r.menge).replace('.', ',')
]);
const csv = [header, ...rows].map(row => row.map(csvEscape).join(';')).join('\r\n');
const aoa: (string | number)[][] = [
['Projekt:'],
['Betreff', '', '', '', '', titel || ''],
['Auftrag', '', '', '', '', auftragsNr || ''],
[],
['Position ', 'PosTyp', 'Dummy2', 'Dummy3', 'Oid', 'Ausschr.', 'Zugebu.', 'Aufmaß', 'EH', '', 'Positionstext']
];
matList.forEach((r, i) => {
const row: (string | number)[] = [];
row[0] = i + 1;
row[7] = num1(r.menge);
row[8] = EH_MAP[r.einheit] ?? r.einheit;
row[10] = r.artNr ? `${r.bezeichnung} (${r.artNr})` : r.bezeichnung;
aoa.push(row);
});
const ws = XLSX.utils.aoa_to_sheet(aoa);
ws['!cols'] = [{ wch: 10 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 9 }, { wch: 9 }, { wch: 9 }, { wch: 7 }, { wch: 2 }, { wch: 55 }];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Aufmaß');
const safeName = (titel || 'Materialschein').replace(/[^\p{L}\p{N}_-]+/gu, '_');
const safeAuftrag = auftragsNr ? auftragsNr.replace(/[^\p{L}\p{N}_-]+/gu, '_') + '_' : '';
const filename = `Materialschein_${safeAuftrag}${safeName}.csv`;
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
XLSX.writeFile(wb, `Materialschein_${safeAuftrag}${safeName}.xlsm`, { bookType: 'xlsm' });
}
if (loading) return <div className="empty-msg"><div className="spin" /></div>;
@@ -477,7 +480,7 @@ export default function ProjektDetail() {
<div className="print-bar">
<button className="btn btn-cyan" onClick={() => window.print()}>🖨 DRUCKEN / PDF</button>
<button className="btn btn-outline" onClick={exportCSV}>⬇ CSV Export (VenDoc)</button>
<button className="btn btn-outline" onClick={exportXlsm}>⬇ Excel Export (VenDoc)</button>
</div>
</div>
);