leistungsbilanz-ts/src/db/repositories/document-branding.repository.ts
Grovy311 7692699ee2 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>
2026-09-26 10:39:18 +02:00

86 lines
2.5 KiB
TypeScript

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.
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
);
}
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();
}
}
});
}
}