Projektdetails und Fortschritt kundenverständlich

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>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 18:46:22 +00:00
1 parent e54f97144c
commit ba9916cfe6
9 files changed
+932 -138

No files matched your search

+311
View File
@@ -0,0 +1,311 @@
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' },
])
})