Löschfunktion für abgeschlossene Projekte + VenDoc-CSV-Export
Build and Deploy / version (push) Successful in 1s
Build and Deploy / build (push) Successful in 14s
Build and Deploy / deploy (push) Successful in 2s

Abgeschlossene Materialscheine können jetzt gelöscht werden (serverseitig
auf Status=Abgeschlossen beschränkt, mit Bestätigungsdialog). Neues Feld
Auftrags-Nr. am Projekt für die Zuordnung zum VenDoc-Auftrag. Neuer CSV-Export
(Semikolon, Komma-Dezimal, UTF-8 BOM) als Positionstabelle für den Import ins
ERP.
This commit is contained in:
Fidelis Huber
2026-09-11 08:08:19 +02:00
parent a61722bb4f
commit 36d5246187
7 changed files with 94 additions and 8 deletions
+61 -3
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { api } from '../lib/api';
import type { MaterialItem, Projekt } from '../lib/types';
import { kabelInfoForBez } from '../data/kabelData';
@@ -11,13 +11,16 @@ const ddDate = (s: string) => { if (!s) return '—'; const [y, m, d] = s.split(
export default function ProjektDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [projekt, setProjekt] = useState<Projekt | null>(null);
const [matList, setMatList] = useState<MaterialItem[]>([]);
const [loading, setLoading] = useState(true);
const [deleteBusy, setDeleteBusy] = useState(false);
// Baustelle-Formular
const [titel, setTitel] = useState('');
const [auftraggeber, setAuftraggeber] = useState('');
const [auftragsNr, setAuftragsNr] = useState('');
const [datum, setDatum] = useState('');
const [monteur, setMonteur] = useState('');
const [notiz, setNotiz] = useState('');
@@ -54,7 +57,7 @@ export default function ProjektDetail() {
setProjekt(p);
setMatList(list);
if (p) {
setTitel(p.titel); setAuftraggeber(p.auftraggeber ?? ''); setDatum(p.datum ? p.datum.slice(0, 10) : '');
setTitel(p.titel); setAuftraggeber(p.auftraggeber ?? ''); setAuftragsNr(p.auftragsNr ?? ''); setDatum(p.datum ? p.datum.slice(0, 10) : '');
setMonteur(p.monteur ?? ''); setNotiz(p.notiz ?? '');
}
setLoading(false);
@@ -64,7 +67,7 @@ export default function ProjektDetail() {
if (id) setMatList(await api.material.list(id));
}
async function saveFeld(feld: 'titel' | 'auftraggeber' | 'datum' | 'monteur' | 'notiz', value: string) {
async function saveFeld(feld: 'titel' | 'auftraggeber' | 'auftragsNr' | 'datum' | 'monteur' | 'notiz', value: string) {
if (!id) return;
await api.projekte.update(id, { [feld]: value } as Partial<Projekt>);
}
@@ -76,6 +79,19 @@ export default function ProjektDetail() {
setProjekt(updated);
}
async function deleteProjekt() {
if (!id || !projekt) return;
if (!confirm(`"${projekt.titel}" endgültig löschen? Alle ${matList.length} Material-Positionen werden mitgelöscht. Das kann nicht rückgängig gemacht werden.`)) return;
setDeleteBusy(true);
try {
await api.projekte.remove(id);
navigate('/');
} catch (e) {
alert('Fehler beim Löschen: ' + (e instanceof Error ? e.message : e));
setDeleteBusy(false);
}
}
// ── Kabel-Rechner ──
const kabpm = kabTyp ? parseFloat(kabTyp.split('|')[1]) : 0;
const kabNetto = (parseFloat(kabKg) || 0) - (parseFloat(kabTara) || 0);
@@ -200,6 +216,38 @@ export default function ProjektDetail() {
setRetourBusy(false);
}
// ── CSV-Export (für Import in VenDoc o.ä. als Auftragspositionen) ──
// Semikolon-getrennt, Komma als Dezimaltrenner, UTF-8 mit BOM – Standardkonvention
// für deutschsprachiges Excel/ERP, damit die Datei ohne Umweg direkt geöffnet/
// importiert werden kann. Reine Positionstabelle ohne Kopf-Metazeilen, damit ein
// einfacher Positions-Import (z. B. VenDoc Universalimport) sie unverändert lesen kann.
function csvEscape(v: string): string {
if (/[;"\n\r]/.test(v)) return '"' + v.replace(/"/g, '""') + '"';
return v;
}
function exportCSV() {
if (!projekt) return;
const header = ['Pos', 'Artikelnummer', 'Bezeichnung', 'Menge', 'Einheit', 'Notiz'];
const rows = matList.map((r, i) => [
String(i + 1),
r.artNr ?? '',
r.bezeichnung,
fmt1(r.menge).replace('.', ','),
r.einheit,
r.notiz ?? ''
]);
const csv = [header, ...rows].map(row => row.map(csvEscape).join(';')).join('\r\n');
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);
}
if (loading) return <div className="empty-msg"><div className="spin" /></div>;
if (!projekt) return <div className="empty-msg">Projekt nicht gefunden.</div>;
@@ -219,6 +267,7 @@ export default function ProjektDetail() {
<table className="pinfo">
<tbody>
<tr><td colSpan={2}>Materialschein</td></tr>
{auftragsNr && <tr><td>Auftrags-Nr.</td><td>{auftragsNr}</td></tr>}
<tr><td>Datum</td><td>{ddDate(datum)}</td></tr>
<tr><td>Monteur</td><td>{monteur || '—'}</td></tr>
<tr><td>Status</td><td>{projekt.status}</td></tr>
@@ -234,6 +283,11 @@ export default function ProjektDetail() {
<button className="btn btn-outline" style={{ height: 36, fontSize: 11, padding: '0 14px' }} onClick={toggleStatus}>
{projekt.status === 'Abgeschlossen' ? '↺ Wieder aktivieren' : '✓ Abschließen'}
</button>
{projekt.status === 'Abgeschlossen' && (
<button className="btn btn-danger" style={{ height: 36, fontSize: 11, padding: '0 14px' }} disabled={deleteBusy} onClick={deleteProjekt}>
🗑 Löschen
</button>
)}
</div>
<div className="card baustelle-card">
@@ -245,6 +299,9 @@ export default function ProjektDetail() {
<label className="field">Auftraggeber
<input value={auftraggeber} onChange={e => setAuftraggeber(e.target.value)} onBlur={e => saveFeld('auftraggeber', e.target.value)} />
</label>
<label className="field">Auftrags-Nr. <span style={{ opacity: .6 }}>(VenDoc)</span>
<input value={auftragsNr} onChange={e => setAuftragsNr(e.target.value)} onBlur={e => saveFeld('auftragsNr', e.target.value)} placeholder="z. B. A00031/2026" />
</label>
<label className="field">Datum
<input type="date" value={datum} onChange={e => setDatum(e.target.value)} onBlur={e => saveFeld('datum', e.target.value)} />
</label>
@@ -420,6 +477,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>
</div>
</div>
);