feat: convert campaign and lead views to cards
This commit is contained in:
@@ -267,187 +267,81 @@ export function CampaignsBoard() {
|
||||
</CardHeader>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<div className="hidden overflow-x-auto rounded-lg border bg-card md:block">
|
||||
<table className="w-full min-w-[820px] border-separate border-spacing-0">
|
||||
<thead>
|
||||
<tr className="text-left text-sm text-muted-foreground">
|
||||
<th className="sticky left-0 bg-card p-3 font-normal">Kampagne</th>
|
||||
<th className="p-3 font-normal">PLZ / Radius</th>
|
||||
<th className="p-3 font-normal">Cadence</th>
|
||||
<th className="p-3 font-normal">Limits</th>
|
||||
<th className="p-3 font-normal">Status</th>
|
||||
<th className="p-3 font-normal">Lauf</th>
|
||||
<th className="p-3 font-normal">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{campaignsSorted.map((campaign) => (
|
||||
<tr
|
||||
className="border-t"
|
||||
key={campaign._id}
|
||||
<div className="grid gap-3">
|
||||
{campaignsSorted.map((campaign) => (
|
||||
<Card key={campaign._id}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="truncate">{campaign.name}</CardTitle>
|
||||
<CardDescription className="truncate">
|
||||
{formatNiche(campaign)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge
|
||||
variant={campaign.status === "active" ? "default" : "secondary"}
|
||||
>
|
||||
<td className="max-w-[220px] p-3 align-top">
|
||||
<div className="space-y-1">
|
||||
<p className="truncate font-medium">{campaign.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatNiche(campaign)}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
{campaign.status === "active" ? "Aktiv" : "Pausiert"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<td className="max-w-[180px] p-3 align-top">
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p className="inline-flex items-center gap-1">
|
||||
<MapPin className="size-3" />
|
||||
<span>{campaign.postalCode}</span>
|
||||
</p>
|
||||
<p>{campaign.radiusKm} km Umkreis</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="p-3 align-top">
|
||||
<span className="rounded-md bg-muted px-2 py-1 text-sm">
|
||||
{recurrenceLabel[campaign.recurrence]}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="p-3 align-top">
|
||||
<p className="text-sm">
|
||||
Leads: {campaign.maxNewLeadsPerRun} · Audits:{" "}
|
||||
{campaign.maxAuditsPerRun}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
<td className="p-3 align-top">
|
||||
<Badge
|
||||
variant={campaign.status === "active" ? "default" : "secondary"}
|
||||
>
|
||||
{campaign.status === "active" ? "Aktiv" : "Pausiert"}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
<td className="p-3 align-top">
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p>Letzter Lauf: {formatDateTime(campaign.lastRunAt)}</p>
|
||||
<p>Nächster Lauf: {formatDateTime(campaign.nextRunAt)}</p>
|
||||
<p>Run-Status: {statusLabel[campaign.currentRunStatus] ?? campaign.currentRunStatus}</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="p-3 align-top">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
variant="outline"
|
||||
onClick={() => openEditDialog(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
variant="outline"
|
||||
onClick={() => toggleCampaign(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
>
|
||||
<RefreshCcw className="size-4" />
|
||||
{campaign.status === "active" ? "Pausieren" : "Fortfahren"}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => runCampaign(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
>
|
||||
<Play className="size-4" />
|
||||
Jetzt ausführen
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:hidden">
|
||||
{campaignsSorted.map((campaign) => (
|
||||
<Card key={campaign._id}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="truncate">{campaign.name}</CardTitle>
|
||||
<CardDescription className="truncate">
|
||||
{formatNiche(campaign)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge
|
||||
variant={campaign.status === "active" ? "default" : "secondary"}
|
||||
>
|
||||
{campaign.status === "active" ? "Aktiv" : "Pausiert"}
|
||||
</Badge>
|
||||
<CardContent className="grid gap-2 text-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<MapPin className="size-3" />
|
||||
<span>{campaign.postalCode}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<span>{campaign.radiusKm} km</span>
|
||||
</div>
|
||||
<Separator className="bg-border" />
|
||||
<div>
|
||||
<p>Cadence: {recurrenceLabel[campaign.recurrence]}</p>
|
||||
<p>
|
||||
Limits: L {campaign.maxNewLeadsPerRun}, A{" "}
|
||||
{campaign.maxAuditsPerRun}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Letzter Lauf: {formatDateTime(campaign.lastRunAt)}</p>
|
||||
<p className="text-muted-foreground">Nächster Lauf: {formatDateTime(campaign.nextRunAt)}</p>
|
||||
<p className="text-muted-foreground">
|
||||
Run-Status: {statusLabel[campaign.currentRunStatus] ?? campaign.currentRunStatus}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CardContent className="grid gap-2 text-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<MapPin className="size-3" />
|
||||
<span>{campaign.postalCode}</span>
|
||||
</div>
|
||||
<span>{campaign.radiusKm} km</span>
|
||||
</div>
|
||||
<Separator className="bg-border" />
|
||||
<div>
|
||||
<p>Cadence: {recurrenceLabel[campaign.recurrence]}</p>
|
||||
<p>
|
||||
Limits: L {campaign.maxNewLeadsPerRun}, A{" "}
|
||||
{campaign.maxAuditsPerRun}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Letzter Lauf: {formatDateTime(campaign.lastRunAt)}</p>
|
||||
<p className="text-muted-foreground">Nächster Lauf: {formatDateTime(campaign.nextRunAt)}</p>
|
||||
<p className="text-muted-foreground">
|
||||
Run-Status: {statusLabel[campaign.currentRunStatus] ?? campaign.currentRunStatus}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => openEditDialog(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => toggleCampaign(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<RefreshCcw className="size-4" />
|
||||
{campaign.status === "active" ? "Pausieren" : "Fortfahren"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => runCampaign(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<Play className="size-4" />
|
||||
Jetzt ausführen
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => openEditDialog(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => toggleCampaign(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<RefreshCcw className="size-4" />
|
||||
{campaign.status === "active" ? "Pausieren" : "Fortfahren"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => runCampaign(campaign)}
|
||||
disabled={actionBusyId === campaign._id}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<Play className="size-4" />
|
||||
Jetzt ausführen
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type LeadBlacklistStatus,
|
||||
} from "@/lib/dashboard-model";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Card, CardHeader } 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";
|
||||
@@ -148,59 +148,23 @@ export function LeadsReviewTable() {
|
||||
<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 className="mx-auto grid w-full max-w-7xl gap-3">
|
||||
{leads === undefined ? (
|
||||
<p className="rounded-md bg-muted p-4 text-sm">Leads werden geladen…</p>
|
||||
) : sortedLeads.length === 0 ? (
|
||||
<p className="rounded-md border p-4 text-sm text-muted-foreground">
|
||||
Keine Leads vorhanden. Bitte zuerst eine Kampagne starten oder
|
||||
importieren.
|
||||
</p>
|
||||
) : (
|
||||
sortedLeads.map((lead) => (
|
||||
<LeadReviewRow
|
||||
key={lead._id}
|
||||
lead={lead}
|
||||
onActionMessage={setActionMessage}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actionMessage ? (
|
||||
@@ -219,6 +183,7 @@ function LeadReviewRow({
|
||||
lead: LeadRow;
|
||||
onActionMessage: (value: string) => void;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [draft, setDraft] = useState<LeadReviewDraft>(() => ({
|
||||
priority: lead.priority,
|
||||
contactStatus: lead.contactStatus,
|
||||
@@ -313,264 +278,290 @@ function LeadReviewRow({
|
||||
setDraft((current) => ({ ...current, [field]: value }));
|
||||
};
|
||||
|
||||
const detailsId = `lead-review-details-${lead._id}`;
|
||||
|
||||
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>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="grid min-w-0 gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="max-w-full truncate font-medium">
|
||||
{lead.companyName}
|
||||
</p>
|
||||
<p className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Building2 className="size-3 shrink-0" />
|
||||
<span className="inline-flex min-w-0 max-w-full break-words">
|
||||
{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 className="inline-flex min-w-0 max-w-full truncate">
|
||||
{location}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p
|
||||
className={`inline-flex shrink-0 rounded-md border px-2 py-1 text-xs font-medium ${priorityBadgeClass(
|
||||
draft.priority,
|
||||
)}`}
|
||||
>
|
||||
{getLeadPriorityLabel(draft.priority)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<td className="p-3 align-top">
|
||||
<p
|
||||
className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${priorityBadgeClass(
|
||||
draft.priority,
|
||||
)}`}
|
||||
<div className="grid min-w-0 gap-1 text-xs text-muted-foreground">
|
||||
<p className="inline-flex min-w-0 items-center gap-1">
|
||||
<Mail className="size-3 shrink-0" />
|
||||
<span className="max-w-full min-w-0 break-all">
|
||||
{lead.email || "Keine E-Mail"}
|
||||
</span>
|
||||
</p>
|
||||
{lead.phone ? (
|
||||
<p className="inline-flex min-w-0 items-center gap-1">
|
||||
<Phone className="size-3 shrink-0" />
|
||||
<span className="max-w-full min-w-0 break-all">{lead.phone}</span>
|
||||
</p>
|
||||
) : null}
|
||||
<p className="truncate max-w-full">
|
||||
Quelle: {contactSourceLabel(lead)}
|
||||
</p>
|
||||
{lead.websiteDomain ? (
|
||||
<p className="truncate max-w-full">Domain: {lead.websiteDomain}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<div className="border-t p-4 pt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded((previous) => !previous)}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={detailsId}
|
||||
size="sm"
|
||||
>
|
||||
{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>
|
||||
{isExpanded ? "Weniger anzeigen" : "Mehr anzeigen"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id={detailsId}
|
||||
className="grid gap-3 border-t p-4"
|
||||
hidden={!isExpanded}
|
||||
>
|
||||
<div className="grid gap-3 xl:grid-cols-2">
|
||||
<section className="grid gap-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Priorität</p>
|
||||
<div className="mt-2">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Kontaktstatus</p>
|
||||
<div className="mt-2">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Prioritätsgrund</p>
|
||||
<Input
|
||||
value={draft.priorityReason}
|
||||
onChange={(event) => {
|
||||
updateDraft("priorityReason", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Kontaktstatus-Notiz
|
||||
</p>
|
||||
<Input
|
||||
value={draft.contactStatusReason}
|
||||
onChange={(event) => {
|
||||
updateDraft("contactStatusReason", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">Notiz</p>
|
||||
<Input
|
||||
value={draft.notes}
|
||||
onChange={(event) => {
|
||||
updateDraft("notes", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Review-E-Mail</p>
|
||||
<Input
|
||||
value={draft.reviewEmail}
|
||||
onChange={(event) => {
|
||||
updateDraft("reviewEmail", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">Review-Quelle</p>
|
||||
<Input
|
||||
value={draft.reviewEmailSource}
|
||||
onChange={(event) => {
|
||||
updateDraft("reviewEmailSource", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">Ansprechperson</p>
|
||||
<Input
|
||||
value={draft.reviewContactPerson}
|
||||
onChange={(event) => {
|
||||
updateDraft("reviewContactPerson", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label className="mt-2 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>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Duplikatstatus</p>
|
||||
<div className="mt-2">
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">Sperrstatus</label>
|
||||
<div className="mt-2">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-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 grid gap-2 sm:grid-cols-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="text-xs text-muted-foreground">{rowMessage}</p>
|
||||
) : null}
|
||||
</section>
|
||||
</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>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user