227 lines
6.8 KiB
TypeScript
227 lines
6.8 KiB
TypeScript
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<typeof getEstimateById>[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<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,
|
|
};
|
|
}
|
|
|
|
export const estimateReadProcedures = {
|
|
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 }) =>
|
|
findUniqueOrThrow(
|
|
getEstimateById(
|
|
ctx.db as unknown as Parameters<typeof getEstimateById>[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<typeof getEstimateById>[0],
|
|
input,
|
|
)),
|
|
};
|