Kundenansicht neu P2: Bausteine für Projektdetails und Aktivität

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 16:53:22 +00:00
1 parent 166188c069
commit e036d24ae5
4 files changed
+268 -1

No files matched your search

+6 -1
View File
@@ -2,7 +2,7 @@ import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
fmtEuro, fmtDatum, fmtDatumZeit, fmtTagMonat, fmtStd, fmtMinuten, fmtBytes, legacyRequest, meldeZaehlerNeu, merkeHinweis, nimmHinweis,
setzeChatVorgabe, nimmChatVorgabe, wechselFunktionAus, zuAnsicht, meldeTitel, K, ABO_NICHT_GELADEN, navigiere, ladeAbo, safeUrl,
setzeChatVorgabe, nimmChatVorgabe, fmtUhrzeit, berlinTag, wechselFunktionAus, zuAnsicht, meldeTitel, K, ABO_NICHT_GELADEN, navigiere, ladeAbo, safeUrl,
} from '../public/neu/ansicht-hilfen.js'
import { mit, mitDom, abo, aboAntwort, me, projekt, rechnung, nachricht, angebot, ledger, anfrageGutschrift, VORSCHAU, VORSCHAU_SPEICHER, LIMITS, neueWurzel } from './support/knDaten.js'
@@ -17,6 +17,11 @@ test('Formate: Euro mit geschütztem Leerzeichen, Daten in Berliner Zeit', () =>
assert.equal(fmtTagMonat('kaputt'), '')
})
test('Formate: Uhrzeit und Kalendertag in Berliner Zeit', () => {
assert.equal(fmtUhrzeit('2026-10-17T10:05:00Z'), '12:05'); assert.equal(fmtUhrzeit('kaputt'), '')
assert.deepEqual(berlinTag('2026-10-15T22:30:00Z'), { j: 2026, m: 10, t: 16 }); assert.equal(berlinTag(''), null)
})
test('Formate: Stunden, Minuten, Bytes', () => {
assert.deepEqual([150, 120, 45, 0].map(fmtStd), ['2,5 Std.', '2 Std.', '0,75 Std.', '0 Std.'])
assert.deepEqual([0, 45, 120, 150].map(fmtMinuten), ['', '45 min', '2 h', '2 h 30 min'])
+72
View File
@@ -0,0 +1,72 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
AUFTRAG_SCHRITTE, statusTon, tagesUeberschrift, baueAuftrag, baueAenderungen, baueArbeiten, baueDateien, baueAktivitaet, ladeProjektDetails, baueProjektDetails,
} from '../public/neu/website-details.js'
import { mit, mitDom } from './support/knDaten.js'
test('Tagesüberschrift im Berliner Tag', () => {
const jetzt = new Date('2026-10-16T09:00:00Z')
assert.deepEqual(['2026-10-15T22:30:00Z', '2026-10-15T20:00:00Z', '2026-10-14T10:00:00Z', '2025-12-24T10:00:00Z'].map((d) => tagesUeberschrift(new Date(d), jetzt)),
['Heute', 'Gestern', 'Mittwoch, 14. Oktober', 'Mittwoch, 24. Dezember 2025'])
})
test('statusTon', () => assert.deepEqual(['new', 'active', 'waiting', 'review', 'done', 'paused', 'stopped', '?'].map(statusTon), ['grau', 'blau', 'orange', 'violett', 'gruen', 'grau', 'rot', 'blau']))
test('Änderungen: nach Tagen, fünf sichtbar, Rest hinter „weitere“, Titel bleibt Text', () => mitDom((fx) => {
const tag = (d, n, p) => Array.from({ length: n }, (_, i) => ({ date: `2026-10-${d}T0${i + 6}:00:00.000Z`, title: `${p} ${i}`, details: [] }))
const el = baueAenderungen([...tag(15, 4, 'Neu'), ...tag(14, 3, 'Alt'), { date: '2026-10-13T06:00:00.000Z', title: '<img src=x onerror=alert(1)>', details: [] }], new Date('2026-10-16T09:00:00Z'))
assert.equal(fx.alle(el, '[data-extra]').length, 3)
assert.equal(fx.finde(el, '[data-mehr]').textContent, '3 weitere Änderungen anzeigen')
assert.equal(fx.finde(el, 'img'), null)
fx.klick(fx.finde(el, '[data-mehr]')); assert.ok(fx.finde(el, '[data-einklappbar]').classList.contains('is-open')); assert.equal(fx.finde(el, '[data-mehr]').textContent, 'Weniger anzeigen')
}))
test('Arbeiten und Dateien: Zähltexte, Leertexte, Ordner mit Schrägstrich', () => mitDom((fx) => {
const e = (i) => ({ date: '2026-10-1' + i, kind: 'work', label: 'Arbeit', minutes: 15, person: 'Kenso' })
assert.equal(fx.finde(baueArbeiten([1, 2, 3, 4, 5].map(e), 'leer'), '[data-mehr]').textContent, '1 weiteren Eintrag anzeigen')
assert.equal(baueArbeiten([], 'Noch keine Arbeiten.').textContent, 'Noch keine Arbeiten.')
const d = baueDateien({ totalFiles: 3, totalSizeBytes: 1536, truncated: true, topLevel: [{ path: 'img', type: 'dir', fileCount: 2, sizeBytes: 1024 }, { path: 'index.html', type: 'file', fileCount: 1, sizeBytes: 512 }] })
assert.match(d.textContent, /3 Dateien · 1,5 KB \(Übersicht unvollständig – sehr großes Projekt\)/); assert.match(d.textContent, /img\//)
assert.equal(baueDateien({ totalFiles: 0 }).textContent, 'Keine Datei-Informationen verfügbar.')
}))
test('Projektdetails: Teilausfall ersetzt nur den betroffenen Abschnitt', () => mitDom(() => {
const el = baueProjektDetails({ git: null, files: { totalFiles: 0 }, work: { order: null, entries: [] } }, new Date())
assert.match(el.textContent, /Die Änderungen können gerade nicht geladen werden\./); assert.match(el.textContent, /Keine Datei-Informationen verfügbar\./)
}))
test('Auftrag: Nummer, Betreff, Status, Fortschritt, Hinweis bei Rückfrage', () => mitDom((fx) => {
assert.deepEqual(AUFTRAG_SCHRITTE, ['Eingegangen', 'In Arbeit', 'Prüfung', 'Erledigt'])
const el = baueAuftrag({ number: '100123', topic: 'Neue Startseite', status: { label: 'Wartet auf dich', tone: 'waiting', step: 2 } }, 150)
assert.match(el.textContent, /Auftrag 100123/); assert.match(el.textContent, /Neue Startseite/); assert.match(el.textContent, /Wartet auf dich/)
assert.match(el.textContent, /Arbeitszeit gesamt: 2 h 30 min/)
const punkte = fx.alle(el, 'li'); assert.equal(punkte.length, 4)
assert.ok(punkte[0].classList.contains('is-done')); assert.ok(punkte[1].classList.contains('is-current')); assert.equal(punkte[1].getAttribute('aria-current'), 'step'); assert.ok(!punkte[2].classList.contains('is-done'))
assert.match(el.textContent, /Wir brauchen noch eine Info von dir\./)
const chat = fx.finde(el, 'a'); assert.deepEqual([chat.getAttribute('href'), chat.getAttribute('data-view')], ['#chat', 'chat'])
const schlicht = baueAuftrag({ number: '', topic: '', status: { label: 'Neu', tone: 'new', step: 0 } })
assert.match(schlicht.textContent, /Dein Auftrag/); assert.equal(fx.alle(schlicht, 'li').length, 0); assert.doesNotMatch(schlicht.textContent, /Wir brauchen/)
}))
test('Aktivität: neueste zuerst, drei sichtbar, „Alle anzeigen (n)“ klappt auf; fremde Inhalte bleiben Text', () => mitDom((fx) => {
const t = (n, createdAt, topic) => ({ number: String(n), topic, createdAt, status: { label: 'Offen', tone: 'active', step: 2 }, entries: [] })
const el = baueAktivitaet([t(1, '2026-10-01T00:00:00Z', 'Eins'), t(4, '2026-10-04T00:00:00Z', '<img src=x onerror=alert(1)>'), t(2, '2026-10-02T00:00:00Z', 'Zwei'), t(3, '2026-10-03T00:00:00Z', 'Drei')], new Date('2026-10-16T09:00:00Z'))
assert.equal(fx.alle(el, 'article').length, 3); assert.match(el.textContent, /Auftrag 4/); assert.doesNotMatch(el.textContent, /Auftrag 1/)
assert.equal(fx.finde(el, 'img'), null)
const alle = fx.finde(el, (e) => e.textContent === 'Alle anzeigen (4)'); assert.ok(alle)
fx.klick(alle); assert.equal(fx.alle(el, 'article').length, 4); assert.ok(fx.finde(el, (e) => e.tagName === 'BUTTON' && e.textContent === 'Weniger anzeigen'))
assert.equal(baueAktivitaet([], new Date()).textContent, 'Es wurden noch keine Arbeiten dokumentiert.')
}))
test('Arbeiten: Unterstützung, Änderungsliste und Text; Zeitangaben', () => mitDom((fx) => {
const el = baueArbeiten([{ date: '2026-09-20', kind: 'update', label: 'Aktualisierung', person: 'Kenso', minutes: 90, changes: ['Menü angepasst'], supports: [{ name: 'Justin', task: 'Bilder', minutes: 15 }] },
{ date: '2026-09-21', kind: 'work', label: 'Arbeit', person: '', minutes: 0, text: 'Kurz geprüft' }], 'leer')
assert.match(el.textContent, /20\.09\.2026 · Kenso/); assert.match(el.textContent, /Menü angepasst/); assert.match(el.textContent, /↳ Unterstützung Justin: Bilder/)
assert.match(el.textContent, /1 h 30 min/); assert.match(el.textContent, /Kurz geprüft/); assert.equal(fx.finde(el, '[data-mehr]'), null)
}))
test('ladeProjektDetails: jede Quelle einzeln, Fehler ergibt null', mit({
'GET /api/projects/p%201/git': { changes: [] },
'GET /api/projects/p%201/files': () => ({ status: 500, body: { error: 'x' } }),
'GET /api/projects/p%201/work': { order: null, entries: [] },
}, async () => {
const d = await ladeProjektDetails('p 1')
assert.deepEqual(d.git, { changes: [] }); assert.equal(d.files, null); assert.deepEqual(d.work, { order: null, entries: [] })
}))