409 lines
12 KiB
Markdown
409 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
|
|||
|
|
```
|