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:
@@ -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<ClientAct
|
||||
public_name: r.public_name,
|
||||
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`);
|
||||
}
|
||||
Reference in New Issue
Block a user