diff --git a/src/datasources/DateColumnDataSource.ts b/src/datasources/DateColumnDataSource.ts index 42af446..a916e04 100644 --- a/src/datasources/DateColumnDataSource.ts +++ b/src/datasources/DateColumnDataSource.ts @@ -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 */ diff --git a/src/datasources/ResourceColumnDataSource.ts b/src/datasources/ResourceColumnDataSource.ts index 96d0b62..6d1df45 100644 --- a/src/datasources/ResourceColumnDataSource.ts +++ b/src/datasources/ResourceColumnDataSource.ts @@ -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 })) ); diff --git a/src/index.ts b/src/index.ts index 5757592..64e1a6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -141,7 +141,7 @@ async function initializeCalendar(): Promise { builder.registerType(MockAuditRepository).as>(); - let calendarMode = 'resource' ; + let calendarMode = 'date' ; // Register DataSource and HeaderRenderer based on mode if (calendarMode === 'resource') { builder.registerType(ResourceColumnDataSource).as(); diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index 6cee9cf..7f7c9ed 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -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! diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 209de3d..11c6952 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -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 }; diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 623ab8b..8310c59 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -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 { - 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); - } - } } diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index fbb64e6..ead6dd0 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -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(); diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts index 638cd88..a74c07a 100644 --- a/src/renderers/ColumnRenderer.ts +++ b/src/renderers/ColumnRenderer.ts @@ -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); diff --git a/src/renderers/DateHeaderRenderer.ts b/src/renderers/DateHeaderRenderer.ts index bc5eff8..d6584fa 100644 --- a/src/renderers/DateHeaderRenderer.ts +++ b/src/renderers/DateHeaderRenderer.ts @@ -53,6 +53,7 @@ export class DateHeaderRenderer implements IHeaderRenderer { `; header.dataset.columnId = columnInfo.identifier; + header.dataset.groupId = columnInfo.groupId; calendarHeader.appendChild(header); }); diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 81bc324..19f267b 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -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'; diff --git a/src/renderers/ResourceColumnRenderer.ts b/src/renderers/ResourceColumnRenderer.ts index 93fe6b6..627546d 100644 --- a/src/renderers/ResourceColumnRenderer.ts +++ b/src/renderers/ResourceColumnRenderer.ts @@ -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); diff --git a/src/renderers/ResourceHeaderRenderer.ts b/src/renderers/ResourceHeaderRenderer.ts index 296f74e..dd8bd29 100644 --- a/src/renderers/ResourceHeaderRenderer.ts +++ b/src/renderers/ResourceHeaderRenderer.ts @@ -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); }); diff --git a/src/types/ColumnDataSource.ts b/src/types/ColumnDataSource.ts index 6ecb563..6df14ea 100644 --- a/src/types/ColumnDataSource.ts +++ b/src/types/ColumnDataSource.ts @@ -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 diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index 6b5a37e..db5468e 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -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; diff --git a/src/utils/AllDayLayoutEngine.ts b/src/utils/AllDayLayoutEngine.ts index a43f8e3..31d5018 100644 --- a/src/utils/AllDayLayoutEngine.ts +++ b/src/utils/AllDayLayoutEngine.ts @@ -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 diff --git a/wwwroot/data/mock-events.json b/wwwroot/data/mock-events.json index de34e27..079c57b 100644 --- a/wwwroot/data/mock-events.json +++ b/wwwroot/data/mock-events.json @@ -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",