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!