forked from jappel/leistungsbilanz-ts
Add Linear XLSX initial import
This commit is contained in:
parent
c633e78cd4
commit
bfdd048269
20 changed files with 932 additions and 85 deletions
68
tests/external-xlsx-initial-import.test.ts
Normal file
68
tests/external-xlsx-initial-import.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import { createDatabaseContext } from "../src/db/database-context.js";
|
||||
import { ExternalInitialImportProjectCommandRepository } from "../src/db/repositories/external-initial-import-project-command.repository.js";
|
||||
import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js";
|
||||
import { externalModelConfigurations } from "../src/db/schema/external-model-configurations.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
import { createEmptyExternalModelState, createExternalInitialImportProjectCommand } from "../src/domain/models/external-initial-import-project-command.model.js";
|
||||
import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js";
|
||||
import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
|
||||
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
|
||||
import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId } from "./fixtures/revit-xlsx-fixtures.js";
|
||||
|
||||
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||
|
||||
function configured() {
|
||||
return {
|
||||
...createDefaultExternalModelConfiguration(),
|
||||
...externalXlsxTestConfiguration,
|
||||
deviceTypes: [
|
||||
{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" } },
|
||||
{ id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } },
|
||||
],
|
||||
familyTypeRules: [
|
||||
{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const },
|
||||
{ id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("Linear XLSX initial import", () => {
|
||||
it("plans exact families, skips suspicious rows and defaults missing GZF to one", async () => {
|
||||
const { plan } = await createExternalXlsxInitialImportPlan({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture(), configuration: configured(), catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] } });
|
||||
assert.equal(plan.objectCount, 2);
|
||||
assert.equal(plan.suspectObjectCount, 1);
|
||||
assert.equal(plan.issueCounts.unknownFamilyAndType, 0);
|
||||
assert.equal(plan.objects[0].worksharingId, firstWorksharingId);
|
||||
assert.equal(plan.objects[0].suggestedPlanningValues.effectiveQuantity, 2);
|
||||
assert.equal(plan.objects[0].suggestedPlanningValues.simultaneityFactor, 1);
|
||||
assert.equal(plan.objects[1].suggestedPlanningValues.simultaneityFactor, 0.8);
|
||||
});
|
||||
|
||||
it("commits atomically and undo/redo uses stored states", async () => {
|
||||
const context = createDatabaseContext(":memory:");
|
||||
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
|
||||
context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run();
|
||||
const configuration = configured();
|
||||
const configurationSnapshot = { id: "config-1", projectId: "project-1", configuration };
|
||||
context.db.insert(externalModelConfigurations).values(configurationSnapshot).run();
|
||||
try {
|
||||
const bytes = await createRevitXlsxFixture();
|
||||
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: "linear.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] } });
|
||||
let sequence = 0;
|
||||
const target = buildExternalXlsxInitialImportTarget({ projectId: "project-1", expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T10:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, createId: () => `30000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
||||
const repository = new ExternalInitialImportProjectCommandRepository(context.db);
|
||||
const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createExternalInitialImportProjectCommand(configurationSnapshot, configurationSnapshot, createEmptyExternalModelState(), target) });
|
||||
assert.equal(inserted.revision.revisionNumber, 1);
|
||||
assert.equal(new ExternalModelStateRepository(context.db).getByProject("project-1").state.objects.length, 2);
|
||||
const undone = repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: inserted.revision.changeSetId, command: inserted.inverse });
|
||||
assert.equal(new ExternalModelStateRepository(context.db).getByProject("project-1").state.source, null);
|
||||
repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: inserted.revision.changeSetId, command: undone.inverse });
|
||||
assert.deepEqual(new ExternalModelStateRepository(context.db).getByProject("project-1").state.objects.map((object) => object.worksharingId), plan.objects.map((object) => object.worksharingId).sort());
|
||||
} finally { context.close(); }
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, it } from "node:test";
|
||||
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
|
||||
import { detectExternalModelWorkbook, getExternalModelConfiguration, updateExternalModelConfiguration } from "../src/frontend/utils/api.js";
|
||||
import { applyExternalInitialImport, detectExternalModelWorkbook, getExternalInitialImportStatus, getExternalModelConfiguration, planExternalInitialImport, updateExternalModelConfiguration } from "../src/frontend/utils/api.js";
|
||||
|
||||
describe("Revit XLSX settings", () => {
|
||||
it("uses the project-scoped configuration endpoint with expectedRevision", async () => {
|
||||
|
|
@ -25,14 +25,41 @@ describe("Revit XLSX settings", () => {
|
|||
} finally { globalThis.fetch = originalFetch; }
|
||||
});
|
||||
|
||||
it("uses stateless preview and retransmits the workbook for apply", async () => {
|
||||
const requests: Array<{ url: string; method: string; body: Record<string, unknown> | undefined }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input, init) => {
|
||||
requests.push({ url: String(input), method: init?.method ?? "GET", body: init?.body ? JSON.parse(String(init.body)) : undefined });
|
||||
return new Response(JSON.stringify({ imported: false, familyGroups: [], sourceRooms: [], objects: [], warnings: [], issueCounts: {}, existing: {}, history: { currentRevision: 9 }, import: { objectCount: 2 } }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
};
|
||||
try {
|
||||
const configuration = createDefaultExternalModelConfiguration();
|
||||
const file = new File([new Uint8Array([1, 2])], "linear.xlsx");
|
||||
await getExternalInitialImportStatus("project-1");
|
||||
await planExternalInitialImport("project-1", file, configuration, 8);
|
||||
await applyExternalInitialImport("project-1", file, { expectedRevision: 8, expectedSha256: "a".repeat(64), sourceName: "Revit", configuration, objectsWithoutRoomDefaultDistributionBoardId: null, roomDecisions: [], familyProjectDeviceDecisions: [] });
|
||||
assert.equal(requests[0].url, "/api/projects/project-1/external-model/import-status");
|
||||
assert.equal(requests[1].url, "/api/projects/project-1/external-model/initial-import/plan");
|
||||
assert.equal(requests[2].url, "/api/projects/project-1/external-model/initial-import/apply");
|
||||
assert.equal(requests[1].body?.contentBase64, "AQI=");
|
||||
assert.equal(requests[1].body?.expectedRevision, 8);
|
||||
assert.equal(requests[2].body?.contentBase64, "AQI=");
|
||||
assert.equal(requests[2].body?.expectedRevision, 8);
|
||||
} finally { globalThis.fetch = originalFetch; }
|
||||
});
|
||||
|
||||
it("exposes the Revit route and removes the old project modal", () => {
|
||||
const projectPage = readFileSync("src/app/projects/[projectId]/page.tsx", "utf8");
|
||||
const revitPage = readFileSync("src/app/projects/[projectId]/revit/page.tsx", "utf8");
|
||||
const settings = readFileSync("src/frontend/components/revit-xlsx-settings-modal.tsx", "utf8");
|
||||
const wizard = readFileSync("src/frontend/components/revit-initial-import-wizard.tsx", "utf8");
|
||||
assert.match(projectPage, /href={`\/projects\/\$\{projectId\}\/revit`}/);
|
||||
assert.doesNotMatch(projectPage, /RevitCsvModal|Revit-CSV/);
|
||||
assert.match(revitPage, /Importstatus/);
|
||||
assert.match(settings, /Musterdatei prüfen/);
|
||||
assert.match(settings, /Kombinierte Stromkreisspalte/);
|
||||
assert.match(wizard, /Schritt.*von/);
|
||||
assert.match(wizard, /keine Gerätezeilen, Stromkreise oder BMKs/);
|
||||
assert.match(wizard, /GZF 1/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue