feat: add lead qualification workflow

This commit is contained in:
2026-06-04 16:09:47 +02:00
parent 15d8bfeb66
commit 59824b7336
19 changed files with 2833 additions and 78 deletions

View File

@@ -0,0 +1,376 @@
"use client";
import { useMemo, useState } from "react";
import { useMutation, useQuery } from "convex/react";
import { FunctionReturnType } from "convex/server";
import { Id } from "@/convex/_generated/dataModel";
import { api } from "@/convex/_generated/api";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
type BlacklistResult = FunctionReturnType<typeof api.blacklist.list>;
type BlacklistEntry = NonNullable<BlacklistResult>[number];
type BlacklistType =
| "domain"
| "email"
| "phone"
| "company"
| "google_place_id";
const blacklistTypeOptions: BlacklistType[] = [
"domain",
"email",
"phone",
"company",
"google_place_id",
];
function labelForType(type: BlacklistType): string {
if (type === "google_place_id") {
return "Google Place ID";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
function formatDate(value: number): string {
return new Intl.DateTimeFormat("de-DE", {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(value));
}
export function BlacklistManager() {
const entries = useQuery(api.blacklist.list, { limit: 150 }) as
| BlacklistResult
| undefined;
const createEntry = useMutation(api.blacklist.create);
const updateEntry = useMutation(api.blacklist.update);
const removeEntry = useMutation(api.blacklist.remove);
const [type, setType] = useState<BlacklistType>("domain");
const [value, setValue] = useState("");
const [note, setNote] = useState("");
const [rowBusyId, setRowBusyId] = useState<Id<"blacklistEntries"> | null>(null);
const [formBusy, setFormBusy] = useState(false);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [statusError, setStatusError] = useState<string | null>(null);
const entriesSorted = useMemo(() => {
if (!entries) {
return [];
}
return [...entries].sort((a, b) => b.createdAt - a.createdAt);
}, [entries]);
const submitNew = async () => {
if (!value.trim()) {
setStatusError("Bitte ein Sperrwert eintragen.");
return;
}
setFormBusy(true);
setStatusError(null);
setStatusMessage(null);
try {
await createEntry({
type,
value: value.trim(),
note: note.trim().length > 0 ? note.trim() : undefined,
});
setValue("");
setNote("");
setStatusMessage("Eintrag hinzugefügt.");
} catch {
setStatusError("Eintrag konnte nicht erstellt werden.");
} finally {
setFormBusy(false);
}
};
const remove = async (id: Id<"blacklistEntries">) => {
setRowBusyId(id);
setStatusError(null);
setStatusMessage(null);
try {
await removeEntry({ id });
setStatusMessage("Eintrag gelöscht.");
} catch {
setStatusError("Eintrag konnte nicht entfernt werden.");
} finally {
setRowBusyId(null);
}
};
return (
<section className="space-y-4 px-4 py-5 sm:px-6 lg:px-8">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-2">
<p className="text-sm text-muted-foreground">Blacklist-Verwaltung</p>
<h1 className="text-2xl font-semibold tracking-normal">Sperrliste</h1>
</div>
<div className="mx-auto w-full max-w-7xl">
<Card className="p-4 space-y-4">
<h2 className="text-sm font-medium">Neuen Eintrag anlegen</h2>
<p className="text-sm text-muted-foreground">
Neue Einträge wirken sofort: bestehende und neue Leads mit passendem
Typ werden automatisch blockiert.
</p>
<div className="grid gap-3 sm:grid-cols-[150px_1fr_1fr_auto]">
<Select
value={type}
onValueChange={(nextType) => setType(nextType as BlacklistType)}
>
<SelectTrigger>
<SelectValue placeholder="Typ" />
</SelectTrigger>
<SelectContent>
{blacklistTypeOptions.map((item) => (
<SelectItem value={item} key={item}>
{labelForType(item)}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="Wert"
/>
<Input
value={note}
onChange={(event) => setNote(event.target.value)}
placeholder="Notiz (optional)"
/>
<Button
onClick={submitNew}
disabled={formBusy || !value.trim()}
className="justify-start sm:w-auto"
>
Eintrag speichern
</Button>
</div>
{statusError ? (
<p className="text-sm text-destructive" role="status">
{statusError}
</p>
) : null}
{statusMessage ? (
<p className="text-sm text-muted-foreground" role="status">
{statusMessage}
</p>
) : null}
</Card>
</div>
<div className="mx-auto w-full max-w-7xl">
<Card>
<div className="overflow-x-auto">
<div className="min-w-[880px]">
<table className="w-full border-separate border-spacing-0 text-sm">
<thead>
<tr className="text-left text-xs text-muted-foreground">
<th className="p-3 font-normal">Typ</th>
<th className="p-3 font-normal">Wert</th>
<th className="p-3 font-normal">Notiz</th>
<th className="p-3 font-normal">Normalisiert</th>
<th className="p-3 font-normal">Erstellt</th>
<th className="p-3 font-normal">Aktion</th>
</tr>
</thead>
{entries === undefined ? (
<tbody>
<tr>
<td className="p-3" colSpan={6}>
<p className="rounded-md bg-muted p-4 text-sm">
Sperrliste wird geladen
</p>
</td>
</tr>
</tbody>
) : entriesSorted.length === 0 ? (
<tbody>
<tr>
<td className="p-3" colSpan={6}>
<p className="rounded-md border p-4 text-sm text-muted-foreground">
Noch keine Sperreinträge.
</p>
</td>
</tr>
</tbody>
) : (
<tbody>
{entriesSorted.map((entry) => (
<BlacklistEntryRow
key={entry._id}
entry={entry}
onDelete={remove}
onUpdate={async (nextEntry) => {
setRowBusyId(nextEntry.id);
setStatusError(null);
setStatusMessage(null);
try {
await updateEntry(nextEntry);
setStatusMessage("Eintrag aktualisiert.");
} catch {
setStatusError("Eintrag konnte nicht gespeichert werden.");
} finally {
setRowBusyId(null);
}
}}
isBusy={rowBusyId === entry._id}
/>
))}
</tbody>
)}
</table>
</div>
</div>
</Card>
</div>
</section>
);
}
function BlacklistEntryRow({
entry,
onDelete,
onUpdate,
isBusy,
}: {
entry: BlacklistEntry;
onDelete: (id: Id<"blacklistEntries">) => Promise<void>;
onUpdate: (next: {
id: Id<"blacklistEntries">;
type?: BlacklistType;
value?: string;
note?: string;
}) => Promise<void>;
isBusy: boolean;
}) {
const [isEditing, setIsEditing] = useState(false);
const [type, setType] = useState<BlacklistType>(entry.type);
const [value, setValue] = useState(entry.value);
const [note, setNote] = useState(entry.note ?? "");
const [rowMessage, setRowMessage] = useState<string | null>(null);
const submitUpdate = async () => {
if (!value.trim()) {
setRowMessage("Wert darf nicht leer sein.");
return;
}
setRowMessage(null);
await onUpdate({
id: entry._id,
type,
value: value.trim(),
note: note.trim().length > 0 ? note.trim() : undefined,
});
setIsEditing(false);
setRowMessage("Gespeichert");
};
return (
<tr className="border-t">
<td className="p-3 align-top">
{isEditing ? (
<Select value={type} onValueChange={(nextType) => setType(nextType as BlacklistType)}>
<SelectTrigger className="max-w-[168px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{blacklistTypeOptions.map((item) => (
<SelectItem value={item} key={item}>
{labelForType(item)}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Badge variant="secondary">{labelForType(entry.type)}</Badge>
)}
</td>
<td className="max-w-[260px] p-3 align-top">
{isEditing ? (
<Input value={value} onChange={(event) => setValue(event.target.value)} />
) : (
<p className="truncate">{entry.value}</p>
)}
</td>
<td className="max-w-[300px] p-3 align-top">
{isEditing ? (
<Input value={note} onChange={(event) => setNote(event.target.value)} />
) : (
<p className="truncate text-muted-foreground">
{entry.note ?? "—"}
</p>
)}
</td>
<td className="p-3 align-top">
<p className="truncate">{entry.normalizedValue}</p>
</td>
<td className="p-3 align-top">
<p className="text-muted-foreground">{formatDate(entry.createdAt)}</p>
</td>
<td className="p-3 align-top">
<div className="grid gap-2 sm:grid-cols-2">
{isEditing ? (
<>
<Button size="sm" onClick={submitUpdate} disabled={isBusy}>
Speichern
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(false)}
disabled={isBusy}
>
Abbrechen
</Button>
</>
) : (
<>
<Button
size="sm"
variant="outline"
onClick={() => setIsEditing(true)}
disabled={isBusy}
>
Bearbeiten
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => onDelete(entry._id)}
disabled={isBusy}
>
Löschen
</Button>
</>
)}
</div>
{rowMessage ? (
<p className="mt-2 text-xs text-muted-foreground">{rowMessage}</p>
) : null}
</td>
</tr>
);
}

View File

@@ -0,0 +1,576 @@
"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<typeof api.leads.list>;
type LeadRow = NonNullable<LeadsListResult>[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<string | null>(null);
const sortedLeads = useMemo(() => {
if (!leads) {
return [];
}
return [...leads].sort((a, b) => b.createdAt - a.createdAt);
}, [leads]);
return (
<section className="space-y-4 px-4 py-5 sm:px-6 lg:px-8">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-2">
<p className="text-sm text-muted-foreground">Leads Review</p>
<h1 className="text-2xl font-semibold tracking-normal">Leads prüfen</h1>
</div>
<div className="mx-auto w-full max-w-7xl">
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<div className="min-w-[1150px]">
<table className="w-full border-separate border-spacing-0 text-sm">
<thead>
<tr className="text-left text-xs text-muted-foreground">
<th className="p-3 font-normal">Firma / Ort</th>
<th className="p-3 font-normal">Kontakt + Quelle</th>
<th className="p-3 font-normal">Priorität</th>
<th className="p-3 font-normal">Kontaktstatus</th>
<th className="p-3 font-normal">Qualität</th>
<th className="p-3 font-normal">Review-Felder</th>
<th className="p-3 font-normal">Aktionen</th>
</tr>
</thead>
{leads === undefined ? (
<tbody>
<tr>
<td className="p-3" colSpan={7}>
<p className="rounded-md bg-muted p-4 text-sm">
Leads werden geladen
</p>
</td>
</tr>
</tbody>
) : sortedLeads.length === 0 ? (
<tbody>
<tr>
<td className="p-3" colSpan={7}>
<p className="rounded-md border p-4 text-sm text-muted-foreground">
Keine Leads vorhanden. Bitte zuerst eine Kampagne starten
oder importieren.
</p>
</td>
</tr>
</tbody>
) : (
<tbody>
{sortedLeads.map((lead) => (
<LeadReviewRow
key={lead._id}
lead={lead}
onActionMessage={setActionMessage}
/>
))}
</tbody>
)}
</table>
</div>
</div>
</Card>
</div>
{actionMessage ? (
<p className="mx-auto max-w-7xl text-sm text-muted-foreground">
{actionMessage}
</p>
) : null}
</section>
);
}
function LeadReviewRow({
lead,
onActionMessage,
}: {
lead: LeadRow;
onActionMessage: (value: string) => void;
}) {
const [draft, setDraft] = useState<LeadReviewDraft>(() => ({
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<string | null>(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<LeadReviewPayload, "id">,
) => {
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 = <T extends keyof LeadReviewDraft>(
field: T,
value: LeadReviewDraft[T],
) => {
setDraft((current) => ({ ...current, [field]: value }));
};
return (
<tr className="border-t">
<td className="max-w-[260px] p-3 align-top">
<p className="font-medium">{lead.companyName}</p>
<p className="mt-1 inline-flex items-center gap-1 truncate text-xs text-muted-foreground">
<Building2 className="size-3 shrink-0" />
<span className="truncate">{lead.niche ?? "Nische offen"}</span>
</p>
<p className="mt-2 inline-flex items-center gap-1 text-xs text-muted-foreground">
<MapPin className="size-3 shrink-0" />
<span>{location}</span>
</p>
{lead.address ? (
<p className="mt-1 max-w-full truncate text-xs text-muted-foreground">
{lead.address}
</p>
) : null}
</td>
<td className="max-w-[260px] p-3 align-top">
<p className="inline-flex w-full items-start gap-1 text-sm">
<Mail className="mt-0.5 size-3 shrink-0" />
<span className="min-w-0 break-all">
{lead.email || "Keine E-Mail"}
</span>
</p>
{lead.phone ? (
<p className="mt-2 inline-flex w-full items-start gap-1 text-xs text-muted-foreground">
<Phone className="size-3 shrink-0" />
<span className="break-all">{lead.phone}</span>
</p>
) : null}
<p className="mt-2 text-xs text-muted-foreground">
Quelle: {contactSourceLabel(lead)}
</p>
{lead.websiteDomain ? (
<p className="mt-1 text-xs text-muted-foreground">
Domain: {lead.websiteDomain}
</p>
) : null}
</td>
<td className="p-3 align-top">
<p
className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${priorityBadgeClass(
draft.priority,
)}`}
>
{getLeadPriorityLabel(draft.priority)}
</p>
<div className="mt-2 max-w-[160px]">
<Select
value={draft.priority}
onValueChange={(nextPriority) =>
updateDraft("priority", nextPriority as LeadPriority)
}
>
<SelectTrigger>
<SelectValue placeholder="Priorität" />
</SelectTrigger>
<SelectContent>
{leadPriorityOptions.map((value) => (
<SelectItem value={value} key={value}>
{getLeadPriorityLabel(value)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</td>
<td className="p-3 align-top">
<Badge variant="outline">
{getLeadContactStatusLabel(draft.contactStatus)}
</Badge>
<div className="mt-2 max-w-[180px]">
<Select
value={draft.contactStatus}
onValueChange={(nextStatus) =>
updateDraft("contactStatus", nextStatus as LeadContactStatus)
}
>
<SelectTrigger>
<SelectValue placeholder="Kontaktstatus" />
</SelectTrigger>
<SelectContent>
{leadContactStatusOptions.map((status) => (
<SelectItem value={status} key={status}>
{getLeadContactStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</td>
<td className="max-w-[220px] p-3 align-top">
<div className="grid gap-2">
<p className="text-xs text-muted-foreground">Prioritätsgrund</p>
<Input
value={draft.priorityReason}
onChange={(event) => {
updateDraft("priorityReason", event.target.value);
}}
/>
</div>
<div className="mt-2 grid gap-2">
<p className="text-xs text-muted-foreground">Kontaktstatus-Notiz</p>
<Input
value={draft.contactStatusReason}
onChange={(event) => {
updateDraft("contactStatusReason", event.target.value);
}}
/>
</div>
<div className="mt-2 grid gap-2">
<p className="text-xs text-muted-foreground">Notiz</p>
<Input
value={draft.notes}
onChange={(event) => {
updateDraft("notes", event.target.value);
}}
/>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Badge
variant={duplicateBadgeVariant(draft.duplicateStatus)}
title={lead.duplicateReason ?? undefined}
>
{getLeadDuplicateStatusLabel(draft.duplicateStatus)}
</Badge>
<Badge
variant={lead.blacklistStatus === "blocked" ? "destructive" : "secondary"}
>
{getLeadBlacklistStatusLabel(lead.blacklistStatus)}
</Badge>
</div>
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
{reasonParts.length === 0 ? (
<p>Keine Zusatzhinweise</p>
) : (
reasonParts.map((reason) => <p key={reason}> {reason}</p>)
)}
</div>
</td>
<td className="min-w-[260px] p-3 align-top">
<div className="grid gap-2">
<p className="text-xs text-muted-foreground">Review-E-Mail</p>
<Input
value={draft.reviewEmail}
onChange={(event) => {
updateDraft("reviewEmail", event.target.value);
}}
/>
</div>
<div className="mt-2 grid gap-2">
<p className="text-xs text-muted-foreground">Review-Quelle</p>
<Input
value={draft.reviewEmailSource}
onChange={(event) => {
updateDraft("reviewEmailSource", event.target.value);
}}
/>
</div>
<div className="mt-2 grid gap-2">
<p className="text-xs text-muted-foreground">Ansprechperson</p>
<Input
value={draft.reviewContactPerson}
onChange={(event) => {
updateDraft("reviewContactPerson", event.target.value);
}}
/>
</div>
<label className="mt-3 inline-flex items-center gap-2 text-xs text-muted-foreground">
<Switch
checked={draft.reviewIsBusinessContactAddress}
onCheckedChange={(checked) => {
updateDraft("reviewIsBusinessContactAddress", checked);
}}
/>
Genannte E-Mail als Business-Kontakt
</label>
<div className="mt-3 grid gap-2">
<p className="text-xs text-muted-foreground">Duplikatstatus</p>
<Select
value={draft.duplicateStatus}
onValueChange={(nextStatus) =>
updateDraft("duplicateStatus", nextStatus as LeadDuplicateStatus)
}
>
<SelectTrigger>
<SelectValue placeholder="Duplikatstatus" />
</SelectTrigger>
<SelectContent>
{leadDuplicateStatusOptions.map((status) => (
<SelectItem value={status} key={status}>
{getLeadDuplicateStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="mt-2">
<label className="text-xs text-muted-foreground">Sperrstatus</label>
<Select
value={draft.blacklistStatus}
onValueChange={(nextStatus) =>
updateDraft("blacklistStatus", nextStatus as LeadBlacklistStatus)
}
>
<SelectTrigger>
<SelectValue placeholder="Sperrstatus" />
</SelectTrigger>
<SelectContent>
{leadBlacklistStatusOptions.map((status) => (
<SelectItem value={status} key={status}>
{getLeadBlacklistStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</td>
<td className="max-w-[170px] p-3 align-top">
<div className="grid gap-2">
<Button
onClick={saveRow}
disabled={isSaving || isBlocking}
size="sm"
>
<span>Speichern</span>
</Button>
<Button
variant="destructive"
onClick={blockLead}
disabled={isSaving || isBlocking}
size="sm"
>
<ShieldAlert className="size-4" />
Sperren
</Button>
</div>
{rowMessage ? (
<p className="mt-2 text-xs text-muted-foreground">{rowMessage}</p>
) : null}
</td>
</tr>
);
}