feat(14-01): add getLeadsWithTags and getLeadFieldOptions to admin-queries
- LeadWithTags type + getLeadsWithTags() left-joins leads with tags scoped to entity_type="leads", Map-reduce pattern mirroring getAllServices (Phase 11) - LeadFieldOptions type + getLeadFieldOptions() returns fixed LEAD_STAGES for status and distinct sorted lead tag names - desc import added to drizzle-orm import; LEAD_STAGES imported from lib/lead-validators
This commit is contained in:
@@ -23,7 +23,8 @@ import {
|
||||
leads,
|
||||
tags,
|
||||
} from "@/db/schema";
|
||||
import { eq, inArray, asc, isNull, sql, and } from "drizzle-orm";
|
||||
import { eq, inArray, asc, desc, isNull, sql, and } from "drizzle-orm";
|
||||
import { LEAD_STAGES } from "@/lib/lead-validators";
|
||||
import type {
|
||||
Client,
|
||||
Project,
|
||||
@@ -878,4 +879,65 @@ export async function getAllOfferMacrosWithMicros(): Promise<
|
||||
.filter((micro) => micro.macro_id === macro.id)
|
||||
.sort((a, b) => a.sort_order - b.sort_order),
|
||||
}));
|
||||
}
|
||||
|
||||
// ── LeadWithTags — leads + tags (Phase 14 database-view) ─────────────────────
|
||||
// Lead tags are stored in the polymorphic `tags` table, scoped by
|
||||
// entity_type="leads". `status` is a fixed enum (LEAD_STAGES), not a
|
||||
// dynamic pool — exposed via getLeadFieldOptions for the OptionSelect UI.
|
||||
|
||||
const LEADS_TAG_ENTITY = "leads";
|
||||
|
||||
export type LeadWithTags = Lead & { tags: string[] };
|
||||
|
||||
export async function getLeadsWithTags(): Promise<LeadWithTags[]> {
|
||||
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,
|
||||
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)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(leads.updated_at), asc(tags.name));
|
||||
|
||||
const leadMap = new Map<string, LeadWithTags>();
|
||||
for (const row of rows) {
|
||||
const { tag_name, ...leadFields } = row;
|
||||
if (!leadMap.has(row.id)) leadMap.set(row.id, { ...leadFields, tags: [] });
|
||||
if (tag_name) leadMap.get(row.id)!.tags.push(tag_name);
|
||||
}
|
||||
return Array.from(leadMap.values());
|
||||
}
|
||||
|
||||
export type LeadFieldOptions = { status: string[]; tags: string[] };
|
||||
|
||||
export async function getLeadFieldOptions(): Promise<LeadFieldOptions> {
|
||||
const tagRows = await db
|
||||
.selectDistinct({ name: tags.name })
|
||||
.from(tags)
|
||||
.where(eq(tags.entity_type, LEADS_TAG_ENTITY));
|
||||
|
||||
return {
|
||||
status: [...LEAD_STAGES],
|
||||
tags: tagRows
|
||||
.map((r) => r.name)
|
||||
.sort((a, b) => a.localeCompare(b, "it")),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user