2025-12-08 22:26:38 +01:00
|
|
|
import { from } from 'ts-linq-light';
|
2025-12-06 01:22:04 +01:00
|
|
|
import { ViewConfig, GroupingConfig } from './ViewConfig';
|
|
|
|
|
import { RenderContext } from './RenderContext';
|
2025-12-08 20:05:32 +01:00
|
|
|
import { IGroupingRenderer } from './IGroupingRenderer';
|
2025-12-07 14:31:16 +01:00
|
|
|
import { IGroupingStore } from './IGroupingStore';
|
2025-12-08 20:05:32 +01:00
|
|
|
import { EventRenderer } from '../features/event/EventRenderer';
|
2025-12-06 01:22:04 +01:00
|
|
|
|
|
|
|
|
interface HierarchyNode {
|
|
|
|
|
type: string;
|
|
|
|
|
id: string;
|
|
|
|
|
data: unknown;
|
|
|
|
|
parentId?: string;
|
|
|
|
|
children: HierarchyNode[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface GroupingData {
|
|
|
|
|
items: { id: string; data: unknown }[];
|
|
|
|
|
byParent: Map<string, { id: string; data: unknown }[]> | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class CalendarOrchestrator {
|
|
|
|
|
constructor(
|
2025-12-08 20:05:32 +01:00
|
|
|
private renderers: IGroupingRenderer[],
|
|
|
|
|
private stores: IGroupingStore[],
|
|
|
|
|
private eventRenderer: EventRenderer
|
2025-12-06 01:22:04 +01:00
|
|
|
) {}
|
|
|
|
|
|
2025-12-08 20:05:32 +01:00
|
|
|
private getRenderer(type: string): IGroupingRenderer | undefined {
|
2025-12-08 22:26:38 +01:00
|
|
|
return from(this.renderers).firstOrDefault(r => r.type === type);
|
2025-12-08 20:05:32 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:31:16 +01:00
|
|
|
private getStore(type: string): IGroupingStore | undefined {
|
2025-12-08 22:26:38 +01:00
|
|
|
return from(this.stores).firstOrDefault(s => s.type === type);
|
2025-12-07 14:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 01:22:04 +01:00
|
|
|
async render(viewConfig: ViewConfig, container: HTMLElement): Promise<void> {
|
|
|
|
|
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
|
|
|
|
|
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
|
|
|
|
|
if (!headerContainer || !columnContainer) {
|
|
|
|
|
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));
|
|
|
|
|
|
2025-12-08 22:26:38 +01:00
|
|
|
const types = from(viewConfig.groupings).select(g => g.type).toArray();
|
2025-12-06 01:22:04 +01:00
|
|
|
headerContainer.dataset.levels = types.join(' ');
|
|
|
|
|
|
|
|
|
|
headerContainer.innerHTML = '';
|
|
|
|
|
columnContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
this.renderHierarchy(hierarchy, headerContainer, columnContainer);
|
|
|
|
|
|
2025-12-08 20:05:32 +01:00
|
|
|
// Render events from IndexedDB
|
|
|
|
|
const visibleDates = this.extractVisibleDates(viewConfig);
|
|
|
|
|
await this.eventRenderer.render(container, visibleDates);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extractVisibleDates(viewConfig: ViewConfig): string[] {
|
2025-12-08 22:26:38 +01:00
|
|
|
return from(viewConfig.groupings).firstOrDefault(g => g.type === 'date')?.values || [];
|
2025-12-06 01:22:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fetchAllData(groupings: GroupingConfig[]): Map<string, GroupingData> {
|
|
|
|
|
const result = new Map<string, GroupingData>();
|
|
|
|
|
|
|
|
|
|
for (const g of groupings) {
|
|
|
|
|
if (g.type === 'date') {
|
|
|
|
|
result.set(g.type, {
|
2025-12-08 22:26:38 +01:00
|
|
|
items: from(g.values).select(v => ({ id: v, data: { id: v } })).toArray(),
|
2025-12-06 01:22:04 +01:00
|
|
|
byParent: null
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:31:16 +01:00
|
|
|
const store = this.getStore(g.type);
|
|
|
|
|
if (!store) continue;
|
|
|
|
|
const rawItems = store.getByIds(g.values);
|
2025-12-08 22:26:38 +01:00
|
|
|
const items = from(rawItems).select((item: any) => ({ id: item.id, data: item })).toArray();
|
|
|
|
|
let byParent: Map<string, { id: string; data: unknown }[]> | 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());
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-06 01:22:04 +01:00
|
|
|
|
|
|
|
|
result.set(g.type, { items, byParent });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildHierarchy(
|
|
|
|
|
groupings: GroupingConfig[],
|
|
|
|
|
data: Map<string, GroupingData>,
|
|
|
|
|
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;
|
|
|
|
|
|
2025-12-08 22:26:38 +01:00
|
|
|
return from(items).select(item => ({
|
2025-12-06 01:22:04 +01:00
|
|
|
type: g.type,
|
|
|
|
|
id: item.id,
|
|
|
|
|
data: item.data,
|
|
|
|
|
parentId,
|
|
|
|
|
children: this.buildHierarchy(groupings, data, level + 1, item.id)
|
2025-12-08 22:26:38 +01:00
|
|
|
})).toArray();
|
2025-12-06 01:22:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-12-08 20:05:32 +01:00
|
|
|
const renderer = this.getRenderer(node.type);
|
2025-12-06 01:22:04 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|