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:
1 parent
0f1dbabd98
commit
6a5f943034
4 files changed
+418
No files matched your search
@@ -0,0 +1,189 @@
|
||||
// Hooks des Rechnungsdialogs: Projektliste, Projektkonto, Vorschau, Tickets des Kunden und Arbeitsblätter weiterer Tickets.
|
||||
// Überall zählt nur die jüngste Antwort; nach dem Ausbau eines Hooks zählt keine mehr.
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getProjektkonten, getProjektkonto, previewInvoice } from '../lib/financeApi'
|
||||
import { getAbgerechnet } from '../lib/teamApi'
|
||||
import { listKundenTickets, listTicketBlaetter } from '../lib/rechnungDialogApi'
|
||||
import { blattAusAppwrite } from '../lib/rechnungDialogView'
|
||||
import { createRequestGuard } from '../lib/requestGuard'
|
||||
|
||||
const VORSCHAU_WARTEZEIT_MS = 350
|
||||
|
||||
/**
|
||||
* Lädt eine Ressource zu einem Schlüssel (Kunde oder Projekt). Ohne Schlüssel wird nichts geladen. Die Daten gehören zu
|
||||
* ihrem Schlüssel: nach einem Wechsel ist sofort `data: null` und `loading: true`. `reload()` behält die Daten stehen und
|
||||
* liefert ein Promise, das nie wirft.
|
||||
*/
|
||||
function useGeladen(laden, schluessel, fallbackError) {
|
||||
const [state, setState] = useState({ key: null, data: null, loading: Boolean(schluessel), error: '' })
|
||||
const guardRef = useRef(null)
|
||||
if (!guardRef.current) guardRef.current = createRequestGuard()
|
||||
const ladenRef = useRef(laden)
|
||||
ladenRef.current = laden
|
||||
const lebtRef = useRef(true)
|
||||
|
||||
const lade = useCallback(
|
||||
async (key) => {
|
||||
const guard = guardRef.current
|
||||
const id = guard.next()
|
||||
setState((s) => (s.key === key ? { ...s, loading: true, error: '' } : { key, data: null, loading: true, error: '' }))
|
||||
const gilt = () => lebtRef.current && guard.isCurrent(id)
|
||||
try {
|
||||
const data = await ladenRef.current()
|
||||
if (gilt()) setState({ key, data: data && typeof data === 'object' ? data : {}, loading: false, error: '' })
|
||||
} catch (err) {
|
||||
if (gilt()) setState((s) => ({ ...s, key, loading: false, error: err?.message || fallbackError }))
|
||||
}
|
||||
},
|
||||
[fallbackError]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
lebtRef.current = true
|
||||
if (!schluessel) {
|
||||
guardRef.current.next()
|
||||
setState({ key: null, data: null, loading: false, error: '' })
|
||||
} else {
|
||||
lade(schluessel)
|
||||
}
|
||||
return () => {
|
||||
lebtRef.current = false
|
||||
}
|
||||
}, [schluessel, lade])
|
||||
|
||||
const reload = useCallback(() => (schluessel ? lade(schluessel) : Promise.resolve()), [schluessel, lade])
|
||||
const passt = Boolean(schluessel) && state.key === schluessel
|
||||
return {
|
||||
data: passt ? state.data : null,
|
||||
loading: Boolean(schluessel) && (!passt || state.loading),
|
||||
error: passt ? state.error : '',
|
||||
reload,
|
||||
}
|
||||
}
|
||||
|
||||
/** Projektkonten eines Kunden: { auftraegeAktiv, kunde, konten, ohneProjekt, stripeOk }. */
|
||||
export function useProjektkonten(kundeId) {
|
||||
return useGeladen(() => getProjektkonten(kundeId), kundeId, 'Die Projekte konnten nicht geladen werden.')
|
||||
}
|
||||
|
||||
/** Detail eines Projektkontos: { konto, auftraege, rechnungen, offeneArbeitsblaetter, stripeOk }. */
|
||||
export function useProjektkontoDetail(projektId) {
|
||||
return useGeladen(() => getProjektkonto(projektId), projektId, 'Das Projektkonto konnte nicht geladen werden.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorschau der Rechnung zum Körper der Anfrage. `vorschau = { key, status: 'laedt' | 'ok' | 'fehler', daten?, message?, httpStatus? }`.
|
||||
* Der Körper wird über einen Ref gelesen, der Effekt hängt nur an [aktiv, schluessel]; nach 350 ms Ruhe geht die Anfrage raus.
|
||||
*/
|
||||
export function useRechnungVorschau({ anfrage, schluessel, aktiv }) {
|
||||
const [vorschau, setVorschau] = useState(null)
|
||||
const bodyRef = useRef(null)
|
||||
bodyRef.current = anfrage?.ok ? anfrage.body : null
|
||||
const datenRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!aktiv || !bodyRef.current || !schluessel) {
|
||||
datenRef.current = null
|
||||
return undefined
|
||||
}
|
||||
let aktuell = true
|
||||
setVorschau({ key: schluessel, status: 'laedt', daten: datenRef.current })
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const daten = await previewInvoice(bodyRef.current)
|
||||
if (!aktuell) return
|
||||
datenRef.current = daten
|
||||
setVorschau({ key: schluessel, status: 'ok', daten })
|
||||
} catch (err) {
|
||||
if (!aktuell) return
|
||||
setVorschau({
|
||||
key: schluessel,
|
||||
status: 'fehler',
|
||||
message: err?.message || 'Die Vorschau konnte nicht geladen werden.',
|
||||
httpStatus: err?.status,
|
||||
daten: datenRef.current,
|
||||
})
|
||||
}
|
||||
}, VORSCHAU_WARTEZEIT_MS)
|
||||
return () => {
|
||||
aktuell = false
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [aktiv, schluessel])
|
||||
|
||||
return vorschau
|
||||
}
|
||||
|
||||
/** Tickets des Kunden (einmal je Kunde, sobald `aktiv` wahr ist). */
|
||||
export function useKundenTickets(kundeId, aktiv) {
|
||||
const [state, setState] = useState({ key: null, tickets: [], loading: false, error: '' })
|
||||
useEffect(() => {
|
||||
if (!aktiv || !kundeId) return undefined
|
||||
let aktuell = true
|
||||
setState({ key: kundeId, tickets: [], loading: true, error: '' })
|
||||
listKundenTickets(kundeId).then(
|
||||
(tickets) => aktuell && setState({ key: kundeId, tickets, loading: false, error: '' }),
|
||||
() => aktuell && setState({ key: kundeId, tickets: [], loading: false, error: 'Die Tickets konnten nicht geladen werden.' })
|
||||
)
|
||||
return () => {
|
||||
aktuell = false
|
||||
}
|
||||
}, [kundeId, aktiv])
|
||||
const passt = state.key === kundeId
|
||||
return { tickets: passt ? state.tickets : [], loading: Boolean(aktiv && kundeId) && (!passt || state.loading), error: passt ? state.error : '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbeitsblätter weiterer Tickets: je Ticket einmal Blätter und Abgerechnet-Liste laden.
|
||||
* Ergebnis: { [ticketId]: { status: 'laedt' | 'ok' | 'fehler', blaetter, message } }; entfernte Tickets verschwinden.
|
||||
*/
|
||||
export function useWeitereBlaetter(weitereTickets) {
|
||||
const [ergebnis, setErgebnis] = useState({})
|
||||
const angefragtRef = useRef(new Set())
|
||||
const lebtRef = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
lebtRef.current = true
|
||||
return () => {
|
||||
lebtRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const liste = Array.isArray(weitereTickets) ? weitereTickets : []
|
||||
const ids = new Set(liste.map((t) => t.ticketId))
|
||||
for (const id of [...angefragtRef.current]) if (!ids.has(id)) angefragtRef.current.delete(id)
|
||||
for (const ticket of liste) {
|
||||
if (angefragtRef.current.has(ticket.ticketId)) continue
|
||||
angefragtRef.current.add(ticket.ticketId)
|
||||
const gilt = () => lebtRef.current && angefragtRef.current.has(ticket.ticketId)
|
||||
setErgebnis((r) => ({ ...r, [ticket.ticketId]: { status: 'laedt', blaetter: [], message: '' } }))
|
||||
Promise.all([listTicketBlaetter(ticket.woid), getAbgerechnet(ticket.ticketId)]).then(
|
||||
([dokumente, antwort]) => {
|
||||
if (!gilt()) return
|
||||
const blaetter = dokumente
|
||||
.map((ws) => blattAusAppwrite(ws, antwort?.abgerechnet))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => String(a.datum).localeCompare(String(b.datum)) || String(a.id).localeCompare(String(b.id)))
|
||||
setErgebnis((r) => ({ ...r, [ticket.ticketId]: { status: 'ok', blaetter, message: '' } }))
|
||||
},
|
||||
() => {
|
||||
if (!gilt()) return
|
||||
setErgebnis((r) => ({
|
||||
...r,
|
||||
[ticket.ticketId]: { status: 'fehler', blaetter: [], message: `Die Arbeitsblätter von #${ticket.woid} konnten nicht geladen werden.` },
|
||||
}))
|
||||
}
|
||||
)
|
||||
}
|
||||
setErgebnis((r) => {
|
||||
const keys = Object.keys(r).filter((k) => !ids.has(k))
|
||||
if (keys.length === 0) return r
|
||||
const rest = { ...r }
|
||||
for (const k of keys) delete rest[k]
|
||||
return rest
|
||||
})
|
||||
}, [weitereTickets])
|
||||
|
||||
return ergebnis
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from './appwrite'
|
||||
|
||||
/** Tickets eines Kunden, neueste zuerst (für „Weiteres Ticket hinzunehmen“). */
|
||||
export async function listKundenTickets(customerId) {
|
||||
const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [
|
||||
Query.equal('customerId', customerId),
|
||||
Query.orderDesc('$createdAt'),
|
||||
Query.limit(100),
|
||||
])
|
||||
return res.documents
|
||||
}
|
||||
|
||||
/** Alle Arbeitsblätter eines Tickets (nach Ticketnummer `woid`). */
|
||||
export async function listTicketBlaetter(woid) {
|
||||
const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKSHEETS, [
|
||||
Query.equal('woid', woid),
|
||||
Query.orderAsc('$createdAt'),
|
||||
Query.limit(5000),
|
||||
])
|
||||
return res.documents
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { anfangszustand, reduce } = await loadModule('src/lib/rechnungDialogState.js')
|
||||
|
||||
test('anfangszustand: nichts gewählt, eine leere Position, Zahlungsziel 14', () => {
|
||||
assert.deepEqual(anfangszustand({ projektId: 'p1' }), {
|
||||
projektId: 'p1', art: '', auftragId: '',
|
||||
items: [{ uid: 1, description: '', quantity: '1', price: '' }], zusatz: [],
|
||||
abgewaehlt: [], weitereTickets: [], anzahlung: {}, zeilentexte: {},
|
||||
zeitraum: '', zeitraumGeaendert: false, days: '14', memo: '',
|
||||
})
|
||||
assert.equal(anfangszustand().projektId, null)
|
||||
assert.equal(anfangszustand({ projektId: '' }).projektId, '')
|
||||
})
|
||||
|
||||
test('projekt: setzt alles zurück, behält aber Zahlungsziel und Notiz', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
s = reduce(s, { type: 'tage', wert: '30' })
|
||||
s = reduce(s, { type: 'notiz', wert: 'Danke' })
|
||||
s = reduce(s, { type: 'art', art: 'aufwand' })
|
||||
s = reduce(s, { type: 'blatt', id: 'b1' })
|
||||
s = reduce(s, { type: 'zeilentext', schluessel: '2026-10-06', text: 'Neu', original: 'Alt' })
|
||||
s = reduce(s, { type: 'projekt', projektId: 'p2' })
|
||||
assert.equal(s.projektId, 'p2')
|
||||
assert.equal(s.art, '')
|
||||
assert.deepEqual(s.abgewaehlt, [])
|
||||
assert.deepEqual(s.zeilentexte, {})
|
||||
assert.equal(s.days, '30')
|
||||
assert.equal(s.memo, 'Danke')
|
||||
assert.equal(reduce(s, { type: 'projekt', projektId: null }).projektId, null)
|
||||
})
|
||||
|
||||
test('art: Positionen für Festbetrag und Zusatzpositionen bleiben getrennt und erhalten', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
s = reduce(s, { type: 'position', liste: 'items', uid: 1, feld: 'description', wert: 'Pauschale' })
|
||||
s = reduce(s, { type: 'position+', liste: 'zusatz' })
|
||||
s = reduce(s, { type: 'position', liste: 'zusatz', uid: 2, feld: 'price', wert: '12,00' })
|
||||
s = reduce(s, { type: 'art', art: 'aufwand' })
|
||||
assert.equal(s.art, 'aufwand')
|
||||
assert.equal(s.items[0].description, 'Pauschale')
|
||||
assert.deepEqual(s.zusatz, [{ uid: 2, description: '', quantity: '1', price: '12,00' }])
|
||||
// eine unbekannte Liste oder ein unbekanntes Feld ändert nichts
|
||||
assert.equal(reduce(s, { type: 'position', liste: 'x', uid: 1, feld: 'description', wert: 'y' }), s)
|
||||
assert.equal(reduce(s, { type: 'position', liste: 'items', uid: 1, feld: 'uid', wert: 9 }), s)
|
||||
})
|
||||
|
||||
test('position+ und position-: Positionen mindestens 1, Zusatzpositionen mindestens 0, höchstens 20', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
assert.equal(reduce(s, { type: 'position-', liste: 'items', uid: 1 }), s)
|
||||
s = reduce(s, { type: 'position+', liste: 'zusatz' })
|
||||
assert.equal(s.zusatz[0].uid, 2)
|
||||
s = reduce(s, { type: 'position-', liste: 'zusatz', uid: 2 })
|
||||
assert.deepEqual(s.zusatz, [])
|
||||
for (let i = 0; i < 25; i += 1) s = reduce(s, { type: 'position+', liste: 'zusatz' })
|
||||
assert.equal(s.zusatz.length, 20)
|
||||
})
|
||||
|
||||
test('blatt: Abwählen und Wiederanwählen', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
s = reduce(s, { type: 'blatt', id: 'b2' })
|
||||
assert.deepEqual(s.abgewaehlt, ['b2'])
|
||||
s = reduce(s, { type: 'blatt', id: 'b2' })
|
||||
assert.deepEqual(s.abgewaehlt, [])
|
||||
})
|
||||
|
||||
test('ticket+ und ticket-: ein Ticket nur einmal, beim Entfernen verschwindet die Abwahl seiner Blätter', () => {
|
||||
const ticket = { ticketId: 't2', woid: '100140', titel: 'Kontaktformular' }
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
s = reduce(s, { type: 'ticket+', ticket })
|
||||
s = reduce(s, { type: 'ticket+', ticket })
|
||||
assert.deepEqual(s.weitereTickets, [ticket])
|
||||
s = reduce(s, { type: 'blatt', id: 'w1' })
|
||||
s = reduce(s, { type: 'blatt', id: 'b9' })
|
||||
s = reduce(s, { type: 'ticket-', ticketId: 't2', blattIds: ['w1'] })
|
||||
assert.deepEqual(s.weitereTickets, [])
|
||||
assert.deepEqual(s.abgewaehlt, ['b9'])
|
||||
})
|
||||
|
||||
test('zeitraum-vorschlag gilt nur, solange der Nutzer das Feld nicht selbst geändert hat', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
s = reduce(s, { type: 'zeitraum-vorschlag', text: '01.10.–20.10.2026' })
|
||||
assert.deepEqual([s.zeitraum, s.zeitraumGeaendert], ['01.10.–20.10.2026', false])
|
||||
assert.equal(reduce(s, { type: 'zeitraum-vorschlag', text: '01.10.–20.10.2026' }), s) // gleicher Text: kein neuer Zustand
|
||||
s = reduce(s, { type: 'zeitraum', text: 'Oktober 2026' })
|
||||
assert.deepEqual([s.zeitraum, s.zeitraumGeaendert], ['Oktober 2026', true])
|
||||
assert.equal(reduce(s, { type: 'zeitraum-vorschlag', text: '02.10.–21.10.2026' }).zeitraum, 'Oktober 2026')
|
||||
// ein Wechsel der Art verwirft einen Vorschlag, nicht aber die Eingabe des Nutzers
|
||||
const vorschlag = reduce(anfangszustand({ projektId: 'p1' }), { type: 'zeitraum-vorschlag', text: '01.10.–20.10.2026' })
|
||||
assert.equal(reduce(vorschlag, { type: 'art', art: 'festbetrag' }).zeitraum, '')
|
||||
assert.equal(reduce(s, { type: 'art', art: 'festbetrag' }).zeitraum, 'Oktober 2026')
|
||||
})
|
||||
|
||||
test('zeilentext: ein unveränderter oder leerer Text entfernt den Eintrag', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
const setze = (text) => reduce(s, { type: 'zeilentext', schluessel: '2026-10-06', text, original: 'Alt' })
|
||||
s = setze('Neu')
|
||||
assert.deepEqual(s.zeilentexte, { '2026-10-06': 'Neu' })
|
||||
s = setze('Alt')
|
||||
assert.deepEqual(s.zeilentexte, {})
|
||||
s = setze('Neu')
|
||||
s = setze(' ')
|
||||
assert.deepEqual(s.zeilentexte, {})
|
||||
})
|
||||
|
||||
test('anzahlung und auftrag: Eingaben ergänzen sich, eine andere AB setzt sie zurück', () => {
|
||||
let s = anfangszustand({ projektId: 'p1' })
|
||||
s = reduce(s, { type: 'anzahlung', patch: { modus: 'betrag' } })
|
||||
s = reduce(s, { type: 'anzahlung', patch: { wert: '400,00' } })
|
||||
assert.deepEqual(s.anzahlung, { modus: 'betrag', wert: '400,00' })
|
||||
s = reduce(s, { type: 'auftrag', auftragId: 'a2' })
|
||||
assert.deepEqual([s.auftragId, s.anzahlung], ['a2', {}])
|
||||
assert.equal(reduce(s, { type: 'gibtsnicht' }), s)
|
||||
})
|
||||
Reference in new issue
Block a user