Rechnung schreiben: neuer Dialog mit Projektwahl, Zusammenfassung, Art, Aufwand mit Vorschau, Anzahlung und Schlussrechnung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:43:17 +00:00
1 parent 8ab78b7b2c
commit 2aa4abeadb
3 files changed
+604 -10

No files matched your search

+595 -9
View File
@@ -1,13 +1,599 @@
import './rechnung-dialog.css'
import { useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { AutocompleteInput, Button, Dialog, Field } from '../ui'
import AddressWarning from '../finance/AddressWarning'
import KontoZusammenfassung from './KontoZusammenfassung'
import RechnungPositionen from './RechnungPositionen'
import RechnungVorschau from './RechnungVorschau'
import AufwandAuswahl from './AufwandAuswahl'
import { useCustomers } from '../../hooks/useCustomers'
import { useScrollIntoView } from '../../hooks/useScrollIntoView'
import {
useKundenTickets,
useProjektkontoDetail,
useProjektkonten,
useRechnungVorschau,
useWeitereBlaetter,
} from '../../hooks/useRechnungDaten'
import { createInvoice } from '../../lib/financeApi'
import { canInvoice, euro } from '../../lib/financeFormat'
import {
ADDRESS_INCOMPLETE_TEXT,
INVOICE_LIMITS,
customerName,
customerPickerOptions,
customerSuggestions,
invoiceSendGate,
invoiceTotalCents,
resolveCustomerText,
} from '../../lib/financeView'
import { kontoZeile, satzText, stripeHinweis, KEINE_PROJEKTE_HINWEIS } from '../../lib/projektkontoView'
import {
abLabel,
anzahlungStandard,
artOptionen,
aufwandAuftrag,
berechnetFuerAuftrag,
gueltigeFestpreisAbs,
projektOptionen,
standardArt,
standardAuftrag,
startProjekt,
ticketOptionen,
wirksamesProjekt,
} from '../../lib/rechnungDialogView'
import {
BEZEICHNUNG_MAX,
VORSCHAU_ARTEN,
createSubmitter,
darfSpeichern,
rechnungAnfrage,
vorschauAnfrage,
vorschauSchluessel,
vorschauStatus,
} from '../../lib/rechnungAnfrage'
import { anfangszustand, reduce } from '../../lib/rechnungDialogState'
/** /**
* Platzhalter von W0: Strang W1 ersetzt diese Datei vollständig (Dateiname, Default-Export und Props bleiben). * Dialog „Rechnung schreiben“ (ersetzt InvoiceCreateModal). Ablauf von oben nach unten: Kunde (fest oder per Suche), Projekt,
* Projektkonto (Vereinbart, Berechnet, Noch offen, Zeit), Art der Rechnung (Anzahlung, Schlussrechnung, nach Aufwand, Festbetrag,
* ohne Projekt nur freie Positionen), Inhalt je Art mit Vorschau vom Server, Leistungszeitraum, Zahlungsziel und Notiz.
* „Als Entwurf speichern“ und „Senden“ bleiben gesperrt, solange die Vorschau nicht zur aktuellen Eingabe gehört.
* *
* Props (V8): open, onClose, onCreated(rechnung, info), kunde (Kundendokument oder null = Kundenwahl im Dialog), * Props: open, onClose, onCreated(rechnung, info), kunde (Kundendokument; null = Kundenwahl im Dialog), projektId (undefined =
* projektId (undefined = keine Vorauswahl, null = „Ohne Projekt“, Text = dieses Projekt), auftraegeAktiv (nur der Startwert, * keine Vorauswahl, null = „Ohne Projekt“, Text = dieses Projekt), auftraegeAktiv (nur der Startwert, danach gilt
* danach gilt getProjektkonten, das der Dialog selbst lädt), onShowOverview (optional, „Zur Übersicht“ in der Adresswarnung). * getProjektkonten), onShowOverview (optional, „Zur Übersicht“ in der Adresswarnung).
* `info` von onCreated wird unverändert weitergereicht wie im bisherigen InvoiceCreateModal ({ senden }, bei festgeschriebener, * onCreated bekommt wie im bisherigen Dialog ({ senden }) bzw. bei festgeschriebener, nicht versendeter Rechnung
* nicht versendeter Rechnung { senden: true, notice, tone: 'warn', finalizedFilter }); danach ruft der Dialog onClose(). * (502 mit invoiceId) (null, { senden: true, notice, tone: 'warn', finalizedFilter }); danach ruft der Dialog onClose().
* Der Dialog sitzt zusätzlich in InvoiceTable.jsx (dort ersetzt W1 das Modal), FinancePage.jsx bleibt unverändert. * Enter sendet nie (das Formular hat keinen Absenden-Knopf); „Senden“ fragt vorher nach.
*/ */
export default function RechnungDialog({ open, onClose, onCreated, kunde, projektId, auftraegeAktiv, onShowOverview }) { export default function RechnungDialog({ open, onClose, onCreated, kunde = null, projektId, auftraegeAktiv, onShowOverview = null }) {
return null if (!open) return null
const gemeinsam = { onClose, onCreated, projektId, auftraegeAktiv, onShowOverview }
// Die Kundenliste (Appwrite, bis 5000 Dokumente) wird nur geholt, wenn der Kunde nicht feststeht
return kunde ? <RechnungFormular {...gemeinsam} festerKunde={kunde} quelle={null} /> : <KundenWahl {...gemeinsam} />
}
function KundenWahl(props) {
const { customers, loading, error } = useCustomers()
return <RechnungFormular {...props} festerKunde={null} quelle={{ customers, loading, error }} />
}
const ZEITRAUM_PLATZHALTER = 'z. B. 01.10.–20.10.2026'
function RechnungFormular({ onClose, onCreated, projektId, auftraegeAktiv, onShowOverview, festerKunde, quelle }) {
const [state, dispatch] = useReducer(reduce, startProjekt(projektId), (p) => anfangszustand({ projektId: p }))
const [customerText, setCustomerText] = useState('')
const [error, setError] = useState('')
const [busy, setBusy] = useState(null) // null | 'draft' | 'send'
const submitter = useMemo(() => createSubmitter({ create: createInvoice, confirm: (t) => window.confirm(t) }), [])
const errorRef = useRef(null)
const aliveRef = useRef(true)
const kundeIdRef = useRef('')
const neuGeladenRef = useRef(null)
useScrollIntoView(errorRef, error)
useEffect(() => {
aliveRef.current = true
return () => {
aliveRef.current = false
}
}, [])
const act = (aktion) => {
if (error) setError('')
dispatch(aktion)
}
// --- Abgeleitete Werte ------------------------------------------------------------------------------------
const options = useMemo(() => customerPickerOptions(quelle?.customers), [quelle?.customers])
const resolved = festerKunde
? { state: canInvoice(festerKunde).ok ? 'ok' : 'blocked', option: null }
: resolveCustomerText(options, customerText)
const kundeDoc = festerKunde || resolved.option?.customer || null
const kunde = kundeDoc && canInvoice(kundeDoc).ok ? kundeDoc : null
const blockiert = Boolean(kundeDoc) && !kunde
const konten = useProjektkonten(kunde?.$id)
const projekt = wirksamesProjekt(state.projektId, konten.data?.konten)
const aktiv = konten.data ? Boolean(konten.data.auftraegeAktiv) : Boolean(auftraegeAktiv)
const detail = useProjektkontoDetail(projekt || '')
const auftraege = detail.data?.auftraege || []
const rechnungen = detail.data?.rechnungen || []
let optionen = []
if (projekt === '') optionen = artOptionen({ projektId: '', auftraege: [], rechnungen: [], auftraegeAktiv: aktiv })
else if (projekt && detail.data) optionen = artOptionen({ projektId: projekt, auftraege, rechnungen, auftraegeAktiv: aktiv })
const art = optionen.some((o) => o.key === state.art && o.enabled)
? state.art
: standardArt(optionen, { auftraege, rechnungen, offeneMinuten: detail.data?.konto?.zeit?.offeneMinuten || 0 })
const festpreisAbs = gueltigeFestpreisAbs(auftraege)
const auftragId =
(art === 'anzahlung' || art === 'schluss') && festpreisAbs.some((a) => a.id === state.auftragId)
? state.auftragId
: standardAuftrag(art, auftraege, rechnungen)
const ab = auftraege.find((a) => a.id === auftragId)
const weitereTickets = state.weitereTickets
const weitere = useWeitereBlaetter(weitereTickets)
const blaetter = [...(detail.data?.offeneArbeitsblaetter || []), ...weitereTickets.flatMap((t) => weitere[t.ticketId]?.blaetter || [])]
const kundenTickets = useKundenTickets(kunde?.$id, art === 'aufwand')
const e = {
kunde,
art,
projektId: projekt ?? '',
auftragId,
items: state.items,
zusatz: state.zusatz,
blaetter,
abgewaehlt: state.abgewaehlt,
anzahlung: { ...anzahlungStandard(ab), ...state.anzahlung },
zeilentexte: state.zeilentexte,
zeitraum: state.zeitraum,
zeitraumGeaendert: state.zeitraumGeaendert,
days: state.days,
memo: state.memo,
}
const vorschauReq = vorschauAnfrage(e)
const schluessel = vorschauReq.ok ? vorschauSchluessel(vorschauReq.body) : ''
const vorschau = useRechnungVorschau({ anfrage: vorschauReq, schluessel, aktiv: VORSCHAU_ARTEN.includes(art) })
const status = vorschauStatus({ art, anfrage: vorschauReq, vorschau, schluessel })
const anfrage = rechnungAnfrage(e)
let totalCents = null
if (art === 'frei' || art === 'festbetrag') totalCents = invoiceTotalCents(state.items)
else if (status.state === 'bereit') totalCents = vorschau.daten.summeCents
const gate = invoiceSendGate(kunde, totalCents)
const darf = darfSpeichern(status) && busy === null && Boolean(kunde) && projekt !== null && art !== ''
// --- Effekte auf die Vorschau -----------------------------------------------------------------------------
const vorschauZeitraum = status.state === 'bereit' ? vorschau.daten.leistungszeitraum || '' : null
useEffect(() => {
// Der Vorschlag überschreibt nie eine Eingabe des Nutzers (der Reducer prüft das)
if (art === 'aufwand' && vorschauZeitraum !== null) dispatch({ type: 'zeitraum-vorschlag', text: vorschauZeitraum })
}, [art, vorschauZeitraum])
const detailReloadRef = useRef(detail.reload)
detailReloadRef.current = detail.reload
useEffect(() => {
// 409 der Vorschau: ein Arbeitsblatt ist inzwischen abgerechnet, das Projektkonto neu laden (einmal je Fehlerobjekt)
if (vorschau?.status === 'fehler' && vorschau.httpStatus === 409 && neuGeladenRef.current !== vorschau) {
neuGeladenRef.current = vorschau
detailReloadRef.current()
}
}, [vorschau])
// --- Aktionen ---------------------------------------------------------------------------------------------
async function abschicken(senden) {
setBusy(senden ? 'send' : 'draft')
setError('')
const r = await submitter({ anfrage, senden, kunde, totalCents })
if (aliveRef.current) setBusy(null)
if (r.status === 'fehler') {
if (aliveRef.current) setError(r.message)
if (r.httpStatus === 409) detail.reload()
} else if (r.status === 'festgeschrieben') {
if (onCreated) onCreated(null, { senden: true, notice: r.notice, tone: 'warn', finalizedFilter: r.finalizedFilter })
onClose()
} else if (r.status === 'ok') {
if (onCreated) onCreated(r.rechnung, { senden: r.senden })
onClose()
}
}
const close = () => {
if (busy !== null) return
onClose()
}
function kundeGeaendert(text) {
setCustomerText(text)
if (error) setError('')
const neu = resolveCustomerText(options, text).option?.customer?.$id || ''
if (neu !== kundeIdRef.current) {
kundeIdRef.current = neu
dispatch({ type: 'projekt', projektId: null })
}
}
const gesperrt = busy !== null
const footer = (
<div className="rd-foot">
<Button type="button" onClick={close} disabled={gesperrt}>
Abbrechen
</Button>
<Button type="button" disabled={!darf} onClick={() => abschicken(false)}>
{busy === 'draft' ? 'Speichere …' : 'Als Entwurf speichern'}
</Button>
<Button
type="button"
variant="primary"
disabled={!darf || !gate.sendAllowed}
title={gate.sendAllowed ? undefined : ADDRESS_INCOMPLETE_TEXT}
onClick={() => abschicken(true)}
>
{busy === 'send' ? 'Sende …' : 'Senden'}
</Button>
</div>
)
// --- Abschnitte -------------------------------------------------------------------------------------------
const mitPositionen = (liste, legende, labelPrefix, idPrefix, minZeilen, addLabel, zeigeSumme) => (
<RechnungPositionen
legende={legende}
labelPrefix={labelPrefix}
idPrefix={idPrefix}
zeilen={state[liste]}
minZeilen={minZeilen}
onChange={(uid, feld, wert) => act({ type: 'position', liste, uid, feld, wert })}
onAdd={() => act({ type: 'position+', liste })}
onRemove={(uid) => act({ type: 'position-', liste, uid })}
addLabel={addLabel}
zeigeSumme={zeigeSumme}
/>
)
const abBlock = () => {
if (!ab) return null
const berechnet = berechnetFuerAuftrag(rechnungen, ab.id)
return (
<div className="rd-stack">
{festpreisAbs.length > 1 ? (
<Field label="Auftragsbestätigung" htmlFor="rd-ab">
<select id="rd-ab" className="select" value={ab.id} onChange={(ev) => act({ type: 'auftrag', auftragId: ev.target.value })}>
{festpreisAbs.map((a) => (
<option key={a.id} value={a.id}>
{abLabel(a)}
</option>
))}
</select>
</Field>
) : (
<p className="rd-hint">{abLabel(ab)}</p>
)}
<p className="rd-hint">
Auftragssumme {euro(ab.summeCents)} · bereits berechnet {euro(berechnet)} · noch nicht berechnet {euro(Math.max(0, ab.summeCents - berechnet))}
</p>
</div>
)
}
const anzahlungFelder = () => {
const a = e.anzahlung
const prozent = a.modus !== 'betrag'
return (
<div className="rd-stack">
<div className="seg rd-seg" role="group" aria-label="Anzahlung in">
<button type="button" aria-pressed={prozent} onClick={() => act({ type: 'anzahlung', patch: { modus: 'prozent' } })}>
Prozent
</button>
<button type="button" aria-pressed={!prozent} onClick={() => act({ type: 'anzahlung', patch: { modus: 'betrag' } })}>
Betrag
</button>
</div>
<div className="field-row">
<Field label={prozent ? 'Anzahlung in %' : 'Betrag in €'} htmlFor="rd-anzahlung-wert">
<input
id="rd-anzahlung-wert"
className="input"
inputMode="decimal"
value={a.wert}
onChange={(ev) => act({ type: 'anzahlung', patch: { wert: ev.target.value } })}
/>
</Field>
<Field label="Bezeichnung" htmlFor="rd-anzahlung-bez">
<input
id="rd-anzahlung-bez"
className="input"
maxLength={BEZEICHNUNG_MAX}
value={a.bezeichnung}
onChange={(ev) => act({ type: 'anzahlung', patch: { bezeichnung: ev.target.value } })}
/>
</Field>
</div>
</div>
)
}
const statusBereich = () => (
<>
{status.state === 'laedt' && (
<p className="msg muted" role="status" aria-live="polite">
Vorschau wird aktualisiert …
</p>
)}
{status.state === 'ungueltig' && status.message && <p className="msg muted">{status.message}</p>}
{status.state === 'fehler' && (
<p className="msg err" role="alert">
{status.message}
</p>
)}
</>
)
const vorschauZeilen = () => {
const daten = vorschau?.daten
if ((status.state !== 'bereit' && status.state !== 'laedt') || !daten) return null
return (
<RechnungVorschau
positionen={daten.positionen}
summeCents={daten.summeCents}
bearbeitbar={art === 'aufwand'}
zeilentexte={state.zeilentexte}
onZeilentext={(schl, text, original) => act({ type: 'zeilentext', schluessel: schl, text, original })}
abgeblendet={status.state === 'laedt'}
/>
)
}
const aufwandBlock = () => {
const konto = detail.data.konto
const laut = aufwandAuftrag(auftraege)
const satzInfo = `Stundensatz ${satzText(konto.zeit?.satzCents)}${laut ? `, laut ${laut.nummer}` : ''}`
const ausgeschlossen = [...(konto.zeitquelle || []), ...(konto.tickets || []).map((t) => t.id), ...weitereTickets.map((t) => t.ticketId)]
return (
<>
<AufwandAuswahl
blaetter={blaetter}
abgewaehlt={state.abgewaehlt}
onToggle={(id) => act({ type: 'blatt', id })}
satzInfo={satzInfo}
ticketOptionen={ticketOptionen(kundenTickets.tickets, { ausgeschlossen })}
ticketsLaden={kundenTickets.loading}
ticketsFehler={kundenTickets.error}
weitereTickets={weitereTickets}
weitereZustand={weitere}
onAddTicket={(o) =>
act({ type: 'ticket+', ticket: { ticketId: o.ticketId, woid: o.woid, titel: o.label.replace(/^#\S+ · /, '') } })
}
onRemoveTicket={(id) => act({ type: 'ticket-', ticketId: id, blattIds: (weitere[id]?.blaetter || []).map((b) => b.id) })}
/>
{statusBereich()}
{vorschauZeilen()}
{mitPositionen('zusatz', 'Zusätzliche Positionen', 'Zusatzposition', 'rd-zus', 0, '+ Zusatzposition', false)}
</>
)
}
const inhalt = () => {
if (art === 'frei' || art === 'festbetrag') return mitPositionen('items', 'Positionen', 'Position', 'rd-pos', 1, '+ Position', true)
if (art === 'aufwand') return aufwandBlock()
if (art === 'anzahlung') {
return (
<>
{abBlock()}
{anzahlungFelder()}
{statusBereich()}
{vorschauZeilen()}
</>
)
}
if (art === 'schluss') {
return (
<>
{abBlock()}
{mitPositionen('zusatz', 'Zusätzliche Positionen (Nachträge)', 'Zusatzposition', 'rd-zus', 0, '+ Zusatzposition', false)}
{statusBereich()}
{vorschauZeilen()}
</>
)
}
return null
}
const projektBereit = projekt !== null
const kontoBereit = projekt === '' || Boolean(detail.data)
const hinweise = [...new Set(optionen.filter((o) => !o.enabled && o.hint).map((o) => o.hint))]
// Enter sendet nie (das wäre eine Mail an den Kunden): das Formular hat keinen Absenden-Knopf, jeder Knopf darin ist type="button"
return (
<Dialog as="form" wide title="Rechnung schreiben" onClose={close} busy={gesperrt} onSubmit={(ev) => ev.preventDefault()} footer={footer}>
{error && (
<div className="msg err" role="alert" ref={errorRef}>
{error}
</div>
)}
<div className="rd-stack">
<Field label="Kunde" htmlFor={festerKunde ? undefined : 'rd-kunde'}>
{festerKunde ? (
<p>
<strong>{customerName(festerKunde) || 'Kunde'}</strong>
{festerKunde.email ? <span className="msg muted"> · {festerKunde.email}</span> : null}
</p>
) : quelle?.loading ? (
<p className="msg muted" role="status">
Kunden werden geladen …
</p>
) : quelle?.error ? (
<p className="msg warn" role="alert">
Die Kunden konnten nicht geladen werden: {quelle.error}
</p>
) : (
<AutocompleteInput
id="rd-kunde"
value={customerText}
onChange={kundeGeaendert}
getSuggestions={(t) => customerSuggestions(options, t)}
placeholder="Kunde suchen …"
/>
)}
</Field>
{blockiert && (
<p className="msg warn" role="alert">
{customerName(kundeDoc)}: {festerKunde ? canInvoice(festerKunde).reason : resolved.option?.reason}. Für diesen Kunden kann keine Rechnung geschrieben werden.
</p>
)}
{!festerKunde && resolved.state === 'unknown' && (
<p className="rd-hint">Bitte einen Kunden aus der Liste wählen. Nur feste Kunden mit E-Mail-Adresse erscheinen dort.</p>
)}
{!festerKunde && kunde && kunde.email && <p className="rd-hint">Die Rechnung geht an {kunde.email}.</p>}
{kunde && gate.warn && (
<AddressWarning
customer={kunde}
variant="dialog"
onShowOverview={
onShowOverview
? () => {
if (busy !== null) return
onClose()
onShowOverview()
}
: undefined
}
/>
)}
{kunde && (
<>
<Field label="Projekt" htmlFor="rd-projekt">
<select
id="rd-projekt"
className="select"
value={projekt === null ? '?' : projekt}
disabled={konten.loading}
onChange={(ev) => act({ type: 'projekt', projektId: ev.target.value })}
>
{projekt === null && (
<option value="?" disabled>
Projekt wählen …
</option>
)}
{projektOptionen(konten.data?.konten).map((o) => (
<option key={o.value || 'ohne'} value={o.value}>
{o.label}
</option>
))}
</select>
</Field>
{konten.loading && (
<p className="msg muted" role="status">
Projekte werden geladen …
</p>
)}
{konten.error && (
<p className="msg warn" role="alert">
Die Projekte konnten nicht geladen werden: {konten.error}{' '}
<button type="button" className="link" onClick={konten.reload}>
Erneut versuchen
</button>
</p>
)}
{konten.data && konten.data.konten.length === 0 && <p className="rd-hint">{KEINE_PROJEKTE_HINWEIS}</p>}
</>
)}
{kunde && projektBereit && projekt !== '' && (
<>
{detail.loading && !detail.data && (
<p className="msg muted" role="status">
Projektkonto wird geladen …
</p>
)}
{detail.error && !detail.data && (
<p className="msg warn" role="alert">
Das Projektkonto konnte nicht geladen werden: {detail.error}{' '}
<button type="button" className="link" onClick={detail.reload}>
Erneut versuchen
</button>
</p>
)}
{detail.data && (
<KontoZusammenfassung
zeile={kontoZeile(detail.data.konto, { stripeOk: detail.data.stripeOk })}
hinweis={stripeHinweis(detail.data.stripeOk)}
/>
)}
</>
)}
{kunde && projektBereit && kontoBereit && optionen.length > 1 && (
<fieldset className="rd-group">
<legend>Art der Rechnung</legend>
<div className="rd-stack">
<div className="seg rd-seg" role="group" aria-label="Art der Rechnung">
{optionen.map((o) => (
<button
key={o.key}
type="button"
aria-pressed={o.key === art}
disabled={!o.enabled}
title={o.hint || undefined}
onClick={() => act({ type: 'art', art: o.key })}
>
{o.label}
</button>
))}
</div>
{hinweise.map((h) => (
<p className="rd-hint" key={h}>
{h}
</p>
))}
</div>
</fieldset>
)}
{kunde && projektBereit && kontoBereit && art !== '' && inhalt()}
{kunde && projektBereit && kontoBereit && art !== '' && (
<>
<Field label="Leistungszeitraum (optional)" htmlFor="rd-zeitraum">
<input
id="rd-zeitraum"
className="input"
value={state.zeitraum}
placeholder={ZEITRAUM_PLATZHALTER}
onChange={(ev) => act({ type: 'zeitraum', text: ev.target.value })}
/>
</Field>
<div className="field-row">
<Field label="Zahlungsziel in Tagen" htmlFor="rd-tage">
<input
id="rd-tage"
className="input fin-amount"
inputMode="numeric"
value={state.days}
onChange={(ev) => act({ type: 'tage', wert: ev.target.value })}
/>
</Field>
<Field label="Notiz (erscheint auf der Rechnung)" htmlFor="rd-notiz">
<textarea
id="rd-notiz"
className="textarea"
rows={2}
maxLength={INVOICE_LIMITS.maxMemo}
value={state.memo}
onChange={(ev) => act({ type: 'notiz', wert: ev.target.value })}
/>
</Field>
</div>
</>
)}
</div>
</Dialog>
)
} }
+2 -1
View File
@@ -2,7 +2,7 @@
müssen, tragen zwei Klassen. Task 9 hängt am Ende die Spalten der Rechnungstabelle an. */ müssen, tragen zwei Klassen. Task 9 hängt am Ende die Spalten der Rechnungstabelle an. */
.rd-group { border: 0; margin: 0; padding: 0; min-width: 0; } .rd-group { border: 0; margin: 0; padding: 0; min-width: 0; }
.rd-group > legend { padding: 0; margin: 0 0 6px; font-weight: 500; } .rd-group > legend { padding: 0; margin: 0 0 6px; font-weight: 500; }
.rd-stack { display: grid; gap: 8px; } .rd-stack { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.rd-facts.facts { margin: 0; } .rd-facts.facts { margin: 0; }
.seg.rd-seg { flex-wrap: wrap; max-width: 100%; justify-self: start; } .seg.rd-seg { flex-wrap: wrap; max-width: 100%; justify-self: start; }
.rd-hint { margin: 0; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; } .rd-hint { margin: 0; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; }
@@ -36,6 +36,7 @@
.rd-lines { table-layout: fixed; } .rd-lines { table-layout: fixed; }
.rd-lines .rd-line-text { overflow-wrap: anywhere; } .rd-lines .rd-line-text { overflow-wrap: anywhere; }
.rd-lines .fin-amount { width: 7.5rem; } .rd-lines .fin-amount { width: 7.5rem; }
.rd-lines th.fin-amount { text-align: right; }
.rd-line-input { width: 100%; min-width: 0; } .rd-line-input { width: 100%; min-width: 0; }
.rd-line-tail { display: block; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; } .rd-line-tail { display: block; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; }
.rd-dim { opacity: 0.5; } .rd-dim { opacity: 0.5; }
+7
View File
@@ -80,12 +80,19 @@ export function useRechnungVorschau({ anfrage, schluessel, aktiv }) {
const bodyRef = useRef(null) const bodyRef = useRef(null)
bodyRef.current = anfrage?.ok ? anfrage.body : null bodyRef.current = anfrage?.ok ? anfrage.body : null
const datenRef = useRef(null) const datenRef = useRef(null)
const kontextRef = useRef('')
useEffect(() => { useEffect(() => {
if (!aktiv || !bodyRef.current || !schluessel) { if (!aktiv || !bodyRef.current || !schluessel) {
datenRef.current = null datenRef.current = null
kontextRef.current = ''
return undefined return undefined
} }
// Vorherige Zeilen bleiben nur innerhalb derselben Rechnung (Kunde, Art, Projekt, AB) abgeblendet stehen
const { customerId, art, projektId, auftragId } = bodyRef.current
const kontext = JSON.stringify([customerId, art, projektId, auftragId])
if (kontext !== kontextRef.current) datenRef.current = null
kontextRef.current = kontext
let aktuell = true let aktuell = true
setVorschau({ key: schluessel, status: 'laedt', daten: datenRef.current }) setVorschau({ key: schluessel, status: 'laedt', daten: datenRef.current })
const timer = setTimeout(async () => { const timer = setTimeout(async () => {