diff --git a/app/api/internal/rybbit/audit/route.ts b/app/api/internal/rybbit/audit/route.ts new file mode 100644 index 0000000..619db04 --- /dev/null +++ b/app/api/internal/rybbit/audit/route.ts @@ -0,0 +1,29 @@ +import { fetchRybbitAuditAnalytics } from "@/lib/rybbit-analytics"; + +export async function GET(request: Request) { + const url = new URL(request.url); + const auditPath = url.searchParams.get("path") ?? ""; + + if (!auditPath.startsWith("/audit/")) { + return Response.json({ + ok: false, + error: "Audit-Pfad fehlt.", + data: null, + }, { status: 400 }); + } + + const result = await fetchRybbitAuditAnalytics({ + apiUrl: process.env.RYBBIT_API_URL, + apiKey: process.env.RYBBIT_API_KEY, + siteId: process.env.NEXT_PUBLIC_RYBBIT_SITE_ID, + auditPath, + startDate: url.searchParams.get("startDate") ?? undefined, + endDate: url.searchParams.get("endDate") ?? undefined, + }); + + if (!result.ok) { + return Response.json({ ok: false, error: result.error, data: result.data }); + } + + return Response.json({ ok: true, data: result.data }); +} diff --git a/app/dashboard/analytics/page.tsx b/app/dashboard/analytics/page.tsx index f50c375..3b20cbf 100644 --- a/app/dashboard/analytics/page.tsx +++ b/app/dashboard/analytics/page.tsx @@ -1,10 +1,5 @@ -import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page"; +import { AnalyticsDashboard } from "@/components/analytics/analytics-dashboard"; export default function AnalyticsPage() { - return ( - - ); + return ; } diff --git a/components/analytics/analytics-dashboard.tsx b/components/analytics/analytics-dashboard.tsx new file mode 100644 index 0000000..a5dcce7 --- /dev/null +++ b/components/analytics/analytics-dashboard.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useMemo } from "react"; +import { useQuery } from "convex/react"; +import { Activity, BarChart3, 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 metricEntries = useMemo(() => { + if (!dashboard) { + return []; + } + + return Object.entries(dashboard.metrics).filter(([key]) => key in metricLabels); + }, [dashboard]); + + 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.

+

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

+
+
+
+
+ ); +} diff --git a/components/public-audit/public-audit-page.tsx b/components/public-audit/public-audit-page.tsx index 5de8631..9bba271 100644 --- a/components/public-audit/public-audit-page.tsx +++ b/components/public-audit/public-audit-page.tsx @@ -1,7 +1,9 @@ -import { ArrowRight, CheckCircle2, ExternalLink } from "lucide-react"; +import { CheckCircle2 } from "lucide-react"; import type { PublicAuditRenderState } from "@/lib/audits/public-audit-types"; +import { RybbitTracking } from "./rybbit-tracking"; import { PublicAuditScreenshot } from "./public-audit-screenshot"; +import { TrackedPublicAuditLink } from "./tracked-public-audit-link"; type PublicAuditPageProps = { audit: Extract["audit"]; @@ -10,6 +12,7 @@ type PublicAuditPageProps = { export function PublicAuditPage({ audit }: PublicAuditPageProps) { return (
+
@@ -105,17 +108,11 @@ export function PublicAuditPage({ audit }: PublicAuditPageProps) {

{audit.finalOffer.ctaHref ? ( - - {audit.finalOffer.ctaLabel ?? "Audit besprechen"} - {audit.finalOffer.ctaHref.startsWith("/") ? ( - - ) : ( - - )} - + label={audit.finalOffer.ctaLabel ?? "Audit besprechen"} + /> ) : null}
diff --git a/components/public-audit/rybbit-tracking.tsx b/components/public-audit/rybbit-tracking.tsx new file mode 100644 index 0000000..9749510 --- /dev/null +++ b/components/public-audit/rybbit-tracking.tsx @@ -0,0 +1,27 @@ +import Script from "next/script"; + +type RybbitTrackingProps = { + domain: string; +}; + +export function RybbitTracking({ domain }: RybbitTrackingProps) { + const siteId = process.env.NEXT_PUBLIC_RYBBIT_SITE_ID?.trim(); + if (!siteId) { + return null; + } + + const apiUrl = process.env.RYBBIT_API_URL?.trim() || "https://app.rybbit.io"; + const src = `${apiUrl.replace(/\/$/, "")}/api/script.js`; + + return ( +