import { v } from "convex/values"; import { normalizeListLimit } from "./domain"; import { query } from "./_generated/server"; const priority = v.union( v.literal("high"), v.literal("medium"), v.literal("low"), v.literal("defer"), v.literal("blocked"), ); const leadStatus = v.union( v.literal("new"), v.literal("missing_contact"), v.literal("audit_ready"), v.literal("outreach_ready"), v.literal("contacted"), v.literal("replied"), v.literal("do_not_contact"), ); export const getDashboard = query({ args: { campaignId: v.optional(v.id("campaigns")), niche: v.optional(v.string()), postalCode: v.optional(v.string()), radiusKm: v.optional(v.number()), priority: v.optional(priority), status: v.optional(leadStatus), from: v.optional(v.number()), to: v.optional(v.number()), limit: v.optional(v.number()), }, handler: async (ctx, args) => { const limit = normalizeListLimit(args.limit); const campaigns = await ctx.db.query("campaigns").order("desc").take(100); const leads = await ctx.db.query("leads").order("desc").take(500); const audits = await ctx.db.query("audits").order("desc").take(500); const outreach = await ctx.db.query("outreachRecords").order("desc").take(500); const runs = await ctx.db .query("agentRuns") .withIndex("by_type", (q) => q.eq("type", "campaign")) .order("desc") .take(100); const filteredLeads = leads.filter((lead) => { const campaign = lead.campaignId ? campaigns.find((row) => row._id === lead.campaignId) : null; if (args.campaignId && lead.campaignId !== args.campaignId) { return false; } if (args.niche && lead.niche !== args.niche) { return false; } if (args.postalCode && lead.postalCode !== args.postalCode) { return false; } if (args.radiusKm && campaign?.radiusKm !== args.radiusKm) { return false; } if (args.priority && lead.priority !== args.priority) { return false; } if (args.status && lead.contactStatus !== args.status) { return false; } if (args.from && lead.createdAt < args.from) { return false; } if (args.to && lead.createdAt > args.to) { return false; } return true; }); const leadIds = new Set(filteredLeads.map((lead) => lead._id)); const filteredAudits = audits.filter((audit) => leadIds.has(audit.leadId)); const filteredOutreach = outreach.filter((row) => leadIds.has(row.leadId)); const runRows = runs.slice(0, limit).map((run) => ({ id: run._id, campaignId: run.campaignId ?? null, status: run.status, newLeads: run.counters?.leadsCreated ?? 0, skippedDuplicates: 0, skippedBlacklisted: 0, errors: run.counters?.errors ?? 0, auditsGenerated: run.counters?.auditsCreated ?? 0, updatedAt: run.updatedAt, errorSummary: run.errorSummary ?? null, })); return { filters: { campaigns: campaigns.map((campaign) => ({ id: campaign._id, name: campaign.name, })), niches: [...new Set(leads.map((lead) => lead.niche).filter(Boolean))].sort(), postalCodes: [...new Set(leads.map((lead) => lead.postalCode).filter(Boolean))].sort(), }, auditSegments: filteredAudits.map((audit) => { const lead = leads.find((row) => row._id === audit.leadId); const campaign = lead?.campaignId ? campaigns.find((row) => row._id === lead.campaignId) : null; return { path: `/audit/${audit.slug}`, campaignId: lead?.campaignId ?? null, campaignName: campaign?.name ?? "Ohne Kampagne", niche: lead?.niche ?? "Nische offen", region: campaign?.region ?? lead?.postalCode ?? "Region offen", }; }), metrics: { foundLeads: filteredLeads.length, leadsWithContact: filteredLeads.filter((lead) => Boolean(lead.email || lead.phone)).length, missingContact: filteredLeads.filter((lead) => lead.contactStatus === "missing_contact").length, auditsCreated: filteredAudits.length, approvalsOpen: filteredOutreach.filter((row) => row.approvalStatus === "draft").length, emailsSent: filteredOutreach.filter((row) => row.sendStatus === "sent").length, followUpsPlanned: filteredOutreach.filter((row) => row.salesStatus === "follow_up_planned").length, followUpsSent: filteredOutreach.filter((row) => row.salesStatus === "follow_up_sent").length, responses: filteredOutreach.filter((row) => row.salesStatus === "reply_received").length, conversations: filteredOutreach.filter((row) => row.salesStatus === "meeting_scheduled" || row.salesStatus === "proposal_requested" || row.salesStatus === "proposal_sent" || row.salesStatus === "won", ).length, offers: filteredOutreach.filter((row) => row.salesStatus === "proposal_requested" || row.salesStatus === "proposal_sent", ).length, wins: filteredOutreach.filter((row) => row.salesStatus === "won").length, losses: filteredOutreach.filter((row) => row.salesStatus === "lost").length, skippedDuplicates: runRows.reduce((total, run) => total + run.skippedDuplicates, 0), skippedBlacklisted: runRows.reduce((total, run) => total + run.skippedBlacklisted, 0), rybbitAuditOpens: 0, rybbitCtaClicks: 0, }, runs: runRows, }; }, });