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
This commit is contained in:
parent
7da88bb977
commit
0eb3bacb41
9 changed files with 99 additions and 71 deletions
|
|
@ -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, string>): 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
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -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<string, string> = { 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,22 +116,24 @@ export class EventRenderer {
|
|||
*/
|
||||
private async handleEventUpdated(payload: IEventUpdatedPayload): Promise<void> {
|
||||
// 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<void> {
|
||||
const column = this.findColumn(dateKey, resourceId);
|
||||
private async rerenderColumn(columnKey: string): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue