forked from jappel/leistungsbilanz-ts
Add print cover fields and office profile
- Project settings gain siteAddress, workStage, projectLead and projectEditor (project.update-settings schema version 3, migration 0009); versions 2 and 1 stay executable and keep the current cover values. Snapshot schema 6 carries the fields; older versions upgrade with null. - Global office profile (office name/address) next to the document logo: table document_branding_settings, PUT /api/document-branding/office, editable from the print view toolbar. - Cover page reads all fields; empty values show the "[ergänzen]" marker. The Planstände editor column shows the project editor. - Shared logo constants moved to a dependency-free module so the Next.js bundler can import them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4779e4a1a7
commit
7692699ee2
35 changed files with 3479 additions and 113 deletions
10
src/db/migrations/0008_project_cover_and_office_profile.sql
Normal file
10
src/db/migrations/0008_project_cover_and_office_profile.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE `document_branding_settings` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `site_address` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `work_stage` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `project_lead` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `project_editor` text;
|
||||
2553
src/db/migrations/meta/0008_snapshot.json
Normal file
2553
src/db/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -57,6 +57,13 @@
|
|||
"when": 1790411540872,
|
||||
"tag": "0007_board_info_and_document_logo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1790411560953,
|
||||
"tag": "0008_project_cover_and_office_profile",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import {
|
||||
documentBrandingAssets,
|
||||
type DocumentBrandingAsset,
|
||||
} from "../schema/document-branding-assets.js";
|
||||
import { documentBrandingSettings } from "../schema/document-branding-settings.js";
|
||||
|
||||
// Plain repository without revision semantics: branding is global office
|
||||
// configuration, not a project mutation.
|
||||
|
|
@ -49,4 +50,37 @@ export class DocumentBrandingRepository {
|
|||
.run().changes > 0
|
||||
);
|
||||
}
|
||||
|
||||
getSettings<TKey extends string>(keys: readonly TKey[]): Record<TKey, string | null> {
|
||||
const rows = this.db
|
||||
.select()
|
||||
.from(documentBrandingSettings)
|
||||
.where(inArray(documentBrandingSettings.key, [...keys]))
|
||||
.all();
|
||||
return Object.fromEntries(
|
||||
keys.map((key) => [key, rows.find((row) => row.key === key)?.value ?? null])
|
||||
) as Record<TKey, string | null>;
|
||||
}
|
||||
|
||||
/** Stores the given values atomically; `null` removes a setting. */
|
||||
setSettings(values: Record<string, string | null>) {
|
||||
const updatedAt = new Date();
|
||||
this.db.transaction((tx) => {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value === null) {
|
||||
tx.delete(documentBrandingSettings)
|
||||
.where(eq(documentBrandingSettings.key, key))
|
||||
.run();
|
||||
} else {
|
||||
tx.insert(documentBrandingSettings)
|
||||
.values({ key, value, updatedAt })
|
||||
.onConflictDoUpdate({
|
||||
target: documentBrandingSettings.key,
|
||||
set: { value, updatedAt },
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import {
|
||||
assertProjectSettingsUpdateProjectCommand,
|
||||
baselineProjectSettingsUpdateCommandSchemaVersion,
|
||||
createProjectSettingsUpdateProjectCommand,
|
||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion,
|
||||
type ProjectSettingsValues,
|
||||
} from "../../domain/models/project-settings-project-command.model.js";
|
||||
import { projectCoverFields } from "../../shared/constants/project-cover.js";
|
||||
import type {
|
||||
ExecuteProjectSettingsUpdateCommandInput,
|
||||
ProjectSettingsProjectCommandStore,
|
||||
|
|
@ -40,13 +43,27 @@ export class ProjectSettingsProjectCommandRepository
|
|||
if (!current) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
const currentCoverValues = {
|
||||
siteAddress: current.siteAddress,
|
||||
workStage: current.workStage,
|
||||
projectLead: current.projectLead,
|
||||
projectEditor: current.projectEditor,
|
||||
};
|
||||
// Older command versions do not carry every value; missing values keep
|
||||
// the current project state and are therefore also absent from the
|
||||
// inverse of the same version.
|
||||
const command = input.command;
|
||||
const target: ProjectSettingsValues =
|
||||
input.command.schemaVersion === 1
|
||||
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
|
||||
? {
|
||||
...input.command.payload,
|
||||
...command.payload,
|
||||
isPublicBuilding: current.isPublicBuilding,
|
||||
...currentCoverValues,
|
||||
}
|
||||
: input.command.payload;
|
||||
: command.schemaVersion ===
|
||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
||||
? { ...command.payload, ...currentCoverValues }
|
||||
: command.payload;
|
||||
if (projectSettingsEqual(current, target)) {
|
||||
throw new Error("Project settings did not change.");
|
||||
}
|
||||
|
|
@ -62,11 +79,15 @@ export class ProjectSettingsProjectCommandRepository
|
|||
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
current.enabledDistributionBoardSupplyTypes,
|
||||
...currentCoverValues,
|
||||
};
|
||||
const inverse =
|
||||
input.command.schemaVersion === 1
|
||||
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
|
||||
? createBaselineProjectSettingsUpdateCommand(inverseValues)
|
||||
: createProjectSettingsUpdateProjectCommand(inverseValues);
|
||||
: command.schemaVersion ===
|
||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
||||
? createWithoutCoverProjectSettingsUpdateCommand(inverseValues)
|
||||
: createProjectSettingsUpdateProjectCommand(inverseValues);
|
||||
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
|
||||
const updated = tx
|
||||
.update(projects)
|
||||
|
|
@ -82,11 +103,32 @@ export class ProjectSettingsProjectCommandRepository
|
|||
}
|
||||
}
|
||||
|
||||
function createWithoutCoverProjectSettingsUpdateCommand(
|
||||
values: ProjectSettingsValues
|
||||
) {
|
||||
return {
|
||||
schemaVersion: withoutCoverProjectSettingsUpdateCommandSchemaVersion,
|
||||
type: "project.update-settings" as const,
|
||||
payload: {
|
||||
name: values.name,
|
||||
internalProjectNumber: values.internalProjectNumber,
|
||||
externalProjectNumber: values.externalProjectNumber,
|
||||
buildingOwner: values.buildingOwner,
|
||||
description: values.description,
|
||||
isPublicBuilding: values.isPublicBuilding,
|
||||
singlePhaseVoltageV: values.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: values.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
values.enabledDistributionBoardSupplyTypes,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createBaselineProjectSettingsUpdateCommand(
|
||||
values: ProjectSettingsValues
|
||||
) {
|
||||
return {
|
||||
schemaVersion: 1 as const,
|
||||
schemaVersion: baselineProjectSettingsUpdateCommandSchemaVersion,
|
||||
type: "project.update-settings" as const,
|
||||
payload: {
|
||||
name: values.name,
|
||||
|
|
@ -114,8 +156,9 @@ function projectSettingsEqual(
|
|||
current.description === target.description &&
|
||||
current.isPublicBuilding === target.isPublicBuilding &&
|
||||
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
|
||||
current.threePhaseVoltageV === target.threePhaseVoltageV
|
||||
&& sameSupplyTypes(
|
||||
current.threePhaseVoltageV === target.threePhaseVoltageV &&
|
||||
projectCoverFields.every((field) => current[field] === target[field]) &&
|
||||
sameSupplyTypes(
|
||||
current.enabledDistributionBoardSupplyTypes,
|
||||
target.enabledDistributionBoardSupplyTypes
|
||||
)
|
||||
|
|
|
|||
|
|
@ -233,6 +233,10 @@ function replaceProjectState(
|
|||
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
state.project.enabledDistributionBoardSupplyTypes,
|
||||
siteAddress: state.project.siteAddress,
|
||||
workStage: state.project.workStage,
|
||||
projectLead: state.project.projectLead,
|
||||
projectEditor: state.project.projectEditor,
|
||||
})
|
||||
.where(eq(projects.id, state.project.id))
|
||||
.run();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ export function readProjectStateSnapshot(
|
|||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
projects.enabledDistributionBoardSupplyTypes,
|
||||
siteAddress: projects.siteAddress,
|
||||
workStage: projects.workStage,
|
||||
projectLead: projects.projectLead,
|
||||
projectEditor: projects.projectEditor,
|
||||
currentRevision: projects.currentRevision,
|
||||
})
|
||||
.from(projects)
|
||||
|
|
@ -249,6 +253,10 @@ export function readProjectStateSnapshot(
|
|||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
project.enabledDistributionBoardSupplyTypes,
|
||||
siteAddress: project.siteAddress,
|
||||
workStage: project.workStage,
|
||||
projectLead: project.projectLead,
|
||||
projectEditor: project.projectEditor,
|
||||
},
|
||||
distributionBoards: boardRows,
|
||||
externalCsvConfiguration,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ export class ProjectRepository {
|
|||
enabledDistributionBoardSupplyTypes: [
|
||||
...defaultDistributionBoardSupplyTypes,
|
||||
],
|
||||
siteAddress: null,
|
||||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
currentRevision: 0,
|
||||
};
|
||||
await db.insert(projects).values(project);
|
||||
|
|
|
|||
13
src/db/schema/document-branding-settings.ts
Normal file
13
src/db/schema/document-branding-settings.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// Office-wide text settings for printed documents (e.g. office name and
|
||||
// address on the cover page). Global application data like
|
||||
// document_branding_assets: outside project revisions, snapshots and transfer.
|
||||
export const documentBrandingSettings = sqliteTable("document_branding_settings", {
|
||||
key: text("key").primaryKey(),
|
||||
value: text("value").notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
});
|
||||
|
|
@ -21,5 +21,9 @@ export const projects = sqliteTable("projects", {
|
|||
.$type<DistributionBoardSupplyType[]>()
|
||||
.notNull()
|
||||
.default(sql`'["AV","SV","EV","USV","MSR","SiBe"]'`),
|
||||
siteAddress: text("site_address"),
|
||||
workStage: text("work_stage"),
|
||||
projectLead: text("project_lead"),
|
||||
projectEditor: text("project_editor"),
|
||||
currentRevision: integer("current_revision").notNull().default(0),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue