# Calendar V2 Architecture ## Oversigt Calendar V2 er bygget med en event-driven arkitektur og et fleksibelt grouping-system der tillader forskellige kalendervisninger (dag, uge, ressource, team). --- ## Event System ### CoreEvents Katalog Alle events er defineret i `src/v2/constants/CoreEvents.ts`. #### Drag-Drop Events | Event | Payload | Emitter | Subscribers | |-------|---------|---------|-------------| | `event:drag-start` | `IDragStartPayload` | DragDropManager | EdgeScrollManager | | `event:drag-move` | `IDragMovePayload` | DragDropManager | EventRenderer | | `event:drag-end` | `IDragEndPayload` | DragDropManager | EdgeScrollManager | | `event:drag-cancel` | `IDragCancelPayload` | DragDropManager | EdgeScrollManager | | `event:drag-column-change` | `IDragColumnChangePayload` | DragDropManager | EventRenderer | #### Resize Events | Event | Payload | Emitter | Subscribers | |-------|---------|---------|-------------| | `event:resize-start` | `IResizeStartPayload` | ResizeManager | - | | `event:resize-end` | `IResizeEndPayload` | ResizeManager | - | #### Edge Scroll Events | Event | Payload | Emitter | Subscribers | |-------|---------|---------|-------------| | `edge-scroll:tick` | `{ scrollDelta: number }` | EdgeScrollManager | DragDropManager | | `edge-scroll:started` | `{}` | EdgeScrollManager | - | | `edge-scroll:stopped` | `{}` | EdgeScrollManager | - | #### Lifecycle Events | Event | Purpose | |-------|---------| | `core:initialized` | DI container klar | | `core:ready` | Kalender fuldt initialiseret | | `core:destroyed` | Cleanup færdig | #### Data Events | Event | Purpose | |-------|---------| | `data:loading` | Data fetch startet | | `data:loaded` | Data fetch færdig | | `data:error` | Data fetch fejlet | | `entity:saved` | Entity gemt i IndexedDB | | `entity:deleted` | Entity slettet fra IndexedDB | #### View Events | Event | Purpose | |-------|---------| | `view:changed` | View/grouping ændret | | `view:rendered` | View rendering færdig | | `events:rendered` | Events renderet til DOM | | `grid:rendered` | Time grid renderet | --- ## Event Flows ### Drag-Drop Flow ``` pointerdown på event └── DragDropManager.handlePointerDown() └── Gem mouseDownPosition, capture pointer pointermove (>5px) └── DragDropManager.initializeDrag() ├── Opret ghost clone (opacity 0.3) ├── Marker original med .dragging ├── EMIT: event:drag-start │ └── EdgeScrollManager starter scrollTick loop └── Start animateDrag() RAF loop pointermove (under drag) ├── DragDropManager.updateDragTarget() │ ├── Detect kolonneændring │ │ └── EMIT: event:drag-column-change │ │ └── EventRenderer flytter element til ny kolonne │ └── Beregn targetY for smooth interpolation │ └── DragDropManager.animateDrag() (RAF) ├── Interpoler currentY mod targetY (factor 0.3) ├── Opdater element.style.top └── EMIT: event:drag-move └── EventRenderer.updateDragTimestamp() └── Beregn snapped tid og opdater visning EdgeScrollManager.scrollTick() (parallel RAF) ├── Beregn velocity baseret på museafstand til kanter │ - Inner zone (0-50px): 640 px/sek │ - Outer zone (50-100px): 140 px/sek ├── Scroll viewport: scrollTop += scrollDelta └── EMIT: edge-scroll:tick └── DragDropManager kompenserer element position pointerup └── DragDropManager.handlePointerUp() ├── Snap currentY til grid (15-min intervaller) ├── Fjern ghost element ├── EMIT: event:drag-end │ └── EdgeScrollManager stopper scroll └── Persist ændringer til IndexedDB ``` --- ## Grouping System ### ViewConfig ```typescript interface ViewConfig { templateId: string; // 'day' | 'simple' | 'resource' | 'team' groupings: GroupingConfig[]; } interface GroupingConfig { type: string; // 'date' | 'resource' | 'team' values: string[]; // IDs der skal vises } ``` ### Sådan bestemmer groupings strukturen 1. **Kolonneantal**: Produkt af alle grouping dimensioner - Eksempel: `5 datoer × 2 ressourcer = 10 kolonner` 2. **Header layout**: CSS grid bruger `data-levels` til at stakke headers - `data-levels="date"` → 1 header række - `data-levels="resource date"` → 2 header rækker - `data-levels="team resource date"` → 3 header rækker 3. **Renderer selektion**: Grouping types matches til tilgængelige renderers ### Konfigurationseksempler #### Simple Date View (3 dage, ingen ressourcer) ```typescript { templateId: 'simple', groupings: [ { type: 'date', values: ['2024-01-15', '2024-01-16', '2024-01-17'] } ] } ``` **Resultat:** 3 kolonner, 1 header række ``` ┌─── Date1 ───┬─── Date2 ───┬─── Date3 ───┐ │ │ │ │ ``` #### Resource View (2 ressourcer, 3 dage) ```typescript { templateId: 'resource', groupings: [ { type: 'resource', values: ['EMP001', 'EMP002'] }, { type: 'date', values: ['2024-01-15', '2024-01-16', '2024-01-17'] } ] } ``` **Resultat:** 6 kolonner, 2 header rækker ``` ┌───── Resource1 (span 3) ─────┬───── Resource2 (span 3) ─────┐ ├─── D1 ───┬─── D2 ───┬─── D3 ─┼─── D1 ───┬─── D2 ───┬─── D3 ─┤ │ │ │ │ │ │ │ ``` #### Team View (2 teams, 2 ressourcer, 3 dage) ```typescript { templateId: 'team', groupings: [ { type: 'team', values: ['team1', 'team2'] }, { type: 'resource', values: ['res1', 'res2'] }, { type: 'date', values: ['2024-01-15', '2024-01-16', '2024-01-17'] } ] } ``` **Resultat:** 12 kolonner, 3 header rækker ``` ┌─────────── Team1 (span 6) ───────────┬─────────── Team2 (span 6) ───────────┐ ├───── Res1 (span 3) ──┬── Res2 (3) ───┼───── Res1 (span 3) ──┬── Res2 (3) ───┤ ├── D1 ─┬── D2 ─┬── D3 ┼── D1 ─┬── D2 ─┼── D1 ─┬── D2 ─┬── D3 ┼── D1 ─┬── D2 ─┤ │ │ │ │ │ │ │ │ │ │ │ ``` --- ## Header Rendering ### data-levels Attribut `swp-calendar-header` modtager `data-levels` som bestemmer header row layout: ```typescript // I CalendarOrchestrator.render() const levels = viewConfig.groupings.map(g => g.type).join(' '); headerContainer.dataset.levels = levels; // "resource date" eller "team resource date" ``` ### CSS Grid Styling ```css swp-calendar-header[data-levels="date"] > swp-day-header { grid-row: 1; } swp-calendar-header[data-levels="resource date"] { > swp-resource-header { grid-row: 1; } > swp-day-header { grid-row: 2; } } swp-calendar-header[data-levels="team resource date"] { > swp-team-header { grid-row: 1; } > swp-resource-header { grid-row: 2; } > swp-day-header { grid-row: 3; } } ``` ### Rendering Pipeline 1. Renderers eksekveres i den rækkefølge de står i `viewConfig.groupings` 2. Hver renderer APPENDER sine headers til `headerContainer` 3. CSS bruger `data-levels` + element type til at positionere i korrekt række --- ## Renderers ### DateRenderer ```typescript class DateRenderer implements IRenderer { readonly type = 'date'; render(context: IRenderContext): void { // For HVER ressource (eller én gang hvis ingen): // For HVER dato i filter['date']: // Opret swp-day-header + swp-day-column // Sæt dataset.date & dataset.resourceId } } ``` ### ResourceRenderer ```typescript class ResourceRenderer implements IRenderer { readonly type = 'resource'; async render(context: IRenderContext): Promise { // Load IResource[] fra ResourceService // For HVER ressource: // Opret swp-resource-header // Sæt grid-column: span ${dateCount} } } ``` ### TeamRenderer ```typescript class TeamRenderer implements IRenderer { readonly type = 'team'; render(context: IRenderContext): void { // For HVERT team: // Tæl ressourcer der tilhører team // Opret swp-team-header // Sæt grid-column: span ${colspan} } } ``` --- ## Grid Konfiguration ### IGridConfig ```typescript interface IGridConfig { hourHeight: number; // pixels per time (fx 60) dayStartHour: number; // fx 6 (06:00) dayEndHour: number; // fx 18 (18:00) snapInterval: number; // minutter (fx 15) } ``` ### Position Beregning ```typescript function calculateEventPosition(start: Date, end: Date, config: IGridConfig): EventPosition { const startMinutes = start.getHours() * 60 + start.getMinutes(); const endMinutes = end.getHours() * 60 + end.getMinutes(); const dayStartMinutes = config.dayStartHour * 60; const minuteHeight = config.hourHeight / 60; return { top: (startMinutes - dayStartMinutes) * minuteHeight, height: (endMinutes - startMinutes) * minuteHeight }; } ``` **Eksempel:** Event 09:00-10:00 med dayStartHour=6, hourHeight=60 - startMinutes = 540, dayStartMinutes = 360 - top = (540 - 360) × 1 = **180px** - height = (600 - 540) × 1 = **60px** --- ## Z-Index Stack ``` z-index: 10 ← Events (interaktive, draggable) z-index: 5 ← Unavailable zones (visuel, pointer-events: none) z-index: 2 ← Time linjer (baggrund) z-index: 1 ← Kvarter linjer (baggrund) z-index: 0 ← Grid baggrund ``` --- ## Komponenter | Komponent | Ansvar | Fil | |-----------|--------|-----| | **CalendarOrchestrator** | Orkestrerer renderer pipeline | `core/CalendarOrchestrator.ts` | | **DateRenderer** | Opretter dag-headers og kolonner | `features/date/DateRenderer.ts` | | **ResourceRenderer** | Opretter ressource-headers | `features/resource/ResourceRenderer.ts` | | **TeamRenderer** | Opretter team-headers | `features/team/TeamRenderer.ts` | | **EventRenderer** | Renderer events med positioner | `features/event/EventRenderer.ts` | | **ScheduleRenderer** | Renderer unavailable zoner | `features/schedule/ScheduleRenderer.ts` | | **DragDropManager** | Håndterer drag-drop | `managers/DragDropManager.ts` | | **ResizeManager** | Håndterer event resize | `managers/ResizeManager.ts` | | **EdgeScrollManager** | Auto-scroll ved kanter | `managers/EdgeScrollManager.ts` | | **ScrollManager** | Synkroniserer scroll | `core/ScrollManager.ts` | | **EventBus** | Central event dispatcher | `core/EventBus.ts` | | **DateService** | Dato formatering og beregning | `core/DateService.ts` | --- ## Filer ``` src/v2/ ├── constants/ │ └── CoreEvents.ts # Alle event konstanter ├── core/ │ ├── CalendarOrchestrator.ts │ ├── DateService.ts │ ├── EventBus.ts │ ├── IGridConfig.ts │ ├── IGroupingRenderer.ts │ ├── RenderBuilder.ts │ ├── ScrollManager.ts │ └── ViewConfig.ts ├── features/ │ ├── date/ │ │ └── DateRenderer.ts │ ├── event/ │ │ └── EventRenderer.ts │ ├── resource/ │ │ └── ResourceRenderer.ts │ ├── schedule/ │ │ └── ScheduleRenderer.ts │ └── team/ │ └── TeamRenderer.ts ├── managers/ │ ├── DragDropManager.ts │ └── EdgeScrollManager.ts ├── storage/ │ ├── BaseEntityService.ts │ ├── IndexedDBContext.ts │ └── events/ │ ├── EventService.ts │ └── EventStore.ts ├── types/ │ ├── CalendarTypes.ts │ ├── DragTypes.ts │ └── ScheduleTypes.ts └── utils/ └── PositionUtils.ts ```