From 349e1e8293715549dd62d411cb56d7fe13f84424 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Fri, 31 Oct 2025 15:26:07 +0100 Subject: [PATCH] Refactor CalendarConfig to static configuration class Converts CalendarConfig to a pure static configuration management class with improved initialization and dependency handling Removes event bus dependencies and simplifies configuration loading Adds static methods for config management and initialization Improves flexibility and reduces class complexity --- src/core/CalendarConfig.ts | 512 ++--- src/index.ts | 37 +- src/managers/ConfigManager.ts | 118 + src/managers/EventManager.ts | 4 +- src/types/CalendarTypes.ts | 2 +- wwwroot/data/mock-events.json | 2809 ++++++++++++++++++++++++ wwwroot/data/mock-resource-events.json | 135 ++ 7 files changed, 3299 insertions(+), 318 deletions(-) create mode 100644 src/managers/ConfigManager.ts create mode 100644 wwwroot/data/mock-events.json create mode 100644 wwwroot/data/mock-resource-events.json diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts index fd07177..030c5e1 100644 --- a/src/core/CalendarConfig.ts +++ b/src/core/CalendarConfig.ts @@ -1,8 +1,7 @@ // Calendar configuration management +// Pure static configuration class - no dependencies, no events -import { eventBus } from './EventBus'; -import { CoreEvents } from '../constants/CoreEvents'; -import { CalendarConfig as ICalendarConfig, ViewPeriod, CalendarMode } from '../types/CalendarTypes'; +import { ICalendarConfig, ViewPeriod, CalendarMode } from '../types/CalendarTypes'; import { TimeFormatter, TimeFormatSettings } from '../utils/TimeFormatter'; /** @@ -86,376 +85,281 @@ interface TimeFormatConfig { } /** - * Calendar configuration management + * Calendar configuration management - Pure static config */ export class CalendarConfig { - private config: ICalendarConfig; - private calendarMode: CalendarMode = 'date'; - private selectedDate: Date | null = null; - private gridSettings: GridSettings; - private dateViewSettings: DateViewSettings; - private resourceViewSettings: ResourceViewSettings; - private currentWorkWeek: string = 'standard'; - private timeFormatConfig: TimeFormatConfig; + private static config: ICalendarConfig = { + // Scrollbar styling + scrollbarWidth: 16, // Width of scrollbar in pixels + scrollbarColor: '#666', // Scrollbar thumb color + scrollbarTrackColor: '#f0f0f0', // Scrollbar track color + scrollbarHoverColor: '#b53f7aff', // Scrollbar thumb hover color + scrollbarBorderRadius: 6, // Border radius for scrollbar thumb - constructor() { - this.config = { - // Scrollbar styling - scrollbarWidth: 16, // Width of scrollbar in pixels - scrollbarColor: '#666', // Scrollbar thumb color - scrollbarTrackColor: '#f0f0f0', // Scrollbar track color - scrollbarHoverColor: '#b53f7aff', // Scrollbar thumb hover color - scrollbarBorderRadius: 6, // Border radius for scrollbar thumb - - // Interaction settings - allowDrag: true, - allowResize: true, - allowCreate: true, - - // API settings - apiEndpoint: '/api/events', - dateFormat: 'YYYY-MM-DD', - timeFormat: 'HH:mm', - - // Feature flags - enableSearch: true, - enableTouch: true, - - // Event defaults - defaultEventDuration: 60, // Minutes - minEventDuration: 15, // Will be same as snapInterval - maxEventDuration: 480 // 8 hours - }; + // Interaction settings + allowDrag: true, + allowResize: true, + allowCreate: true, - // Grid display settings - this.gridSettings = { - hourHeight: 60, - dayStartHour: 0, - dayEndHour: 24, - workStartHour: 8, - workEndHour: 17, - snapInterval: 15, - gridStartThresholdMinutes: 30, // Events starting within ±15 min share grid columns - showCurrentTime: true, - showWorkHours: true, - fitToWidth: false, - scrollToHour: 8 - }; + // API settings + apiEndpoint: '/api/events', + dateFormat: 'YYYY-MM-DD', + timeFormat: 'HH:mm', - // Date view settings - this.dateViewSettings = { - period: 'week', - weekDays: 7, - firstDayOfWeek: 1, - showAllDay: true - }; + // Feature flags + enableSearch: true, + enableTouch: true, - // Resource view settings - this.resourceViewSettings = { - maxResources: 10, - showAvatars: true, - avatarSize: 32, - resourceNameFormat: 'full', - showResourceDetails: true, - showAllDay: true - }; + // Event defaults + defaultEventDuration: 60, // Minutes + minEventDuration: 15, // Will be same as snapInterval + maxEventDuration: 480 // 8 hours + }; - // Time format settings - default to Denmark with technical format - this.timeFormatConfig = { - timezone: 'Europe/Copenhagen', - use24HourFormat: true, - locale: 'da-DK', - dateFormat: 'technical', - showSeconds: false - }; + private static calendarMode: CalendarMode = 'date'; + private static selectedDate: Date | null = new Date(); + private static currentWorkWeek: string = 'standard'; - // Set computed values - this.config.minEventDuration = this.gridSettings.snapInterval; - - // Initialize TimeFormatter with default settings - TimeFormatter.configure(this.timeFormatConfig); - - // Load calendar type from URL parameter - this.loadCalendarType(); - - // Load from data attributes - this.loadFromDOM(); - } + // Grid display settings + private static gridSettings: GridSettings = { + hourHeight: 60, + dayStartHour: 0, + dayEndHour: 24, + workStartHour: 8, + workEndHour: 17, + snapInterval: 15, + gridStartThresholdMinutes: 30, // Events starting within ±15 min share grid columns + showCurrentTime: true, + showWorkHours: true, + fitToWidth: false, + scrollToHour: 8 + }; + + // Date view settings + private static dateViewSettings: DateViewSettings = { + period: 'week', + weekDays: 7, + firstDayOfWeek: 1, + showAllDay: true + }; + + // Resource view settings + private static resourceViewSettings: ResourceViewSettings = { + maxResources: 10, + showAvatars: true, + avatarSize: 32, + resourceNameFormat: 'full', + showResourceDetails: true, + showAllDay: true + }; + + // Time format settings - default to Denmark with technical format + private static timeFormatConfig: TimeFormatConfig = { + timezone: 'Europe/Copenhagen', + use24HourFormat: true, + locale: 'da-DK', + dateFormat: 'technical', + showSeconds: false + }; /** - * Load calendar type and date from URL parameters + * Initialize configuration - called once at startup */ - private loadCalendarType(): void { - const urlParams = new URLSearchParams(window.location.search); - const typeParam = urlParams.get('type'); - const dateParam = urlParams.get('date'); - - // Set calendar mode - if (typeParam === 'resource' || typeParam === 'date') { - this.calendarMode = typeParam; - } else { - this.calendarMode = 'date'; // Default - } - - // Set selected date - if (dateParam) { - const parsedDate = new Date(dateParam); - if (!isNaN(parsedDate.getTime())) { - this.selectedDate = parsedDate; - } else { - this.selectedDate = new Date(); - } - } else { - this.selectedDate = new Date(); // Default to today - } + static initialize(): void { + // Set computed values + CalendarConfig.config.minEventDuration = CalendarConfig.gridSettings.snapInterval; + + // Initialize TimeFormatter with default settings + TimeFormatter.configure(CalendarConfig.timeFormatConfig); + + // Load from data attributes + CalendarConfig.loadFromDOM(); } + /** * Load configuration from DOM data attributes */ - private loadFromDOM(): void { + private static loadFromDOM(): void { const calendar = document.querySelector('swp-calendar') as HTMLElement; if (!calendar) return; // Read data attributes const attrs = calendar.dataset; - + // Update date view settings - if (attrs.view) this.dateViewSettings.period = attrs.view as ViewPeriod; - if (attrs.weekDays) this.dateViewSettings.weekDays = parseInt(attrs.weekDays); - + if (attrs.view) CalendarConfig.dateViewSettings.period = attrs.view as ViewPeriod; + if (attrs.weekDays) CalendarConfig.dateViewSettings.weekDays = parseInt(attrs.weekDays); + // Update grid settings - if (attrs.snapInterval) this.gridSettings.snapInterval = parseInt(attrs.snapInterval); - if (attrs.dayStartHour) this.gridSettings.dayStartHour = parseInt(attrs.dayStartHour); - if (attrs.dayEndHour) this.gridSettings.dayEndHour = parseInt(attrs.dayEndHour); - if (attrs.hourHeight) this.gridSettings.hourHeight = parseInt(attrs.hourHeight); - if (attrs.fitToWidth !== undefined) this.gridSettings.fitToWidth = attrs.fitToWidth === 'true'; - + if (attrs.snapInterval) CalendarConfig.gridSettings.snapInterval = parseInt(attrs.snapInterval); + if (attrs.dayStartHour) CalendarConfig.gridSettings.dayStartHour = parseInt(attrs.dayStartHour); + if (attrs.dayEndHour) CalendarConfig.gridSettings.dayEndHour = parseInt(attrs.dayEndHour); + if (attrs.hourHeight) CalendarConfig.gridSettings.hourHeight = parseInt(attrs.hourHeight); + if (attrs.fitToWidth !== undefined) CalendarConfig.gridSettings.fitToWidth = attrs.fitToWidth === 'true'; + // Update computed values - this.config.minEventDuration = this.gridSettings.snapInterval; + CalendarConfig.config.minEventDuration = CalendarConfig.gridSettings.snapInterval; } /** * Get a config value */ - get(key: K): ICalendarConfig[K] { - return this.config[key]; + static get(key: K): ICalendarConfig[K] { + return CalendarConfig.config[key]; } /** - * Set a config value + * Set a config value (no events - use ConfigManager for updates with events) */ - set(key: K, value: ICalendarConfig[K]): void { - const oldValue = this.config[key]; - this.config[key] = value; - - // Update computed values handled in specific update methods - - // Emit config update event - eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key, - value, - oldValue - }); + static set(key: K, value: ICalendarConfig[K]): void { + CalendarConfig.config[key] = value; } /** - * Update multiple config values + * Update multiple config values (no events - use ConfigManager for updates with events) */ - update(updates: Partial): void { + static update(updates: Partial): void { Object.entries(updates).forEach(([key, value]) => { - this.set(key as keyof ICalendarConfig, value); + CalendarConfig.set(key as keyof ICalendarConfig, value); }); } /** * Get all config */ - getAll(): ICalendarConfig { - return { ...this.config }; + static getAll(): ICalendarConfig { + return { ...CalendarConfig.config }; } /** * Calculate derived values */ - - get minuteHeight(): number { - return this.gridSettings.hourHeight / 60; + + static get minuteHeight(): number { + return CalendarConfig.gridSettings.hourHeight / 60; } - get totalHours(): number { - return this.gridSettings.dayEndHour - this.gridSettings.dayStartHour; + static get totalHours(): number { + return CalendarConfig.gridSettings.dayEndHour - CalendarConfig.gridSettings.dayStartHour; } - get totalMinutes(): number { - return this.totalHours * 60; + static get totalMinutes(): number { + return CalendarConfig.totalHours * 60; } - get slotsPerHour(): number { - return 60 / this.gridSettings.snapInterval; + static get slotsPerHour(): number { + return 60 / CalendarConfig.gridSettings.snapInterval; } - get totalSlots(): number { - return this.totalHours * this.slotsPerHour; + static get totalSlots(): number { + return CalendarConfig.totalHours * CalendarConfig.slotsPerHour; } - get slotHeight(): number { - return this.gridSettings.hourHeight / this.slotsPerHour; + static get slotHeight(): number { + return CalendarConfig.gridSettings.hourHeight / CalendarConfig.slotsPerHour; } /** * Validate snap interval */ - isValidSnapInterval(interval: number): boolean { + static isValidSnapInterval(interval: number): boolean { return [5, 10, 15, 30, 60].includes(interval); } /** * Get grid display settings */ - getGridSettings(): GridSettings { - return { ...this.gridSettings }; + static getGridSettings(): GridSettings { + return { ...CalendarConfig.gridSettings }; } /** - * Update grid display settings + * Update grid display settings (no events - use ConfigManager for updates with events) */ - updateGridSettings(updates: Partial): void { - this.gridSettings = { ...this.gridSettings, ...updates }; - + static updateGridSettings(updates: Partial): void { + CalendarConfig.gridSettings = { ...CalendarConfig.gridSettings, ...updates }; + // Update computed values if (updates.snapInterval) { - this.config.minEventDuration = updates.snapInterval; + CalendarConfig.config.minEventDuration = updates.snapInterval; } - - // Grid settings changes trigger general refresh - avoid specific event - eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key: 'gridSettings', - value: this.gridSettings - }); } /** * Get date view settings */ - getDateViewSettings(): DateViewSettings { - return { ...this.dateViewSettings }; - } - - /** - * Update date view settings - */ - updateDateViewSettings(updates: Partial): void { - this.dateViewSettings = { ...this.dateViewSettings, ...updates }; - - // Date view settings changes trigger general refresh - avoid specific event - eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key: 'dateViewSettings', - value: this.dateViewSettings - }); + static getDateViewSettings(): DateViewSettings { + return { ...CalendarConfig.dateViewSettings }; } /** * Get resource view settings */ - getResourceViewSettings(): ResourceViewSettings { - return { ...this.resourceViewSettings }; + static getResourceViewSettings(): ResourceViewSettings { + return { ...CalendarConfig.resourceViewSettings }; } - /** - * Update resource view settings - */ - updateResourceViewSettings(updates: Partial): void { - this.resourceViewSettings = { ...this.resourceViewSettings, ...updates }; - - // Resource view settings changes trigger general refresh - avoid specific event - eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key: 'resourceViewSettings', - value: this.resourceViewSettings - }); - } - - /** - * Check if current mode is resource-based - */ - isResourceMode(): boolean { - return this.calendarMode === 'resource'; - } - - /** - * Check if current mode is date-based - */ - isDateMode(): boolean { - return this.calendarMode === 'date'; - } - /** * Get calendar mode */ - getCalendarMode(): CalendarMode { - return this.calendarMode; + static getCalendarMode(): CalendarMode { + return CalendarConfig.calendarMode; } /** * Set calendar mode */ - setCalendarMode(mode: CalendarMode): void { - const oldMode = this.calendarMode; - this.calendarMode = mode; - - // Emit calendar mode change event - eventBus.emit(CoreEvents.VIEW_CHANGED, { - oldType: oldMode, - newType: mode - }); + static setCalendarMode(mode: CalendarMode): void { + CalendarConfig.calendarMode = mode; } /** * Get selected date */ - getSelectedDate(): Date | null { - return this.selectedDate; + static getSelectedDate(): Date | null { + return CalendarConfig.selectedDate; } /** * Set selected date * Note: Does not emit events - caller is responsible for event emission */ - setSelectedDate(date: Date): void { - this.selectedDate = date; + static setSelectedDate(date: Date): void { + CalendarConfig.selectedDate = date; } /** * Get work week presets */ - private getWorkWeekPresets(): { [key: string]: WorkWeekSettings } { + private static getWorkWeekPresets(): { [key: string]: WorkWeekSettings } { return { - 'standard': { + 'standard': { id: 'standard', workDays: [1,2,3,4,5], // Monday-Friday (ISO) totalDays: 5, firstWorkDay: 1 }, - 'compressed': { + 'compressed': { id: 'compressed', workDays: [1,2,3,4], // Monday-Thursday (ISO) totalDays: 4, firstWorkDay: 1 }, - 'midweek': { + 'midweek': { id: 'midweek', workDays: [3,4,5], // Wednesday-Friday (ISO) totalDays: 3, firstWorkDay: 3 }, - 'weekend': { + 'weekend': { id: 'weekend', workDays: [6,7], // Saturday-Sunday (ISO) totalDays: 2, firstWorkDay: 6 }, - 'fullweek': { + 'fullweek': { id: 'fullweek', workDays: [1,2,3,4,5,6,7], // Monday-Sunday (ISO) totalDays: 7, @@ -467,109 +371,115 @@ export class CalendarConfig { /** * Get current work week settings */ - getWorkWeekSettings(): WorkWeekSettings { - const presets = this.getWorkWeekPresets(); - return presets[this.currentWorkWeek] || presets['standard']; + static getWorkWeekSettings(): WorkWeekSettings { + const presets = CalendarConfig.getWorkWeekPresets(); + return presets[CalendarConfig.currentWorkWeek] || presets['standard']; } /** * Set work week preset * Note: Does not emit events - caller is responsible for event emission */ - setWorkWeek(workWeekId: string): void { - const presets = this.getWorkWeekPresets(); + static setWorkWeek(workWeekId: string): void { + const presets = CalendarConfig.getWorkWeekPresets(); if (presets[workWeekId]) { - this.currentWorkWeek = workWeekId; + CalendarConfig.currentWorkWeek = workWeekId; // Update dateViewSettings to match work week - this.dateViewSettings.weekDays = presets[workWeekId].totalDays; + CalendarConfig.dateViewSettings.weekDays = presets[workWeekId].totalDays; } } /** * Get current work week ID */ - getCurrentWorkWeek(): string { - return this.currentWorkWeek; + static getCurrentWorkWeek(): string { + return CalendarConfig.currentWorkWeek; } /** * Get time format settings */ - getTimeFormatSettings(): TimeFormatConfig { - return { ...this.timeFormatConfig }; - } - - /** - * Update time format settings - */ - updateTimeFormatSettings(updates: Partial): void { - this.timeFormatConfig = { ...this.timeFormatConfig, ...updates }; - - // Update TimeFormatter with new settings - TimeFormatter.configure(this.timeFormatConfig); - - // Emit time format change event - eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key: 'timeFormatSettings', - value: this.timeFormatConfig - }); - } - - /** - * Set timezone (convenience method) - */ - setTimezone(timezone: string): void { - this.updateTimeFormatSettings({ timezone }); - } - - /** - * Set 12/24 hour format (convenience method) - */ - set24HourFormat(use24Hour: boolean): void { - this.updateTimeFormatSettings({ use24HourFormat: use24Hour }); + static getTimeFormatSettings(): TimeFormatConfig { + return { ...CalendarConfig.timeFormatConfig }; } /** * Get configured timezone */ - getTimezone(): string { - return this.timeFormatConfig.timezone; + static getTimezone(): string { + return CalendarConfig.timeFormatConfig.timezone; } /** * Get configured locale */ - getLocale(): string { - return this.timeFormatConfig.locale; + static getLocale(): string { + return CalendarConfig.timeFormatConfig.locale; } /** * Check if using 24-hour format */ - is24HourFormat(): boolean { - return this.timeFormatConfig.use24HourFormat; - } - - /** - * Set date format (convenience method) - */ - setDateFormat(format: 'locale' | 'technical'): void { - this.updateTimeFormatSettings({ dateFormat: format }); - } - - /** - * Set whether to show seconds (convenience method) - */ - setShowSeconds(show: boolean): void { - this.updateTimeFormatSettings({ showSeconds: show }); + static is24HourFormat(): boolean { + return CalendarConfig.timeFormatConfig.use24HourFormat; } /** * Get current date format */ - getDateFormat(): 'locale' | 'technical' { - return this.timeFormatConfig.dateFormat; + static getDateFormat(): 'locale' | 'technical' { + return CalendarConfig.timeFormatConfig.dateFormat; } + /** + * Load configuration from JSON + */ + static loadFromJSON(json: string): void { + try { + const data = JSON.parse(json); + if (data.gridSettings) CalendarConfig.updateGridSettings(data.gridSettings); + if (data.dateViewSettings) CalendarConfig.dateViewSettings = { ...CalendarConfig.dateViewSettings, ...data.dateViewSettings }; + if (data.resourceViewSettings) CalendarConfig.resourceViewSettings = { ...CalendarConfig.resourceViewSettings, ...data.resourceViewSettings }; + if (data.timeFormatConfig) { + CalendarConfig.timeFormatConfig = { ...CalendarConfig.timeFormatConfig, ...data.timeFormatConfig }; + TimeFormatter.configure(CalendarConfig.timeFormatConfig); + } + } catch (error) { + console.error('Failed to load config from JSON:', error); + } + } + + // ======================================================================== + // Instance method wrappers for backward compatibility + // These allow injected CalendarConfig to work with existing code + // ======================================================================== + + get(key: keyof ICalendarConfig) { return CalendarConfig.get(key); } + set(key: keyof ICalendarConfig, value: any) { return CalendarConfig.set(key, value); } + update(updates: Partial) { return CalendarConfig.update(updates); } + getAll() { return CalendarConfig.getAll(); } + get minuteHeight() { return CalendarConfig.minuteHeight; } + get totalHours() { return CalendarConfig.totalHours; } + get totalMinutes() { return CalendarConfig.totalMinutes; } + get slotsPerHour() { return CalendarConfig.slotsPerHour; } + get totalSlots() { return CalendarConfig.totalSlots; } + get slotHeight() { return CalendarConfig.slotHeight; } + isValidSnapInterval(interval: number) { return CalendarConfig.isValidSnapInterval(interval); } + getGridSettings() { return CalendarConfig.getGridSettings(); } + updateGridSettings(updates: Partial) { return CalendarConfig.updateGridSettings(updates); } + getDateViewSettings() { return CalendarConfig.getDateViewSettings(); } + getResourceViewSettings() { return CalendarConfig.getResourceViewSettings(); } + getCalendarMode() { return CalendarConfig.getCalendarMode(); } + setCalendarMode(mode: CalendarMode) { return CalendarConfig.setCalendarMode(mode); } + getSelectedDate() { return CalendarConfig.getSelectedDate(); } + setSelectedDate(date: Date) { return CalendarConfig.setSelectedDate(date); } + getWorkWeekSettings() { return CalendarConfig.getWorkWeekSettings(); } + setWorkWeek(workWeekId: string) { return CalendarConfig.setWorkWeek(workWeekId); } + getCurrentWorkWeek() { return CalendarConfig.getCurrentWorkWeek(); } + getTimeFormatSettings() { return CalendarConfig.getTimeFormatSettings(); } + getTimezone() { return CalendarConfig.getTimezone(); } + getLocale() { return CalendarConfig.getLocale(); } + is24HourFormat() { return CalendarConfig.is24HourFormat(); } + getDateFormat() { return CalendarConfig.getDateFormat(); } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a02be2d..69b1868 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,10 +19,11 @@ import { ResizeHandleManager } from './managers/ResizeHandleManager'; import { EdgeScrollManager } from './managers/EdgeScrollManager'; import { DragHoverManager } from './managers/DragHoverManager'; import { HeaderManager } from './managers/HeaderManager'; +import { ConfigManager } from './managers/ConfigManager'; // Import renderers -import { DateHeaderRenderer, type HeaderRenderer } from './renderers/HeaderRenderer'; -import { DateColumnRenderer, type ColumnRenderer } from './renderers/ColumnRenderer'; +import { DateHeaderRenderer, ResourceHeaderRenderer, type HeaderRenderer } from './renderers/HeaderRenderer'; +import { DateColumnRenderer, ResourceColumnRenderer, type ColumnRenderer } from './renderers/ColumnRenderer'; import { DateEventRenderer, type EventRendererStrategy } from './renderers/EventRenderer'; import { AllDayEventRenderer } from './renderers/AllDayEventRenderer'; import { GridRenderer } from './renderers/GridRenderer'; @@ -66,7 +67,8 @@ async function handleDeepLinking(eventManager: EventManager, urlManager: URLMana */ async function initializeCalendar(): Promise { try { - // Use the singleton calendar configuration + // Initialize static calendar configuration + CalendarConfig.initialize(); // Create NovaDI container const container = new Container(); @@ -75,20 +77,25 @@ async function initializeCalendar(): Promise { // Enable debug mode for development eventBus.setDebug(true); - builder.registerType(CalendarConfig).as().singleInstance(); + // Register CalendarConfig as singleton instance (static class, not instantiated) + builder.registerInstance(CalendarConfig).as(); + + // Register ConfigManager for event-driven config updates + builder.registerType(ConfigManager).as().singleInstance(); + // Bind core services as instances builder.registerInstance(eventBus).as(); - // Register renderers with keyed registration based on calendar mode - // Date mode renderers - builder.registerType(DateHeaderRenderer).as().keyed('date'); - builder.registerType(DateColumnRenderer).as().keyed('date'); - builder.registerType(DateEventRenderer).as().keyed('date'); - - // Resource mode renderers (using same renderers for now) - builder.registerType(DateHeaderRenderer).as().keyed('resource'); - builder.registerType(DateColumnRenderer).as().keyed('resource'); - builder.registerType(DateEventRenderer).as().keyed('resource'); + // Register renderers based on calendar mode + const calendarMode = CalendarConfig.getCalendarMode(); + if (calendarMode === 'resource') { + builder.registerType(ResourceHeaderRenderer).as().singleInstance(); + builder.registerType(ResourceColumnRenderer).as().singleInstance(); + } else { + builder.registerType(DateHeaderRenderer).as().singleInstance(); + builder.registerType(DateColumnRenderer).as().singleInstance(); + } + builder.registerType(DateEventRenderer).as().singleInstance(); // Register core services and utilities builder.registerType(DateService).as().singleInstance(); @@ -167,9 +174,11 @@ async function initializeCalendar(): Promise { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initializeCalendar().catch(error => { + console.error('Calendar initialization failed:', error); }); }); } else { initializeCalendar().catch(error => { + console.error('Calendar initialization failed:', error); }); } \ No newline at end of file diff --git a/src/managers/ConfigManager.ts b/src/managers/ConfigManager.ts new file mode 100644 index 0000000..f9920d2 --- /dev/null +++ b/src/managers/ConfigManager.ts @@ -0,0 +1,118 @@ +// Configuration manager - handles config updates with event emission +// Uses static CalendarConfig internally but adds event-driven updates + +import { IEventBus, ICalendarConfig } from '../types/CalendarTypes'; +import { CoreEvents } from '../constants/CoreEvents'; +import { CalendarConfig } from '../core/CalendarConfig'; + +/** + * Grid display settings interface (re-export from CalendarConfig) + */ +interface GridSettings { + dayStartHour: number; + dayEndHour: number; + workStartHour: number; + workEndHour: number; + hourHeight: number; + snapInterval: number; + fitToWidth: boolean; + scrollToHour: number | null; + gridStartThresholdMinutes: number; + showCurrentTime: boolean; + showWorkHours: boolean; +} + +/** + * ConfigManager - Handles configuration updates with event emission + * Wraps static CalendarConfig with event-driven functionality for DI system + */ +export class ConfigManager { + constructor(private eventBus: IEventBus) {} + + /** + * Set a config value and emit event + */ + set(key: K, value: ICalendarConfig[K]): void { + const oldValue = CalendarConfig.get(key); + CalendarConfig.set(key, value); + + // Emit config update event + this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { + key, + value, + oldValue + }); + } + + /** + * Update multiple config values and emit event + */ + update(updates: Partial): void { + Object.entries(updates).forEach(([key, value]) => { + this.set(key as keyof ICalendarConfig, value); + }); + } + + /** + * Update grid display settings and emit event + */ + updateGridSettings(updates: Partial): void { + CalendarConfig.updateGridSettings(updates); + + // Emit event after update + this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { + key: 'gridSettings', + value: CalendarConfig.getGridSettings() + }); + } + + /** + * Set selected date and emit event + */ + setSelectedDate(date: Date): void { + const oldDate = CalendarConfig.getSelectedDate(); + CalendarConfig.setSelectedDate(date); + + // Emit date change event if it actually changed + if (!oldDate || oldDate.getTime() !== date.getTime()) { + this.eventBus.emit(CoreEvents.DATE_CHANGED, { + date, + oldDate + }); + } + } + + /** + * Set work week and emit event + */ + setWorkWeek(workWeekId: string): void { + const oldWorkWeek = CalendarConfig.getCurrentWorkWeek(); + CalendarConfig.setWorkWeek(workWeekId); + + // Emit event if changed + if (oldWorkWeek !== workWeekId) { + this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { + key: 'workWeek', + value: workWeekId, + oldValue: oldWorkWeek + }); + } + } + + /** + * Set calendar mode and emit event + */ + setCalendarMode(mode: 'date' | 'resource'): void { + const oldMode = CalendarConfig.getCalendarMode(); + CalendarConfig.setCalendarMode(mode); + + // Emit event if changed + if (oldMode !== mode) { + this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { + key: 'calendarMode', + value: mode, + oldValue: oldMode + }); + } + } +} diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 66647ee..640cb43 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -57,8 +57,8 @@ export class EventManager { private async loadMockData(): Promise { const calendarType = this.config.getCalendarMode(); const jsonFile = calendarType === 'resource' - ? '/src/data/mock-resource-events.json' - : '/src/data/mock-events.json'; + ? '/data/mock-resource-events.json' + : '/data/mock-events.json'; const response = await fetch(jsonFile); if (!response.ok) { diff --git a/src/types/CalendarTypes.ts b/src/types/CalendarTypes.ts index b6166ea..2aeb070 100644 --- a/src/types/CalendarTypes.ts +++ b/src/types/CalendarTypes.ts @@ -50,7 +50,7 @@ export interface CalendarEvent { metadata?: Record; } -export interface CalendarConfig { +export interface ICalendarConfig { // Scrollbar styling scrollbarWidth: number; // Width of scrollbar in pixels scrollbarColor: string; // Scrollbar thumb color diff --git a/wwwroot/data/mock-events.json b/wwwroot/data/mock-events.json new file mode 100644 index 0000000..a04b946 --- /dev/null +++ b/wwwroot/data/mock-events.json @@ -0,0 +1,2809 @@ +[ + { + "id": "1", + "title": "Team Standup", + "start": "2025-07-07T05:00:00Z", + "end": "2025-07-07T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "2", + "title": "Sprint Planning", + "start": "2025-07-07T06:00:00Z", + "end": "2025-07-07T07:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#673ab7" + } + }, + { + "id": "3", + "title": "Development Session", + "start": "2025-07-07T10:00:00Z", + "end": "2025-07-07T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "4", + "title": "Team Standup", + "start": "2025-07-08T05:00:00Z", + "end": "2025-07-08T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "5", + "title": "Client Review", + "start": "2025-07-08T11:00:00Z", + "end": "2025-07-08T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "6", + "title": "Team Standup", + "start": "2025-07-09T05:00:00Z", + "end": "2025-07-09T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "7", + "title": "Deep Work Session", + "start": "2025-07-09T06:00:00Z", + "end": "2025-07-09T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#3f51b5" + } + }, + { + "id": "8", + "title": "Architecture Review", + "start": "2025-07-09T10:00:00Z", + "end": "2025-07-09T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#009688" + } + }, + { + "id": "9", + "title": "Team Standup", + "start": "2025-07-10T05:00:00Z", + "end": "2025-07-10T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "10", + "title": "Lunch & Learn", + "start": "2025-07-10T08:00:00Z", + "end": "2025-07-10T09:00:00Z", + "type": "meal", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff9800" + } + }, + { + "id": "11", + "title": "Team Standup", + "start": "2025-07-11T05:00:00Z", + "end": "2025-07-11T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "12", + "title": "Sprint Review", + "start": "2025-07-11T10:00:00Z", + "end": "2025-07-11T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#607d8b" + } + }, + { + "id": "13", + "title": "Weekend Project", + "start": "2025-07-12T06:00:00Z", + "end": "2025-07-12T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#f44336" + } + }, + { + "id": "14", + "title": "Team Standup", + "start": "2025-07-14T05:00:00Z", + "end": "2025-07-14T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "15", + "title": "Code Reviews", + "start": "2025-07-14T14:00:00Z", + "end": "2025-07-14T23:59:59Z", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#009688" + } + }, + { + "id": "16", + "title": "Team Standup", + "start": "2025-07-15T05:00:00Z", + "end": "2025-07-15T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "17", + "title": "Product Demo", + "start": "2025-07-15T11:00:00Z", + "end": "2025-07-15T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#e91e63" + } + }, + { + "id": "18", + "title": "Team Standup", + "start": "2025-07-16T05:00:00Z", + "end": "2025-07-16T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "19", + "title": "Workshop: New Technologies", + "start": "2025-07-16T10:00:00Z", + "end": "2025-07-16T13:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#9c27b0" + } + }, + { + "id": "20", + "title": "Team Standup", + "start": "2025-07-17T05:00:00Z", + "end": "2025-07-17T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "21", + "title": "Deadline: Feature Release", + "start": "2025-07-17T13:00:00Z", + "end": "2025-07-17T13:00:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 0, + "color": "#f44336" + } + }, + { + "id": "22", + "title": "Team Standup", + "start": "2025-07-18T05:00:00Z", + "end": "2025-07-18T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "23", + "title": "Summer Team Event", + "start": "2025-07-18T00:00:00Z", + "end": "2025-07-17T23:59:59Z", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 1440, + "color": "#4caf50" + } + }, + { + "id": "24", + "title": "Team Standup", + "start": "2025-07-21T05:00:00Z", + "end": "2025-07-21T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "25", + "title": "Sprint Planning", + "start": "2025-07-21T06:00:00Z", + "end": "2025-07-21T07:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#673ab7" + } + }, + { + "id": "26", + "title": "Team Standup", + "start": "2025-07-22T05:00:00Z", + "end": "2025-07-22T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "27", + "title": "Client Meeting", + "start": "2025-07-22T10:00:00Z", + "end": "2025-07-22T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#cddc39" + } + }, + { + "id": "28", + "title": "Team Standup", + "start": "2025-07-23T05:00:00Z", + "end": "2025-07-23T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "29", + "title": "Performance Review", + "start": "2025-07-23T07:00:00Z", + "end": "2025-07-23T08:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "30", + "title": "Team Standup", + "start": "2025-07-24T05:00:00Z", + "end": "2025-07-24T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "31", + "title": "Technical Discussion", + "start": "2025-07-24T11:00:00Z", + "end": "2025-07-24T12:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#3f51b5" + } + }, + { + "id": "32", + "title": "Team Standup", + "start": "2025-07-25T05:00:00Z", + "end": "2025-07-25T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "33", + "title": "Sprint Review", + "start": "2025-07-25T10:00:00Z", + "end": "2025-07-25T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#607d8b" + } + }, + { + "id": "34", + "title": "Team Standup", + "start": "2025-07-28T05:00:00Z", + "end": "2025-07-28T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "35", + "title": "Monthly Planning", + "start": "2025-07-28T06:00:00Z", + "end": "2025-07-28T08:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#9c27b0" + } + }, + { + "id": "36", + "title": "Team Standup", + "start": "2025-07-29T05:00:00Z", + "end": "2025-07-29T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "37", + "title": "Development Work", + "start": "2025-07-29T10:00:00Z", + "end": "2025-07-29T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "38", + "title": "Team Standup", + "start": "2025-07-30T05:00:00Z", + "end": "2025-07-30T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "39", + "title": "Security Review", + "start": "2025-07-30T11:00:00Z", + "end": "2025-07-30T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#f44336" + } + }, + { + "id": "40", + "title": "Team Standup", + "start": "2025-07-31T05:00:00Z", + "end": "2025-07-31T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "41", + "title": "Month End Review", + "start": "2025-07-31T10:00:00Z", + "end": "2025-07-31T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#795548" + } + }, + { + "id": "42", + "title": "Team Standup", + "start": "2025-08-01T05:00:00Z", + "end": "2025-08-01T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "43", + "title": "August Kickoff", + "start": "2025-08-01T06:00:00Z", + "end": "2025-08-01T07:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4caf50" + } + }, + { + "id": "44", + "title": "Weekend Planning", + "start": "2025-08-03T06:00:00Z", + "end": "2025-08-03T07:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#9c27b0" + } + }, + { + "id": "45", + "title": "Team Standup", + "start": "2025-08-04T05:00:00Z", + "end": "2025-08-04T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "46", + "title": "Project Kickoff", + "start": "2025-08-04T10:00:00Z", + "end": "2025-08-04T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#e91e63" + } + }, + { + "id": "47", + "title": "Company Holiday", + "start": "2025-08-04T00:00:00Z", + "end": "2025-08-04T23:59:59Z", + "type": "milestone", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 1440, + "color": "#4caf50" + } + }, + { + "id": "48", + "title": "Deep Work Session", + "start": "2025-08-05T06:00:00Z", + "end": "2025-08-05T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#3f51b5" + } + }, + { + "id": "49", + "title": "Lunch Meeting", + "start": "2025-08-05T08:30:00Z", + "end": "2025-08-05T09:30:00Z", + "type": "meal", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff9800" + } + }, + { + "id": "50", + "title": "Early Morning Workout", + "start": "2025-08-05T02:00:00Z", + "end": "2025-08-05T03:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#00bcd4" + } + }, + { + "id": "51", + "title": "Client Review", + "start": "2025-08-06T11:00:00Z", + "end": "2025-08-06T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "52", + "title": "Late Evening Call", + "start": "2025-08-06T17:00:00Z", + "end": "2025-08-06T18:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#673ab7" + } + }, + { + "id": "53", + "title": "Team Building Event", + "start": "2025-08-06T00:00:00Z", + "end": "2025-08-05T23:59:59Z", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 1440, + "color": "#2196f3" + } + }, + { + "id": "54", + "title": "Sprint Planning", + "start": "2025-08-07T05:00:00Z", + "end": "2025-08-07T06:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#607d8b" + } + }, + { + "id": "55", + "title": "Code Review", + "start": "2025-08-07T10:00:00Z", + "end": "2025-08-07T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#009688" + } + }, + { + "id": "56", + "title": "Midnight Deployment", + "start": "2025-08-07T19:00:00Z", + "end": "2025-08-07T21:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#ffc107" + } + }, + { + "id": "57", + "title": "Team Standup", + "start": "2025-08-08T05:00:00Z", + "end": "2025-08-08T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#8bc34a" + } + }, + { + "id": "58", + "title": "Client Meeting", + "start": "2025-08-08T10:00:00Z", + "end": "2025-08-08T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#cddc39" + } + }, + { + "id": "59", + "title": "Weekend Project", + "start": "2025-08-09T06:00:00Z", + "end": "2025-08-09T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#f44336" + } + }, + { + "id": "60", + "title": "Team Standup", + "start": "2025-08-11T05:00:00Z", + "end": "2025-08-11T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "61", + "title": "Sprint Planning", + "start": "2025-08-11T06:00:00Z", + "end": "2025-08-11T07:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#673ab7" + } + }, + { + "id": "62", + "title": "Team Standup", + "start": "2025-08-12T05:00:00Z", + "end": "2025-08-12T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "63", + "title": "Technical Workshop", + "start": "2025-08-12T10:00:00Z", + "end": "2025-08-12T13:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#9c27b0" + } + }, + { + "id": "64", + "title": "Team Standup", + "start": "2025-08-13T05:00:00Z", + "end": "2025-08-13T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "65", + "title": "Development Session", + "start": "2025-08-13T06:00:00Z", + "end": "2025-08-13T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#2196f3" + } + }, + { + "id": "66", + "title": "Team Standup", + "start": "2025-08-14T05:00:00Z", + "end": "2025-08-14T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "67", + "title": "Client Presentation", + "start": "2025-08-14T11:00:00Z", + "end": "2025-08-14T12:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#e91e63" + } + }, + { + "id": "68", + "title": "Team Standup", + "start": "2025-08-15T05:00:00Z", + "end": "2025-08-15T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "69", + "title": "Sprint Review", + "start": "2025-08-15T10:00:00Z", + "end": "2025-08-15T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#607d8b" + } + }, + { + "id": "70", + "title": "Summer Festival", + "start": "2025-08-14T00:00:00Z", + "end": "2025-08-15T23:59:59Z", + "type": "milestone", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 2880, + "color": "#4caf50" + } + }, + { + "id": "71", + "title": "Team Standup", + "start": "2025-08-18T05:00:00Z", + "end": "2025-08-18T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "72", + "title": "Strategy Meeting", + "start": "2025-08-18T06:00:00Z", + "end": "2025-08-18T08:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#9c27b0" + } + }, + { + "id": "73", + "title": "Team Standup", + "start": "2025-08-19T05:00:00Z", + "end": "2025-08-19T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "74", + "title": "Development Work", + "start": "2025-08-19T10:00:00Z", + "end": "2025-08-19T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#3f51b5" + } + }, + { + "id": "75", + "title": "Team Standup", + "start": "2025-08-20T05:00:00Z", + "end": "2025-08-20T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "76", + "title": "Architecture Planning", + "start": "2025-08-20T11:00:00Z", + "end": "2025-08-20T12:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#009688" + } + }, + { + "id": "77", + "title": "Team Standup", + "start": "2025-08-21T05:00:00Z", + "end": "2025-08-21T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "78", + "title": "Product Review", + "start": "2025-08-21T10:00:00Z", + "end": "2025-08-21T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "79", + "title": "Team Standup", + "start": "2025-08-22T05:00:00Z", + "end": "2025-08-22T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "80", + "title": "End of Sprint", + "start": "2025-08-22T12:00:00Z", + "end": "2025-08-22T13:00:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#f44336" + } + }, + { + "id": "81", + "title": "Team Standup", + "start": "2025-08-25T05:00:00Z", + "end": "2025-08-25T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "82", + "title": "Sprint Planning", + "start": "2025-08-25T06:00:00Z", + "end": "2025-08-25T07:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#673ab7" + } + }, + { + "id": "83", + "title": "Team Standup", + "start": "2025-08-26T05:00:00Z", + "end": "2025-08-26T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "84", + "title": "Design Review", + "start": "2025-08-26T10:00:00Z", + "end": "2025-08-26T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#e91e63" + } + }, + { + "id": "85", + "title": "Team Standup", + "start": "2025-08-27T05:00:00Z", + "end": "2025-08-27T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "86", + "title": "Development Session", + "start": "2025-08-27T06:00:00Z", + "end": "2025-08-27T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#2196f3" + } + }, + { + "id": "87", + "title": "Team Standup", + "start": "2025-08-28T05:00:00Z", + "end": "2025-08-28T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "88", + "title": "Customer Call", + "start": "2025-08-28T11:00:00Z", + "end": "2025-08-28T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#cddc39" + } + }, + { + "id": "89", + "title": "Team Standup", + "start": "2025-08-29T05:00:00Z", + "end": "2025-08-29T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "90", + "title": "Monthly Review", + "start": "2025-08-29T10:00:00Z", + "end": "2025-08-29T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#795548" + } + }, + { + "id": "91", + "title": "Team Standup", + "start": "2025-09-01T05:00:00Z", + "end": "2025-09-01T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "92", + "title": "September Kickoff", + "start": "2025-09-01T06:00:00Z", + "end": "2025-09-01T07:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4caf50" + } + }, + { + "id": "93", + "title": "Team Standup", + "start": "2025-09-02T05:00:00Z", + "end": "2025-09-02T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "94", + "title": "Product Planning", + "start": "2025-09-02T10:00:00Z", + "end": "2025-09-02T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#9c27b0" + } + }, + { + "id": "95", + "title": "Team Standup", + "start": "2025-09-03T05:00:00Z", + "end": "2025-09-03T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "96", + "title": "Deep Work", + "start": "2025-09-02T11:00:00Z", + "end": "2025-09-02T11:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#3f51b5" + } + }, + { + "id": "97", + "title": "Team Standup", + "start": "2025-09-04T05:00:00Z", + "end": "2025-09-04T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "98", + "title": "Technical Review", + "start": "2025-09-04T11:00:00Z", + "end": "2025-09-04T12:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#009688" + } + }, + { + "id": "99", + "title": "Team Standup", + "start": "2025-09-05T05:00:00Z", + "end": "2025-09-05T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "100", + "title": "Sprint Review", + "start": "2025-09-04T11:00:00Z", + "end": "2025-09-04T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#607d8b" + } + }, + { + "id": "101", + "title": "Weekend Workshop", + "start": "2025-09-06T06:00:00Z", + "end": "2025-09-06T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#f44336" + } + }, + { + "id": "102", + "title": "Team Standup", + "start": "2025-09-08T05:00:00Z", + "end": "2025-09-08T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "103", + "title": "Sprint Planning", + "start": "2025-09-08T06:00:00Z", + "end": "2025-09-08T07:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#673ab7" + } + }, + { + "id": "104", + "title": "Team Standup", + "start": "2025-09-09T05:00:00Z", + "end": "2025-09-09T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "105", + "title": "Client Workshop", + "start": "2025-09-09T10:00:00Z", + "end": "2025-09-09T13:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#e91e63" + } + }, + { + "id": "106", + "title": "Team Standup", + "start": "2025-09-10T05:00:00Z", + "end": "2025-09-10T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "107", + "title": "Development Work", + "start": "2025-09-10T06:00:00Z", + "end": "2025-09-10T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#2196f3" + } + }, + { + "id": "108", + "title": "Team Standup", + "start": "2025-09-11T05:00:00Z", + "end": "2025-09-11T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "109", + "title": "Performance Review", + "start": "2025-09-11T11:00:00Z", + "end": "2025-09-11T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "110", + "title": "Team Standup", + "start": "2025-09-12T05:00:00Z", + "end": "2025-09-12T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "111", + "title": "Q3 Review", + "start": "2025-09-12T10:00:00Z", + "end": "2025-09-12T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#9c27b0" + } + }, + { + "id": "112", + "title": "Autumn Equinox", + "start": "2025-09-23T00:00:00Z", + "end": "2025-09-22T23:59:59Z", + "type": "milestone", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 1440, + "color": "#ff6f00" + } + }, + { + "id": "113", + "title": "Team Standup", + "start": "2025-09-15T05:00:00Z", + "end": "2025-09-15T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "114", + "title": "Weekly Planning", + "start": "2025-09-15T06:00:00Z", + "end": "2025-09-15T07:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#3f51b5" + } + }, + { + "id": "115", + "title": "Team Standup", + "start": "2025-09-16T05:00:00Z", + "end": "2025-09-16T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "116", + "title": "Feature Demo", + "start": "2025-09-16T11:00:00Z", + "end": "2025-09-16T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#cddc39" + } + }, + { + "id": "117", + "title": "Team Standup", + "start": "2025-09-17T05:00:00Z", + "end": "2025-09-17T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "118", + "title": "Code Refactoring", + "start": "2025-09-17T06:00:00Z", + "end": "2025-09-17T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#009688" + } + }, + { + "id": "119", + "title": "Team Standup", + "start": "2025-09-18T05:00:00Z", + "end": "2025-09-18T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "120", + "title": "End of Sprint", + "start": "2025-09-19T12:00:00Z", + "end": "2025-09-19T13:00:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#f44336" + } + }, + { + "id": "121", + "title": "Azure Setup", + "start": "2025-09-10T06:30:00Z", + "end": "2025-09-10T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#2196f3" + } + }, + { + "id": "122", + "title": "Multi-Day Conference", + "start": "2025-09-22T00:00:00Z", + "end": "2025-09-23T23:59:59Z", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#4caf50" + } + }, + { + "id": "123", + "title": "Project Sprint", + "start": "2025-09-23T00:00:00Z", + "end": "2025-09-24T23:59:59Z", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#2196f3" + } + }, + { + "id": "124", + "title": "Training Week", + "start": "2025-09-29T00:00:00Z", + "end": "2025-10-02T23:59:59Z", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 7200, + "color": "#9c27b0" + } + }, + { + "id": "125", + "title": "Holiday Weekend", + "start": "2025-10-04T00:00:00Z", + "end": "2025-10-05T23:59:59Z", + "type": "milestone", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#ff6f00" + } + }, + { + "id": "126", + "title": "Client Visit", + "start": "2025-10-07T00:00:00Z", + "end": "2025-10-08T23:59:59Z", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#e91e63" + } + }, + { + "id": "127", + "title": "Development Marathon", + "start": "2025-10-13T00:00:00Z", + "end": "2025-10-14T23:59:59Z", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#3f51b5" + } + }, + { + "id": "128", + "title": "Morgen Standup", + "start": "2025-09-22T05:00:00Z", + "end": "2025-09-22T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "129", + "title": "Klient Præsentation", + "start": "2025-09-22T10:00:00Z", + "end": "2025-09-22T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#e91e63" + } + }, + { + "id": "130", + "title": "Eftermiddags Kodning", + "start": "2025-09-22T12:00:00Z", + "end": "2025-09-22T14:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#2196f3" + } + }, + { + "id": "131", + "title": "Team Standup", + "start": "2025-09-23T05:00:00Z", + "end": "2025-09-23T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "132", + "title": "Arkitektur Review", + "start": "2025-09-23T07:00:00Z", + "end": "2025-09-23T08:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#009688" + } + }, + { + "id": "133", + "title": "Frokost & Læring", + "start": "2025-09-23T08:30:00Z", + "end": "2025-09-23T09:30:00Z", + "type": "meal", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff9800" + } + }, + { + "id": "134", + "title": "Team Standup", + "start": "2025-09-24T05:00:00Z", + "end": "2025-09-24T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "135", + "title": "Database Optimering", + "start": "2025-09-24T06:00:00Z", + "end": "2025-09-24T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#3f51b5" + } + }, + { + "id": "136", + "title": "Klient Opkald", + "start": "2025-09-24T11:00:00Z", + "end": "2025-09-24T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "137", + "title": "Team Standup", + "start": "2025-09-25T05:00:00Z", + "end": "2025-09-25T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "138", + "title": "Sprint Review", + "start": "2025-09-25T10:00:00Z", + "end": "2025-09-25T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#607d8b" + } + }, + { + "id": "139", + "title": "Retrospektiv", + "start": "2025-09-25T11:30:00Z", + "end": "2025-09-25T12:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#9c27b0" + } + }, + { + "id": "140", + "title": "Team Standup", + "start": "2025-09-26T05:00:00Z", + "end": "2025-09-26T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "141", + "title": "Ny Feature Udvikling", + "start": "2025-09-26T06:00:00Z", + "end": "2025-09-26T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#4caf50" + } + }, + { + "id": "142", + "title": "Sikkerhedsgennemgang", + "start": "2025-09-26T10:00:00Z", + "end": "2025-09-26T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#f44336" + } + }, + { + "id": "143", + "title": "Weekend Hackathon", + "start": "2025-09-27T00:00:00Z", + "end": "2025-09-27T23:59:59Z", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 2880, + "color": "#673ab7" + } + }, + { + "id": "144", + "title": "Team Standup", + "start": "2025-09-29T07:30:00Z", + "end": "2025-09-29T08:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "145", + "title": "Månedlig Planlægning", + "start": "2025-09-29T07:00:00Z", + "end": "2025-09-29T08:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#9c27b0" + } + }, + { + "id": "146", + "title": "Performance Test", + "start": "2025-09-29T08:15:00Z", + "end": "2025-09-29T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#00bcd4" + } + }, + { + "id": "147", + "title": "Team Standup", + "start": "2025-09-30T05:00:00Z", + "end": "2025-09-30T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "148", + "title": "Kvartal Afslutning", + "start": "2025-09-30T11:00:00Z", + "end": "2025-09-30T13:00:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#f44336" + } + },{ + "id": "1481", + "title": "Kvartal Afslutning 2", + "start": "2025-09-30T11:20:00Z", + "end": "2025-09-30T13:00:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#f44336" + } + }, + { + "id": "149", + "title": "Oktober Kickoff", + "start": "2025-10-01T05:00:00Z", + "end": "2025-10-01T06:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4caf50" + } + }, + { + "id": "150", + "title": "Sprint Planlægning", + "start": "2025-10-01T06:30:00Z", + "end": "2025-10-01T08:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#673ab7" + } + }, + { + "id": "151", + "title": "Eftermiddags Kodning", + "start": "2025-10-01T10:00:00Z", + "end": "2025-10-01T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "1511", + "title": "Eftermiddags Kodning", + "start": "2025-10-01T10:30:00Z", + "end": "2025-10-01T11:00:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "1512", + "title": "Eftermiddags Kodning", + "start": "2025-10-01T11:30:00Z", + "end": "2025-10-01T12:30:00Z", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "1513", + "title": "Eftermiddags Kodning", + "start": "2025-10-01T12:00:00Z", + "end": "2025-10-01T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "1514", + "title": "Eftermiddags Kodning 2", + "start": "2025-10-01T12:00:00Z", + "end": "2025-10-01T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "152", + "title": "Team Standup", + "start": "2025-10-02T05:00:00Z", + "end": "2025-10-02T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "153", + "title": "API Design Workshop", + "start": "2025-10-02T07:00:00Z", + "end": "2025-10-02T08:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#009688" + } + }, + { + "id": "154", + "title": "Bug Fixing Session", + "start": "2025-10-02T07:00:00Z", + "end": "2025-10-02T09:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#ff5722" + } + }, + { + "id": "155", + "title": "Team Standup", + "start": "2025-10-03T05:00:00Z", + "end": "2025-10-03T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "156", + "title": "Klient Demo", + "start": "2025-10-03T10:00:00Z", + "end": "2025-10-03T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#e91e63" + } + }, + { + "id": "157", + "title": "Code Review Session", + "start": "2025-10-03T12:00:00Z", + "end": "2025-10-03T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#009688" + } + }, + { + "id": "158", + "title": "Fredag Standup", + "start": "2025-10-04T05:00:00Z", + "end": "2025-10-04T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "159", + "title": "Uge Retrospektiv", + "start": "2025-10-04T11:00:00Z", + "end": "2025-10-04T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#9c27b0" + } + }, + { + "id": "160", + "title": "Weekend Projekt", + "start": "2025-10-05T06:00:00Z", + "end": "2025-10-05T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 240, + "color": "#3f51b5" + } + }, + { + "id": "161", + "title": "Teknisk Workshop", + "start": "2025-09-24T00:00:00Z", + "end": "2025-09-25T23:59:59Z", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#795548" + } + }, + { + "id": "162", + "title": "Produktudvikling Sprint", + "start": "2025-10-01T08:00:00Z", + "end": "2025-10-02T21:00:00Z", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { + "duration": 4320, + "color": "#cddc39" + } + }, + { + "id": "163", + "title": "Tidlig Morgen Træning", + "start": "2025-09-23T02:30:00Z", + "end": "2025-09-23T03:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#00bcd4" + } + }, + { + "id": "164", + "title": "Sen Aften Deploy", + "start": "2025-09-25T18:00:00Z", + "end": "2025-09-25T20:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 150, + "color": "#ffc107" + } + }, + { + "id": "165", + "title": "Overlappende Møde A", + "start": "2025-09-30T06:00:00Z", + "end": "2025-09-30T07:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#8bc34a" + } + }, + { + "id": "166", + "title": "Overlappende Møde B", + "start": "2025-09-30T06:30:00Z", + "end": "2025-09-30T08:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#ff6f00" + } + }, + { + "id": "167", + "title": "Kort Check-in", + "start": "2025-10-02T05:45:00Z", + "end": "2025-10-02T06:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 15, + "color": "#607d8b" + } + }, + { + "id": "168", + "title": "Lang Udviklingssession", + "start": "2025-10-04T05:00:00Z", + "end": "2025-10-04T09:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 240, + "color": "#2196f3" + } + }, + { + "id": "S1A", + "title": "Scenario 1: Event A", + "start": "2025-10-06T05:00:00Z", + "end": "2025-10-06T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 300, + "color": "#ff6b6b" + } + }, + { + "id": "S1B", + "title": "Scenario 1: Event B", + "start": "2025-10-06T06:00:00Z", + "end": "2025-10-06T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#4ecdc4" + } + }, + { + "id": "S1C", + "title": "Scenario 1: Event C", + "start": "2025-10-06T08:30:00Z", + "end": "2025-10-06T09:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ffe66d" + } + }, + { + "id": "S2A", + "title": "Scenario 2: Event A", + "start": "2025-10-06T11:00:00Z", + "end": "2025-10-06T17:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 360, + "color": "#ff6b6b" + } + }, + { + "id": "S2B", + "title": "Scenario 2: Event B", + "start": "2025-10-06T12:00:00Z", + "end": "2025-10-06T13:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4ecdc4" + } + }, + { + "id": "S2C", + "title": "Scenario 2: Event C", + "start": "2025-10-06T13:30:00Z", + "end": "2025-10-06T14:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S2D", + "title": "Scenario 2: Event D", + "start": "2025-10-06T15:00:00Z", + "end": "2025-10-06T16:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#a8e6cf" + } + }, + { + "id": "S3A", + "title": "Scenario 3: Event A", + "start": "2025-10-07T07:00:00Z", + "end": "2025-10-07T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 360, + "color": "#ff6b6b" + } + }, + { + "id": "S3B", + "title": "Scenario 3: Event B", + "start": "2025-10-07T08:00:00Z", + "end": "2025-10-07T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#4ecdc4" + } + }, + { + "id": "S3C", + "title": "Scenario 3: Event C", + "start": "2025-10-07T09:00:00Z", + "end": "2025-10-07T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S3D", + "title": "Scenario 3: Event D", + "start": "2025-10-07T10:30:00Z", + "end": "2025-10-07T11:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#a8e6cf" + } + }, + { + "id": "S4A", + "title": "Scenario 4: Event A", + "start": "2025-10-07T14:00:00Z", + "end": "2025-10-07T20:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 360, + "color": "#ff6b6b" + } + }, + { + "id": "S4B", + "title": "Scenario 4: Event B", + "start": "2025-10-07T15:00:00Z", + "end": "2025-10-07T19:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 240, + "color": "#4ecdc4" + } + }, + { + "id": "S4C", + "title": "Scenario 4: Event C", + "start": "2025-10-07T16:00:00Z", + "end": "2025-10-07T18:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#ffe66d" + } + }, + { + "id": "S5A", + "title": "Scenario 5: Event A", + "start": "2025-10-08T05:00:00Z", + "end": "2025-10-08T08:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#ff6b6b" + } + }, + { + "id": "S5B", + "title": "Scenario 5: Event B", + "start": "2025-10-08T06:00:00Z", + "end": "2025-10-08T07:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#4ecdc4" + } + }, + { + "id": "S5C", + "title": "Scenario 5: Event C", + "start": "2025-10-08T06:00:00Z", + "end": "2025-10-08T07:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S6A", + "title": "Scenario 6: Event A", + "start": "2025-10-08T09:00:00Z", + "end": "2025-10-08T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#ff6b6b" + } + }, + { + "id": "S6B", + "title": "Scenario 6: Event B", + "start": "2025-10-08T10:00:00Z", + "end": "2025-10-08T11:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#4ecdc4" + } + }, + { + "id": "S6C", + "title": "Scenario 6: Event C", + "start": "2025-10-08T10:00:00Z", + "end": "2025-10-08T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S6D", + "title": "Scenario 6: Event D", + "start": "2025-10-08T10:30:00Z", + "end": "2025-10-08T10:45:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 15, + "color": "#a8e6cf" + } + }, + { + "id": "S7A", + "title": "Scenario 7: Event A", + "start": "2025-10-09T05:00:00Z", + "end": "2025-10-09T07:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 150, + "color": "#009688" + } + }, + { + "id": "S7B", + "title": "Scenario 7: Event B", + "start": "2025-10-09T05:00:00Z", + "end": "2025-10-09T07:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 120, + "color": "#ff5722" + } + }, + { + "id": "S8A", + "title": "Scenario 8: Event A", + "start": "2025-10-09T08:00:00Z", + "end": "2025-10-09T09:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff6b6b" + } + }, + { + "id": "S8B", + "title": "Scenario 8: Event B", + "start": "2025-10-09T08:15:00Z", + "end": "2025-10-09T09:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 75, + "color": "#4ecdc4" + } + }, + { + "id": "S9A", + "title": "Scenario 9: Event A", + "start": "2025-10-09T10:00:00Z", + "end": "2025-10-09T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ff6b6b" + } + }, + { + "id": "S9B", + "title": "Scenario 9: Event B", + "start": "2025-10-09T10:30:00Z", + "end": "2025-10-09T11:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#4ecdc4" + } + }, + { + "id": "S9C", + "title": "Scenario 9: Event C", + "start": "2025-10-09T11:15:00Z", + "end": "2025-10-09T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 105, + "color": "#ffe66d" + } + }, + { + "id": "S10A", + "title": "Scenario 10: Event A", + "start": "2025-10-10T10:00:00Z", + "end": "2025-10-10T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#ff6b6b" + } + }, + { + "id": "S10B", + "title": "Scenario 10: Event B", + "start": "2025-10-10T10:30:00Z", + "end": "2025-10-10T11:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#4ecdc4" + } + }, + { + "id": "S10C", + "title": "Scenario 10: Event C", + "start": "2025-10-10T11:30:00Z", + "end": "2025-10-10T12:30:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#ffe66d" + } + }, + { + "id": "S10D", + "title": "Scenario 10: Event D", + "start": "2025-10-10T12:00:00Z", + "end": "2025-10-10T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#a8e6cf" + } + }, + { + "id": "S10E", + "title": "Scenario 10: Event E", + "start": "2025-10-10T12:00:00Z", + "end": "2025-10-10T13:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#dda15e" + } + }, + { + "id": "169", + "title": "Morgen Standup", + "start": "2025-10-13T05:00:00Z", + "end": "2025-10-13T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "170", + "title": "Produktvejledning", + "start": "2025-10-13T07:00:00Z", + "end": "2025-10-13T08:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 90, + "color": "#9c27b0" + } + }, + { + "id": "171", + "title": "Team Standup", + "start": "2025-10-14T05:00:00Z", + "end": "2025-10-14T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "172", + "title": "Udviklingssession", + "start": "2025-10-14T06:00:00Z", + "end": "2025-10-14T09:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#2196f3" + } + }, + { + "id": "173", + "title": "Klient Gennemgang", + "start": "2025-10-15T11:00:00Z", + "end": "2025-10-15T12:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#795548" + } + }, + { + "id": "174", + "title": "Team Standup", + "start": "2025-10-16T05:00:00Z", + "end": "2025-10-16T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "175", + "title": "Arkitektur Workshop", + "start": "2025-10-16T10:00:00Z", + "end": "2025-10-16T13:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 180, + "color": "#009688" + } + }, + { + "id": "176", + "title": "Team Standup", + "start": "2025-10-17T05:00:00Z", + "end": "2025-10-17T05:30:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 30, + "color": "#ff5722" + } + }, + { + "id": "177", + "title": "Sprint Review", + "start": "2025-10-17T10:00:00Z", + "end": "2025-10-17T11:00:00Z", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 60, + "color": "#607d8b" + } + }, + { + "id": "178", + "title": "Weekend Kodning", + "start": "2025-10-18T06:00:00Z", + "end": "2025-10-18T10:00:00Z", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { + "duration": 240, + "color": "#3f51b5" + } + } +] \ No newline at end of file diff --git a/wwwroot/data/mock-resource-events.json b/wwwroot/data/mock-resource-events.json new file mode 100644 index 0000000..a569174 --- /dev/null +++ b/wwwroot/data/mock-resource-events.json @@ -0,0 +1,135 @@ +{ + "date": "2025-08-05", + "resources": [ + { + "name": "karina.knudsen", + "displayName": "Karina Knudsen", + "avatarUrl": "/avatars/karina.jpg", + "employeeId": "EMP001", + "events": [ + { + "id": "1", + "title": "Balayage langt hår", + "start": "2025-08-05T10:00:00", + "end": "2025-08-05T11:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#9c27b0" } + }, + { + "id": "2", + "title": "Klipning og styling", + "start": "2025-08-05T14:00:00", + "end": "2025-08-05T15:30:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#e91e63" } + } + ] + }, + { + "name": "maria.hansen", + "displayName": "Maria Hansen", + "avatarUrl": "/avatars/maria.jpg", + "employeeId": "EMP002", + "events": [ + { + "id": "3", + "title": "Permanent", + "start": "2025-08-05T09:00:00", + "end": "2025-08-05T11:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#3f51b5" } + }, + { + "id": "4", + "title": "Farve behandling", + "start": "2025-08-05T13:00:00", + "end": "2025-08-05T15:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#ff9800" } + } + ] + }, + { + "name": "lars.nielsen", + "displayName": "Lars Nielsen", + "avatarUrl": "/avatars/lars.jpg", + "employeeId": "EMP003", + "events": [ + { + "id": "5", + "title": "Herreklipning", + "start": "2025-08-05T11:00:00", + "end": "2025-08-05T11:30:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#795548" } + }, + { + "id": "6", + "title": "Skæg trimning", + "start": "2025-08-05T16:00:00", + "end": "2025-08-05T16:30:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#607d8b" } + } + ] + }, + { + "name": "anna.petersen", + "displayName": "Anna Petersen", + "avatarUrl": "/avatars/anna.jpg", + "employeeId": "EMP004", + "events": [ + { + "id": "7", + "title": "Bryllupsfrisure", + "start": "2025-08-05T08:00:00", + "end": "2025-08-05T10:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#009688" } + } + ] + }, + { + "name": "thomas.olsen", + "displayName": "Thomas Olsen", + "avatarUrl": "/avatars/thomas.jpg", + "employeeId": "EMP005", + "events": [ + { + "id": "8", + "title": "Highlights", + "start": "2025-08-05T12:00:00", + "end": "2025-08-05T14:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#8bc34a" } + }, + { + "id": "9", + "title": "Styling konsultation", + "start": "2025-08-05T15:00:00", + "end": "2025-08-05T15:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#cddc39" } + } + ] + } + ] +} \ No newline at end of file