From f7434102deada9326d9e28f7abbd5c7e2347ebc1 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sat, 13 Jun 2026 15:38:57 +0200 Subject: [PATCH] feat(11-02): extend getAllServices with tags join (ServiceWithTags) - Add ServiceWithTags type = Service & { tags: string[] } - getAllServices() now left-joins tags scoped to entity_type=services - Tags aggregated per service, ordered by service name then tag name --- src/lib/admin-queries.ts | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/lib/admin-queries.ts b/src/lib/admin-queries.ts index 034c294..9222168 100644 --- a/src/lib/admin-queries.ts +++ b/src/lib/admin-queries.ts @@ -21,6 +21,7 @@ import { offer_phase_services, quotes, leads, + tags, } from "@/db/schema"; import { eq, inArray, asc, isNull, sql, and } from "drizzle-orm"; import type { @@ -356,11 +357,42 @@ export async function getClientFullDetail(id: string): Promise { - return db - .select() +// ── ServiceWithTags — services + assigned tag names (Phase 11 database-view) ── + +export type ServiceWithTags = Service & { tags: string[] }; + +export async function getAllServices(): Promise { + const rows = await db + .select({ + id: services.id, + name: services.name, + description: services.description, + unit_price: services.unit_price, + category: services.category, + active: services.active, + migrated_from: services.migrated_from, + migrated_id: services.migrated_id, + created_at: services.created_at, + tag_name: tags.name, + }) .from(services) - .orderBy(asc(services.name)); + .leftJoin( + tags, + and(eq(tags.entity_type, "services"), eq(tags.entity_id, services.id)) + ) + .orderBy(asc(services.name), asc(tags.name)); + + const serviceMap = new Map(); + for (const row of rows) { + const { tag_name, ...serviceFields } = row; + if (!serviceMap.has(row.id)) { + serviceMap.set(row.id, { ...serviceFields, tags: [] }); + } + if (tag_name) { + serviceMap.get(row.id)!.tags.push(tag_name); + } + } + return Array.from(serviceMap.values()); } // ── ProjectWithPayments — used by /admin/projects list ───────────────────────