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:
Grovy311 2026-09-25 19:31:04 +02:00
parent 2742b26b87
commit 94688d7a8c
38 changed files with 5301 additions and 32 deletions

View file

@ -387,6 +387,110 @@ describe("distribution-board structure project command", () => {
}
});
it("updates the info card fields with persisted undo", () => {
const context = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
context.db
);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV-EG",
{ floorId: null, supplyType: "AV" }
);
const insert = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createDistributionBoardInsertProjectCommand(structure),
});
const readInfo = () =>
context.db
.select({
boardNumber: distributionBoards.boardNumber,
location: distributionBoards.location,
fedFrom: distributionBoards.fedFrom,
remark: distributionBoards.remark,
})
.from(distributionBoards)
.get();
assert.deepEqual(readInfo(), {
boardNumber: null,
location: null,
fedFrom: null,
remark: null,
});
const forward = repository.executeUpdate({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: createDistributionBoardUpdateProjectCommand(
structure.distributionBoard.id,
{
boardNumber: "UV 0.1",
location: "Raum 003 Technik",
fedFrom: "HAK",
}
),
});
assert.deepEqual(readInfo(), {
boardNumber: "UV 0.1",
location: "Raum 003 Technik",
fedFrom: "HAK",
remark: null,
});
assert.deepEqual(
forward.inverse.payload.changes.map((change) => change.field),
["boardNumber", "location", "fedFrom"]
);
// A board with info values is no longer the unchanged empty setup.
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "user",
command: insert.inverse,
}),
/changed before deletion/
);
const undoStep = new ProjectHistoryRepository(
context.db
).getNextCommand("project-1", "undo");
assert.ok(undoStep);
repository.executeUpdate({
projectId: "project-1",
expectedRevision: 2,
source: "undo",
historyTargetChangeSetId: undoStep.changeSetId,
command: forward.inverse,
});
assert.deepEqual(readInfo(), {
boardNumber: null,
location: null,
fedFrom: null,
remark: null,
});
for (const value of ["", " UV 1", "x".repeat(51)]) {
assert.throws(
() =>
createDistributionBoardUpdateProjectCommand(
structure.distributionBoard.id,
{ boardNumber: value }
),
/boardNumber/
);
}
} finally {
context.close();
}
});
it("sends distribution-board writes to the revision-safe API", async () => {
const requests: Array<{ url: string; method: string; body: unknown }> = [];
const originalFetch = globalThis.fetch;