From 9f360237cf95ac3c7c91269b7f27f726605b8dc1 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Wed, 17 Dec 2025 20:53:47 +0100 Subject: [PATCH] Refactor settings model to separate record-based approach Restructures tenant settings to use individual records instead of a single document Decouples settings sections into separate typed interfaces with unique IDs Modifies data loading and service methods to support new record-based settings Updates mock data and repository to align with new settings structure --- src/v2/V2CompositionRoot.ts | 6 +- src/v2/repositories/MockSettingsRepository.ts | 27 ++++---- src/v2/storage/settings/SettingsService.ts | 61 +++++++++++++------ src/v2/types/SettingsTypes.ts | 45 ++++++++------ wwwroot/data/tenant-settings.json | 27 ++++---- 5 files changed, 97 insertions(+), 69 deletions(-) diff --git a/src/v2/V2CompositionRoot.ts b/src/v2/V2CompositionRoot.ts index 5950e06..27e2b9a 100644 --- a/src/v2/V2CompositionRoot.ts +++ b/src/v2/V2CompositionRoot.ts @@ -38,7 +38,7 @@ import { DepartmentStore } from './storage/departments/DepartmentStore'; import { DepartmentService } from './storage/departments/DepartmentService'; import { SettingsStore } from './storage/settings/SettingsStore'; import { SettingsService } from './storage/settings/SettingsService'; -import { ITenantSettings } from './types/SettingsTypes'; +import { TenantSetting } from './types/SettingsTypes'; import { ViewConfigStore } from './storage/viewconfigs/ViewConfigStore'; import { ViewConfigService } from './storage/viewconfigs/ViewConfigService'; import { ViewConfig } from './core/ViewConfig'; @@ -150,7 +150,7 @@ export function createV2Container(): Container { builder.registerType(DepartmentService).as>(); builder.registerType(DepartmentService).as(); - builder.registerType(SettingsService).as>(); + builder.registerType(SettingsService).as>(); builder.registerType(SettingsService).as>(); builder.registerType(SettingsService).as(); @@ -180,7 +180,7 @@ export function createV2Container(): Container { builder.registerType(MockDepartmentRepository).as>(); builder.registerType(MockDepartmentRepository).as>(); - builder.registerType(MockSettingsRepository).as>(); + builder.registerType(MockSettingsRepository).as>(); builder.registerType(MockSettingsRepository).as>(); builder.registerType(MockViewConfigRepository).as>(); diff --git a/src/v2/repositories/MockSettingsRepository.ts b/src/v2/repositories/MockSettingsRepository.ts index 9507fec..0490d89 100644 --- a/src/v2/repositories/MockSettingsRepository.ts +++ b/src/v2/repositories/MockSettingsRepository.ts @@ -1,18 +1,18 @@ import { EntityType } from '../types/CalendarTypes'; -import { ITenantSettings } from '../types/SettingsTypes'; +import { TenantSetting } from '../types/SettingsTypes'; import { IApiRepository } from './IApiRepository'; /** * MockSettingsRepository - Loads tenant settings from local JSON file * - * Settings is a single document, but we wrap it in an array to match - * the IApiRepository interface used by DataSeeder. + * Settings are stored as separate records per section (workweek, grid, etc.). + * The JSON file is already an array of TenantSetting records. */ -export class MockSettingsRepository implements IApiRepository { +export class MockSettingsRepository implements IApiRepository { public readonly entityType: EntityType = 'Settings'; private readonly dataUrl = 'data/tenant-settings.json'; - public async fetchAll(): Promise { + public async fetchAll(): Promise { try { const response = await fetch(this.dataUrl); @@ -20,24 +20,23 @@ export class MockSettingsRepository implements IApiRepository { throw new Error(`Failed to load tenant settings: ${response.status} ${response.statusText}`); } - const rawData = await response.json(); - // Ensure syncStatus is set - const settings: ITenantSettings = { - ...rawData, - syncStatus: rawData.syncStatus || 'synced' - }; - return [settings]; + const settings: TenantSetting[] = await response.json(); + // Ensure syncStatus is set on each record + return settings.map(s => ({ + ...s, + syncStatus: s.syncStatus || 'synced' + })); } catch (error) { console.error('Failed to load tenant settings:', error); throw error; } } - public async sendCreate(_settings: ITenantSettings): Promise { + public async sendCreate(_settings: TenantSetting): Promise { throw new Error('MockSettingsRepository does not support sendCreate. Mock data is read-only.'); } - public async sendUpdate(_id: string, _updates: Partial): Promise { + public async sendUpdate(_id: string, _updates: Partial): Promise { throw new Error('MockSettingsRepository does not support sendUpdate. Mock data is read-only.'); } diff --git a/src/v2/storage/settings/SettingsService.ts b/src/v2/storage/settings/SettingsService.ts index 535fcc0..5bc57b4 100644 --- a/src/v2/storage/settings/SettingsService.ts +++ b/src/v2/storage/settings/SettingsService.ts @@ -1,21 +1,24 @@ import { EntityType, IEventBus } from '../../types/CalendarTypes'; -import { ITenantSettings, IWorkweekPreset } from '../../types/SettingsTypes'; +import { + TenantSetting, + IWorkweekSettings, + IGridSettings, + ITimeFormatSettings, + IViewSettings, + IWorkweekPreset, + SettingsIds +} from '../../types/SettingsTypes'; import { SettingsStore } from './SettingsStore'; import { BaseEntityService } from '../BaseEntityService'; import { IndexedDBContext } from '../IndexedDBContext'; -/** - * Default settings ID - single document per tenant - */ -const TENANT_SETTINGS_ID = 'tenant-settings'; - /** * SettingsService - CRUD operations for tenant settings * - * Settings are stored as a single document with sections. - * This service provides convenience methods for accessing specific sections. + * Settings are stored as separate records per section. + * This service provides typed methods for accessing specific settings. */ -export class SettingsService extends BaseEntityService { +export class SettingsService extends BaseEntityService { readonly storeName = SettingsStore.STORE_NAME; readonly entityType: EntityType = 'Settings'; @@ -24,37 +27,57 @@ export class SettingsService extends BaseEntityService { } /** - * Get the tenant settings document - * Returns null if not yet loaded from backend + * Get workweek settings */ - async getSettings(): Promise { - return this.get(TENANT_SETTINGS_ID); + async getWorkweekSettings(): Promise { + return this.get(SettingsIds.WORKWEEK) as Promise; + } + + /** + * Get grid settings + */ + async getGridSettings(): Promise { + return this.get(SettingsIds.GRID) as Promise; + } + + /** + * Get time format settings + */ + async getTimeFormatSettings(): Promise { + return this.get(SettingsIds.TIME_FORMAT) as Promise; + } + + /** + * Get view settings + */ + async getViewSettings(): Promise { + return this.get(SettingsIds.VIEWS) as Promise; } /** * Get workweek preset by ID */ async getWorkweekPreset(presetId: string): Promise { - const settings = await this.getSettings(); + const settings = await this.getWorkweekSettings(); if (!settings) return null; - return settings.workweek.presets[presetId] || null; + return settings.presets[presetId] || null; } /** * Get the default workweek preset */ async getDefaultWorkweekPreset(): Promise { - const settings = await this.getSettings(); + const settings = await this.getWorkweekSettings(); if (!settings) return null; - return settings.workweek.presets[settings.workweek.defaultPreset] || null; + return settings.presets[settings.defaultPreset] || null; } /** * Get all available workweek presets */ async getWorkweekPresets(): Promise { - const settings = await this.getSettings(); + const settings = await this.getWorkweekSettings(); if (!settings) return []; - return Object.values(settings.workweek.presets); + return Object.values(settings.presets); } } diff --git a/src/v2/types/SettingsTypes.ts b/src/v2/types/SettingsTypes.ts index 1b10728..8f25f45 100644 --- a/src/v2/types/SettingsTypes.ts +++ b/src/v2/types/SettingsTypes.ts @@ -3,6 +3,8 @@ * * Settings are tenant-specific configuration that comes from the backend * and is stored in IndexedDB for offline access. + * + * Each settings section is stored as a separate record with its own id. */ import { ISync } from './CalendarTypes'; @@ -18,18 +20,20 @@ export interface IWorkweekPreset { } /** - * Workweek settings section + * Workweek settings - stored as separate record */ -export interface IWorkweekSettings { +export interface IWorkweekSettings extends ISync { + id: 'workweek'; presets: Record; defaultPreset: string; firstDayOfWeek: number; // ISO: 1=Monday } /** - * Grid display settings section + * Grid display settings - stored as separate record */ -export interface IGridSettings { +export interface IGridSettings extends ISync { + id: 'grid'; dayStartHour: number; dayEndHour: number; workStartHour: number; @@ -39,34 +43,35 @@ export interface IGridSettings { } /** - * Time format settings section + * Time format settings - stored as separate record */ -export interface ITimeFormatSettings { +export interface ITimeFormatSettings extends ISync { + id: 'timeFormat'; timezone: string; locale: string; use24HourFormat: boolean; } /** - * View settings section + * View settings - stored as separate record */ -export interface IViewSettings { +export interface IViewSettings extends ISync { + id: 'views'; availableViews: string[]; defaultView: string; } /** - * ITenantSettings - Complete tenant configuration - * - * Single document stored in IndexedDB 'settings' store. - * Sections can be extended as needed without schema changes. + * Union type for all tenant settings records */ -export interface ITenantSettings extends ISync { - id: string; - lastModified?: string; +export type TenantSetting = IWorkweekSettings | IGridSettings | ITimeFormatSettings | IViewSettings; - workweek: IWorkweekSettings; - grid: IGridSettings; - timeFormat: ITimeFormatSettings; - views: IViewSettings; -} +/** + * Settings IDs as const for type safety + */ +export const SettingsIds = { + WORKWEEK: 'workweek', + GRID: 'grid', + TIME_FORMAT: 'timeFormat', + VIEWS: 'views' +} as const; diff --git a/wwwroot/data/tenant-settings.json b/wwwroot/data/tenant-settings.json index 7509d65..0c70562 100644 --- a/wwwroot/data/tenant-settings.json +++ b/wwwroot/data/tenant-settings.json @@ -1,9 +1,7 @@ -{ - "id": "tenant-settings", - "syncStatus": "synced", - "lastModified": "2025-12-15T10:00:00Z", - - "workweek": { +[ + { + "id": "workweek", + "syncStatus": "synced", "presets": { "standard": { "id": "standard", @@ -34,8 +32,9 @@ "defaultPreset": "standard", "firstDayOfWeek": 1 }, - - "grid": { + { + "id": "grid", + "syncStatus": "synced", "dayStartHour": 6, "dayEndHour": 22, "workStartHour": 8, @@ -43,15 +42,17 @@ "hourHeight": 80, "snapInterval": 15 }, - - "timeFormat": { + { + "id": "timeFormat", + "syncStatus": "synced", "timezone": "Europe/Copenhagen", "locale": "da-DK", "use24HourFormat": true }, - - "views": { + { + "id": "views", + "syncStatus": "synced", "availableViews": ["simple", "resource", "team", "department"], "defaultView": "simple" } -} +]