diff --git a/src/lib/ticketStatus.js b/src/lib/ticketStatus.js index 274411f..0ca3fca 100644 --- a/src/lib/ticketStatus.js +++ b/src/lib/ticketStatus.js @@ -32,6 +32,14 @@ export function statusLabel(status) { return LABELS[status] || status } +// Auswahl fuer das Status-Feld: die sechs waehlbaren Werte, ein alter Wert des Tickets bleibt zusaetzlich sichtbar +export function statusSelectOptions(current) { + const options = TICKET_STATUSES.map((s) => ({ ...s })) + const value = current || 'Open' + if (!options.some((o) => o.value === value)) options.push({ value, label: statusLabel(value) }) + return options +} + // Filter: jede Gruppe fasst die gespeicherten Werte zusammen, auch die alten export const STATUS_GROUPS = [ { id: 'open', label: 'Offen', values: ['Open'] }, diff --git a/src/lib/ticketView.js b/src/lib/ticketView.js new file mode 100644 index 0000000..2091635 --- /dev/null +++ b/src/lib/ticketView.js @@ -0,0 +1,77 @@ +/* Anzeige-Helfer der Ticketseite und -liste. + Termin- und Faelligkeitsfelder sind Freitext aus Altdaten ("2.5.2026", "0930"): lesbare Werte + erscheinen normalisiert, alles andere als Rohtext - nie ein Rueckfall auf das Erstellungsdatum. */ + +import { formatTime } from './planboard' +import { isoToGermanDate, germanDateToIso } from './ticketForm' + +const text = (value) => String(value ?? '').trim() + +// "2.5.2026" -> "02.05.2026"; Unlesbares ("31.02.2026", ISO-Zeitstempel) bleibt als Text stehen +export function displayDate(value) { + return isoToGermanDate(germanDateToIso(value)) || text(value) +} + +// "0930" oder "9:30" -> "09:30"; Unlesbares ("2560") bleibt als Text stehen +export function displayTime(value) { + return formatTime(value) || text(value) +} + +// "02.05.2026, 09:30–10:30"; ohne Startdatum leer, das Ende steht nur mit Beginn +export function appointmentText(ticket) { + if (!text(ticket?.startDate)) return '' + let out = displayDate(ticket.startDate) + const start = displayTime(ticket.startTime) + if (start) { + out += ', ' + start + const end = displayTime(ticket.endTime) + if (end) out += '–' + end + } + return out +} + +// "Faellig" nutzt nur die Frist, nie die Uhrzeit des Termins +export function dueText(ticket) { + return displayDate(ticket?.deadline) +} + +// Nummer: woid, sonst die letzten fuenf Zeichen der $id (wie bisher in der Karte) +export function ticketNumber(ticket) { + return text(ticket?.woid) || text(ticket?.$id).slice(-5) +} + +// Erstellzeitpunkt als Date, null wenn fehlt oder kaputt +export function createdDate(ticket) { + const raw = ticket?.$createdAt || ticket?.createdAt + if (!raw) return null + const date = new Date(raw) + return Number.isNaN(date.getTime()) ? null : date +} + +// "Kenso Grimm" -> "Kenso G.", "Anna Maria Meier" -> "Anna M." +export function shortName(name) { + const words = text(name).split(/\s+/).filter(Boolean) + if (words.length < 2) return words[0] || '' + return `${words[0]} ${Array.from(words[words.length - 1])[0]}.` +} + +// Zustaendig in der Liste: Person aus der Mitarbeiterliste, sonst der am Ticket gespeicherte Name +export function assigneeLabel(ticket, employees = []) { + const id = text(ticket?.assignedTo) + if (!id) return { text: 'Niemand', none: true } + const employee = employees.find((e) => e.userId === id) + return { text: shortName(employee?.displayName) || shortName(ticket.assignedName) || 'Unbekannt', none: false } +} + +// Auswahl "Zustaendig": der aktuelle Wert bleibt immer waehlbar, auch wenn die Person nicht (mehr) in der Liste steht +export function assigneeOptions(ticket, employees = []) { + const options = [{ value: '', label: 'Nicht zugewiesen' }] + for (const e of employees) { + if (e.userId) options.push({ value: e.userId, label: e.displayName || e.email || e.userId }) + } + const id = text(ticket?.assignedTo) + if (id && !options.some((o) => o.value === id)) { + options.push({ value: id, label: text(ticket.assignedName) || 'Unbekannte Person' }) + } + return options +} diff --git a/tests/unit/ticketStatus.test.mjs b/tests/unit/ticketStatus.test.mjs index 1575309..3f78e43 100644 --- a/tests/unit/ticketStatus.test.mjs +++ b/tests/unit/ticketStatus.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test' import assert from 'node:assert/strict' import { loadModule } from './load.mjs' -const { statusTone } = await loadModule('src/lib/ticketStatus.js') +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), '') @@ -12,3 +12,11 @@ test('statusTone: Ton je gespeichertem Wert', () => { 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' }) +}) diff --git a/tests/unit/ticketView.test.mjs b/tests/unit/ticketView.test.mjs new file mode 100644 index 0000000..4195cdf --- /dev/null +++ b/tests/unit/ticketView.test.mjs @@ -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') +})