refactor(api): extract estimate read procedures
This commit is contained in:
@@ -245,6 +245,187 @@ describe("estimate router", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("listVersions", () => {
|
||||
it("returns estimate versions ordered from newest to oldest", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "est_1",
|
||||
name: "Test Estimate",
|
||||
status: EstimateStatus.DRAFT,
|
||||
latestVersionNumber: 2,
|
||||
versions: [
|
||||
{
|
||||
id: "ver_2",
|
||||
versionNumber: 2,
|
||||
label: "v2",
|
||||
status: EstimateVersionStatus.SUBMITTED,
|
||||
notes: null,
|
||||
lockedAt: new Date("2026-03-14"),
|
||||
createdAt: new Date("2026-03-14"),
|
||||
updatedAt: new Date("2026-03-14"),
|
||||
_count: {
|
||||
assumptions: 1,
|
||||
scopeItems: 2,
|
||||
demandLines: 3,
|
||||
resourceSnapshots: 4,
|
||||
exports: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
const result = await caller.listVersions({ estimateId: "est_1" });
|
||||
|
||||
expect(result.versions).toHaveLength(1);
|
||||
expect(result.versions[0]?.id).toBe("ver_2");
|
||||
expect(findUnique).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: "est_1" },
|
||||
select: expect.objectContaining({
|
||||
versions: expect.objectContaining({
|
||||
orderBy: { versionNumber: "desc" },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND when the estimate does not exist", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue(null);
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
await expect(caller.listVersions({ estimateId: "missing" })).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
code: "NOT_FOUND",
|
||||
message: "Estimate not found",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVersionSnapshot", () => {
|
||||
it("returns aggregate counts and totals for the selected version", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "est_1",
|
||||
name: "Test Estimate",
|
||||
status: EstimateStatus.DRAFT,
|
||||
baseCurrency: "EUR",
|
||||
versions: [
|
||||
{
|
||||
id: "ver_2",
|
||||
versionNumber: 2,
|
||||
label: "Revision 2",
|
||||
status: EstimateVersionStatus.SUBMITTED,
|
||||
notes: "Ready",
|
||||
lockedAt: new Date("2026-03-14"),
|
||||
createdAt: new Date("2026-03-14"),
|
||||
updatedAt: new Date("2026-03-15"),
|
||||
assumptions: [
|
||||
{ id: "a_1", category: "delivery", key: "onsite", label: "Onsite" },
|
||||
{ id: "a_2", category: "delivery", key: "travel", label: "Travel" },
|
||||
{ id: "a_3", category: "commercial", key: "buffer", label: "Buffer" },
|
||||
],
|
||||
scopeItems: [
|
||||
{ id: "s_1", scopeType: "FEATURE", sequenceNo: 1, name: "Alpha" },
|
||||
{ id: "s_2", scopeType: "FEATURE", sequenceNo: 2, name: "Beta" },
|
||||
{ id: "s_3", scopeType: "SERVICE", sequenceNo: 3, name: "Gamma" },
|
||||
],
|
||||
demandLines: [
|
||||
{
|
||||
id: "d_1",
|
||||
name: "Lead",
|
||||
chapter: "Delivery",
|
||||
hours: 10,
|
||||
costTotalCents: 100_00,
|
||||
priceTotalCents: 150_00,
|
||||
currency: "EUR",
|
||||
},
|
||||
{
|
||||
id: "d_2",
|
||||
name: "QA",
|
||||
chapter: null,
|
||||
hours: 5,
|
||||
costTotalCents: 50_00,
|
||||
priceTotalCents: 90_00,
|
||||
currency: "EUR",
|
||||
},
|
||||
],
|
||||
resourceSnapshots: [
|
||||
{
|
||||
id: "r_1",
|
||||
displayName: "Alice",
|
||||
chapter: "Delivery",
|
||||
currency: "EUR",
|
||||
lcrCents: 10_000,
|
||||
ucrCents: 15_000,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
{
|
||||
id: "x_1",
|
||||
format: EstimateExportFormat.XLSX,
|
||||
fileName: "estimate.xlsx",
|
||||
createdAt: new Date("2026-03-16"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
const result = await caller.getVersionSnapshot({ estimateId: "est_1" });
|
||||
|
||||
expect(result.counts).toEqual({
|
||||
assumptions: 3,
|
||||
scopeItems: 3,
|
||||
demandLines: 2,
|
||||
resourceSnapshots: 1,
|
||||
exports: 1,
|
||||
});
|
||||
expect(result.totals).toMatchObject({
|
||||
hours: 15,
|
||||
costTotalCents: 15000,
|
||||
priceTotalCents: 24000,
|
||||
});
|
||||
expect(result.chapterBreakdown).toEqual([
|
||||
expect.objectContaining({ chapter: "Delivery", lineCount: 1, hours: 10 }),
|
||||
expect.objectContaining({ chapter: "Unassigned", lineCount: 1, hours: 5 }),
|
||||
]);
|
||||
expect(result.scopeTypeBreakdown).toEqual([
|
||||
{ scopeType: "FEATURE", count: 2 },
|
||||
{ scopeType: "SERVICE", count: 1 },
|
||||
]);
|
||||
expect(result.assumptionCategoryBreakdown).toEqual([
|
||||
{ category: "delivery", count: 2 },
|
||||
{ category: "commercial", count: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND when no matching version can be resolved", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "est_1",
|
||||
name: "Test Estimate",
|
||||
status: EstimateStatus.DRAFT,
|
||||
baseCurrency: "EUR",
|
||||
versions: [],
|
||||
});
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
await expect(
|
||||
caller.getVersionSnapshot({ estimateId: "est_1", versionId: "missing_version" }),
|
||||
).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
code: "NOT_FOUND",
|
||||
message: "Estimate version not found",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── create ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("create", () => {
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
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,
|
||||
)),
|
||||
};
|
||||
@@ -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