feat(08-03): add query layer for offer phases and quotes

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 07:16:02 +02:00
parent f727954dfb
commit cad582dd06
+82
View File
@@ -16,6 +16,10 @@ import {
settings, settings,
offer_micros, offer_micros,
project_offers, project_offers,
offer_phases,
offer_phase_services,
quotes,
leads,
} from "@/db/schema"; } from "@/db/schema";
import { eq, inArray, asc, isNull, sql, and } from "drizzle-orm"; import { eq, inArray, asc, isNull, sql, and } from "drizzle-orm";
import type { import type {
@@ -32,6 +36,10 @@ import type {
Service, Service,
OfferMicro, OfferMicro,
ProjectOffer, ProjectOffer,
OfferPhase,
OfferPhaseService,
Quote,
Lead,
} from "@/db/schema"; } from "@/db/schema";
// ── ClientWithPayments — used by /admin clients list ───────────────────────── // ── ClientWithPayments — used by /admin clients list ─────────────────────────
@@ -687,3 +695,77 @@ export async function getClientActiveOffers(clientId: string): Promise<ClientAct
accepted_total: r.accepted_total ? String(r.accepted_total) : null, accepted_total: r.accepted_total ? String(r.accepted_total) : null,
})); }));
} }
// ── Phase 8 Query Layer: Offer Phases & Quotes ──────────────────────────────
/**
* Get all phases for a given offer micro (ordered by sort_order)
* Used by: Quote builder, admin phase editor
*/
export async function getOfferPhases(microId: string): Promise<OfferPhase[]> {
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<Service[]> {
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<Quote | null> {
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<Quote | null> {
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<Quote[]> {
return await db
.select()
.from(quotes)
.where(eq(quotes.client_id, clientId))
.orderBy(sql`${quotes.created_at} DESC`);
}