343 lines
8.4 KiB
Markdown
343 lines
8.4 KiB
Markdown
|
|
# 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 |
|