diff --git a/src/v2/V2CompositionRoot.ts b/src/v2/V2CompositionRoot.ts index eda37be..ed34959 100644 --- a/src/v2/V2CompositionRoot.ts +++ b/src/v2/V2CompositionRoot.ts @@ -1,5 +1,5 @@ import { Container } from '@novadi/core'; -import { IGroupingRenderer } from './core/IGroupingRenderer'; +import { Renderer } from './core/IGroupingRenderer'; import { IGroupingStore } from './core/IGroupingStore'; import { DateRenderer } from './features/date/DateRenderer'; import { DateService } from './core/DateService'; @@ -75,10 +75,10 @@ export function createV2Container(): Container { // Features builder.registerType(EventRenderer).as(); - // Renderers - registreres som IGroupingRenderer (array injection til CalendarOrchestrator) - builder.registerType(DateRenderer).as(); - builder.registerType(ResourceRenderer).as(); - builder.registerType(TeamRenderer).as(); + // Renderers - registreres som Renderer (array injection til CalendarOrchestrator) + builder.registerType(DateRenderer).as(); + builder.registerType(ResourceRenderer).as(); + builder.registerType(TeamRenderer).as(); // Stores - registreres som IGroupingStore builder.registerType(MockTeamStore).as(); diff --git a/src/v2/core/CalendarOrchestrator.ts b/src/v2/core/CalendarOrchestrator.ts index 4c4e818..fd12dcd 100644 --- a/src/v2/core/CalendarOrchestrator.ts +++ b/src/v2/core/CalendarOrchestrator.ts @@ -1,25 +1,14 @@ -import { from } from 'ts-linq-light'; -import { ViewConfig } from './ViewConfig'; -import { IGroupingRenderer, RenderContext } from './IGroupingRenderer'; -import { IGroupingStore } from './IGroupingStore'; -import { RenderBuilder } from './RenderBuilder'; +import { Renderer, RenderContext } from './IGroupingRenderer'; +import { buildPipeline } from './RenderBuilder'; import { EventRenderer } from '../features/event/EventRenderer'; +import { ViewConfig } from './ViewConfig'; export class CalendarOrchestrator { constructor( - private renderers: IGroupingRenderer[], - private stores: IGroupingStore[], + private allRenderers: Renderer[], private eventRenderer: EventRenderer ) {} - private getRenderer(type: string): IGroupingRenderer | undefined { - return from(this.renderers).firstOrDefault(r => r.type === type); - } - - private getStore(type: string): IGroupingStore | undefined { - return from(this.stores).firstOrDefault(s => s.type === type); - } - async render(viewConfig: ViewConfig, container: HTMLElement): Promise { const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement; const columnContainer = container.querySelector('swp-day-columns') as HTMLElement; @@ -27,53 +16,42 @@ export class CalendarOrchestrator { throw new Error('Missing swp-calendar-header or swp-day-columns'); } - const context: RenderContext = { headerContainer, columnContainer }; + // Byg filter fra viewConfig + const filter: Record = {}; + for (const grouping of viewConfig.groupings) { + filter[grouping.type] = grouping.values; + } - // Clear containers + const context: RenderContext = { headerContainer, columnContainer, filter }; + + // Clear headerContainer.innerHTML = ''; columnContainer.innerHTML = ''; - // Set header levels - const types = from(viewConfig.groupings).select(g => g.type).toArray(); - headerContainer.dataset.levels = types.join(' '); + // Vælg renderers baseret på groupings types + const activeRenderers = this.selectRenderers(viewConfig); - // Byg renderer chain - const builder = new RenderBuilder(context); - - 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(); + // Beregn total kolonner dynamisk + const totalColumns = this.calculateTotalColumns(viewConfig); container.style.setProperty('--grid-columns', String(totalColumns)); - builder.build(); + // Byg og kør pipeline + const pipeline = buildPipeline(activeRenderers); + pipeline.run(context); - // Render events - const visibleDates = this.extractVisibleDates(viewConfig); - await this.eventRenderer.render(container, visibleDates); + // Events + const dates = filter['date'] || []; + await this.eventRenderer.render(container, dates); } - 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 selectRenderers(viewConfig: ViewConfig): Renderer[] { + const types = viewConfig.groupings.map(g => g.type); + return this.allRenderers.filter(r => types.includes(r.type)); } - private extractVisibleDates(viewConfig: ViewConfig): string[] { - return from(viewConfig.groupings).firstOrDefault(g => g.type === 'date')?.values || []; + private calculateTotalColumns(viewConfig: ViewConfig): number { + const dateCount = viewConfig.groupings.find(g => g.type === 'date')?.values.length || 1; + const resourceCount = viewConfig.groupings.find(g => g.type === 'resource')?.values.length || 1; + return dateCount * resourceCount; } } diff --git a/src/v2/core/IGroupingRenderer.ts b/src/v2/core/IGroupingRenderer.ts index c1ec8dd..c4b9061 100644 --- a/src/v2/core/IGroupingRenderer.ts +++ b/src/v2/core/IGroupingRenderer.ts @@ -1,16 +1,11 @@ -import { IEnumerable } from 'ts-linq-light'; -import { NextFunction } from './RenderBuilder'; - export interface RenderContext { headerContainer: HTMLElement; columnContainer: HTMLElement; + filter: Record; // { team: ['alpha'], resource: ['alice', 'bob'], date: [...] } } -export interface IGroupingRenderer { +export interface Renderer { readonly type: string; - render( - items: IEnumerable, - next: NextFunction, - context: RenderContext - ): void; + next: Renderer | null; + render(context: RenderContext): void; } diff --git a/src/v2/core/RenderBuilder.ts b/src/v2/core/RenderBuilder.ts index 2162627..c1f3892 100644 --- a/src/v2/core/RenderBuilder.ts +++ b/src/v2/core/RenderBuilder.ts @@ -1,73 +1,20 @@ -import { from, IEnumerable } from 'ts-linq-light'; -import { IGroupingRenderer, RenderContext } from './IGroupingRenderer'; +import { Renderer, RenderContext } from './IGroupingRenderer'; -export interface NextFunction { - count(items: IEnumerable): number; - render(items: IEnumerable): void; +export interface Pipeline { + run(context: RenderContext): void; } -interface RenderLevel { - renderer: IGroupingRenderer; - items: IEnumerable; -} - -export class RenderBuilder { - private levels: RenderLevel[] = []; - - constructor(private context: RenderContext) {} - - add(renderer: IGroupingRenderer, items: IEnumerable): this { - this.levels.push({ renderer, items }); - return this; +export function buildPipeline(renderers: Renderer[]): Pipeline { + // Link renderers + for (let i = 0; i < renderers.length - 1; i++) { + renderers[i].next = renderers[i + 1]; } - getTotalCount(): number { - if (this.levels.length === 0) return 0; + const first = renderers[0] ?? null; - 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: () => {} - }; + return { + run(context: RenderContext) { + if (first) first.render(context); } - - 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, 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/demo/DemoApp.ts b/src/v2/demo/DemoApp.ts index 5e49265..60171b8 100644 --- a/src/v2/demo/DemoApp.ts +++ b/src/v2/demo/DemoApp.ts @@ -4,15 +4,15 @@ import { NavigationAnimator } from '../core/NavigationAnimator'; import { DateService } from '../core/DateService'; import { ScrollManager } from '../core/ScrollManager'; import { HeaderDrawerManager } from '../core/HeaderDrawerManager'; -import { ViewConfig } from '../core/ViewConfig'; import { IndexedDBContext } from '../storage/IndexedDBContext'; import { DataSeeder } from '../workers/DataSeeder'; +import { ViewConfig } from '../core/ViewConfig'; export class DemoApp { private animator!: NavigationAnimator; private container!: HTMLElement; private weekOffset = 0; - private views!: Record; + private currentView: 'simple' | 'resource' | 'team' = 'team'; constructor( private orchestrator: CalendarOrchestrator, @@ -41,27 +41,6 @@ export class DemoApp { document.querySelector('swp-content-track') as HTMLElement ); - // View configs - const dates = this.dateService.getWeekDates(); - this.views = { - simple: { templateId: 'simple', groupings: [{ type: 'date', values: dates }] }, - resource: { - templateId: 'resource', - groupings: [ - { type: 'resource', values: ['alice', 'bob', 'carol'] }, - { type: 'date', values: dates.slice(0, 3) } - ] - }, - team: { - templateId: 'team', - groupings: [ - { type: 'team', values: ['alpha', 'beta'] }, - { type: 'resource', values: ['alice', 'bob', 'carol', 'dave'], parentKey: 'teamId' }, - { type: 'date', values: dates.slice(0, 3) } - ] - } - }; - // Render time axis (06:00 - 18:00) this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement, 6, 18); @@ -73,36 +52,78 @@ export class DemoApp { // Setup event handlers this.setupNavigation(); - this.setupViewSwitchers(); this.setupDrawerToggle(); + this.setupViewSwitching(); // Initial render - this.orchestrator.render(this.views.simple, this.container); + this.render(); + } + + private async render(): Promise { + const viewConfig = this.buildViewConfig(); + await this.orchestrator.render(viewConfig, this.container); + } + + private buildViewConfig(): ViewConfig { + const dates = this.dateService.getWeekDates(this.weekOffset); + + switch (this.currentView) { + case 'simple': + return { + templateId: 'simple', + groupings: [ + { type: 'date', values: dates } + ] + }; + + case 'resource': + return { + templateId: 'resource', + groupings: [ + { type: 'resource', values: ['res1', 'res2', 'res3'] }, + { type: 'date', values: dates } + ] + }; + + case 'team': + return { + templateId: 'team', + groupings: [ + { type: 'team', values: ['team1', 'team2'] }, + { type: 'resource', values: ['res1', 'res2', 'res3'] }, + { type: 'date', values: dates } + ] + }; + } } private setupNavigation(): void { document.getElementById('btn-prev')!.onclick = () => { this.weekOffset--; - this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset); - this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container)); + this.animator.slide('right', () => this.render()); }; document.getElementById('btn-next')!.onclick = () => { this.weekOffset++; - this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset); - this.animator.slide('left', () => this.orchestrator.render(this.views.simple, this.container)); + this.animator.slide('left', () => this.render()); }; } - private setupViewSwitchers(): void { - document.getElementById('btn-simple')!.onclick = () => - this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container)); + private setupViewSwitching(): void { + document.getElementById('btn-simple')?.addEventListener('click', () => { + this.currentView = 'simple'; + this.render(); + }); - document.getElementById('btn-resource')!.onclick = () => - this.animator.slide('left', () => this.orchestrator.render(this.views.resource, this.container)); + document.getElementById('btn-resource')?.addEventListener('click', () => { + this.currentView = 'resource'; + this.render(); + }); - document.getElementById('btn-team')!.onclick = () => - this.animator.slide('left', () => this.orchestrator.render(this.views.team, this.container)); + document.getElementById('btn-team')?.addEventListener('click', () => { + this.currentView = 'team'; + this.render(); + }); } private setupDrawerToggle(): void { diff --git a/src/v2/features/date/DateRenderer.ts b/src/v2/features/date/DateRenderer.ts index 702bc69..e05141a 100644 --- a/src/v2/features/date/DateRenderer.ts +++ b/src/v2/features/date/DateRenderer.ts @@ -1,36 +1,38 @@ -import { IEnumerable } from 'ts-linq-light'; -import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer'; -import { NextFunction, RenderData } from '../../core/RenderBuilder'; +import { Renderer, RenderContext } from '../../core/IGroupingRenderer'; import { DateService } from '../../core/DateService'; -export class DateRenderer implements IGroupingRenderer { +export class DateRenderer implements Renderer { readonly type = 'date'; + next: Renderer | null = null; constructor(private dateService: DateService) {} - render( - dates: IEnumerable, - _data: RenderData, - _next: NextFunction, - context: RenderContext - ): void { - for (const dateStr of dates) { - const date = this.dateService.parseISO(dateStr); + render(context: RenderContext): void { + const dates = context.filter['date'] || []; + const resourceCount = context.filter['resource']?.length || 1; - const headerCell = document.createElement('swp-day-header'); - headerCell.dataset.date = dateStr; - headerCell.innerHTML = ` - ${this.dateService.getDayName(date, 'short')} - ${date.getDate()} - `; - context.headerContainer.appendChild(headerCell); + // Render dates for HVER resource (resourceCount gange) + for (let r = 0; r < resourceCount; r++) { + for (const dateStr of dates) { + const date = this.dateService.parseISO(dateStr); - const column = document.createElement('swp-day-column'); - column.dataset.date = dateStr; - column.innerHTML = ''; - context.columnContainer.appendChild(column); + // Header + const header = document.createElement('swp-day-header'); + header.dataset.date = dateStr; + header.innerHTML = ` + ${this.dateService.getDayName(date, 'short')} + ${date.getDate()} + `; + context.headerContainer.appendChild(header); - // Leaf renderer - ingen next.render() kald + // Column + const column = document.createElement('swp-day-column'); + column.dataset.date = dateStr; + column.innerHTML = ''; + context.columnContainer.appendChild(column); + } } + + // Leaf - ingen next } } diff --git a/src/v2/features/resource/ResourceRenderer.ts b/src/v2/features/resource/ResourceRenderer.ts index 4b72e71..3844b40 100644 --- a/src/v2/features/resource/ResourceRenderer.ts +++ b/src/v2/features/resource/ResourceRenderer.ts @@ -1,37 +1,37 @@ -import { from, IEnumerable } from 'ts-linq-light'; -import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer'; -import { NextFunction, RenderData } from '../../core/RenderBuilder'; +import { Renderer, RenderContext } from '../../core/IGroupingRenderer'; interface Resource { id: string; - name?: string; + name: string; } -export class ResourceRenderer implements IGroupingRenderer { +export class ResourceRenderer implements Renderer { readonly type = 'resource'; + next: Renderer | null = null; - render( - resources: IEnumerable, - data: RenderData, - next: NextFunction, - context: RenderContext - ): void { - const dates = data.dates || from([]); + // Hardcoded data + private resources: Resource[] = [ + { id: 'res1', name: 'Anders' }, + { id: 'res2', name: 'Bente' }, + { id: 'res3', name: 'Carsten' } + ]; - for (const resource of resources) { - const colspan = next.count(dates); + render(context: RenderContext): void { + const allowedIds = context.filter['resource'] || []; + const filteredResources = this.resources.filter(r => allowedIds.includes(r.id)); - const cell = document.createElement('swp-resource-header'); - cell.dataset.resourceId = resource.id; - cell.textContent = resource.name || resource.id; + const dateCount = context.filter['date']?.length || 1; - if (colspan > 1) { - cell.style.gridColumn = `span ${colspan}`; - } - - context.headerContainer.appendChild(cell); - - next.render(dates); + // Render ALLE resource headers + for (const resource of filteredResources) { + const header = document.createElement('swp-resource-header'); + header.dataset.resourceId = resource.id; + header.textContent = resource.name; + header.style.gridColumn = `span ${dateCount}`; + context.headerContainer.appendChild(header); } + + // Derefter kald next ÉN gang + if (this.next) this.next.render(context); } } diff --git a/src/v2/features/team/TeamRenderer.ts b/src/v2/features/team/TeamRenderer.ts index bd74039..c0b5f9f 100644 --- a/src/v2/features/team/TeamRenderer.ts +++ b/src/v2/features/team/TeamRenderer.ts @@ -1,47 +1,42 @@ -import { from, IEnumerable } from 'ts-linq-light'; -import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer'; -import { NextFunction, RenderData } from '../../core/RenderBuilder'; +import { Renderer, RenderContext } from '../../core/IGroupingRenderer'; interface Team { id: string; - name?: string; + name: string; + resourceIds: string[]; } -interface Resource { - id: string; - teamId?: string; -} - -export class TeamRenderer implements IGroupingRenderer { +export class TeamRenderer implements Renderer { readonly type = 'team'; + next: Renderer | null = null; - 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 || ''); + // Hardcoded data + private teams: Team[] = [ + { id: 'team1', name: 'Team Alpha', resourceIds: ['res1', 'res2'] }, + { id: 'team2', name: 'Team Beta', resourceIds: ['res3'] } + ]; - for (const team of teams) { - const teamResources = from(resourcesByTeam) - .firstOrDefault(g => g.key === team.id); + render(context: RenderContext): void { + const allowedIds = context.filter['team'] || []; + const filteredTeams = this.teams.filter(t => allowedIds.includes(t.id)); - const teamResourcesEnum = teamResources ? from(teamResources) : from([]); - const colspan = next.count(teamResourcesEnum); + const dateCount = context.filter['date']?.length || 1; + const resourceIds = context.filter['resource'] || []; - const cell = document.createElement('swp-team-header'); - cell.dataset.teamId = team.id; - cell.textContent = team.name || team.id; + // Render ALLE team headers først + for (const team of filteredTeams) { + // Tæl resources der tilhører dette team OG er i filter + const teamResourceCount = team.resourceIds.filter(id => resourceIds.includes(id)).length; + const colspan = teamResourceCount * dateCount; - if (colspan > 1) { - cell.style.gridColumn = `span ${colspan}`; - } - - context.headerContainer.appendChild(cell); - - next.render(teamResourcesEnum); + const header = document.createElement('swp-team-header'); + header.dataset.teamId = team.id; + header.textContent = team.name; + header.style.setProperty('--team-cols', String(colspan)); + context.headerContainer.appendChild(header); } + + // Derefter kald next ÉN gang + if (this.next) this.next.render(context); } } diff --git a/src/v2/index.ts b/src/v2/index.ts index ecd176f..5976bd8 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 { IGroupingRenderer, RenderContext } from './core/IGroupingRenderer'; +export { Renderer, 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'; +export { buildPipeline, Pipeline } from './core/RenderBuilder'; // Feature exports export { DateRenderer } from './features/date';