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
|
|
@ -1,664 +1,98 @@
|
||||||
# Specification: Calendar Command Event System
|
# CalendarApp Event Specification
|
||||||
|
|
||||||
## 1. Overview
|
## 1. Oversigt
|
||||||
|
|
||||||
### 1.1 Purpose
|
CalendarApp initialiseres med `CalendarApp.create(container)`.
|
||||||
Gør kalenderen til en genbrugelig komponent der:
|
|
||||||
1. Initialiseres med **én linje kode**
|
|
||||||
2. Modtager **ViewConfig** via command event
|
|
||||||
3. Håndterer al intern setup (scroll, drag-drop, resize, etc.)
|
|
||||||
|
|
||||||
### 1.2 Design Principles
|
Kommunikation sker via DOM events:
|
||||||
| Principle | Description |
|
- **Command events**: Host → Calendar
|
||||||
|-----------|-------------|
|
- **Status events**: Calendar → Host
|
||||||
| **Encapsulated** | Al init pakket ind i CalendarApp |
|
|
||||||
| **ViewConfig-driven** | Host sender ViewConfig - kalender renderer |
|
|
||||||
| **Single command** | Kun `calendar:cmd:render` |
|
|
||||||
| **Zero knowledge** | Kalenderen ved ikke hvad den renderer |
|
|
||||||
|
|
||||||
### 1.3 Ansvarsfordeling
|
Settings hentes fra `SettingsService` (IndexedDB).
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ HOST APPLICATION │
|
|
||||||
│ │
|
|
||||||
│ // Init (én gang) │
|
|
||||||
│ const calendar = await CalendarApp.create(container, { │
|
|
||||||
│ hourHeight: 64, │
|
|
||||||
│ dayStartHour: 6, │
|
|
||||||
│ dayEndHour: 18 │
|
|
||||||
│ }); │
|
|
||||||
│ │
|
|
||||||
│ // Render (når som helst) │
|
|
||||||
│ document.dispatchEvent(new CustomEvent('calendar:cmd:render', │
|
|
||||||
│ { detail: { viewConfig, animation: 'left' } } │
|
|
||||||
│ )); │
|
|
||||||
│ │
|
|
||||||
│ Ansvar: │
|
|
||||||
│ - Bygge ViewConfig │
|
|
||||||
│ - Sende render command │
|
|
||||||
│ - Lytte på status events │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
calendar:cmd:render + ViewConfig
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ CALENDAR APP (Black Box) │
|
|
||||||
│ │
|
|
||||||
│ Pakker ind: │
|
|
||||||
│ - IndexedDB init │
|
|
||||||
│ - DataSeeder │
|
|
||||||
│ - ScrollManager │
|
|
||||||
│ - DragDropManager │
|
|
||||||
│ - EdgeScrollManager │
|
|
||||||
│ - ResizeManager │
|
|
||||||
│ - HeaderDrawerManager │
|
|
||||||
│ - TimeAxisRenderer │
|
|
||||||
│ - NavigationAnimator │
|
|
||||||
│ - CalendarOrchestrator │
|
|
||||||
│ - Command event listeners │
|
|
||||||
│ │
|
|
||||||
│ Host behøver IKKE vide om disse │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. ViewConfig (Eksisterende - Uændret)
|
## 2. Command Events (Host → Calendar)
|
||||||
|
|
||||||
Vi genbruger den eksisterende ViewConfig fra `src/v2/core/ViewConfig.ts`:
|
| Event | Payload | Beskrivelse |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `calendar:cmd:render` | `{ viewConfig }` | Render kalenderen med ViewConfig |
|
||||||
|
|
||||||
```typescript
|
|
||||||
// EKSISTERENDE - INGEN ÆNDRINGER
|
|
||||||
export interface ViewConfig {
|
|
||||||
templateId: string;
|
|
||||||
groupings: GroupingConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GroupingConfig {
|
|
||||||
type: string;
|
|
||||||
values: string[];
|
|
||||||
idProperty?: string;
|
|
||||||
derivedFrom?: string;
|
|
||||||
belongsTo?: string;
|
|
||||||
hideHeader?: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Eksempel ViewConfig (som DemoApp allerede bygger):**
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
templateId: 'team',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'team', values: ['team1', 'team2'] },
|
|
||||||
{ type: 'resource', values: ['EMP001', 'EMP002'],
|
|
||||||
idProperty: 'resourceId', belongsTo: 'team.resourceIds' },
|
|
||||||
{ type: 'date', values: ['2025-12-08', '2025-12-09', ...],
|
|
||||||
idProperty: 'date', derivedFrom: 'start' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Event Contract
|
|
||||||
|
|
||||||
### 3.1 Command Events (Inbound)
|
|
||||||
| Event Name | Payload | Beskrivelse |
|
|
||||||
|------------|---------|-------------|
|
|
||||||
| `calendar:cmd:render` | `{ viewConfig, animation? }` | Render med ViewConfig |
|
|
||||||
|
|
||||||
**Payload interface:**
|
|
||||||
```typescript
|
```typescript
|
||||||
interface IRenderCommandPayload {
|
interface IRenderCommandPayload {
|
||||||
viewConfig: ViewConfig;
|
viewConfig: ViewConfig;
|
||||||
animation?: 'left' | 'right'; // Optional slide animation
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 Status Events (Outbound)
|
**Eksempel:**
|
||||||
| Event Name | Payload | Beskrivelse |
|
|
||||||
|------------|---------|-------------|
|
|
||||||
| `calendar:status:ready` | `{}` | Kalender klar til commands |
|
|
||||||
| `calendar:status:rendered` | `{ templateId }` | Rendering færdig |
|
|
||||||
| `calendar:status:error` | `{ message, code }` | Fejl |
|
|
||||||
|
|
||||||
### 3.3 Error Codes
|
|
||||||
```typescript
|
```typescript
|
||||||
type CommandErrorCode =
|
|
||||||
| 'INVALID_PAYLOAD' // ViewConfig mangler eller ugyldig
|
|
||||||
| 'ANIMATION_IN_PROGRESS' // Render afvist pga. igangværende animation
|
|
||||||
| 'RENDER_FAILED'; // CalendarOrchestrator fejlede
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Sequence Diagrams
|
|
||||||
|
|
||||||
### 4.1 Render Flow (med animation)
|
|
||||||
```
|
|
||||||
┌──────────┐ ┌──────────┐ ┌─────────────────┐ ┌────────────┐ ┌──────────────┐
|
|
||||||
│ Host App │ │ document │ │ CalendarApp │ │ Animator │ │ Orchestrator │
|
|
||||||
└────┬─────┘ └────┬─────┘ └───────┬─────────┘ └─────┬──────┘ └──────┬───────┘
|
|
||||||
│ │ │ │ │
|
|
||||||
│ // Host bygger │ │ │ │
|
|
||||||
│ // ViewConfig │ │ │ │
|
|
||||||
│ weekOffset++; │ │ │ │
|
|
||||||
│ viewConfig = │ │ │ │
|
|
||||||
│ buildViewConfig()│ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ dispatchEvent │ │ │ │
|
|
||||||
│ ('calendar:cmd: │ │ │ │
|
|
||||||
│ render', { │ │ │ │
|
|
||||||
│ viewConfig, │ │ │ │
|
|
||||||
│ animation:'left'})│ │ │ │
|
|
||||||
│────────────────────>│ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ CustomEvent │ │ │
|
|
||||||
│ │───────────────────────>│ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ validate payload │ │
|
|
||||||
│ │ │──────────┐ │ │
|
|
||||||
│ │ │<─────────┘ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ slide('left', () => { │ │
|
|
||||||
│ │ │─────────────────────────>│ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ animateOut() │
|
|
||||||
│ │ │ │─────────┐ │
|
|
||||||
│ │ │ │<────────┘ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ renderCallback() │ │
|
|
||||||
│ │ │<─────────────────────────│ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ render(viewConfig) │ │
|
|
||||||
│ │ │─────────────────────────────────────────────────>│
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │ DOM
|
|
||||||
│ │ │ │ │─────┐
|
|
||||||
│ │ │ │ │<────┘
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ animateIn() │
|
|
||||||
│ │ │ │─────────┐ │
|
|
||||||
│ │ │ │<────────┘ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ 'calendar:status: │ │ │ │
|
|
||||||
│ rendered' │ │ │ │
|
|
||||||
│<────────────────────│────────────────────────│ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Render Flow (uden animation)
|
|
||||||
```
|
|
||||||
┌──────────┐ ┌──────────┐ ┌─────────────────┐ ┌──────────────┐
|
|
||||||
│ Host App │ │ document │ │ CalendarApp │ │ Orchestrator │
|
|
||||||
└────┬─────┘ └────┬─────┘ └───────┬─────────┘ └──────┬───────┘
|
|
||||||
│ │ │ │
|
|
||||||
│ dispatchEvent │ │ │
|
|
||||||
│ ('calendar:cmd: │ │ │
|
|
||||||
│ render', { │ │ │
|
|
||||||
│ viewConfig }) │ │ │
|
|
||||||
│────────────────────>│ │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ CustomEvent │ │
|
|
||||||
│ │───────────────────────>│ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ │ render(viewConfig) │
|
|
||||||
│ │ │──────────────────────────>│
|
|
||||||
│ │ │ │
|
|
||||||
│ │ │ │ DOM
|
|
||||||
│ │ │ │─────┐
|
|
||||||
│ │ │ │<────┘
|
|
||||||
│ │ │ │
|
|
||||||
│ 'calendar:status: │ │ │
|
|
||||||
│ rendered' │ │ │
|
|
||||||
│<────────────────────│───────────────────────│ │
|
|
||||||
│ │ │ │
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Error Flow
|
|
||||||
```
|
|
||||||
┌──────────┐ ┌──────────┐ ┌─────────────────┐
|
|
||||||
│ Host App │ │ document │ │ CalendarApp │
|
|
||||||
└────┬─────┘ └────┬─────┘ └───────┬─────────┘
|
|
||||||
│ │ │
|
|
||||||
│ dispatchEvent │ │
|
|
||||||
│ ('calendar:cmd: │ │
|
|
||||||
│ render', {}) │ │
|
|
||||||
│ // Missing │ │
|
|
||||||
│ // viewConfig │ │
|
|
||||||
│────────────────────>│ │
|
|
||||||
│ │ │
|
|
||||||
│ │ CustomEvent │
|
|
||||||
│ │───────────────────────>│
|
|
||||||
│ │ │
|
|
||||||
│ │ │ validate: no viewConfig
|
|
||||||
│ │ │──────────┐
|
|
||||||
│ │ │<─────────┘
|
|
||||||
│ │ │
|
|
||||||
│ 'calendar:status: │ │
|
|
||||||
│ error' │ │
|
|
||||||
│ { code: │ │
|
|
||||||
│ 'INVALID_PAYLOAD'}│ │
|
|
||||||
│<────────────────────│────────────────────────│
|
|
||||||
│ │ │
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Type Definitions
|
|
||||||
|
|
||||||
### 5.1 CommandTypes.ts
|
|
||||||
```typescript
|
|
||||||
import { ViewConfig } from '../core/ViewConfig';
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// COMMAND PAYLOAD
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface IRenderCommandPayload {
|
|
||||||
viewConfig: ViewConfig;
|
|
||||||
animation?: 'left' | 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// STATUS PAYLOADS
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface IReadyStatusPayload {
|
|
||||||
// Empty - just signals ready
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRenderedStatusPayload {
|
|
||||||
templateId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IErrorStatusPayload {
|
|
||||||
message: string;
|
|
||||||
code: CommandErrorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandErrorCode =
|
|
||||||
| 'INVALID_PAYLOAD'
|
|
||||||
| 'ANIMATION_IN_PROGRESS'
|
|
||||||
| 'RENDER_FAILED';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 CommandEvents.ts
|
|
||||||
```typescript
|
|
||||||
export const CommandEvents = {
|
|
||||||
// Command (inbound)
|
|
||||||
RENDER: 'calendar:cmd:render',
|
|
||||||
|
|
||||||
// Status (outbound)
|
|
||||||
READY: 'calendar:status:ready',
|
|
||||||
RENDERED: 'calendar:status:rendered',
|
|
||||||
ERROR: 'calendar:status:error',
|
|
||||||
} as const;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Implementation
|
|
||||||
|
|
||||||
### 6.1 New Files
|
|
||||||
| File | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `src/v2/CalendarApp.ts` | **Hovedentry** - pakker alt init ind |
|
|
||||||
| `src/v2/types/CommandTypes.ts` | Payload interfaces |
|
|
||||||
| `src/v2/constants/CommandEvents.ts` | Event constants |
|
|
||||||
|
|
||||||
### 6.2 Modified Files
|
|
||||||
| File | Changes |
|
|
||||||
|------|---------|
|
|
||||||
| `src/v2/demo/DemoApp.ts` | Simplificeret - bruger CalendarApp + emit events |
|
|
||||||
|
|
||||||
### 6.3 CalendarAppOptions Interface
|
|
||||||
```typescript
|
|
||||||
export interface ICalendarAppOptions {
|
|
||||||
// Grid settings
|
|
||||||
hourHeight?: number; // Default: 64
|
|
||||||
dayStartHour?: number; // Default: 6
|
|
||||||
dayEndHour?: number; // Default: 18
|
|
||||||
snapInterval?: number; // Default: 15
|
|
||||||
|
|
||||||
// Features (toggle on/off)
|
|
||||||
enableDragDrop?: boolean; // Default: true
|
|
||||||
enableResize?: boolean; // Default: true
|
|
||||||
enableHeaderDrawer?: boolean; // Default: true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.4 CalendarApp Class
|
|
||||||
```typescript
|
|
||||||
import { createV2Container } from './V2CompositionRoot';
|
|
||||||
import { CommandEvents } from './constants/CommandEvents';
|
|
||||||
import { ViewConfig } from './core/ViewConfig';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CalendarApp - Single entry point for calendar component
|
|
||||||
*
|
|
||||||
* Pakker al init ind så host-app kun skal:
|
|
||||||
* 1. CalendarApp.create(container, options)
|
|
||||||
* 2. dispatchEvent('calendar:cmd:render', { viewConfig })
|
|
||||||
*/
|
|
||||||
export class CalendarApp {
|
|
||||||
private isAnimating = false;
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
private container: HTMLElement,
|
|
||||||
private orchestrator: CalendarOrchestrator,
|
|
||||||
private animator: NavigationAnimator,
|
|
||||||
private eventBus: IEventBus
|
|
||||||
) {
|
|
||||||
this.setupCommandListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory method - async init
|
|
||||||
*/
|
|
||||||
static async create(
|
|
||||||
container: HTMLElement,
|
|
||||||
options: ICalendarAppOptions = {}
|
|
||||||
): Promise<CalendarApp> {
|
|
||||||
// Create DI container
|
|
||||||
const diContainer = createV2Container();
|
|
||||||
|
|
||||||
// Resolve dependencies
|
|
||||||
const indexedDB = diContainer.resolve<IndexedDBContext>();
|
|
||||||
const dataSeeder = diContainer.resolve<DataSeeder>();
|
|
||||||
const orchestrator = diContainer.resolve<CalendarOrchestrator>();
|
|
||||||
const eventBus = diContainer.resolve<IEventBus>();
|
|
||||||
|
|
||||||
// Initialize IndexedDB
|
|
||||||
await indexedDB.initialize();
|
|
||||||
await dataSeeder.seedIfEmpty();
|
|
||||||
|
|
||||||
// Initialize managers
|
|
||||||
const scrollManager = diContainer.resolve<ScrollManager>();
|
|
||||||
const dragDropManager = diContainer.resolve<DragDropManager>();
|
|
||||||
const edgeScrollManager = diContainer.resolve<EdgeScrollManager>();
|
|
||||||
const resizeManager = diContainer.resolve<ResizeManager>();
|
|
||||||
const headerDrawerManager = diContainer.resolve<HeaderDrawerManager>();
|
|
||||||
const timeAxisRenderer = diContainer.resolve<TimeAxisRenderer>();
|
|
||||||
|
|
||||||
scrollManager.init(container);
|
|
||||||
if (options.enableDragDrop !== false) dragDropManager.init(container);
|
|
||||||
if (options.enableResize !== false) resizeManager.init(container);
|
|
||||||
if (options.enableHeaderDrawer !== false) headerDrawerManager.init(container);
|
|
||||||
|
|
||||||
// Render time axis
|
|
||||||
const startHour = options.dayStartHour ?? 6;
|
|
||||||
const endHour = options.dayEndHour ?? 18;
|
|
||||||
const timeAxisEl = container.querySelector('#time-axis') as HTMLElement;
|
|
||||||
if (timeAxisEl) timeAxisRenderer.render(timeAxisEl, startHour, endHour);
|
|
||||||
|
|
||||||
// Edge scroll
|
|
||||||
const scrollableContent = container.querySelector('swp-scrollable-content') as HTMLElement;
|
|
||||||
if (scrollableContent) edgeScrollManager.init(scrollableContent);
|
|
||||||
|
|
||||||
// Create animator
|
|
||||||
const headerTrack = document.querySelector('swp-header-track') as HTMLElement;
|
|
||||||
const contentTrack = document.querySelector('swp-content-track') as HTMLElement;
|
|
||||||
const animator = new NavigationAnimator(headerTrack, contentTrack);
|
|
||||||
|
|
||||||
// Create app instance
|
|
||||||
const app = new CalendarApp(container, orchestrator, animator, eventBus);
|
|
||||||
|
|
||||||
// Emit ready
|
|
||||||
eventBus.emit(CommandEvents.READY, {});
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup command event listeners
|
|
||||||
*/
|
|
||||||
private setupCommandListeners(): void {
|
|
||||||
this.eventBus.on(CommandEvents.RENDER, this.handleRender);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle render command
|
|
||||||
*/
|
|
||||||
private handleRender = async (e: Event): Promise<void> => {
|
|
||||||
const { viewConfig, animation } = (e as CustomEvent).detail || {};
|
|
||||||
|
|
||||||
if (!viewConfig) {
|
|
||||||
this.eventBus.emit(CommandEvents.ERROR, {
|
|
||||||
message: 'viewConfig is required',
|
|
||||||
code: 'INVALID_PAYLOAD'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isAnimating) {
|
|
||||||
this.eventBus.emit(CommandEvents.ERROR, {
|
|
||||||
message: 'Animation in progress',
|
|
||||||
code: 'ANIMATION_IN_PROGRESS'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (animation) {
|
|
||||||
this.isAnimating = true;
|
|
||||||
await this.animator.slide(animation, async () => {
|
|
||||||
await this.orchestrator.render(viewConfig, this.container);
|
|
||||||
});
|
|
||||||
this.isAnimating = false;
|
|
||||||
} else {
|
|
||||||
await this.orchestrator.render(viewConfig, this.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.eventBus.emit(CommandEvents.RENDERED, {
|
|
||||||
templateId: viewConfig.templateId
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
this.eventBus.emit(CommandEvents.ERROR, {
|
|
||||||
message: (err as Error).message,
|
|
||||||
code: 'RENDER_FAILED'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Usage Examples
|
|
||||||
|
|
||||||
### 7.1 Minimal Host Application
|
|
||||||
```typescript
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
|
||||||
// INIT - Én gang
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
|
||||||
const container = document.querySelector('swp-calendar-container') as HTMLElement;
|
|
||||||
|
|
||||||
const calendar = await CalendarApp.create(container, {
|
|
||||||
hourHeight: 64,
|
|
||||||
dayStartHour: 6,
|
|
||||||
dayEndHour: 18
|
|
||||||
});
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
|
||||||
// RENDER - Når som helst
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
|
||||||
document.dispatchEvent(new CustomEvent('calendar:cmd:render', {
|
|
||||||
detail: {
|
|
||||||
viewConfig: {
|
|
||||||
templateId: 'simple',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'date', values: ['2025-12-08', '2025-12-09', '2025-12-10'],
|
|
||||||
idProperty: 'date', derivedFrom: 'start' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 DemoApp (Simplificeret)
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* DemoApp - Nu kun ansvarlig for:
|
|
||||||
* - State (weekOffset, currentView, selectedResources)
|
|
||||||
* - UI event handlers
|
|
||||||
* - Bygge ViewConfig
|
|
||||||
* - Emit render commands
|
|
||||||
*/
|
|
||||||
export class DemoApp {
|
|
||||||
private weekOffset = 0;
|
|
||||||
private currentView = 'simple';
|
|
||||||
private selectedResourceIds: string[] = [];
|
|
||||||
|
|
||||||
async init(): Promise<void> {
|
|
||||||
const container = document.querySelector('swp-calendar-container') as HTMLElement;
|
|
||||||
|
|
||||||
// Init kalender - ALT pakket ind
|
|
||||||
await CalendarApp.create(container, {
|
|
||||||
dayStartHour: 6,
|
|
||||||
dayEndHour: 18
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup UI
|
|
||||||
this.setupNavigation();
|
|
||||||
this.setupViewSwitching();
|
|
||||||
|
|
||||||
// Initial render
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
private render(animation?: 'left' | 'right'): void {
|
|
||||||
const viewConfig = this.buildViewConfig();
|
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent('calendar:cmd:render', {
|
|
||||||
detail: { viewConfig, animation }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupNavigation(): void {
|
|
||||||
document.getElementById('btn-prev')!.onclick = () => {
|
|
||||||
this.weekOffset--;
|
|
||||||
this.render('right');
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('btn-next')!.onclick = () => {
|
|
||||||
this.weekOffset++;
|
|
||||||
this.render('left');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupViewSwitching(): void {
|
|
||||||
document.querySelectorAll('.view-chip').forEach(chip => {
|
|
||||||
chip.addEventListener('click', () => {
|
|
||||||
this.currentView = (chip as HTMLElement).dataset.view!;
|
|
||||||
this.render(); // Ingen animation ved view change
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildViewConfig() - Uændret fra nuværende implementation
|
|
||||||
private buildViewConfig(): ViewConfig {
|
|
||||||
const dates = this.dateService.getWorkWeekDates(this.weekOffset, [1,2,3,4,5]);
|
|
||||||
|
|
||||||
switch (this.currentView) {
|
|
||||||
case 'simple':
|
|
||||||
return {
|
|
||||||
templateId: 'simple',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
// ... andre views ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 Ekstern App (uden DemoApp)
|
|
||||||
```typescript
|
|
||||||
// Enhver ekstern app kan styre kalenderen
|
|
||||||
import { CalendarApp } from '@swp/calendar';
|
|
||||||
|
|
||||||
// Init
|
|
||||||
const calendar = await CalendarApp.create(
|
|
||||||
document.getElementById('my-calendar'),
|
|
||||||
{ hourHeight: 80 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Listen for ready
|
|
||||||
document.addEventListener('calendar:status:ready', () => {
|
|
||||||
console.log('Calendar ready!');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render team view
|
|
||||||
document.dispatchEvent(new CustomEvent('calendar:cmd:render', {
|
document.dispatchEvent(new CustomEvent('calendar:cmd:render', {
|
||||||
detail: {
|
detail: {
|
||||||
viewConfig: {
|
viewConfig: {
|
||||||
templateId: 'team',
|
templateId: 'team',
|
||||||
groupings: [
|
groupings: [
|
||||||
{ type: 'team', values: ['sales', 'support'] },
|
{ type: 'team', values: ['team1', 'team2'] },
|
||||||
{ type: 'resource', values: ['john', 'jane', 'bob'],
|
{ type: 'resource', values: ['EMP001', 'EMP002'], idProperty: 'resourceId', belongsTo: 'team.resourceIds' },
|
||||||
idProperty: 'resourceId', belongsTo: 'team.members' },
|
{ type: 'date', values: ['2025-12-08', '2025-12-09'], idProperty: 'date', derivedFrom: 'start' }
|
||||||
{ type: 'date', values: ['2025-12-15', '2025-12-16'],
|
|
||||||
idProperty: 'date', derivedFrom: 'start' }
|
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
animation: 'left'
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Implementation Plan
|
## 3. Status Events (Calendar → Host)
|
||||||
|
|
||||||
| Phase | Tasks | Files |
|
| Event | Payload | Beskrivelse |
|
||||||
|-------|-------|-------|
|
|-------|---------|-------------|
|
||||||
| **1** | Create types | `src/v2/types/CommandTypes.ts` |
|
| `calendar:status:ready` | `{}` | Calendar initialiseret |
|
||||||
| **2** | Create events | `src/v2/constants/CommandEvents.ts` |
|
| `calendar:status:rendered` | `{ templateId }` | Rendering færdig |
|
||||||
| **3** | Create CalendarApp | `src/v2/CalendarApp.ts` |
|
| `calendar:status:error` | `{ message, code }` | Fejl opstået |
|
||||||
| **4** | Simplify DemoApp | `src/v2/demo/DemoApp.ts` |
|
|
||||||
| **5** | Test | Manual testing |
|
```typescript
|
||||||
|
interface IRenderedStatusPayload {
|
||||||
|
templateId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IErrorStatusPayload {
|
||||||
|
message: string;
|
||||||
|
code: 'INVALID_PAYLOAD' | 'RENDER_FAILED';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Summary
|
## 4. CalendarApp
|
||||||
|
|
||||||
### Før (nuværende DemoApp)
|
### 4.1 Initialisering
|
||||||
```typescript
|
```typescript
|
||||||
// DemoApp.init() - 100+ linjer setup
|
await CalendarApp.create(container);
|
||||||
await indexedDB.initialize();
|
|
||||||
await dataSeeder.seedIfEmpty();
|
|
||||||
scrollManager.init(container);
|
|
||||||
dragDropManager.init(container);
|
|
||||||
resizeManager.init(container);
|
|
||||||
headerDrawerManager.init(container);
|
|
||||||
timeAxisRenderer.render(...);
|
|
||||||
edgeScrollManager.init(...);
|
|
||||||
// ... og mere
|
|
||||||
|
|
||||||
// Render
|
|
||||||
await orchestrator.render(viewConfig, container);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Efter (med CalendarApp)
|
### 4.2 Dependencies (via DI)
|
||||||
```typescript
|
- CalendarOrchestrator
|
||||||
// Init - én linje
|
- SettingsService
|
||||||
await CalendarApp.create(container, { dayStartHour: 6, dayEndHour: 18 });
|
- TimeAxisRenderer
|
||||||
|
- ScrollManager
|
||||||
|
- DragDropManager
|
||||||
|
- EdgeScrollManager
|
||||||
|
- ResizeManager
|
||||||
|
- HeaderDrawerManager
|
||||||
|
- EventBus
|
||||||
|
|
||||||
// Render - command event
|
### 4.3 Ansvar
|
||||||
document.dispatchEvent(new CustomEvent('calendar:cmd:render', {
|
- Subscribe på `calendar:cmd:render`
|
||||||
detail: { viewConfig, animation: 'left' }
|
- Emit `calendar:status:ready` ved init
|
||||||
}));
|
- Emit `calendar:status:rendered` efter render
|
||||||
```
|
- Emit `calendar:status:error` ved fejl
|
||||||
|
|
||||||
### Fordele
|
---
|
||||||
| Aspekt | Før | Efter |
|
|
||||||
|--------|-----|-------|
|
## 5. Filer
|
||||||
| Init kompleksitet | 100+ linjer | 1 linje |
|
|
||||||
| Kalender viden | Host kender alle managers | Host kender kun ViewConfig |
|
| Fil | Beskrivelse |
|
||||||
| Genbrugelighed | Svær - tæt koblet | Nem - løs koblet |
|
|-----|-------------|
|
||||||
| ViewConfig | Uændret | Uændret |
|
| `src/v2/CalendarApp.ts` | Entry point |
|
||||||
| Ekstern kontrol | Ikke mulig | Via events |
|
| `src/v2/types/CommandTypes.ts` | Payload interfaces |
|
||||||
|
| `src/v2/constants/CommandEvents.ts` | Event konstanter |
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ import { DepartmentService } from './storage/departments/DepartmentService';
|
||||||
import { SettingsStore } from './storage/settings/SettingsStore';
|
import { SettingsStore } from './storage/settings/SettingsStore';
|
||||||
import { SettingsService } from './storage/settings/SettingsService';
|
import { SettingsService } from './storage/settings/SettingsService';
|
||||||
import { ITenantSettings } from './types/SettingsTypes';
|
import { ITenantSettings } from './types/SettingsTypes';
|
||||||
|
import { ViewConfigStore } from './storage/viewconfigs/ViewConfigStore';
|
||||||
|
import { ViewConfigService } from './storage/viewconfigs/ViewConfigService';
|
||||||
|
import { ViewConfig } from './core/ViewConfig';
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
import { AuditStore } from './storage/audit/AuditStore';
|
import { AuditStore } from './storage/audit/AuditStore';
|
||||||
|
|
@ -54,6 +57,7 @@ import { MockAuditRepository } from './repositories/MockAuditRepository';
|
||||||
import { MockTeamRepository } from './repositories/MockTeamRepository';
|
import { MockTeamRepository } from './repositories/MockTeamRepository';
|
||||||
import { MockDepartmentRepository } from './repositories/MockDepartmentRepository';
|
import { MockDepartmentRepository } from './repositories/MockDepartmentRepository';
|
||||||
import { MockSettingsRepository } from './repositories/MockSettingsRepository';
|
import { MockSettingsRepository } from './repositories/MockSettingsRepository';
|
||||||
|
import { MockViewConfigRepository } from './repositories/MockViewConfigRepository';
|
||||||
|
|
||||||
// Workers
|
// Workers
|
||||||
import { DataSeeder } from './workers/DataSeeder';
|
import { DataSeeder } from './workers/DataSeeder';
|
||||||
|
|
@ -118,6 +122,7 @@ export function createV2Container(): Container {
|
||||||
builder.registerType(ScheduleOverrideStore).as<IStore>();
|
builder.registerType(ScheduleOverrideStore).as<IStore>();
|
||||||
builder.registerType(AuditStore).as<IStore>();
|
builder.registerType(AuditStore).as<IStore>();
|
||||||
builder.registerType(SettingsStore).as<IStore>();
|
builder.registerType(SettingsStore).as<IStore>();
|
||||||
|
builder.registerType(ViewConfigStore).as<IStore>();
|
||||||
|
|
||||||
// Entity services (for DataSeeder polymorphic array)
|
// Entity services (for DataSeeder polymorphic array)
|
||||||
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
|
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
|
||||||
|
|
@ -148,6 +153,10 @@ export function createV2Container(): Container {
|
||||||
builder.registerType(SettingsService).as<IEntityService<ISync>>();
|
builder.registerType(SettingsService).as<IEntityService<ISync>>();
|
||||||
builder.registerType(SettingsService).as<SettingsService>();
|
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)
|
// Repositories (for DataSeeder polymorphic array)
|
||||||
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
|
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
|
||||||
builder.registerType(MockEventRepository).as<IApiRepository<ISync>>();
|
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<ITenantSettings>>();
|
||||||
builder.registerType(MockSettingsRepository).as<IApiRepository<ISync>>();
|
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)
|
// Audit service (listens to ENTITY_SAVED/DELETED events automatically)
|
||||||
builder.registerType(AuditService).as<AuditService>();
|
builder.registerType(AuditService).as<AuditService>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
|
import { ISync } from '../types/CalendarTypes';
|
||||||
|
|
||||||
export interface ViewTemplate {
|
export interface ViewTemplate {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
groupingTypes: string[];
|
groupingTypes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewConfig {
|
export interface ViewConfig extends ISync {
|
||||||
templateId: string;
|
id: string; // templateId (e.g. 'day', 'simple', 'resource')
|
||||||
groupings: GroupingConfig[];
|
groupings: GroupingConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRende
|
||||||
import { AuditService } from '../storage/audit/AuditService';
|
import { AuditService } from '../storage/audit/AuditService';
|
||||||
import { SettingsService } from '../storage/settings/SettingsService';
|
import { SettingsService } from '../storage/settings/SettingsService';
|
||||||
import { ResourceService } from '../storage/resources/ResourceService';
|
import { ResourceService } from '../storage/resources/ResourceService';
|
||||||
|
import { ViewConfigService } from '../storage/viewconfigs/ViewConfigService';
|
||||||
import { IWorkweekPreset } from '../types/SettingsTypes';
|
import { IWorkweekPreset } from '../types/SettingsTypes';
|
||||||
|
|
||||||
export class DemoApp {
|
export class DemoApp {
|
||||||
|
|
@ -23,7 +24,6 @@ export class DemoApp {
|
||||||
private weekOffset = 0;
|
private weekOffset = 0;
|
||||||
private currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple';
|
private currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple';
|
||||||
private workweekPreset: IWorkweekPreset | null = null;
|
private workweekPreset: IWorkweekPreset | null = null;
|
||||||
private selectedResourceIds: string[] = [];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private orchestrator: CalendarOrchestrator,
|
private orchestrator: CalendarOrchestrator,
|
||||||
|
|
@ -40,7 +40,8 @@ export class DemoApp {
|
||||||
private eventPersistenceManager: EventPersistenceManager,
|
private eventPersistenceManager: EventPersistenceManager,
|
||||||
private auditService: AuditService,
|
private auditService: AuditService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private resourceService: ResourceService
|
private resourceService: ResourceService,
|
||||||
|
private viewConfigService: ViewConfigService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
@ -99,74 +100,30 @@ export class DemoApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async render(): Promise<void> {
|
private async render(): Promise<void> {
|
||||||
const viewConfig = this.buildViewConfig();
|
// 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);
|
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' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupNavigation(): void {
|
private setupNavigation(): void {
|
||||||
document.getElementById('btn-prev')!.onclick = () => {
|
document.getElementById('btn-prev')!.onclick = () => {
|
||||||
this.weekOffset--;
|
this.weekOffset--;
|
||||||
|
|
@ -224,25 +181,15 @@ export class DemoApp {
|
||||||
// Clear existing
|
// Clear existing
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// Create checkboxes for each resource
|
// Display resources (read-only, values are stored in ViewConfig)
|
||||||
resources.forEach(r => {
|
resources.forEach(r => {
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.innerHTML = `
|
label.innerHTML = `
|
||||||
<input type="checkbox" value="${r.id}" checked>
|
<input type="checkbox" value="${r.id}" checked disabled>
|
||||||
${r.displayName}
|
${r.displayName}
|
||||||
`;
|
`;
|
||||||
container.appendChild(label);
|
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 {
|
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 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
|
* CalendarEventType - Used by ICalendarEvent.type
|
||||||
|
|
|
||||||
45
wwwroot/data/viewconfigs.json
Normal file
45
wwwroot/data/viewconfigs.json
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "day",
|
||||||
|
"groupings": [
|
||||||
|
{ "type": "resource", "values": ["EMP001", "EMP002"], "idProperty": "resourceId" },
|
||||||
|
{ "type": "date", "values": [], "idProperty": "date", "derivedFrom": "start", "hideHeader": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "simple",
|
||||||
|
"groupings": [
|
||||||
|
{ "type": "date", "values": [], "idProperty": "date", "derivedFrom": "start" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "resource",
|
||||||
|
"groupings": [
|
||||||
|
{ "type": "resource", "values": ["EMP001", "EMP002"], "idProperty": "resourceId" },
|
||||||
|
{ "type": "date", "values": [], "idProperty": "date", "derivedFrom": "start" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "team",
|
||||||
|
"groupings": [
|
||||||
|
{ "type": "team", "values": ["team1", "team2"] },
|
||||||
|
{ "type": "resource", "values": ["EMP001", "EMP002", "EMP003", "EMP004"], "idProperty": "resourceId", "belongsTo": "team.resourceIds" },
|
||||||
|
{ "type": "date", "values": [], "idProperty": "date", "derivedFrom": "start" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "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": [], "idProperty": "date", "derivedFrom": "start" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picker",
|
||||||
|
"groupings": [
|
||||||
|
{ "type": "resource", "values": ["EMP001", "EMP002", "EMP003", "EMP004"], "idProperty": "resourceId" },
|
||||||
|
{ "type": "date", "values": [], "idProperty": "date", "derivedFrom": "start" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue