Monteur-Dropdown + VenDoc-Spaltennamen im CSV-Export angepasst
Build and Deploy / version (push) Successful in 1s
Build and Deploy / build (push) Successful in 8s
Build and Deploy / deploy (push) Successful in 2s

Monteur ist jetzt ein Dropdown (Michael/Daniel/Dietmar/Fidelis) mit freiem
Textfeld für "Andere". CSV-Export nutzt jetzt VenDocs eigene Spaltennamen
(Position/EH/Positionstext/Aufmaß), ermittelt aus einem echten VenDoc-
Aufmaßblatt-Export des Nutzers; Art.-Nr. wird wie dort im Positionstext
eingebettet, da VenDoc keine eigene Artikelnummer-Spalte führt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fidelis Huber
2026-09-11 08:29:44 +02:00
co-authored by Claude Sonnet 5
parent 8a4ce769a0
commit 18a3a636dd
3 changed files with 57 additions and 16 deletions
+42
View File
@@ -0,0 +1,42 @@
import { useState } from 'react';
export const MONTEURE = ['Michael', 'Daniel', 'Dietmar', 'Fidelis'];
interface Props {
value: string;
onChange: (v: string) => void;
onCommit?: (v: string) => void;
}
export default function MonteurField({ value, onChange, onCommit }: Props) {
const [customMode, setCustomMode] = useState(() => value !== '' && !MONTEURE.includes(value));
function handleSelect(v: string) {
if (v === '__custom__') {
setCustomMode(true);
return;
}
setCustomMode(false);
onChange(v);
onCommit?.(v);
}
return (
<label className="field">Monteur
<select value={customMode ? '__custom__' : value} onChange={e => handleSelect(e.target.value)}>
<option value="">— wählen —</option>
{MONTEURE.map(m => <option key={m} value={m}>{m}</option>)}
<option value="__custom__">Andere …</option>
</select>
{customMode && (
<input
style={{ marginTop: 8 }}
value={value}
onChange={e => onChange(e.target.value)}
onBlur={e => onCommit?.(e.target.value)}
placeholder="Name eingeben"
/>
)}
</label>
);
}
+2 -3
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { Projekt, ProjektStatus } from '../lib/types'; import type { Projekt, ProjektStatus } from '../lib/types';
import MonteurField from '../components/MonteurField';
function dd(s: string | null) { function dd(s: string | null) {
if (!s) return '–'; if (!s) return '–';
@@ -101,9 +102,7 @@ export default function Dashboard() {
<label className="field">Datum <label className="field">Datum
<input type="date" value={form.datum} onChange={e => setForm({ ...form, datum: e.target.value })} /> <input type="date" value={form.datum} onChange={e => setForm({ ...form, datum: e.target.value })} />
</label> </label>
<label className="field">Monteur <MonteurField value={form.monteur} onChange={v => setForm({ ...form, monteur: v })} />
<input value={form.monteur} onChange={e => setForm({ ...form, monteur: e.target.value })} placeholder="Name" />
</label>
<label className="field" style={{ gridColumn: 'span 2' }}>Notiz / Bereich <label className="field" style={{ gridColumn: 'span 2' }}>Notiz / Bereich
<textarea value={form.notiz} onChange={e => setForm({ ...form, notiz: e.target.value })} placeholder="Stockwerk, Bereich, Bemerkung …" /> <textarea value={form.notiz} onChange={e => setForm({ ...form, notiz: e.target.value })} placeholder="Stockwerk, Bereich, Bemerkung …" />
</label> </label>
+13 -13
View File
@@ -4,6 +4,7 @@ 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';
import KabelOptions from '../components/KabelOptions'; import KabelOptions from '../components/KabelOptions';
import MonteurField from '../components/MonteurField';
const fmt1 = (v: number | string) => (Math.round((parseFloat(String(v)) || 0) * 10) / 10).toFixed(1); const fmt1 = (v: number | string) => (Math.round((parseFloat(String(v)) || 0) * 10) / 10).toFixed(1);
const num1 = (v: number | string) => Math.round((parseFloat(String(v)) || 0) * 10) / 10; const num1 = (v: number | string) => Math.round((parseFloat(String(v)) || 0) * 10) / 10;
@@ -216,25 +217,26 @@ export default function ProjektDetail() {
setRetourBusy(false); setRetourBusy(false);
} }
// ── CSV-Export (für Import in VenDoc o.ä. als Auftragspositionen) ── // ── CSV-Export (für Import in VenDoc als Auftragspositionen) ──
// Semikolon-getrennt, Komma als Dezimaltrenner, UTF-8 mit BOM – Standardkonvention // Semikolon-getrennt, Komma als Dezimaltrenner, UTF-8 mit BOM – Standardkonvention
// für deutschsprachiges Excel/ERP, damit die Datei ohne Umweg direkt geöffnet/ // für deutschsprachiges Excel/ERP. Spaltennamen (Position/EH/Positionstext/Aufmaß)
// importiert werden kann. Reine Positionstabelle ohne Kopf-Metazeilen, damit ein // richten sich nach VenDocs eigenem Aufmaßblatt-Export (Auftrag A00079/2026), das
// einfacher Positions-Import (z. B. VenDoc Universalimport) sie unverändert lesen kann. // 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.
const EH_MAP: Record<string, string> = { Stk: 'ST', m: 'M' }; // aus VenDoc-Beispiel bestätigt
function csvEscape(v: string): string { function csvEscape(v: string): string {
if (/[;"\n\r]/.test(v)) return '"' + v.replace(/"/g, '""') + '"'; if (/[;"\n\r]/.test(v)) return '"' + v.replace(/"/g, '""') + '"';
return v; return v;
} }
function exportCSV() { function exportCSV() {
if (!projekt) return; if (!projekt) return;
const header = ['Pos', 'Artikelnummer', 'Bezeichnung', 'Menge', 'Einheit', 'Notiz']; const header = ['Position', 'EH', 'Positionstext', 'Aufmaß'];
const rows = matList.map((r, i) => [ const rows = matList.map((r, i) => [
String(i + 1), String(i + 1),
r.artNr ?? '', EH_MAP[r.einheit] ?? r.einheit,
r.bezeichnung, r.artNr ? `${r.bezeichnung} (${r.artNr})` : r.bezeichnung,
fmt1(r.menge).replace('.', ','), fmt1(r.menge).replace('.', ',')
r.einheit,
r.notiz ?? ''
]); ]);
const csv = [header, ...rows].map(row => row.map(csvEscape).join(';')).join('\r\n'); const csv = [header, ...rows].map(row => row.map(csvEscape).join(';')).join('\r\n');
const safeName = (titel || 'Materialschein').replace(/[^\p{L}\p{N}_-]+/gu, '_'); const safeName = (titel || 'Materialschein').replace(/[^\p{L}\p{N}_-]+/gu, '_');
@@ -305,9 +307,7 @@ export default function ProjektDetail() {
<label className="field">Datum <label className="field">Datum
<input type="date" value={datum} onChange={e => setDatum(e.target.value)} onBlur={e => saveFeld('datum', e.target.value)} /> <input type="date" value={datum} onChange={e => setDatum(e.target.value)} onBlur={e => saveFeld('datum', e.target.value)} />
</label> </label>
<label className="field">Monteur <MonteurField key={id} value={monteur} onChange={setMonteur} onCommit={v => saveFeld('monteur', v)} />
<input value={monteur} onChange={e => setMonteur(e.target.value)} onBlur={e => saveFeld('monteur', e.target.value)} />
</label>
<label className="field" style={{ gridColumn: 'span 2' }}>Notiz / Bereich <label className="field" style={{ gridColumn: 'span 2' }}>Notiz / Bereich
<textarea value={notiz} onChange={e => setNotiz(e.target.value)} onBlur={e => saveFeld('notiz', e.target.value)} /> <textarea value={notiz} onChange={e => setNotiz(e.target.value)} onBlur={e => saveFeld('notiz', e.target.value)} />
</label> </label>