Adds workweek settings and dynamic view configuration
Introduces settings service for managing tenant-specific calendar configurations Enables dynamic workweek presets with configurable work days Improves view switching with enhanced UI components Adds flexible calendar rendering based on tenant settings Extends DateService to support workweek date generation
This commit is contained in:
parent
58cedb9fad
commit
ad2df353b5
13 changed files with 751 additions and 38 deletions
342
docs/filter-template-spec.md
Normal file
342
docs/filter-template-spec.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# FilterTemplate & Grouping System Specification
|
||||
|
||||
> **Version:** 1.0
|
||||
> **Dato:** 2025-12-15
|
||||
> **Status:** Godkendt
|
||||
|
||||
## Formål
|
||||
|
||||
Dette dokument specificerer hvordan kalenderen matcher events til kolonner i alle view-typer (Simple, Dag, Resource, Team, Department). Formålet er at sikre konsistent opførsel og undgå fremtidige hacks.
|
||||
|
||||
---
|
||||
|
||||
## Kerneprincipper
|
||||
|
||||
### 1. Én sandhedskilde for key-format
|
||||
|
||||
**FilterTemplate** er den ENESTE kilde til key-format for event-kolonne matching.
|
||||
|
||||
```
|
||||
KORREKT: filterTemplate.buildKeyFromColumn(column)
|
||||
KORREKT: filterTemplate.buildKeyFromEvent(event)
|
||||
FORKERT: column.dataset.columnKey (bruger DateService-format)
|
||||
FORKERT: Manuel key-konstruktion med string concatenation
|
||||
```
|
||||
|
||||
### 2. Fields-rækkefølge bestemmer key-format
|
||||
|
||||
Keys bygges fra `fields` array i samme rækkefølge som defineret i ViewConfig groupings.
|
||||
|
||||
```typescript
|
||||
// ViewConfig groupings: [resource, date]
|
||||
// Resultat: "EMP001:2025-12-09"
|
||||
|
||||
// ViewConfig groupings: [date, resource]
|
||||
// Resultat: "2025-12-09:EMP001"
|
||||
```
|
||||
|
||||
### 3. Kolonner og events bruger SAMME template
|
||||
|
||||
```typescript
|
||||
// Opret template fra ViewConfig
|
||||
const filterTemplate = new FilterTemplate(dateService);
|
||||
for (const grouping of viewConfig.groupings) {
|
||||
if (grouping.idProperty) {
|
||||
filterTemplate.addField(grouping.idProperty, grouping.derivedFrom);
|
||||
}
|
||||
}
|
||||
|
||||
// Brug til BÅDE kolonner og events
|
||||
const columnKey = filterTemplate.buildKeyFromColumn(column);
|
||||
const eventKey = filterTemplate.buildKeyFromEvent(event);
|
||||
const matches = columnKey === eventKey;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Kontrakt
|
||||
|
||||
### FilterTemplate
|
||||
|
||||
```typescript
|
||||
class FilterTemplate {
|
||||
constructor(dateService: DateService, entityResolver?: IEntityResolver)
|
||||
|
||||
// Tilføj felt til template
|
||||
addField(idProperty: string, derivedFrom?: string): this
|
||||
|
||||
// Byg key fra kolonne (læser fra dataset)
|
||||
buildKeyFromColumn(column: HTMLElement): string
|
||||
|
||||
// Byg key fra event (læser fra event properties)
|
||||
buildKeyFromEvent(event: ICalendarEvent): string
|
||||
|
||||
// Convenience: matcher event mod kolonne
|
||||
matches(event: ICalendarEvent, column: HTMLElement): boolean
|
||||
}
|
||||
```
|
||||
|
||||
### Field Definition
|
||||
|
||||
| Parameter | Type | Beskrivelse |
|
||||
|-----------|------|-------------|
|
||||
| `idProperty` | `string` | Property-navn på event ELLER dot-notation |
|
||||
| `derivedFrom` | `string?` | Kilde-property hvis værdi skal udledes |
|
||||
|
||||
### Dot-Notation
|
||||
|
||||
For hierarkiske relationer bruges dot-notation:
|
||||
|
||||
```typescript
|
||||
idProperty: 'resource.teamId'
|
||||
// Betyder: event.resourceId → opslag i resource → teamId
|
||||
```
|
||||
|
||||
**Convention:** `{entityType}.{property}` → foreignKey er `{entityType}Id`
|
||||
|
||||
---
|
||||
|
||||
## Kolonne Dataset Krav
|
||||
|
||||
Kolonner (`swp-day-column`) SKAL have dataset-attributter for alle felter i template:
|
||||
|
||||
```html
|
||||
<!-- Simple view (kun date) -->
|
||||
<swp-day-column data-date="2025-12-09"></swp-day-column>
|
||||
|
||||
<!-- Resource view (resource + date) -->
|
||||
<swp-day-column
|
||||
data-date="2025-12-09"
|
||||
data-resource-id="EMP001">
|
||||
</swp-day-column>
|
||||
|
||||
<!-- Team view (team + resource + date) -->
|
||||
<swp-day-column
|
||||
data-date="2025-12-09"
|
||||
data-resource-id="EMP001"
|
||||
data-team-id="team-1">
|
||||
</swp-day-column>
|
||||
```
|
||||
|
||||
### Dataset Key Mapping
|
||||
|
||||
| idProperty | Dataset Key | Eksempel |
|
||||
|------------|-------------|----------|
|
||||
| `date` | `data-date` | `"2025-12-09"` |
|
||||
| `resourceId` | `data-resource-id` | `"EMP001"` |
|
||||
| `resource.teamId` | `data-team-id` | `"team-1"` |
|
||||
|
||||
**Regel:** Dot-notation bruger sidste segment som dataset-key.
|
||||
|
||||
---
|
||||
|
||||
## Event Property Krav
|
||||
|
||||
Events SKAL have properties der matcher template fields:
|
||||
|
||||
```typescript
|
||||
interface ICalendarEvent {
|
||||
id: string;
|
||||
start: Date; // derivedFrom: 'start' → date key
|
||||
end: Date;
|
||||
resourceId?: string; // Direkte match
|
||||
// ... andre properties
|
||||
}
|
||||
```
|
||||
|
||||
### Derived Values
|
||||
|
||||
| idProperty | derivedFrom | Transformation |
|
||||
|------------|-------------|----------------|
|
||||
| `date` | `start` | `Date → "YYYY-MM-DD"` |
|
||||
|
||||
---
|
||||
|
||||
## ViewConfig Groupings
|
||||
|
||||
### Struktur
|
||||
|
||||
```typescript
|
||||
interface GroupingConfig {
|
||||
type: string; // 'date', 'resource', 'team', 'department'
|
||||
values: string[]; // Synlige værdier
|
||||
idProperty?: string; // Felt til key-matching
|
||||
derivedFrom?: string; // Kilde hvis udledt
|
||||
belongsTo?: string; // Parent-child relation
|
||||
}
|
||||
```
|
||||
|
||||
### Eksempler
|
||||
|
||||
```typescript
|
||||
// Simple view
|
||||
groupings: [
|
||||
{ type: 'date', values: ['2025-12-09', ...], idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
|
||||
// Resource view
|
||||
groupings: [
|
||||
{ type: 'resource', values: ['EMP001', 'EMP002'], idProperty: 'resourceId' },
|
||||
{ type: 'date', values: ['2025-12-09', ...], idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
|
||||
// Team view
|
||||
groupings: [
|
||||
{ type: 'team', values: ['team-1', 'team-2'], idProperty: 'resource.teamId' },
|
||||
{ type: 'resource', values: ['EMP001', ...], idProperty: 'resourceId', belongsTo: 'team.resourceIds' },
|
||||
{ type: 'date', values: ['2025-12-09', ...], idProperty: 'date', derivedFrom: 'start' }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BelongsTo Resolution
|
||||
|
||||
### Formål
|
||||
|
||||
`belongsTo` definerer parent-child relationer for nested groupings.
|
||||
|
||||
### Syntax
|
||||
|
||||
```
|
||||
belongsTo: '{parentEntityType}.{childArrayProperty}'
|
||||
```
|
||||
|
||||
### Eksempel
|
||||
|
||||
```typescript
|
||||
// Team har resourceIds array
|
||||
{ type: 'resource', belongsTo: 'team.resourceIds' }
|
||||
|
||||
// Resolver:
|
||||
// 1. Hent team entities fra filter['team']
|
||||
// 2. For hver team, læs team.resourceIds
|
||||
// 3. Byg map: { 'team-1': ['EMP001', 'EMP002'], 'team-2': ['EMP003'] }
|
||||
```
|
||||
|
||||
### Implementering
|
||||
|
||||
```typescript
|
||||
// CalendarOrchestrator.resolveBelongsTo()
|
||||
const [entityType, property] = belongsTo.split('.');
|
||||
const service = entityServices.find(s => s.entityType === entityType);
|
||||
const entities = await service.getAll();
|
||||
// Byg parent-child map
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HeaderDrawerRenderer Regler
|
||||
|
||||
### Key Matching for AllDay Events
|
||||
|
||||
```typescript
|
||||
// KORREKT: Brug FilterTemplate
|
||||
private getVisibleColumnKeysFromDOM(): string[] {
|
||||
const columns = document.querySelectorAll('swp-day-column');
|
||||
return Array.from(columns).map(col =>
|
||||
this.filterTemplate.buildKeyFromColumn(col as HTMLElement)
|
||||
);
|
||||
}
|
||||
|
||||
// FORKERT: Læs dataset.columnKey direkte
|
||||
// (bruger DateService-format som ikke matcher FilterTemplate)
|
||||
```
|
||||
|
||||
### Layout Beregning
|
||||
|
||||
1. Hent synlige columnKeys via `getVisibleColumnKeysFromDOM()`
|
||||
2. For hver event, byg key via `filterTemplate.buildKeyFromEvent(event)`
|
||||
3. Find kolonne-index via `columnKeys.indexOf(eventKey)`
|
||||
4. Beregn row via track-algoritme
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (UNDGÅ)
|
||||
|
||||
### 1. Manuel Key Konstruktion
|
||||
|
||||
```typescript
|
||||
// FORKERT
|
||||
const key = `${resourceId}:${dateStr}`;
|
||||
|
||||
// KORREKT
|
||||
const key = filterTemplate.buildKeyFromEvent(event);
|
||||
```
|
||||
|
||||
### 2. Direkte Dataset Læsning for Matching
|
||||
|
||||
```typescript
|
||||
// FORKERT
|
||||
const columnKey = column.dataset.columnKey;
|
||||
|
||||
// KORREKT
|
||||
const columnKey = filterTemplate.buildKeyFromColumn(column);
|
||||
```
|
||||
|
||||
### 3. Hardcoded Field Order
|
||||
|
||||
```typescript
|
||||
// FORKERT
|
||||
const key = [event.resourceId, dateStr].join(':');
|
||||
|
||||
// KORREKT
|
||||
// Lad FilterTemplate håndtere rækkefølge fra ViewConfig
|
||||
```
|
||||
|
||||
### 4. Separate Key-Formater
|
||||
|
||||
```typescript
|
||||
// FORKERT: DateService til kolonner, FilterTemplate til events
|
||||
DateService.buildColumnKey(segments) // "2025-12-09:EMP001"
|
||||
FilterTemplate.buildKeyFromEvent(e) // "EMP001:2025-12-09"
|
||||
|
||||
// KORREKT: FilterTemplate til begge
|
||||
FilterTemplate.buildKeyFromColumn(col)
|
||||
FilterTemplate.buildKeyFromEvent(event)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testcases
|
||||
|
||||
### TC1: Simple View Matching
|
||||
|
||||
```
|
||||
Given: ViewConfig med [date] grouping
|
||||
When: Event har start=2025-12-09
|
||||
Then: Event matcher kolonne med data-date="2025-12-09"
|
||||
```
|
||||
|
||||
### TC2: Resource View Matching
|
||||
|
||||
```
|
||||
Given: ViewConfig med [resource, date] groupings
|
||||
When: Event har resourceId=EMP001, start=2025-12-09
|
||||
Then: Event matcher kolonne med data-resource-id="EMP001" OG data-date="2025-12-09"
|
||||
```
|
||||
|
||||
### TC3: Team View Matching
|
||||
|
||||
```
|
||||
Given: ViewConfig med [team, resource, date] groupings
|
||||
Resource EMP001 tilhører team-1
|
||||
When: Event har resourceId=EMP001, start=2025-12-09
|
||||
Then: Event matcher kolonne med data-team-id="team-1" OG data-resource-id="EMP001" OG data-date="2025-12-09"
|
||||
```
|
||||
|
||||
### TC4: Multi-Day Event
|
||||
|
||||
```
|
||||
Given: Event spænder 2025-12-09 til 2025-12-11
|
||||
When: HeaderDrawerRenderer beregner layout
|
||||
Then: Event vises fra kolonne 09 til kolonne 11 (inclusive)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ændringslog
|
||||
|
||||
| Version | Dato | Ændring |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2025-12-15 | Initial specifikation |
|
||||
Loading…
Add table
Add a link
Reference in a new issue