Rechnungsdialog: Bausteine für Projektzusammenfassung, Positionen, Vorschauzeilen und Arbeitsblattauswahl
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user