// 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), tageGeaendert: false, 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, tageGeaendert: s.tageGeaendert, 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, tageGeaendert: true } case 'tage-vorschlag': // das Zahlungsziel der gewählten AB; eine Eingabe des Nutzers überschreibt er nie if (s.tageGeaendert || s.days === a.wert) return s return { ...s, days: a.wert } case 'notiz': return { ...s, memo: a.wert } default: return s } }