Files
everythingbutwork/packages/shared/src/schemas/lead.ts
T
dejanandClaude Opus 4.8 19db774c4c
ci / quality (push) Failing after 44s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 54s
feat(leads): Leads universell mit Projekten verknüpfen
Ein Projekt lebt in genau einem Heimat-Bereich und wird einmal angelegt;
Leads bekommen eine optionale Verknüpfung dorthin (lead.projectId), egal in
welchem Bereich das Projekt wohnt — kein redundantes Anlegen mehr.

- Migration 0011: lead.project_id (FK → project, ON DELETE SET NULL).
- Lead anlegen / CSV-Import / Lead-Detail: Projekt zuordnen; CSV ordnet alle
  importierten Leads einem Projekt zu.
- Projektseite: neue "Leads"-Sektion (Vertriebs-Sicht des Projekts).
- Vertrieb: "Projekte mit Vertriebsaktivität" — bereichsübergreifend alle
  Projekte mit verknüpften Leads (so erscheint ein Entwicklungsprojekt hier,
  WEIL es Leads hat). Rein interne Projekte ohne Leads tauchen nicht auf.
- Lead-Liste/Detail zeigen das verknüpfte Projekt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:08:59 +02:00

149 lines
5.6 KiB
TypeScript

import { z } from 'zod';
// Status & Enums --------------------------------------------------------------
export const leadStatusSchema = z.enum(['cold', 'warm', 'hot', 'won', 'lost']);
export const LEAD_STATUSES = leadStatusSchema.options;
export const contactFunktionSchema = z.enum([
'entscheider',
'influencer',
'gatekeeper',
'end_user',
'champion',
]);
export const CONTACT_FUNKTIONEN = contactFunktionSchema.options;
export const touchpointKanalSchema = z.enum([
'call',
'mail',
'linkedin',
'termin',
'event',
'sonstiges',
]);
export const TOUCHPOINT_KANAELE = touchpointKanalSchema.options;
// JSONB-Substrukturen ---------------------------------------------------------
export const offeneFrageSchema = z.object({
text: z.string().min(1).max(500),
done: z.boolean(),
});
export const anhangSchema = z.object({
label: z.string().min(1).max(200),
url: z.string().min(1).max(2000),
});
// Lead-Kernfelder (alle optional außer firma). Wird für create + update genutzt.
const leadCoreFields = {
firma: z.string().min(1).max(200),
status: leadStatusSchema,
score: z.number().int().min(0).max(10),
ownerUserId: z.string().uuid().nullable(),
projectId: z.string().uuid().nullable(),
branche: z.string().max(200).nullable(),
subSegment: z.string().max(200).nullable(),
standorte: z.string().max(500).nullable(),
mitarbeiterzahl: z.string().max(100).nullable(),
umsatz: z.string().max(100).nullable(),
website: z.string().max(500).nullable(),
quelle: z.string().max(500).nullable(),
painPoint: z.string().max(2000).nullable(),
statusQuoKosten: z.string().max(2000).nullable(),
bestehendeLoesung: z.string().max(1000).nullable(),
wechselgrund: z.string().max(2000).nullable(),
persoenlicherErfolg: z.string().max(2000).nullable(),
budgetIndikation: z.string().max(500).nullable(),
entscheidungshorizont: z.string().max(500).nullable(),
compellingEvent: z.string().max(1000).nullable(),
mitbewerber: z.string().max(1000).nullable(),
buyingCenter: z.string().max(1000).nullable(),
beschaffungsweg: z.string().max(500).nullable(),
kommunikationsstil: z.string().max(1000).nullable(),
vorlieben: z.string().max(1000).nullable(),
hotButtons: z.string().max(1000).nullable(),
noGos: z.string().max(1000).nullable(),
druckpunkte: z.string().max(1000).nullable(),
anker: z.string().max(1000).nullable(),
vertrauenslevel: z.number().int().min(1).max(5).nullable(),
nutzenHypothese: z.string().max(1000).nullable(),
differenzierer: z.string().max(1000).nullable(),
naechsterSchritt: z.string().max(1000).nullable(),
naechsterSchrittDatum: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable(),
risiken: z.string().max(1000).nullable(),
planB: z.string().max(1000).nullable(),
offeneFragen: z.array(offeneFrageSchema).max(100),
anhaenge: z.array(anhangSchema).max(50),
};
// Create: nur firma ist Pflicht, Rest optional mit sinnvollen Defaults.
export const createLeadInput = z.object({
firma: leadCoreFields.firma,
status: leadStatusSchema.default('cold'),
score: z.number().int().min(0).max(10).default(0),
ownerUserId: z.string().uuid().optional(),
projectId: z.string().uuid().optional(),
branche: z.string().max(200).optional(),
subSegment: z.string().max(200).optional(),
website: z.string().max(500).optional(),
quelle: z.string().max(500).optional(),
});
export type CreateLeadInput = z.infer<typeof createLeadInput>;
export const leadsForProjectInput = z.object({ projectId: z.string().uuid() });
// Update: id + beliebige Teilmenge der Kernfelder (partial patch).
export const updateLeadInput = z
.object({ id: z.string().uuid() })
.merge(z.object(leadCoreFields).partial());
export type UpdateLeadInput = z.infer<typeof updateLeadInput>;
export const leadIdInput = z.object({ id: z.string().uuid() });
// Kontakte --------------------------------------------------------------------
export const addContactInput = z.object({
leadId: z.string().uuid(),
name: z.string().min(1).max(200),
rolle: z.string().max(200).optional(),
funktion: contactFunktionSchema.optional(),
email: z.string().max(320).optional(),
telefon: z.string().max(100).optional(),
linkedin: z.string().max(500).optional(),
imBuyingCenterSeit: z.string().max(100).optional(),
sprache: z.string().max(100).optional(),
isPrimary: z.boolean().default(false),
});
export type AddContactInput = z.infer<typeof addContactInput>;
export const removeContactInput = z.object({ contactId: z.string().uuid() });
// Touchpoints (Austausch-Log) -------------------------------------------------
export const addTouchpointInput = z.object({
leadId: z.string().uuid(),
datum: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
kanal: touchpointKanalSchema,
inhalt: z.string().min(1).max(2000),
outcome: z.string().max(1000).optional(),
followUpBis: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional(),
});
export type AddTouchpointInput = z.infer<typeof addTouchpointInput>;
export const removeTouchpointInput = z.object({ touchpointId: z.string().uuid() });
// Promotion → Pipeline-Deal ---------------------------------------------------
export const promoteToDealInput = z.object({
leadId: z.string().uuid(),
valueEur: z.number().int().min(0).default(0),
probability: z.number().int().min(0).max(100).default(50),
});
export type PromoteToDealInput = z.infer<typeof promoteToDealInput>;
// CSV-Import: rohe CSV-Datei wird serverseitig geparst (header-basiert).
export const importLeadsCsvInput = z.object({
csv: z.string().min(1).max(2_000_000),
projectId: z.string().uuid().optional(), // alle importierten Leads diesem Projekt zuordnen
});
export type ImportLeadsCsvInput = z.infer<typeof importLeadsCsvInput>;