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
This commit is contained in:
Janus C. H. Knudsen 2025-12-17 20:53:47 +01:00
parent b2c81dc163
commit 9f360237cf
5 changed files with 97 additions and 69 deletions

View file

@ -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<IEntityService<ISync>>();
builder.registerType(DepartmentService).as<DepartmentService>();
builder.registerType(SettingsService).as<IEntityService<ITenantSettings>>();
builder.registerType(SettingsService).as<IEntityService<TenantSetting>>();
builder.registerType(SettingsService).as<IEntityService<ISync>>();
builder.registerType(SettingsService).as<SettingsService>();
@ -180,7 +180,7 @@ export function createV2Container(): Container {
builder.registerType(MockDepartmentRepository).as<IApiRepository<IDepartment>>();
builder.registerType(MockDepartmentRepository).as<IApiRepository<ISync>>();
builder.registerType(MockSettingsRepository).as<IApiRepository<ITenantSettings>>();
builder.registerType(MockSettingsRepository).as<IApiRepository<TenantSetting>>();
builder.registerType(MockSettingsRepository).as<IApiRepository<ISync>>();
builder.registerType(MockViewConfigRepository).as<IApiRepository<ViewConfig>>();

View file

@ -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<ITenantSettings> {
export class MockSettingsRepository implements IApiRepository<TenantSetting> {
public readonly entityType: EntityType = 'Settings';
private readonly dataUrl = 'data/tenant-settings.json';
public async fetchAll(): Promise<ITenantSettings[]> {
public async fetchAll(): Promise<TenantSetting[]> {
try {
const response = await fetch(this.dataUrl);
@ -20,24 +20,23 @@ export class MockSettingsRepository implements IApiRepository<ITenantSettings> {
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<ITenantSettings> {
public async sendCreate(_settings: TenantSetting): Promise<TenantSetting> {
throw new Error('MockSettingsRepository does not support sendCreate. Mock data is read-only.');
}
public async sendUpdate(_id: string, _updates: Partial<ITenantSettings>): Promise<ITenantSettings> {
public async sendUpdate(_id: string, _updates: Partial<TenantSetting>): Promise<TenantSetting> {
throw new Error('MockSettingsRepository does not support sendUpdate. Mock data is read-only.');
}

View file

@ -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<ITenantSettings> {
export class SettingsService extends BaseEntityService<TenantSetting> {
readonly storeName = SettingsStore.STORE_NAME;
readonly entityType: EntityType = 'Settings';
@ -24,37 +27,57 @@ export class SettingsService extends BaseEntityService<ITenantSettings> {
}
/**
* Get the tenant settings document
* Returns null if not yet loaded from backend
* Get workweek settings
*/
async getSettings(): Promise<ITenantSettings | null> {
return this.get(TENANT_SETTINGS_ID);
async getWorkweekSettings(): Promise<IWorkweekSettings | null> {
return this.get(SettingsIds.WORKWEEK) as Promise<IWorkweekSettings | null>;
}
/**
* Get grid settings
*/
async getGridSettings(): Promise<IGridSettings | null> {
return this.get(SettingsIds.GRID) as Promise<IGridSettings | null>;
}
/**
* Get time format settings
*/
async getTimeFormatSettings(): Promise<ITimeFormatSettings | null> {
return this.get(SettingsIds.TIME_FORMAT) as Promise<ITimeFormatSettings | null>;
}
/**
* Get view settings
*/
async getViewSettings(): Promise<IViewSettings | null> {
return this.get(SettingsIds.VIEWS) as Promise<IViewSettings | null>;
}
/**
* Get workweek preset by ID
*/
async getWorkweekPreset(presetId: string): Promise<IWorkweekPreset | null> {
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<IWorkweekPreset | null> {
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<IWorkweekPreset[]> {
const settings = await this.getSettings();
const settings = await this.getWorkweekSettings();
if (!settings) return [];
return Object.values(settings.workweek.presets);
return Object.values(settings.presets);
}
}

View file

@ -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<string, IWorkweekPreset>;
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;