From c2f7564f8e746e6c6c463d9c7b7d71a3ec40ba68 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sat, 13 Dec 2025 12:52:27 +0100 Subject: [PATCH] Refactor column key handling and event positioning Introduces more robust column key management across renderers and drag/resize operations Decouples column key parsing from date extraction Simplifies event positioning logic Improves multi-resource view compatibility --- src/v2/features/event/EventRenderer.ts | 21 ++-- .../headerdrawer/HeaderDrawerRenderer.ts | 97 ++++++++++--------- src/v2/features/schedule/ScheduleRenderer.ts | 6 +- src/v2/managers/DragDropManager.ts | 11 ++- src/v2/managers/ResizeManager.ts | 4 +- src/v2/types/DragTypes.ts | 4 +- src/v2/types/SwpEvent.ts | 9 +- 7 files changed, 81 insertions(+), 71 deletions(-) diff --git a/src/v2/features/event/EventRenderer.ts b/src/v2/features/event/EventRenderer.ts index 596cc71..630e3bf 100644 --- a/src/v2/features/event/EventRenderer.ts +++ b/src/v2/features/event/EventRenderer.ts @@ -131,12 +131,15 @@ export class EventRenderer { const column = this.findColumn(columnKey); if (!column) return; - // Parse dateKey and resourceId from columnKey - const { date: dateKey, resource: resourceId } = this.dateService.parseColumnKey(columnKey); + // Read date and resourceId directly from column attributes (columnKey is opaque) + const date = column.dataset.date; + const resourceId = column.dataset.resourceId; + + if (!date) return; // Get date range for this day - const startDate = new Date(dateKey); - const endDate = new Date(dateKey); + const startDate = new Date(date); + const endDate = new Date(date); endDate.setHours(23, 59, 59, 999); // Fetch events from IndexedDB @@ -144,9 +147,9 @@ export class EventRenderer { ? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate) : await this.eventService.getByDateRange(startDate, endDate); - // Filter to timed events and match dateKey exactly + // Filter to timed events and match date exactly const timedEvents = events.filter(event => - !event.allDay && this.dateService.getDateKey(event.start) === dateKey + !event.allDay && this.dateService.getDateKey(event.start) === date ); // Get or create events layer @@ -260,15 +263,15 @@ export class EventRenderer { // Render events into each column based on data attributes columns.forEach(column => { - const dateKey = (column as HTMLElement).dataset.date; + const date = (column as HTMLElement).dataset.date; const columnResourceId = (column as HTMLElement).dataset.resourceId; - if (!dateKey) return; + if (!date) return; // Filter events for this column const columnEvents = events.filter(event => { // Must match date - if (this.dateService.getDateKey(event.start) !== dateKey) return false; + if (this.dateService.getDateKey(event.start) !== date) return false; // If column has resourceId, event must match if (columnResourceId && event.resourceId !== columnResourceId) return false; diff --git a/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts b/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts index 9f14547..8a2531f 100644 --- a/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts +++ b/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts @@ -16,6 +16,7 @@ import { */ interface IHeaderItemLayout { event: ICalendarEvent; + columnKey: string; // Opaque column identifier row: number; // 1-indexed colStart: number; // 1-indexed colEnd: number; // exclusive @@ -56,6 +57,10 @@ export class HeaderDrawerRenderer { const visibleDates = filter['date'] || []; if (visibleDates.length === 0) return; + // Get column keys from DOM for correct multi-resource positioning + const visibleColumnKeys = this.getVisibleColumnKeysFromDOM(); + if (visibleColumnKeys.length === 0) return; + // Fetch events for date range const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); @@ -71,8 +76,8 @@ export class HeaderDrawerRenderer { if (allDayEvents.length === 0) return; - // Calculate layout with row stacking - const layouts = this.calculateLayout(allDayEvents, visibleDates); + // Calculate layout with row stacking using columnKeys + const layouts = this.calculateLayout(allDayEvents, visibleColumnKeys); const rowCount = Math.max(1, ...layouts.map(l => l.row)); // Render each item with layout @@ -89,13 +94,14 @@ export class HeaderDrawerRenderer { * Create a header item element from layout */ private createHeaderItem(layout: IHeaderItemLayout): HTMLElement { - const { event, row, colStart, colEnd } = layout; + const { event, columnKey, row, colStart, colEnd } = layout; const item = document.createElement('swp-header-item'); item.dataset.eventId = event.id; item.dataset.itemType = 'event'; item.dataset.start = event.start.toISOString(); item.dataset.end = event.end.toISOString(); + item.dataset.columnKey = columnKey; item.textContent = event.title; // Color class @@ -112,19 +118,22 @@ export class HeaderDrawerRenderer { * Calculate layout for all events with row stacking * Uses track-based algorithm to find available rows for overlapping events */ - private calculateLayout(events: ICalendarEvent[], visibleDates: string[]): IHeaderItemLayout[] { + private calculateLayout(events: ICalendarEvent[], visibleColumnKeys: string[]): IHeaderItemLayout[] { // tracks[row][col] = occupied - const tracks: boolean[][] = [new Array(visibleDates.length).fill(false)]; + const tracks: boolean[][] = [new Array(visibleColumnKeys.length).fill(false)]; const layouts: IHeaderItemLayout[] = []; for (const event of events) { - const startCol = this.getColIndex(event.start, visibleDates); - const endCol = this.getColIndex(event.end, visibleDates); + // Build columnKey from event fields (only place we need to construct it) + const columnKey = this.buildColumnKeyFromEvent(event); + const startCol = visibleColumnKeys.indexOf(columnKey); + const endColumnKey = this.buildColumnKeyFromEvent(event, event.end); + const endCol = visibleColumnKeys.indexOf(endColumnKey); if (startCol === -1 && endCol === -1) continue; // Clamp til synlige kolonner const colStart = Math.max(0, startCol); - const colEnd = (endCol !== -1 ? endCol : visibleDates.length - 1) + 1; + const colEnd = (endCol !== -1 ? endCol : visibleColumnKeys.length - 1) + 1; // Find ledig række const row = this.findAvailableRow(tracks, colStart, colEnd); @@ -134,12 +143,23 @@ export class HeaderDrawerRenderer { tracks[row][c] = true; } - layouts.push({ event, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 }); + layouts.push({ event, columnKey, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 }); } return layouts; } + /** + * Build columnKey from event fields + * This is the only place we construct columnKey from event data + */ + private buildColumnKeyFromEvent(event: ICalendarEvent, date?: Date): string { + const dateStr = this.dateService.getDateKey(date || event.start); + const segments: Record = { date: dateStr }; + if (event.resourceId) segments.resource = event.resourceId; + return this.dateService.buildColumnKey(segments); + } + /** * Find available row for event spanning columns [colStart, colEnd) */ @@ -156,14 +176,6 @@ export class HeaderDrawerRenderer { return tracks.length - 1; } - /** - * Get column index for a date (0-indexed, -1 if not found) - */ - private getColIndex(date: Date, visibleDates: string[]): number { - const dateKey = this.dateService.getDateKey(date); - return visibleDates.indexOf(dateKey); - } - /** * Get color class based on event metadata or type */ @@ -233,15 +245,9 @@ export class HeaderDrawerRenderer { item.dataset.eventId = payload.eventId; item.dataset.itemType = payload.itemType; item.dataset.duration = String(payload.duration); + item.dataset.columnKey = payload.sourceColumnKey; item.textContent = payload.title; - // Set start/end as ISO dates (for recalculateDrawerLayout) - const startDate = new Date(payload.sourceDate); - const endDate = new Date(payload.sourceDate); - endDate.setDate(endDate.getDate() + payload.duration - 1); - item.dataset.start = startDate.toISOString(); - item.dataset.end = endDate.toISOString(); - // Apply color class if present if (payload.colorClass) { item.classList.add(payload.colorClass); @@ -276,12 +282,8 @@ export class HeaderDrawerRenderer { this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`; - // Update start/end dates based on new position - const startDate = new Date(payload.dateKey); - const endDate = new Date(payload.dateKey); - endDate.setDate(endDate.getDate() + duration - 1); - this.currentItem.dataset.start = startDate.toISOString(); - this.currentItem.dataset.end = endDate.toISOString(); + // Update columnKey to new position + this.currentItem.dataset.columnKey = payload.columnKey; } /** @@ -327,26 +329,27 @@ export class HeaderDrawerRenderer { const items = Array.from(drawer.querySelectorAll('swp-header-item')) as HTMLElement[]; if (items.length === 0) return; - // Get visible dates from existing items - const visibleDates = this.getVisibleDatesFromDOM(); - if (visibleDates.length === 0) return; + // Get visible column keys for correct multi-resource positioning + const visibleColumnKeys = this.getVisibleColumnKeysFromDOM(); + if (visibleColumnKeys.length === 0) return; - // Build layout data from DOM items + // Build layout data from DOM items - use columnKey directly (opaque matching) const itemData = items.map(item => ({ element: item, - start: new Date(item.dataset.start || ''), - end: new Date(item.dataset.end || '') + columnKey: item.dataset.columnKey || '', + duration: parseInt(item.dataset.duration || '1', 10) })); // Calculate new layout using track algorithm - const tracks: boolean[][] = [new Array(visibleDates.length).fill(false)]; + const tracks: boolean[][] = [new Array(visibleColumnKeys.length).fill(false)]; for (const item of itemData) { - const startCol = this.getColIndex(item.start, visibleDates); - const endCol = this.getColIndex(item.end, visibleDates); + // Direct columnKey matching - no parsing or construction needed + const startCol = visibleColumnKeys.indexOf(item.columnKey); + if (startCol === -1) continue; - const colStart = Math.max(0, startCol); - const colEnd = (endCol !== -1 ? endCol : visibleDates.length - 1) + 1; + const colStart = startCol; + const colEnd = Math.min(startCol + item.duration, visibleColumnKeys.length); const row = this.findAvailableRow(tracks, colStart, colEnd); @@ -364,16 +367,16 @@ export class HeaderDrawerRenderer { } /** - * Get visible dates from header columns in DOM + * Get visible column keys from DOM (preserves order for multi-resource views) */ - private getVisibleDatesFromDOM(): string[] { + private getVisibleColumnKeysFromDOM(): string[] { const columns = document.querySelectorAll('swp-day-column'); - const dates: string[] = []; + const columnKeys: string[] = []; columns.forEach(col => { - const date = (col as HTMLElement).dataset.date; - if (date && !dates.includes(date)) dates.push(date); + const columnKey = (col as HTMLElement).dataset.columnKey; + if (columnKey) columnKeys.push(columnKey); }); - return dates; + return columnKeys; } /** diff --git a/src/v2/features/schedule/ScheduleRenderer.ts b/src/v2/features/schedule/ScheduleRenderer.ts index 93c3c14..012d657 100644 --- a/src/v2/features/schedule/ScheduleRenderer.ts +++ b/src/v2/features/schedule/ScheduleRenderer.ts @@ -36,10 +36,10 @@ export class ScheduleRenderer { const columns = dayColumns.querySelectorAll('swp-day-column'); for (const column of columns) { - const dateKey = (column as HTMLElement).dataset.date; + const date = (column as HTMLElement).dataset.date; const resourceId = (column as HTMLElement).dataset.resourceId; - if (!dateKey || !resourceId) continue; + if (!date || !resourceId) continue; // Get or create unavailable layer let unavailableLayer = column.querySelector('swp-unavailable-layer'); @@ -52,7 +52,7 @@ export class ScheduleRenderer { unavailableLayer.innerHTML = ''; // Get schedule for this resource/date - const schedule = await this.scheduleService.getScheduleForDate(resourceId, dateKey); + const schedule = await this.scheduleService.getScheduleForDate(resourceId, date); // Render unavailable zones this.renderUnavailableZones(unavailableLayer as HTMLElement, schedule); diff --git a/src/v2/managers/DragDropManager.ts b/src/v2/managers/DragDropManager.ts index 451c278..4bf50b9 100644 --- a/src/v2/managers/DragDropManager.ts +++ b/src/v2/managers/DragDropManager.ts @@ -176,7 +176,8 @@ export class DragDropManager { if (gridEvent) { const columnKey = this.dragState.currentColumn.dataset.columnKey || ''; - const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, this.gridConfig); + const date = this.dragState.currentColumn.dataset.date || ''; + const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, date, this.gridConfig); const payload: IDragEndPayload = { swpEvent, @@ -203,13 +204,15 @@ export class DragDropManager { // Remove ghost this.dragState.ghostElement?.remove(); - // Get columnKey from target column + // Get columnKey and date from target column const columnKey = this.dragState.columnElement.dataset.columnKey || ''; + const date = this.dragState.columnElement.dataset.date || ''; // Create SwpEvent from element (reads top/height/eventId from element) const swpEvent = SwpEvent.fromElement( this.dragState.element, columnKey, + date, this.gridConfig ); @@ -406,7 +409,7 @@ export class DragDropManager { eventId: this.dragState.eventId, element: this.dragState.element, sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement), - sourceDate: this.dragState.columnElement.dataset.date || '', + sourceColumnKey: this.dragState.columnElement.dataset.columnKey || '', title: this.dragState.element.querySelector('swp-event-title')?.textContent || '', colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')), itemType: 'event', @@ -468,7 +471,7 @@ export class DragDropManager { const payload: IDragMoveHeaderPayload = { eventId: this.dragState.eventId, columnIndex: this.getColumnIndex(column), - dateKey: column.dataset.date || '' + columnKey: column.dataset.columnKey || '' }; this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload); diff --git a/src/v2/managers/ResizeManager.ts b/src/v2/managers/ResizeManager.ts index 02a55da..5448def 100644 --- a/src/v2/managers/ResizeManager.ts +++ b/src/v2/managers/ResizeManager.ts @@ -251,14 +251,16 @@ export class ResizeManager { // Remove global resizing class document.documentElement.classList.remove('swp--resizing'); - // Get columnKey from parent column + // Get columnKey and date from parent column const column = this.resizeState.element.closest('swp-day-column') as HTMLElement; const columnKey = column?.dataset.columnKey || ''; + const date = column?.dataset.date || ''; // Create SwpEvent from element (reads top/height/eventId from element) const swpEvent = SwpEvent.fromElement( this.resizeState.element, columnKey, + date, this.gridConfig ); diff --git a/src/v2/types/DragTypes.ts b/src/v2/types/DragTypes.ts index 0d720ab..d63c1a8 100644 --- a/src/v2/types/DragTypes.ts +++ b/src/v2/types/DragTypes.ts @@ -50,7 +50,7 @@ export interface IDragEnterHeaderPayload { eventId: string; element: HTMLElement; // Original dragged element sourceColumnIndex: number; - sourceDate: string; + sourceColumnKey: string; // Opaque column identifier (for matching only) title: string; colorClass?: string; itemType: 'event' | 'reminder'; @@ -60,7 +60,7 @@ export interface IDragEnterHeaderPayload { export interface IDragMoveHeaderPayload { eventId: string; columnIndex: number; - dateKey: string; + columnKey: string; // Opaque column identifier (for matching only) } export interface IDragLeaveHeaderPayload { diff --git a/src/v2/types/SwpEvent.ts b/src/v2/types/SwpEvent.ts index 63b23a8..ff8373b 100644 --- a/src/v2/types/SwpEvent.ts +++ b/src/v2/types/SwpEvent.ts @@ -51,24 +51,23 @@ export class SwpEvent { /** * 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") + * @param columnKey - Opaque column identifier (do NOT parse - use only for matching) + * @param date - Date string (YYYY-MM-DD) for time calculations */ static fromElement( element: HTMLElement, columnKey: string, + date: 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; - const start = new Date(dateKey); + const start = new Date(date); start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0); // Calculate end from height