diff --git a/src/lib/rechnungDialogView.js b/src/lib/rechnungDialogView.js new file mode 100644 index 0000000..52e4f8a --- /dev/null +++ b/src/lib/rechnungDialogView.js @@ -0,0 +1,204 @@ +// Dialoglogik der Rechnung „Rechnung schreiben“ (rein, ohne React und Appwrite; unter Node testbar). +// Aufgaben: Projektwahl, waehlbare Rechnungsarten, gültige Auftragsbestätigungen (AB), Vorgaben für Anzahlung und Standardart, +// Arbeitsblätter (Auswahl, Summen, Umwandlung aus Appwrite-Dokumenten) und die Ticketliste für „Weiteres Ticket hinzunehmen“. +import { euro, dateDe, ymdBerlin } from './financeFormat' +import { worksheetMinutes } from './support' +import { germanDateToIso } from './ticketForm' +import { ticketsText } from './projektkontoView' + +export const ART_OPTION_LABEL = { + frei: 'Freie Positionen', + anzahlung: 'Anzahlung', + schluss: 'Schlussrechnung', + aufwand: 'Nach Aufwand', + festbetrag: 'Festbetrag', +} +export const AB_HINWEIS = 'Anzahlung und Schlussrechnung brauchen eine Auftragsbestätigung.' +export const ENTWURF_HINWEIS = 'Zu dieser Auftragsbestätigung gibt es noch einen Entwurf. Erst senden oder löschen.' +export const OHNE_PROJEKT = 'Ohne Projekt' + +const GUELTIG = ['versendet', 'bestaetigt'] +const FINALISIERT = ['offen', 'ueberfaellig', 'bezahlt', 'uneinbringlich'] + +// --- Projektwahl -------------------------------------------------------------------------------------------- + +/** Vorauswahl der Aufrufer: undefined = keine (null), null = „Ohne Projekt“ (''), Text = dieses Projekt. */ +export function startProjekt(propProjektId) { + if (propProjektId === undefined) return null + if (propProjektId === null) return '' + return String(propProjektId) +} + +/** Auswahlliste: Projekte mit Ticketnummer, am Ende „Ohne Projekt“. */ +export function projektOptionen(konten) { + const liste = Array.isArray(konten) ? konten : [] + const optionen = liste + .filter((k) => k && String(k.projektId ?? '') !== '') + .map((k) => { + const name = String(k.name ?? '').trim() || 'Projekt' + const tickets = ticketsText(k.tickets) + return { value: String(k.projektId), label: tickets ? `${name} · ${tickets}` : name } + }) + optionen.push({ value: '', label: OHNE_PROJEKT }) + return optionen +} + +/** null = noch zu wählen, '' = Ohne Projekt. Ein Kunde ohne Projekte hat nur „Ohne Projekt“. */ +export function wirksamesProjekt(wahl, konten) { + if (wahl === '') return '' + if (typeof wahl === 'string' && wahl) { + if (!Array.isArray(konten)) return wahl + return konten.some((k) => k?.projektId === wahl) ? wahl : null + } + return Array.isArray(konten) && konten.length === 0 ? '' : null +} + +// --- Auftragsbestätigungen ---------------------------------------------------------------------------------- + +function neuesteZuerst(liste) { + const zeit = (a) => { + const t = Date.parse(a?.gesendetAm) + return Number.isFinite(t) ? t : 0 + } + return [...liste].sort((a, b) => zeit(b) - zeit(a) || String(b?.nummer ?? '').localeCompare(String(a?.nummer ?? ''))) +} + +function gueltige(auftraege, art) { + if (!Array.isArray(auftraege)) return [] + return neuesteZuerst(auftraege.filter((a) => a && a.art === art && GUELTIG.includes(a.status))) +} + +/** Gültige Festpreis-ABs (versendet oder bestätigt), neueste zuerst. */ +export function gueltigeFestpreisAbs(auftraege) { + return gueltige(auftraege, 'festpreis') +} + +/** Jüngste gültige Aufwands-AB oder null. */ +export function aufwandAuftrag(auftraege) { + return gueltige(auftraege, 'aufwand')[0] ?? null +} + +/** 'AB-2026-0002 vom 06.10.2026 · 800,00 €' bzw. '… · nach Aufwand'. */ +export function abLabel(auftrag) { + let text = String(auftrag?.nummer ?? '') + const datum = dateDe(auftrag?.gesendetAm) + if (datum !== '–') text += ` vom ${datum}` + text += auftrag?.art === 'aufwand' ? ' · nach Aufwand' : ` · ${euro(auftrag?.summeCents)}` + return text +} + +/** Summe der finalisierten Rechnungen zu dieser AB (Cent). */ +export function berechnetFuerAuftrag(rechnungen, auftragId) { + if (!Array.isArray(rechnungen)) return 0 + return rechnungen + .filter((r) => r && r.auftragId === auftragId && FINALISIERT.includes(r.status)) + .reduce((summe, r) => summe + (Number(r.totalCents) || 0), 0) +} + +/** Gibt es zu dieser AB einen noch nicht gesendeten Entwurf? */ +export function hatEntwurfZurAuftrag(rechnungen, auftragId) { + return Array.isArray(rechnungen) && rechnungen.some((r) => r && r.auftragId === auftragId && r.status === 'entwurf') +} + +// --- Rechnungsarten ----------------------------------------------------------------------------------------- + +/** Wählbare Arten mit Sperrgrund: [{ key, label, enabled, hint }]. */ +export function artOptionen({ projektId, auftraege, rechnungen, auftraegeAktiv }) { + if (projektId === null || projektId === undefined) return [] + const option = (key, enabled, hint = '') => ({ key, label: ART_OPTION_LABEL[key], enabled, hint: enabled ? '' : hint }) + if (projektId === '') return [option('frei', true)] + const abs = auftraegeAktiv ? gueltigeFestpreisAbs(auftraege) : [] + const freie = abs.filter((a) => !hatEntwurfZurAuftrag(rechnungen, a.id)) + const schlussHinweis = abs.length === 0 ? AB_HINWEIS : ENTWURF_HINWEIS + return [ + option('anzahlung', abs.length > 0, AB_HINWEIS), + option('schluss', freie.length > 0, schlussHinweis), + option('aufwand', true), + option('festbetrag', true), + ] +} + +/** Vorbelegte Art: Anzahlung, Schlussrechnung, Aufwand oder Festbetrag je nach Lage ('' ohne Wahl). */ +export function standardArt(optionen, { auftraege, rechnungen, offeneMinuten }) { + const liste = Array.isArray(optionen) ? optionen : [] + if (liste.length === 0) return '' + if (liste.length === 1 && liste[0].key === 'frei') return 'frei' + const waehlbar = (key) => liste.some((o) => o.key === key && o.enabled) + const jung = gueltigeFestpreisAbs(auftraege)[0] + if ( + waehlbar('anzahlung') && + jung?.zahlungsplan?.art === 'anzahlung' && + berechnetFuerAuftrag(rechnungen, jung.id) === 0 && + !hatEntwurfZurAuftrag(rechnungen, jung.id) + ) { + return 'anzahlung' + } + if (waehlbar('schluss')) return 'schluss' + return Number(offeneMinuten) > 0 ? 'aufwand' : 'festbetrag' +} + +/** Vorbelegte AB: jüngste passende; bei Schlussrechnung ohne offenen Entwurf; sonst ''. */ +export function standardAuftrag(art, auftraege, rechnungen = []) { + const abs = gueltigeFestpreisAbs(auftraege) + if (art === 'anzahlung') return abs[0]?.id ?? '' + if (art === 'schluss') return abs.find((a) => !hatEntwurfZurAuftrag(rechnungen, a.id))?.id ?? '' + return '' +} + +/** Vorgabe der Anzahlung: Prozent aus dem Zahlungsplan, sonst 50. */ +export function anzahlungStandard(auftrag) { + const plan = auftrag?.zahlungsplan + const prozent = plan?.art === 'anzahlung' && Number(plan.prozent) > 0 ? Number(plan.prozent) : 50 + return { modus: 'prozent', wert: String(prozent).replace('.', ','), bezeichnung: 'Anzahlung' } +} + +// --- Arbeitsblätter ----------------------------------------------------------------------------------------- + +/** Kennungen der gewählten Blätter (alle außer den abgewählten) in Reihenfolge der Liste. */ +export function ausgewaehlteIds(blaetter, abgewaehlt) { + if (!Array.isArray(blaetter)) return [] + const weg = Array.isArray(abgewaehlt) ? abgewaehlt : [] + return blaetter.map((b) => b.id).filter((id) => !weg.includes(id)) +} + +/** Anzahl und Minutensumme der Blätter mit diesen Kennungen. */ +export function blaetterSumme(blaetter, ids) { + const liste = (Array.isArray(blaetter) ? blaetter : []).filter((b) => Array.isArray(ids) && ids.includes(b.id)) + return { anzahl: liste.length, minuten: liste.reduce((s, b) => s + (Number(b.minuten) || 0), 0) } +} + +/** Appwrite-Arbeitsblatt -> Blatt der Portal-Form; null, wenn es nicht abrechenbar ist oder schon abgerechnet wurde. */ +export function blattAusAppwrite(ws, abgerechnet) { + if (!ws || !ws.$id) return null + if (ws.isComment || ws.countsToPlan) return null + if (abgerechnet && Object.prototype.hasOwnProperty.call(abgerechnet, ws.$id)) return null + const roh = worksheetMinutes(ws) + if (!(roh > 0)) return null + const zeile = String(ws.details ?? '') + .split('\n') + .map((z) => z.trim()) + .find(Boolean) + return { + id: ws.$id, + ticketId: ws.workorderId, + woid: ws.woid, + datum: germanDateToIso(ws.startDate || ws.endDate) || ymdBerlin(ws.$createdAt) || '', + minuten: Math.ceil(roh / 15) * 15, + text: (zeile ?? '').slice(0, 120), + aboZeit: false, + kommentar: false, + } +} + +/** Tickets des Kunden mit Nummer, ohne die schon gewählten. */ +export function ticketOptionen(tickets, { ausgeschlossen = [] } = {}) { + if (!Array.isArray(tickets)) return [] + return tickets + .filter((t) => t && t.$id && t.woid && !ausgeschlossen.includes(t.$id)) + .map((t) => ({ + value: t.$id, + ticketId: t.$id, + woid: t.woid, + label: `#${t.woid} · ${String(t.topic || t.title || '').trim() || 'Kein Betreff'}`, + })) +} diff --git a/tests/unit/rechnungDialogView.test.mjs b/tests/unit/rechnungDialogView.test.mjs new file mode 100644 index 0000000..3a3c532 --- /dev/null +++ b/tests/unit/rechnungDialogView.test.mjs @@ -0,0 +1,217 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { + AB_HINWEIS, ENTWURF_HINWEIS, + startProjekt, projektOptionen, wirksamesProjekt, + gueltigeFestpreisAbs, aufwandAuftrag, abLabel, berechnetFuerAuftrag, hatEntwurfZurAuftrag, + artOptionen, standardArt, standardAuftrag, anzahlungStandard, + ausgewaehlteIds, blaetterSumme, blattAusAppwrite, ticketOptionen, +} = await loadModule('src/lib/rechnungDialogView.js') + +const ab = (over = {}) => ({ + id: 'a1', nummer: 'AB-2026-0002', status: 'bestaetigt', art: 'festpreis', summeCents: 80000, + gesendetAm: '2026-10-06T08:15:00.000Z', zahlungsplan: { art: 'anzahlung', prozent: 50, text: '' }, stundensatzCents: null, ...over, +}) +const rk = (over = {}) => ({ + id: 'in_1', number: 'WEBKLAR-0007', status: 'offen', createdDate: '2026-10-08', totalCents: 40000, paidCents: 0, art: 'anzahlung', auftragId: 'a1', ...over, +}) +const bl = (id, minuten, over = {}) => ({ + id, ticketId: 't1', woid: '100131', datum: '2026-10-06', minuten, text: 'Startseite', aboZeit: false, kommentar: false, ...over, +}) +const KONTEN = [ + { projektId: 'p1', name: 'Website Relaunch', tickets: [{ id: 't1', woid: '100131' }] }, + { projektId: 'p2', name: 'Shop', tickets: [] }, +] +const optionen = (args) => artOptionen({ projektId: 'p1', auftraege: [], rechnungen: [], auftraegeAktiv: true, ...args }) +const zustand = (o) => o.map((x) => [x.key, x.enabled]) + +test('startProjekt: Vorauswahl der Aufrufer (undefined = keine, null = Ohne Projekt, Text = Projekt)', () => { + assert.equal(startProjekt(undefined), null) + assert.equal(startProjekt(null), '') + assert.equal(startProjekt('p1'), 'p1') +}) + +test('projektOptionen: Projekte mit Ticketnummer, am Ende „Ohne Projekt“', () => { + assert.deepEqual(projektOptionen(KONTEN), [ + { value: 'p1', label: 'Website Relaunch · #100131' }, + { value: 'p2', label: 'Shop' }, + { value: '', label: 'Ohne Projekt' }, + ]) + assert.deepEqual(projektOptionen([]), [{ value: '', label: 'Ohne Projekt' }]) + assert.deepEqual(projektOptionen(undefined), [{ value: '', label: 'Ohne Projekt' }]) +}) + +test('wirksamesProjekt: null = noch zu wählen, „“ = Ohne Projekt, Kunde ohne Projekte = Ohne Projekt', () => { + assert.equal(wirksamesProjekt('', KONTEN), '') + assert.equal(wirksamesProjekt('p2', KONTEN), 'p2') + assert.equal(wirksamesProjekt('gibtsnicht', KONTEN), null) + assert.equal(wirksamesProjekt('p1', undefined), 'p1') // Liste noch nicht geladen: die Vorauswahl bleibt + assert.equal(wirksamesProjekt(null, KONTEN), null) + assert.equal(wirksamesProjekt(null, []), '') // Kunde ohne Projekte: gleich „Ohne Projekt“ + assert.equal(wirksamesProjekt(null, undefined), null) +}) + +test('gueltigeFestpreisAbs: nur Festpreis mit Status versendet oder bestätigt, neueste zuerst', () => { + const list = [ + ab({ id: 'a1' }), + ab({ id: 'a2', status: 'entwurf' }), + ab({ id: 'a3', art: 'aufwand' }), + ab({ id: 'a4', status: 'abgerechnet' }), + ab({ id: 'a5', status: 'versendet', gesendetAm: '2026-10-07T08:00:00.000Z' }), + ab({ id: 'a6', status: 'storniert' }), + ] + assert.deepEqual(gueltigeFestpreisAbs(list).map((a) => a.id), ['a5', 'a1']) + assert.deepEqual(gueltigeFestpreisAbs(undefined), []) +}) + +test('aufwandAuftrag: jüngste gültige Aufwands-AB', () => { + const list = [ + ab({ id: 'x1', art: 'aufwand', status: 'bestaetigt', gesendetAm: '2026-10-01T08:00:00.000Z' }), + ab({ id: 'x2', art: 'aufwand', status: 'versendet', gesendetAm: '2026-10-05T08:00:00.000Z' }), + ab({ id: 'x3', art: 'aufwand', status: 'storniert', gesendetAm: '2026-10-09T08:00:00.000Z' }), + ab({ id: 'a1' }), + ] + assert.equal(aufwandAuftrag(list).id, 'x2') + assert.equal(aufwandAuftrag([ab()]), null) + assert.equal(aufwandAuftrag(undefined), null) +}) + +test('abLabel: Nummer, Datum des Sendens und Summe bzw. „nach Aufwand“', () => { + assert.equal(abLabel(ab()), 'AB-2026-0002 vom 06.10.2026 · 800,00 €') + assert.equal(abLabel(ab({ art: 'aufwand', summeCents: null })), 'AB-2026-0002 vom 06.10.2026 · nach Aufwand') +}) + +test('berechnetFuerAuftrag und hatEntwurfZurAuftrag: nur finalisierte zählen, ein Entwurf blockiert', () => { + const list = [ + rk(), + rk({ id: 'in_2', status: 'bezahlt', totalCents: 10000 }), + rk({ id: 'in_3', status: 'entwurf', totalCents: 5000 }), + rk({ id: 'in_4', status: 'storniert', totalCents: 7000 }), + rk({ id: 'in_5', auftragId: 'a2', totalCents: 9000 }), + ] + assert.equal(berechnetFuerAuftrag(list, 'a1'), 50000) + assert.equal(berechnetFuerAuftrag(list, 'a2'), 9000) + assert.equal(berechnetFuerAuftrag(undefined, 'a1'), 0) + assert.equal(hatEntwurfZurAuftrag(list, 'a1'), true) + assert.equal(hatEntwurfZurAuftrag(list, 'a2'), false) +}) + +test('artOptionen: mit gültiger Festpreis-AB sind alle vier Arten wählbar', () => { + const o = optionen({ auftraege: [ab()] }) + assert.deepEqual(zustand(o), [['anzahlung', true], ['schluss', true], ['aufwand', true], ['festbetrag', true]]) + assert.deepEqual(o.map((x) => x.label), ['Anzahlung', 'Schlussrechnung', 'Nach Aufwand', 'Festbetrag']) +}) + +test('artOptionen: ohne AB oder bei ausgeschaltetem Schalter erklärt ein Hinweis die Sperre', () => { + for (const args of [{ auftraege: [] }, { auftraege: [ab()], auftraegeAktiv: false }, { auftraege: [ab({ status: 'entwurf' })] }]) { + const o = optionen(args) + assert.deepEqual(zustand(o), [['anzahlung', false], ['schluss', false], ['aufwand', true], ['festbetrag', true]]) + assert.equal(o[0].hint, AB_HINWEIS) + assert.equal(o[1].hint, AB_HINWEIS) + } + assert.equal(AB_HINWEIS, 'Anzahlung und Schlussrechnung brauchen eine Auftragsbestätigung.') +}) + +test('artOptionen: ein offener Entwurf zur AB sperrt die Schlussrechnung, nicht die Anzahlung', () => { + const o = optionen({ auftraege: [ab()], rechnungen: [rk({ status: 'entwurf' })] }) + assert.deepEqual(zustand(o), [['anzahlung', true], ['schluss', false], ['aufwand', true], ['festbetrag', true]]) + assert.equal(o[1].hint, ENTWURF_HINWEIS) + assert.equal(ENTWURF_HINWEIS, 'Zu dieser Auftragsbestätigung gibt es noch einen Entwurf. Erst senden oder löschen.') + // zwei ABs: die Schlussrechnung bleibt wählbar, solange eine AB ohne Entwurf da ist + const zwei = optionen({ auftraege: [ab(), ab({ id: 'a2', nummer: 'AB-2026-0003' })], rechnungen: [rk({ status: 'entwurf' })] }) + assert.equal(zwei[1].enabled, true) +}) + +test('artOptionen: ohne Wahl keine Arten, ohne Projekt nur die freie Rechnung', () => { + assert.deepEqual(artOptionen({ projektId: null, auftraege: [ab()], rechnungen: [], auftraegeAktiv: true }), []) + const frei = artOptionen({ projektId: '', auftraege: [ab()], rechnungen: [], auftraegeAktiv: true }) + assert.deepEqual(zustand(frei), [['frei', true]]) +}) + +test('standardArt: Anzahlung, Schlussrechnung, Aufwand oder Festbetrag je nach Lage', () => { + const lage = (args, extra = {}) => { + const eingabe = { auftraege: [], rechnungen: [], offeneMinuten: 0, ...args, ...extra } + return standardArt(optionen({ auftraege: eingabe.auftraege, rechnungen: eingabe.rechnungen }), eingabe) + } + assert.equal(lage({ auftraege: [ab()] }), 'anzahlung') // Zahlungsplan Anzahlung, noch nichts berechnet + assert.equal(lage({ auftraege: [ab()], rechnungen: [rk()] }), 'schluss') // Anzahlung schon berechnet + const nachAbnahme = ab({ zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' } }) + assert.equal(lage({ auftraege: [nachAbnahme] }), 'schluss') + assert.equal(lage({}, { offeneMinuten: 90 }), 'aufwand') + assert.equal(lage({}), 'festbetrag') + const ohneProjekt = artOptionen({ projektId: '', auftraege: [], rechnungen: [], auftraegeAktiv: true }) + assert.equal(standardArt(ohneProjekt, { auftraege: [], rechnungen: [], offeneMinuten: 0 }), 'frei') + assert.equal(standardArt([], { auftraege: [], rechnungen: [], offeneMinuten: 0 }), '') +}) + +test('standardAuftrag: die jüngste passende AB, bei Schlussrechnung ohne offenen Entwurf', () => { + const neu = ab({ id: 'a5', gesendetAm: '2026-10-07T08:00:00.000Z' }) + assert.equal(standardAuftrag('anzahlung', [ab(), neu]), 'a5') + assert.equal(standardAuftrag('schluss', [ab(), neu]), 'a5') + assert.equal(standardAuftrag('schluss', [ab(), neu], [rk({ auftragId: 'a5', status: 'entwurf' })]), 'a1') + assert.equal(standardAuftrag('anzahlung', []), '') + assert.equal(standardAuftrag('aufwand', [ab({ id: 'x2', art: 'aufwand' })]), '') // die Aufwands-AB ordnet der Server zu + assert.equal(standardAuftrag('festbetrag', [ab()]), '') +}) + +test('anzahlungStandard: Prozent aus dem Zahlungsplan, sonst 50', () => { + const vorgabe = { modus: 'prozent', wert: '50', bezeichnung: 'Anzahlung' } + assert.deepEqual(anzahlungStandard(ab()), vorgabe) + assert.deepEqual(anzahlungStandard(ab({ zahlungsplan: { art: 'anzahlung', prozent: 33.5, text: '' } })), { ...vorgabe, wert: '33,5' }) + assert.deepEqual(anzahlungStandard(ab({ zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' } })), vorgabe) + assert.deepEqual(anzahlungStandard(null), vorgabe) +}) + +test('ausgewaehlteIds und blaetterSumme: alle sind vorausgewählt, abgewählte fallen heraus', () => { + const B = [bl('b1', 60), bl('b2', 90), bl('b3', 45)] + assert.deepEqual(ausgewaehlteIds(B, []), ['b1', 'b2', 'b3']) + assert.deepEqual(ausgewaehlteIds(B, ['b2']), ['b1', 'b3']) + assert.deepEqual(ausgewaehlteIds(undefined, []), []) + assert.deepEqual(blaetterSumme(B, ['b1', 'b3']), { anzahl: 2, minuten: 105 }) + assert.deepEqual(blaetterSumme(B, []), { anzahl: 0, minuten: 0 }) +}) + +const ws = (over = {}) => ({ + $id: 'w1', woid: '100140', workorderId: 't2', totalTime: 60, supporters: '', isComment: false, countsToPlan: false, + startDate: '06.10.2026', endDate: '06.10.2026', details: 'Kontaktformular gebaut', $createdAt: '2026-10-06T07:00:00.000Z', ...over, +}) + +test('blattAusAppwrite: abrechenbares Blatt mit Minuten (inkl. Unterstützung), Datum und kurzem Text', () => { + assert.deepEqual(blattAusAppwrite(ws(), {}), { + id: 'w1', ticketId: 't2', woid: '100140', datum: '2026-10-06', minuten: 60, text: 'Kontaktformular gebaut', aboZeit: false, kommentar: false, + }) + // Unterstützung zählt mit: 60 + 1 × 30 + const mit = ws({ supporters: JSON.stringify([{ employeeId: 'e1', name: 'A', task: 'Review' }]) }) + assert.equal(blattAusAppwrite(mit, {}).minuten, 90) + // ein Altwert außerhalb des 15-Minuten-Rasters wird aufgerundet + assert.equal(blattAusAppwrite(ws({ totalTime: 70 }), {}).minuten, 75) + // ohne gültiges Datum zählt der Erstellungstag, der Text ist die erste Zeile, höchstens 120 Zeichen + assert.equal(blattAusAppwrite(ws({ startDate: '', endDate: '' }), {}).datum, '2026-10-06') + assert.equal(blattAusAppwrite(ws({ details: 'Zeile 1\nZeile 2' }), {}).text, 'Zeile 1') + assert.equal(blattAusAppwrite(ws({ details: 'a'.repeat(300) }), {}).text.length, 120) +}) + +test('blattAusAppwrite: Kommentar, Abo-Zeit, 0 Minuten und schon Abgerechnetes sind null', () => { + assert.equal(blattAusAppwrite(ws({ isComment: true }), {}), null) + assert.equal(blattAusAppwrite(ws({ countsToPlan: true }), {}), null) + assert.equal(blattAusAppwrite(ws({ totalTime: 0 }), {}), null) + assert.equal(blattAusAppwrite(ws(), { w1: 'WEBKLAR-0009' }), null) + assert.equal(blattAusAppwrite(ws(), { w1: 'Entwurf' }), null) + assert.equal(blattAusAppwrite({}, {}), null) +}) + +test('ticketOptionen: Tickets des Kunden mit Nummer, ohne die schon gewählten', () => { + const T = [ + { $id: 't1', woid: '100131', topic: 'Relaunch' }, + { $id: 't2', woid: '100140', topic: 'Kontaktformular' }, + { $id: 't3', woid: '', topic: '', title: 'Alt' }, + { $id: 't4', woid: '100150', topic: '', title: '' }, + ] + assert.deepEqual(ticketOptionen(T, { ausgeschlossen: ['t1'] }), [ + { value: 't2', ticketId: 't2', woid: '100140', label: '#100140 · Kontaktformular' }, + { value: 't4', ticketId: 't4', woid: '100150', label: '#100150 · Kein Betreff' }, + ]) + assert.deepEqual(ticketOptionen(undefined), []) +})