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:
1 parent
166188c069
commit
e036d24ae5
4 files changed
+268
-1
No files matched your search
@@ -72,6 +72,18 @@ export function fmtTagMonat(wert) {
|
||||
return t ? `${t.day}.${t.month}.` : ''
|
||||
}
|
||||
|
||||
/** 'HH:MM' in Berliner Zeit; ungültig ''. */
|
||||
export function fmtUhrzeit(wert) {
|
||||
const t = berlinTeile(wert, { hour: '2-digit', minute: '2-digit' })
|
||||
return t ? `${t.hour}:${t.minute}` : ''
|
||||
}
|
||||
|
||||
/** Kalendertag in Berliner Zeit als { j, m, t } (Zahlen); ungültig null. */
|
||||
export function berlinTag(wert) {
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
return t ? { j: Number(t.year), m: Number(t.month), t: Number(t.day) } : null
|
||||
}
|
||||
|
||||
/** Minuten → '2,5 Std.' | '2 Std.' | '0,75 Std.' (Komma, höchstens zwei Stellen). */
|
||||
export const fmtStd = (minuten) => `${ZAHL_STD.format(Math.round(((Number(minuten) || 0) / 60) * 100) / 100)} Std.`
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/* Kundenansicht neu (P2): Bausteine der Projektdetails und der Aktivität (Auftrag, Änderungen, Arbeiten, Dateien).
|
||||
Alles über h() und Textknoten, nie als HTML. Kein DOM-Zugriff beim Import. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h } from './dom.js'
|
||||
import { K, zuAnsicht, fmtBytes, fmtMinuten, fmtUhrzeit, berlinTag } from './ansicht-hilfen.js'
|
||||
|
||||
export const AUFTRAG_SCHRITTE = ['Eingegangen', 'In Arbeit', 'Prüfung', 'Erledigt']
|
||||
const ANZAHL_AENDERUNGEN = 5
|
||||
const ANZAHL_EINTRAEGE = 4
|
||||
const ANZAHL_AKTIVITAET = 3
|
||||
const TON = { new: 'grau', active: 'blau', waiting: 'orange', review: 'violett', done: 'gruen', paused: 'grau', stopped: 'rot' }
|
||||
|
||||
/** Farbton der Status-Pille zum Ton des Servers. */
|
||||
export const statusTon = (ton) => TON[ton] ?? 'blau'
|
||||
|
||||
const tagNummer = (b) => Math.floor(Date.UTC(b.j, b.m - 1, b.t) / 86400000)
|
||||
|
||||
/** 'Heute' | 'Gestern' | 'Mittwoch, 14. Oktober' (mit Jahr, wenn es nicht das laufende ist); Kalendertag in Berliner Zeit. */
|
||||
export function tagesUeberschrift(datum, jetzt = new Date()) {
|
||||
const tag = berlinTag(datum)
|
||||
const heute = berlinTag(jetzt)
|
||||
if (!tag || !heute) return ''
|
||||
const diff = tagNummer(heute) - tagNummer(tag)
|
||||
if (diff === 0) return 'Heute'
|
||||
if (diff === 1) return 'Gestern'
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone: 'Europe/Berlin', weekday: 'long', day: 'numeric', month: 'long', ...(tag.j === heute.j ? {} : { year: 'numeric' }),
|
||||
}).format(datum)
|
||||
}
|
||||
|
||||
// '2026-09-20' → '20.09.2026' (reines Datum ohne Zeitzonen-Umrechnung)
|
||||
function isoTag(tag) {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(tag || ''))
|
||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
|
||||
}
|
||||
|
||||
const leerText = (text) => h('p', { class: 'kn-hinweis' }, text)
|
||||
|
||||
/** Knopf "n weitere … anzeigen" / "Weniger anzeigen"; schaltet is-open am Wrapper [data-einklappbar]. */
|
||||
function mehrKnopf(gesamt, sichtbar, [eins, viele]) {
|
||||
const rest = gesamt - sichtbar
|
||||
if (rest <= 0) return null
|
||||
const beschriftung = `${rest} ${rest === 1 ? eins : viele} anzeigen`
|
||||
const knopf = h('button', { type: 'button', class: 'kn-textknopf kn-mehr', 'data-mehr': true }, beschriftung)
|
||||
knopf.addEventListener('click', () => {
|
||||
const offen = knopf.closest('[data-einklappbar]').classList.toggle('is-open')
|
||||
knopf.textContent = offen ? 'Weniger anzeigen' : beschriftung
|
||||
})
|
||||
return knopf
|
||||
}
|
||||
|
||||
const einklappbar = (...kinder) => h('div', { class: 'kn-einklappbar', 'data-einklappbar': true }, ...kinder)
|
||||
|
||||
/** Auftrag: Nummer, Betreff, Status, Fortschritt Eingegangen → Erledigt, bei Rückfrage der Hinweis zum Chat. */
|
||||
export function baueAuftrag(order, gesamtMinuten = 0) {
|
||||
const status = order?.status ?? { label: '', tone: 'new', step: 0 }
|
||||
const schritte = status.step > 0
|
||||
? h('ol', { class: 'kn-schritte', 'aria-label': 'Fortschritt des Auftrags' }, AUFTRAG_SCHRITTE.map((label, i) => {
|
||||
const n = i + 1
|
||||
return h('li', { class: n < status.step ? 'is-done' : n === status.step ? 'is-current' : null, 'aria-current': n === status.step ? 'step' : null }, label)
|
||||
}))
|
||||
: null
|
||||
const hinweis = status.tone === 'waiting'
|
||||
? h('p', { class: 'kn-auftrag-hinweis' }, 'Wir brauchen noch eine Info von dir. ', h('a', zuAnsicht('chat'), 'Im Chat antworten'))
|
||||
: null
|
||||
return h('div', { class: `kn-auftrag kn-ton-${statusTon(status.tone)}` },
|
||||
h('div', { class: 'kn-auftrag-kopf' },
|
||||
h('div', { class: 'kn-auftrag-name' },
|
||||
order?.number ? h('span', { class: 'kn-auftrag-nr' }, `Auftrag ${order.number}`) : null,
|
||||
h('h3', null, order?.topic || 'Dein Auftrag')),
|
||||
h('span', { class: K.pille[statusTon(status.tone)] }, status.label)),
|
||||
gesamtMinuten > 0 ? h('p', { class: 'kn-auftrag-gesamt' }, 'Arbeitszeit gesamt: ', h('strong', null, fmtMinuten(gesamtMinuten))) : null,
|
||||
schritte, hinweis)
|
||||
}
|
||||
|
||||
/** Letzte Änderungen nach Tagen gruppiert; die ersten fünf sind sichtbar. */
|
||||
export function baueAenderungen(changes, jetzt = new Date()) {
|
||||
const liste = Array.isArray(changes) ? changes : []
|
||||
if (!liste.length) return leerText('Noch keine Änderungen an der Website.')
|
||||
const gruppen = []
|
||||
liste.forEach((c, i) => {
|
||||
const datum = new Date(c.date)
|
||||
const tag = Number.isNaN(datum.getTime()) ? '' : tagesUeberschrift(datum, jetzt)
|
||||
if (!gruppen.length || gruppen.at(-1).tag !== tag) gruppen.push({ tag, erster: i, eintraege: [] })
|
||||
gruppen.at(-1).eintraege.push({ c, i })
|
||||
})
|
||||
const zeile = ({ c, i }, ganzeGruppeVersteckt) => {
|
||||
const zeit = fmtUhrzeit(c.date)
|
||||
return h('li', { class: 'kn-aenderung', 'data-extra': !ganzeGruppeVersteckt && i >= ANZAHL_AENDERUNGEN },
|
||||
h('div', { class: 'kn-aenderung-kopf' }, h('strong', null, c.title), zeit ? h('time', { datetime: c.date }, zeit) : null),
|
||||
c.details?.length ? h('ul', { class: 'kn-aenderung-details' }, c.details.map((t) => h('li', null, t))) : null)
|
||||
}
|
||||
return einklappbar(
|
||||
h('div', { class: 'kn-zeitleiste' }, gruppen.map((g) => {
|
||||
const versteckt = g.erster >= ANZAHL_AENDERUNGEN
|
||||
return h('div', { class: 'kn-tag', 'data-extra': versteckt },
|
||||
g.tag ? h('p', { class: 'kn-tag-name' }, g.tag) : null,
|
||||
h('ul', { class: 'kn-aenderungen' }, g.eintraege.map((e) => zeile(e, versteckt))))
|
||||
})),
|
||||
mehrKnopf(liste.length, ANZAHL_AENDERUNGEN, ['weitere Änderung', 'weitere Änderungen']))
|
||||
}
|
||||
|
||||
function arbeitsZeile(e, extra) {
|
||||
const meta = [isoTag(e.date), e.person].filter(Boolean).join(' · ')
|
||||
const koerper = e.changes?.length
|
||||
? h('ul', { class: 'kn-eintrag-aenderungen' }, e.changes.map((t) => h('li', null, t)))
|
||||
: e.text ? h('p', { class: 'kn-eintrag-text' }, e.text) : null
|
||||
return h('li', { class: `kn-eintrag kn-art-${String(e.kind || 'work').replace(/[^a-z-]/gi, '')}`, 'data-extra': extra },
|
||||
h('div', { class: 'kn-eintrag-haupt' },
|
||||
h('div', { class: 'kn-eintrag-kopf' }, h('strong', null, e.label), meta ? h('span', { class: 'kn-eintrag-meta' }, meta) : null),
|
||||
koerper,
|
||||
e.supports?.length ? h('ul', { class: 'kn-unterstuetzung' }, e.supports.map((x) => h('li', null,
|
||||
h('span', null, `↳ Unterstützung ${x.name}: ${x.task}`), h('span', { class: 'kn-zeit', title: 'Angerechnete Zeit der Unterstützung' }, fmtMinuten(x.minutes))))) : null),
|
||||
e.minutes ? h('span', { class: 'kn-zeit', title: 'Arbeitszeit' }, fmtMinuten(e.minutes)) : null)
|
||||
}
|
||||
|
||||
/** Arbeiten am Projekt; vier sichtbar, der Rest hinter „weitere Einträge“. */
|
||||
export function baueArbeiten(entries, leerTextWert) {
|
||||
const liste = Array.isArray(entries) ? entries : []
|
||||
if (!liste.length) return leerText(leerTextWert)
|
||||
return einklappbar(
|
||||
h('ul', { class: 'kn-arbeiten' }, liste.map((e, i) => arbeitsZeile(e, i >= ANZAHL_EINTRAEGE))),
|
||||
mehrKnopf(liste.length, ANZAHL_EINTRAEGE, ['weiteren Eintrag', 'weitere Einträge']))
|
||||
}
|
||||
|
||||
/** Datei-Übersicht: Summe und Tabelle der obersten Ebene (Ordner mit Schrägstrich). */
|
||||
export function baueDateien(files) {
|
||||
if (!files || !files.totalFiles) return leerText('Keine Datei-Informationen verfügbar.')
|
||||
const zeilen = (files.topLevel || []).map((eintrag) => h('tr', null,
|
||||
h('td', null, eintrag.type === 'dir' ? `${eintrag.path}/` : eintrag.path), h('td', { class: 'kn-num' }, eintrag.fileCount), h('td', { class: 'kn-num' }, fmtBytes(eintrag.sizeBytes))))
|
||||
return h('div', { class: 'kn-dateien' },
|
||||
h('p', { class: 'kn-notiz' }, `${files.totalFiles} Dateien · ${fmtBytes(files.totalSizeBytes)}${files.truncated ? ' (Übersicht unvollständig – sehr großes Projekt)' : ''}`),
|
||||
h('div', { class: 'kn-tabelle' }, h('table', null,
|
||||
h('thead', null, h('tr', null, h('th', null, 'Ordner / Datei'), h('th', null, 'Dateien'), h('th', null, 'Größe'))),
|
||||
h('tbody', null, zeilen))))
|
||||
}
|
||||
|
||||
/** Aktivität: je Auftrag Kopf und Arbeiten, die drei neuesten sichtbar, „Alle anzeigen (n)“ zeigt den Rest. */
|
||||
export function baueAktivitaet(tickets, jetzt = new Date()) {
|
||||
const liste = Array.isArray(tickets) ? tickets : []
|
||||
if (!liste.length) return leerText('Es wurden noch keine Arbeiten dokumentiert.')
|
||||
const sortiert = [...liste].sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')))
|
||||
const behaelter = h('div', { class: 'kn-aktivitaet' })
|
||||
const zeichne = (alle) => {
|
||||
const knopf = sortiert.length > ANZAHL_AKTIVITAET
|
||||
? h('button', { type: 'button', class: K.knopf, on: { click: () => zeichne(!alle) } }, alle ? 'Weniger anzeigen' : `Alle anzeigen (${sortiert.length})`)
|
||||
: null
|
||||
behaelter.replaceChildren(
|
||||
...sortiert.slice(0, alle ? sortiert.length : ANZAHL_AKTIVITAET).map((t) => h('article', { class: 'kn-auftrag-karte' }, baueAuftrag(t), baueArbeiten(t.entries, 'Noch keine Arbeiten dokumentiert.'))),
|
||||
...(knopf ? [knopf] : []))
|
||||
}
|
||||
zeichne(false)
|
||||
return behaelter
|
||||
}
|
||||
|
||||
/** Die drei Quellen der Projektdetails; jede einzeln, ein Fehler ergibt null. */
|
||||
export async function ladeProjektDetails(projectId, apiFn = api) {
|
||||
const id = encodeURIComponent(projectId)
|
||||
const [git, files, work] = await Promise.all(['git', 'files', 'work'].map((teil) => apiFn(`/api/projects/${id}/${teil}`).catch(() => null)))
|
||||
return { git, files, work }
|
||||
}
|
||||
|
||||
const abschnitt = (titel, inhalt) => h('section', { class: 'kn-sektion' }, h('h2', { class: K.abschnitt }, titel), inhalt)
|
||||
|
||||
/** Auftrag, Änderungen, Arbeiten und Dateien; fehlt eine Quelle, ersetzt ein Fehlertext nur ihren Abschnitt. */
|
||||
export function baueProjektDetails({ git, files, work }, jetzt = new Date()) {
|
||||
const order = work?.order
|
||||
return h('div', { class: 'kn-details' },
|
||||
order ? h('section', { class: 'kn-sektion' }, baueAuftrag(order, work?.totalMinutes || 0)) : null,
|
||||
abschnitt('Letzte Änderungen', git ? baueAenderungen(git.changes || [], jetzt) : leerText('Die Änderungen können gerade nicht geladen werden.')),
|
||||
order || !work
|
||||
? abschnitt('Arbeiten am Projekt', work
|
||||
? baueArbeiten(work.entries || [], 'Noch keine Arbeitszeit eingetragen. Was sich an der Website geändert hat, steht unter „Letzte Änderungen“.')
|
||||
: leerText('Die Arbeiten können gerade nicht geladen werden.'))
|
||||
: null,
|
||||
abschnitt('Dateien', files ? baueDateien(files) : leerText('Die Datei-Übersicht kann gerade nicht geladen werden.')))
|
||||
}
|
||||
@@ -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'])
|
||||
|
||||
@@ -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: [] })
|
||||
}))
|
||||
Reference in new issue
Block a user