Files
tickte-system/src/lib/rechnungDialogState.js
T
ClaudeandClaude Sonnet 5.5 dec7bea7d2 Kontrolle: Rechnungsdialog übernimmt das Zahlungsziel der gewählten AB
Bei Anzahlung, Schlussrechnung und Aufwand wird 'Zahlungsziel in Tagen' mit
auftrag.zahlungszielTage vorbelegt (sonst 14); eine Eingabe des Nutzers bleibt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:17:08 +00:00

99 lines
3.7 KiB
JavaScript

// 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
}
}