feat: lead → client conversion + client contact fields (email/phone)
Blocco A+B (milestone v2.3). Migration 0011 is additive (ADD COLUMN only): clients.email, clients.phone, leads.archived. - createClientCore() extracted from createClient and reused by conversion - clients.email/phone added to create + edit forms and shown on client header (never exposed on the public /client/[token] page) - convertLeadToClient(): reuses core, carries over lead transcripts (client_transcripts.client_id), links project.created_from_lead_id, archives the lead while keeping its "won" status; idempotent - "Converti in cliente" button on won leads; "Convertito → Apri cliente" once converted (clientId resolved via created_from_lead_id) - archived leads hidden from list/kanban; detail page fetches by id incl. archived Migration must be applied to the prod DB BEFORE this is deployed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -892,6 +892,7 @@ export async function getLeadsWithTags(): Promise<LeadWithTags[]> {
|
||||
next_action: leads.next_action,
|
||||
next_action_date: leads.next_action_date,
|
||||
notes: leads.notes,
|
||||
archived: leads.archived,
|
||||
created_at: leads.created_at,
|
||||
updated_at: leads.updated_at,
|
||||
tag_name: tags.name,
|
||||
@@ -904,6 +905,7 @@ export async function getLeadsWithTags(): Promise<LeadWithTags[]> {
|
||||
eq(tags.entity_type, LEADS_TAG_ENTITY)
|
||||
)
|
||||
)
|
||||
.where(eq(leads.archived, false)) // converted leads are archived → hidden from pipeline
|
||||
.orderBy(desc(leads.updated_at), asc(tags.name));
|
||||
|
||||
const leadMap = new Map<string, LeadWithTags>();
|
||||
@@ -915,6 +917,54 @@ export async function getLeadsWithTags(): Promise<LeadWithTags[]> {
|
||||
return Array.from(leadMap.values());
|
||||
}
|
||||
|
||||
// Single lead by id WITH tags — does NOT filter archived (the detail page must
|
||||
// still show a converted/archived lead).
|
||||
export async function getLeadByIdWithTags(leadId: string): Promise<LeadWithTags | null> {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: leads.id,
|
||||
name: leads.name,
|
||||
email: leads.email,
|
||||
phone: leads.phone,
|
||||
company: leads.company,
|
||||
status: leads.status,
|
||||
last_contact_date: leads.last_contact_date,
|
||||
next_action: leads.next_action,
|
||||
next_action_date: leads.next_action_date,
|
||||
notes: leads.notes,
|
||||
archived: leads.archived,
|
||||
created_at: leads.created_at,
|
||||
updated_at: leads.updated_at,
|
||||
tag_name: tags.name,
|
||||
})
|
||||
.from(leads)
|
||||
.leftJoin(
|
||||
tags,
|
||||
and(eq(tags.entity_id, leads.id), eq(tags.entity_type, LEADS_TAG_ENTITY))
|
||||
)
|
||||
.where(eq(leads.id, leadId));
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
let result: LeadWithTags | null = null;
|
||||
for (const row of rows) {
|
||||
const { tag_name, ...leadFields } = row;
|
||||
if (!result) result = { ...leadFields, tags: [] };
|
||||
if (tag_name) result.tags.push(tag_name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// If this lead was converted, returns the client id of the project created from
|
||||
// it (projects.created_from_lead_id), else null.
|
||||
export async function getClientIdFromLead(leadId: string): Promise<string | null> {
|
||||
const [row] = await db
|
||||
.select({ client_id: projects.client_id })
|
||||
.from(projects)
|
||||
.where(eq(projects.created_from_lead_id, leadId))
|
||||
.limit(1);
|
||||
return row?.client_id ?? null;
|
||||
}
|
||||
|
||||
export type LeadFieldOptions = { status: string[]; tags: string[] };
|
||||
|
||||
export async function getLeadFieldOptions(): Promise<LeadFieldOptions> {
|
||||
|
||||
Reference in New Issue
Block a user