Refactors calendar view configuration management
Decouples view configuration from DemoApp logic by: - Introducing ViewConfigService and MockViewConfigRepository - Moving view configuration to centralized JSON data - Simplifying DemoApp rendering process Improves separation of concerns and makes view configurations more maintainable
This commit is contained in:
parent
6a56396721
commit
7f9d0129bf
9 changed files with 217 additions and 708 deletions
|
|
@ -38,6 +38,9 @@ import { DepartmentService } from './storage/departments/DepartmentService';
|
|||
import { SettingsStore } from './storage/settings/SettingsStore';
|
||||
import { SettingsService } from './storage/settings/SettingsService';
|
||||
import { ITenantSettings } from './types/SettingsTypes';
|
||||
import { ViewConfigStore } from './storage/viewconfigs/ViewConfigStore';
|
||||
import { ViewConfigService } from './storage/viewconfigs/ViewConfigService';
|
||||
import { ViewConfig } from './core/ViewConfig';
|
||||
|
||||
// Audit
|
||||
import { AuditStore } from './storage/audit/AuditStore';
|
||||
|
|
@ -54,6 +57,7 @@ import { MockAuditRepository } from './repositories/MockAuditRepository';
|
|||
import { MockTeamRepository } from './repositories/MockTeamRepository';
|
||||
import { MockDepartmentRepository } from './repositories/MockDepartmentRepository';
|
||||
import { MockSettingsRepository } from './repositories/MockSettingsRepository';
|
||||
import { MockViewConfigRepository } from './repositories/MockViewConfigRepository';
|
||||
|
||||
// Workers
|
||||
import { DataSeeder } from './workers/DataSeeder';
|
||||
|
|
@ -118,6 +122,7 @@ export function createV2Container(): Container {
|
|||
builder.registerType(ScheduleOverrideStore).as<IStore>();
|
||||
builder.registerType(AuditStore).as<IStore>();
|
||||
builder.registerType(SettingsStore).as<IStore>();
|
||||
builder.registerType(ViewConfigStore).as<IStore>();
|
||||
|
||||
// Entity services (for DataSeeder polymorphic array)
|
||||
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
|
||||
|
|
@ -148,6 +153,10 @@ export function createV2Container(): Container {
|
|||
builder.registerType(SettingsService).as<IEntityService<ISync>>();
|
||||
builder.registerType(SettingsService).as<SettingsService>();
|
||||
|
||||
builder.registerType(ViewConfigService).as<IEntityService<ViewConfig>>();
|
||||
builder.registerType(ViewConfigService).as<IEntityService<ISync>>();
|
||||
builder.registerType(ViewConfigService).as<ViewConfigService>();
|
||||
|
||||
// Repositories (for DataSeeder polymorphic array)
|
||||
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
|
||||
builder.registerType(MockEventRepository).as<IApiRepository<ISync>>();
|
||||
|
|
@ -173,6 +182,9 @@ export function createV2Container(): Container {
|
|||
builder.registerType(MockSettingsRepository).as<IApiRepository<ITenantSettings>>();
|
||||
builder.registerType(MockSettingsRepository).as<IApiRepository<ISync>>();
|
||||
|
||||
builder.registerType(MockViewConfigRepository).as<IApiRepository<ViewConfig>>();
|
||||
builder.registerType(MockViewConfigRepository).as<IApiRepository<ISync>>();
|
||||
|
||||
// Audit service (listens to ENTITY_SAVED/DELETED events automatically)
|
||||
builder.registerType(AuditService).as<AuditService>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { ISync } from '../types/CalendarTypes';
|
||||
|
||||
export interface ViewTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
groupingTypes: string[];
|
||||
}
|
||||
|
||||
export interface ViewConfig {
|
||||
templateId: string;
|
||||
export interface ViewConfig extends ISync {
|
||||
id: string; // templateId (e.g. 'day', 'simple', 'resource')
|
||||
groupings: GroupingConfig[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRende
|
|||
import { AuditService } from '../storage/audit/AuditService';
|
||||
import { SettingsService } from '../storage/settings/SettingsService';
|
||||
import { ResourceService } from '../storage/resources/ResourceService';
|
||||
import { ViewConfigService } from '../storage/viewconfigs/ViewConfigService';
|
||||
import { IWorkweekPreset } from '../types/SettingsTypes';
|
||||
|
||||
export class DemoApp {
|
||||
|
|
@ -23,7 +24,6 @@ export class DemoApp {
|
|||
private weekOffset = 0;
|
||||
private currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple';
|
||||
private workweekPreset: IWorkweekPreset | null = null;
|
||||
private selectedResourceIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private orchestrator: CalendarOrchestrator,
|
||||
|
|
@ -40,7 +40,8 @@ export class DemoApp {
|
|||
private eventPersistenceManager: EventPersistenceManager,
|
||||
private auditService: AuditService,
|
||||
private settingsService: SettingsService,
|
||||
private resourceService: ResourceService
|
||||
private resourceService: ResourceService,
|
||||
private viewConfigService: ViewConfigService
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
|
|
@ -99,72 +100,28 @@ export class DemoApp {
|
|||
}
|
||||
|
||||
private async render(): Promise<void> {
|
||||
const viewConfig = this.buildViewConfig();
|
||||
await this.orchestrator.render(viewConfig, this.container);
|
||||
}
|
||||
|
||||
private buildViewConfig(): ViewConfig {
|
||||
// Use workweek preset to determine which days to show
|
||||
const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5]; // Fallback to Mon-Fri
|
||||
const dates = this.dateService.getWorkWeekDates(this.weekOffset, workDays);
|
||||
const today = this.dateService.getWeekDates(this.weekOffset, 1);
|
||||
|
||||
switch (this.currentView) {
|
||||
case 'day':
|
||||
return {
|
||||
templateId: 'day',
|
||||
groupings: [
|
||||
{ type: 'resource', values: this.selectedResourceIds, idProperty: 'resourceId' },
|
||||
{ type: 'date', values: today, idProperty: 'date', derivedFrom: 'start', hideHeader: true }
|
||||
]
|
||||
};
|
||||
|
||||
case 'simple':
|
||||
return {
|
||||
templateId: 'simple',
|
||||
groupings: [
|
||||
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
};
|
||||
|
||||
case 'resource':
|
||||
return {
|
||||
templateId: 'resource',
|
||||
groupings: [
|
||||
{ type: 'resource', values: ['EMP001', 'EMP002'], idProperty: 'resourceId' },
|
||||
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
};
|
||||
|
||||
case 'team':
|
||||
return {
|
||||
templateId: 'team',
|
||||
groupings: [
|
||||
{ type: 'team', values: ['team1', 'team2'] },
|
||||
{ type: 'resource', values: ['EMP001', 'EMP002', 'EMP003', 'EMP004'], idProperty: 'resourceId', belongsTo: 'team.resourceIds' },
|
||||
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
};
|
||||
|
||||
case 'department':
|
||||
return {
|
||||
templateId: 'department',
|
||||
groupings: [
|
||||
{ type: 'department', values: ['dept-styling', 'dept-training'] },
|
||||
{ type: 'resource', values: ['EMP001', 'EMP002', 'EMP003', 'EMP004', 'STUDENT001', 'STUDENT002'], idProperty: 'resourceId', belongsTo: 'department.resourceIds' },
|
||||
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
};
|
||||
|
||||
case 'picker':
|
||||
return {
|
||||
templateId: 'picker',
|
||||
groupings: [
|
||||
{ type: 'resource', values: this.selectedResourceIds, idProperty: 'resourceId' },
|
||||
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
};
|
||||
// Load ViewConfig from IndexedDB
|
||||
const storedConfig = await this.viewConfigService.getById(this.currentView);
|
||||
if (!storedConfig) {
|
||||
console.error(`[DemoApp] ViewConfig not found for templateId: ${this.currentView}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate date values based on workweek and offset
|
||||
const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5];
|
||||
const dates = this.currentView === 'day'
|
||||
? this.dateService.getWeekDates(this.weekOffset, 1)
|
||||
: this.dateService.getWorkWeekDates(this.weekOffset, workDays);
|
||||
|
||||
// Clone config and populate dates
|
||||
const viewConfig: ViewConfig = {
|
||||
...storedConfig,
|
||||
groupings: storedConfig.groupings.map(g =>
|
||||
g.type === 'date' ? { ...g, values: dates } : g
|
||||
)
|
||||
};
|
||||
|
||||
await this.orchestrator.render(viewConfig, this.container);
|
||||
}
|
||||
|
||||
private setupNavigation(): void {
|
||||
|
|
@ -224,25 +181,15 @@ export class DemoApp {
|
|||
// Clear existing
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create checkboxes for each resource
|
||||
// Display resources (read-only, values are stored in ViewConfig)
|
||||
resources.forEach(r => {
|
||||
const label = document.createElement('label');
|
||||
label.innerHTML = `
|
||||
<input type="checkbox" value="${r.id}" checked>
|
||||
<input type="checkbox" value="${r.id}" checked disabled>
|
||||
${r.displayName}
|
||||
`;
|
||||
container.appendChild(label);
|
||||
});
|
||||
|
||||
// Default: all selected
|
||||
this.selectedResourceIds = resources.map(r => r.id);
|
||||
|
||||
// Event listener for checkbox changes
|
||||
container.addEventListener('change', () => {
|
||||
const checked = container.querySelectorAll('input:checked') as NodeListOf<HTMLInputElement>;
|
||||
this.selectedResourceIds = Array.from(checked).map(cb => cb.value);
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
private updateSelectorVisibility(): void {
|
||||
|
|
|
|||
41
src/v2/repositories/MockViewConfigRepository.ts
Normal file
41
src/v2/repositories/MockViewConfigRepository.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { EntityType } from '../types/CalendarTypes';
|
||||
import { ViewConfig } from '../core/ViewConfig';
|
||||
import { IApiRepository } from './IApiRepository';
|
||||
|
||||
export class MockViewConfigRepository implements IApiRepository<ViewConfig> {
|
||||
public readonly entityType: EntityType = 'ViewConfig';
|
||||
private readonly dataUrl = 'data/viewconfigs.json';
|
||||
|
||||
public async fetchAll(): Promise<ViewConfig[]> {
|
||||
try {
|
||||
const response = await fetch(this.dataUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load viewconfigs: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
// Ensure syncStatus is set on each config
|
||||
const configs: ViewConfig[] = rawData.map((config: ViewConfig) => ({
|
||||
...config,
|
||||
syncStatus: config.syncStatus || 'synced'
|
||||
}));
|
||||
return configs;
|
||||
} catch (error) {
|
||||
console.error('Failed to load viewconfigs:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async sendCreate(_config: ViewConfig): Promise<ViewConfig> {
|
||||
throw new Error('MockViewConfigRepository does not support sendCreate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
public async sendUpdate(_id: string, _updates: Partial<ViewConfig>): Promise<ViewConfig> {
|
||||
throw new Error('MockViewConfigRepository does not support sendUpdate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
public async sendDelete(_id: string): Promise<void> {
|
||||
throw new Error('MockViewConfigRepository does not support sendDelete. Mock data is read-only.');
|
||||
}
|
||||
}
|
||||
18
src/v2/storage/viewconfigs/ViewConfigService.ts
Normal file
18
src/v2/storage/viewconfigs/ViewConfigService.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { EntityType, IEventBus } from '../../types/CalendarTypes';
|
||||
import { ViewConfig } from '../../core/ViewConfig';
|
||||
import { ViewConfigStore } from './ViewConfigStore';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
import { IndexedDBContext } from '../IndexedDBContext';
|
||||
|
||||
export class ViewConfigService extends BaseEntityService<ViewConfig> {
|
||||
readonly storeName = ViewConfigStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'ViewConfig';
|
||||
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
super(context, eventBus);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<ViewConfig | null> {
|
||||
return this.get(id);
|
||||
}
|
||||
}
|
||||
10
src/v2/storage/viewconfigs/ViewConfigStore.ts
Normal file
10
src/v2/storage/viewconfigs/ViewConfigStore.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { IStore } from '../IStore';
|
||||
|
||||
export class ViewConfigStore implements IStore {
|
||||
static readonly STORE_NAME = 'viewconfigs';
|
||||
readonly storeName = ViewConfigStore.STORE_NAME;
|
||||
|
||||
create(db: IDBDatabase): void {
|
||||
db.createObjectStore(ViewConfigStore.STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { IWeekSchedule } from './ScheduleTypes';
|
|||
|
||||
export type SyncStatus = 'synced' | 'pending' | 'error';
|
||||
|
||||
export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit' | 'Settings';
|
||||
export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit' | 'Settings' | 'ViewConfig';
|
||||
|
||||
/**
|
||||
* CalendarEventType - Used by ICalendarEvent.type
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue