Der Server bereitet Commits, Tickets und Arbeitseinträge für Kunden auf (server/services/customerProjectView.js), die Rohdaten bleiben intern: - keine Co-Authored-By-/Signed-off-by-Zeilen und KI-Hinweise, keine Commit-Kürzel, Kontonamen, Git-Links oder Branch - technische Commits (Preview-Config, Merges) ausgeblendet, bekannte Automatik-Texte übersetzt, "fix"/"upload" -> "Website aktualisiert" - Ticketstatus und Arbeitsarten auf Deutsch (Open -> Eingegangen, Remote -> Online erledigt, ...), Fortschritt in 4 Schritten - Akquise-Tickets auch in den Projektdetails verborgen - /api/projects ohne Gitea-URL, Repo- und Vorlagennamen Darstellung: Auftragskarte mit Status und Fortschrittsleiste, Zeitleiste "Letzte Änderungen" nach Tagen, Arbeiten mit Farbe/Symbol je Art und Arbeitszeit; der Tab "Fortschritt" nutzt dieselben Bausteine. In der App-Ansicht öffnen sich die Projektdetails als eigene Seite mit Zurück zu "Projekte". Tests: npm test (node --test, 22 Fälle). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
13 KiB
JavaScript
312 lines
13 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import {
|
||
customerChanges,
|
||
customerTicketStatus,
|
||
isInternalTicket,
|
||
customerOrder,
|
||
customerWorkEntries,
|
||
} 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 <noreply@anthropic.com>\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 <copilot@github.com>',
|
||
'Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>',
|
||
'Signed-off-by: Kenso Grimm <kenso@webklar.com>',
|
||
'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 <noreply@anthropic.com>',
|
||
].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 <noreply@anthropic.com>',
|
||
'- [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' },
|
||
])
|
||
})
|