import { test } from 'node:test' import assert from 'node:assert/strict' import { customerChanges, customerTicketStatus, isInternalTicket, customerOrder, customerWorkEntries, workTotalMinutes, } from '../server/services/customerProjectView.js' // Commit wie listRepoCommits() ihn liefert (giteaAdmin.js) function commit(message, date = '2026-09-24T12:05:00Z') { return { sha: '88d3c37ab7b15337772cf59a5a9294ec0cc8cca4', shortSha: '88d3c37', message, authorName: 'Claude', date, url: 'https://git.webklar.com/knso/ff-lack/commit/88d3c37ab7b15337772cf59a5a9294ec0cc8cca4', } } const titles = (commits) => customerChanges(commits).map((c) => c.title) /* --------------------------------------------------------- Änderungen --- */ test('Änderung: KI-Trailer fallen weg, Kürzel/Autor/Link werden nicht ausgeliefert', () => { const changes = customerChanges([ commit('Öffnungszeiten für den Winter\n\nTeamfotos ausgetauscht\n\nCo-Authored-By: Claude Opus 5.5 \n'), ]) assert.deepEqual(changes, [ { date: '2026-09-24T12:05:00Z', title: 'Öffnungszeiten für den Winter', details: ['Teamfotos ausgetauscht'] }, ]) }) test('Änderung: auch andere Modelle, Tools und Git-Trailer fallen weg', () => { const [change] = customerChanges([ commit([ 'Kontaktformular', '', 'Co-authored-by: Copilot ', 'Co-Authored-By: Claude Fable 5 ', 'Signed-off-by: Kenso Grimm ', 'Change-Id: I8d2f0a1b', '🤖 Generated with [Claude Code](https://claude.com/claude-code)', 'https://claude.ai/code/session_01AbCdEf', ].join('\n')), ]) assert.deepEqual(change.details, []) }) test('Änderung: Präfixe wie feat(...)/fix: verschwinden, erster Buchstabe groß', () => { assert.deepEqual( titles([commit('feat(roadmaps): Ticket-Integration'), commit('fix: kaputten Link im Footer repariert', '2026-09-23T10:00:00Z')]), ['Ticket-Integration', 'Kaputten Link im Footer repariert'] ) }) test('Änderung: rein technische Commits werden ausgeblendet', () => { assert.deepEqual( titles([ commit('Update preview config from ticket system\n'), commit('Preview-Config: unger-reisen.project.webklar.com\n'), commit('Hosting: Preview aktivieren (automatisch)\n'), commit('Migration: Preview auf Pipeline-Repo umgezogen (identica-kaiser)\n'), commit("Merge branch 'main' of https://git.webklar.com/knso/lackwerk-kl\n"), commit("Merge pull request 'Neue Startseite' (#3) from dev into main\n"), ]), [] ) }) test('Änderung: bekannte Automatik-Texte werden übersetzt', () => { const day = (d) => `2026-09-${d}T08:00:00Z` assert.deepEqual( titles([ commit('Initial commit\n', day(10)), commit('Automatisch erstellte Preview-Seite\n', day(11)), commit('Anreicherung: Bilder, Logo, Text-Feinarbeit (Cloud-Routine)\n', day(12)), commit('Google Fonts self-hosten (DSGVO): assets/fonts.css statt fonts.googleapis.com\n', day(13)), commit('Website ff-lack – aus tamplate-lack, auf Fahrzeuglackierer Fruck – Altenglan angepasst\n', day(14)), ]), [ 'Projekt angelegt', 'Erste Vorschau der Website erstellt', 'Inhalte ergänzt: Bilder, Logo, Text-Feinarbeit', 'Schriftarten datenschutzkonform eingebunden (DSGVO)', 'Website erstellt und an den Betrieb angepasst', ] ) }) test('Änderung: nichtssagende Titel werden zu "Website aktualisiert", echte Wörter bleiben', () => { const day = (d) => `2026-09-${d}T08:00:00Z` assert.deepEqual( titles([ commit('fix\n', day(10)), commit('upload\n', day(11)), commit('pagev1\n', day(12)), commit('webseite-version-01\n', day(13)), commit('Teamseite\n', day(14)), ]), ['Website aktualisiert', 'Website aktualisiert', 'Website aktualisiert', 'Website aktualisiert', 'Teamseite'] ) }) test('Änderung: Revert wird verständlich, die "This reverts commit"-Zeile fällt weg', () => { const [change] = customerChanges([ commit('Revert "Anreicherung: Bilder, Logo, Farbwelt, Texte (Cloud-Routine)"\n\nThis reverts commit 4ff6e7a0348c5553769fbff2ac4bc1e44817ff28.\n'), ]) assert.deepEqual(change, { date: '2026-09-24T12:05:00Z', title: '„Inhalte ergänzt: Bilder, Logo, Farbwelt, Texte“ zurückgenommen', details: [], }) }) test('Änderung: umbrochene Aufzählungen und Absätze werden wieder zusammengefügt', () => { const [bullets] = customerChanges([ commit([ 'Website stärker personalisiert', '', '- Echte Betriebsfotos statt Stock: Luftbild als Hero, Galerie', ' mit rotem Lackier-Fahrzeug, Farbtonanalyse-', ' Mischsystem', '- Marken-Rot statt Blau', '', 'Co-Authored-By: Claude Opus 4.8 ', ].join('\n')), ]) assert.deepEqual(bullets.details, [ 'Echte Betriebsfotos statt Stock: Luftbild als Hero, Galerie mit rotem Lackier-Fahrzeug, Farbtonanalyse-Mischsystem', 'Marken-Rot statt Blau', ]) const [paragraphs] = customerChanges([ commit('Planboard\n\nDas Planboard war eine Hülle - sieben Spalten\nmit "Keine Termine".\n\nZweiter Absatz.\n'), ]) assert.deepEqual(paragraphs.details, ['Das Planboard war eine Hülle - sieben Spalten mit "Keine Termine".', 'Zweiter Absatz.']) }) test('Änderung: Backticks aus Markdown verschwinden', () => { assert.deepEqual(titles([commit('Kontakt: `info@autohaus-meier.de` ergänzt')]), ['Kontakt: info@autohaus-meier.de ergänzt']) }) test('Änderung: gleiche Titel am selben Tag stehen nur einmal da, an verschiedenen Tagen zweimal', () => { const changes = customerChanges([ commit('fix\n', '2026-09-24T15:00:00Z'), commit('upload\n', '2026-09-24T09:00:00Z'), commit('fix\n', '2026-09-20T09:00:00Z'), ]) assert.deepEqual(changes.map((c) => c.date), ['2026-09-24T15:00:00Z', '2026-09-20T09:00:00Z']) }) /* ------------------------------------------------------ Ticketstatus --- */ test('Status: kein Status aus dem Ticketsystem kommt unübersetzt beim Kunden an', () => { const woms = ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info', 'In Test', 'Halted', 'Aborted', 'Cancelled', 'Closed'] for (const status of woms) { const { label, step } = customerTicketStatus(status) assert.notEqual(label, status, status) assert.ok(Number.isInteger(step) && step >= 0 && step <= 4, `${status}: step ${step}`) } }) test('Status: Fortschritt und Ton passen zur Bedeutung', () => { assert.deepEqual(customerTicketStatus('Open'), { label: 'Eingegangen', tone: 'new', step: 1 }) assert.deepEqual(customerTicketStatus('Occupied'), { label: 'In Arbeit', tone: 'active', step: 2 }) assert.deepEqual(customerTicketStatus('Awaiting'), { label: 'Wartet auf deine Rückmeldung', tone: 'waiting', step: 2 }) assert.deepEqual(customerTicketStatus('In Test'), { label: 'Wird geprüft', tone: 'review', step: 3 }) assert.deepEqual(customerTicketStatus('Closed'), { label: 'Erledigt', tone: 'done', step: 4 }) assert.deepEqual(customerTicketStatus('Cancelled'), { label: 'Storniert', tone: 'stopped', step: 0 }) }) test('Status: leer heißt "Eingegangen", Unbekanntes wird nie roh durchgereicht', () => { assert.equal(customerTicketStatus('').label, 'Eingegangen') assert.equal(customerTicketStatus(undefined).label, 'Eingegangen') assert.deepEqual(customerTicketStatus('Zugesagt'), { label: 'In Bearbeitung', tone: 'active', step: 2 }) }) /* ------------------------------------------------- Auftrag / Akquise --- */ test('Auftrag: Akquise-Tickets sind intern, auch wenn nur der Betreff so beginnt', () => { assert.equal(isInternalTicket({ type: 'Akquise', topic: 'Akquise: Mover GmbH' }), true) assert.equal(isInternalTicket({ type: 'akquise', topic: 'Lead' }), true) assert.equal(isInternalTicket({ type: 'Webpage', topic: 'Akquise: Mover GmbH' }), true) assert.equal(isInternalTicket({ type: 'Webpage', topic: 'Relaunch der Website' }), false) }) test('Auftrag: Nummer, Betreff und übersetzter Status; intern -> null', () => { assert.deepEqual(customerOrder({ woid: '100004', topic: 'Demo Webseite', status: 'Closed', type: 'Webpage' }), { number: '100004', topic: 'Demo Webseite', status: { label: 'Erledigt', tone: 'done', step: 4 }, }) assert.equal(customerOrder({ woid: '100015', topic: 'Akquise: Mover GmbH', status: 'Open', type: 'Akquise' }), null) assert.equal(customerOrder(null), null) }) /* --------------------------------------------------- Arbeitseinträge --- */ // Worksheet wie gitWorksheet.js ihn beim Push anlegt const gitSheet = { serviceType: 'GIT', employeeName: 'knso', employeeShort: 'GIT', totalTime: 0, startTime: '', startDate: '24.09.2026', endDate: '24.09.2026', endTime: '1405', isComment: true, details: [ 'Git Push auf `knso/ff-lack` (Branch: main)', 'Commit: 88d3c37', '', '- [88d3c37](https://git.webklar.com/knso/ff-lack/commit/88d3c37ab7b15337772cf59a5a9294ec0cc8cca4): Hintergrundbilder leicht rot einfaerben (knso)', ' Dezenter roter Tint auf den Hintergrundbildern', ' Co-Authored-By: Claude Opus 4.8 ', '- [1234567](https://git.webklar.com/knso/ff-lack/commit/1234567890abcdef1234567890abcdef12345678): Update preview config from ticket system (knso)', '- [2ca14eb](https://git.webklar.com/knso/ff-lack/commit/2ca14ebd93d53c45dee69e1beb8e668c4ce2a016): Anreicherung: Bilder, Logo, Text-Feinarbeit (Cloud-Routine) (webklar-automation)', ].join('\n'), } test('Arbeit: Git-Push wird zu "Website aktualisiert" mit bereinigten Änderungstiteln', () => { assert.deepEqual(customerWorkEntries([gitSheet], { includeUpdates: true }), [ { kind: 'update', label: 'Website aktualisiert', date: '2026-09-24', minutes: 0, changes: ['Hintergrundbilder leicht rot einfaerben', 'Inhalte ergänzt: Bilder, Logo, Text-Feinarbeit'], }, ]) }) test('Arbeit: mehrere Pushes am selben Tag werden ein Eintrag, andere Tage bleiben getrennt', () => { const push = (day, sha, title, totalTime = 0) => ({ ...gitSheet, endDate: day, totalTime, details: `Git Push auf \`knso/ff-lack\` (Branch: main)\nCommit: ${sha}\n\n- [${sha}](https://git.webklar.com/knso/ff-lack/commit/${sha}): ${title} (knso)`, }) const entries = customerWorkEntries( [ push('24.09.2026', 'aaaaaaa', 'Handy-Ansicht überarbeitet', 15), push('24.09.2026', 'bbbbbbb', 'Trust-Icons repariert', 30), push('23.09.2026', 'ccccccc', 'Anfahrt ergänzt'), ], { includeUpdates: true } ) assert.deepEqual(entries, [ { kind: 'update', label: 'Website aktualisiert', date: '2026-09-24', minutes: 45, changes: ['Handy-Ansicht überarbeitet', 'Trust-Icons repariert'] }, { kind: 'update', label: 'Website aktualisiert', date: '2026-09-23', minutes: 0, changes: ['Anfahrt ergänzt'] }, ]) }) test('Arbeit: Push mit nur technischen Commits verschwindet ganz', () => { const onlyConfig = { ...gitSheet, details: 'Git Push auf `knso/ff-lack` (Branch: main)\nCommit: 1234567\n\n- [1234567](https://git.webklar.com/x/y/commit/1234567): Update preview config from ticket system (knso)', } const noDetails = { ...gitSheet, details: 'Git Push auf `knso/ff-lack` (Branch: main)\nCommit: ?\n\n- (keine Commit-Details im Webhook)' } assert.deepEqual(customerWorkEntries([onlyConfig, noDetails], { includeUpdates: true }), []) }) test('Arbeit: ohne includeUpdates bleiben nur Git-Einträge mit nachgetragener Zeit', () => { const withTime = { ...gitSheet, totalTime: 45, startTime: '1320' } const entries = customerWorkEntries([gitSheet, withTime], { includeUpdates: false }) assert.deepEqual(entries.map((e) => [e.kind, e.minutes]), [['update', 45]]) }) test('Arbeit: manuelle Einträge bekommen deutsche Art, Person, Zeit und Text', () => { const sheet = (serviceType, extra = {}) => ({ serviceType, employeeName: 'Kenso Grimm', totalTime: 150, startDate: '20.09.2026', endDate: '20.09.2026', details: 'Website neu erstellt und als Vorschau bereitgestellt.', isComment: false, ...extra, }) assert.deepEqual(customerWorkEntries([sheet('Remote')]), [ { kind: 'online', label: 'Online erledigt', date: '2026-09-20', minutes: 150, person: 'Kenso Grimm', text: 'Website neu erstellt und als Vorschau bereitgestellt.', }, ]) assert.deepEqual( customerWorkEntries([sheet('On Site'), sheet('Off Site'), sheet('Sonderwunsch')]).map((e) => [e.kind, e.label]), [['onsite', 'Vor Ort bei dir'], ['office', 'In unserem Büro'], ['work', 'Arbeit am Projekt']] ) }) test('Arbeit: interne Kommentare tauchen nicht auf', () => { const comment = { serviceType: 'Remote', employeeName: 'Kenso Grimm', totalTime: 0, endDate: '20.09.2026', details: 'Roadmap-Knoten "Erstellen": Offen -> In Arbeit', isComment: true } const commentType = { serviceType: 'COMMENT', employeeName: 'Kenso Grimm', totalTime: 0, endDate: '20.09.2026', details: 'intern', isComment: false } assert.deepEqual(customerWorkEntries([comment, commentType], { includeUpdates: true }), []) }) test('Arbeit: Einträge aus getCustomerActivity (date statt endDate, Zeit als Text) werden verstanden', () => { const fromActivity = { serviceType: 'Remote', employeeName: 'Andrej Stevanovski', totalTime: '30', date: '18.09.2026', details: 'Demo-Version erstellt', isGit: false, pending: false } assert.deepEqual(customerWorkEntries([fromActivity]), [ { kind: 'online', label: 'Online erledigt', date: '2026-09-18', minutes: 30, person: 'Andrej Stevanovski', text: 'Demo-Version erstellt' }, ]) }) /* ---------------------------------------------------- Unterstützung --- */ const andrej = { employeeId: 'andrej', name: 'Andrej Stevanovski', task: 'Farbkonzept' } const nico = { employeeId: 'nico', name: 'Nico Rech', task: 'Texte' } const workSheet = (extra = {}) => ({ serviceType: 'Remote', employeeName: 'Justin Klein', totalTime: 60, startDate: '30.09.2026', endDate: '30.09.2026', details: 'Startseite umgebaut', isComment: false, ...extra, }) test('Unterstützung: eigene Zeile mit Vorname, Tätigkeit und halber Zeit', () => { const [entry] = customerWorkEntries([workSheet({ supporters: JSON.stringify([andrej]) })]) assert.equal(entry.minutes, 60) assert.deepEqual(entry.supports, [{ name: 'Andrej', task: 'Farbkonzept', minutes: 30 }]) }) test('Unterstützung: ohne supporters kein supports-Feld (bisherige Form bleibt)', () => { const [entry] = customerWorkEntries([workSheet()]) assert.equal('supports' in entry, false) }) test('Unterstützung: zusammengefasste Git-Tage sammeln alle Unterstützungen', () => { const push = (sha, title, sup) => ({ ...gitSheet, endDate: '24.09.2026', totalTime: 30, startTime: '1300', isComment: false, supporters: JSON.stringify([sup]), details: `Git Push auf \`knso/ff-lack\` (Branch: main)\nCommit: ${sha}\n\n- [${sha}](https://git.webklar.com/knso/ff-lack/commit/${sha}): ${title} (knso)`, }) const entries = customerWorkEntries([push('aaaaaaa', 'Handy-Ansicht', andrej), push('bbbbbbb', 'Icons', nico)], { includeUpdates: false }) assert.equal(entries.length, 1) assert.equal(entries[0].minutes, 60) assert.deepEqual(entries[0].supports, [ { name: 'Andrej', task: 'Farbkonzept', minutes: 15 }, { name: 'Nico', task: 'Texte', minutes: 15 }, ]) }) test('Unterstützung: Kommentar mit supporters erscheint nicht', () => { assert.deepEqual(customerWorkEntries([workSheet({ isComment: true, supporters: JSON.stringify([andrej]) })]), []) }) test('Unterstützung: kaputtes supporters -> Eintrag ohne Unterstützung', () => { const [entry] = customerWorkEntries([workSheet({ supporters: 'kaputt' })]) assert.equal(entry.minutes, 60) assert.equal('supports' in entry, false) }) test('workTotalMinutes: Summe aller Schritte inkl. Unterstützung', () => { const entries = customerWorkEntries([ workSheet({ supporters: JSON.stringify([andrej, nico]) }), workSheet({ totalTime: 45 }), ]) assert.equal(workTotalMinutes(entries), 60 + 30 + 30 + 45) assert.equal(workTotalMinutes([]), 0) })