From cad582dd0690787fb468dd175a30bdfd5cf71642 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 11 Jun 2026 07:16:02 +0200 Subject: [PATCH] feat(08-03): add query layer for offer phases and quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getOfferPhases(microId) — get all phases for an offer micro - Add getOfferPhaseServices(phaseId) — get all services assigned to a phase - Add getQuoteById(quoteId) — get full quote header by ID - Add getQuoteByToken(token) — get quote by public token - Add getQuotesByClient(clientId) — get all quotes for a client - Update imports to include new tables and types - All queries properly typed with Drizzle ORM - Build verified with npm run build Co-Authored-By: Claude Haiku 4.5 --- src/lib/admin-queries.ts | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/lib/admin-queries.ts b/src/lib/admin-queries.ts index 215961d..9dcea76 100644 --- a/src/lib/admin-queries.ts +++ b/src/lib/admin-queries.ts @@ -16,6 +16,10 @@ import { settings, offer_micros, project_offers, + offer_phases, + offer_phase_services, + quotes, + leads, } from "@/db/schema"; import { eq, inArray, asc, isNull, sql, and } from "drizzle-orm"; import type { @@ -32,6 +36,10 @@ import type { Service, OfferMicro, ProjectOffer, + OfferPhase, + OfferPhaseService, + Quote, + Lead, } from "@/db/schema"; // ── ClientWithPayments — used by /admin clients list ───────────────────────── @@ -686,4 +694,78 @@ export async function getClientActiveOffers(clientId: string): Promise { + return await db + .select() + .from(offer_phases) + .where(eq(offer_phases.micro_id, microId)) + .orderBy(asc(offer_phases.sort_order)); +} + +/** + * Get all unified services assigned to a phase + * Used by: Quote builder, public quote page + */ +export async function getOfferPhaseServices(phaseId: string): Promise { + const phaseServices = await db + .select({ + service_id: offer_phase_services.service_id, + }) + .from(offer_phase_services) + .where(eq(offer_phase_services.phase_id, phaseId)); + + if (phaseServices.length === 0) return []; + + const serviceIds = phaseServices.map((ps) => ps.service_id); + + return await db + .select() + .from(services) + .where(inArray(services.id, serviceIds)); +} + +/** + * Get full quote by ID with all relations (header + items) + * Used by: Quote detail page + */ +export async function getQuoteById(quoteId: string): Promise { + const quote = await db + .select() + .from(quotes) + .where(eq(quotes.id, quoteId)); + + return quote.length > 0 ? quote[0] : null; +} + +/** + * Get quote by public token (for /quote/[token] routes) + * Used by: Public quote page access validation + */ +export async function getQuoteByToken(token: string): Promise { + const quote = await db + .select() + .from(quotes) + .where(eq(quotes.token, token)); + + return quote.length > 0 ? quote[0] : null; +} + +/** + * Get all quotes for a client (ordered by created_at DESC) + * Used by: /admin/clients/[id] detail page + */ +export async function getQuotesByClient(clientId: string): Promise { + return await db + .select() + .from(quotes) + .where(eq(quotes.client_id, clientId)) + .orderBy(sql`${quotes.created_at} DESC`); } \ No newline at end of file