diff --git a/app/dashboard/analytics/page.tsx b/app/dashboard/analytics/page.tsx index 3b20cbf..0bbeead 100644 --- a/app/dashboard/analytics/page.tsx +++ b/app/dashboard/analytics/page.tsx @@ -1,5 +1,11 @@ import { AnalyticsDashboard } from "@/components/analytics/analytics-dashboard"; export default function AnalyticsPage() { - return ; + return ( +
+
+ +
+
+ ); } diff --git a/app/dashboard/campaigns/page.tsx b/app/dashboard/campaigns/page.tsx index a94ac69..8ee6041 100644 --- a/app/dashboard/campaigns/page.tsx +++ b/app/dashboard/campaigns/page.tsx @@ -3,7 +3,7 @@ import { CampaignsBoard } from "@/components/campaigns/campaigns-board"; export default function CampaignsPage() { return (
-
+
diff --git a/app/globals.css b/app/globals.css index 2563e3c..661e5a7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -224,7 +224,8 @@ } .campaign-ledger { - overflow-x: auto; + max-width: 100%; + overflow: hidden; border: 1px solid color-mix(in oklch, var(--border), transparent 8%); border-radius: var(--radius); background: @@ -237,20 +238,21 @@ .campaign-ledger-row { display: grid; grid-template-columns: - minmax(10rem, 1.15fr) - minmax(6.5rem, 0.75fr) - minmax(7rem, 0.7fr) - minmax(6rem, 0.55fr) - minmax(7rem, 0.62fr) - minmax(8rem, 0.85fr) - minmax(6rem, 0.55fr) - minmax(7rem, 0.65fr); - gap: 0.75rem; + minmax(7.25rem, 1.08fr) + minmax(5.25rem, 0.62fr) + minmax(5.75rem, 0.66fr) + minmax(5rem, 0.5fr) + minmax(5.5rem, 0.56fr) + minmax(8.5rem, 0.9fr) + minmax(4.75rem, 0.46fr) + minmax(4.25rem, 0.36fr); + gap: 0.5rem; align-items: center; - min-width: 58rem; + width: 100%; + min-width: 0; min-height: 4.75rem; border-top: 1px solid color-mix(in oklch, var(--border), transparent 25%); - padding: 0.85rem 1rem; + padding: 0.8rem 0.625rem; } .campaign-ledger-row:first-child { diff --git a/components/analytics/analytics-dashboard.tsx b/components/analytics/analytics-dashboard.tsx index a459d9b..603a8f6 100644 --- a/components/analytics/analytics-dashboard.tsx +++ b/components/analytics/analytics-dashboard.tsx @@ -2,11 +2,23 @@ import { useEffect, useMemo, useState } from "react"; import { useQuery } from "convex/react"; -import { Activity, Filter, MousePointerClick } from "lucide-react"; +import { + Activity, + BarChart3, + CheckCircle2, + Filter, + MailCheck, + MousePointerClick, + SearchCheck, + ShieldCheck, + TrendingUp, + type LucideIcon, +} 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"; +import { cn } from "@/lib/utils"; const metricLabels: Record = { foundLeads: "Gefundene Leads", @@ -28,6 +40,66 @@ const metricLabels: Record = { rybbitCtaClicks: "CTA-Klicks", }; +type MetricKey = keyof typeof metricLabels; + +const heroMetrics: Array<{ + key: MetricKey; + note: string; + icon: LucideIcon; + surface: string; +}> = [ + { + key: "approvalsOpen", + note: "Wartet auf manuelle Prüfung", + icon: ShieldCheck, + surface: "review-surface", + }, + { + key: "auditsCreated", + note: "Belege im System", + icon: SearchCheck, + surface: "evidence-surface", + }, + { + key: "foundLeads", + note: "Aus Kampagnenläufen", + icon: BarChart3, + surface: "safe-surface", + }, + { + key: "leadsWithContact", + note: "E-Mail oder Telefon vorhanden", + icon: CheckCircle2, + surface: "bg-muted text-muted-foreground", + }, +]; + +const metricGroups: Array<{ + title: string; + description: string; + icon: LucideIcon; + keys: MetricKey[]; +}> = [ + { + title: "Kontaktlage", + description: "Lead-Qualität und fehlende Kontaktwege.", + icon: SearchCheck, + keys: ["missingContact", "skippedBlacklisted", "skippedDuplicates"], + }, + { + title: "Outreach", + description: "Freigaben, Versand und geplante nächste Schritte.", + icon: MailCheck, + keys: ["emailsSent", "followUpsPlanned", "followUpsSent"], + }, + { + title: "Pipeline", + description: "Antworten, Gespräche, Angebote und Abschlüsse.", + icon: TrendingUp, + keys: ["responses", "conversations", "offers", "wins", "losses"], + }, +]; + export function AnalyticsDashboard() { const dashboard = useQuery(api.campaignMetrics.getDashboard, { limit: 20 }); const [rybbitData, setRybbitData] = useState<{ @@ -41,13 +113,6 @@ export function AnalyticsDashboard() { }>; } | 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 []; @@ -108,56 +173,160 @@ export function AnalyticsDashboard() { if (dashboard === undefined) { return (
- - + +
); } + const metricValue = (key: MetricKey) => dashboard.metrics[key] ?? 0; + return (
-
-

Kampagnen-Reporting

-

Analytics

+
+
+

Kampagnen-Reporting

+

+ Analytics +

+

+ Funnel-Signale, Freigaben und Tracking bleiben zusammen sichtbar, + ohne die operative Prüfung aus dem Blick zu verlieren. +

+
+
+

{dashboard.filters.campaigns.length} Kampagnen

+

+ {dashboard.filters.niches.length} Nischen · {dashboard.filters.postalCodes.length} PLZ +

+
- - - - - 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}

-
-
- ))} +
+
+
+

+ + Filter +

+

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

+
+
+ + Radius: Kampagnenradius + + + Priorität: Hoch/Mittel/Niedrig + + + Status: Funnel-Status + + + Zeitraum: Erstellungsdatum + +
+
-
- +
+
+
+

Funnel Snapshot

+

+ Operative Kennzahlen +

+
+

+ Live aus Convex, Tracking bei Bedarf aus Rybbit. +

+
+ +
+ {heroMetrics.map((metric) => { + const Icon = metric.icon; + + return ( +
+
+
+

+ {metricLabels[metric.key]} +

+

+ {metricValue(metric.key)} +

+
+ + + +
+

{metric.note}

+
+ ); + })} +
+ +
+ {metricGroups.map((group) => { + const Icon = group.icon; + + return ( +
+
+ + + +
+

+ {group.title} +

+

+ {group.description} +

+
+
+
+ {group.keys.map((key) => ( +
+ {metricLabels[key]} + {metricValue(key)} +
+ ))} +
+
+ ); + })} +
+
+ +
+ - + Run-Details @@ -166,10 +335,12 @@ export function AnalyticsDashboard() { {dashboard.runs.length === 0 ? ( -

Noch keine Kampagnenläufe.

+

+ Noch keine Kampagnenläufe. +

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

{run.status}

@@ -185,22 +356,46 @@ export function AnalyticsDashboard() { - + - + 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}

+ + {rybbitError ? ( +

+ {rybbitError} +

+ ) : ( +

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

+ )} +
+
+

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) => ( @@ -210,7 +405,9 @@ export function AnalyticsDashboard() { ))}
) : null} -

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

+

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

diff --git a/components/campaigns/campaigns-board.tsx b/components/campaigns/campaigns-board.tsx index 9e125dc..d121936 100644 --- a/components/campaigns/campaigns-board.tsx +++ b/components/campaigns/campaigns-board.tsx @@ -333,7 +333,7 @@ export function CampaignsBoard() { {rowError ?

{rowError}

: null} {actionLabel ?

{actionLabel}

: null} -
+
{campaignsSorted.length === 0 ? ( @@ -438,6 +438,7 @@ export function CampaignsBoard() { onClick={() => openEditDialog(campaign)} disabled={isBusy} aria-label={`${campaign.name} bearbeiten`} + title={`${campaign.name} bearbeiten`} > @@ -445,10 +446,11 @@ export function CampaignsBoard() { variant="outline" onClick={() => runCampaign(campaign)} disabled={isBusy} - size="sm" + size="icon-sm" + aria-label={`${campaign.name} Suchlauf starten`} + title={`${campaign.name} Suchlauf starten`} > - Suchlauf starten
diff --git a/components/dashboard-sidebar.tsx b/components/dashboard-sidebar.tsx index ad7fa8b..7419688 100644 --- a/components/dashboard-sidebar.tsx +++ b/components/dashboard-sidebar.tsx @@ -20,7 +20,7 @@ export function DashboardSidebar() { const { data: session, isPending } = authClient.useSession(); return ( -