Löschfunktion für abgeschlossene Projekte + VenDoc-CSV-Export
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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user