feat: build dashboard lead funnel

This commit is contained in:
2026-06-04 12:35:34 +02:00
parent e660ec24aa
commit 07841aea0f
14 changed files with 766 additions and 64 deletions

View File

@@ -29,6 +29,257 @@ export type ReviewQueueItem = {
detail: string;
};
export type LeadPriority = "high" | "medium" | "low" | "defer";
export type LeadContactStatus =
| "new"
| "missing_contact"
| "audit_ready"
| "outreach_ready"
| "contacted"
| "replied"
| "do_not_contact";
export type LeadBlacklistStatus = "clear" | "blocked";
export type OutreachApprovalStatus = "draft" | "approved" | "rejected";
export type OutreachSendStatus = "not_sent" | "queued" | "sent" | "failed";
export type OutreachResponseStatus =
| "none"
| "manual_reply_recorded"
| "no_interest"
| "follow_up_needed";
export type OutreachSalesStatus =
| "follow_up_planned"
| "follow_up_sent"
| "reply_received"
| "not_interested"
| "later"
| "meeting_scheduled"
| "proposal_requested"
| "proposal_sent"
| "won"
| "lost"
| "do_not_pursue";
export type LeadFunnelStageId =
| "missing_contact"
| "audit_ready"
| "review_open"
| "contacted"
| "follow_up"
| "deferred";
export type LeadFunnelStage = {
id: LeadFunnelStageId;
title: string;
description: string;
};
export type LeadFunnelOutreach = {
approvalStatus?: OutreachApprovalStatus | null;
sendStatus?: OutreachSendStatus | null;
responseStatus?: OutreachResponseStatus | null;
salesStatus?: OutreachSalesStatus | null;
};
export type LeadFunnelInput = {
id: string;
companyName: string;
niche?: string | null;
address?: string | null;
city?: string | null;
postalCode?: string | null;
priority: LeadPriority;
contactStatus: LeadContactStatus;
blacklistStatus: LeadBlacklistStatus;
email?: string | null;
phone?: string | null;
contactPerson?: string | null;
websiteDomain?: string | null;
outreach?: LeadFunnelOutreach | null;
};
export type LeadFunnelCard = {
id: string;
stageId: LeadFunnelStageId;
company: string;
niche: string;
location: string;
priorityLabel: string;
contactStatusLabel: string;
nextAction: string;
websiteDomain?: string | null;
contactDetail: string;
};
export type LeadFunnelGroup = {
stage: LeadFunnelStage;
cards: LeadFunnelCard[];
};
export const leadFunnelStages: LeadFunnelStage[] = [
{
id: "missing_contact",
title: "Kontakt fehlt",
description: "Leads ohne belastbare E-Mail oder Telefonnummer.",
},
{
id: "audit_ready",
title: "Audit bereit",
description: "Analyse ist vorbereitet und braucht Einordnung.",
},
{
id: "review_open",
title: "Freigabe offen",
description: "Kontaktstrategie, Audit-Link oder Text warten auf Review.",
},
{
id: "contacted",
title: "Kontaktiert",
description: "Erstkontakt ist erfolgt; Antwort wird manuell gepflegt.",
},
{
id: "follow_up",
title: "Follow-up",
description: "Respektvolle Wiedervorlage ohne automatischen Versand.",
},
{
id: "deferred",
title: "Zurückgestellt",
description: "Nicht jetzt kontaktieren oder bewusst pausieren.",
},
];
const priorityLabels: Record<LeadPriority, string> = {
high: "Hoch",
medium: "Mittel",
low: "Niedrig",
defer: "Zurückstellen",
};
const contactStatusLabels: Record<LeadContactStatus, string> = {
new: "Neu",
missing_contact: "Kontakt fehlt",
audit_ready: "Audit bereit",
outreach_ready: "Freigabe offen",
contacted: "Kontaktiert",
replied: "Antwort erfasst",
do_not_contact: "Nicht kontaktieren",
};
export function toLeadFunnelCard(lead: LeadFunnelInput): LeadFunnelCard {
return {
id: lead.id,
stageId: getLeadFunnelStageId(lead),
company: lead.companyName,
niche: lead.niche ?? "Nische offen",
location: formatLeadLocation(lead),
priorityLabel: priorityLabels[lead.priority],
contactStatusLabel: contactStatusLabels[lead.contactStatus],
nextAction: getLeadNextAction(lead),
websiteDomain: lead.websiteDomain,
contactDetail: formatContactDetail(lead),
};
}
export function groupLeadFunnelCards(
leads: LeadFunnelInput[],
): LeadFunnelGroup[] {
const cards = leads.map(toLeadFunnelCard);
return leadFunnelStages.map((stage) => ({
stage,
cards: cards.filter((card) => card.stageId === stage.id),
}));
}
function getLeadFunnelStageId(lead: LeadFunnelInput): LeadFunnelStageId {
if (
lead.blacklistStatus === "blocked" ||
lead.priority === "defer" ||
lead.contactStatus === "do_not_contact"
) {
return "deferred";
}
if (lead.outreach?.responseStatus === "follow_up_needed") {
return "follow_up";
}
if (
lead.outreach?.salesStatus === "follow_up_planned" &&
lead.outreach.sendStatus === "sent"
) {
return "follow_up";
}
if (
lead.contactStatus === "contacted" ||
lead.contactStatus === "replied" ||
lead.outreach?.sendStatus === "sent"
) {
return "contacted";
}
if (
lead.contactStatus === "outreach_ready" ||
lead.outreach?.approvalStatus === "draft"
) {
return "review_open";
}
if (lead.contactStatus === "audit_ready") {
return "audit_ready";
}
return "missing_contact";
}
function getLeadNextAction(lead: LeadFunnelInput): string {
const stageId = getLeadFunnelStageId(lead);
if (stageId === "deferred") {
return "Zurückstellung prüfen";
}
if (stageId === "follow_up") {
return "Follow-up manuell prüfen";
}
if (stageId === "contacted") {
return "Antwortstatus nachtragen";
}
if (stageId === "review_open") {
return "Freigabe im Review öffnen";
}
if (stageId === "audit_ready") {
return "Audit prüfen";
}
return "Kontaktquelle recherchieren";
}
function formatLeadLocation(lead: LeadFunnelInput): string {
if (lead.postalCode && lead.city) {
return `${lead.postalCode} ${lead.city}`;
}
return lead.city ?? lead.postalCode ?? lead.address ?? "Ort offen";
}
function formatContactDetail(lead: LeadFunnelInput): string {
const details = [lead.email, lead.phone].filter(Boolean);
if (details.length > 0) {
return details.join(" · ");
}
return "Keine Kontaktdaten";
}
export const pipelineStages: PipelineStage[] = [
{
title: "Kampagnen",
@@ -46,9 +297,9 @@ export const pipelineStages: PipelineStage[] = [
},
{
title: "Audit-Freigabe",
description: "Interne Audits warten auf manuelle Pruefung.",
description: "Interne Audits warten auf manuelle Prüfung.",
count: 6,
meta: "2 Seiten bereit zur Veroeffentlichung",
meta: "2 Seiten bereit zur Veröffentlichung",
icon: ShieldCheck,
},
{
@@ -67,7 +318,7 @@ export const dashboardKpis: DashboardKpi[] = [
detail: "aus 3 aktiven Kampagnen",
},
{
label: "Audit-Entwuerfe",
label: "Audit-Entwürfe",
value: "6",
detail: "manuelle Freigabe offen",
},

View File

@@ -17,12 +17,12 @@ export type DashboardNavigationItem = {
};
export const dashboardNavigation: DashboardNavigationItem[] = [
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ label: "Campaigns", href: "/dashboard/campaigns", icon: MapPinned },
{ label: "Übersicht", href: "/dashboard", icon: LayoutDashboard },
{ label: "Kampagnen", href: "/dashboard/campaigns", icon: MapPinned },
{ label: "Leads", href: "/dashboard/leads", icon: UsersRound },
{ label: "Audits", href: "/dashboard/audits", icon: FileSearch },
{ label: "Outreach", href: "/dashboard/outreach", icon: MailCheck },
{ label: "Review", href: "/dashboard/outreach", icon: MailCheck },
{ label: "Analytics", href: "/dashboard/analytics", icon: BarChart3 },
{ label: "Blacklist", href: "/dashboard/blacklist", icon: OctagonMinus },
{ label: "Settings", href: "/dashboard/settings", icon: Settings },
{ label: "Sperrliste", href: "/dashboard/blacklist", icon: OctagonMinus },
{ label: "Einstellungen", href: "/dashboard/settings", icon: Settings },
];