372 lines
8.2 KiB
TypeScript
372 lines
8.2 KiB
TypeScript
import {
|
|
Gauge,
|
|
MailCheck,
|
|
MapPinned,
|
|
ShieldCheck,
|
|
UsersRound,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
|
|
export { dashboardNavigation } from "./dashboard-navigation";
|
|
|
|
export type PipelineStage = {
|
|
title: string;
|
|
description: string;
|
|
count: number;
|
|
meta: string;
|
|
icon: LucideIcon;
|
|
};
|
|
|
|
export type DashboardKpi = {
|
|
label: string;
|
|
value: string;
|
|
detail: string;
|
|
};
|
|
|
|
export type ReviewQueueItem = {
|
|
title: string;
|
|
company: string;
|
|
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",
|
|
description: "Aktive Suchlaeufe nach Kategorie, PLZ und Radius.",
|
|
count: 3,
|
|
meta: "1 Lauf heute geplant",
|
|
icon: MapPinned,
|
|
},
|
|
{
|
|
title: "Lead-Recherche",
|
|
description: "Neue Places-Quellen, Kontaktluecken und Dubletten.",
|
|
count: 18,
|
|
meta: "5 Leads brauchen E-Mail-Quelle",
|
|
icon: UsersRound,
|
|
},
|
|
{
|
|
title: "Audit-Freigabe",
|
|
description: "Interne Audits warten auf manuelle Prüfung.",
|
|
count: 6,
|
|
meta: "2 Seiten bereit zur Veröffentlichung",
|
|
icon: ShieldCheck,
|
|
},
|
|
{
|
|
title: "Outreach",
|
|
description: "Freigegebene E-Mails und Telefon-Skripte.",
|
|
count: 4,
|
|
meta: "Keine automatische Kontaktaufnahme",
|
|
icon: MailCheck,
|
|
},
|
|
];
|
|
|
|
export const dashboardKpis: DashboardKpi[] = [
|
|
{
|
|
label: "Neue Leads",
|
|
value: "18",
|
|
detail: "aus 3 aktiven Kampagnen",
|
|
},
|
|
{
|
|
label: "Audit-Entwürfe",
|
|
value: "6",
|
|
detail: "manuelle Freigabe offen",
|
|
},
|
|
{
|
|
label: "Outreach bereit",
|
|
value: "4",
|
|
detail: "E-Mail und Telefon-Skript",
|
|
},
|
|
{
|
|
label: "Antworten",
|
|
value: "2",
|
|
detail: "manuell nachzutragen",
|
|
},
|
|
];
|
|
|
|
export const reviewQueue: ReviewQueueItem[] = [
|
|
{
|
|
title: "Audit-Freigabe pruefen",
|
|
company: "Malerbetrieb Klein",
|
|
detail: "Mobile Kontaktfuehrung und lokaler CTA fehlen.",
|
|
},
|
|
{
|
|
title: "Kontaktstrategie bestaetigen",
|
|
company: "Physio am Park",
|
|
detail: "Telefon zuerst, E-Mail nach persoenlicher Einordnung.",
|
|
},
|
|
{
|
|
title: "Follow-up vormerken",
|
|
company: "Tischlerei Weber",
|
|
detail: "Respektvolles Follow-up in 5 Tagen, kein Autoversand.",
|
|
},
|
|
];
|
|
|
|
export const pipelineHealth = [
|
|
{
|
|
label: "Recherche",
|
|
value: "hoch",
|
|
icon: Gauge,
|
|
},
|
|
{
|
|
label: "Freigabe",
|
|
value: "manuell",
|
|
icon: ShieldCheck,
|
|
},
|
|
{
|
|
label: "Versand",
|
|
value: "gesperrt bis Review",
|
|
icon: MailCheck,
|
|
},
|
|
];
|