Rechnungen: Positionen auch aus der Domain-Aufstellung übernehmen (Verkaufspreis)
This commit is contained in:
parent
6f9efb91dc
commit
e0fd57532d
3 changed files with 38 additions and 4 deletions
|
|
@ -14,23 +14,25 @@ interface Suggested { description: string; quantity: number; unitPriceNetCents:
|
|||
const toRow = (it: Suggested): Row => ({ description: it.description, quantity: String(it.quantity), unitPrice: (it.unitPriceNetCents / 100).toFixed(2).replace('.', ','), taxBp: String(it.taxBp / 100) });
|
||||
interface Prod { id: string; status: string; current: { name: string } | null }
|
||||
interface Contract { id: string; number: string; productName: string | null }
|
||||
interface Dom { id: string; domain: string; sellNetCents: number | null }
|
||||
|
||||
export default function RechnungDetail() {
|
||||
const { id } = useParams<{ id: string }>(); const { can } = useSession(); const staff = can('invoices.read');
|
||||
const [inv, setInv] = useState<Inv | null>(null); const [err, setErr] = useState(''); const [ok, setOk] = useState('');
|
||||
const [rows, setRows] = useState<Row[]>([emptyRow()]); const [busy, setBusy] = useState(false);
|
||||
const [products, setProducts] = useState<Prod[]>([]); const [contracts, setContracts] = useState<Contract[]>([]);
|
||||
const [products, setProducts] = useState<Prod[]>([]); const [contracts, setContracts] = useState<Contract[]>([]); const [domains, setDomains] = useState<Dom[]>([]);
|
||||
const load = useCallback(() => api<Inv>('GET', `/invoices/${id}`).then((v) => { setInv(v); if (v.status === 'draft') setRows(v.items.length ? v.items.map((it) => toRow(it)) : [emptyRow()]); }).catch((e) => setErr(errMsg(e))), [id]);
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
useEffect(() => {
|
||||
if (!inv || inv.status !== 'draft' || !inv.canWrite) return;
|
||||
api<Prod[]>('GET', '/admin/products').then((l) => setProducts(l.filter((p) => p.status === 'active' && p.current))).catch(() => undefined);
|
||||
api<Contract[]>('GET', `/contracts?org=${inv.orgId}&status=active`).then(setContracts).catch(() => undefined);
|
||||
}, [inv]);
|
||||
if (can('domains.read')) api<Dom[]>('GET', `/admin/domain-records?orgId=${inv.orgId}`).then(setDomains).catch(() => undefined);
|
||||
}, [inv, can]);
|
||||
|
||||
const run = async (fn: () => Promise<unknown>, msg?: string) => { setErr(''); setOk(''); setBusy(true); try { await fn(); if (msg) setOk(msg); void load(); } catch (x) { setErr(errMsg(x)); } finally { setBusy(false); } };
|
||||
function appendSuggested(items: Suggested[]) { setRows((rs) => [...rs.filter((r) => r.description.trim() || r.unitPrice.trim()), ...items.map(toRow)]); }
|
||||
async function takeFrom(kind: 'product' | 'contract', pickId: string) {
|
||||
async function takeFrom(kind: 'product' | 'contract' | 'domain', pickId: string) {
|
||||
if (!pickId) return; setErr('');
|
||||
try { const r = await api<{ items: Suggested[] }>('GET', `/invoices/suggest-from-${kind}/${pickId}`); appendSuggested(r.items); } catch (x) { setErr(errMsg(x)); }
|
||||
}
|
||||
|
|
@ -52,9 +54,10 @@ export default function RechnungDetail() {
|
|||
{err && <Alert kind="err">{err}</Alert>}{ok && <Alert kind="ok">{ok}</Alert>}
|
||||
|
||||
{isDraft && inv.canWrite ? (<form className="card" onSubmit={saveItems}><h2>Positionen</h2>
|
||||
{(products.length > 0 || contracts.length > 0) && <div className="cols" style={{ marginBottom: 16 }}>
|
||||
{(products.length > 0 || contracts.length > 0 || domains.length > 0) && <div className="cols" style={{ marginBottom: 16 }}>
|
||||
{products.length > 0 && <Field id="fromProduct" label="Position aus Produkt übernehmen" hint="Fügt Einrichtung und/oder wiederkehrenden Preis als eigene Zeilen hinzu."><select id="fromProduct" value="" onChange={(e) => void takeFrom('product', e.target.value)}><option value="">Produkt wählen …</option>{products.map((p) => <option key={p.id} value={p.id}>{p.current!.name}</option>)}</select></Field>}
|
||||
{contracts.length > 0 && <Field id="fromContract" label="Position aus laufendem Vertrag übernehmen" hint="Übernimmt den wiederkehrenden Preis der aktuellen Periode."><select id="fromContract" value="" onChange={(e) => void takeFrom('contract', e.target.value)}><option value="">Vertrag wählen …</option>{contracts.map((c) => <option key={c.id} value={c.id}>{c.number} · {c.productName ?? '–'}</option>)}</select></Field>}
|
||||
{domains.length > 0 && <Field id="fromDomain" label="Position aus Domain übernehmen" hint="Verkaufspreis aus der Domain-Aufstellung, nie der Einkauf."><select id="fromDomain" value="" onChange={(e) => void takeFrom('domain', e.target.value)}><option value="">Domain wählen …</option>{domains.map((d) => <option key={d.id} value={d.id} disabled={d.sellNetCents === null}>{d.domain}{d.sellNetCents === null ? ' (kein Verkaufspreis)' : ''}</option>)}</select></Field>}
|
||||
</div>}
|
||||
{rows.map((r, i) => (<div className="cols" key={i} style={{ gridTemplateColumns: '3fr 1fr 1fr 1fr auto', alignItems: 'end' }}>
|
||||
<Field id={`d${i}`} label="Beschreibung"><input id={`d${i}`} value={r.description} onChange={(e) => setRow(i, 'description', e.target.value)} /></Field>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue