Calendar/docs/V2-ARCHITECTURE.md
Janus C. H. Knudsen 6723658fd9 Adds header drawer and event drag interactions
Introduces HeaderDrawerRenderer and HeaderDrawerLayoutEngine to support dragging events into an all-day header drawer

Enables dynamic event placement and conversion between timed and all-day events through new drag interactions
Implements flexible layout calculation for header items with column and row management

Extends DragDropManager to handle header zone interactions
Adds new event types for header drag events
2025-12-10 23:11:11 +01:00

12 KiB
Raw Permalink Blame History

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

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)

{
  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)

{
  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)

{
  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:

// 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

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

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

class ResourceRenderer implements IRenderer {
  readonly type = 'resource';

  async render(context: IRenderContext): Promise<void> {
    // Load IResource[] fra ResourceService
    // For HVER ressource:
    //   Opret swp-resource-header
    //   Sæt grid-column: span ${dateCount}
  }
}

TeamRenderer

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

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

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