refactor(api): extract estimate read procedures
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
createEstimatePlanningHandoff,
|
||||
createEstimateRevision,
|
||||
getEstimateById,
|
||||
listEstimates,
|
||||
submitEstimateVersion,
|
||||
updateEstimateDraft,
|
||||
} from "@capakraken/application";
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
CreateEstimatePlanningHandoffSchema,
|
||||
CreateEstimateSchema,
|
||||
CreateEstimateRevisionSchema,
|
||||
EstimateListFiltersSchema,
|
||||
GenerateWeeklyPhasingSchema,
|
||||
PermissionKey,
|
||||
SubmitEstimateVersionSchema,
|
||||
@@ -42,10 +40,10 @@ import {
|
||||
controllerProcedure,
|
||||
createTRPCRouter,
|
||||
managerProcedure,
|
||||
protectedProcedure,
|
||||
requirePermission,
|
||||
} from "../trpc.js";
|
||||
import { emitAllocationCreated } from "../sse/event-bus.js";
|
||||
import { estimateReadProcedures } from "./estimate-read.js";
|
||||
|
||||
type EstimateRouterErrorCode = "NOT_FOUND" | "PRECONDITION_FAILED";
|
||||
|
||||
@@ -245,220 +243,7 @@ async function autoFillDemandLineRates(
|
||||
}
|
||||
|
||||
export const estimateRouter = createTRPCRouter({
|
||||
list: controllerProcedure
|
||||
.input(EstimateListFiltersSchema.default({}))
|
||||
.query(async ({ ctx, input }) =>
|
||||
listEstimates(
|
||||
ctx.db as unknown as Parameters<typeof listEstimates>[0],
|
||||
input,
|
||||
)),
|
||||
|
||||
getById: controllerProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const estimate = await findUniqueOrThrow(
|
||||
getEstimateById(
|
||||
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
|
||||
input.id,
|
||||
),
|
||||
"Estimate",
|
||||
);
|
||||
|
||||
return estimate;
|
||||
}),
|
||||
|
||||
listVersions: controllerProcedure
|
||||
.input(z.object({ estimateId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const estimate = await 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",
|
||||
);
|
||||
|
||||
return estimate;
|
||||
}),
|
||||
|
||||
getVersionSnapshot: controllerProcedure
|
||||
.input(z.object({ estimateId: z.string(), versionId: z.string().optional() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const estimate = await ctx.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<Record<string, {
|
||||
lineCount: number;
|
||||
hours: number;
|
||||
costTotalCents: number;
|
||||
priceTotalCents: number;
|
||||
currency: string;
|
||||
}>>((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<Record<string, number>>((acc, item) => {
|
||||
acc[item.scopeType] = (acc[item.scopeType] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const assumptionCategoryTotals = version.assumptions.reduce<Record<string, number>>((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,
|
||||
};
|
||||
}),
|
||||
...estimateReadProcedures,
|
||||
|
||||
create: managerProcedure
|
||||
.input(CreateEstimateSchema)
|
||||
|
||||
Reference in New Issue
Block a user