Rechnungsdialog: Bausteine für Projektzusammenfassung, Positionen, Vorschauzeilen und Arbeitsblattauswahl

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:35:04 +00:00
1 parent 6a5f943034
commit 8ab78b7b2c
5 files changed
+356

No files matched your search

@@ -0,0 +1,100 @@
import { Button } from '../ui'
import { euro } from '../../lib/financeFormat'
import { INVOICE_LIMITS, lineTotalCents, invoiceTotalCents } from '../../lib/financeView'
/**
* Positionszeilen des Rechnungsdialogs (Beschreibung, Menge, Einzelpreis, Summe, Entfernen) mit „+ Position“-Knopf.
* Rein darstellend, hält keinen Zustand. Props: legende, labelPrefix (z. B. „Position“ oder „Zusatzposition“), idPrefix,
* zeilen ([{ uid, description, quantity, price }]), minZeilen, onChange(uid, feld, wert), onAdd(), onRemove(uid), addLabel,
* zeigeSumme (Gesamtsumme und Hinweis zu § 19 UStG).
*/
export default function RechnungPositionen({
legende,
labelPrefix,
idPrefix,
zeilen,
minZeilen = 0,
onChange,
onAdd,
onRemove,
addLabel,
zeigeSumme = false,
}) {
return (
<fieldset className="rd-group">
<legend>{legende}</legend>
<div className="rd-stack">
{zeilen.map((zeile, index) => {
const n = index + 1
const summe = lineTotalCents(zeile)
return (
<div className="rd-item" key={zeile.uid}>
<div className="rd-item-field rd-item-desc">
<label className="rd-item-label" htmlFor={`${idPrefix}-desc-${zeile.uid}`}>Beschreibung</label>
<input
id={`${idPrefix}-desc-${zeile.uid}`}
className="input"
value={zeile.description}
maxLength={INVOICE_LIMITS.maxDescription}
placeholder="z. B. Webdesign Startseite"
aria-label={`${labelPrefix} ${n}: Beschreibung`}
onChange={(e) => onChange(zeile.uid, 'description', e.target.value)}
/>
</div>
<div className="rd-item-field rd-item-qty">
<label className="rd-item-label" htmlFor={`${idPrefix}-qty-${zeile.uid}`}>Menge</label>
<input
id={`${idPrefix}-qty-${zeile.uid}`}
className="input fin-amount"
value={zeile.quantity}
inputMode="numeric"
aria-label={`${labelPrefix} ${n}: Menge`}
onChange={(e) => onChange(zeile.uid, 'quantity', e.target.value)}
/>
</div>
<div className="rd-item-field rd-item-price">
<label className="rd-item-label" htmlFor={`${idPrefix}-price-${zeile.uid}`}>Einzelpreis in €</label>
<input
id={`${idPrefix}-price-${zeile.uid}`}
className="input fin-amount"
value={zeile.price}
inputMode="decimal"
placeholder="0,00"
aria-label={`${labelPrefix} ${n}: Einzelpreis in Euro`}
onChange={(e) => onChange(zeile.uid, 'price', e.target.value)}
/>
</div>
<div className="rd-item-field rd-item-sumfeld">
<span className="rd-item-label">Summe</span>
<span className="fin-amount rd-item-sum">{summe === null ? '–' : euro(summe)}</span>
</div>
<div className="rd-item-remove">
<button
type="button"
className="link"
disabled={zeilen.length <= minZeilen}
aria-label={`${labelPrefix} ${n} entfernen`}
onClick={() => onRemove(zeile.uid)}
>
Entfernen
</button>
</div>
</div>
)
})}
<div className="rd-items-foot">
<Button type="button" onClick={onAdd} disabled={zeilen.length >= INVOICE_LIMITS.maxItems}>
{addLabel}
</Button>
{zeigeSumme && (
<div className="rd-total" aria-live="polite">
<span>Summe</span>
<span className="fin-amount">{euro(invoiceTotalCents(zeilen))}</span>
</div>
)}
</div>
{zeigeSumme && <p className="rd-hint">Gemäß § 19 UStG ohne Umsatzsteuer.</p>}
</div>
</fieldset>
)
}