feat(vertrieb): Lead-CRM mit Dossiers, Kontakten & Austausch-Log

Neues Lead-Modul im Vertriebsbereich: qualitative Dossiers (Stammdaten,
Pain & Bedarf, Soft Factors, Strategie), Kontaktpersonen, Austausch-Log
mit Follow-up-Daten und Lead→Deal-Promotion. Routen /leads (Liste, nach
Status gruppiert) und /lead/:id (Dossier), Teaser im Bereich Vertrieb.

Migration 0008_lead_crm (lead, lead_contact, lead_touchpoint).
Datenmodell-Tabellen liegen in der gemeinsamen schema.ts (Folge-Commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-05 17:31:14 +02:00
1 parent 8933169ba9
commit d72aa9fbf3
9 files changed
+4291

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
// Anzeige-Metadaten für Leads — zentral, von /leads und /lead/:id genutzt.
export type LeadStatus = 'cold' | 'warm' | 'hot' | 'won' | 'lost';
export const LEAD_STATUS_META: Record<
LeadStatus,
{ label: string; badge: string; dot: string }
> = {
cold: {
label: 'Cold',
badge: 'bg-black/5 text-[var(--color-ink-muted)]',
dot: 'bg-[var(--color-border-strong)]',
},
warm: {
label: 'Warm',
badge: 'bg-[var(--color-ampel-amber-bg)] text-[var(--color-ampel-amber-fg)]',
dot: 'bg-[var(--color-ampel-amber-fg)]',
},
hot: {
label: 'Hot',
badge: 'bg-[var(--color-ampel-red-bg)] text-[var(--color-ampel-red-fg)]',
dot: 'bg-[var(--color-ampel-red-fg)]',
},
won: {
label: 'Won',
badge: 'bg-[var(--color-ampel-green-bg)] text-[var(--color-ampel-green-fg)]',
dot: 'bg-[var(--color-ampel-green-fg)]',
},
lost: {
label: 'Lost',
badge: 'bg-black/5 text-[var(--color-ink-faint)]',
dot: 'bg-[var(--color-ink-faint)]',
},
};
// Reihenfolge für Gruppierung/Sortierung in der Liste.
export const LEAD_STATUS_ORDER: LeadStatus[] = ['hot', 'warm', 'cold', 'won', 'lost'];
export const CONTACT_FUNKTION_LABEL: Record<string, string> = {
entscheider: 'Entscheider',
influencer: 'Influencer',
gatekeeper: 'Gatekeeper',
end_user: 'End-User',
champion: 'Champion',
};
export const TOUCHPOINT_KANAL_LABEL: Record<string, string> = {
call: 'Call',
mail: 'Mail',
linkedin: 'LinkedIn',
termin: 'Termin',
event: 'Event',
sonstiges: 'Sonstiges',
};
+4
View File
@@ -12,6 +12,8 @@ const Reflexion = lazy(() =>
);
const Bereich = lazy(() => import('../routes/bereich').then((m) => ({ default: m.BereichRoute })));
const Projekt = lazy(() => import('../routes/projekt').then((m) => ({ default: m.ProjektRoute })));
const Leads = lazy(() => import('../routes/leads').then((m) => ({ default: m.LeadsRoute })));
const Lead = lazy(() => import('../routes/lead').then((m) => ({ default: m.LeadRoute })));
const Archiv = lazy(() => import('../routes/archiv').then((m) => ({ default: m.ArchivRoute })));
const NotFound = lazy(() =>
import('../routes/not-found').then((m) => ({ default: m.NotFoundRoute })),
@@ -40,6 +42,8 @@ const router = createBrowserRouter([
{ path: 'bereich', element: withSuspense(<Bereich />) },
{ path: 'bereich/:slug', element: withSuspense(<Bereich />) },
{ path: 'projekt/:id', element: withSuspense(<Projekt />) },
{ path: 'leads', element: withSuspense(<Leads />) },
{ path: 'lead/:id', element: withSuspense(<Lead />) },
{ path: 'archiv', element: withSuspense(<Archiv />) },
{ path: '*', element: withSuspense(<NotFound />) },
],
+3
View File
@@ -11,6 +11,8 @@ export const voice = {
blockerResolved: 'Blocker erledigt.',
projectCreated: 'Projekt angelegt.',
projectArchived: 'Projekt archiviert.',
leadCreated: 'Lead angelegt.',
leadPromoted: 'Lead ist jetzt ein Deal.',
saved: 'Gespeichert.',
strong: 'Stark.',
},
@@ -19,6 +21,7 @@ export const voice = {
reflect: 'Reflektieren',
newBlocker: 'Neuer Blocker',
newProject: 'Neues Projekt',
newLead: 'Neuer Lead',
},
hint: {
notCheckedInToday: 'Du hast heute noch nicht gepingt.',