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
12
src/db/migrations/0007_board_info_and_document_logo.sql
Normal file
12
src/db/migrations/0007_board_info_and_document_logo.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE `document_branding_assets` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`mime_type` text NOT NULL,
|
||||
`file_name` text,
|
||||
`data` blob NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `distribution_boards` ADD `board_number` text;--> statement-breakpoint
|
||||
ALTER TABLE `distribution_boards` ADD `location` text;--> statement-breakpoint
|
||||
ALTER TABLE `distribution_boards` ADD `fed_from` text;--> statement-breakpoint
|
||||
ALTER TABLE `distribution_boards` ADD `remark` text;
|
||||
2493
src/db/migrations/meta/0007_snapshot.json
Normal file
2493
src/db/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -50,6 +50,13 @@
|
|||
"when": 1786043080323,
|
||||
"tag": "0006_damp_skrulls",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1790411540872,
|
||||
"tag": "0007_board_info_and_document_logo",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -388,10 +388,15 @@ function structureMatches(
|
|||
) {
|
||||
const {
|
||||
simultaneityFactor,
|
||||
boardNumber,
|
||||
location,
|
||||
fedFrom,
|
||||
remark,
|
||||
...actualDistributionBoard
|
||||
} = actual.board;
|
||||
if (
|
||||
simultaneityFactor !== 1 ||
|
||||
[boardNumber, location, fedFrom, remark].some((value) => value !== null) ||
|
||||
!sameRecord(
|
||||
expected.distributionBoard,
|
||||
actualDistributionBoard
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from "../../domain/models/distribution-board-subtree-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardSubtreeSnapshot,
|
||||
normalizeDistributionBoardSubtreeSnapshot,
|
||||
type DistributionBoardSubtreeSnapshot,
|
||||
} from "../../domain/models/distribution-board-subtree-snapshot.model.js";
|
||||
import type {
|
||||
|
|
@ -63,19 +64,20 @@ export class DistributionBoardSubtreeProjectCommandRepository
|
|||
projectId: string,
|
||||
command: DistributionBoardSubtreeProjectCommand
|
||||
) {
|
||||
const snapshot = normalizeDistributionBoardSubtreeSnapshot(
|
||||
command.payload.snapshot
|
||||
);
|
||||
if (command.type === distributionBoardInsertSubtreeCommandType) {
|
||||
this.insert(database, projectId, command.payload.snapshot);
|
||||
return createDistributionBoardDeleteSubtreeProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
this.insert(database, projectId, snapshot);
|
||||
return createDistributionBoardDeleteSubtreeProjectCommand(snapshot);
|
||||
}
|
||||
if (command.type === distributionBoardDeleteSubtreeCommandType) {
|
||||
const current = this.captureFrom(
|
||||
database,
|
||||
projectId,
|
||||
command.payload.snapshot.distributionBoard.id
|
||||
snapshot.distributionBoard.id
|
||||
);
|
||||
if (!sameSnapshot(current, command.payload.snapshot)) {
|
||||
if (!sameSnapshot(current, snapshot)) {
|
||||
throw new Error(
|
||||
"Distribution-board subtree changed before deletion."
|
||||
);
|
||||
|
|
@ -97,9 +99,7 @@ export class DistributionBoardSubtreeProjectCommandRepository
|
|||
"Distribution-board subtree could not be deleted."
|
||||
);
|
||||
}
|
||||
return createDistributionBoardInsertSubtreeProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
return createDistributionBoardInsertSubtreeProjectCommand(snapshot);
|
||||
}
|
||||
throw new Error("Unsupported distribution-board subtree command.");
|
||||
}
|
||||
|
|
|
|||
52
src/db/repositories/document-branding.repository.ts
Normal file
52
src/db/repositories/document-branding.repository.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import {
|
||||
documentBrandingAssets,
|
||||
type DocumentBrandingAsset,
|
||||
} from "../schema/document-branding-assets.js";
|
||||
|
||||
// Plain repository without revision semantics: branding is global office
|
||||
// configuration, not a project mutation.
|
||||
export class DocumentBrandingRepository {
|
||||
constructor(private readonly db: AppDatabase) {}
|
||||
|
||||
get(key: string): DocumentBrandingAsset | undefined {
|
||||
return this.db
|
||||
.select()
|
||||
.from(documentBrandingAssets)
|
||||
.where(eq(documentBrandingAssets.key, key))
|
||||
.get();
|
||||
}
|
||||
|
||||
upsert(asset: {
|
||||
key: string;
|
||||
mimeType: string;
|
||||
fileName: string | null;
|
||||
data: Buffer;
|
||||
}): DocumentBrandingAsset {
|
||||
const updatedAt = new Date();
|
||||
return this.db
|
||||
.insert(documentBrandingAssets)
|
||||
.values({ ...asset, updatedAt })
|
||||
.onConflictDoUpdate({
|
||||
target: documentBrandingAssets.key,
|
||||
set: {
|
||||
mimeType: asset.mimeType,
|
||||
fileName: asset.fileName,
|
||||
data: asset.data,
|
||||
updatedAt,
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
.get();
|
||||
}
|
||||
|
||||
delete(key: string): boolean {
|
||||
return (
|
||||
this.db
|
||||
.delete(documentBrandingAssets)
|
||||
.where(eq(documentBrandingAssets.key, key))
|
||||
.run().changes > 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-sn
|
|||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||
import {
|
||||
deserializeProjectStateSnapshot,
|
||||
isSupportedProjectStateSnapshotSchemaVersion,
|
||||
projectStateSnapshotSchemaVersion,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type {
|
||||
|
|
@ -138,7 +139,7 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
|||
if (actualChecksum !== stored.payloadSha256) {
|
||||
throw new Error("Project snapshot checksum verification failed.");
|
||||
}
|
||||
if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) {
|
||||
if (!isSupportedProjectStateSnapshotSchemaVersion(stored.schemaVersion)) {
|
||||
throw new Error("Project snapshot schema version is not supported.");
|
||||
}
|
||||
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
|
||||
|
|
|
|||
|
|
@ -16,5 +16,9 @@ export const distributionBoards = sqliteTable("distribution_boards", {
|
|||
simultaneityFactor: real("simultaneity_factor")
|
||||
.notNull()
|
||||
.default(1),
|
||||
boardNumber: text("board_number"),
|
||||
location: text("location"),
|
||||
fedFrom: text("fed_from"),
|
||||
remark: text("remark"),
|
||||
});
|
||||
|
||||
|
|
|
|||
18
src/db/schema/document-branding-assets.ts
Normal file
18
src/db/schema/document-branding-assets.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { sql } from "drizzle-orm";
|
||||
import { blob, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// Office-wide branding for printed documents (currently only the logo shown
|
||||
// on the cover page and plan title blocks). Global application data, not part
|
||||
// of any project: it is deliberately outside project revisions, logical
|
||||
// project snapshots and project transfer.
|
||||
export const documentBrandingAssets = sqliteTable("document_branding_assets", {
|
||||
key: text("key").primaryKey(),
|
||||
mimeType: text("mime_type").notNull(),
|
||||
fileName: text("file_name"),
|
||||
data: blob("data", { mode: "buffer" }).notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export type DocumentBrandingAsset = typeof documentBrandingAssets.$inferSelect;
|
||||
Loading…
Add table
Add a link
Reference in a new issue