Hustler-Stufe: P4-Aktionen im Pult erst ab Stufe 4 (rowModel-Parameter stufe, PultList reicht ihn durch)

Unterhalb von Stufe 4 sind canReply, canPaste, canApprovePreview und canStartBuild immer false: Antwort des Kunden einfügen, Als Mensch antworten / Jetzt senden, Freigeben & senden
und Vorschau bauen verschwinden samt Feldern und Rückfragen, die Aktionsgruppe schließt sich ohne Lücke. P3-Aktionen (übernehmen, Kopieren & öffnen, als gesendet markieren, verwerfen,
erledigt, E-Mail schreiben) bleiben. Bestehende P4-Tests laufen ausdrücklich auf Stufe 6; neue Tests prüfen Stufe 3 gerendert und bedient (Reihenfolge der Knöpfe, kein Feld, Esc, Verwerfen mit Rückfrage).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-22 10:36:44 +00:00
1 parent 7cd74a23d2
commit 5e48fbf264
5 files changed
+207 -15

No files matched your search

+114 -4
View File
@@ -248,9 +248,11 @@ async function loadList() {
"import PultList from './src/components/workers/PultList.jsx'",
"import Brief from './src/components/workers/pult/Brief.jsx'",
"import { rowModel } from './src/lib/workersPult.js'",
'export const render = (props) => renderToStaticMarkup(createElement(PultList, props))',
'export const renderBrief = (item, { me, nowMs, employees, text, fields } = {}) => {',
' const row = rowModel(item, { me, nowMs, employees })',
// Stufen-Sperre (hustlerStufe.js): die Zusicherungen dieser Datei gelten für das volle Pult (Stufe 6, P4-Aktionen offen, Verhalten wie vor der Sperre);
// die Tests „Stufe 3“ am Ende der Datei reichen stufe: 3 durch
'export const render = (props) => renderToStaticMarkup(createElement(PultList, { stufe: 6, ...props }))',
'export const renderBrief = (item, { me, nowMs, employees, text, fields, stufe = 6 } = {}) => {',
' const row = rowModel(item, { me, nowMs, employees, stufe })',
' return renderToStaticMarkup(createElement(Brief, { api: {}, item, row, text: text ?? row.copyText, fields: fields ?? {}, onText() {}, onField() {}, refresh: async () => {}, onFinished() {}, onClose() {}, onDetails() {}, onSpaet() {} }))',
'}',
].join('\n'),
@@ -571,7 +573,7 @@ async function startList(items, api, props = {}) {
}
globalThis.__pultEmployees = [{ userId: 'u2', displayName: 'Kenso' }]
globalThis.__pultFeedFor = (statuses) => ({ items: statuses === 'offen,in_arbeit' ? items : [], loading: false, error: null, refresh: async () => { refreshes.push(statuses) } })
const mounted = __mount(jsx(PultList, { api, me: { id: 'u1', name: 'Justin' }, ...props }))
const mounted = __mount(jsx(PultList, { api, me: { id: 'u1', name: 'Justin' }, stufe: 6, ...props })) // Stufe 6 = volles Pult; Stufe 3 siehe die Tests am Ende der Datei
let current = null // Nummer des Umschlags, dessen Brief offen ist
const view = {
refreshes, timers, opened,
@@ -1269,3 +1271,111 @@ test('Pult (bedient, P4-T32): mehrere Einträge desselben Interessenten – Feld
assert.deepEqual(api.calls[1], ['k9', 'paste', { text: 'Ja, gern' }])
} finally { ui.end() }
})
// ---- Stufen-Sperre (hustlerStufe.js): Stufe 3 = Stand der Produktion, der Dienst kann P3, aber noch nicht P4. Das Pult bietet dann keine der vier P4-Aktionen an ----
// ---- (Antwort des Kunden einfügen, Als Mensch antworten / Jetzt senden, Vorschau freigeben, Vorschau bauen); die P3-Aktionen bleiben, ohne Loch und ohne Fokusfalle. ----
const P4_TEXTE = ['Antwort des Kunden einfügen', 'Als Mensch antworten', 'Jetzt senden', 'Freigeben &amp; senden', 'Wirklich freigeben?', 'Vorschau bauen', 'Wirklich bauen?', 'Vorschau ansehen', 'Einfügen', 'Text der Antwort', 'Antwort des Kunden']
const P4_MERKMALE = ['data-opens', 'aria-expanded', 'hk-feld-panel', 'hk-feld-antwort', 'hk-feld-einfuegen', 'hk-zaehler', 'hk-brief-hinweis-bau']
test('Stufe 3 (gerendert): kein Brief, keine Art und kein Status zeigt eine P4-Aktion; Stufe 6 zeigt sie weiter', async () => {
const { renderBrief } = await loadList()
const now = Date.now()
const me = { id: 'u1', name: 'Justin' }
const prospect = { $id: 'p1', name: 'Verein', city: 'Namborn', postalCode: '66640', score: 72, email: 'x@verein.example' }
const base = { $id: 'a', status: 'offen', title: 'Eintrag', draft: 'Hallo', createdAt: new Date(now).toISOString(), sourceUrl: 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3', prospect, prospectId: 'p1' }
const vorschau = { ...base, kind: 'vorschau_freigabe', sourceUrl: 'https://entwurf.project.webklar.com/verein' }
const alle = [
...['offen', 'in_arbeit', 'veraltet', 'gesendet', 'erledigt', 'verworfen'].flatMap((status) => [
{ ...base, kind: 'plattform_antwort', status }, { ...base, kind: 'mail_antwort', status }, { ...vorschau, status },
{ ...base, kind: 'unsicher', status }, { ...base, kind: 'preisfrage', status }, { ...base, kind: 'mensch_gewuenscht', status },
{ ...base, kind: 'beschwerde', status }, { ...base, kind: 'bau_fehlgeschlagen', status },
]),
{ ...base, kind: 'plattform_antwort', status: 'in_arbeit', claimedBy: 'mensch:u2' }, // von einem Kollegen übernommen
{ ...base, kind: 'plattform_antwort', status: 'gesendet', prospect: null },
]
for (const item of alle) {
const name = `${item.kind}/${item.status}${item.claimedBy ? '/Kollege' : ''}`
const html = renderBrief(item, { me, nowMs: now, employees: [], stufe: 3 })
for (const t of P4_TEXTE) assert.ok(!html.includes(t), `${name}: „${t}“`)
for (const m of P4_MERKMALE) assert.ok(!html.includes(m), `${name}: ${m}`)
assert.ok(html.includes('hk-brief'), name) // der Brief selbst steht
}
// Gegenprobe Stufe 6: dieselben Einträge zeigen die P4-Aktionen (sonst prüft die Schleife oben nichts)
const gezeigt = (item) => P4_TEXTE.filter((t) => renderBrief(item, { me, nowMs: now, employees: [], stufe: 6 }).includes(t))
assert.ok(gezeigt({ ...base, kind: 'plattform_antwort' }).includes('Antwort des Kunden einfügen'))
assert.ok(gezeigt({ ...base, kind: 'mail_antwort' }).includes('Als Mensch antworten'))
assert.ok(gezeigt(vorschau).includes('Freigeben &amp; senden'))
assert.ok(gezeigt({ ...base, kind: 'unsicher' }).includes('Vorschau bauen'))
})
test('Stufe 3 (gerendert): die P3-Aktionen je Art bleiben unverändert, Mail-Link, Anzeige öffnen und Interessent auch; abgeschlossene Einträge sagen es', async () => {
const { renderBrief } = await loadList()
const now = Date.now()
const me = { id: 'u1', name: 'Justin' }
const prospect = { $id: 'p1', name: 'Verein', email: 'x@verein.example' }
const base = { $id: 'a', status: 'offen', title: 'Eintrag', draft: 'Hallo', createdAt: new Date(now).toISOString(), sourceUrl: 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3', prospect, prospectId: 'p1' }
const brief = (item) => renderBrief(item, { me, nowMs: now, employees: [], stufe: 3 })
// Plattform-Antwort: übernehmen, Kopieren & öffnen, als gesendet markieren, verwerfen
let html = brief({ ...base, kind: 'plattform_antwort' })
for (const t of ['Kopieren &amp; öffnen', 'Ich kümmere mich', 'Als gesendet markieren', 'Verwerfen', 'Anzeige öffnen', 'Interessent']) assert.ok(html.includes(t), t)
assert.ok(!stempel(html, 'Erledigt'))
// Mail-Antwort: E-Mail schreiben (mailto) bleibt
html = brief({ ...base, kind: 'mail_antwort', sourceUrl: '' })
assert.match(html, /<a class="hk-stempel hk-stempel-haupt" href="mailto:x@verein\.example\?[^"]*"><span class="hk-ic hk-ic-art-mail" aria-hidden="true"><\/span>E-Mail schreiben<\/a>/)
assert.ok(html.includes('Ich kümmere mich')); assert.ok(html.includes('Als gesendet markieren')); assert.ok(html.includes('Verwerfen'))
// nicht sendbare Arten: Erledigt und Verwerfen; „unsicher“ bietet kein Bauen mehr
for (const kind of ['unsicher', 'preisfrage', 'mensch_gewuenscht', 'beschwerde', 'bau_fehlgeschlagen']) {
html = brief({ ...base, kind })
assert.ok(stempel(html, 'Erledigt'), kind); assert.ok(html.includes('Verwerfen'), kind); assert.ok(html.includes('Ich kümmere mich'), kind); assert.ok(!html.includes('Als gesendet markieren'), kind)
}
// Vorschau-Freigabe ohne P4: übernehmen und verwerfen (Erledigt gibt es dort nie), mit dem Hinweis des Verwerfens
html = brief({ ...base, kind: 'vorschau_freigabe', sourceUrl: 'https://entwurf.project.webklar.com/verein' })
assert.ok(html.includes('Ich kümmere mich')); assert.ok(html.includes('Verwerfen')); assert.ok(!stempel(html, 'Erledigt')); assert.ok(html.includes('„Verwerfen“ sendet die Vorschau nicht.'))
// abgeschlossen: keine Aktionsgruppe, auch nicht für „gesendet mit Interessent“ (dort stand bisher nur „Antwort des Kunden einfügen“)
html = brief({ ...base, kind: 'plattform_antwort', status: 'gesendet' })
assert.ok(!html.includes('hk-aktionen')); assert.ok(html.includes('Dieser Eintrag ist abgeschlossen.')); assert.ok(html.includes('<h3 class="hk-brief-h">Entwurf</h3>'))
})
test('Stufe 3 (bedient): das Pult arbeitet mit den P3-Aktionen, ohne P4-Knöpfe, ohne Loch in der Aktionsgruppe und ohne Fokusfalle', async () => {
const api = scriptedApi(Promise.resolve({}), Promise.resolve({}), Promise.resolve({}))
const ui = await startList([{ ...unsicher, $id: 'k1', kind: 'plattform_antwort', title: 'Suche Webdesigner', draft: 'Claras Entwurf', sourceUrl: 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3', prospect: { $id: 'p1', name: 'Verein' } }], api, { stufe: 3 })
try {
await ui.open(0)
const aktionen = nodes(ui.brief(), (n) => n.tag === 'div' && n.props.role === 'group' && hasClass(n, 'hk-aktionen'))[0]
assert.ok(aktionen)
// die Gruppe enthält genau die P3-Knöpfe, in dieser Reihenfolge, ohne leere Plätze
const beschriftung = aktionen.children.filter((c) => c !== null && c !== false).map((c) => textOf(c).trim())
assert.deepEqual(beschriftung, ['Kopieren & öffnen', 'Ich kümmere mich', 'Als gesendet markieren', 'Verwerfen'])
assert.equal(aktionen.children.length, aktionen.children.filter((c) => c !== null && c !== false).length)
// nichts im Brief öffnet ein Feld; kein Textfeld außer dem Entwurf
assert.equal(nodes(ui.brief(), (n) => n.props && (n.props['data-opens'] !== undefined || n.props['aria-expanded'] !== undefined)).length, 0)
assert.equal(nodes(ui.brief(), (n) => n.tag === 'textarea').length, 1)
for (const label of ['Antwort des Kunden einfügen', 'Als Mensch antworten', 'Jetzt senden', 'Freigeben & senden', 'Vorschau bauen']) assert.equal(ui.has(label), false, label)
// Esc schließt den Brief (es gibt kein Feld, das zuerst schließen müsste), Umschlag bleibt bedienbar
const esc = await ui.briefKey('Escape'); assert.equal(esc.defaultPrevented, true); assert.equal(ui.brief(), null)
// P3 läuft: erster Klick auf Verwerfen fragt, der zweite verwirft; es geht nur discard hinaus
await ui.click('Verwerfen')
assert.deepEqual(ui.disabled('Wirklich verwerfen?'), [false]); assert.equal(api.calls.length, 0)
await ui.click('Wirklich verwerfen?')
assert.deepEqual(api.calls, [['k1', 'discard', {}]])
assert.deepEqual(ui.messages(), [{ kind: 'ok', text: '„Suche Webdesigner“ verworfen.', role: 'status' }])
} finally { ui.end() }
})
test('Stufe 3 (bedient): „unsicher“ und „Vorschau freigeben“ zeigen kein Bauen und kein Freigeben; Erledigt und Verwerfen bleiben; die Zahl der Umschläge bleibt gleich', async () => {
const api = scriptedApi()
const vorschau = { ...unsicher, $id: 'v1', kind: 'vorschau_freigabe', title: 'Vorschau Verein', sourceUrl: 'https://entwurf.project.webklar.com/verein' }
const ui = await startList([unsicher, vorschau], api, { stufe: 3 })
try {
assert.equal(envelopes(ui.tree()).length, 2)
await ui.open(0)
assert.deepEqual(['Erledigt', 'Verwerfen', 'Ich kümmere mich'].map((l) => ui.has(l)), [true, true, true])
assert.deepEqual(['Vorschau bauen', 'Als Mensch antworten', 'Antwort des Kunden einfügen'].map((l) => ui.has(l)), [false, false, false])
await ui.close()
await ui.open(1)
assert.deepEqual(['Verwerfen', 'Ich kümmere mich'].map((l) => ui.has(l)), [true, true])
assert.deepEqual(['Freigeben & senden', 'Vorschau ansehen', 'Erledigt'].map((l) => ui.has(l)), [false, false, false])
assert.equal(api.calls.length, 0)
} finally { ui.end() }
})