Rechnungsdialog: Zustand mit Reducer, Appwrite-Lesezugriffe und Hooks für Projektkonto, Vorschau und weitere Tickets

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:34:22 +00:00
1 parent 0f1dbabd98
commit 6a5f943034
4 files changed
+418

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
// Zustand des Rechnungsdialogs „Rechnung schreiben“ (rein, ohne React; unter Node testbar).
// `anfangszustand` liefert den leeren Zustand, `reduce` verarbeitet die Aktionen des Dialogs. Unbekannte Aktionen,
// unbekannte Listen und Felder sowie Aktionen ohne Wirkung geben denselben Zustand (dieselbe Referenz) zurück.
import { INVOICE_LIMITS } from './financeView'
const LISTEN = ['items', 'zusatz']
const FELDER = ['description', 'quantity', 'price']
const MINDESTENS = { items: 1, zusatz: 0 }
const leereZeile = (uid) => ({ uid, description: '', quantity: '1', price: '' })
/** Startzustand. projektId: null = noch zu wählen, '' = Ohne Projekt, Text = dieses Projekt. */
export function anfangszustand({ projektId = null } = {}) {
return {
projektId,
art: '',
auftragId: '',
items: [leereZeile(1)],
zusatz: [],
abgewaehlt: [],
weitereTickets: [],
anzahlung: {},
zeilentexte: {},
zeitraum: '',
zeitraumGeaendert: false,
days: String(INVOICE_LIMITS.defaultDays),
memo: '',
}
}
function neueUid(s) {
return 1 + Math.max(0, ...s.items.map((z) => z.uid), ...s.zusatz.map((z) => z.uid))
}
export function reduce(s, a) {
switch (a?.type) {
case 'projekt':
return { ...anfangszustand({ projektId: a.projektId }), days: s.days, memo: s.memo }
case 'art':
return {
...s,
art: a.art,
auftragId: '',
zeilentexte: {},
zeitraum: s.zeitraumGeaendert ? s.zeitraum : '',
}
case 'auftrag':
return { ...s, auftragId: a.auftragId, anzahlung: {} }
case 'position': {
if (!LISTEN.includes(a.liste) || !FELDER.includes(a.feld)) return s
return { ...s, [a.liste]: s[a.liste].map((z) => (z.uid === a.uid ? { ...z, [a.feld]: a.wert } : z)) }
}
case 'position+': {
if (!LISTEN.includes(a.liste) || s[a.liste].length >= INVOICE_LIMITS.maxItems) return s
return { ...s, [a.liste]: [...s[a.liste], leereZeile(neueUid(s))] }
}
case 'position-': {
if (!LISTEN.includes(a.liste) || s[a.liste].length <= MINDESTENS[a.liste]) return s
return { ...s, [a.liste]: s[a.liste].filter((z) => z.uid !== a.uid) }
}
case 'blatt':
return { ...s, abgewaehlt: s.abgewaehlt.includes(a.id) ? s.abgewaehlt.filter((id) => id !== a.id) : [...s.abgewaehlt, a.id] }
case 'ticket+':
return s.weitereTickets.some((t) => t.ticketId === a.ticket.ticketId) ? s : { ...s, weitereTickets: [...s.weitereTickets, a.ticket] }
case 'ticket-': {
const weg = Array.isArray(a.blattIds) ? a.blattIds : []
return {
...s,
weitereTickets: s.weitereTickets.filter((t) => t.ticketId !== a.ticketId),
abgewaehlt: s.abgewaehlt.filter((id) => !weg.includes(id)),
}
}
case 'anzahlung':
return { ...s, anzahlung: { ...s.anzahlung, ...a.patch } }
case 'zeilentext': {
const zeilentexte = { ...s.zeilentexte }
if (!String(a.text ?? '').trim() || a.text === a.original) delete zeilentexte[a.schluessel]
else zeilentexte[a.schluessel] = a.text
return { ...s, zeilentexte }
}
case 'zeitraum':
return { ...s, zeitraum: a.text, zeitraumGeaendert: true }
case 'zeitraum-vorschlag':
if (s.zeitraumGeaendert || s.zeitraum === a.text) return s
return { ...s, zeitraum: a.text }
case 'tage':
return { ...s, days: a.wert }
case 'notiz':
return { ...s, memo: a.wert }
default:
return s
}
}