"use client"; import { useMemo, useState } from "react"; import { useMutation, useQuery } from "convex/react"; import { FunctionReturnType } from "convex/server"; import { Building2, ExternalLink, Mail, MapPin, Phone, PlayCircle, ShieldAlert } from "lucide-react"; import { api } from "@/convex/_generated/api"; import { Id } from "@/convex/_generated/dataModel"; import { getLeadBlacklistStatusLabel, getLeadContactStatusLabel, getLeadDuplicateStatusLabel, getLeadPriorityLabel, leadBlacklistStatusOptions, leadContactStatusOptions, leadDuplicateStatusOptions, leadPriorityOptions, type LeadContactStatus, type LeadDuplicateStatus, type LeadPriority, type LeadBlacklistStatus, } from "@/lib/dashboard-model"; import { Button } from "@/components/ui/button"; import { Card, CardHeader } from "@/components/ui/card"; import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; type LeadsListResult = FunctionReturnType; type LeadRow = NonNullable[number]; type AuditStartStatesResult = FunctionReturnType< typeof api.pageSpeed.getLeadAuditStartStates >; type AuditStartState = NonNullable[number]; type LeadReviewDraft = { priority: LeadPriority; contactStatus: LeadContactStatus; priorityReason: string; contactStatusReason: string; notes: string; reviewEmail: string; reviewEmailSource: string; reviewContactPerson: string; reviewIsBusinessContactAddress: boolean; duplicateStatus: LeadDuplicateStatus; blacklistStatus: LeadBlacklistStatus; }; type LeadReviewPayload = { id: Id<"leads">; priority?: LeadPriority; priorityReason?: string; contactStatus?: LeadContactStatus; contactStatusReason?: string; notes?: string; duplicateStatus?: LeadDuplicateStatus; duplicateReason?: string; blacklistStatus?: LeadBlacklistStatus; blacklistReason?: string; duplicateOfLeadId?: Id<"leads">; applyBlacklist?: boolean; reviewEmail?: string; reviewEmailSource?: string; reviewContactPerson?: string; reviewIsBusinessContactAddress?: boolean; }; type LeadStatusFilter = "all" | "high" | "blocked"; function normalizeTextInput(value: string): string | undefined { const next = value.trim(); return next.length > 0 ? next : undefined; } function toEmailHref(email?: string | null): string | null { const normalizedEmail = normalizeTextInput(email ?? ""); return normalizedEmail ? `mailto:${normalizedEmail}` : null; } function toPhoneHref(phone?: string | null): string | null { const normalizedPhone = normalizeTextInput(phone ?? ""); const dialablePhone = normalizedPhone?.replace(/[^\d+]/g, ""); return dialablePhone ? `tel:${dialablePhone}` : null; } function toWebsiteHref(lead: Pick): string | null { const website = normalizeTextInput(lead.websiteUrl ?? "") ?? normalizeTextInput(lead.websiteDomain ?? ""); if (!website) { return null; } if (/^https?:\/\//i.test(website)) { return website; } return `https://${website.replace(/^\/+/, "")}`; } function contactSourceLabel(lead: LeadRow): string { if (lead.sourceProvider) { return lead.sourceProvider; } if (lead.emailSource) { return lead.emailSource; } return "Unbekannt"; } function formatLocation(lead: LeadRow): string { if (lead.postalCode && lead.city) { return `${lead.postalCode} ${lead.city}`; } if (lead.city || lead.address) { return lead.city ?? lead.address ?? ""; } return lead.address ?? "Ort offen"; } function priorityBadgeClass(priority: LeadPriority): string { switch (priority) { case "high": return "text-destructive border-destructive/30 bg-destructive/15"; case "medium": return "text-muted-foreground border-muted-foreground/30 bg-muted/20"; case "low": return "text-muted-foreground border-muted/40 bg-muted/35"; case "defer": return "text-muted-foreground border-secondary/50 bg-secondary/30"; case "blocked": return "text-destructive border-destructive/40 bg-destructive/15"; default: return "text-muted-foreground border-muted bg-muted/20"; } } function duplicateBadgeVariant( duplicateStatus: LeadDuplicateStatus, ): "secondary" | "default" | "outline" | "destructive" { if (duplicateStatus === "duplicate") { return "destructive"; } if (duplicateStatus === "possible_duplicate") { return "outline"; } if (duplicateStatus === "unique") { return "secondary"; } return "outline"; } function auditStartDisabledReason({ lead, auditStartState, isLoading, isStarting, }: { lead: LeadRow; auditStartState?: AuditStartState; isLoading: boolean; isStarting: boolean; }) { if (isStarting) { return "Audit läuft"; } if (!lead.websiteUrl) { return "Keine Website hinterlegt."; } if ( lead.priority === "blocked" || lead.priority === "defer" || lead.blacklistStatus === "blocked" || lead.contactStatus === "do_not_contact" ) { return "Lead ist gesperrt oder zurückgestellt."; } if (isLoading) { return "Audit-Status wird geladen."; } if (auditStartState && !auditStartState.canStart) { return auditStartState.reason ?? "Audit kann aktuell nicht gestartet werden."; } return null; } export function LeadsReviewTable() { const leads = useQuery(api.leads.list, { limit: 120 }); const [actionMessage, setActionMessage] = useState(null); const [activeFilter, setActiveFilter] = useState("all"); const sortedLeads = useMemo(() => { if (!leads) { return []; } return [...leads].sort((a, b) => b.createdAt - a.createdAt); }, [leads]); const auditStartStates = useQuery( api.pageSpeed.getLeadAuditStartStates, leads ? { leadIds: sortedLeads.map((lead) => lead._id), } : "skip", ); const auditStartStateByLeadId = useMemo(() => { const next = new Map(); for (const state of auditStartStates ?? []) { next.set(state.leadId, state); } return next; }, [auditStartStates]); const filteredLeads = useMemo(() => { if (activeFilter === "high") { return sortedLeads.filter((lead) => lead.priority === "high"); } if (activeFilter === "blocked") { return sortedLeads.filter((lead) => lead.blacklistStatus === "blocked"); } return sortedLeads; }, [activeFilter, sortedLeads]); const leadStatusFilters: Array<{ label: string; value: LeadStatusFilter; count: number }> = [ { label: "Alle Leads", value: "all", count: sortedLeads.length }, { label: "Hohe Priorität", value: "high", count: sortedLeads.filter((lead) => lead.priority === "high").length, }, { label: "Gesperrt", value: "blocked", count: sortedLeads.filter((lead) => lead.blacklistStatus === "blocked").length, }, ]; return (

Lead Intake

Leads prüfen

Kontaktlage, Sperrlisten, Duplikate und Audit-Start bleiben vor jedem Outreach als überprüfbare Entscheidungen sichtbar.

{leadStatusFilters.map((filter) => ( ))}
{leads === undefined ? ( Array.from({ length: 4 }, (_, index) => (
)) ) : sortedLeads.length === 0 ? (

Keine Leads vorhanden

Bitte zuerst eine Kampagne starten oder importieren.

) : filteredLeads.length === 0 ? (

Keine Treffer

Für diesen Filter sind aktuell keine Leads vorhanden.

) : ( filteredLeads.map((lead) => ( )) )}
{actionMessage ? (

{actionMessage}

) : null}
); } function LeadReviewRow({ lead, auditStartState, auditStartStateLoading, onActionMessage, }: { lead: LeadRow; auditStartState?: AuditStartState; auditStartStateLoading: boolean; onActionMessage: (value: string) => void; }) { const [isDialogOpen, setIsDialogOpen] = useState(false); const [draft, setDraft] = useState(() => ({ priority: lead.priority, contactStatus: lead.contactStatus, priorityReason: lead.priorityReason ?? "", contactStatusReason: lead.contactStatusReason ?? "", notes: lead.notes ?? "", reviewEmail: lead.email ?? "", reviewEmailSource: lead.emailSource ?? "", reviewContactPerson: lead.contactPerson ?? "", reviewIsBusinessContactAddress: false, duplicateStatus: (lead.duplicateStatus as LeadDuplicateStatus) ?? "unchecked", blacklistStatus: lead.blacklistStatus, })); const [isSaving, setIsSaving] = useState(false); const [isBlocking, setIsBlocking] = useState(false); const [isStartingAudit, setIsStartingAudit] = useState(false); const [rowMessage, setRowMessage] = useState(null); const reviewUpdate = useMutation(api.leads.reviewUpdate); const requestLeadAudit = useMutation(api.pageSpeed.requestLeadAudit); const location = formatLocation(lead); const emailHref = toEmailHref(lead.email); const phoneHref = toPhoneHref(lead.phone); const websiteHref = toWebsiteHref(lead); const websiteLabel = lead.websiteDomain ?? lead.websiteUrl; const reasonParts = [ lead.priorityReason, lead.contactStatusReason, lead.duplicateReason, lead.blacklistReason, ].filter((item): item is string => Boolean(item)); const manualAuditDisabledReason = auditStartDisabledReason({ lead, auditStartState, isLoading: auditStartStateLoading, isStarting: isStartingAudit, }); const update = async ( payload?: Omit, ) => { setIsSaving(true); setRowMessage(null); onActionMessage(""); try { await reviewUpdate({ id: lead._id, ...payload } as LeadReviewPayload); setRowMessage("Gespeichert"); onActionMessage("Aktualisierung übernommen"); } catch { setRowMessage("Speichern fehlgeschlagen"); } finally { setIsSaving(false); setTimeout(() => setRowMessage(null), 1400); } }; const saveRow = async () => { const reviewEmail = normalizeTextInput(draft.reviewEmail); const reviewEmailSource = normalizeTextInput(draft.reviewEmailSource); const reviewContactPerson = draft.reviewContactPerson.trim(); const shouldUpdateEmailReview = reviewEmail !== normalizeTextInput(lead.email ?? "") || reviewEmailSource !== normalizeTextInput(lead.emailSource ?? "") || reviewContactPerson !== normalizeTextInput(lead.contactPerson ?? ""); if (shouldUpdateEmailReview && !reviewEmail && !lead.email) { setRowMessage("Review-E-Mail setzen, um Kontaktinfos zu ändern."); return; } const payload = { id: lead._id, priority: draft.priority, priorityReason: draft.priorityReason, contactStatus: draft.contactStatus, contactStatusReason: draft.contactStatusReason, notes: draft.notes, duplicateStatus: draft.duplicateStatus, duplicateReason: lead.duplicateReason, blacklistStatus: draft.blacklistStatus, blacklistReason: lead.blacklistReason, reviewIsBusinessContactAddress: draft.reviewIsBusinessContactAddress, ...(shouldUpdateEmailReview ? { reviewEmail: reviewEmail ?? lead.email, reviewEmailSource: reviewEmailSource ?? lead.emailSource, reviewContactPerson, } : {}), }; await update(payload); }; const blockLead = async () => { setIsBlocking(true); await update({ applyBlacklist: true }); setIsBlocking(false); }; const startAudit = async () => { if (manualAuditDisabledReason) { setRowMessage(manualAuditDisabledReason); return; } setIsStartingAudit(true); setRowMessage(null); onActionMessage(""); try { const result = await requestLeadAudit({ leadId: lead._id }); setRowMessage(result.message); onActionMessage(result.message); } catch { setRowMessage("Audit-Start fehlgeschlagen"); } finally { setIsStartingAudit(false); setTimeout(() => setRowMessage(null), 1800); } }; const updateDraft = ( field: T, value: LeadReviewDraft[T], ) => { setDraft((current) => ({ ...current, [field]: value })); }; const detailsId = `lead-review-details-${lead._id}`; const titleId = `lead-review-title-${lead._id}`; const priorityId = `lead-priority-${lead._id}`; const contactStatusId = `lead-contact-status-${lead._id}`; const priorityReasonId = `lead-priority-reason-${lead._id}`; const contactReasonId = `lead-contact-reason-${lead._id}`; const notesId = `lead-notes-${lead._id}`; const reviewEmailId = `lead-review-email-${lead._id}`; const reviewSourceId = `lead-review-source-${lead._id}`; const contactPersonId = `lead-contact-person-${lead._id}`; const businessContactId = `lead-business-contact-${lead._id}`; const duplicateStatusId = `lead-duplicate-status-${lead._id}`; const blacklistStatusId = `lead-blacklist-status-${lead._id}`; return (

{lead.companyName}

{lead.niche ?? "Nische offen"}

{location}

{getLeadPriorityLabel(draft.priority)}

{emailHref ? ( {lead.email} ) : ( Keine E-Mail )}

{lead.phone && phoneHref ? (

{lead.phone}

) : null}

Quelle: {contactSourceLabel(lead)}

{websiteHref && websiteLabel ? (

Website: {websiteLabel}

) : null}
{manualAuditDisabledReason ? (

{manualAuditDisabledReason}

) : null}
{lead.companyName} prüfen Priorität, Kontaktstatus, Duplikate und Kontaktinformationen bearbeiten.
{ updateDraft("priorityReason", event.target.value); }} />
{ updateDraft("contactStatusReason", event.target.value); }} />
{ updateDraft("notes", event.target.value); }} />
{reasonParts.length === 0 ? (

Keine Zusatzhinweise

) : ( reasonParts.map((reason) =>

• {reason}

) )}
{ updateDraft("reviewEmail", event.target.value); }} />
{ updateDraft("reviewEmailSource", event.target.value); }} />
{ updateDraft("reviewContactPerson", event.target.value); }} />
{getLeadDuplicateStatusLabel(draft.duplicateStatus)} {getLeadBlacklistStatusLabel(lead.blacklistStatus)}
{rowMessage ? ( rowMessage === "Speichern fehlgeschlagen" ? (

{rowMessage}

) : (

{rowMessage}

) ) : null}
); }