From 0eb3bacb4150090fcd9bd02d8a294502e4660f1c Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sat, 13 Dec 2025 11:46:57 +0100 Subject: [PATCH] Introduces uniform column key concept for calendar events Refactors column identification with a new buildColumnKey method to support flexible date and resource tracking Replaces separate dateKey and resourceId handling with a unified columnKey approach Improves column rendering and event management with more consistent key generation Simplifies cross-component event tracking and column lookups --- src/v2/core/DateService.ts | 42 ++++++++++++++++++++++ src/v2/features/date/DateRenderer.ts | 7 ++++ src/v2/features/event/EventRenderer.ts | 34 ++++++------------ src/v2/managers/DragDropManager.ts | 27 ++++++-------- src/v2/managers/EventPersistenceManager.ts | 19 +++++----- src/v2/managers/ResizeManager.ts | 8 ++--- src/v2/types/CalendarTypes.ts | 6 ++-- src/v2/types/DragTypes.ts | 5 ++- src/v2/types/SwpEvent.ts | 22 ++++++------ 9 files changed, 99 insertions(+), 71 deletions(-) diff --git a/src/v2/core/DateService.ts b/src/v2/core/DateService.ts index a12236e..265e6e3 100644 --- a/src/v2/core/DateService.ts +++ b/src/v2/core/DateService.ts @@ -52,6 +52,48 @@ export class DateService { return this.formatDate(date); } + // ============================================ + // COLUMN KEY + // ============================================ + + /** + * Build a uniform columnKey from grouping segments + * Handles any combination of date, resource, team, etc. + * + * @example + * buildColumnKey({ date: '2025-12-09' }) → "2025-12-09" + * buildColumnKey({ date: '2025-12-09', resource: 'EMP001' }) → "2025-12-09:EMP001" + */ + buildColumnKey(segments: Record): string { + // Always put date first if present, then other segments alphabetically + const date = segments.date; + const others = Object.entries(segments) + .filter(([k]) => k !== 'date') + .sort(([a], [b]) => a.localeCompare(b)) + .map(([, v]) => v); + + return date ? [date, ...others].join(':') : others.join(':'); + } + + /** + * Parse a columnKey back into segments + * Assumes format: "date:resource:..." or just "date" + */ + parseColumnKey(columnKey: string): { date: string; resource?: string } { + const parts = columnKey.split(':'); + return { + date: parts[0], + resource: parts[1] + }; + } + + /** + * Extract dateKey from columnKey (first segment) + */ + getDateFromColumnKey(columnKey: string): string { + return columnKey.split(':')[0]; + } + // ============================================ // TIME CALCULATIONS // ============================================ diff --git a/src/v2/features/date/DateRenderer.ts b/src/v2/features/date/DateRenderer.ts index 9295c4d..5e6cc36 100644 --- a/src/v2/features/date/DateRenderer.ts +++ b/src/v2/features/date/DateRenderer.ts @@ -19,9 +19,15 @@ export class DateRenderer implements IRenderer { for (const dateStr of dates) { const date = this.dateService.parseISO(dateStr); + // Build columnKey for uniform identification + const segments: Record = { date: dateStr }; + if (resourceId) segments.resource = resourceId; + const columnKey = this.dateService.buildColumnKey(segments); + // Header const header = document.createElement('swp-day-header'); header.dataset.date = dateStr; + header.dataset.columnKey = columnKey; if (resourceId) { header.dataset.resourceId = resourceId; } @@ -34,6 +40,7 @@ export class DateRenderer implements IRenderer { // Column const column = document.createElement('swp-day-column'); column.dataset.date = dateStr; + column.dataset.columnKey = columnKey; if (resourceId) { column.dataset.resourceId = resourceId; } diff --git a/src/v2/features/event/EventRenderer.ts b/src/v2/features/event/EventRenderer.ts index 6d9164a..596cc71 100644 --- a/src/v2/features/event/EventRenderer.ts +++ b/src/v2/features/event/EventRenderer.ts @@ -116,22 +116,24 @@ export class EventRenderer { */ private async handleEventUpdated(payload: IEventUpdatedPayload): Promise { // Re-render source column (if different from target) - if (payload.sourceDateKey !== payload.targetDateKey || - payload.sourceResourceId !== payload.targetResourceId) { - await this.rerenderColumn(payload.sourceDateKey, payload.sourceResourceId); + if (payload.sourceColumnKey !== payload.targetColumnKey) { + await this.rerenderColumn(payload.sourceColumnKey); } // Re-render target column - await this.rerenderColumn(payload.targetDateKey, payload.targetResourceId); + await this.rerenderColumn(payload.targetColumnKey); } /** * Re-render a single column with fresh data from IndexedDB */ - private async rerenderColumn(dateKey: string, resourceId?: string): Promise { - const column = this.findColumn(dateKey, resourceId); + private async rerenderColumn(columnKey: string): Promise { + const column = this.findColumn(columnKey); if (!column) return; + // Parse dateKey and resourceId from columnKey + const { date: dateKey, resource: resourceId } = this.dateService.parseColumnKey(columnKey); + // Get date range for this day const startDate = new Date(dateKey); const endDate = new Date(dateKey); @@ -174,25 +176,11 @@ export class EventRenderer { } /** - * Find a column element by dateKey and optional resourceId + * Find a column element by columnKey */ - private findColumn(dateKey: string, resourceId?: string): HTMLElement | null { + private findColumn(columnKey: string): HTMLElement | null { if (!this.container) return null; - - const columns = this.container.querySelectorAll('swp-day-column'); - for (const col of columns) { - const colEl = col as HTMLElement; - if (colEl.dataset.date !== dateKey) continue; - - // If resourceId specified, must match - if (resourceId && colEl.dataset.resourceId !== resourceId) continue; - - // If no resourceId specified but column has one, skip (simple view case) - if (!resourceId && colEl.dataset.resourceId) continue; - - return colEl; - } - return null; + return this.container.querySelector(`swp-day-column[data-column-key="${columnKey}"]`) as HTMLElement; } /** diff --git a/src/v2/managers/DragDropManager.ts b/src/v2/managers/DragDropManager.ts index 09cfed6..451c278 100644 --- a/src/v2/managers/DragDropManager.ts +++ b/src/v2/managers/DragDropManager.ts @@ -26,8 +26,7 @@ interface DragState { targetY: number; currentY: number; animationId: number; - sourceDateKey: string; // Source column date (where drag started) - sourceResourceId?: string; // Source column resource (where drag started) + sourceColumnKey: string; // Source column key (where drag started) dragSource: 'grid' | 'header'; // Where drag originated } @@ -176,13 +175,12 @@ export class DragDropManager { ) as HTMLElement; if (gridEvent) { - const dateKey = this.dragState.currentColumn.dataset.date || ''; - const swpEvent = SwpEvent.fromElement(gridEvent, dateKey, this.gridConfig); + const columnKey = this.dragState.currentColumn.dataset.columnKey || ''; + const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, this.gridConfig); const payload: IDragEndPayload = { swpEvent, - sourceDateKey: this.dragState.sourceDateKey, - sourceResourceId: this.dragState.sourceResourceId, + sourceColumnKey: this.dragState.sourceColumnKey, target: 'grid' }; @@ -205,21 +203,20 @@ export class DragDropManager { // Remove ghost this.dragState.ghostElement?.remove(); - // Get dateKey from target column - const dateKey = this.dragState.columnElement.dataset.date || ''; + // Get columnKey from target column + const columnKey = this.dragState.columnElement.dataset.columnKey || ''; - // Create SwpEvent from element (reads top/height/eventId/resourceId from element) + // Create SwpEvent from element (reads top/height/eventId from element) const swpEvent = SwpEvent.fromElement( this.dragState.element, - dateKey, + columnKey, this.gridConfig ); // Emit drag:end const payload: IDragEndPayload = { swpEvent, - sourceDateKey: this.dragState.sourceDateKey, - sourceResourceId: this.dragState.sourceResourceId, + sourceColumnKey: this.dragState.sourceColumnKey, target: this.inHeader ? 'header' : 'grid' }; @@ -262,8 +259,7 @@ export class DragDropManager { targetY: 0, currentY: 0, animationId: 0, - sourceDateKey: '', // Will be set from header item data - sourceResourceId: undefined, + sourceColumnKey: '', // Will be set from header item data dragSource: 'header' }; @@ -323,8 +319,7 @@ export class DragDropManager { targetY: Math.max(0, targetY), currentY: startY, animationId: 0, - sourceDateKey: columnElement.dataset.date || '', - sourceResourceId: columnElement.dataset.resourceId, + sourceColumnKey: columnElement.dataset.columnKey || '', dragSource: 'grid' }; diff --git a/src/v2/managers/EventPersistenceManager.ts b/src/v2/managers/EventPersistenceManager.ts index e71f486..ae59df9 100644 --- a/src/v2/managers/EventPersistenceManager.ts +++ b/src/v2/managers/EventPersistenceManager.ts @@ -40,6 +40,9 @@ export class EventPersistenceManager { return; } + // Parse resourceId from columnKey if present + const { resource } = this.dateService.parseColumnKey(swpEvent.columnKey); + // Update and save - start/end already calculated in SwpEvent // Set allDay based on drop target: // - header: allDay = true @@ -48,7 +51,7 @@ export class EventPersistenceManager { ...event, start: swpEvent.start, end: swpEvent.end, - resourceId: swpEvent.resourceId ?? event.resourceId, + resourceId: resource ?? event.resourceId, allDay: payload.target === 'header', syncStatus: 'pending' }; @@ -56,13 +59,10 @@ export class EventPersistenceManager { await this.eventService.save(updatedEvent); // Emit EVENT_UPDATED for EventRenderer to re-render affected columns - const targetDateKey = this.dateService.getDateKey(swpEvent.start); const updatePayload: IEventUpdatedPayload = { eventId: updatedEvent.id, - sourceDateKey: payload.sourceDateKey, - sourceResourceId: payload.sourceResourceId, - targetDateKey, - targetResourceId: swpEvent.resourceId + sourceColumnKey: payload.sourceColumnKey, + targetColumnKey: swpEvent.columnKey }; this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload); }; @@ -92,13 +92,10 @@ export class EventPersistenceManager { // Emit EVENT_UPDATED for EventRenderer to re-render the column // Resize stays in same column, so source and target are the same - const dateKey = this.dateService.getDateKey(swpEvent.start); const updatePayload: IEventUpdatedPayload = { eventId: updatedEvent.id, - sourceDateKey: dateKey, - sourceResourceId: swpEvent.resourceId, - targetDateKey: dateKey, - targetResourceId: swpEvent.resourceId + sourceColumnKey: swpEvent.columnKey, + targetColumnKey: swpEvent.columnKey }; this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload); }; diff --git a/src/v2/managers/ResizeManager.ts b/src/v2/managers/ResizeManager.ts index b33d82c..02a55da 100644 --- a/src/v2/managers/ResizeManager.ts +++ b/src/v2/managers/ResizeManager.ts @@ -251,14 +251,14 @@ export class ResizeManager { // Remove global resizing class document.documentElement.classList.remove('swp--resizing'); - // Get dateKey from parent column + // Get columnKey from parent column const column = this.resizeState.element.closest('swp-day-column') as HTMLElement; - const dateKey = column?.dataset.date || ''; + const columnKey = column?.dataset.columnKey || ''; - // Create SwpEvent from element (reads top/height/eventId/resourceId from element) + // Create SwpEvent from element (reads top/height/eventId from element) const swpEvent = SwpEvent.fromElement( this.resizeState.element, - dateKey, + columnKey, this.gridConfig ); diff --git a/src/v2/types/CalendarTypes.ts b/src/v2/types/CalendarTypes.ts index 79c32ba..5c2be2f 100644 --- a/src/v2/types/CalendarTypes.ts +++ b/src/v2/types/CalendarTypes.ts @@ -94,10 +94,8 @@ export interface IEntityDeletedPayload { // Event update payload (for re-rendering columns after drag/resize) export interface IEventUpdatedPayload { eventId: string; - sourceDateKey: string; // Source column date (where event came from) - sourceResourceId?: string; // Source column resource - targetDateKey: string; // Target column date (where event landed) - targetResourceId?: string; // Target column resource + sourceColumnKey: string; // Source column key (where event came from) + targetColumnKey: string; // Target column key (where event landed) } // Resource types diff --git a/src/v2/types/DragTypes.ts b/src/v2/types/DragTypes.ts index 32f6dbd..0d720ab 100644 --- a/src/v2/types/DragTypes.ts +++ b/src/v2/types/DragTypes.ts @@ -26,9 +26,8 @@ export interface IDragMovePayload { } export interface IDragEndPayload { - swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, resourceId - sourceDateKey: string; // Source column date (where drag started) - sourceResourceId?: string; // Source column resource (where drag started) + swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, columnKey + sourceColumnKey: string; // Source column key (where drag started) target: 'grid' | 'header'; // Where the event was dropped } diff --git a/src/v2/types/SwpEvent.ts b/src/v2/types/SwpEvent.ts index 9a956ce..63b23a8 100644 --- a/src/v2/types/SwpEvent.ts +++ b/src/v2/types/SwpEvent.ts @@ -7,17 +7,20 @@ import { IGridConfig } from '../core/IGridConfig'; * for start/end times based on element position and grid config. * * Usage: - * - All data (eventId, resourceId) is read from element.dataset + * - eventId is read from element.dataset + * - columnKey identifies the column uniformly * - Position (top, height) is read from element.style * - Factory method `fromElement()` calculates Date objects */ export class SwpEvent { readonly element: HTMLElement; + readonly columnKey: string; private _start: Date; private _end: Date; - constructor(element: HTMLElement, start: Date, end: Date) { + constructor(element: HTMLElement, columnKey: string, start: Date, end: Date) { this.element = element; + this.columnKey = columnKey; this._start = start; this._end = end; } @@ -27,11 +30,6 @@ export class SwpEvent { return this.element.dataset.eventId || ''; } - /** Resource ID from element.dataset.resourceId */ - get resourceId(): string | undefined { - return this.element.dataset.resourceId; - } - get start(): Date { return this._start; } @@ -51,17 +49,21 @@ export class SwpEvent { } /** - * Factory: Create SwpEvent from element + dateKey + * Factory: Create SwpEvent from element + columnKey * Reads top/height from element.style to calculate start/end + * @param columnKey - Uniform column identifier (e.g. "2025-12-09" or "2025-12-09:EMP001") */ static fromElement( element: HTMLElement, - dateKey: string, + columnKey: string, gridConfig: IGridConfig ): SwpEvent { const topPixels = parseFloat(element.style.top) || 0; const heightPixels = parseFloat(element.style.height) || 0; + // Extract dateKey from columnKey (first segment) + const dateKey = columnKey.split(':')[0]; + // Calculate start from top position const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60; const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid; @@ -73,6 +75,6 @@ export class SwpEvent { const durationMinutes = (heightPixels / gridConfig.hourHeight) * 60; const end = new Date(start.getTime() + durationMinutes * 60 * 1000); - return new SwpEvent(element, start, end); + return new SwpEvent(element, columnKey, start, end); } }