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
408 lines
12 KiB
Markdown
408 lines
12 KiB
Markdown
# 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<void> {
|
||
// 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
|
||
```
|