import { useCallback, useEffect, useRef, useState } from 'react' import Badge from '../ui/Badge' import Button from '../ui/Button' import EmptyState from '../ui/EmptyState' import { copyTextChecked } from '../CopyableCredential' import { useEmployees } from '../../hooks/useEmployees' import { usePultFeed } from '../../hooks/usePultFeed' import { PULT_FILTERS, copyAndOpen, mailtoHref, rowModel } from '../../lib/workersPult' import ProspectPanel from './ProspectPanel' const CONFIRM_MS = 4000 // so lange gilt die Rückfrage „Wirklich verwerfen?“ nach dem ersten Klick const ACTION_FAILED = 'Das hat nicht geklappt.' // Leerzustand je Filter const EMPTY = { offen: { title: 'Nichts wartet auf dich', hint: 'Neue Antworten erscheinen hier, sobald ein Entwurf bereit ist.' }, veraltet: { title: 'Nichts ist veraltet', hint: 'Einträge, die zu lange liegen, landen hier und lassen sich weiter bearbeiten.' }, erledigt: { title: 'Noch nichts erledigt', hint: 'Gesendete, verworfene und erledigte Einträge stehen hier.' }, } // Neuer Tab ohne Rückbezug auf diese Seite const openTab = (url) => window.open(url, '_blank', 'noopener,noreferrer') // Meldung nach „Kopieren & öffnen“: ok nur, wenn alles geklappt hat, was die Zeile anbietet (ohne Anzeigen-Adresse gibt es nur Kopieren) function copyNote(result, hasUrl) { if (!hasUrl) { return result.copied ? { kind: 'ok', text: 'Kopiert.' } : { kind: 'err', text: 'Kopieren hat nicht geklappt. Bitte den Text von Hand markieren und kopieren.' } } if (result.copied && result.opened) return { kind: 'ok', text: 'Kopiert und geöffnet.' } if (result.copied) return { kind: 'err', text: 'Kopiert, aber die Anzeige ließ sich nicht öffnen. Ein Pop-up-Blocker kann der Grund sein.' } if (result.opened) return { kind: 'err', text: 'Geöffnet, aber das Kopieren hat nicht geklappt. Bitte den Text von Hand kopieren.' } return { kind: 'err', text: 'Kopieren und Öffnen haben nicht geklappt.' } } // Eine Zeile des Pults. Die Zeile hält nur, was ihr gehört (läuft gerade etwas, Rückfrage, Meldung); der bearbeitete Text liegt in der // Liste, damit er einen Wechsel des Filters und das Neuladen überlebt. api.pultAction führt aus, der Dienst entscheidet über Rechte und // Reihenfolge (409 mit deutschem Satz); danach wird die Liste neu geholt. function PultRow({ api, item, row, text, onText, refresh, onFinished, onDetails }) { const [busy, setBusy] = useState(false) const [confirming, setConfirming] = useState(false) const [note, setNote] = useState(null) // { kind: 'ok' | 'err', text } const busyRef = useRef(false) // sperrt schon im selben Takt, in dem busy noch nicht greift (Doppelklick) const aliveRef = useRef(true) useEffect(() => { aliveRef.current = true return () => { aliveRef.current = false } }, []) // Die Rückfrage vor dem Verwerfen verfällt nach 4 s useEffect(() => { if (!confirming) return undefined const timer = window.setTimeout(() => setConfirming(false), CONFIRM_MS) return () => window.clearTimeout(timer) }, [confirming]) const begin = () => { if (busyRef.current) return false busyRef.current = true setBusy(true) setConfirming(false) setNote(null) return true } const end = () => { busyRef.current = false if (aliveRef.current) setBusy(false) } // Aktion am Dienst. finished: der Eintrag verlässt die Liste, die Erfolgsmeldung steht deshalb oben bei der Liste und nicht in der Zeile. const run = async (call, done, finished) => { if (!begin()) return try { await call() if (finished) onFinished(row.id, `„${row.title}“ ${done}`) else if (aliveRef.current) setNote({ kind: 'ok', text: done }) } catch (err) { if (aliveRef.current) setNote({ kind: 'err', text: err?.message || ACTION_FAILED }) } await refresh() end() } // Kopieren & öffnen: copyAndOpen steht ganz vorn und ohne await davor, nur im Zug des Klicks darf der Browser den Tab öffnen. // Kopiert wird mit der strengen Fassung (copyTextChecked): scheitern Clipboard-API und Notweg, meldet die Zeile das ehrlich, // statt „Kopiert“ zu zeigen, während noch der alte Inhalt der Zwischenablage drinliegt. // Danach wird der Eintrag best-effort übernommen; ein Kollege, der schon dran ist, erscheint als Hinweis. const copyOpen = async () => { if (!begin()) return const result = await copyAndOpen({ text, url: row.sourceHref, copy: copyTextChecked, open: openTab }) let next = copyNote(result, row.sourceHref !== null) if (!row.claimedByMe) { try { await api.pultAction(row.id, 'claim') } catch (err) { const hint = err?.status === 409 && row.claimedByOther ? `Wird schon von ${row.claimedByLabel} bearbeitet.` : err?.message || ACTION_FAILED next = { kind: 'err', text: `${next.text} ${hint}` } } await refresh() } if (aliveRef.current) setNote(next) end() } const discard = () => { if (!confirming) { setConfirming(true) return } run(() => api.pultAction(row.id, 'discard', {}), 'verworfen.', true) } const { prospect } = row const place = prospect ? [prospect.postalCode, prospect.city].filter(Boolean).join(' ') : '' const info = [place, prospect && prospect.score != null ? `Score ${prospect.score}` : ''].filter(Boolean).join(' · ') // Der Mail-Link trägt den Text aus dem Feld (auch Bearbeitetes), nicht den gespeicherten Entwurf const mail = row.kind === 'mail_antwort' && prospect && prospect.email ? mailtoHref({ to: prospect.email, subject: prospect.name ? `Re: ${prospect.name}` : '', body: text }) : '' return (
{item.originalText}