feat: convert campaign and lead views to cards

This commit is contained in:
2026-06-04 17:11:39 +02:00
parent 59824b7336
commit ca42c8d5a6
5 changed files with 563 additions and 485 deletions

View File

@@ -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>
);