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:
1 parent
7cd74a23d2
commit
5e48fbf264
5 files changed
+207
-15
No files matched your search
@@ -7,7 +7,8 @@
|
||||
//
|
||||
// Props: api (Dienst-API), me = { id, name } des Angemeldeten, kpis (Kennzahlen aus dem Zustand: pultOpen steuert die Zahl am Reiter „Wartet“, im Kopf und die Boten-
|
||||
// Schleife), paused (Hustler angehalten: die Szene steht still, Klasse hk-steht), onBack() („◂ Hochhaus“). workers (Hustler-Tabelle) reicht die Seite durch, das Pult
|
||||
// braucht sie nicht: ein Pult-Eintrag nennt keinen Hustler.
|
||||
// braucht sie nicht: ein Pult-Eintrag nennt keinen Hustler. stufe (Standard HUSTLER_STUFE, hustlerStufe.js) geht an rowModel: unterhalb der Antwort-Stufe (4) bietet kein
|
||||
// Brief die P4-Aktionen an (Antwort einfügen, als Mensch antworten, Vorschau freigeben, Vorschau bauen).
|
||||
// Laufende Aktionen (T13 P1): welche Einträge gerade eine Anfrage an den Dienst laufen haben, merkt PultList (laufRef, nach Eintrags-Id), nicht der Brief: wer den Brief
|
||||
// während der Anfrage schließt und denselben Eintrag wieder öffnet, findet alle Aktionsknöpfe gesperrt und kann keine zweite Anfrage auslösen (sonst fügte „Antwort
|
||||
// einfügen“ doppelt ein); sobald die Anfrage endet, sind sie frei.
|
||||
@@ -17,6 +18,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useEmployees } from '../../hooks/useEmployees'
|
||||
import { usePultFeed } from '../../hooks/usePultFeed'
|
||||
import { reiterZahl, wartetZahl, botenLaufen } from '../../lib/hustlerPult'
|
||||
import { HUSTLER_STUFE } from '../../lib/hustlerStufe'
|
||||
import { activeControl, restoreFocus } from '../../lib/workersFocus'
|
||||
import { PULT_FILTERS, rowModel } from '../../lib/workersPult'
|
||||
import Brief from './pult/Brief'
|
||||
@@ -29,7 +31,7 @@ const NO_FIELDS = Object.freeze({}) // Eintrag ohne bearbeitete Felder
|
||||
// Das document des Browsers; unter Node (Tests, Server) gibt es keins, dann bleibt der Fokus unangetastet
|
||||
const browserDocument = () => (typeof document === 'undefined' ? null : document)
|
||||
|
||||
export default function PultList({ api, me, kpis, paused = false, onBack }) {
|
||||
export default function PultList({ api, me, kpis, paused = false, onBack, stufe = HUSTLER_STUFE }) {
|
||||
const [filterId, setFilterId] = useState('offen')
|
||||
const [drafts, setDrafts] = useState({}) // bearbeitete Texte je Eintrag; was fehlt, zeigt den Entwurf des Dienstes
|
||||
const [fields, setFields] = useState({}) // Texte der Felder „Als Mensch antworten“ (reply) und „Antwort einfügen“ (paste) je Eintrag
|
||||
@@ -123,7 +125,7 @@ export default function PultList({ api, me, kpis, paused = false, onBack }) {
|
||||
return true
|
||||
}, [])
|
||||
|
||||
const rows = items.map((item) => ({ item, row: rowModel(item, { me, nowMs, employees }) }))
|
||||
const rows = items.map((item) => ({ item, row: rowModel(item, { me, nowMs, employees, stufe }) }))
|
||||
const offen = briefId === null ? null : rows.find(({ row }) => row.id === briefId) ?? null
|
||||
// Ein Brief, dessen Eintrag nicht mehr in der Liste steht (ein Kollege war schneller), schließt sich: auf einem alten Stand wird nichts mehr gesendet
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Plattform-Antworten schickt der Mensch selbst ab (Plattform) und markiert sie danach als gesendet; Mail-Antworten und die Freigabe
|
||||
// einer Vorschau gehen über den Dienst (api.pultAction), der sie durch das Sende-Tor führt.
|
||||
|
||||
import { HUSTLER_STUFE, antwortenOffen } from './hustlerStufe'
|
||||
import { DOWN_TEXT, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS } from './workersApi'
|
||||
import { roleOf } from './workersRoster'
|
||||
|
||||
@@ -179,8 +180,10 @@ function doneByPhrase(by, employees) {
|
||||
* Zeilenmodell eines Pult-Eintrags (GET /pult: Eintrag plus `prospect`). me = { id, name } des Angemeldeten, employees = Mitarbeiterliste.
|
||||
* Rechte nach Status (offen, in_arbeit, veraltet sind bearbeitbar) und Art (nur Antwort-Arten sind sendbar); die Zeile sperrt
|
||||
* nichts wegen eines Kollegen, der übernommen hat: der Dienst entscheidet (409 mit deutschem Satz).
|
||||
* stufe (Standard HUSTLER_STUFE, hustlerStufe.js): unterhalb der Antwort-Stufe (4, Dienst P4) sind die vier P4-Aktionen canReply, canPaste, canApprovePreview und
|
||||
* canStartBuild immer false, denn deren Endpunkte gibt es im Dienst noch nicht; die P3-Aktionen (übernehmen, gesendet, verwerfen, erledigt, Mail-Link) bleiben.
|
||||
*/
|
||||
export function rowModel(item, { me, nowMs, employees }) {
|
||||
export function rowModel(item, { me, nowMs, employees, stufe = HUSTLER_STUFE }) {
|
||||
const editable = BEARBEITBAR.includes(item.status)
|
||||
const sendable = SENDABLE_KINDS.includes(item.kind)
|
||||
const prospect = item.prospect ?? null
|
||||
@@ -197,6 +200,7 @@ export function rowModel(item, { me, nowMs, employees }) {
|
||||
// P4: Antworten, Freigeben und Bauen nur an einem bearbeitbaren Eintrag, den kein anderer hält (Einfügen kennt diese Sperre nicht, siehe canPaste)
|
||||
const free = editable && !claimedByOther
|
||||
const freigabe = item.kind === 'vorschau_freigabe'
|
||||
const p4 = antwortenOffen(stufe) // ohne P4 im Dienst bietet die Zeile keine der vier neuen Aktionen an
|
||||
|
||||
const draft = typeof item.draft === 'string' ? item.draft : ''
|
||||
let mail = ''
|
||||
@@ -228,14 +232,14 @@ export function rowModel(item, { me, nowMs, employees }) {
|
||||
// Erledigt würde eine Vorschau-Freigabe schließen, ohne dass der Link hinausgeht: dort bleibt nur Freigeben oder Verwerfen (D20)
|
||||
canDone: !sendable && editable && !freigabe,
|
||||
// Mail-Antwort als Mensch: braucht einen Interessenten mit Adresse („Mail ohne Faden“ und Plattform-Interessenten haben keinen Mail-Weg)
|
||||
canReply: REPLY_KINDS.includes(item.kind) && free && prospect !== null && filled(prospect.email),
|
||||
canApprovePreview: freigabe && free,
|
||||
canReply: p4 && REPLY_KINDS.includes(item.kind) && free && prospect !== null && filled(prospect.email),
|
||||
canApprovePreview: p4 && freigabe && free,
|
||||
// Bau starten wie ein „Ja“: Eintrag „unsicher“ mit Interessent. Ein toter Interessent (gesperrt, verloren, Deal) bekommt den Knopf nicht; ohne
|
||||
// Kurzfassung (prospect null) oder ohne Status entscheidet der Dienst (409 mit deutschem Satz, auch bei einer schon vorhandenen Vorschau)
|
||||
canStartBuild: item.kind === 'unsicher' && free && filled(item.prospectId) && !(prospect !== null && DEAD_PROSPECT.includes(prospect.status)),
|
||||
canStartBuild: p4 && item.kind === 'unsicher' && free && filled(item.prospectId) && !(prospect !== null && DEAD_PROSPECT.includes(prospect.status)),
|
||||
// Die Rückmeldung des Kunden kommt meist erst, nachdem jemand Claras Entwurf auf der Plattform gesendet hat: auch bei gesendet,
|
||||
// erledigt oder von einem Kollegen übernommen. Nur ein verworfener Eintrag nimmt nichts mehr an.
|
||||
canPaste: PASTE_KINDS.includes(item.kind) && item.status !== 'verworfen' && prospect !== null,
|
||||
canPaste: p4 && PASTE_KINDS.includes(item.kind) && item.status !== 'verworfen' && prospect !== null,
|
||||
claimedByMe,
|
||||
claimedByOther,
|
||||
claimedByLabel,
|
||||
|
||||
@@ -25,7 +25,8 @@ const NOW = Date.now() // die Bauteile rechnen mit der Uhr des Rechners: Alter u
|
||||
const iso = (ms) => new Date(ms).toISOString()
|
||||
const ME = { id: 'u1', name: 'Justin' }
|
||||
const EMPLOYEES = [{ userId: 'u2', displayName: 'Kenso' }]
|
||||
const rowOf = (item) => rowModel(item, { me: ME, nowMs: NOW, employees: EMPLOYEES })
|
||||
// Stufen-Sperre (hustlerStufe.js): diese Datei prüft das volle Pult (Stufe 6, P4-Aktionen offen, Verhalten wie vor der Sperre); Stufe 3 prüfen workersPultUi.test.mjs und hustlerStufe.test.mjs
|
||||
const rowOf = (item) => rowModel(item, { me: ME, nowMs: NOW, employees: EMPLOYEES, stufe: 6 })
|
||||
const entry = (extra = {}) => ({ $id: 'a', kind: 'plattform_antwort', status: 'offen', title: 'Suche Webdesigner', draft: 'Hallo', createdAt: iso(NOW - 3 * 3600e3), sourceUrl: 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3', prospect: { $id: 'p1', name: 'Verein', city: 'Namborn', postalCode: '66640', score: 72, email: 'x@verein.example' }, ...extra })
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -209,7 +210,7 @@ async function startPult(items, { api = { pultAction: async () => ({}) }, kpis,
|
||||
globalThis.__pultEmployees = EMPLOYEES
|
||||
globalThis.__pultFeedFor = (statuses) => ({ items: feed.byStatus ? (feed.byStatus[statuses] ?? []) : items, loading: false, error: null, refresh: async () => { refreshes.push(statuses) }, ...feed.extra })
|
||||
delete globalThis.__panel
|
||||
const mounted = __mount(jsx(PultList, { api, me: ME, kpis, onBack, paused }))
|
||||
const mounted = __mount(jsx(PultList, { api, me: ME, kpis, onBack, paused, stufe: 6 }))
|
||||
const view = {
|
||||
timers, refreshes,
|
||||
tree: () => mounted.tree(),
|
||||
|
||||
@@ -5,8 +5,13 @@ import { loadModule } from './load.mjs'
|
||||
// Reine Pult-Logik des Reiters „Freigabe“: Filter, Alter, Veraltet-Anzeige, sichere Links, Zeilenmodell, Kopieren & Öffnen
|
||||
const {
|
||||
PULT_KIND_LABELS, SENDABLE_KINDS, PULT_FILTERS, REPLY_KINDS, PASTE_KINDS, REPLY_TEXT_MAX, UNCERTAIN_TEXT,
|
||||
staleView, ageLabel, safeExternalUrl, mailtoHref, employeeName, rowModel, copyAndOpen, validateReplyText, charCount, actionErrorText, pasteDoneText,
|
||||
staleView, ageLabel, safeExternalUrl, mailtoHref, employeeName, rowModel: rowModelNachStufe, copyAndOpen, validateReplyText, charCount, actionErrorText, pasteDoneText,
|
||||
} = await loadModule('src/lib/workersPult.js')
|
||||
const { HUSTLER_STUFE } = await loadModule('src/lib/hustlerStufe.js')
|
||||
|
||||
// Stufen-Sperre (hustlerStufe.js): rowModel bietet die P4-Aktionen erst ab Stufe 4 an. Die Zusicherungen dieser Datei gelten für das volle Pult und laufen deshalb auf
|
||||
// Stufe 6 (P4 und Keller offen, Verhalten wie vor der Sperre). Was unterhalb von Stufe 4 gilt, prüft hustlerStufe.test.mjs.
|
||||
const rowModel = (item, ctx) => rowModelNachStufe(item, { stufe: 6, ...ctx })
|
||||
|
||||
const NOW = Date.parse('2026-10-19T09:00:00Z')
|
||||
const MIN = 60 * 1000
|
||||
@@ -462,3 +467,73 @@ test('pasteDoneText: neu und fortgesetzt melden das Einsortieren, doppelt meldet
|
||||
assert.equal(pasteDoneText({ status: 'doppelt' }), 'Diese Antwort war schon eingefügt.')
|
||||
for (const odd of [undefined, null, {}, 'doppelt', { status: 7 }]) assert.equal(pasteDoneText(odd), 'Eingefügt. Nora sortiert die Antwort ein.', String(odd))
|
||||
})
|
||||
|
||||
// ---------- Stufen-Sperre (hustlerStufe.js): rowModel auf Stufe 3 und Stufe 6 ----------
|
||||
|
||||
const ME = { id: 'u1', name: 'Justin' }
|
||||
const PROSPEKT = { $id: 'p1', name: 'Verein', email: 'x@verein.example', status: 'neu' }
|
||||
const eintrag = (extra) => ({ $id: 'e1', kind: 'plattform_antwort', status: 'offen', title: 'Titel', draft: 'Hallo', createdAt: '2026-10-19T08:00:00Z', sourceUrl: 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3', prospectId: 'p1', prospect: PROSPEKT, ...extra })
|
||||
const zeile = (item, s) => rowModelNachStufe(item, { me: ME, nowMs: NOW, employees: [], stufe: s })
|
||||
const P4 = ['canReply', 'canPaste', 'canApprovePreview', 'canStartBuild']
|
||||
const P3 = ['canClaim', 'canSend', 'canDiscard', 'canDone']
|
||||
const VORSCHAU = 'https://entwurf.project.webklar.com/verein'
|
||||
|
||||
test('rowModel auf Stufe 1 bis 3: canReply, canPaste, canApprovePreview und canStartBuild sind für jede Art und jeden Status false', () => {
|
||||
const kinds = Object.keys(PULT_KIND_LABELS)
|
||||
const states = ['offen', 'in_arbeit', 'veraltet', 'gesendet', 'verworfen', 'erledigt']
|
||||
for (const s of [1, 2, 3]) {
|
||||
for (const kind of kinds) {
|
||||
for (const status of states) {
|
||||
for (const extra of [{}, { claimedBy: 'mensch:u1' }, { claimedBy: 'mensch:u2' }, { prospect: null }]) {
|
||||
const item = eintrag({ kind, status, sourceUrl: kind === 'vorschau_freigabe' ? VORSCHAU : undefined, ...extra })
|
||||
const m = zeile(item, s)
|
||||
for (const key of P4) assert.equal(m[key], false, `Stufe ${s} ${kind}/${status} ${JSON.stringify(extra)}: ${key}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('rowModel auf Stufe 4, 6 und 9: die P4-Aktionen erscheinen wie vor der Sperre', () => {
|
||||
for (const s of [4, 6, 9]) {
|
||||
assert.equal(zeile(eintrag({ kind: 'mail_antwort' }), s).canReply, true, `Stufe ${s}`)
|
||||
assert.equal(zeile(eintrag({ kind: 'unsicher' }), s).canReply, true)
|
||||
assert.equal(zeile(eintrag({ kind: 'plattform_antwort', status: 'gesendet' }), s).canPaste, true)
|
||||
assert.equal(zeile(eintrag({ kind: 'plattform_antwort', status: 'verworfen' }), s).canPaste, false) // die Regeln selbst bleiben
|
||||
assert.equal(zeile(eintrag({ kind: 'vorschau_freigabe', sourceUrl: VORSCHAU }), s).canApprovePreview, true)
|
||||
assert.equal(zeile(eintrag({ kind: 'unsicher' }), s).canStartBuild, true)
|
||||
assert.equal(zeile(eintrag({ kind: 'unsicher', prospect: { ...PROSPEKT, status: 'gesperrt' } }), s).canStartBuild, false)
|
||||
assert.equal(zeile(eintrag({ kind: 'mail_antwort', claimedBy: 'mensch:u2', status: 'in_arbeit' }), s).canReply, false) // von einem Kollegen übernommen
|
||||
}
|
||||
})
|
||||
|
||||
test('rowModel: die P3-Aktionen sind auf Stufe 3 und 6 gleich; die Stufe ändert nur die vier P4-Felder', () => {
|
||||
const fixtures = []
|
||||
for (const kind of Object.keys(PULT_KIND_LABELS)) {
|
||||
for (const status of ['offen', 'in_arbeit', 'veraltet', 'gesendet', 'verworfen', 'erledigt']) fixtures.push(eintrag({ kind, status, sourceUrl: kind === 'vorschau_freigabe' ? VORSCHAU : undefined }))
|
||||
}
|
||||
fixtures.push(eintrag({ status: 'in_arbeit', claimedBy: 'mensch:u1' }), eintrag({ status: 'in_arbeit', claimedBy: 'mensch:u2' }), eintrag({ kind: 'mail_antwort', sourceUrl: '' }))
|
||||
for (const item of fixtures) {
|
||||
const gesperrt = zeile(item, 3); const offen = zeile(item, 6)
|
||||
for (const key of P3) assert.equal(gesperrt[key], offen[key], `${item.kind}/${item.status}: ${key}`)
|
||||
const ohneP4 = (m) => Object.fromEntries(Object.entries(m).filter(([k]) => !P4.includes(k)))
|
||||
assert.deepEqual(ohneP4(gesperrt), ohneP4(offen), `${item.kind}/${item.status}`)
|
||||
}
|
||||
// und konkret die P3-Aktionen auf Stufe 3
|
||||
const plattform = zeile(eintrag({}), 3)
|
||||
assert.deepEqual([plattform.canClaim, plattform.canSend, plattform.canDiscard, plattform.canDone], [true, true, true, false])
|
||||
assert.equal(plattform.sourceHref, 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3'); assert.equal(plattform.copyText, 'Hallo')
|
||||
const mail = zeile(eintrag({ kind: 'mail_antwort', sourceUrl: '' }), 3)
|
||||
assert.match(mail.mailtoHref, /^mailto:x@verein\.example\?/); assert.equal(mail.canSend, true)
|
||||
const unsicher = zeile(eintrag({ kind: 'unsicher' }), 3)
|
||||
assert.deepEqual([unsicher.canDone, unsicher.canDiscard, unsicher.canClaim], [true, true, true])
|
||||
const freigabe = zeile(eintrag({ kind: 'vorschau_freigabe', sourceUrl: VORSCHAU }), 3)
|
||||
assert.deepEqual([freigabe.canClaim, freigabe.canDiscard, freigabe.canDone, freigabe.canSend], [true, true, false, false])
|
||||
})
|
||||
|
||||
test('rowModel ohne Stufe gilt HUSTLER_STUFE (die Stufe der Produktion)', () => {
|
||||
const item = eintrag({ kind: 'mail_antwort' })
|
||||
assert.equal(rowModelNachStufe(item, { me: ME, nowMs: NOW, employees: [] }).canReply, HUSTLER_STUFE >= 4)
|
||||
assert.equal(rowModelNachStufe(item, { me: ME, nowMs: NOW, employees: [], stufe: undefined }).canReply, HUSTLER_STUFE >= 4)
|
||||
assert.equal(rowModelNachStufe(item, { me: ME, nowMs: NOW, employees: [], stufe: 'x' }).canReply, HUSTLER_STUFE >= 4)
|
||||
})
|
||||
@@ -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 & 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 & 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 & ö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() }
|
||||
})
|
||||
Reference in new issue
Block a user