149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
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,
|
|
};
|
|
},
|
|
});
|