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>
This commit is contained in:
1 parent
68480c1eb1
commit
19db774c4c
10 files changed
+3522
-3
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { asc, desc, eq, sql } from 'drizzle-orm';
|
||||
import { lead, leadContact, leadTouchpoint, deal, area, user } from '@cockpit/db';
|
||||
import { lead, leadContact, leadTouchpoint, deal, area, user, project } from '@cockpit/db';
|
||||
import type { Db } from '@cockpit/db';
|
||||
import { LeadSchemas } from '@cockpit/shared';
|
||||
import { router, protectedProcedure } from '../trpc.js';
|
||||
@@ -21,13 +21,28 @@ export const leadRouter = router({
|
||||
.select({
|
||||
lead: lead,
|
||||
ownerName: user.name,
|
||||
projectName: project.name,
|
||||
})
|
||||
.from(lead)
|
||||
.leftJoin(user, eq(user.id, lead.ownerUserId))
|
||||
.leftJoin(project, eq(project.id, lead.projectId))
|
||||
.orderBy(desc(lead.updatedAt));
|
||||
return rows.map((r) => ({ ...r.lead, ownerName: r.ownerName }));
|
||||
return rows.map((r) => ({ ...r.lead, ownerName: r.ownerName, projectName: r.projectName }));
|
||||
}),
|
||||
|
||||
// Leads, die einem Projekt zugeordnet sind (Vertriebs-Sicht im Projekt).
|
||||
forProject: protectedProcedure
|
||||
.input(LeadSchemas.leadsForProjectInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const rows = await ctx.db
|
||||
.select({ lead: lead, ownerName: user.name })
|
||||
.from(lead)
|
||||
.leftJoin(user, eq(user.id, lead.ownerUserId))
|
||||
.where(eq(lead.projectId, input.projectId))
|
||||
.orderBy(desc(lead.score), desc(lead.updatedAt));
|
||||
return rows.map((r) => ({ ...r.lead, ownerName: r.ownerName }));
|
||||
}),
|
||||
|
||||
byId: protectedProcedure.input(LeadSchemas.leadIdInput).query(async ({ ctx, input }) => {
|
||||
const l = (await ctx.db.select().from(lead).where(eq(lead.id, input.id)).limit(1))[0];
|
||||
if (!l) throw new TRPCError({ code: 'NOT_FOUND', message: 'Lead nicht gefunden.' });
|
||||
@@ -45,9 +60,17 @@ export const leadRouter = router({
|
||||
.leftJoin(user, eq(user.id, leadTouchpoint.bearbeiterUserId))
|
||||
.where(eq(leadTouchpoint.leadId, l.id))
|
||||
.orderBy(desc(leadTouchpoint.datum), desc(leadTouchpoint.createdAt));
|
||||
const linkedProject = l.projectId
|
||||
? (await ctx.db
|
||||
.select({ id: project.id, name: project.name })
|
||||
.from(project)
|
||||
.where(eq(project.id, l.projectId))
|
||||
.limit(1))[0] ?? null
|
||||
: null;
|
||||
return {
|
||||
lead: l,
|
||||
contacts,
|
||||
project: linkedProject,
|
||||
touchpoints: touchpoints.map((t) => ({ ...t.touchpoint, bearbeiterName: t.bearbeiterName })),
|
||||
};
|
||||
}),
|
||||
@@ -64,6 +87,7 @@ export const leadRouter = router({
|
||||
status: input.status,
|
||||
score: input.score,
|
||||
ownerUserId: input.ownerUserId ?? null,
|
||||
projectId: input.projectId ?? null,
|
||||
branche: input.branche ?? null,
|
||||
subSegment: input.subSegment ?? null,
|
||||
website: input.website ?? null,
|
||||
@@ -259,6 +283,7 @@ export const leadRouter = router({
|
||||
firma,
|
||||
status: 'cold',
|
||||
score: 0,
|
||||
projectId: input.projectId ?? null,
|
||||
branche: get(row, cells.branche) || null,
|
||||
website: get(row, cells.website) || null,
|
||||
quelle: get(row, cells.quelle) || null,
|
||||
|
||||
Reference in new issue
Block a user