"use client"; import { useEffect, useMemo, useState } from "react"; import { useQuery } from "convex/react"; import { Activity, Filter, MousePointerClick } from "lucide-react"; import { api } from "@/convex/_generated/api"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; const metricLabels: Record = { foundLeads: "Gefundene Leads", leadsWithContact: "Mit Kontakt", missingContact: "Kontakt fehlt", auditsCreated: "Audits erstellt", approvalsOpen: "Freigaben offen", emailsSent: "E-Mails gesendet", followUpsPlanned: "Follow-ups geplant", followUpsSent: "Follow-ups gesendet", responses: "Antworten", conversations: "Gespräche", offers: "Angebote", wins: "Gewonnen", losses: "Verloren", skippedDuplicates: "Duplikate übersprungen", skippedBlacklisted: "Sperrliste übersprungen", rybbitAuditOpens: "Audit-Öffnungen", rybbitCtaClicks: "CTA-Klicks", }; export function AnalyticsDashboard() { const dashboard = useQuery(api.campaignMetrics.getDashboard, { limit: 20 }); const [rybbitData, setRybbitData] = useState<{ auditOpens: number; ctaClicks: number; outboundClicks: number; byPath?: Record; } | null>(null); const [rybbitError, setRybbitError] = useState(null); const metricEntries = useMemo(() => { if (!dashboard) { return []; } return Object.entries(dashboard.metrics).filter(([key]) => key in metricLabels); }, [dashboard]); const rybbitGroups = useMemo(() => { if (!dashboard || !rybbitData?.byPath) { return []; } const grouped = new Map(); for (const segment of dashboard.auditSegments) { const metrics = rybbitData.byPath[segment.path]; if (!metrics) { continue; } for (const [kind, label] of [ ["Kampagne", segment.campaignName], ["Nische", segment.niche], ["Region", segment.region], ] as const) { const key = `${kind}:${label}`; const current = grouped.get(key) ?? { label: `${kind}: ${label}`, auditOpens: 0, ctaClicks: 0, }; current.auditOpens += metrics.auditOpens; current.ctaClicks += metrics.ctaClicks; grouped.set(key, current); } } return [...grouped.values()].slice(0, 8); }, [dashboard, rybbitData]); useEffect(() => { let isMounted = true; fetch("/api/internal/rybbit/campaign") .then(async (response) => { const payload = await response.json(); if (!isMounted) { return; } if (!payload.ok) { setRybbitError("Rybbit-Daten konnten nicht geladen werden."); } setRybbitData(payload.data ?? null); }) .catch(() => { if (isMounted) { setRybbitError("Rybbit-Daten konnten nicht geladen werden."); } }); return () => { isMounted = false; }; }, []); if (dashboard === undefined) { return (
); } return (

Kampagnen-Reporting

Analytics

Filter Kampagne, Nische, PLZ, Radius, Priorität, Status und Zeitraum.

Kampagne: {dashboard.filters.campaigns.length}

Nische: {dashboard.filters.niches.length}

PLZ: {dashboard.filters.postalCodes.length}

Radius: Kampagnenradius

Priorität: Hoch/Mittel/Niedrig

Status: Funnel-Status

Zeitraum: Erstellungsdatum

{metricEntries.map(([key, value]) => (

{metricLabels[key]}

{value}

))}
Run-Details Neue Leads, übersprungene Duplikate, Sperrliste, Fehler und erzeugte Audits. {dashboard.runs.length === 0 ? (

Noch keine Kampagnenläufe.

) : ( dashboard.runs.map((run) => (

{run.status}

Leads {run.newLeads} · Audits {run.auditsGenerated} · Fehler {run.errors}

{run.errorSummary ? (

{run.errorSummary}

) : null}
)) )}
Rybbit Audit-Öffnungen und CTA-Aktivität werden bei Bedarf aus der Rybbit API geladen.

Rybbit-Daten konnten nicht geladen werden, wenn API-URL, Site-ID oder API-Key fehlen.

{rybbitError ?

{rybbitError}

: null}

Audit-Öffnungen: {rybbitData?.auditOpens ?? dashboard.metrics.rybbitAuditOpens}

CTA-Klicks: {rybbitData?.ctaClicks ?? dashboard.metrics.rybbitCtaClicks}

Website-Link-Klicks: {rybbitData?.outboundClicks ?? 0}

{rybbitGroups.length > 0 ? (
{rybbitGroups.map((group) => (

{group.label}: {group.auditOpens} Öffnungen · {group.ctaClicks} CTA

))}
) : null}

Public-Audit Tracking läuft nur auf veröffentlichten Audit-Seiten.

); }