feat: build dashboard lead funnel
This commit is contained in:
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user