Logo auf hellem Hintergrund + Export als echtes VenDoc-Aufmaßblatt (xlsm)
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:
co-authored by
Claude Sonnet 5
parent
dceddeef1f
commit
61a39682de
+2
-1
@@ -13,7 +13,8 @@
|
|||||||
"@azure/msal-react": "^2.1.1",
|
"@azure/msal-react": "^2.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.2"
|
"react-router-dom": "^6.26.2",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.9",
|
"@types/react": "^18.3.9",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export default function Login({ onLocalLogin }: { onLocalLogin: (name: string) =
|
|||||||
return (
|
return (
|
||||||
<div className="login-screen">
|
<div className="login-screen">
|
||||||
<div className="lcard">
|
<div className="lcard">
|
||||||
<img src="/logo/bergvolt-logo-dark.png" alt="BergVOLT Elektrotechnik" className="llogo-img" />
|
<img src="/logo/bergvolt-logo-dark.png" alt="BergVOLT Elektrotechnik" className="llogo-img llogo-ondark" />
|
||||||
|
<img src="/logo/bergvolt-logo-light.png" alt="BergVOLT Elektrotechnik" className="llogo-img llogo-onlight" />
|
||||||
<div className="lsub">Materialschein</div>
|
<div className="lsub">Materialschein</div>
|
||||||
<div className="ltitle">Willkommen</div>
|
<div className="ltitle">Willkommen</div>
|
||||||
<div className="ldesc">Melde dich mit deinem BergVOLT<br />Office 365 Konto an.</div>
|
<div className="ldesc">Melde dich mit deinem BergVOLT<br />Office 365 Konto an.</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { MaterialItem, Projekt } from '../lib/types';
|
import type { MaterialItem, Projekt } from '../lib/types';
|
||||||
import { kabelInfoForBez } from '../data/kabelData';
|
import { kabelInfoForBez } from '../data/kabelData';
|
||||||
@@ -217,37 +218,39 @@ export default function ProjektDetail() {
|
|||||||
setRetourBusy(false);
|
setRetourBusy(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CSV-Export (für Import in VenDoc als Auftragspositionen) ──
|
// ── XLSM-Export (für Import in VenDoc als Auftragspositionen) ──
|
||||||
// Semikolon-getrennt, Komma als Dezimaltrenner, UTF-8 mit BOM – Standardkonvention
|
// Spaltenlayout und -namen sind 1:1 aus einem echten VenDoc-Aufmaßblatt-Export des
|
||||||
// für deutschsprachiges Excel/ERP. Spaltennamen (Position/EH/Positionstext/Aufmaß)
|
// Nutzers (Auftrag A00079/2026, Blatt "Aufmaß") übernommen: Position in Spalte A,
|
||||||
// richten sich nach VenDocs eigenem Aufmaßblatt-Export (Auftrag A00079/2026), das
|
// Aufmaß (Menge) in H, EH (Einheit) in I, Positionstext in K. VenDoc führt dort keine
|
||||||
// keine eigene Artikelnummer-Spalte führt – die Art.-Nr. wird daher wie dort in den
|
// eigene Artikelnummer-Spalte – die Art.-Nr. wird daher wie im Original in den
|
||||||
// Positionstext eingebettet. Ob das exakte Spaltenformat vom Universalimport 1:1
|
// Positionstext eingebettet. Oid/PosTyp/Lohn/Material bleiben leer, da das echte
|
||||||
// übernommen wird, ist ungeprüft – ggf. anhand des ersten Testimports nachschärfen.
|
// 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
|
const EH_MAP: Record<string, string> = { Stk: 'ST', m: 'M' }; // aus VenDoc-Beispiel bestätigt
|
||||||
function csvEscape(v: string): string {
|
function exportXlsm() {
|
||||||
if (/[;"\n\r]/.test(v)) return '"' + v.replace(/"/g, '""') + '"';
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
function exportCSV() {
|
|
||||||
if (!projekt) return;
|
if (!projekt) return;
|
||||||
const header = ['Position', 'EH', 'Positionstext', 'Aufmaß'];
|
const aoa: (string | number)[][] = [
|
||||||
const rows = matList.map((r, i) => [
|
['Projekt:'],
|
||||||
String(i + 1),
|
['Betreff', '', '', '', '', titel || ''],
|
||||||
EH_MAP[r.einheit] ?? r.einheit,
|
['Auftrag', '', '', '', '', auftragsNr || ''],
|
||||||
r.artNr ? `${r.bezeichnung} (${r.artNr})` : r.bezeichnung,
|
[],
|
||||||
fmt1(r.menge).replace('.', ',')
|
['Position ', 'PosTyp', 'Dummy2', 'Dummy3', 'Oid', 'Ausschr.', 'Zugebu.', 'Aufmaß', 'EH', '', 'Positionstext']
|
||||||
]);
|
];
|
||||||
const csv = [header, ...rows].map(row => row.map(csvEscape).join(';')).join('\r\n');
|
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 safeName = (titel || 'Materialschein').replace(/[^\p{L}\p{N}_-]+/gu, '_');
|
||||||
const safeAuftrag = auftragsNr ? auftragsNr.replace(/[^\p{L}\p{N}_-]+/gu, '_') + '_' : '';
|
const safeAuftrag = auftragsNr ? auftragsNr.replace(/[^\p{L}\p{N}_-]+/gu, '_') + '_' : '';
|
||||||
const filename = `Materialschein_${safeAuftrag}${safeName}.csv`;
|
XLSX.writeFile(wb, `Materialschein_${safeAuftrag}${safeName}.xlsm`, { bookType: 'xlsm' });
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="empty-msg"><div className="spin" /></div>;
|
if (loading) return <div className="empty-msg"><div className="spin" /></div>;
|
||||||
@@ -477,7 +480,7 @@ export default function ProjektDetail() {
|
|||||||
|
|
||||||
<div className="print-bar">
|
<div className="print-bar">
|
||||||
<button className="btn btn-cyan" onClick={() => window.print()}>🖨 DRUCKEN / PDF</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,8 +57,20 @@ button,input,select,textarea{font-family:inherit}
|
|||||||
.login-screen::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 60% at 30% 30%,rgba(0,200,216,.08) 0%,transparent 70%)}
|
.login-screen::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 60% at 30% 30%,rgba(0,200,216,.08) 0%,transparent 70%)}
|
||||||
.lcard{background:var(--card);border:1px solid var(--bor);border-radius:var(--r);box-shadow:var(--sh);padding:44px 36px;width:380px;max-width:100%;text-align:center;position:relative;z-index:1}
|
.lcard{background:var(--card);border:1px solid var(--bor);border-radius:var(--r);box-shadow:var(--sh);padding:44px 36px;width:380px;max-width:100%;text-align:center;position:relative;z-index:1}
|
||||||
.llogo-img{height:46px;margin:0 auto;display:block}
|
.llogo-img{height:46px;margin:0 auto;display:block}
|
||||||
|
/* .lcard wechselt mit dem Theme zwischen dunkel und hell (siehe --card oben) – daher
|
||||||
|
je nach Kartenhintergrund die passende Logovariante zeigen (helle Tinte auf dunkel,
|
||||||
|
dunkle Tinte auf hell), sonst verschwindet das Logo im hellen Modus auf weiß. */
|
||||||
|
.llogo-onlight{display:none}
|
||||||
|
:root[data-theme="light"] .llogo-ondark{display:none}
|
||||||
|
:root[data-theme="light"] .llogo-onlight{display:block}
|
||||||
|
@media(prefers-color-scheme:light){
|
||||||
|
:root:not([data-theme="dark"]) .llogo-ondark{display:none}
|
||||||
|
:root:not([data-theme="dark"]) .llogo-onlight{display:block}
|
||||||
|
}
|
||||||
.lsub{font-size:10px;letter-spacing:5px;text-transform:uppercase;color:var(--cyan);opacity:.8;margin-top:8px;margin-bottom:26px}
|
.lsub{font-size:10px;letter-spacing:5px;text-transform:uppercase;color:var(--cyan);opacity:.8;margin-top:8px;margin-bottom:26px}
|
||||||
.ltitle{font-size:16px;font-weight:700;color:#fff;margin-bottom:8px}
|
.ltitle{font-size:16px;font-weight:700;color:#fff;margin-bottom:8px}
|
||||||
|
:root[data-theme="light"] .ltitle{color:var(--text)}
|
||||||
|
@media(prefers-color-scheme:light){:root:not([data-theme="dark"]) .ltitle{color:var(--text)}}
|
||||||
.ldesc{color:var(--muted);font-size:13px;margin-bottom:26px;line-height:1.6}
|
.ldesc{color:var(--muted);font-size:13px;margin-bottom:26px;line-height:1.6}
|
||||||
.btn-ms{display:flex;align-items:center;justify-content:center;gap:10px;background:#0078d4;color:#fff;border:none;border-radius:9px;padding:14px;width:100%;font-size:14px;font-weight:700;cursor:pointer}
|
.btn-ms{display:flex;align-items:center;justify-content:center;gap:10px;background:#0078d4;color:#fff;border:none;border-radius:9px;padding:14px;width:100%;font-size:14px;font-weight:700;cursor:pointer}
|
||||||
.btn-ms:hover{background:#1084d8}
|
.btn-ms:hover{background:#1084d8}
|
||||||
@@ -224,6 +236,10 @@ table.pinfo td:last-child{text-align:right;font-weight:600}
|
|||||||
@media print{
|
@media print{
|
||||||
header,.tabs,.print-bar,.detail-back,.kab-card,.manual-card,.baustelle-card,.card-hint,.del-btn,.qty-input,.notiz-input,
|
header,.tabs,.print-bar,.detail-back,.kab-card,.manual-card,.baustelle-card,.card-hint,.del-btn,.qty-input,.notiz-input,
|
||||||
.chk-col,.retour-panel,.btn-retour-toggle{display:none!important}
|
.chk-col,.retour-panel,.btn-retour-toggle{display:none!important}
|
||||||
|
/* Ohne dies lassen viele Browser Hintergrundfarben beim Drucken/PDF-Export weg
|
||||||
|
(Häkchen "Hintergrundgrafiken" aus) – dann verschwindet der schwarze Banner
|
||||||
|
und das helle Logo wirkt "weiß"/unsichtbar auf der weißen Seite. */
|
||||||
|
.print-doc,.print-doc *{-webkit-print-color-adjust:exact;print-color-adjust:exact;color-adjust:exact}
|
||||||
body{background:#fff;color:#000}
|
body{background:#fff;color:#000}
|
||||||
main{max-width:none;padding:0;margin:0}
|
main{max-width:none;padding:0;margin:0}
|
||||||
.card{box-shadow:none;border:none;background:#fff;padding:0 4px;page-break-inside:avoid}
|
.card{box-shadow:none;border:none;background:#fff;padding:0 4px;page-break-inside:avoid}
|
||||||
|
|||||||
Reference in New Issue
Block a user