Add print view, board info card and document logo upload
- Seitenansicht (/projects/:projectId/print): tabular cover page, board overview with supply structure and power sums, one plan per board with a repeating title block; PDF via the browser print dialog with named @page rules (A4 portrait cover, A4/A3 landscape plans, page x of y). - Distribution-board info card (board number, location, fed-from, remark) in the board edit dialog, persisted through the additively extended distribution-board.update command (migration 0008, snapshot schema 6, legacy subtree commands normalized). - Global document logo upload (PNG/JPEG/SVG, max 2 MB) outside project revisions and snapshots. - fed-from stays free text; the overview hierarchy and cumulated sums are preliminary until the feed link is modeled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2742b26b87
commit
94688d7a8c
38 changed files with 5301 additions and 32 deletions
176
tests/print-document-model.test.ts
Normal file
176
tests/print-document-model.test.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
buildPreliminaryBoardHierarchy,
|
||||
formatProtectionDevice,
|
||||
summarizePrintBoard,
|
||||
} from "../src/frontend/print/print-document-model.js";
|
||||
import type {
|
||||
CircuitTreeResponseDto,
|
||||
DistributionBoardDto,
|
||||
} from "../src/frontend/types.js";
|
||||
|
||||
function board(
|
||||
id: string,
|
||||
info: Partial<DistributionBoardDto> = {}
|
||||
): DistributionBoardDto {
|
||||
return {
|
||||
id,
|
||||
projectId: "project-1",
|
||||
name: id,
|
||||
floorId: null,
|
||||
supplyType: "AV",
|
||||
simultaneityFactor: 1,
|
||||
boardNumber: null,
|
||||
location: null,
|
||||
fedFrom: null,
|
||||
remark: null,
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
function tree(
|
||||
rows: Array<{
|
||||
quantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
}>,
|
||||
boardSimultaneityFactor = 1
|
||||
): CircuitTreeResponseDto {
|
||||
const deviceRows = rows.map((row, index) => ({
|
||||
id: `row-${index}`,
|
||||
sortOrder: index,
|
||||
name: "Gerät",
|
||||
displayName: "Gerät",
|
||||
manualQuantity: row.quantity,
|
||||
rowTotalPower: row.quantity * row.powerPerUnit * row.simultaneityFactor,
|
||||
...row,
|
||||
}));
|
||||
const total = deviceRows.reduce((sum, row) => sum + row.rowTotalPower, 0);
|
||||
return {
|
||||
circuitListId: "list",
|
||||
currentRevision: 1,
|
||||
singlePhaseVoltageV: 230,
|
||||
threePhaseVoltageV: 400,
|
||||
distributionBoardSimultaneityFactor: boardSimultaneityFactor,
|
||||
distributionBoardTotalPower: total,
|
||||
distributionBoardTotalPowerWithSimultaneityFactor:
|
||||
total * boardSimultaneityFactor,
|
||||
headerComponents: [],
|
||||
footerComponents: [],
|
||||
sections: [
|
||||
{
|
||||
id: "section",
|
||||
key: "single_phase",
|
||||
displayName: "1-phasig 1",
|
||||
prefix: "-2F1.",
|
||||
sortOrder: 1,
|
||||
sectionTotalPower: total,
|
||||
components: [],
|
||||
circuits: [
|
||||
{
|
||||
id: "circuit",
|
||||
circuitListId: "list",
|
||||
sectionId: "section",
|
||||
equipmentIdentifier: "-2F1.1",
|
||||
sortOrder: 1,
|
||||
isReserve: false,
|
||||
circuitTotalPower: total,
|
||||
deviceRows,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function summary(
|
||||
id: string,
|
||||
info: Partial<DistributionBoardDto>,
|
||||
demandKw: number
|
||||
) {
|
||||
return summarizePrintBoard({
|
||||
board: board(id, info),
|
||||
circuitListId: id,
|
||||
floorName: null,
|
||||
tree: tree([{ quantity: 1, powerPerUnit: demandKw, simultaneityFactor: 1 }]),
|
||||
});
|
||||
}
|
||||
|
||||
describe("print document model", () => {
|
||||
it("derives installed, device and demand power and the design current", () => {
|
||||
const result = summarizePrintBoard({
|
||||
board: board("uv"),
|
||||
circuitListId: "uv",
|
||||
floorName: "EG",
|
||||
tree: tree(
|
||||
[
|
||||
{ quantity: 4, powerPerUnit: 2, simultaneityFactor: 0.5, cosPhi: 0.8 },
|
||||
{ quantity: 1, powerPerUnit: 6, simultaneityFactor: 1 },
|
||||
],
|
||||
0.5
|
||||
),
|
||||
});
|
||||
assert.equal(result.circuitCount, 1);
|
||||
assert.equal(result.installedPowerKw, 14);
|
||||
assert.equal(result.devicePowerKw, 10);
|
||||
assert.equal(result.demandPowerKw, 5);
|
||||
// S = (4 / 0.8 + 6 / 1) × 0.5 = 5.5 kVA → I = 5500 / (√3 · 400)
|
||||
assert.ok(Math.abs(result.designCurrentA - 5500 / (Math.sqrt(3) * 400)) < 1e-9);
|
||||
});
|
||||
|
||||
it("nests boards by matching free-text feed and sums downstream demand", () => {
|
||||
const entries = buildPreliminaryBoardHierarchy([
|
||||
summary("hv", { boardNumber: "HV 0.1", fedFrom: "HAK" }, 10),
|
||||
summary("uv-a", { boardNumber: "UV 1.1", fedFrom: " hv 0.1 " }, 4),
|
||||
summary("uv-b", { boardNumber: "UV 1.2", fedFrom: "UV 1.1" }, 2),
|
||||
summary("uv-c", { name: "UV-C", fedFrom: "unbekannt" }, 1),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
entries.map((entry) => [
|
||||
entry.summary.board.id,
|
||||
entry.depth,
|
||||
entry.parentBoardId,
|
||||
entry.cumulatedDemandPowerKw,
|
||||
]),
|
||||
[
|
||||
["hv", 0, null, 16],
|
||||
["uv-a", 1, "hv", 6],
|
||||
["uv-b", 2, "uv-a", 2],
|
||||
["uv-c", 0, null, 1],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps boards with cyclic feed texts at the top level", () => {
|
||||
const entries = buildPreliminaryBoardHierarchy([
|
||||
summary("a", { boardNumber: "A", fedFrom: "B" }, 1),
|
||||
summary("b", { boardNumber: "B", fedFrom: "A" }, 1),
|
||||
]);
|
||||
assert.equal(entries.length, 2);
|
||||
assert.ok(entries.some((entry) => entry.parentBoardId === null));
|
||||
assert.equal(
|
||||
entries.reduce((sum, entry) => sum + (entry.depth === 0 ? entry.cumulatedDemandPowerKw : 0), 0),
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
it("formats protection devices for the plan table", () => {
|
||||
assert.equal(
|
||||
formatProtectionDevice({ type: "LS", ratedCurrentA: 16, tripCharacteristic: "B" }),
|
||||
"LS B 16 A"
|
||||
);
|
||||
assert.equal(
|
||||
formatProtectionDevice({
|
||||
type: "FI_LS",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
}),
|
||||
"FI/LS B 16 A, 30 mA Typ A"
|
||||
);
|
||||
assert.equal(formatProtectionDevice(undefined), null);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue