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>
99 lines
3.7 KiB
JavaScript
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
|
|
}
|
|
}
|