Merge origin/test (WOMS-Überarbeitung Planboard/Tickets) in feature/oeffnungszeiten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 08:56:02 +00:00
commit 35e2761e68
9002 files changed
+12366 -1176888

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { shouldCloseOnKey } = await loadModule('src/lib/dialogKeys.js')
test('Esc schliesst nur ohne Sperre und ohne vorherige Behandlung', () => {
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: false }, false), true)
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: true }, false), false)
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: false }, true), false)
assert.equal(shouldCloseOnKey({ key: 'Enter', defaultPrevented: false }, false), false)
})
+1
View File
@@ -17,6 +17,7 @@ test('buildNavLinks: vier Unterpunkte unter Kunden mit Zahlen, Admin-Eintraege n
])
assert.deepEqual(links.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'legal'])
assert.equal(links.find((l) => l.key === 'leads').paused, true)
assert.equal(kunden.children.find((c) => c.key === 'chat').hot, true)
const admin = buildNavLinks({ isAdmin: true, pulseLevel: 'warn', counts: {} })
assert.deepEqual(admin.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'finance', 'legal', 'serverpuls', 'admin'])
assert.equal(admin.find((l) => l.key === 'serverpuls').dot, 'warn')
+55
View File
@@ -0,0 +1,55 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { parseGermanDate, formatTime, dayKey, groupAppointmentsByDay, countInRange } = await loadModule('src/lib/planboard.js')
test('parseGermanDate: ein- und zweistellig, Altwerte ergeben null', () => {
assert.equal(parseGermanDate('2.5.2026').getTime(), new Date(2026, 4, 2).getTime())
assert.equal(parseGermanDate(' 07.10.2026 ').getTime(), new Date(2026, 9, 7).getTime())
for (const v of ['31.02.2026', '0.1.2026', '2026-10-07T09:00:00.000Z', '2026-10-07', '7.10.26', '', undefined, null, 'morgen']) {
assert.equal(parseGermanDate(v), null, String(v))
}
})
test('formatTime: hhmm und hh:mm, ungültige Werte ergeben leer', () => {
for (const [v, out] of [['0900', '09:00'], ['900', '09:00'], ['9:00', '09:00'], ['09:30', '09:30'], ['2359', '23:59']]) {
assert.equal(formatTime(v), out, v)
}
for (const v of ['2400', '1260', '9', 'abc', '', undefined]) assert.equal(formatTime(v), '', String(v))
assert.equal(dayKey(new Date(2026, 0, 5)), '2026-01-05')
})
const employees = [{ userId: 'u1', displayName: 'Kenso Grimm', shortcode: 'KNSO' }]
const workorders = [
{ $id: 'a', woid: '1', startDate: '7.10.2026', startTime: '1400', endTime: '1500', topic: 'Spät', type: 'Webpage', status: 'Open', customerName: 'Alpha', assignedTo: 'u1' },
{ $id: 'b', woid: '2', startDate: '07.10.2026', startTime: '', topic: 'Ohne Uhrzeit', type: 'Akquise', status: 'Open', assignedName: 'Ehemalige' },
{ $id: 'c', woid: '3', startDate: '7.10.2026', startTime: '0830', topic: 'Früh', type: 'Webpage', status: 'Cancelled' },
{ $id: 'd', startDate: '8.10.2026', startTime: '1000', title: 'Nur Titel' },
{ $id: 'h', startDate: '9.10.2026' },
{ $id: 'e', woid: '5', startDate: '31.02.2026', startTime: '1000', topic: 'Ungültig' },
{ $id: 'f', woid: '6', startDate: '2026-10-07T09:00:00.000Z', topic: 'ISO' },
{ $id: 'g', woid: '7', startDate: '', topic: 'Leer' },
]
test('groupAppointmentsByDay: nach Uhrzeit, ohne Uhrzeit am Ende, Stornierte bleiben, Altwerte fallen weg', () => {
const byDay = groupAppointmentsByDay(workorders, employees)
assert.deepEqual(Object.keys(byDay).sort(), ['2026-10-07', '2026-10-08', '2026-10-09'])
assert.deepEqual(byDay['2026-10-07'].map((a) => a.id), ['c', 'a', 'b'])
assert.equal(byDay['2026-10-07'][0].status, 'Cancelled')
assert.equal(byDay['2026-10-07'][1].employeeShort, 'KNSO')
assert.equal(byDay['2026-10-07'][1].employeeName, 'Kenso Grimm')
assert.equal(byDay['2026-10-07'][2].employeeName, 'Ehemalige')
assert.equal(byDay['2026-10-07'][2].employeeShort, '')
assert.equal(byDay['2026-10-08'][0].woid, '')
assert.equal(byDay['2026-10-08'][0].topic, 'Nur Titel')
assert.equal(byDay['2026-10-09'][0].topic, 'Ohne Betreff')
})
test('countInRange: zählt nur die genannten Tage', () => {
const byDay = groupAppointmentsByDay(workorders, employees)
const week = (day) => Array.from({ length: 7 }, (_, i) => new Date(2026, 9, day + i))
assert.equal(countInRange(byDay, week(5)), 5)
assert.equal(countInRange(byDay, week(12)), 0)
assert.equal(countInRange(byDay), 0)
})
+43
View File
@@ -0,0 +1,43 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { planboardSubtitle, dayLabels, appointmentClass, appointmentTimeLabel, appointmentOwnerLabel, appointmentTitle } =
await loadModule('src/lib/planboardView.js')
test('planboardSubtitle: Kalenderwoche, Zeitraum, Terminzahl', () => {
const mo = new Date(2026, 9, 5)
assert.equal(planboardSubtitle({ weekStart: mo, count: 13 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober 2026 · 13 Termine')
assert.equal(planboardSubtitle({ weekStart: mo, count: 1 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober 2026 · 1 Termin')
assert.equal(planboardSubtitle({ weekStart: mo, count: 0 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober 2026 · 0 Termine')
assert.equal(planboardSubtitle({ weekStart: mo, count: 0, loading: true }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober 2026 · Wird geladen …')
assert.equal(planboardSubtitle({ weekStart: new Date(2026, 11, 28), count: 2 }), 'Kalenderwoche 53 · 28. Dezember 2026 bis 3. Januar 2027 · 2 Termine')
assert.equal(planboardSubtitle({ weekStart: new Date(2027, 0, 4), count: 2 }), 'Kalenderwoche 1 · 4. Januar bis 10. Januar 2027 · 2 Termine')
})
test('dayLabels: Wochentag, Datum, volles Datum', () => {
assert.deepEqual(dayLabels(new Date(2026, 9, 5)), { weekday: 'Montag', number: '05.10', full: 'Montag, 5. Oktober 2026' })
assert.deepEqual(dayLabels(new Date(2026, 9, 11)), { weekday: 'Sonntag', number: '11.10', full: 'Sonntag, 11. Oktober 2026' })
})
test('appointmentClass und appointmentTimeLabel', () => {
for (const t of ['Akquise', 'akquise', 'AKQUISE']) assert.equal(appointmentClass(t), 'appt akquise', t)
for (const t of ['Webpage', 'Project', 'Migration', '', undefined, null]) assert.equal(appointmentClass(t), 'appt', String(t))
assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '09:30' }), '08:00–09:30')
assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '' }), '08:00')
assert.equal(appointmentTimeLabel({ time: '', endTime: '09:30' }), 'ohne Uhrzeit')
assert.equal(appointmentTimeLabel({ time: '', endTime: '' }), 'ohne Uhrzeit')
})
test('appointmentOwnerLabel: Nummer und Mitarbeiter', () => {
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), '#100201 · KNSO')
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Ehemalige', employeeShort: '' }), '#100201 · Ehemalige')
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: '', employeeShort: '' }), '#100201 · niemand zugewiesen')
assert.equal(appointmentOwnerLabel({ woid: '', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), 'KNSO')
assert.equal(appointmentOwnerLabel({ woid: '', employeeName: '', employeeShort: '' }), 'niemand zugewiesen')
})
test('appointmentTitle: Tooltip mit und ohne woid', () => {
assert.equal(appointmentTitle({ woid: '100201' }), 'Ticket #100201 öffnen')
assert.equal(appointmentTitle({ woid: '' }), 'Ticket öffnen')
})
+11
View File
@@ -0,0 +1,11 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { createRequestGuard } = await loadModule('src/lib/requestGuard.js')
test('nur die jüngste Anfrage gilt', () => {
const g = createRequestGuard()
const a = g.next(); const b = g.next()
assert.equal(g.isCurrent(a), false)
assert.equal(g.isCurrent(b), true)
})
+36
View File
@@ -0,0 +1,36 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { isRowActivation } = await loadModule('src/lib/rowClick.js')
const TAGS = ['a', 'button', 'select', 'input', 'textarea', 'label']
// Ereignis ohne DOM: closest liefert nur dann einen Treffer, wenn der Selektor das Tag nennt
function click({ target = 'td', outside = false, ...over } = {}) {
const row = { contains: (el) => el?.inRow === true }
const hit = TAGS.includes(target) ? { inRow: !outside } : null
return {
target: { closest: (sel) => (hit && sel.split(',').map((s) => s.trim()).includes(target) ? hit : null) },
currentTarget: row, button: 0, defaultPrevented: false, metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, ...over,
}
}
test('Linksklick auf Zelle oder Zeile aktiviert die Zeile', () => {
assert.equal(isRowActivation(click()), true)
assert.equal(isRowActivation(click({ target: 'tr' })), true)
})
test('Klick auf a, button, select, input, textarea oder label aktiviert nicht', () => {
for (const tag of TAGS) assert.equal(isRowActivation(click({ target: tag })), false, tag)
})
test('Mittelklick, Modifikatortasten und behandelte Klicks aktivieren nicht', () => {
assert.equal(isRowActivation(click({ button: 1 })), false)
for (const key of ['metaKey', 'ctrlKey', 'shiftKey', 'altKey']) assert.equal(isRowActivation(click({ [key]: true })), false, key)
assert.equal(isRowActivation(click({ defaultPrevented: true })), false)
})
test('ein interaktiver Vorfahre außerhalb der Zeile zählt nicht', () => {
assert.equal(isRowActivation(click({ target: 'label', outside: true })), true)
})
-27
View File
@@ -1,27 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { startsCollapsed, AUTO_COLLAPSE_MAX_WIDTH } = await loadModule('src/lib/sidebar.js')
// Fenster mit matchMedia, das "(max-width: Npx)" gegen eine feste Breite prueft
const windowOf = (width) => ({
matchMedia: (query) => ({ matches: width <= Number(/max-width:\s*(\d+)px/.exec(query)?.[1]) }),
})
test('startsCollapsed: Handy (390 px) und Grenze (720 px) starten eingeklappt', () => {
assert.equal(AUTO_COLLAPSE_MAX_WIDTH, 720)
assert.equal(startsCollapsed(windowOf(390)), true)
assert.equal(startsCollapsed(windowOf(720)), true)
})
test('startsCollapsed: breitere Fenster starten ausgeklappt', () => {
assert.equal(startsCollapsed(windowOf(721)), false)
assert.equal(startsCollapsed(windowOf(1280)), false)
})
test('startsCollapsed: ohne window oder matchMedia (oder bei Fehler) ausgeklappt', () => {
assert.equal(startsCollapsed(null), false)
assert.equal(startsCollapsed({}), false)
assert.equal(startsCollapsed({ matchMedia: () => { throw new Error('kaputt') } }), false)
})
+47
View File
@@ -0,0 +1,47 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const m = await loadModule('src/lib/ticketListUrl.js')
const { LIST_TABS, LIST_DEFAULTS, parseListParams: parse, serializeListParams: ser, withFilter, nextLimit, listFilters, countFilters, woidRedirectTarget } = m
const { TYPE_FILTERS } = await loadModule('src/lib/ticketStatus.js')
test('Reiter und Art-Auswahl nach Spec', () => {
assert.deepEqual(LIST_TABS.map((t) => [t.id, t.label]), [['aktiv', 'Aktiv'], ['open', 'Offen'], ['working', 'In Arbeit'], ['waiting', 'Wartet auf Kunde'], ['review', 'Prüfung'], ['done', 'Erledigt'], ['cancelled', 'Storniert'], ['alle', 'Alle']])
assert.deepEqual(TYPE_FILTERS.map((f) => f.label), ['Aufträge', 'Webpage', 'Migration', 'Projekt', 'Akquise', 'Aufträge und Akquise'])
})
test('parse: Standard, gültige und ungültige Werte', () => {
assert.deepEqual(parse(''), LIST_DEFAULTS)
assert.deepEqual(parse('?status=done&art=all&dringend=1&q=%20Müller%20&limit=30'), { tab: 'done', typeId: 'all', urgentOnly: true, search: 'Müller', limit: 30 })
assert.deepEqual(parse('status=foo&art=bar&dringend=ja&limit=abc'), LIST_DEFAULTS)
for (const [raw, want] of [['5', 10], ['0', 10], ['25', 25], ['500', 500], ['9999', 500], ['-3', 10], ['1e3', 10], ['', 10]]) assert.equal(parse('limit=' + raw).limit, want, raw)
})
test('serialize: Standardwerte werden nicht geschrieben, Rundlauf', () => {
assert.equal(ser(LIST_DEFAULTS).toString(), '')
assert.equal(ser({ ...LIST_DEFAULTS, tab: 'alle', limit: 20 }).toString(), 'status=alle&limit=20')
assert.equal(ser({ ...LIST_DEFAULTS, urgentOnly: true, search: 'a b' }).toString(), 'dringend=1&q=a+b')
for (const s of [LIST_DEFAULTS, { tab: 'done', typeId: 'Akquise', urgentOnly: true, search: 'x y', limit: 40 }]) assert.deepEqual(parse(ser(s)), s)
})
test('Filteränderung setzt limit zurück, Mehr laden +10 bis 500', () => {
for (const patch of [{ tab: 'done' }, { typeId: 'all' }, { urgentOnly: true }, { search: 'x' }]) assert.equal(withFilter({ ...LIST_DEFAULTS, limit: 50 }, patch).limit, 10)
assert.deepEqual([10, 495, 500].map((limit) => nextLimit({ limit })), [20, 500, 500])
})
test('listFilters und countFilters', () => {
assert.deepEqual(listFilters(LIST_DEFAULTS), { status: ['Open', 'Occupied', 'Assigned', 'Added Info', 'In Kontakt', 'Awaiting', 'Halted', 'In Test'], type: [], excludeType: 'Akquise', priority: [], search: undefined, limit: 10 })
assert.deepEqual(listFilters({ ...LIST_DEFAULTS, tab: 'alle', typeId: 'Akquise', urgentOnly: true, search: 'abc' }), { status: [], type: ['Akquise'], excludeType: undefined, priority: [3, 4], search: 'abc', limit: 10 })
assert.deepEqual(listFilters({ ...LIST_DEFAULTS, tab: 'done' }).status, ['Closed', 'Zugesagt'])
const s = { ...LIST_DEFAULTS, typeId: 'Webpage', urgentOnly: true, search: 'zzz', limit: 80 }
assert.deepEqual(countFilters(s, 'review'), { status: ['In Test'], type: ['Webpage'], excludeType: undefined, priority: [3, 4] })
for (const t of LIST_TABS) assert.deepEqual(countFilters(s, t.id).status, listFilters({ ...s, tab: t.id }).status, t.id)
})
test('woid-Umleitung hat Vorrang', () => {
assert.equal(woidRedirectTarget('?woid=100131'), '/tickets/100131')
assert.equal(woidRedirectTarget('?woid=%20100131%20&status=done&q=x'), '/tickets/100131')
assert.equal(woidRedirectTarget('?woid=a%2Fb'), '/tickets/a%2Fb')
for (const none of ['', '?woid=', '?status=done']) assert.equal(woidRedirectTarget(none), null)
})
+40
View File
@@ -0,0 +1,40 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { ticketPath, ticketRef, resolveTicketTab, listBackPath } = await loadModule('src/lib/ticketRoutes.js')
test('ticketPath: woid, Zahl, $id, Reiter', () => {
assert.equal(ticketPath('100131'), '/tickets/100131')
assert.equal(ticketPath(100131), '/tickets/100131')
assert.equal(ticketPath('100131', 'details'), '/tickets/100131')
assert.equal(ticketPath('100131', 'arbeitsblaetter'), '/tickets/100131/arbeitsblaetter')
assert.equal(ticketPath('100131', 'unbekannt'), '/tickets/100131')
assert.equal(ticketPath('66f1c2a9000b1c2d3e4f'), '/tickets/66f1c2a9000b1c2d3e4f')
})
test('ticketPath: Sonderzeichen kodiert, leeres ref ergibt /tickets', () => {
assert.equal(ticketPath('a/b c?d#e'), '/tickets/a%2Fb%20c%3Fd%23e')
for (const empty of ['', ' ', null, undefined]) {
assert.equal(ticketPath(empty), '/tickets')
assert.equal(ticketPath(empty, 'arbeitsblaetter'), '/tickets')
}
})
test('ticketRef: woid vor $id, nie die Kurznummer', () => {
assert.equal(ticketRef({ woid: '100131', $id: 'abc' }), '100131')
assert.equal(ticketRef({ woid: '', $id: 'abcdefghij' }), 'abcdefghij')
assert.equal(ticketRef({ woid: ' ', $id: 'abcdefghij' }), 'abcdefghij')
assert.equal(ticketRef(null), '')
})
test('resolveTicketTab: unbekannt ergibt null', () => {
assert.equal(resolveTicketTab(undefined), 'details')
assert.equal(resolveTicketTab('arbeitsblaetter'), 'arbeitsblaetter')
for (const bad of ['Details', 'worksheets', '', 'x']) assert.equal(resolveTicketTab(bad), null)
})
test('listBackPath: nur Suchteile mit ?', () => {
assert.equal(listBackPath({ listSearch: '?status=done&q=x' }), '/tickets?status=done&q=x')
for (const bad of [undefined, null, {}, { listSearch: '' }, { listSearch: '?' }, { listSearch: 'evil' }, { listSearch: 42 }]) assert.equal(listBackPath(bad), '/tickets')
})
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { statusTone, statusSelectOptions } = await loadModule('src/lib/ticketStatus.js')
test('statusTone: Ton je gespeichertem Wert', () => {
for (const s of ['Open', '', undefined, null, 'Completed']) assert.equal(statusTone(s), '')
for (const s of ['Occupied', 'Assigned', 'Added Info', 'In Kontakt']) assert.equal(statusTone(s), 'blue')
for (const s of ['Awaiting', 'Halted']) assert.equal(statusTone(s), 'orange')
assert.equal(statusTone('In Test'), 'violet')
for (const s of ['Closed', 'Zugesagt']) assert.equal(statusTone(s), 'green')
for (const s of ['Cancelled', 'Aborted', 'Abgesagt']) assert.equal(statusTone(s), 'plain')
})
test('statusSelectOptions: sechs Werte, aktueller Wert zusätzlich', () => {
const six = ['Open', 'Occupied', 'Awaiting', 'In Test', 'Closed', 'Cancelled']
for (const cur of ['Open', 'Closed', '', undefined, null]) assert.deepEqual(statusSelectOptions(cur).map((o) => o.value), six, String(cur))
assert.deepEqual(statusSelectOptions('In Kontakt').at(-1), { value: 'In Kontakt', label: 'In Kontakt' })
assert.deepEqual(statusSelectOptions('Assigned').at(-1), { value: 'Assigned', label: 'Eingeplant' })
assert.deepEqual(statusSelectOptions('Completed').at(-1), { value: 'Completed', label: 'Completed' })
})
+54
View File
@@ -0,0 +1,54 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const t = await loadModule('src/lib/ticketTimes.js')
const tickets = (n, from = 100000) => Array.from({ length: n }, (_, i) => ({ $id: 'id' + i, woid: String(from + i) }))
const sup = JSON.stringify([{ employeeId: 'e', name: 'N', task: 'x' }])
test('chunk und planTimeLoad: erste 200 Zeilen, Blöcke zu 100', () => {
assert.deepEqual(t.chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]])
const plan = t.planTimeLoad(tickets(250), new Set())
assert.deepEqual(plan.blocks.map((b) => b.length), [100, 100])
assert.equal(plan.blocks[1].at(-1), '100199')
assert.equal(plan.capped, true)
assert.equal(t.planTimeLoad(tickets(200), new Set()).capped, false)
})
test('planTimeLoad: nur Neues, ohne woid, ohne Doppelte', () => {
const known = new Set(tickets(100).map((x) => x.woid))
assert.deepEqual(t.planTimeLoad(tickets(120), known).blocks, [tickets(20, 100100).map((x) => x.woid)])
assert.deepEqual(t.planTimeLoad([{ $id: 'a' }, { woid: '' }, { woid: '5' }, { woid: 5 }, { woid: '5' }], new Set()).blocks, [['5']])
assert.deepEqual(t.planTimeLoad(tickets(3), new Set(tickets(3).map((x) => x.woid))).blocks, [])
})
test('blockMinutes: Summe ohne Kommentare, mit Unterstützung, nur woids des Blocks', () => {
const docs = [
{ woid: '1', totalTime: 60 }, { woid: '1', totalTime: 30, supporters: sup }, { woid: '1', totalTime: 0, isComment: true },
{ woid: '2', totalTime: 45, isComment: true }, { woid: 7, totalTime: 15 }, { woid: '99', totalTime: 15 },
]
assert.deepEqual(t.blockMinutes(['1', '2', '3', '7'], docs, 6), { 1: 105, 2: 0, 3: 0, 7: 15 })
})
test('blockMinutes: abgeschnittene Blöcke liefern null, nie eine falsche Zahl', () => {
assert.equal(t.blockMinutes(['1'], Array.from({ length: 5000 }, () => ({ woid: '1', totalTime: 15 })), 5000), null)
assert.equal(t.blockMinutes(['1'], [{ woid: '1', totalTime: 15 }], 7), null)
assert.deepEqual(t.blockMinutes(['1'], [{ woid: '1', totalTime: 15 }], 1), { 1: 15 })
})
test('minutesCellText: Fehler, null, 0, ohne woid und jenseits von 200 zeigen –', () => {
const by = new Map([['1', 105], ['2', 0], ['3', null]])
assert.equal(t.minutesCellText({ woid: '1' }, 0, by), '1 h 45 min')
for (const x of [{ woid: '2' }, { woid: '3' }, { woid: '4' }, {}, { woid: '' }]) assert.equal(t.minutesCellText(x, 0, by), '–')
assert.equal(t.minutesCellText({ woid: '1' }, 199, by), '1 h 45 min')
assert.equal(t.minutesCellText({ woid: '1' }, 200, by), '–')
})
test('mapLimit: höchstens 3 gleichzeitig, Reihenfolge, Fehler je Element', async () => {
let running = 0, peak = 0
const out = await t.mapLimit([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
running += 1; peak = Math.max(peak, running)
await new Promise((r) => setTimeout(r, 5))
running -= 1
if (n === 4) throw new Error('x')
return n * 2
})
assert.equal(peak, 3)
assert.deepEqual(out.map((r) => r.ok), [true, true, true, false, true, true, true])
assert.deepEqual(out.filter((r) => r.ok).map((r) => r.value), [2, 4, 6, 10, 12, 14])
assert.deepEqual(await t.mapLimit([], 3, async () => 1), [])
})
+51
View File
@@ -0,0 +1,51 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const v = await loadModule('src/lib/ticketView.js')
test('displayDate und displayTime: lesbar normalisiert, sonst Rohtext', () => {
for (const [raw, want] of [['2.5.2026', '02.05.2026'], ['31.02.2026', '31.02.2026'], ['2026-10-07T09:00:00.000Z', '2026-10-07T09:00:00.000Z'], [' ', ''], [undefined, '']]) assert.equal(v.displayDate(raw), want, String(raw))
for (const [raw, want] of [['0930', '09:30'], ['9:30', '09:30'], ['2560', '2560'], ['ab 9', 'ab 9'], ['', '']]) assert.equal(v.displayTime(raw), want, raw)
})
test('appointmentText: kein Rückfall, Rohtext bei Unlesbarem', () => {
const t = (o) => v.appointmentText(o)
assert.equal(t({ startDate: '2.5.2026', startTime: '0930', endTime: '1030' }), '02.05.2026, 09:30–10:30')
assert.equal(t({ startDate: '02.05.2026', startTime: '0930' }), '02.05.2026, 09:30')
assert.equal(t({ startDate: '02.05.2026', endTime: '1030' }), '02.05.2026')
assert.equal(t({ startDate: '31.02.2026', startTime: '0900' }), '31.02.2026, 09:00')
assert.equal(t({ startDate: '', startTime: '0900' }), '')
assert.equal(t({}), '')
assert.equal(v.dueText({ deadline: '5.10.2026' }), '05.10.2026')
assert.equal(v.dueText({}), '')
})
test('ticketNumber, createdDate, shortName', () => {
assert.equal(v.ticketNumber({ woid: '100131', $id: 'x' }), '100131')
assert.equal(v.ticketNumber({ $id: '66f1c2a9abcde' }), 'abcde')
assert.equal(v.ticketNumber({}), '')
assert.ok(v.createdDate({ $createdAt: '2026-10-01T10:00:00.000Z' }) instanceof Date)
assert.ok(v.createdDate({ createdAt: '2026-10-01T10:00:00.000Z' }) instanceof Date)
assert.equal(v.createdDate({ $createdAt: 'kaputt' }), null)
assert.equal(v.createdDate({}), null)
for (const [raw, want] of [['Kenso Grimm', 'Kenso G.'], [' Anna Maria Meier ', 'Anna M.'], ['Andrej', 'Andrej'], ['', '']]) assert.equal(v.shortName(raw), want)
})
const emps = [{ userId: 'u1', displayName: 'Kenso Grimm' }, { userId: 'u2', displayName: 'Andrej' }, { displayName: 'Ohne Login' }]
test('assigneeLabel: Niemand, Liste, assignedName, Unbekannt', () => {
assert.deepEqual(v.assigneeLabel({ assignedTo: '' }, emps), { text: 'Niemand', none: true })
assert.deepEqual(v.assigneeLabel({ assignedTo: 'u1' }, emps), { text: 'Kenso G.', none: false })
assert.equal(v.assigneeLabel({ assignedTo: 'u9', assignedName: 'Justin Nowak' }, emps).text, 'Justin N.')
assert.equal(v.assigneeLabel({ assignedTo: 'u9' }, emps).text, 'Unbekannt')
assert.equal(v.assigneeLabel({ assignedTo: 'u1', assignedName: 'Kenso Grimm' }, []).text, 'Kenso G.')
})
test('assigneeOptions: aktueller Wert bleibt immer wählbar', () => {
assert.deepEqual(v.assigneeOptions({ assignedTo: 'u1' }, emps), [{ value: '', label: 'Nicht zugewiesen' }, { value: 'u1', label: 'Kenso Grimm' }, { value: 'u2', label: 'Andrej' }])
assert.deepEqual(v.assigneeOptions({ assignedTo: 'u9', assignedName: 'Justin Nowak' }, emps).at(-1), { value: 'u9', label: 'Justin Nowak' })
assert.deepEqual(v.assigneeOptions({ assignedTo: 'u9' }, emps).at(-1), { value: 'u9', label: 'Unbekannte Person' })
assert.deepEqual(v.assigneeOptions({ assignedTo: '' }, []), [{ value: '', label: 'Nicht zugewiesen' }])
assert.equal(v.assigneeOptions({ assignedTo: 'u1' }, []).at(-1).label, 'Unbekannte Person')
})
+20
View File
@@ -0,0 +1,20 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { badgeToneClass, BUTTON_CLASSES } = await loadModule('src/components/ui/tones.js')
test('badgeToneClass', () => {
for (const t of ['muted', 'grey', undefined, 'unbekannt']) assert.equal(badgeToneClass(t), 'badge')
assert.equal(badgeToneClass('info'), 'badge blue')
assert.equal(badgeToneClass('blue'), 'badge blue')
assert.equal(badgeToneClass('ok'), 'badge green')
assert.equal(badgeToneClass('warn'), 'badge orange')
assert.equal(badgeToneClass('danger'), 'badge red')
assert.equal(badgeToneClass('violet'), 'badge violet')
assert.equal(badgeToneClass('plain'), 'badge plain')
})
test('Button-Varianten', () => {
assert.deepEqual(BUTTON_CLASSES, { default: 'btn', ghost: 'btn', primary: 'btn btn-primary', quiet: 'btn btn-quiet', danger: 'link-danger' })
})
+27
View File
@@ -0,0 +1,27 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { lookupWorkorder } = await loadModule('src/lib/workorderLookup.js')
const fake = ({ byWoid = null, byId, byIdError } = {}) => {
const calls = []
return { calls, findByWoid: async (w) => { calls.push(['woid', w]); return byWoid }, getById: async (id) => { calls.push(['id', id]); if (byIdError) throw byIdError; return byId } }
}
test('erst woid, dann $id', async () => {
let d = fake({ byWoid: { $id: 'a', woid: '5' } })
assert.deepEqual(await lookupWorkorder(' 5 ', d), { status: 'ok', ticket: { $id: 'a', woid: '5' } })
assert.deepEqual(d.calls, [['woid', '5']])
d = fake({ byId: { $id: 'abc' } })
assert.deepEqual(await lookupWorkorder('abc', d), { status: 'ok', ticket: { $id: 'abc' } })
assert.deepEqual(d.calls, [['woid', 'abc'], ['id', 'abc']])
})
test('400 und 404 sind nicht gefunden, leeres ref ohne Abfrage', async () => {
for (const code of [400, 404]) assert.deepEqual(await lookupWorkorder('x', fake({ byIdError: { code } })), { status: 'notfound' })
const d = fake()
for (const empty of ['', ' ', null, undefined]) assert.deepEqual(await lookupWorkorder(empty, d), { status: 'notfound' })
assert.deepEqual(d.calls, [])
})
test('andere Fehler werden weitergereicht', async () => {
await assert.rejects(lookupWorkorder('x', fake({ byIdError: { code: 500, message: 'boom' } })))
await assert.rejects(lookupWorkorder('x', { findByWoid: async () => { throw new Error('netz') }, getById: async () => ({}) }), /netz/)
})
+40
View File
@@ -0,0 +1,40 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { createWorksheetFlow, acquisitionOutcome } = await loadModule('src/lib/worksheetFlow.js')
async function run(ticket, newStatus, over = {}) {
const calls = { ticket: [], customer: [] }
const result = await createWorksheetFlow({
ticket, worksheetData: { newStatus }, user: { $id: 'u' },
createWorksheet: async () => over.result ?? { success: true },
updateTicket: async (id, data) => { calls.ticket.push([id, data]) },
updateCustomer: async (id, data) => { calls.customer.push([id, data]); if (over.customerFails) throw new Error('weg') },
now: () => new Date('2026-10-05T10:00:00Z'),
})
return { result, calls }
}
const akq = { $id: 't1', type: 'Akquise', status: 'In Kontakt', customerId: 'c1' }
test('Akquise + Zugesagt: Status zurückschreiben, Kunde fest', async () => {
const { calls } = await run(akq, 'Zugesagt')
assert.deepEqual(calls.ticket, [['t1', { status: 'Zugesagt' }]])
assert.deepEqual(calls.customer, [['c1', { customerStatus: 'customer', updatedAt: '2026-10-05T10:00:00.000Z' }]])
})
test('Akquise + Abgesagt: Kunde verloren', async () => {
assert.deepEqual((await run(akq, 'Abgesagt')).calls.customer, [['c1', { customerStatus: 'lost', updatedAt: '2026-10-05T10:00:00.000Z' }]])
})
test('Akquise + In Kontakt: keine Änderung', async () => {
assert.deepEqual((await run(akq, 'In Kontakt')).calls, { ticket: [], customer: [] })
assert.deepEqual((await run({ ...akq, status: 'Zugesagt' }, 'In Kontakt')).calls.customer, [])
})
test('Nicht-Akquise ändert nie den Kunden, Status nur bei Abweichung', async () => {
for (const status of ['Zugesagt', 'Abgesagt', 'Closed']) assert.deepEqual((await run({ ...akq, type: 'Webpage', status: 'Open' }, status)).calls.customer, [])
assert.deepEqual((await run({ ...akq, type: 'Webpage', status: 'Closed' }, 'Closed')).calls.ticket, [])
assert.deepEqual((await run({ ...akq, customerId: '' }, 'Zugesagt')).calls.customer, [])
})
test('Fehlschlag und Kundenfehler', async () => {
const failed = await run(akq, 'Zugesagt', { result: { success: false, error: 'x' } })
assert.deepEqual(failed, { result: { success: false, error: 'x' }, calls: { ticket: [], customer: [] } })
assert.equal((await run(akq, 'Zugesagt', { customerFails: true })).result.success, true)
assert.equal(acquisitionOutcome({ type: 'Akquise', customerId: 'c' }, 'Zugesagt'), 'customer')
})
+45
View File
@@ -0,0 +1,45 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const w = await loadModule('src/lib/worksheetView.js')
const ws = (o = {}) => ({ totalTime: 60, isComment: false, countsToPlan: false, serviceType: 'Remote', ...o })
const sup = (n, name = 'Anna Meier') => JSON.stringify(Array.from({ length: n }, (_, i) => ({ employeeId: 'e' + i, name, task: 'DNS' })))
test('worksheetSums: Kommentare zählen nie, Abo nur countsToPlan, Zeiten offen', () => {
const list = [ws(), ws({ supporters: sup(1), countsToPlan: true }), ws({ isComment: true, totalTime: 0 }), ws({ serviceType: 'GIT', isComment: true, totalTime: 0 }), ws({ serviceType: 'GIT', totalTime: 30, startTime: '0900' })]
assert.deepEqual(w.worksheetSums(list), { totalMinutes: 180, aboMinutes: 90, openCount: 1 })
assert.deepEqual(w.worksheetSums([]), { totalMinutes: 0, aboMinutes: 0, openCount: 0 })
assert.deepEqual(w.worksheetSums([ws({ isComment: true, countsToPlan: true })]), { totalMinutes: 0, aboMinutes: 0, openCount: 0 })
})
test('showAboToggle: Regel bleibt exakt', () => {
assert.equal(w.showAboToggle(ws(), true), true)
assert.equal(w.showAboToggle(ws(), false), false)
assert.equal(w.showAboToggle(ws({ countsToPlan: true }), false), true)
assert.equal(w.showAboToggle(ws({ totalTime: 0 }), true), false)
assert.equal(w.showAboToggle(ws({ isComment: true }), true), false)
assert.equal(w.showAboToggle(ws({ totalTime: '45' }), true), true)
})
test('worksheetWhen: Start-, sonst Endzweig, Altwerte als Rohtext', () => {
assert.deepEqual(w.worksheetWhen({ startDate: '2.5.2026', startTime: '0930', endDate: '2.5.2026', endTime: '1100' }), { date: '02.05.2026', time: '09:30' })
assert.deepEqual(w.worksheetWhen({ startDate: '', startTime: '', endDate: '31.02.2026', endTime: '1405' }), { date: '31.02.2026', time: '14:05' })
assert.deepEqual(w.worksheetWhen({ startDate: '03.10.2026', startTime: '' }), { date: '03.10.2026', time: '' })
assert.deepEqual(w.worksheetWhen({}), { date: '', time: '' })
})
test('Art, Badge, Status danach, Zeit', () => {
assert.equal(w.worksheetKindLabel(ws({ isComment: true })), 'Kommentar')
assert.deepEqual(['Remote', 'On Site', 'Off Site', 'GIT'].map((s) => w.worksheetKindLabel(ws({ serviceType: s, isComment: s === 'GIT' }))), ['Fernwartung', 'Vor Ort', 'Fernwartung', 'GIT'])
assert.deepEqual(w.worksheetBadge(ws({ serviceType: 'GIT', isComment: true, totalTime: 0 })), { tone: 'orange', text: 'Zeit offen' })
assert.deepEqual(w.worksheetBadge(ws({ serviceType: 'GIT', startTime: '0900' })), { tone: 'green', text: 'Zeit eingetragen' })
assert.equal(w.worksheetBadge(ws()), null)
assert.equal(w.statusAfterLabel({ oldStatus: 'Open', newStatus: 'Occupied' }), 'Status danach: In Arbeit')
assert.equal(w.statusAfterLabel({ oldStatus: 'Closed', newStatus: 'Open' }), 'Status danach: Offen')
assert.equal(w.statusAfterLabel({ oldStatus: 'Open', newStatus: 'Completed' }), 'Status danach: Completed')
for (const same of [{ oldStatus: 'Open', newStatus: 'Open' }, { oldStatus: 'Open', newStatus: '' }, {}]) assert.equal(w.statusAfterLabel(same), '')
assert.deepEqual([ws({ isComment: true }), ws({ totalTime: 0 }), ws({ totalTime: 90 }), ws({ totalTime: '45' })].map(w.timeLabel), ['', '–', '1 h 30 min', '45 min'])
})
test('supportLines: je Person eine Zeile, Wörter wie bisher', () => {
assert.deepEqual(w.supportLines(ws({ supporters: sup(1) })), ['Unterstützung Anna · DNS · 30 min'])
assert.deepEqual(w.supportLines(ws({ totalTime: 75, supporters: sup(1, '') })), ['Unterstützung · DNS · 45 min'])
assert.deepEqual(w.supportLines(ws({ isComment: true, supporters: sup(1) })), [])
assert.deepEqual(w.supportLines(ws({ supporters: '{kaputt' })), [])
})