"use client"; import { useMemo, useState } from "react"; import { useMutation, useQuery } from "convex/react"; import { FunctionReturnType } from "convex/server"; import { Building2, Mail, MapPin, Phone, 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 } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; 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 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; }; function normalizeTextInput(value: string): string | undefined { const next = value.trim(); return next.length > 0 ? next : undefined; } 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"; } export function LeadsReviewTable() { const leads = useQuery(api.leads.list, { limit: 120 }); const [actionMessage, setActionMessage] = useState(null); const sortedLeads = useMemo(() => { if (!leads) { return []; } return [...leads].sort((a, b) => b.createdAt - a.createdAt); }, [leads]); return (

Leads Review

Leads prüfen

{leads === undefined ? ( ) : sortedLeads.length === 0 ? ( ) : ( {sortedLeads.map((lead) => ( ))} )}
Firma / Ort Kontakt + Quelle Priorität Kontaktstatus Qualität Review-Felder Aktionen

Leads werden geladen…

Keine Leads vorhanden. Bitte zuerst eine Kampagne starten oder importieren.

{actionMessage ? (

{actionMessage}

) : null}
); } function LeadReviewRow({ lead, onActionMessage, }: { lead: LeadRow; onActionMessage: (value: string) => void; }) { 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 [rowMessage, setRowMessage] = useState(null); const reviewUpdate = useMutation(api.leads.reviewUpdate); const location = formatLocation(lead); const reasonParts = [ lead.priorityReason, lead.contactStatusReason, lead.duplicateReason, lead.blacklistReason, ].filter((item): item is string => Boolean(item)); 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 updateDraft = ( field: T, value: LeadReviewDraft[T], ) => { setDraft((current) => ({ ...current, [field]: value })); }; return (

{lead.companyName}

{lead.niche ?? "Nische offen"}

{location}

{lead.address ? (

{lead.address}

) : null}

{lead.email || "Keine E-Mail"}

{lead.phone ? (

{lead.phone}

) : null}

Quelle: {contactSourceLabel(lead)}

{lead.websiteDomain ? (

Domain: {lead.websiteDomain}

) : null}

{getLeadPriorityLabel(draft.priority)}

{getLeadContactStatusLabel(draft.contactStatus)}

Prioritätsgrund

{ updateDraft("priorityReason", event.target.value); }} />

Kontaktstatus-Notiz

{ updateDraft("contactStatusReason", event.target.value); }} />

Notiz

{ updateDraft("notes", event.target.value); }} />
{getLeadDuplicateStatusLabel(draft.duplicateStatus)} {getLeadBlacklistStatusLabel(lead.blacklistStatus)}
{reasonParts.length === 0 ? (

Keine Zusatzhinweise

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

• {reason}

) )}

Review-E-Mail

{ updateDraft("reviewEmail", event.target.value); }} />

Review-Quelle

{ updateDraft("reviewEmailSource", event.target.value); }} />

Ansprechperson

{ updateDraft("reviewContactPerson", event.target.value); }} />

Duplikatstatus

{rowMessage ? (

{rowMessage}

) : null} ); }