Refactor calendar datasource and event management

Enhances calendar flexibility by introducing group-based column spanning and improving cross-mode event handling

Adds support for:
- Dynamic column grouping in date and resource modes
- Consistent event drag-and-drop across different calendar views
- More robust all-day event layout calculations

Improves event management logic to handle resource and date mode transitions more elegantly
This commit is contained in:
Janus C. H. Knudsen 2025-11-25 19:04:06 +01:00
parent 17909696ed
commit d8b9f6dabd
16 changed files with 192 additions and 79 deletions

View file

@ -5,6 +5,7 @@ import { ALL_DAY_CONSTANTS } from '../configurations/CalendarConfig';
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine';
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { IColumnDataSource } from '../types/ColumnDataSource';
import { ICalendarEvent } from '../types/CalendarTypes';
import { CalendarEventType } from '../types/BookingTypes';
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
@ -30,12 +31,13 @@ export class AllDayManager {
private allDayEventRenderer: AllDayEventRenderer;
private eventManager: EventManager;
private dateService: DateService;
private dataSource: IColumnDataSource;
private layoutEngine: AllDayLayoutEngine | null = null;
// State tracking for layout calculation
private currentAllDayEvents: ICalendarEvent[] = [];
private currentWeekDates: IColumnBounds[] = [];
private currentColumns: IColumnBounds[] = [];
// Expand/collapse state
private isExpanded: boolean = false;
@ -45,11 +47,13 @@ export class AllDayManager {
constructor(
eventManager: EventManager,
allDayEventRenderer: AllDayEventRenderer,
dateService: DateService
dateService: DateService,
dataSource: IColumnDataSource
) {
this.eventManager = eventManager;
this.allDayEventRenderer = allDayEventRenderer;
this.dateService = dateService;
this.dataSource = dataSource;
// Sync CSS variable with TypeScript constant to ensure consistency
document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`);
@ -140,7 +144,7 @@ export class AllDayManager {
// Recalculate layout WITHOUT the removed event to compress gaps
const remainingEvents = this.currentAllDayEvents.filter(e => e.id !== eventId);
const newLayouts = this.calculateAllDayEventsLayout(remainingEvents, this.currentWeekDates);
const newLayouts = this.calculateAllDayEventsLayout(remainingEvents, this.currentColumns);
// Re-render all-day events with compressed layout
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
@ -395,10 +399,18 @@ export class AllDayManager {
// Store current state
this.currentAllDayEvents = events;
this.currentWeekDates = dayHeaders;
this.currentColumns = dayHeaders;
// Initialize layout engine with provided week dates
let layoutEngine = new AllDayLayoutEngine(dayHeaders.map(column => column.identifier));
// Map IColumnBounds to IColumnInfo structure (identifier + groupId)
const columns = dayHeaders.map(column => ({
identifier: column.identifier,
groupId: column.element.dataset.groupId || column.identifier,
data: new Date(), // Not used by AllDayLayoutEngine
events: [] // Not used by AllDayLayoutEngine
}));
// Initialize layout engine with column info including groupId
let layoutEngine = new AllDayLayoutEngine(columns);
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
return layoutEngine.calculateLayout(events);
@ -489,23 +501,43 @@ export class AllDayManager {
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
const eventId = clone.eventId.replace('clone-', '');
const targetDate = this.dateService.parseISO(dragEndEvent.finalPosition.column.identifier);
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
// Determine target date based on mode
let targetDate: Date;
let resourceId: string | undefined;
if (this.dataSource.isResource()) {
// Resource mode: keep event's existing date, set resourceId
targetDate = clone.start;
resourceId = columnIdentifier;
} else {
// Date mode: parse date from column identifier
targetDate = this.dateService.parseISO(columnIdentifier);
}
console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate, resourceId });
console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate });
// Create new dates preserving time
const newStart = new Date(targetDate);
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
const newEnd = new Date(targetDate);
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
// Update event in repository
await this.eventManager.updateEvent(eventId, {
// Build update payload
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
start: newStart,
end: newEnd,
allDay: true
});
};
if (resourceId) {
updatePayload.resourceId = resourceId;
}
// Update event in repository
await this.eventManager.updateEvent(eventId, updatePayload);
// Remove original timed event
this.fadeOutAndRemove(dragEndEvent.originalElement);
@ -522,7 +554,7 @@ export class AllDayManager {
};
const updatedEvents = [...this.currentAllDayEvents, newEvent];
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentColumns);
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
// Animate height
@ -537,25 +569,45 @@ export class AllDayManager {
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
const eventId = clone.eventId.replace('clone-', '');
const targetDate = this.dateService.parseISO(dragEndEvent.finalPosition.column.identifier);
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
// Determine target date based on mode
let targetDate: Date;
let resourceId: string | undefined;
if (this.dataSource.isResource()) {
// Resource mode: keep event's existing date, set resourceId
targetDate = clone.start;
resourceId = columnIdentifier;
} else {
// Date mode: parse date from column identifier
targetDate = this.dateService.parseISO(columnIdentifier);
}
// Calculate duration in days
const durationDays = this.dateService.differenceInCalendarDays(clone.end, clone.start);
// Create new dates preserving time
const newStart = new Date(targetDate);
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
const newEnd = new Date(targetDate);
newEnd.setDate(newEnd.getDate() + durationDays);
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
// Update event in repository
await this.eventManager.updateEvent(eventId, {
// Build update payload
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
start: newStart,
end: newEnd,
allDay: true
});
};
if (resourceId) {
updatePayload.resourceId = resourceId;
}
// Update event in repository
await this.eventManager.updateEvent(eventId, updatePayload);
// Remove original and fade out
this.fadeOutAndRemove(dragEndEvent.originalElement);
@ -564,7 +616,7 @@ export class AllDayManager {
const updatedEvents = this.currentAllDayEvents.map(e =>
e.id === eventId ? { ...e, start: newStart, end: newEnd } : e
);
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentColumns);
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
// Animate height - this also handles overflow classes!

View file

@ -457,12 +457,20 @@ export class DragDropManager {
if (!dropTarget)
throw "dropTarget is null";
// Read date and resourceId directly from DOM
const dateString = column.element.dataset.date;
if (!dateString) {
throw "column.element.dataset.date is not set";
}
const date = new Date(dateString);
const resourceId = column.element.dataset.resourceId; // undefined in date mode
const dragEndPayload: IDragEndEventPayload = {
originalElement: this.originalElement,
draggedClone: this.draggedClone,
mousePosition,
originalSourceColumn: this.originalSourceColumn!!,
finalPosition: { column, snappedY }, // Where drag ended
finalPosition: { column, date, resourceId, snappedY },
target: dropTarget
};

View file

@ -196,30 +196,4 @@ export class EventManager {
return false;
}
}
/**
* Handle remote update from SignalR
* Saves remote event directly (no queue logic yet)
*/
public async handleRemoteUpdate(event: ICalendarEvent): Promise<void> {
try {
// Mark as synced since it comes from remote
const remoteEvent: ICalendarEvent = {
...event,
syncStatus: 'synced'
};
await this.eventService.save(remoteEvent);
this.eventBus.emit(CoreEvents.REMOTE_UPDATE_RECEIVED, {
event: remoteEvent
});
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
event: remoteEvent
});
} catch (error) {
console.error(`Failed to handle remote update for event ${event.id}:`, error);
}
}
}

View file

@ -197,7 +197,7 @@ export class NavigationManager {
const columns = await this.dataSource.getColumns();
// Always create a fresh container for consistent behavior
newGrid = this.gridRenderer.createNavigationGrid(container, columns);
newGrid = this.gridRenderer.createNavigationGrid(container, columns, targetWeek);
console.groupEnd();