diff --git a/package-lock.json b/package-lock.json index 9de71a8..3b1a7b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4960,9 +4960,9 @@ } }, "node_modules/ts-linq-light": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ts-linq-light/-/ts-linq-light-1.0.0.tgz", - "integrity": "sha512-8GGyHkHlKuKFTbT/Xz/0y72OTKl1hVRMJ0MqXAaWaergCExKw3bd5M6DR3NSOA7UL0gkTMhWsql9kFYyAjkIdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-linq-light/-/ts-linq-light-1.0.1.tgz", + "integrity": "sha512-Qk1TKZ8M/XYH6Vt+zUOtAyVOqezIMd3r7EDtgCPOnWgIs0Xdrj/miqUQAEoRl3LttbQQ/6gBMhM/84S/mTb/sg==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/src/v2/core/CalendarOrchestrator.ts b/src/v2/core/CalendarOrchestrator.ts index 78964a7..b3ac610 100644 --- a/src/v2/core/CalendarOrchestrator.ts +++ b/src/v2/core/CalendarOrchestrator.ts @@ -1,23 +1,10 @@ import { from } from 'ts-linq-light'; -import { ViewConfig, GroupingConfig } from './ViewConfig'; -import { RenderContext } from './RenderContext'; -import { IGroupingRenderer } from './IGroupingRenderer'; +import { ViewConfig } from './ViewConfig'; +import { IGroupingRenderer, RenderContext } from './IGroupingRenderer'; import { IGroupingStore } from './IGroupingStore'; +import { RenderBuilder, RenderData } from './RenderBuilder'; import { EventRenderer } from '../features/event/EventRenderer'; -interface HierarchyNode { - type: string; - id: string; - data: unknown; - parentId?: string; - children: HierarchyNode[]; -} - -interface GroupingData { - items: { id: string; data: unknown }[]; - byParent: Map | null; -} - export class CalendarOrchestrator { constructor( private renderers: IGroupingRenderer[], @@ -40,115 +27,60 @@ export class CalendarOrchestrator { throw new Error('Missing swp-calendar-header or swp-day-columns'); } - const groupingData = this.fetchAllData(viewConfig.groupings); - const hierarchy = this.buildHierarchy(viewConfig.groupings, groupingData); - const totalColumns = this.countLeaves(hierarchy); - - container.style.setProperty('--grid-columns', String(totalColumns)); - - const types = from(viewConfig.groupings).select(g => g.type).toArray(); - headerContainer.dataset.levels = types.join(' '); + const context: RenderContext = { headerContainer, columnContainer }; + // Clear containers headerContainer.innerHTML = ''; columnContainer.innerHTML = ''; - this.renderHierarchy(hierarchy, headerContainer, columnContainer); + // Set header levels + const types = from(viewConfig.groupings).select(g => g.type).toArray(); + headerContainer.dataset.levels = types.join(' '); - // Render events from IndexedDB + // Hent alt data + const data: RenderData = { + teams: this.getItems('team', viewConfig), + resources: this.getItems('resource', viewConfig), + dates: this.getItems('date', viewConfig) + }; + + // Byg renderer chain + const builder = new RenderBuilder(context, data); + + for (const grouping of viewConfig.groupings) { + const renderer = this.getRenderer(grouping.type); + if (renderer) { + const items = this.getItems(grouping.type, viewConfig); + builder.add(renderer, items); + } + } + + // Beregn total columns og render + const totalColumns = builder.getTotalCount(); + container.style.setProperty('--grid-columns', String(totalColumns)); + + builder.build(); + + // Render events const visibleDates = this.extractVisibleDates(viewConfig); await this.eventRenderer.render(container, visibleDates); } + private getItems(type: string, viewConfig: ViewConfig): ReturnType { + const grouping = from(viewConfig.groupings).firstOrDefault(g => g.type === type); + if (!grouping) return from([]); + + if (type === 'date') { + return from(grouping.values); + } + + const store = this.getStore(type); + if (!store) return from([]); + + return from(store.getByIds(grouping.values)); + } + private extractVisibleDates(viewConfig: ViewConfig): string[] { return from(viewConfig.groupings).firstOrDefault(g => g.type === 'date')?.values || []; } - - private fetchAllData(groupings: GroupingConfig[]): Map { - const result = new Map(); - - for (const g of groupings) { - if (g.type === 'date') { - result.set(g.type, { - items: from(g.values).select(v => ({ id: v, data: { id: v } })).toArray(), - byParent: null - }); - continue; - } - - const store = this.getStore(g.type); - if (!store) continue; - const rawItems = store.getByIds(g.values); - const items = from(rawItems).select((item: any) => ({ id: item.id, data: item })).toArray(); - let byParent: Map | null = null; - if (g.parentKey) { - byParent = new Map(); - const grouped = from(items) - .where(item => (item.data as any)[g.parentKey!] != null) - .groupBy(item => (item.data as any)[g.parentKey!] as string); - for (const group of grouped) { - byParent.set(group.key, group.toArray()); - } - } - - result.set(g.type, { items, byParent }); - } - - return result; - } - - private buildHierarchy( - groupings: GroupingConfig[], - data: Map, - level = 0, - parentId?: string - ): HierarchyNode[] { - if (level >= groupings.length) return []; - - const g = groupings[level]; - const gData = data.get(g.type); - if (!gData) return []; - - const items = parentId && gData.byParent - ? gData.byParent.get(parentId) ?? [] - : gData.items; - - return from(items).select(item => ({ - type: g.type, - id: item.id, - data: item.data, - parentId, - children: this.buildHierarchy(groupings, data, level + 1, item.id) - })).toArray(); - } - - private countLeaves(nodes: HierarchyNode[]): number { - return nodes.reduce((sum, n) => - sum + (n.children.length ? this.countLeaves(n.children) : 1), 0); - } - - private renderHierarchy( - nodes: HierarchyNode[], - headerContainer: HTMLElement, - columnContainer: HTMLElement, - headerRow = 1 - ): void { - for (const node of nodes) { - const renderer = this.getRenderer(node.type); - const colspan = this.countLeaves([node]) || 1; - - renderer?.render({ - headerContainer, - columnContainer, - values: [node.id], - headerRow, - columnIndex: 0, // Not used - grid auto-places - colspan, - parentId: node.parentId - } as RenderContext); - - if (node.children.length) { - this.renderHierarchy(node.children, headerContainer, columnContainer, headerRow + 1); - } - } - } } diff --git a/src/v2/core/IGroupingRenderer.ts b/src/v2/core/IGroupingRenderer.ts index 392e381..39a784a 100644 --- a/src/v2/core/IGroupingRenderer.ts +++ b/src/v2/core/IGroupingRenderer.ts @@ -1,6 +1,17 @@ -import { RenderContext } from './RenderContext'; +import { IEnumerable } from 'ts-linq-light'; +import { NextFunction, RenderData } from './RenderBuilder'; -export interface IGroupingRenderer { - readonly type: string; - render(context: RenderContext): void; +export interface RenderContext { + headerContainer: HTMLElement; + columnContainer: HTMLElement; +} + +export interface IGroupingRenderer { + readonly type: string; + render( + items: IEnumerable, + data: RenderData, + next: NextFunction, + context: RenderContext + ): void; } diff --git a/src/v2/core/RenderBuilder.ts b/src/v2/core/RenderBuilder.ts new file mode 100644 index 0000000..e847547 --- /dev/null +++ b/src/v2/core/RenderBuilder.ts @@ -0,0 +1,82 @@ +import { from, IEnumerable } from 'ts-linq-light'; +import { IGroupingRenderer, RenderContext } from './IGroupingRenderer'; + +export interface NextFunction { + count(items: IEnumerable): number; + render(items: IEnumerable): void; +} + +export interface RenderData { + teams?: IEnumerable; + resources?: IEnumerable; + dates?: IEnumerable; +} + +interface RenderLevel { + renderer: IGroupingRenderer; + items: IEnumerable; +} + +export class RenderBuilder { + private levels: RenderLevel[] = []; + + constructor( + private context: RenderContext, + private data: RenderData + ) {} + + add(renderer: IGroupingRenderer, items: IEnumerable): this { + this.levels.push({ renderer, items }); + return this; + } + + getTotalCount(): number { + if (this.levels.length === 0) return 0; + + const chain = this.buildChain(0); + return chain.count(this.levels[0].items); + } + + build(): void { + if (this.levels.length === 0) return; + + const chain = this.buildChain(0); + chain.render(this.levels[0].items); + } + + private buildChain(index: number): NextFunction { + if (index >= this.levels.length) { + // Leaf - ingen flere levels + return { + count: (items) => from(items).count(), + render: () => {} + }; + } + + const level = this.levels[index]; + const nextChain = this.buildChain(index + 1); + + return { + count: (items) => { + let total = 0; + for (const item of items) { + const childItems = this.getChildItems(index, item); + total += nextChain.count(childItems); + } + return total || from(items).count(); + }, + render: (items) => { + level.renderer.render(items, this.data, nextChain, this.context); + } + }; + } + + private getChildItems(levelIndex: number, _parentItem: unknown): IEnumerable { + // Returnerer næste levels items - rendereren selv filtrerer baseret på parent + const nextLevel = this.levels[levelIndex + 1]; + if (!nextLevel) { + return from([]); + } + return nextLevel.items; + } +} diff --git a/src/v2/core/RenderContext.ts b/src/v2/core/RenderContext.ts deleted file mode 100644 index 841789e..0000000 --- a/src/v2/core/RenderContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface RenderContext { - headerContainer: HTMLElement; - columnContainer: HTMLElement; - values: string[]; - headerRow: number; - colspan: number; - parentId?: string; - columnIndex: number; // Kept for interface compatibility, but not used by renderers -} diff --git a/src/v2/features/date/DateRenderer.ts b/src/v2/features/date/DateRenderer.ts index 667ab4a..702bc69 100644 --- a/src/v2/features/date/DateRenderer.ts +++ b/src/v2/features/date/DateRenderer.ts @@ -1,14 +1,20 @@ -import { IGroupingRenderer } from '../../core/IGroupingRenderer'; -import { RenderContext } from '../../core/RenderContext'; +import { IEnumerable } from 'ts-linq-light'; +import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer'; +import { NextFunction, RenderData } from '../../core/RenderBuilder'; import { DateService } from '../../core/DateService'; -export class DateRenderer implements IGroupingRenderer { +export class DateRenderer implements IGroupingRenderer { readonly type = 'date'; constructor(private dateService: DateService) {} - render(context: RenderContext): void { - for (const dateStr of context.values) { + render( + dates: IEnumerable, + _data: RenderData, + _next: NextFunction, + context: RenderContext + ): void { + for (const dateStr of dates) { const date = this.dateService.parseISO(dateStr); const headerCell = document.createElement('swp-day-header'); @@ -21,9 +27,10 @@ export class DateRenderer implements IGroupingRenderer { const column = document.createElement('swp-day-column'); column.dataset.date = dateStr; - if (context.parentId) column.dataset.parentId = context.parentId; column.innerHTML = ''; context.columnContainer.appendChild(column); + + // Leaf renderer - ingen next.render() kald } } } diff --git a/src/v2/features/resource/ResourceRenderer.ts b/src/v2/features/resource/ResourceRenderer.ts index b7b9fb4..4b72e71 100644 --- a/src/v2/features/resource/ResourceRenderer.ts +++ b/src/v2/features/resource/ResourceRenderer.ts @@ -1,16 +1,37 @@ -import { IGroupingRenderer } from '../../core/IGroupingRenderer'; -import { RenderContext } from '../../core/RenderContext'; +import { from, IEnumerable } from 'ts-linq-light'; +import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer'; +import { NextFunction, RenderData } from '../../core/RenderBuilder'; -export class ResourceRenderer implements IGroupingRenderer { +interface Resource { + id: string; + name?: string; +} + +export class ResourceRenderer implements IGroupingRenderer { readonly type = 'resource'; - render(context: RenderContext): void { - for (const resourceId of context.values) { + render( + resources: IEnumerable, + data: RenderData, + next: NextFunction, + context: RenderContext + ): void { + const dates = data.dates || from([]); + + for (const resource of resources) { + const colspan = next.count(dates); + const cell = document.createElement('swp-resource-header'); - cell.dataset.resourceId = resourceId; - cell.textContent = resourceId; - if (context.colspan > 1) cell.style.gridColumn = `span ${context.colspan}`; + cell.dataset.resourceId = resource.id; + cell.textContent = resource.name || resource.id; + + if (colspan > 1) { + cell.style.gridColumn = `span ${colspan}`; + } + context.headerContainer.appendChild(cell); + + next.render(dates); } } } diff --git a/src/v2/features/team/TeamRenderer.ts b/src/v2/features/team/TeamRenderer.ts index c1e1d8e..bd74039 100644 --- a/src/v2/features/team/TeamRenderer.ts +++ b/src/v2/features/team/TeamRenderer.ts @@ -1,16 +1,47 @@ -import { IGroupingRenderer } from '../../core/IGroupingRenderer'; -import { RenderContext } from '../../core/RenderContext'; +import { from, IEnumerable } from 'ts-linq-light'; +import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer'; +import { NextFunction, RenderData } from '../../core/RenderBuilder'; -export class TeamRenderer implements IGroupingRenderer { +interface Team { + id: string; + name?: string; +} + +interface Resource { + id: string; + teamId?: string; +} + +export class TeamRenderer implements IGroupingRenderer { readonly type = 'team'; - render(context: RenderContext): void { - for (const teamId of context.values) { + render( + teams: IEnumerable, + data: RenderData, + next: NextFunction, + context: RenderContext + ): void { + const resources = data.resources as IEnumerable || from([]); + const resourcesByTeam = from(resources).groupBy((r: Resource) => r.teamId || ''); + + for (const team of teams) { + const teamResources = from(resourcesByTeam) + .firstOrDefault(g => g.key === team.id); + + const teamResourcesEnum = teamResources ? from(teamResources) : from([]); + const colspan = next.count(teamResourcesEnum); + const cell = document.createElement('swp-team-header'); - cell.dataset.teamId = teamId; - cell.textContent = teamId; - if (context.colspan > 1) cell.style.gridColumn = `span ${context.colspan}`; + cell.dataset.teamId = team.id; + cell.textContent = team.name || team.id; + + if (colspan > 1) { + cell.style.gridColumn = `span ${colspan}`; + } + context.headerContainer.appendChild(cell); + + next.render(teamResourcesEnum); } } } diff --git a/src/v2/index.ts b/src/v2/index.ts index 7ddcc28..ecd176f 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,10 +1,10 @@ // Core exports export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig'; -export { RenderContext } from './core/RenderContext'; -export { IGroupingRenderer } from './core/IGroupingRenderer'; +export { IGroupingRenderer, RenderContext } from './core/IGroupingRenderer'; export { IGroupingStore } from './core/IGroupingStore'; export { CalendarOrchestrator } from './core/CalendarOrchestrator'; export { NavigationAnimator } from './core/NavigationAnimator'; +export { RenderBuilder, NextFunction, RenderData } from './core/RenderBuilder'; // Feature exports export { DateRenderer } from './features/date';