"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery } from "convex/react"; import { FunctionReturnType } from "convex/server"; import { MapPin, Pencil, Play, RefreshCcw, Plus } from "lucide-react"; import { api } from "@/convex/_generated/api"; import { Id } from "@/convex/_generated/dataModel"; import { campaignFormDefaults } from "@/lib/campaign-form"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Separator } from "@/components/ui/separator"; import { CampaignFormDialog } from "@/components/campaigns/campaign-form-dialog"; type CampaignsListResult = FunctionReturnType; type CampaignRunsListResult = FunctionReturnType; type CampaignRow = NonNullable[number]; type CampaignRunRow = NonNullable[number]; type RecurrenceLabel = Record; type CurrentRunStatusLabel = { [key: string]: string; }; const recurrenceLabel: RecurrenceLabel = { manual: "manuell", daily: "täglich", weekly: "wöchentlich", monthly: "monatlich", }; const statusLabel: CurrentRunStatusLabel = { running: "Läuft", pending: "Ausstehend", succeeded: "Erledigt", failed: "Fehlgeschlagen", canceled: "Abgebrochen", idle: "Leerlauf", paused: "Pausiert", }; const stepLabel: Record = { campaign_cron_queued: "Cron geplant", campaign_cron_skipped: "Cron übersprungen", campaign_cron_stale_pending: "Timeout bereinigt", lead_discovery: "Lead-Recherche", }; const dateFormatter = new Intl.DateTimeFormat("de-DE", { dateStyle: "short", timeStyle: "short", }); function formatDateTime(value?: number | null): string { if (!value) { return "Nicht gesetzt"; } return dateFormatter.format(new Date(value)); } const formPayloadFromCampaign = (campaign?: CampaignRow | null) => { if (!campaign) { return campaignFormDefaults; } return { status: campaign.status, categoryMode: campaign.categoryMode, recurrence: campaign.recurrence, radiusKm: campaign.radiusKm, maxNewLeadsPerRun: campaign.maxNewLeadsPerRun, maxAuditsPerRun: campaign.maxAuditsPerRun, name: campaign.name, category: campaign.category, customSearchTerm: campaign.customSearchTerm ?? "", postalCode: campaign.postalCode, }; }; const formatNiche = (campaign: CampaignRow): string => { if (campaign.category !== "Anderes") { return campaign.category; } return campaign.customSearchTerm?.trim() ? `${campaign.category}: ${campaign.customSearchTerm}` : campaign.category; }; export function CampaignsBoard() { const campaigns = useQuery(api.campaigns.list, { limit: 100 }); const recentCampaignRuns = useQuery(api.runs.list, { limit: 8, type: "campaign", }); const createCampaign = useMutation(api.campaigns.create); const updateCampaign = useMutation(api.campaigns.update); const setStatus = useMutation(api.campaigns.setStatus); const requestRun = useMutation(api.campaigns.requestRun); const [editingCampaign, setEditingCampaign] = useState(null); const [isFormOpen, setIsFormOpen] = useState(false); const [actionBusyId, setActionBusyId] = useState | null>(null); const [actionLabel, setActionLabel] = useState(null); const [formError, setFormError] = useState(null); const [rowError, setRowError] = useState(null); const actionLabelTimerRef = useRef | null>(null); const clearActionLabelTimer = () => { if (actionLabelTimerRef.current) { clearTimeout(actionLabelTimerRef.current); actionLabelTimerRef.current = null; } }; const setActionLabelWithTimeout = ( label: string, clearAfterMs = 1200, ) => { clearActionLabelTimer(); setActionLabel(label); if (clearAfterMs > 0) { actionLabelTimerRef.current = setTimeout(() => setActionLabel(null), clearAfterMs); } }; useEffect(() => { return () => { clearActionLabelTimer(); }; }, []); const campaignsSorted = useMemo(() => { if (!campaigns) { return []; } return [...campaigns].sort((a, b) => b.createdAt - a.createdAt); }, [campaigns]); const visibleRuns = useMemo(() => { return recentCampaignRuns ?? []; }, [recentCampaignRuns]); const closeDialog = () => { setEditingCampaign(null); setIsFormOpen(false); setFormError(null); }; const openCreateDialog = () => { setEditingCampaign(null); setRowError(null); setIsFormOpen(true); }; const openEditDialog = (campaign: CampaignRow) => { setEditingCampaign(campaign); setRowError(null); setIsFormOpen(true); }; const submitCampaign = async (payload: { status: CampaignRow["status"]; categoryMode: CampaignRow["categoryMode"]; category: string; customSearchTerm?: string; postalCode: string; radiusKm: number; maxNewLeadsPerRun: number; maxAuditsPerRun: number; recurrence: CampaignRow["recurrence"]; countryCode: "DE"; country: "Deutschland"; name: string; }) => { setActionLabel("Speichere..."); setFormError(null); try { if (!editingCampaign) { await createCampaign(payload); } else { await updateCampaign({ id: editingCampaign._id, ...payload, }); } setActionLabelWithTimeout("Gespeichert"); setIsFormOpen(false); setEditingCampaign(null); } catch { setFormError("Speichern fehlgeschlagen."); setActionLabelWithTimeout("Fehler", 2000); } }; const runCampaign = async (campaign: CampaignRow) => { setActionBusyId(campaign._id); setRowError(null); try { await requestRun({ id: campaign._id }); setActionLabelWithTimeout(`${campaign.name}: Lauf gestartet`); } catch { setRowError("Kampagne konnte nicht gestartet werden."); setActionLabelWithTimeout("Kampagne konnte nicht gestartet werden.", 2400); } finally { setActionBusyId(null); } }; const toggleCampaign = async (campaign: CampaignRow) => { const nextStatus = campaign.status === "active" ? "paused" : "active"; setActionBusyId(campaign._id); setRowError(null); try { await setStatus({ id: campaign._id, status: nextStatus }); setActionLabelWithTimeout( `${campaign.name}: ${nextStatus === "active" ? "Aktiviert" : "Pausiert"}`, ); } catch { setRowError("Status konnte nicht geändert werden."); setActionLabelWithTimeout("Status konnte nicht geändert werden.", 2400); } finally { setActionBusyId(null); } }; if (campaigns === undefined) { return (
{Array.from({ length: 4 }, (_, index) => ( ))}
); } return (

Lokale Kampagnenverwaltung

Kampagnen

{formError ?

{formError}

: null} {rowError ?

{rowError}

: null} {actionLabel ?

{actionLabel}

: null} {campaignsSorted.length === 0 ? ( Keine Kampagnen Lege zuerst eine Kampagne mit Kategorie, PLZ und Limits an. ) : (
{campaignsSorted.map((campaign) => (
{campaign.name} {formatNiche(campaign)}
{campaign.status === "active" ? "Aktiv" : "Pausiert"}
{campaign.postalCode}
{campaign.radiusKm} km

Cadence: {recurrenceLabel[campaign.recurrence]}

Limits: L {campaign.maxNewLeadsPerRun}, A{" "} {campaign.maxAuditsPerRun}

Letzter Lauf: {formatDateTime(campaign.lastRunAt)}

Nächster Lauf: {formatDateTime(campaign.nextRunAt)}

Run-Status: {statusLabel[campaign.currentRunStatus] ?? campaign.currentRunStatus}

))}
)} Aktuelle Run-Logs Letzte Kampagnenläufe inklusive Cron-Skips und Fehlerhinweisen. {recentCampaignRuns === undefined ? ( ) : visibleRuns.length === 0 ? (

Noch keine Kampagnenläufe.

) : ( visibleRuns.map((run) => (

{statusLabel[run.status] ?? run.status}

{formatDateTime(run.updatedAt)}

{stepLabel[run.currentStep ?? ""] ?? run.currentStep ?? "Schritt offen"}

{run.currentStep === "campaign_cron_skipped" ? (

Cron wurde übersprungen, weil bereits ein Agentenlauf aktiv war.

) : null} {run.errorSummary ? (

{run.errorSummary}

) : null}
)) )}
); }