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
2e3e85a8e5
commit
4779e4a1a7
40 changed files with 5297 additions and 33 deletions
|
|
@ -24,3 +24,44 @@ export const distributionBoardSupplyTypeLabels: Record<
|
|||
MSR: "MSR – Mess-, Steuerungs- und Regelungstechnik",
|
||||
SiBe: "SiBe – Sicherheitsbeleuchtung",
|
||||
};
|
||||
|
||||
/**
|
||||
* Planner-owned descriptive distribution-board data shown on the board info
|
||||
* card and in the print view. The values are local planning values. A later
|
||||
* Revit/CSV round-trip may prefill them from mapped model parameters, but only
|
||||
* through a previewed, explicitly confirmed project command (see
|
||||
* docs/spec/revit-csv-integration-requirements.md); imports never overwrite
|
||||
* them silently.
|
||||
*
|
||||
* `fedFrom` is intentionally free text for now. How a board is logically
|
||||
* linked to its upstream board or feeding circuit is not decided yet.
|
||||
*/
|
||||
export const distributionBoardInfoFields = [
|
||||
"boardNumber",
|
||||
"location",
|
||||
"fedFrom",
|
||||
"remark",
|
||||
] as const;
|
||||
|
||||
export type DistributionBoardInfoField =
|
||||
(typeof distributionBoardInfoFields)[number];
|
||||
|
||||
export const distributionBoardInfoFieldLabels: Record<
|
||||
DistributionBoardInfoField,
|
||||
string
|
||||
> = {
|
||||
boardNumber: "Verteilernummer",
|
||||
location: "Örtlichkeit",
|
||||
fedFrom: "Gespeist von",
|
||||
remark: "Bemerkung",
|
||||
};
|
||||
|
||||
export const distributionBoardInfoFieldMaxLengths: Record<
|
||||
DistributionBoardInfoField,
|
||||
number
|
||||
> = {
|
||||
boardNumber: 50,
|
||||
location: 200,
|
||||
fedFrom: 200,
|
||||
remark: 2000,
|
||||
};
|
||||
|
|
|
|||
25
src/shared/validation/document-branding.schemas.ts
Normal file
25
src/shared/validation/document-branding.schemas.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const documentLogoMimeTypes = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
] as const;
|
||||
|
||||
export type DocumentLogoMimeType = (typeof documentLogoMimeTypes)[number];
|
||||
|
||||
export const maxDocumentLogoBytes = 2 * 1024 * 1024;
|
||||
|
||||
export const uploadDocumentLogoSchema = z
|
||||
.object({
|
||||
fileName: z.string().trim().min(1).max(200),
|
||||
mimeType: z.enum(documentLogoMimeTypes),
|
||||
contentBase64: z
|
||||
.string()
|
||||
.min(1)
|
||||
// Base64 expands by 4/3; the decoded size is checked again server-side.
|
||||
.max(Math.ceil((maxDocumentLogoBytes * 4) / 3) + 4),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type UploadDocumentLogoInput = z.infer<typeof uploadDocumentLogoSchema>;
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { z } from "zod";
|
||||
import { distributionBoardSupplyTypes } from "../constants/distribution-board.js";
|
||||
import {
|
||||
distributionBoardInfoFieldMaxLengths,
|
||||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardInfoField,
|
||||
} from "../constants/distribution-board.js";
|
||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
||||
|
||||
export const createProjectSchema = z.object({
|
||||
|
|
@ -42,12 +46,27 @@ export const createDistributionBoardSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
// Empty info text is stored as null so that "not entered" has one
|
||||
// representation in the database, snapshots and print view.
|
||||
const distributionBoardInfoValueSchema = (field: DistributionBoardInfoField) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(distributionBoardInfoFieldMaxLengths[field])
|
||||
.nullable()
|
||||
.transform((value) => (value ? value : null))
|
||||
.optional();
|
||||
|
||||
export const updateDistributionBoardSchema = z
|
||||
.object({
|
||||
expectedRevision: expectedProjectRevisionSchema,
|
||||
floorId: z.string().trim().min(1).nullable(),
|
||||
supplyType: z.enum(distributionBoardSupplyTypes),
|
||||
simultaneityFactor: z.number().finite().min(0).max(1),
|
||||
boardNumber: distributionBoardInfoValueSchema("boardNumber"),
|
||||
location: distributionBoardInfoValueSchema("location"),
|
||||
fedFrom: distributionBoardInfoValueSchema("fedFrom"),
|
||||
remark: distributionBoardInfoValueSchema("remark"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue