"use client"; import { useMemo, useState } from "react"; import { useMutation, useQuery } from "convex/react"; import type { FunctionReturnType } from "convex/server"; import { ChevronDown, ChevronRight, ExternalLink, MailCheck, Save } from "lucide-react"; import Link from "next/link"; import { api } from "@/convex/_generated/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; type ReviewWorkspaceListResult = FunctionReturnType< typeof api.outreach.listReviewWorkspace >; type ReviewWorkspaceItem = NonNullable[number]; type UsedSkill = ReviewWorkspaceItem["usedSkills"][number]; type DraftState = { auditBody: string; auditSummary: string; emailBody: string; emailSubject: string; followUpDraft: string; phoneScript: string; }; const emptyDraft: DraftState = { auditBody: "", auditSummary: "", emailBody: "", emailSubject: "", followUpDraft: "", phoneScript: "", }; const textAreaClassName = "min-h-24 w-full rounded-md border border-input bg-background px-2.5 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"; function getDraft(record: ReviewWorkspaceItem): DraftState { const outreach = record.latestOutreach; return { auditBody: record.audit?.publicBody ?? "", auditSummary: record.audit?.publicSummary ?? "", emailBody: outreach?.emailBody ?? "", emailSubject: outreach?.emailSubject ?? "", followUpDraft: outreach?.followUpDraft ?? "", phoneScript: outreach?.phoneScript ?? "", }; } function compactText(value?: string | null, fallback = "Offen") { const trimmed = value?.trim(); return trimmed ? trimmed : fallback; } function formatStrategy(strategy?: string | null) { const labels: Record = { call_first: "Erst anrufen", defer: "Zurückstellen", do_not_contact: "Nicht kontaktieren", email_first: "Erst E-Mail", }; return strategy ? labels[strategy] ?? strategy : "Strategie offen"; } function formatRaw(value: unknown) { if (value === undefined || value === null) { return "Keine Rohdaten vorhanden."; } return JSON.stringify(value, null, 2); } function skillLabel(skill: UsedSkill) { const name = compactText(skill?.name, "Skill"); return skill.category ? `${name} · ${skill.category}` : name; } function DetailToggle({ isOpen, label, onClick, }: { isOpen: boolean; label: string; onClick: () => void; }) { const Icon = isOpen ? ChevronDown : ChevronRight; return ( ); } function FieldPair({ label, value }: { label: string; value?: string | null }) { return (
{label}
{compactText(value)}
); } function WorkspaceLoading() { return (

Interne Outreach-Prüfung

Review Workspace

{Array.from({ length: 3 }, (_, index) => ( ))}
); } export function OutreachReviewWorkspace() { const records = useQuery(api.outreach.listReviewWorkspace, { limit: 100 }); const saveReviewDraft = useMutation(api.outreach.saveReviewDraft); const approveEmailDraft = useMutation(api.outreach.approveEmailDraft); const savePublicAuditContent = useMutation(api.audits.savePublicAuditContent); const publishPublicAudit = useMutation(api.audits.publishPublicAudit); const [drafts, setDrafts] = useState>({}); const [openSources, setOpenSources] = useState>({}); const [openRaw, setOpenRaw] = useState>({}); const [busyAction, setBusyAction] = useState(null); const [notice, setNotice] = useState(null); const rows = useMemo(() => records ?? [], [records]); if (records === undefined) { return ; } if (rows.length === 0) { return (

Interne Outreach-Prüfung

Review Workspace

Keine offenen Reviews

Sobald Audit- und Outreach-Entwürfe bereitstehen, erscheinen sie hier.

); } const updateDraft = ( id: string, field: keyof DraftState, value: string, ) => { const record = rows.find((row) => row.id === id); setDrafts((current) => ({ ...current, [id]: { ...(current[id] ?? (record ? getDraft(record) : emptyDraft)), [field]: value, }, })); }; const saveAudit = async (record: ReviewWorkspaceItem) => { const auditId = record.audit?._id; if (!auditId) { setNotice("Audit kann ohne Audit-ID nicht gespeichert werden."); return; } setBusyAction(`${record.id}:audit-save`); setNotice(null); try { const draft = drafts[record.id] ?? getDraft(record); await savePublicAuditContent({ id: auditId, publicBody: draft.auditBody, publicSummary: draft.auditSummary, }); setNotice("Audit-Änderungen gespeichert."); } catch { setNotice("Audit-Änderungen konnten nicht gespeichert werden."); } finally { setBusyAction(null); } }; const publishAudit = async (record: ReviewWorkspaceItem) => { const auditId = record.audit?._id; if (!auditId) { setNotice("Audit kann ohne Audit-ID nicht veröffentlicht werden."); return; } setBusyAction(`${record.id}:audit-publish`); setNotice(null); try { const draft = drafts[record.id] ?? getDraft(record); await savePublicAuditContent({ id: auditId, publicBody: draft.auditBody, publicSummary: draft.auditSummary, }); await publishPublicAudit({ id: auditId }); setNotice("Audit veröffentlicht."); } catch { setNotice("Audit konnte nicht veröffentlicht werden."); } finally { setBusyAction(null); } }; const saveOutreach = async (record: ReviewWorkspaceItem) => { const outreach = record.latestOutreach; if (!outreach) { setNotice("Outreach-Entwurf kann ohne Outreach-ID nicht gespeichert werden."); return; } const draft = drafts[record.id] ?? getDraft(record); const strategy = outreach.strategy; const hasCallablePhone = Boolean(record.lead?.phone) && (strategy === "call_first" || record.lead?.contactStatus === "missing_contact"); setBusyAction(`${record.id}:outreach-save`); setNotice(null); try { await saveReviewDraft({ id: outreach._id, strategy, emailBody: draft.emailBody, emailSubject: draft.emailSubject, followUpDraft: draft.followUpDraft, ...(hasCallablePhone ? { phoneScript: draft.phoneScript } : {}), }); setNotice("Outreach-Entwurf gespeichert."); } catch { setNotice("Outreach-Entwurf konnte nicht gespeichert werden."); } finally { setBusyAction(null); } }; const approveEmail = async (record: ReviewWorkspaceItem) => { const outreach = record.latestOutreach; if (!outreach) { setNotice("E-Mail kann ohne Outreach-ID nicht freigegeben werden."); return; } const draft = drafts[record.id] ?? getDraft(record); const strategy = outreach.strategy; const hasCallablePhone = Boolean(record.lead?.phone) && (strategy === "call_first" || record.lead?.contactStatus === "missing_contact"); setBusyAction(`${record.id}:email-approval`); setNotice(null); try { await saveReviewDraft({ id: outreach._id, strategy, emailBody: draft.emailBody, emailSubject: draft.emailSubject, followUpDraft: draft.followUpDraft, ...(hasCallablePhone ? { phoneScript: draft.phoneScript } : {}), }); await approveEmailDraft({ id: outreach._id }); setNotice("E-Mail freigegeben."); } catch { setNotice("E-Mail konnte nicht freigegeben werden."); } finally { setBusyAction(null); } }; return (

Interne Outreach-Prüfung

Review Workspace

Audits, E-Mail-Empfehlung und Telefonnotizen prüfen, bevor etwas öffentlich wird oder eine Freigabe erhält.

{notice ? (

{notice}

) : null}
{rows.map((record) => { const draft = drafts[record.id] ?? getDraft(record); const lead = record.lead; const audit = record.audit; const outreach = record.latestOutreach; const strategy = outreach?.strategy; const contactSources = [ lead.email ? `E-Mail: ${lead.email}` : null, lead.phone ? `Telefon: ${lead.phone}` : null, ...record.sourceSummaries.emailCandidates.map( (candidate) => `${candidate.email} · ${candidate.emailSource}${ candidate.accepted ? " · akzeptiert" : "" }`, ), ].filter((source): source is string => Boolean(source)); const skills = record.usedSkills; const hasCallablePhone = Boolean(lead?.phone) && (strategy === "call_first" || lead?.contactStatus === "missing_contact"); const publicAuditHref = audit?.slug ? `/audit/${audit.slug}` : null; return (
{compactText(lead?.companyName, "Unbenannter Lead")}

{compactText( lead?.websiteDomain ?? lead?.websiteUrl, "Keine Domain", )}

{formatStrategy(strategy)} {compactText(lead?.contactStatus, "Kontaktstatus offen")} {compactText(audit?.status, "Auditstatus offen")}

Lead-Details

Prioritätsgrund

{compactText(lead?.priorityReason)}

Kontaktstrategie

{formatStrategy(strategy)}

Audit-Zusammenfassung

{publicAuditHref ? ( ) : ( Public-Audit ohne Slug )}