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);
|
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
|
// TIME CALCULATIONS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,15 @@ export class DateRenderer implements IRenderer {
|
||||||
for (const dateStr of dates) {
|
for (const dateStr of dates) {
|
||||||
const date = this.dateService.parseISO(dateStr);
|
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
|
// Header
|
||||||
const header = document.createElement('swp-day-header');
|
const header = document.createElement('swp-day-header');
|
||||||
header.dataset.date = dateStr;
|
header.dataset.date = dateStr;
|
||||||
|
header.dataset.columnKey = columnKey;
|
||||||
if (resourceId) {
|
if (resourceId) {
|
||||||
header.dataset.resourceId = resourceId;
|
header.dataset.resourceId = resourceId;
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +40,7 @@ export class DateRenderer implements IRenderer {
|
||||||
// Column
|
// Column
|
||||||
const column = document.createElement('swp-day-column');
|
const column = document.createElement('swp-day-column');
|
||||||
column.dataset.date = dateStr;
|
column.dataset.date = dateStr;
|
||||||
|
column.dataset.columnKey = columnKey;
|
||||||
if (resourceId) {
|
if (resourceId) {
|
||||||
column.dataset.resourceId = resourceId;
|
column.dataset.resourceId = resourceId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,22 +116,24 @@ export class EventRenderer {
|
||||||
*/
|
*/
|
||||||
private async handleEventUpdated(payload: IEventUpdatedPayload): Promise<void> {
|
private async handleEventUpdated(payload: IEventUpdatedPayload): Promise<void> {
|
||||||
// Re-render source column (if different from target)
|
// Re-render source column (if different from target)
|
||||||
if (payload.sourceDateKey !== payload.targetDateKey ||
|
if (payload.sourceColumnKey !== payload.targetColumnKey) {
|
||||||
payload.sourceResourceId !== payload.targetResourceId) {
|
await this.rerenderColumn(payload.sourceColumnKey);
|
||||||
await this.rerenderColumn(payload.sourceDateKey, payload.sourceResourceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-render target column
|
// 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
|
* Re-render a single column with fresh data from IndexedDB
|
||||||
*/
|
*/
|
||||||
private async rerenderColumn(dateKey: string, resourceId?: string): Promise<void> {
|
private async rerenderColumn(columnKey: string): Promise<void> {
|
||||||
const column = this.findColumn(dateKey, resourceId);
|
const column = this.findColumn(columnKey);
|
||||||
if (!column) return;
|
if (!column) return;
|
||||||
|
|
||||||
|
// Parse dateKey and resourceId from columnKey
|
||||||
|
const { date: dateKey, resource: resourceId } = this.dateService.parseColumnKey(columnKey);
|
||||||
|
|
||||||
// Get date range for this day
|
// Get date range for this day
|
||||||
const startDate = new Date(dateKey);
|
const startDate = new Date(dateKey);
|
||||||
const endDate = 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;
|
if (!this.container) return null;
|
||||||
|
return this.container.querySelector(`swp-day-column[data-column-key="${columnKey}"]`) as HTMLElement;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,7 @@ interface DragState {
|
||||||
targetY: number;
|
targetY: number;
|
||||||
currentY: number;
|
currentY: number;
|
||||||
animationId: number;
|
animationId: number;
|
||||||
sourceDateKey: string; // Source column date (where drag started)
|
sourceColumnKey: string; // Source column key (where drag started)
|
||||||
sourceResourceId?: string; // Source column resource (where drag started)
|
|
||||||
dragSource: 'grid' | 'header'; // Where drag originated
|
dragSource: 'grid' | 'header'; // Where drag originated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,13 +175,12 @@ export class DragDropManager {
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
if (gridEvent) {
|
if (gridEvent) {
|
||||||
const dateKey = this.dragState.currentColumn.dataset.date || '';
|
const columnKey = this.dragState.currentColumn.dataset.columnKey || '';
|
||||||
const swpEvent = SwpEvent.fromElement(gridEvent, dateKey, this.gridConfig);
|
const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, this.gridConfig);
|
||||||
|
|
||||||
const payload: IDragEndPayload = {
|
const payload: IDragEndPayload = {
|
||||||
swpEvent,
|
swpEvent,
|
||||||
sourceDateKey: this.dragState.sourceDateKey,
|
sourceColumnKey: this.dragState.sourceColumnKey,
|
||||||
sourceResourceId: this.dragState.sourceResourceId,
|
|
||||||
target: 'grid'
|
target: 'grid'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -205,21 +203,20 @@ export class DragDropManager {
|
||||||
// Remove ghost
|
// Remove ghost
|
||||||
this.dragState.ghostElement?.remove();
|
this.dragState.ghostElement?.remove();
|
||||||
|
|
||||||
// Get dateKey from target column
|
// Get columnKey from target column
|
||||||
const dateKey = this.dragState.columnElement.dataset.date || '';
|
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(
|
const swpEvent = SwpEvent.fromElement(
|
||||||
this.dragState.element,
|
this.dragState.element,
|
||||||
dateKey,
|
columnKey,
|
||||||
this.gridConfig
|
this.gridConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
// Emit drag:end
|
// Emit drag:end
|
||||||
const payload: IDragEndPayload = {
|
const payload: IDragEndPayload = {
|
||||||
swpEvent,
|
swpEvent,
|
||||||
sourceDateKey: this.dragState.sourceDateKey,
|
sourceColumnKey: this.dragState.sourceColumnKey,
|
||||||
sourceResourceId: this.dragState.sourceResourceId,
|
|
||||||
target: this.inHeader ? 'header' : 'grid'
|
target: this.inHeader ? 'header' : 'grid'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -262,8 +259,7 @@ export class DragDropManager {
|
||||||
targetY: 0,
|
targetY: 0,
|
||||||
currentY: 0,
|
currentY: 0,
|
||||||
animationId: 0,
|
animationId: 0,
|
||||||
sourceDateKey: '', // Will be set from header item data
|
sourceColumnKey: '', // Will be set from header item data
|
||||||
sourceResourceId: undefined,
|
|
||||||
dragSource: 'header'
|
dragSource: 'header'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -323,8 +319,7 @@ export class DragDropManager {
|
||||||
targetY: Math.max(0, targetY),
|
targetY: Math.max(0, targetY),
|
||||||
currentY: startY,
|
currentY: startY,
|
||||||
animationId: 0,
|
animationId: 0,
|
||||||
sourceDateKey: columnElement.dataset.date || '',
|
sourceColumnKey: columnElement.dataset.columnKey || '',
|
||||||
sourceResourceId: columnElement.dataset.resourceId,
|
|
||||||
dragSource: 'grid'
|
dragSource: 'grid'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ export class EventPersistenceManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse resourceId from columnKey if present
|
||||||
|
const { resource } = this.dateService.parseColumnKey(swpEvent.columnKey);
|
||||||
|
|
||||||
// Update and save - start/end already calculated in SwpEvent
|
// Update and save - start/end already calculated in SwpEvent
|
||||||
// Set allDay based on drop target:
|
// Set allDay based on drop target:
|
||||||
// - header: allDay = true
|
// - header: allDay = true
|
||||||
|
|
@ -48,7 +51,7 @@ export class EventPersistenceManager {
|
||||||
...event,
|
...event,
|
||||||
start: swpEvent.start,
|
start: swpEvent.start,
|
||||||
end: swpEvent.end,
|
end: swpEvent.end,
|
||||||
resourceId: swpEvent.resourceId ?? event.resourceId,
|
resourceId: resource ?? event.resourceId,
|
||||||
allDay: payload.target === 'header',
|
allDay: payload.target === 'header',
|
||||||
syncStatus: 'pending'
|
syncStatus: 'pending'
|
||||||
};
|
};
|
||||||
|
|
@ -56,13 +59,10 @@ export class EventPersistenceManager {
|
||||||
await this.eventService.save(updatedEvent);
|
await this.eventService.save(updatedEvent);
|
||||||
|
|
||||||
// Emit EVENT_UPDATED for EventRenderer to re-render affected columns
|
// Emit EVENT_UPDATED for EventRenderer to re-render affected columns
|
||||||
const targetDateKey = this.dateService.getDateKey(swpEvent.start);
|
|
||||||
const updatePayload: IEventUpdatedPayload = {
|
const updatePayload: IEventUpdatedPayload = {
|
||||||
eventId: updatedEvent.id,
|
eventId: updatedEvent.id,
|
||||||
sourceDateKey: payload.sourceDateKey,
|
sourceColumnKey: payload.sourceColumnKey,
|
||||||
sourceResourceId: payload.sourceResourceId,
|
targetColumnKey: swpEvent.columnKey
|
||||||
targetDateKey,
|
|
||||||
targetResourceId: swpEvent.resourceId
|
|
||||||
};
|
};
|
||||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
||||||
};
|
};
|
||||||
|
|
@ -92,13 +92,10 @@ export class EventPersistenceManager {
|
||||||
|
|
||||||
// Emit EVENT_UPDATED for EventRenderer to re-render the column
|
// Emit EVENT_UPDATED for EventRenderer to re-render the column
|
||||||
// Resize stays in same column, so source and target are the same
|
// Resize stays in same column, so source and target are the same
|
||||||
const dateKey = this.dateService.getDateKey(swpEvent.start);
|
|
||||||
const updatePayload: IEventUpdatedPayload = {
|
const updatePayload: IEventUpdatedPayload = {
|
||||||
eventId: updatedEvent.id,
|
eventId: updatedEvent.id,
|
||||||
sourceDateKey: dateKey,
|
sourceColumnKey: swpEvent.columnKey,
|
||||||
sourceResourceId: swpEvent.resourceId,
|
targetColumnKey: swpEvent.columnKey
|
||||||
targetDateKey: dateKey,
|
|
||||||
targetResourceId: swpEvent.resourceId
|
|
||||||
};
|
};
|
||||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -251,14 +251,14 @@ export class ResizeManager {
|
||||||
// Remove global resizing class
|
// Remove global resizing class
|
||||||
document.documentElement.classList.remove('swp--resizing');
|
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 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(
|
const swpEvent = SwpEvent.fromElement(
|
||||||
this.resizeState.element,
|
this.resizeState.element,
|
||||||
dateKey,
|
columnKey,
|
||||||
this.gridConfig
|
this.gridConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,8 @@ export interface IEntityDeletedPayload {
|
||||||
// Event update payload (for re-rendering columns after drag/resize)
|
// Event update payload (for re-rendering columns after drag/resize)
|
||||||
export interface IEventUpdatedPayload {
|
export interface IEventUpdatedPayload {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
sourceDateKey: string; // Source column date (where event came from)
|
sourceColumnKey: string; // Source column key (where event came from)
|
||||||
sourceResourceId?: string; // Source column resource
|
targetColumnKey: string; // Target column key (where event landed)
|
||||||
targetDateKey: string; // Target column date (where event landed)
|
|
||||||
targetResourceId?: string; // Target column resource
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource types
|
// Resource types
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,8 @@ export interface IDragMovePayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDragEndPayload {
|
export interface IDragEndPayload {
|
||||||
swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, resourceId
|
swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, columnKey
|
||||||
sourceDateKey: string; // Source column date (where drag started)
|
sourceColumnKey: string; // Source column key (where drag started)
|
||||||
sourceResourceId?: string; // Source column resource (where drag started)
|
|
||||||
target: 'grid' | 'header'; // Where the event was dropped
|
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.
|
* for start/end times based on element position and grid config.
|
||||||
*
|
*
|
||||||
* Usage:
|
* 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
|
* - Position (top, height) is read from element.style
|
||||||
* - Factory method `fromElement()` calculates Date objects
|
* - Factory method `fromElement()` calculates Date objects
|
||||||
*/
|
*/
|
||||||
export class SwpEvent {
|
export class SwpEvent {
|
||||||
readonly element: HTMLElement;
|
readonly element: HTMLElement;
|
||||||
|
readonly columnKey: string;
|
||||||
private _start: Date;
|
private _start: Date;
|
||||||
private _end: Date;
|
private _end: Date;
|
||||||
|
|
||||||
constructor(element: HTMLElement, start: Date, end: Date) {
|
constructor(element: HTMLElement, columnKey: string, start: Date, end: Date) {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
|
this.columnKey = columnKey;
|
||||||
this._start = start;
|
this._start = start;
|
||||||
this._end = end;
|
this._end = end;
|
||||||
}
|
}
|
||||||
|
|
@ -27,11 +30,6 @@ export class SwpEvent {
|
||||||
return this.element.dataset.eventId || '';
|
return this.element.dataset.eventId || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resource ID from element.dataset.resourceId */
|
|
||||||
get resourceId(): string | undefined {
|
|
||||||
return this.element.dataset.resourceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get start(): Date {
|
get start(): Date {
|
||||||
return this._start;
|
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
|
* 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(
|
static fromElement(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
dateKey: string,
|
columnKey: string,
|
||||||
gridConfig: IGridConfig
|
gridConfig: IGridConfig
|
||||||
): SwpEvent {
|
): SwpEvent {
|
||||||
const topPixels = parseFloat(element.style.top) || 0;
|
const topPixels = parseFloat(element.style.top) || 0;
|
||||||
const heightPixels = parseFloat(element.style.height) || 0;
|
const heightPixels = parseFloat(element.style.height) || 0;
|
||||||
|
|
||||||
|
// Extract dateKey from columnKey (first segment)
|
||||||
|
const dateKey = columnKey.split(':')[0];
|
||||||
|
|
||||||
// Calculate start from top position
|
// Calculate start from top position
|
||||||
const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60;
|
const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60;
|
||||||
const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid;
|
const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid;
|
||||||
|
|
@ -73,6 +75,6 @@ export class SwpEvent {
|
||||||
const durationMinutes = (heightPixels / gridConfig.hourHeight) * 60;
|
const durationMinutes = (heightPixels / gridConfig.hourHeight) * 60;
|
||||||
const end = new Date(start.getTime() + durationMinutes * 60 * 1000);
|
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