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,
|
leads,
|
||||||
tags,
|
tags,
|
||||||
} from "@/db/schema";
|
} 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 {
|
import type {
|
||||||
Client,
|
Client,
|
||||||
Project,
|
Project,
|
||||||
@@ -879,3 +880,64 @@ export async function getAllOfferMacrosWithMicros(): Promise<
|
|||||||
.sort((a, b) => a.sort_order - b.sort_order),
|
.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