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:
parent
17909696ed
commit
d8b9f6dabd
16 changed files with 192 additions and 79 deletions
|
|
@ -62,7 +62,8 @@ export class DateColumnDataSource implements IColumnDataSource {
|
|||
events: await this.eventService.getByDateRange(
|
||||
this.dateService.startOfDay(date),
|
||||
this.dateService.endOfDay(date)
|
||||
)
|
||||
),
|
||||
groupId: 'week' // All columns in date mode share same group for spanning
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -90,6 +91,13 @@ export class DateColumnDataSource implements IColumnDataSource {
|
|||
this.currentDate = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current date
|
||||
*/
|
||||
public getCurrentDate(): Date {
|
||||
return this.currentDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current view
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ export class ResourceColumnDataSource implements IColumnDataSource {
|
|||
resources.map(async resource => ({
|
||||
identifier: resource.id,
|
||||
data: resource,
|
||||
events: await this.eventService.getByResourceAndDateRange(resource.id, startDate, endDate)
|
||||
events: await this.eventService.getByResourceAndDateRange(resource.id, startDate, endDate),
|
||||
groupId: resource.id // Each resource is its own group - no spanning across resources
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ async function initializeCalendar(): Promise<void> {
|
|||
builder.registerType(MockAuditRepository).as<IApiRepository<IAuditEntry>>();
|
||||
|
||||
|
||||
let calendarMode = 'resource' ;
|
||||
let calendarMode = 'date' ;
|
||||
// Register DataSource and HeaderRenderer based on mode
|
||||
if (calendarMode === 'resource') {
|
||||
builder.registerType(ResourceColumnDataSource).as<IColumnDataSource>();
|
||||
|
|
|
|||
|
|
@ -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,9 +501,22 @@ 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;
|
||||
|
||||
console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate });
|
||||
// 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 });
|
||||
|
||||
// Create new dates preserving time
|
||||
const newStart = new Date(targetDate);
|
||||
|
|
@ -500,12 +525,19 @@ export class AllDayManager {
|
|||
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,7 +569,20 @@ 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);
|
||||
|
|
@ -550,12 +595,19 @@ export class AllDayManager {
|
|||
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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface IColumnRenderer {
|
|||
export interface IColumnRenderContext {
|
||||
columns: IColumnInfo[];
|
||||
config: Configuration;
|
||||
currentDate?: Date; // Optional: Only used by ResourceColumnRenderer in resource mode
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,6 +44,7 @@ export class DateColumnRenderer implements IColumnRenderer {
|
|||
const column = document.createElement('swp-day-column');
|
||||
|
||||
column.dataset.columnId = columnInfo.identifier;
|
||||
column.dataset.date = this.dateService.formatISODate(date);
|
||||
|
||||
// Apply work hours styling
|
||||
this.applyWorkHoursToColumn(column, date);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export class DateHeaderRenderer implements IHeaderRenderer {
|
|||
`;
|
||||
|
||||
header.dataset.columnId = columnInfo.identifier;
|
||||
header.dataset.groupId = columnInfo.groupId;
|
||||
|
||||
calendarHeader.appendChild(header);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export class GridRenderer {
|
|||
fragment.appendChild(timeAxis);
|
||||
|
||||
// Create grid container with caching
|
||||
const gridContainer = this.createOptimizedGridContainer(columns);
|
||||
const gridContainer = this.createOptimizedGridContainer(columns, currentDate);
|
||||
this.cachedGridContainer = gridContainer;
|
||||
fragment.appendChild(gridContainer);
|
||||
|
||||
|
|
@ -211,10 +211,12 @@ export class GridRenderer {
|
|||
* - Column container - created here, populated by ColumnRenderer
|
||||
*
|
||||
* @param columns - Array of columns to render (each column contains its events)
|
||||
* @param currentDate - Current view date
|
||||
* @returns Complete grid container element
|
||||
*/
|
||||
private createOptimizedGridContainer(
|
||||
columns: IColumnInfo[]
|
||||
columns: IColumnInfo[],
|
||||
currentDate: Date
|
||||
): HTMLElement {
|
||||
const gridContainer = document.createElement('swp-grid-container');
|
||||
|
||||
|
|
@ -232,7 +234,7 @@ export class GridRenderer {
|
|||
|
||||
// Create column container
|
||||
const columnContainer = document.createElement('swp-day-columns');
|
||||
this.renderColumnContainer(columnContainer, columns);
|
||||
this.renderColumnContainer(columnContainer, columns, currentDate);
|
||||
timeGrid.appendChild(columnContainer);
|
||||
|
||||
scrollableContent.appendChild(timeGrid);
|
||||
|
|
@ -250,15 +252,18 @@ export class GridRenderer {
|
|||
*
|
||||
* @param columnContainer - Empty container to populate
|
||||
* @param columns - Array of columns to render (each column contains its events)
|
||||
* @param currentDate - Current view date
|
||||
*/
|
||||
private renderColumnContainer(
|
||||
columnContainer: HTMLElement,
|
||||
columns: IColumnInfo[]
|
||||
columns: IColumnInfo[],
|
||||
currentDate: Date
|
||||
): void {
|
||||
// Delegate to ColumnRenderer
|
||||
this.columnRenderer.render(columnContainer, {
|
||||
columns: columns,
|
||||
config: this.config
|
||||
config: this.config,
|
||||
currentDate: currentDate
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +288,7 @@ export class GridRenderer {
|
|||
const columnContainer = grid.querySelector('swp-day-columns');
|
||||
if (columnContainer) {
|
||||
columnContainer.innerHTML = '';
|
||||
this.renderColumnContainer(columnContainer as HTMLElement, columns);
|
||||
this.renderColumnContainer(columnContainer as HTMLElement, columns, currentDate);
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -296,12 +301,13 @@ export class GridRenderer {
|
|||
* Events will be rendered by EventRenderingService when GRID_RENDERED emits.
|
||||
*
|
||||
* @param parentContainer - Container for the new grid
|
||||
* @param dates - Array of dates to render
|
||||
* @param columns - Array of columns to render
|
||||
* @param currentDate - Current view date
|
||||
* @returns New grid element ready for animation
|
||||
*/
|
||||
public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[]): HTMLElement {
|
||||
public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[], currentDate: Date): HTMLElement {
|
||||
// Create grid structure (events are in columns, rendered by EventRenderingService)
|
||||
const newGrid = this.createOptimizedGridContainer(columns);
|
||||
const newGrid = this.createOptimizedGridContainer(columns, currentDate);
|
||||
|
||||
// Position new grid for animation - NO transform here, let Animation API handle it
|
||||
newGrid.style.position = 'absolute';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { WorkHoursManager } from '../managers/WorkHoursManager';
|
||||
import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
|
||||
import { DateService } from '../utils/DateService';
|
||||
|
||||
/**
|
||||
* Resource-based column renderer
|
||||
|
|
@ -10,13 +11,19 @@ import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
|
|||
*/
|
||||
export class ResourceColumnRenderer implements IColumnRenderer {
|
||||
private workHoursManager: WorkHoursManager;
|
||||
private dateService: DateService;
|
||||
|
||||
constructor(workHoursManager: WorkHoursManager) {
|
||||
constructor(workHoursManager: WorkHoursManager, dateService: DateService) {
|
||||
this.workHoursManager = workHoursManager;
|
||||
this.dateService = dateService;
|
||||
}
|
||||
|
||||
render(columnContainer: HTMLElement, context: IColumnRenderContext): void {
|
||||
const { columns } = context;
|
||||
const { columns, currentDate } = context;
|
||||
|
||||
if (!currentDate) {
|
||||
throw new Error('ResourceColumnRenderer requires currentDate in context');
|
||||
}
|
||||
|
||||
// Hardcoded work hours for all resources: 09:00 - 18:00
|
||||
const workHours = { start: 9, end: 18 };
|
||||
|
|
@ -25,6 +32,7 @@ export class ResourceColumnRenderer implements IColumnRenderer {
|
|||
const column = document.createElement('swp-day-column');
|
||||
|
||||
column.dataset.columnId = columnInfo.identifier;
|
||||
column.dataset.date = this.dateService.formatISODate(currentDate);
|
||||
|
||||
// Apply hardcoded work hours to all resource columns
|
||||
this.applyWorkHoursToColumn(column, workHours);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export class ResourceHeaderRenderer implements IHeaderRenderer {
|
|||
|
||||
header.dataset.columnId = columnInfo.identifier;
|
||||
header.dataset.resourceId = resource.id;
|
||||
header.dataset.groupId = columnInfo.groupId;
|
||||
|
||||
calendarHeader.appendChild(header);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface IColumnInfo {
|
|||
identifier: string; // "2024-11-13" (date mode) or "person-1" (resource mode)
|
||||
data: Date | IResource; // Date for date-mode, IResource for resource-mode
|
||||
events: ICalendarEvent[]; // Events for this column (pre-filtered by datasource)
|
||||
groupId: string; // Group ID for spanning logic - events can only span columns with same groupId
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,6 +41,11 @@ export interface IColumnDataSource {
|
|||
*/
|
||||
setCurrentDate(date: Date): void;
|
||||
|
||||
/**
|
||||
* Get the current date
|
||||
*/
|
||||
getCurrentDate(): Date;
|
||||
|
||||
/**
|
||||
* Update the current view (day/week/month)
|
||||
* @param view - The new calendar view
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ export interface IDragEndEventPayload {
|
|||
originalSourceColumn: IColumnBounds; // Original column where drag started
|
||||
finalPosition: {
|
||||
column: IColumnBounds | null; // Where drag ended
|
||||
date: Date; // Always present - the date for this position
|
||||
resourceId?: string; // Only in resource mode
|
||||
snappedY: number;
|
||||
};
|
||||
target: 'swp-day-column' | 'swp-day-header' | null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { IColumnInfo } from '../types/ColumnDataSource';
|
||||
|
||||
export interface IEventLayout {
|
||||
calenderEvent: ICalendarEvent;
|
||||
|
|
@ -10,11 +11,13 @@ export interface IEventLayout {
|
|||
}
|
||||
|
||||
export class AllDayLayoutEngine {
|
||||
private weekDates: string[];
|
||||
private columnIdentifiers: string[]; // Column identifiers (date or resource ID)
|
||||
private columnGroups: string[]; // Group ID for each column (same index as columnIdentifiers)
|
||||
private tracks: boolean[][];
|
||||
|
||||
constructor(weekDates: string[]) {
|
||||
this.weekDates = weekDates;
|
||||
constructor(columns: IColumnInfo[]) {
|
||||
this.columnIdentifiers = columns.map(col => col.identifier);
|
||||
this.columnGroups = columns.map(col => col.groupId);
|
||||
this.tracks = [];
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +28,7 @@ export class AllDayLayoutEngine {
|
|||
|
||||
let layouts: IEventLayout[] = [];
|
||||
// Reset tracks for new calculation
|
||||
this.tracks = [new Array(this.weekDates.length).fill(false)];
|
||||
this.tracks = [new Array(this.columnIdentifiers.length).fill(false)];
|
||||
|
||||
// Filter to only visible events
|
||||
const visibleEvents = events.filter(event => this.isEventVisible(event));
|
||||
|
|
@ -70,7 +73,7 @@ export class AllDayLayoutEngine {
|
|||
}
|
||||
|
||||
// Create new track if none available
|
||||
this.tracks.push(new Array(this.weekDates.length).fill(false));
|
||||
this.tracks.push(new Array(this.columnIdentifiers.length).fill(false));
|
||||
return this.tracks.length - 1;
|
||||
}
|
||||
|
||||
|
|
@ -88,42 +91,82 @@ export class AllDayLayoutEngine {
|
|||
|
||||
/**
|
||||
* Get start day index for event (1-based, 0 if not visible)
|
||||
* Clips to group boundaries - events can only span columns with same groupId
|
||||
*/
|
||||
private getEventStartDay(event: ICalendarEvent): number {
|
||||
const eventStartDate = this.formatDate(event.start);
|
||||
const firstVisibleDate = this.weekDates[0];
|
||||
const firstVisibleDate = this.columnIdentifiers[0];
|
||||
|
||||
// If event starts before visible range, clip to first visible day
|
||||
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
|
||||
|
||||
const dayIndex = this.weekDates.indexOf(clippedStartDate);
|
||||
return dayIndex >= 0 ? dayIndex + 1 : 0;
|
||||
const dayIndex = this.columnIdentifiers.indexOf(clippedStartDate);
|
||||
if (dayIndex < 0) return 0;
|
||||
|
||||
// Find group start boundary for this column
|
||||
const groupId = this.columnGroups[dayIndex];
|
||||
const groupStart = this.getGroupStartIndex(dayIndex, groupId);
|
||||
|
||||
// Return the later of event start and group start (1-based)
|
||||
return Math.max(groupStart, dayIndex) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end day index for event (1-based, 0 if not visible)
|
||||
* Clips to group boundaries - events can only span columns with same groupId
|
||||
*/
|
||||
private getEventEndDay(event: ICalendarEvent): number {
|
||||
const eventEndDate = this.formatDate(event.end);
|
||||
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
|
||||
const lastVisibleDate = this.columnIdentifiers[this.columnIdentifiers.length - 1];
|
||||
|
||||
// If event ends after visible range, clip to last visible day
|
||||
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
|
||||
|
||||
const dayIndex = this.weekDates.indexOf(clippedEndDate);
|
||||
return dayIndex >= 0 ? dayIndex + 1 : 0;
|
||||
const dayIndex = this.columnIdentifiers.indexOf(clippedEndDate);
|
||||
if (dayIndex < 0) return 0;
|
||||
|
||||
// Find group end boundary for this column
|
||||
const groupId = this.columnGroups[dayIndex];
|
||||
const groupEnd = this.getGroupEndIndex(dayIndex, groupId);
|
||||
|
||||
// Return the earlier of event end and group end (1-based)
|
||||
return Math.min(groupEnd, dayIndex) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the start index of a group (0-based)
|
||||
* Scans backwards from columnIndex to find where this group starts
|
||||
*/
|
||||
private getGroupStartIndex(columnIndex: number, groupId: string): number {
|
||||
let startIndex = columnIndex;
|
||||
while (startIndex > 0 && this.columnGroups[startIndex - 1] === groupId) {
|
||||
startIndex--;
|
||||
}
|
||||
return startIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the end index of a group (0-based)
|
||||
* Scans forwards from columnIndex to find where this group ends
|
||||
*/
|
||||
private getGroupEndIndex(columnIndex: number, groupId: string): number {
|
||||
let endIndex = columnIndex;
|
||||
while (endIndex < this.columnGroups.length - 1 && this.columnGroups[endIndex + 1] === groupId) {
|
||||
endIndex++;
|
||||
}
|
||||
return endIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event is visible in the current date range
|
||||
*/
|
||||
private isEventVisible(event: ICalendarEvent): boolean {
|
||||
if (this.weekDates.length === 0) return false;
|
||||
if (this.columnIdentifiers.length === 0) return false;
|
||||
|
||||
const eventStartDate = this.formatDate(event.start);
|
||||
const eventEndDate = this.formatDate(event.end);
|
||||
const firstVisibleDate = this.weekDates[0];
|
||||
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
|
||||
const firstVisibleDate = this.columnIdentifiers[0];
|
||||
const lastVisibleDate = this.columnIdentifiers[this.columnIdentifiers.length - 1];
|
||||
|
||||
// Event overlaps if it doesn't end before visible range starts
|
||||
// AND doesn't start after visible range ends
|
||||
|
|
|
|||
|
|
@ -4140,6 +4140,7 @@
|
|||
{
|
||||
"id": "RES-NOV25-001",
|
||||
"title": "Balayage kort hår",
|
||||
"description": "Daily team sync - status updates",
|
||||
"start": "2025-11-25T09:00:00Z",
|
||||
"end": "2025-11-25T10:30:00Z",
|
||||
"type": "customer",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue