import { getEstimateById, listEstimates } from "@capakraken/application"; import { summarizeEstimateDemandLines } from "@capakraken/engine"; import { EstimateListFiltersSchema } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { controllerProcedure } from "../trpc.js"; async function readEstimateVersionSnapshot( db: Parameters[0], input: { estimateId: string; versionId?: string | undefined }, ) { const estimate = await db.estimate.findUnique({ where: { id: input.estimateId }, select: { id: true, name: true, status: true, baseCurrency: true, versions: { ...(input.versionId ? { where: { id: input.versionId } } : { orderBy: { versionNumber: "desc" as const }, take: 1 }), select: { id: true, versionNumber: true, label: true, status: true, notes: true, lockedAt: true, createdAt: true, updatedAt: true, assumptions: { select: { id: true, category: true, key: true, label: true }, }, scopeItems: { select: { id: true, scopeType: true, sequenceNo: true, name: true }, orderBy: [{ sequenceNo: "asc" }, { name: "asc" }], }, demandLines: { select: { id: true, name: true, chapter: true, hours: true, costTotalCents: true, priceTotalCents: true, currency: true, }, }, resourceSnapshots: { select: { id: true, displayName: true, chapter: true, currency: true, lcrCents: true, ucrCents: true, }, }, exports: { select: { id: true, format: true, fileName: true, createdAt: true, }, orderBy: { createdAt: "desc" }, }, }, }, }, }); if (!estimate || estimate.versions.length === 0) { throw new TRPCError({ code: "NOT_FOUND", message: "Estimate version not found" }); } const version = estimate.versions[0]!; const demandSummary = summarizeEstimateDemandLines(version.demandLines); const chapterTotals = version.demandLines.reduce>((acc, line) => { const key = line.chapter ?? "Unassigned"; const current = acc[key] ?? { lineCount: 0, hours: 0, costTotalCents: 0, priceTotalCents: 0, currency: line.currency, }; current.lineCount += 1; current.hours += line.hours; current.costTotalCents += line.costTotalCents; current.priceTotalCents += line.priceTotalCents; acc[key] = current; return acc; }, {}); const scopeTypeTotals = version.scopeItems.reduce>((acc, item) => { acc[item.scopeType] = (acc[item.scopeType] ?? 0) + 1; return acc; }, {}); const assumptionCategoryTotals = version.assumptions.reduce>((acc, assumption) => { acc[assumption.category] = (acc[assumption.category] ?? 0) + 1; return acc; }, {}); return { estimate: { id: estimate.id, name: estimate.name, status: estimate.status, baseCurrency: estimate.baseCurrency, }, version: { id: version.id, versionNumber: version.versionNumber, label: version.label, status: version.status, notes: version.notes, lockedAt: version.lockedAt, createdAt: version.createdAt, updatedAt: version.updatedAt, }, counts: { assumptions: version.assumptions.length, scopeItems: version.scopeItems.length, demandLines: version.demandLines.length, resourceSnapshots: version.resourceSnapshots.length, exports: version.exports.length, }, totals: { hours: demandSummary.totalHours, costTotalCents: demandSummary.totalCostCents, priceTotalCents: demandSummary.totalPriceCents, marginCents: demandSummary.marginCents, marginPercent: demandSummary.marginPercent, }, chapterBreakdown: Object.entries(chapterTotals) .sort((left, right) => right[1].hours - left[1].hours) .map(([chapter, totals]) => ({ chapter, ...totals, })), scopeTypeBreakdown: Object.entries(scopeTypeTotals) .sort((left, right) => right[1] - left[1]) .map(([scopeType, count]) => ({ scopeType, count })), assumptionCategoryBreakdown: Object.entries(assumptionCategoryTotals) .sort((left, right) => right[1] - left[1]) .map(([category, count]) => ({ category, count })), exports: version.exports, }; } export const estimateReadProcedures = { list: controllerProcedure .input(EstimateListFiltersSchema.default({})) .query(async ({ ctx, input }) => listEstimates( ctx.db as unknown as Parameters[0], input, )), getById: controllerProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => findUniqueOrThrow( getEstimateById( ctx.db as unknown as Parameters[0], input.id, ), "Estimate", )), listVersions: controllerProcedure .input(z.object({ estimateId: z.string() })) .query(async ({ ctx, input }) => findUniqueOrThrow( ctx.db.estimate.findUnique({ where: { id: input.estimateId }, select: { id: true, name: true, status: true, latestVersionNumber: true, versions: { orderBy: { versionNumber: "desc" }, select: { id: true, versionNumber: true, label: true, status: true, notes: true, lockedAt: true, createdAt: true, updatedAt: true, _count: { select: { assumptions: true, scopeItems: true, demandLines: true, resourceSnapshots: true, exports: true, }, }, }, }, }, }), "Estimate", )), getVersionSnapshot: controllerProcedure .input(z.object({ estimateId: z.string(), versionId: z.string().optional() })) .query(async ({ ctx, input }) => readEstimateVersionSnapshot( ctx.db as unknown as Parameters[0], input, )), };