diff --git a/src/components/workers/PultList.jsx b/src/components/workers/PultList.jsx index 14a30cc..5842b77 100644 --- a/src/components/workers/PultList.jsx +++ b/src/components/workers/PultList.jsx @@ -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(() => { diff --git a/src/lib/workersPult.js b/src/lib/workersPult.js index 8338814..799795c 100644 --- a/src/lib/workersPult.js +++ b/src/lib/workersPult.js @@ -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, diff --git a/tests/unit/hustlerPult.test.mjs b/tests/unit/hustlerPult.test.mjs index 715dee7..32761a0 100644 --- a/tests/unit/hustlerPult.test.mjs +++ b/tests/unit/hustlerPult.test.mjs @@ -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(), diff --git a/tests/unit/workersPult.test.mjs b/tests/unit/workersPult.test.mjs index 12258eb..b368667 100644 --- a/tests/unit/workersPult.test.mjs +++ b/tests/unit/workersPult.test.mjs @@ -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) +}) diff --git a/tests/unit/workersPultUi.test.mjs b/tests/unit/workersPultUi.test.mjs index 3074342..cdb7da1 100644 --- a/tests/unit/workersPultUi.test.mjs +++ b/tests/unit/workersPultUi.test.mjs @@ -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, /