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(
|
events: await this.eventService.getByDateRange(
|
||||||
this.dateService.startOfDay(date),
|
this.dateService.startOfDay(date),
|
||||||
this.dateService.endOfDay(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;
|
this.currentDate = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current date
|
||||||
|
*/
|
||||||
|
public getCurrentDate(): Date {
|
||||||
|
return this.currentDate;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update current view
|
* Update current view
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ export class ResourceColumnDataSource implements IColumnDataSource {
|
||||||
resources.map(async resource => ({
|
resources.map(async resource => ({
|
||||||
identifier: resource.id,
|
identifier: resource.id,
|
||||||
data: resource,
|
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>>();
|
builder.registerType(MockAuditRepository).as<IApiRepository<IAuditEntry>>();
|
||||||
|
|
||||||
|
|
||||||
let calendarMode = 'resource' ;
|
let calendarMode = 'date' ;
|
||||||
// Register DataSource and HeaderRenderer based on mode
|
// Register DataSource and HeaderRenderer based on mode
|
||||||
if (calendarMode === 'resource') {
|
if (calendarMode === 'resource') {
|
||||||
builder.registerType(ResourceColumnDataSource).as<IColumnDataSource>();
|
builder.registerType(ResourceColumnDataSource).as<IColumnDataSource>();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ALL_DAY_CONSTANTS } from '../configurations/CalendarConfig';
|
||||||
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
||||||
import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine';
|
import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine';
|
||||||
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
||||||
|
import { IColumnDataSource } from '../types/ColumnDataSource';
|
||||||
import { ICalendarEvent } from '../types/CalendarTypes';
|
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||||
import { CalendarEventType } from '../types/BookingTypes';
|
import { CalendarEventType } from '../types/BookingTypes';
|
||||||
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
||||||
|
|
@ -30,12 +31,13 @@ export class AllDayManager {
|
||||||
private allDayEventRenderer: AllDayEventRenderer;
|
private allDayEventRenderer: AllDayEventRenderer;
|
||||||
private eventManager: EventManager;
|
private eventManager: EventManager;
|
||||||
private dateService: DateService;
|
private dateService: DateService;
|
||||||
|
private dataSource: IColumnDataSource;
|
||||||
|
|
||||||
private layoutEngine: AllDayLayoutEngine | null = null;
|
private layoutEngine: AllDayLayoutEngine | null = null;
|
||||||
|
|
||||||
// State tracking for layout calculation
|
// State tracking for layout calculation
|
||||||
private currentAllDayEvents: ICalendarEvent[] = [];
|
private currentAllDayEvents: ICalendarEvent[] = [];
|
||||||
private currentWeekDates: IColumnBounds[] = [];
|
private currentColumns: IColumnBounds[] = [];
|
||||||
|
|
||||||
// Expand/collapse state
|
// Expand/collapse state
|
||||||
private isExpanded: boolean = false;
|
private isExpanded: boolean = false;
|
||||||
|
|
@ -45,11 +47,13 @@ export class AllDayManager {
|
||||||
constructor(
|
constructor(
|
||||||
eventManager: EventManager,
|
eventManager: EventManager,
|
||||||
allDayEventRenderer: AllDayEventRenderer,
|
allDayEventRenderer: AllDayEventRenderer,
|
||||||
dateService: DateService
|
dateService: DateService,
|
||||||
|
dataSource: IColumnDataSource
|
||||||
) {
|
) {
|
||||||
this.eventManager = eventManager;
|
this.eventManager = eventManager;
|
||||||
this.allDayEventRenderer = allDayEventRenderer;
|
this.allDayEventRenderer = allDayEventRenderer;
|
||||||
this.dateService = dateService;
|
this.dateService = dateService;
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
|
||||||
// Sync CSS variable with TypeScript constant to ensure consistency
|
// Sync CSS variable with TypeScript constant to ensure consistency
|
||||||
document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`);
|
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
|
// Recalculate layout WITHOUT the removed event to compress gaps
|
||||||
const remainingEvents = this.currentAllDayEvents.filter(e => e.id !== eventId);
|
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
|
// Re-render all-day events with compressed layout
|
||||||
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
||||||
|
|
@ -395,10 +399,18 @@ export class AllDayManager {
|
||||||
|
|
||||||
// Store current state
|
// Store current state
|
||||||
this.currentAllDayEvents = events;
|
this.currentAllDayEvents = events;
|
||||||
this.currentWeekDates = dayHeaders;
|
this.currentColumns = dayHeaders;
|
||||||
|
|
||||||
// Initialize layout engine with provided week dates
|
// Map IColumnBounds to IColumnInfo structure (identifier + groupId)
|
||||||
let layoutEngine = new AllDayLayoutEngine(dayHeaders.map(column => column.identifier));
|
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
|
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
|
||||||
return layoutEngine.calculateLayout(events);
|
return layoutEngine.calculateLayout(events);
|
||||||
|
|
@ -489,23 +501,43 @@ export class AllDayManager {
|
||||||
|
|
||||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||||
const eventId = clone.eventId.replace('clone-', '');
|
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
|
// Create new dates preserving time
|
||||||
const newStart = new Date(targetDate);
|
const newStart = new Date(targetDate);
|
||||||
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
|
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
|
||||||
|
|
||||||
const newEnd = new Date(targetDate);
|
const newEnd = new Date(targetDate);
|
||||||
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
|
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
|
||||||
|
|
||||||
// Update event in repository
|
// Build update payload
|
||||||
await this.eventManager.updateEvent(eventId, {
|
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
|
||||||
start: newStart,
|
start: newStart,
|
||||||
end: newEnd,
|
end: newEnd,
|
||||||
allDay: true
|
allDay: true
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (resourceId) {
|
||||||
|
updatePayload.resourceId = resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update event in repository
|
||||||
|
await this.eventManager.updateEvent(eventId, updatePayload);
|
||||||
|
|
||||||
// Remove original timed event
|
// Remove original timed event
|
||||||
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
||||||
|
|
@ -522,7 +554,7 @@ export class AllDayManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedEvents = [...this.currentAllDayEvents, newEvent];
|
const updatedEvents = [...this.currentAllDayEvents, newEvent];
|
||||||
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
|
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentColumns);
|
||||||
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
||||||
|
|
||||||
// Animate height
|
// Animate height
|
||||||
|
|
@ -537,25 +569,45 @@ export class AllDayManager {
|
||||||
|
|
||||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||||
const eventId = clone.eventId.replace('clone-', '');
|
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
|
// Calculate duration in days
|
||||||
const durationDays = this.dateService.differenceInCalendarDays(clone.end, clone.start);
|
const durationDays = this.dateService.differenceInCalendarDays(clone.end, clone.start);
|
||||||
|
|
||||||
// Create new dates preserving time
|
// Create new dates preserving time
|
||||||
const newStart = new Date(targetDate);
|
const newStart = new Date(targetDate);
|
||||||
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
|
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
|
||||||
|
|
||||||
const newEnd = new Date(targetDate);
|
const newEnd = new Date(targetDate);
|
||||||
newEnd.setDate(newEnd.getDate() + durationDays);
|
newEnd.setDate(newEnd.getDate() + durationDays);
|
||||||
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
|
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
|
||||||
|
|
||||||
// Update event in repository
|
// Build update payload
|
||||||
await this.eventManager.updateEvent(eventId, {
|
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
|
||||||
start: newStart,
|
start: newStart,
|
||||||
end: newEnd,
|
end: newEnd,
|
||||||
allDay: true
|
allDay: true
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (resourceId) {
|
||||||
|
updatePayload.resourceId = resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update event in repository
|
||||||
|
await this.eventManager.updateEvent(eventId, updatePayload);
|
||||||
|
|
||||||
// Remove original and fade out
|
// Remove original and fade out
|
||||||
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
||||||
|
|
@ -564,7 +616,7 @@ export class AllDayManager {
|
||||||
const updatedEvents = this.currentAllDayEvents.map(e =>
|
const updatedEvents = this.currentAllDayEvents.map(e =>
|
||||||
e.id === eventId ? { ...e, start: newStart, end: newEnd } : 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);
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
||||||
|
|
||||||
// Animate height - this also handles overflow classes!
|
// Animate height - this also handles overflow classes!
|
||||||
|
|
|
||||||
|
|
@ -457,12 +457,20 @@ export class DragDropManager {
|
||||||
if (!dropTarget)
|
if (!dropTarget)
|
||||||
throw "dropTarget is null";
|
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 = {
|
const dragEndPayload: IDragEndEventPayload = {
|
||||||
originalElement: this.originalElement,
|
originalElement: this.originalElement,
|
||||||
draggedClone: this.draggedClone,
|
draggedClone: this.draggedClone,
|
||||||
mousePosition,
|
mousePosition,
|
||||||
originalSourceColumn: this.originalSourceColumn!!,
|
originalSourceColumn: this.originalSourceColumn!!,
|
||||||
finalPosition: { column, snappedY }, // Where drag ended
|
finalPosition: { column, date, resourceId, snappedY },
|
||||||
target: dropTarget
|
target: dropTarget
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,30 +196,4 @@ export class EventManager {
|
||||||
return false;
|
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();
|
const columns = await this.dataSource.getColumns();
|
||||||
|
|
||||||
// Always create a fresh container for consistent behavior
|
// Always create a fresh container for consistent behavior
|
||||||
newGrid = this.gridRenderer.createNavigationGrid(container, columns);
|
newGrid = this.gridRenderer.createNavigationGrid(container, columns, targetWeek);
|
||||||
|
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export interface IColumnRenderer {
|
||||||
export interface IColumnRenderContext {
|
export interface IColumnRenderContext {
|
||||||
columns: IColumnInfo[];
|
columns: IColumnInfo[];
|
||||||
config: Configuration;
|
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');
|
const column = document.createElement('swp-day-column');
|
||||||
|
|
||||||
column.dataset.columnId = columnInfo.identifier;
|
column.dataset.columnId = columnInfo.identifier;
|
||||||
|
column.dataset.date = this.dateService.formatISODate(date);
|
||||||
|
|
||||||
// Apply work hours styling
|
// Apply work hours styling
|
||||||
this.applyWorkHoursToColumn(column, date);
|
this.applyWorkHoursToColumn(column, date);
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export class DateHeaderRenderer implements IHeaderRenderer {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
header.dataset.columnId = columnInfo.identifier;
|
header.dataset.columnId = columnInfo.identifier;
|
||||||
|
header.dataset.groupId = columnInfo.groupId;
|
||||||
|
|
||||||
calendarHeader.appendChild(header);
|
calendarHeader.appendChild(header);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ export class GridRenderer {
|
||||||
fragment.appendChild(timeAxis);
|
fragment.appendChild(timeAxis);
|
||||||
|
|
||||||
// Create grid container with caching
|
// Create grid container with caching
|
||||||
const gridContainer = this.createOptimizedGridContainer(columns);
|
const gridContainer = this.createOptimizedGridContainer(columns, currentDate);
|
||||||
this.cachedGridContainer = gridContainer;
|
this.cachedGridContainer = gridContainer;
|
||||||
fragment.appendChild(gridContainer);
|
fragment.appendChild(gridContainer);
|
||||||
|
|
||||||
|
|
@ -211,10 +211,12 @@ export class GridRenderer {
|
||||||
* - Column container - created here, populated by ColumnRenderer
|
* - Column container - created here, populated by ColumnRenderer
|
||||||
*
|
*
|
||||||
* @param columns - Array of columns to render (each column contains its events)
|
* @param columns - Array of columns to render (each column contains its events)
|
||||||
|
* @param currentDate - Current view date
|
||||||
* @returns Complete grid container element
|
* @returns Complete grid container element
|
||||||
*/
|
*/
|
||||||
private createOptimizedGridContainer(
|
private createOptimizedGridContainer(
|
||||||
columns: IColumnInfo[]
|
columns: IColumnInfo[],
|
||||||
|
currentDate: Date
|
||||||
): HTMLElement {
|
): HTMLElement {
|
||||||
const gridContainer = document.createElement('swp-grid-container');
|
const gridContainer = document.createElement('swp-grid-container');
|
||||||
|
|
||||||
|
|
@ -232,7 +234,7 @@ export class GridRenderer {
|
||||||
|
|
||||||
// Create column container
|
// Create column container
|
||||||
const columnContainer = document.createElement('swp-day-columns');
|
const columnContainer = document.createElement('swp-day-columns');
|
||||||
this.renderColumnContainer(columnContainer, columns);
|
this.renderColumnContainer(columnContainer, columns, currentDate);
|
||||||
timeGrid.appendChild(columnContainer);
|
timeGrid.appendChild(columnContainer);
|
||||||
|
|
||||||
scrollableContent.appendChild(timeGrid);
|
scrollableContent.appendChild(timeGrid);
|
||||||
|
|
@ -250,15 +252,18 @@ export class GridRenderer {
|
||||||
*
|
*
|
||||||
* @param columnContainer - Empty container to populate
|
* @param columnContainer - Empty container to populate
|
||||||
* @param columns - Array of columns to render (each column contains its events)
|
* @param columns - Array of columns to render (each column contains its events)
|
||||||
|
* @param currentDate - Current view date
|
||||||
*/
|
*/
|
||||||
private renderColumnContainer(
|
private renderColumnContainer(
|
||||||
columnContainer: HTMLElement,
|
columnContainer: HTMLElement,
|
||||||
columns: IColumnInfo[]
|
columns: IColumnInfo[],
|
||||||
|
currentDate: Date
|
||||||
): void {
|
): void {
|
||||||
// Delegate to ColumnRenderer
|
// Delegate to ColumnRenderer
|
||||||
this.columnRenderer.render(columnContainer, {
|
this.columnRenderer.render(columnContainer, {
|
||||||
columns: columns,
|
columns: columns,
|
||||||
config: this.config
|
config: this.config,
|
||||||
|
currentDate: currentDate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +288,7 @@ export class GridRenderer {
|
||||||
const columnContainer = grid.querySelector('swp-day-columns');
|
const columnContainer = grid.querySelector('swp-day-columns');
|
||||||
if (columnContainer) {
|
if (columnContainer) {
|
||||||
columnContainer.innerHTML = '';
|
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.
|
* Events will be rendered by EventRenderingService when GRID_RENDERED emits.
|
||||||
*
|
*
|
||||||
* @param parentContainer - Container for the new grid
|
* @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
|
* @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)
|
// 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
|
// Position new grid for animation - NO transform here, let Animation API handle it
|
||||||
newGrid.style.position = 'absolute';
|
newGrid.style.position = 'absolute';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { WorkHoursManager } from '../managers/WorkHoursManager';
|
import { WorkHoursManager } from '../managers/WorkHoursManager';
|
||||||
import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
|
import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
|
||||||
|
import { DateService } from '../utils/DateService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resource-based column renderer
|
* Resource-based column renderer
|
||||||
|
|
@ -10,13 +11,19 @@ import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
|
||||||
*/
|
*/
|
||||||
export class ResourceColumnRenderer implements IColumnRenderer {
|
export class ResourceColumnRenderer implements IColumnRenderer {
|
||||||
private workHoursManager: WorkHoursManager;
|
private workHoursManager: WorkHoursManager;
|
||||||
|
private dateService: DateService;
|
||||||
|
|
||||||
constructor(workHoursManager: WorkHoursManager) {
|
constructor(workHoursManager: WorkHoursManager, dateService: DateService) {
|
||||||
this.workHoursManager = workHoursManager;
|
this.workHoursManager = workHoursManager;
|
||||||
|
this.dateService = dateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(columnContainer: HTMLElement, context: IColumnRenderContext): void {
|
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
|
// Hardcoded work hours for all resources: 09:00 - 18:00
|
||||||
const workHours = { start: 9, end: 18 };
|
const workHours = { start: 9, end: 18 };
|
||||||
|
|
@ -25,6 +32,7 @@ export class ResourceColumnRenderer implements IColumnRenderer {
|
||||||
const column = document.createElement('swp-day-column');
|
const column = document.createElement('swp-day-column');
|
||||||
|
|
||||||
column.dataset.columnId = columnInfo.identifier;
|
column.dataset.columnId = columnInfo.identifier;
|
||||||
|
column.dataset.date = this.dateService.formatISODate(currentDate);
|
||||||
|
|
||||||
// Apply hardcoded work hours to all resource columns
|
// Apply hardcoded work hours to all resource columns
|
||||||
this.applyWorkHoursToColumn(column, workHours);
|
this.applyWorkHoursToColumn(column, workHours);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export class ResourceHeaderRenderer implements IHeaderRenderer {
|
||||||
|
|
||||||
header.dataset.columnId = columnInfo.identifier;
|
header.dataset.columnId = columnInfo.identifier;
|
||||||
header.dataset.resourceId = resource.id;
|
header.dataset.resourceId = resource.id;
|
||||||
|
header.dataset.groupId = columnInfo.groupId;
|
||||||
|
|
||||||
calendarHeader.appendChild(header);
|
calendarHeader.appendChild(header);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export interface IColumnInfo {
|
||||||
identifier: string; // "2024-11-13" (date mode) or "person-1" (resource mode)
|
identifier: string; // "2024-11-13" (date mode) or "person-1" (resource mode)
|
||||||
data: Date | IResource; // Date for date-mode, IResource for resource-mode
|
data: Date | IResource; // Date for date-mode, IResource for resource-mode
|
||||||
events: ICalendarEvent[]; // Events for this column (pre-filtered by datasource)
|
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;
|
setCurrentDate(date: Date): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current date
|
||||||
|
*/
|
||||||
|
getCurrentDate(): Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current view (day/week/month)
|
* Update the current view (day/week/month)
|
||||||
* @param view - The new calendar view
|
* @param view - The new calendar view
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ export interface IDragEndEventPayload {
|
||||||
originalSourceColumn: IColumnBounds; // Original column where drag started
|
originalSourceColumn: IColumnBounds; // Original column where drag started
|
||||||
finalPosition: {
|
finalPosition: {
|
||||||
column: IColumnBounds | null; // Where drag ended
|
column: IColumnBounds | null; // Where drag ended
|
||||||
|
date: Date; // Always present - the date for this position
|
||||||
|
resourceId?: string; // Only in resource mode
|
||||||
snappedY: number;
|
snappedY: number;
|
||||||
};
|
};
|
||||||
target: 'swp-day-column' | 'swp-day-header' | null;
|
target: 'swp-day-column' | 'swp-day-header' | null;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ICalendarEvent } from '../types/CalendarTypes';
|
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||||
|
import { IColumnInfo } from '../types/ColumnDataSource';
|
||||||
|
|
||||||
export interface IEventLayout {
|
export interface IEventLayout {
|
||||||
calenderEvent: ICalendarEvent;
|
calenderEvent: ICalendarEvent;
|
||||||
|
|
@ -10,11 +11,13 @@ export interface IEventLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AllDayLayoutEngine {
|
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[][];
|
private tracks: boolean[][];
|
||||||
|
|
||||||
constructor(weekDates: string[]) {
|
constructor(columns: IColumnInfo[]) {
|
||||||
this.weekDates = weekDates;
|
this.columnIdentifiers = columns.map(col => col.identifier);
|
||||||
|
this.columnGroups = columns.map(col => col.groupId);
|
||||||
this.tracks = [];
|
this.tracks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +28,7 @@ export class AllDayLayoutEngine {
|
||||||
|
|
||||||
let layouts: IEventLayout[] = [];
|
let layouts: IEventLayout[] = [];
|
||||||
// Reset tracks for new calculation
|
// 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
|
// Filter to only visible events
|
||||||
const visibleEvents = events.filter(event => this.isEventVisible(event));
|
const visibleEvents = events.filter(event => this.isEventVisible(event));
|
||||||
|
|
@ -70,7 +73,7 @@ export class AllDayLayoutEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new track if none available
|
// 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;
|
return this.tracks.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,42 +91,82 @@ export class AllDayLayoutEngine {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get start day index for event (1-based, 0 if not visible)
|
* 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 {
|
private getEventStartDay(event: ICalendarEvent): number {
|
||||||
const eventStartDate = this.formatDate(event.start);
|
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
|
// If event starts before visible range, clip to first visible day
|
||||||
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
|
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
|
||||||
|
|
||||||
const dayIndex = this.weekDates.indexOf(clippedStartDate);
|
const dayIndex = this.columnIdentifiers.indexOf(clippedStartDate);
|
||||||
return dayIndex >= 0 ? dayIndex + 1 : 0;
|
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)
|
* 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 {
|
private getEventEndDay(event: ICalendarEvent): number {
|
||||||
const eventEndDate = this.formatDate(event.end);
|
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
|
// If event ends after visible range, clip to last visible day
|
||||||
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
|
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
|
||||||
|
|
||||||
const dayIndex = this.weekDates.indexOf(clippedEndDate);
|
const dayIndex = this.columnIdentifiers.indexOf(clippedEndDate);
|
||||||
return dayIndex >= 0 ? dayIndex + 1 : 0;
|
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
|
* Check if event is visible in the current date range
|
||||||
*/
|
*/
|
||||||
private isEventVisible(event: ICalendarEvent): boolean {
|
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 eventStartDate = this.formatDate(event.start);
|
||||||
const eventEndDate = this.formatDate(event.end);
|
const eventEndDate = this.formatDate(event.end);
|
||||||
const firstVisibleDate = this.weekDates[0];
|
const firstVisibleDate = this.columnIdentifiers[0];
|
||||||
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
|
const lastVisibleDate = this.columnIdentifiers[this.columnIdentifiers.length - 1];
|
||||||
|
|
||||||
// Event overlaps if it doesn't end before visible range starts
|
// Event overlaps if it doesn't end before visible range starts
|
||||||
// AND doesn't start after visible range ends
|
// AND doesn't start after visible range ends
|
||||||
|
|
|
||||||
|
|
@ -4140,6 +4140,7 @@
|
||||||
{
|
{
|
||||||
"id": "RES-NOV25-001",
|
"id": "RES-NOV25-001",
|
||||||
"title": "Balayage kort hår",
|
"title": "Balayage kort hår",
|
||||||
|
"description": "Daily team sync - status updates",
|
||||||
"start": "2025-11-25T09:00:00Z",
|
"start": "2025-11-25T09:00:00Z",
|
||||||
"end": "2025-11-25T10:30:00Z",
|
"end": "2025-11-25T10:30:00Z",
|
||||||
"type": "customer",
|
"type": "customer",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue