Leeren Bestellschein aus Materialschein anlegen (bei Neuanlage und im bestehenden Schein)
Build and Deploy / version (push) Successful in 1s
Build and Deploy / build (push) Successful in 9s
Build and Deploy / deploy (push) Successful in 2s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Fidelis Huber
2026-10-09 20:16:19 +02:00
co-authored by Claude Sonnet 5.5
parent 8dd61d8889
commit 1a4559589a
4 changed files with 66 additions and 3 deletions
+26 -1
View File
@@ -1,4 +1,5 @@
import type { ScheinTyp } from './types'; import { api } from './api';
import type { Projekt, ScheinTyp } from './types';
// Materialschein und Bestellschein verhalten sich identisch – nur Beschriftung, Routen // Materialschein und Bestellschein verhalten sich identisch – nur Beschriftung, Routen
// und Dateiname des Exports unterscheiden sich. // und Dateiname des Exports unterscheiden sich.
@@ -17,3 +18,27 @@ export function scheinInfo(typ: ScheinTyp): ScheinInfo {
? { name: 'Bestellschein', plural: 'Bestellscheine', liste: 'Bestellliste', druckTitel: 'Bestellschein', betreff: 'Bestellung', listBase: '/bestellungen', detailBase: '/bestellungen' } ? { name: 'Bestellschein', plural: 'Bestellscheine', liste: 'Bestellliste', druckTitel: 'Bestellschein', betreff: 'Bestellung', listBase: '/bestellungen', detailBase: '/bestellungen' }
: { name: 'Materialschein', plural: 'Materialscheine', liste: 'Materialliste', druckTitel: 'Materialschein', betreff: 'Materialverbrauch', listBase: '/', detailBase: '/projekte' }; : { name: 'Materialschein', plural: 'Materialscheine', liste: 'Materialliste', druckTitel: 'Materialschein', betreff: 'Materialverbrauch', listBase: '/', detailBase: '/projekte' };
} }
// Legt zu einem Materialschein einen leeren Bestellschein mit denselben Kopfdaten an
// (Projekt/Baustelle, Kunde, Auftrags-Nr., Datum, Monteur, Notiz). Beide Scheine sind danach
// unabhängig voneinander – Löschen geht nur im jeweiligen Reiter.
export interface ScheinKopf {
titel: string;
auftraggeber: string;
auftragsNr: string;
datum: string;
monteur: string;
notiz: string;
}
export const normName = (s: string) => s.trim().toLowerCase().replace(/\s+/g, ' ');
export async function bestellscheinFuerKopf(kopf: ScheinKopf): Promise<Projekt> {
return api.projekte.create({ ...kopf, typ: 'Bestell' });
}
// Gibt es zu Projekt + Kunde schon einen Bestellschein? (Schutz vor versehentlichen Doppelten)
export async function findeBestellschein(kopf: Pick<ScheinKopf, 'titel' | 'auftraggeber'>): Promise<Projekt | undefined> {
const alle = await api.projekte.list('Bestell');
return alle.find(p => normName(p.titel) === normName(kopf.titel) && normName(p.auftraggeber ?? '') === normName(kopf.auftraggeber));
}
+11 -1
View File
@@ -5,7 +5,7 @@ import type { Projekt, ProjektStatus, ScheinTyp } from '../lib/types';
import { useFavoriten } from '../lib/favoriten'; import { useFavoriten } from '../lib/favoriten';
import MonteurField from '../components/MonteurField'; import MonteurField from '../components/MonteurField';
import FavStar from '../components/FavStar'; import FavStar from '../components/FavStar';
import { scheinInfo } from '../lib/schein'; import { bestellscheinFuerKopf, scheinInfo } from '../lib/schein';
function dd(s: string | null) { function dd(s: string | null) {
if (!s) return '–'; if (!s) return '–';
@@ -23,6 +23,7 @@ export default function Dashboard({ typ }: { typ: ScheinTyp }) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [mitBestellschein, setMitBestellschein] = useState(false);
const [err, setErr] = useState(''); const [err, setErr] = useState('');
const [form, setForm] = useState({ titel: '', auftraggeber: '', auftragsNr: '', datum: new Date().toISOString().slice(0, 10), monteur: '', notiz: '' }); const [form, setForm] = useState({ titel: '', auftraggeber: '', auftragsNr: '', datum: new Date().toISOString().slice(0, 10), monteur: '', notiz: '' });
@@ -59,7 +60,10 @@ export default function Dashboard({ typ }: { typ: ScheinTyp }) {
setErr(''); setErr('');
try { try {
const projekt = await api.projekte.create({ ...form, typ }); const projekt = await api.projekte.create({ ...form, typ });
// Optional zusätzlich ein leerer Bestellschein mit denselben Kopfdaten (Kunde etc.).
if (typ === 'Material' && mitBestellschein) await bestellscheinFuerKopf(form);
setShowForm(false); setShowForm(false);
setMitBestellschein(false);
navigate(`${info.detailBase}/${projekt.id}`); navigate(`${info.detailBase}/${projekt.id}`);
} catch (e) { } catch (e) {
setErr(e instanceof Error ? e.message : String(e)); setErr(e instanceof Error ? e.message : String(e));
@@ -121,6 +125,12 @@ export default function Dashboard({ typ }: { typ: ScheinTyp }) {
<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>
</div> </div>
{typ === 'Material' && (
<label className="check-line">
<input type="checkbox" checked={mitBestellschein} onChange={e => setMitBestellschein(e.target.checked)} />
Zusätzlich einen leeren Bestellschein mit diesem Kunden anlegen
</label>
)}
<div style={{ display: 'flex', gap: 10, marginTop: 16 }}> <div style={{ display: 'flex', gap: 10, marginTop: 16 }}>
<button className="btn btn-cyan" disabled={busy} onClick={createProjekt}>✓ Anlegen &amp; öffnen</button> <button className="btn btn-cyan" disabled={busy} onClick={createProjekt}>✓ Anlegen &amp; öffnen</button>
<button className="btn btn-ghost" onClick={() => setShowForm(false)}>Abbrechen</button> <button className="btn btn-ghost" onClick={() => setShowForm(false)}>Abbrechen</button>
+27 -1
View File
@@ -6,7 +6,7 @@ import type { MaterialItem, Projekt, ScheinTyp } from '../lib/types';
import { kabelInfoForBez } from '../data/kabelData'; import { kabelInfoForBez } from '../data/kabelData';
import KabelSelect from '../components/KabelSelect'; import KabelSelect from '../components/KabelSelect';
import FavStar from '../components/FavStar'; import FavStar from '../components/FavStar';
import { scheinInfo } from '../lib/schein'; import { bestellscheinFuerKopf, findeBestellschein, scheinInfo } from '../lib/schein';
import MonteurField from '../components/MonteurField'; 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);
@@ -21,6 +21,7 @@ export default function ProjektDetail({ typ }: { typ: ScheinTyp }) {
const [matList, setMatList] = useState<MaterialItem[]>([]); const [matList, setMatList] = useState<MaterialItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [deleteBusy, setDeleteBusy] = useState(false); const [deleteBusy, setDeleteBusy] = useState(false);
const [bestellBusy, setBestellBusy] = useState(false);
// Baustelle-Formular // Baustelle-Formular
const [titel, setTitel] = useState(''); const [titel, setTitel] = useState('');
@@ -84,6 +85,26 @@ export default function ProjektDetail({ typ }: { typ: ScheinTyp }) {
setProjekt(updated); setProjekt(updated);
} }
// Leeren Bestellschein mit denselben Kopfdaten (v. a. Kunde) zu diesem Materialschein anlegen.
async function createBestellschein() {
if (!projekt) return;
const kopf = { titel, auftraggeber, auftragsNr, datum, monteur, notiz };
setBestellBusy(true);
try {
const vorhanden = await findeBestellschein(kopf);
if (vorhanden && !confirm(`Es gibt bereits einen Bestellschein "${vorhanden.titel}"${vorhanden.auftraggeber ? ' (' + vorhanden.auftraggeber + ')' : ''}. Trotzdem einen weiteren anlegen?`)) {
navigate(`/bestellungen/${vorhanden.id}`);
return;
}
const neu = await bestellscheinFuerKopf(kopf);
navigate(`/bestellungen/${neu.id}`);
} catch (e) {
alert('Fehler beim Anlegen: ' + (e instanceof Error ? e.message : e));
} finally {
setBestellBusy(false);
}
}
async function deleteProjekt() { async function deleteProjekt() {
if (!id || !projekt) return; 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; 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;
@@ -292,6 +313,11 @@ export default function ProjektDetail({ typ }: { typ: ScheinTyp }) {
<button className="btn btn-outline" style={{ height: 36, fontSize: 11, padding: '0 14px' }} onClick={toggleStatus}> <button className="btn btn-outline" style={{ height: 36, fontSize: 11, padding: '0 14px' }} onClick={toggleStatus}>
{projekt.status === 'Abgeschlossen' ? '↺ Wieder aktivieren' : '✓ Abschließen'} {projekt.status === 'Abgeschlossen' ? '↺ Wieder aktivieren' : '✓ Abschließen'}
</button> </button>
{typ === 'Material' && (
<button className="btn btn-outline" style={{ height: 36, fontSize: 11, padding: '0 14px' }} disabled={bestellBusy} onClick={createBestellschein}>
🛒 Bestellschein erstellen
</button>
)}
{projekt.status === 'Abgeschlossen' && ( {projekt.status === 'Abgeschlossen' && (
<button className="btn btn-danger" style={{ height: 36, fontSize: 11, padding: '0 14px' }} disabled={deleteBusy} onClick={deleteProjekt}> <button className="btn btn-danger" style={{ height: 36, fontSize: 11, padding: '0 14px' }} disabled={deleteBusy} onClick={deleteProjekt}>
🗑 Löschen 🗑 Löschen
+2
View File
@@ -347,3 +347,5 @@ table.pinfo td:last-child{text-align:right;font-weight:600}
.kab-search-info{font-size:11px;color:var(--muted)} .kab-search-info{font-size:11px;color:var(--muted)}
.reftab td .fav-star{font-size:18px;padding:0 4px} .reftab td .fav-star{font-size:18px;padding:0 4px}
@media print{.fav-star,.kab-select .kab-search-row{display:none!important}} @media print{.fav-star,.kab-select .kab-search-row{display:none!important}}
.check-line{display:flex;align-items:center;gap:10px;margin-top:14px;font-size:13px;color:var(--text);cursor:pointer}
.check-line input{width:18px;height:18px;accent-color:var(--cyan)}