From 11ccb2d83c57f55817bb80b39a6fe19db1497928 Mon Sep 17 00:00:00 2001 From: Bryan de Bourbon Date: Tue, 18 Aug 2026 14:56:30 -0700 Subject: [PATCH] Expose built element counts per floor and trade [HMA-1938] Adds Avvir.api.projects.getProjectFloorTradeBuiltElementCounts for the gateway's new GET /projects/{projectId}/built-elements-by-floor-and-trade. The as-built IFC export's trade step uses it to disable a trade with nothing built in the selected areas. The deviation magnitudes it already fetches cannot answer that: a trade built at its designed position reports a magnitude of zero, exactly like a trade that was never built. Co-Authored-By: Claude Opus 5 (1M context) --- source/api/project_api.ts | 5 +++++ tests/api/project_api_test.ts | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/source/api/project_api.ts b/source/api/project_api.ts index e62c758..60efe08 100644 --- a/source/api/project_api.ts +++ b/source/api/project_api.ts @@ -52,6 +52,11 @@ export default class ProjectApi { return Http.get(url, user) as unknown as Promise<{[floorFirebaseId: string]: number}>; } + static getProjectFloorTradeBuiltElementCounts({ projectId }: AssociationIds, user: User): Promise<{[floorFirebaseId: string]: {[tradeCode: string]: number}}> { + let url = `${Http.baseUrl()}/projects/${projectId}/built-elements-by-floor-and-trade`; + return Http.get(url, user) as unknown as Promise<{[floorFirebaseId: string]: {[tradeCode: string]: number}}>; + } + static getProjectDeviationSummary({ projectId }: AssociationIds, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/deviation-summary`; return Http.get(url, user) as unknown as Promise; diff --git a/tests/api/project_api_test.ts b/tests/api/project_api_test.ts index fd1ebde..4aa7ff1 100644 --- a/tests/api/project_api_test.ts +++ b/tests/api/project_api_test.ts @@ -290,6 +290,46 @@ describe("ProjectApi", () => { }); }); + describe("#getProjectFloorTradeBuiltElementCounts", () => { + beforeEach(() => { + fetchMock.get(`${Http.baseUrl()}/projects/some-project-id/built-elements-by-floor-and-trade`, { + status: 200, + body: { + "some-floor-id": { "03": 12, "06": 3 }, + } + }); + }); + + it("makes a request to the gateway api", () => { + ProjectApi.getProjectFloorTradeBuiltElementCounts({ projectId: "some-project-id" }, { + authType: "GATEWAY_JWT", + gatewayUser: { idToken: "some-firebase.id.token" } + }); + const fetchCall = fetchMock.lastCall(); + + expect(fetchCall[0]).to.eq(`${Http.baseUrl()}/projects/some-project-id/built-elements-by-floor-and-trade`); + expect(fetchMock.lastOptions().headers.Accept).to.eq("application/json"); + }); + + it("includes the authorization headers", () => { + ProjectApi.getProjectFloorTradeBuiltElementCounts({ projectId: "some-project-id" }, { + authType: "GATEWAY_JWT", + gatewayUser: { idToken: "some-firebase.id.token" } + }); + + expect(fetchMock.lastOptions().headers.Authorization).to.eq("Bearer some-firebase.id.token"); + }); + + it("returns the built element counts keyed by floor firebase id and then trade code", () => { + return ProjectApi.getProjectFloorTradeBuiltElementCounts({ projectId: "some-project-id" }, { + authType: "GATEWAY_JWT", + gatewayUser: { idToken: "some-firebase.id.token" } + }).then((builtCounts) => { + expect(builtCounts).to.deep.eq({ "some-floor-id": { "03": 12, "06": 3 } }); + }); + }); + }); + describe("#getProjectDeviationSummary", () => { beforeEach(() => { fetchMock.get(`${Http.baseUrl()}/projects/some-project-id/deviation-summary`, {