Leeren Bestellschein aus Materialschein anlegen (bei Neuanlage und im bestehenden Schein)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8dd61d8889
commit
1a4559589a
@@ -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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 & öffnen</button>
|
<button className="btn btn-cyan" disabled={busy} onClick={createProjekt}>✓ Anlegen & öffnen</button>
|
||||||
<button className="btn btn-ghost" onClick={() => setShowForm(false)}>Abbrechen</button>
|
<button className="btn btn-ghost" onClick={() => setShowForm(false)}>Abbrechen</button>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user