"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; type BlacklistEntry = NonNullable[number]; type BlacklistType = | "domain" | "email" | "phone" | "company" | "google_place_id" | "source_business_id"; const blacklistTypeOptions: BlacklistType[] = [ "domain", "email", "phone", "company", "source_business_id", "google_place_id", ]; function labelForType(type: BlacklistType): string { if (type === "source_business_id") { return "Source Business ID"; } 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("domain"); const [value, setValue] = useState(""); const [note, setNote] = useState(""); const [rowBusyId, setRowBusyId] = useState | null>(null); const [formBusy, setFormBusy] = useState(false); const [statusMessage, setStatusMessage] = useState(null); const [statusError, setStatusError] = useState(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 (

Blacklist-Verwaltung

Sperrliste

Neuen Eintrag anlegen

Neue Einträge wirken sofort: bestehende und neue Leads mit passendem Typ werden automatisch blockiert.

setValue(event.target.value)} placeholder="Wert" /> setNote(event.target.value)} placeholder="Notiz (optional)" />
{statusError ? (

{statusError}

) : null} {statusMessage ? (

{statusMessage}

) : null}
{entries === undefined ? ( ) : entriesSorted.length === 0 ? ( ) : ( {entriesSorted.map((entry) => ( { 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} /> ))} )}
Typ Wert Notiz Normalisiert Erstellt Aktion

Sperrliste wird geladen…

Noch keine Sperreinträge.

); } function BlacklistEntryRow({ entry, onDelete, onUpdate, isBusy, }: { entry: BlacklistEntry; onDelete: (id: Id<"blacklistEntries">) => Promise; onUpdate: (next: { id: Id<"blacklistEntries">; type?: BlacklistType; value?: string; note?: string; }) => Promise; isBusy: boolean; }) { const [isEditing, setIsEditing] = useState(false); const [type, setType] = useState(entry.type); const [value, setValue] = useState(entry.value); const [note, setNote] = useState(entry.note ?? ""); const [rowMessage, setRowMessage] = useState(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 ( {isEditing ? ( ) : ( {labelForType(entry.type)} )} {isEditing ? ( setValue(event.target.value)} /> ) : (

{entry.value}

)} {isEditing ? ( setNote(event.target.value)} /> ) : (

{entry.note ?? "—"}

)}

{entry.normalizedValue}

{formatDate(entry.createdAt)}

{isEditing ? ( <> ) : ( <> )}
{rowMessage ? (

{rowMessage}

) : null} ); }