From eeaeddeef87cff353fc49f10db8798bb5e914bb0 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sat, 22 Nov 2025 19:42:12 +0100 Subject: [PATCH] Adds resource-based calendar view mode Introduces new ResourceColumnDataSource and ResourceHeaderRenderer to support column rendering by resources instead of dates Enables dynamic calendar mode switching between date and resource views Updates core managers and services to support async column retrieval Refactors data source interfaces to use Promise-based methods Improves calendar flexibility and resource management capabilities --- src/datasources/DateColumnDataSource.ts | 2 +- src/datasources/ResourceColumnDataSource.ts | 61 +++ src/index.ts | 25 +- src/managers/GridManager.ts | 5 +- src/managers/HeaderManager.ts | 4 +- src/managers/NavigationManager.ts | 4 +- src/renderers/ResourceColumnRenderer.ts | 46 ++ src/renderers/ResourceHeaderRenderer.ts | 58 +++ src/repositories/MockAuditRepository.ts | 6 +- src/storage/IndexedDBContext.ts | 2 +- src/storage/resources/ResourceService.ts | 55 +-- src/storage/resources/ResourceStore.ts | 9 - src/types/ColumnDataSource.ts | 2 +- wwwroot/data/bookings.json | 306 ------------ wwwroot/data/customers.json | 49 -- wwwroot/data/events.json | 485 -------------------- wwwroot/data/mock-bookings.json | 208 +++++++++ wwwroot/data/mock-events.json | 349 ++++++++++++++ wwwroot/data/resources.json | 80 ---- 19 files changed, 765 insertions(+), 991 deletions(-) create mode 100644 src/datasources/ResourceColumnDataSource.ts create mode 100644 src/renderers/ResourceColumnRenderer.ts create mode 100644 src/renderers/ResourceHeaderRenderer.ts delete mode 100644 wwwroot/data/bookings.json delete mode 100644 wwwroot/data/customers.json delete mode 100644 wwwroot/data/events.json delete mode 100644 wwwroot/data/resources.json diff --git a/src/datasources/DateColumnDataSource.ts b/src/datasources/DateColumnDataSource.ts index d4ac0dc..1e7fd51 100644 --- a/src/datasources/DateColumnDataSource.ts +++ b/src/datasources/DateColumnDataSource.ts @@ -30,7 +30,7 @@ export class DateColumnDataSource implements IColumnDataSource { /** * Get columns (dates) to display */ - public getColumns(): IColumnInfo[] { + public async getColumns(): Promise { let dates: Date[]; switch (this.currentView) { diff --git a/src/datasources/ResourceColumnDataSource.ts b/src/datasources/ResourceColumnDataSource.ts new file mode 100644 index 0000000..9e340ed --- /dev/null +++ b/src/datasources/ResourceColumnDataSource.ts @@ -0,0 +1,61 @@ +import { IColumnDataSource, IColumnInfo } from '../types/ColumnDataSource'; +import { CalendarView } from '../types/CalendarTypes'; +import { ResourceService } from '../storage/resources/ResourceService'; + +/** + * ResourceColumnDataSource - Provides resource-based columns + * + * In resource mode, columns represent resources (people, rooms, etc.) + * instead of dates. Events are still filtered by current date, + * but grouped by resourceId. + */ +export class ResourceColumnDataSource implements IColumnDataSource { + private resourceService: ResourceService; + private currentDate: Date; + private currentView: CalendarView; + + constructor(resourceService: ResourceService) { + this.resourceService = resourceService; + this.currentDate = new Date(); + this.currentView = 'day'; + } + + /** + * Get columns (resources) to display + */ + public async getColumns(): Promise { + const resources = await this.resourceService.getActive(); + return resources.map(resource => ({ + identifier: resource.id, + data: resource + })); + } + + /** + * Get type of datasource + */ + public getType(): 'date' | 'resource' { + return 'resource'; + } + + /** + * Update current date (for event filtering) + */ + public setCurrentDate(date: Date): void { + this.currentDate = date; + } + + /** + * Update current view + */ + public setCurrentView(view: CalendarView): void { + this.currentView = view; + } + + /** + * Get current date (for event filtering) + */ + public getCurrentDate(): Date { + return this.currentDate; + } +} diff --git a/src/index.ts b/src/index.ts index 71d0181..1992d83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,9 @@ import { EventStackManager } from './managers/EventStackManager'; import { EventLayoutCoordinator } from './managers/EventLayoutCoordinator'; import { IColumnDataSource } from './types/ColumnDataSource'; import { DateColumnDataSource } from './datasources/DateColumnDataSource'; +import { ResourceColumnDataSource } from './datasources/ResourceColumnDataSource'; +import { ResourceHeaderRenderer } from './renderers/ResourceHeaderRenderer'; +import { ResourceColumnRenderer } from './renderers/ResourceColumnRenderer'; import { IBooking } from './types/BookingTypes'; import { ICustomer } from './types/CustomerTypes'; import { IResource } from './types/ResourceTypes'; @@ -137,13 +140,25 @@ async function initializeCalendar(): Promise { builder.registerType(MockResourceRepository).as>(); builder.registerType(MockAuditRepository).as>(); - builder.registerType(DateColumnDataSource).as(); + // Calendar mode: 'date' or 'resource' (default to resource) + const calendarMode: 'date' | 'resource' = 'resource'; + + // Register DataSource and HeaderRenderer based on mode + if (calendarMode === 'resource') { + builder.registerType(ResourceColumnDataSource).as(); + builder.registerType(ResourceHeaderRenderer).as(); + } else { + builder.registerType(DateColumnDataSource).as(); + builder.registerType(DateHeaderRenderer).as(); + } + // Register entity services (sync status management) // Open/Closed Principle: Adding new entity only requires adding one line here builder.registerType(EventService).as>(); builder.registerType(BookingService).as>(); builder.registerType(CustomerService).as>(); builder.registerType(ResourceService).as>(); + builder.registerType(ResourceService).as(); builder.registerType(AuditService).as(); // Register workers @@ -151,8 +166,12 @@ async function initializeCalendar(): Promise { builder.registerType(DataSeeder).as(); // Register renderers - builder.registerType(DateHeaderRenderer).as(); - builder.registerType(DateColumnRenderer).as(); + // Note: IHeaderRenderer and IColumnRenderer are registered above based on calendarMode + if (calendarMode === 'resource') { + builder.registerType(ResourceColumnRenderer).as(); + } else { + builder.registerType(DateColumnRenderer).as(); + } builder.registerType(DateEventRenderer).as(); // Register core services and utilities diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 36cf352..042da5f 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -89,7 +89,10 @@ export class GridManager { } // Get columns from datasource - single source of truth - const columns = this.dataSource.getColumns(); + const columns = await this.dataSource.getColumns(); + + // Set grid columns CSS variable based on actual column count + document.documentElement.style.setProperty('--grid-columns', columns.length.toString()); // Extract dates for EventManager query const dates = columns.map(col => col.data as Date); diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index e12ef4f..41b0358 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -99,7 +99,7 @@ export class HeaderManager { /** * Update header content for navigation */ - private updateHeader(currentDate: Date): void { + private async updateHeader(currentDate: Date): Promise { console.log('🎯 HeaderManager.updateHeader called', { currentDate, rendererType: this.headerRenderer.constructor.name @@ -116,7 +116,7 @@ export class HeaderManager { // Update DataSource with current date and get columns this.dataSource.setCurrentDate(currentDate); - const columns = this.dataSource.getColumns(); + const columns = await this.dataSource.getColumns(); // Render new header content using injected renderer const context: IHeaderRenderContext = { diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index 3aa8b8d..fbb64e6 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -173,7 +173,7 @@ export class NavigationManager { /** * Animation transition using pre-rendered containers when available */ - private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { + private async animateTransition(direction: 'prev' | 'next', targetWeek: Date): Promise { const container = document.querySelector('swp-calendar-container') as HTMLElement; const currentGrid = document.querySelector('swp-calendar-container swp-grid-container:not([data-prerendered])') as HTMLElement; @@ -194,7 +194,7 @@ export class NavigationManager { // Update DataSource with target week and get columns this.dataSource.setCurrentDate(targetWeek); - const columns = this.dataSource.getColumns(); + const columns = await this.dataSource.getColumns(); // Always create a fresh container for consistent behavior newGrid = this.gridRenderer.createNavigationGrid(container, columns); diff --git a/src/renderers/ResourceColumnRenderer.ts b/src/renderers/ResourceColumnRenderer.ts new file mode 100644 index 0000000..93fe6b6 --- /dev/null +++ b/src/renderers/ResourceColumnRenderer.ts @@ -0,0 +1,46 @@ +import { WorkHoursManager } from '../managers/WorkHoursManager'; +import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer'; + +/** + * Resource-based column renderer + * + * In resource mode, columns represent resources (people, rooms, etc.) + * Work hours are hardcoded (09:00-18:00) for all columns. + * TODO: Each resource should have its own work hours. + */ +export class ResourceColumnRenderer implements IColumnRenderer { + private workHoursManager: WorkHoursManager; + + constructor(workHoursManager: WorkHoursManager) { + this.workHoursManager = workHoursManager; + } + + render(columnContainer: HTMLElement, context: IColumnRenderContext): void { + const { columns } = context; + + // Hardcoded work hours for all resources: 09:00 - 18:00 + const workHours = { start: 9, end: 18 }; + + columns.forEach((columnInfo) => { + const column = document.createElement('swp-day-column'); + + column.dataset.columnId = columnInfo.identifier; + + // Apply hardcoded work hours to all resource columns + this.applyWorkHoursToColumn(column, workHours); + + const eventsLayer = document.createElement('swp-events-layer'); + column.appendChild(eventsLayer); + + columnContainer.appendChild(column); + }); + } + + private applyWorkHoursToColumn(column: HTMLElement, workHours: { start: number; end: number }): void { + const nonWorkStyle = this.workHoursManager.calculateNonWorkHoursStyle(workHours); + if (nonWorkStyle) { + column.style.setProperty('--before-work-height', `${nonWorkStyle.beforeWorkHeight}px`); + column.style.setProperty('--after-work-top', `${nonWorkStyle.afterWorkTop}px`); + } + } +} diff --git a/src/renderers/ResourceHeaderRenderer.ts b/src/renderers/ResourceHeaderRenderer.ts new file mode 100644 index 0000000..296f74e --- /dev/null +++ b/src/renderers/ResourceHeaderRenderer.ts @@ -0,0 +1,58 @@ +import { IHeaderRenderer, IHeaderRenderContext } from './DateHeaderRenderer'; +import { IResource } from '../types/ResourceTypes'; + +/** + * ResourceHeaderRenderer - Renders resource-based headers + * + * Displays resource information (avatar, name) instead of dates. + * Used in resource mode where columns represent people/rooms/equipment. + */ +export class ResourceHeaderRenderer implements IHeaderRenderer { + render(calendarHeader: HTMLElement, context: IHeaderRenderContext): void { + const { columns } = context; + + // Create all-day container (same structure as date mode) + const allDayContainer = document.createElement('swp-allday-container'); + calendarHeader.appendChild(allDayContainer); + + columns.forEach((columnInfo) => { + const resource = columnInfo.data as IResource; + const header = document.createElement('swp-day-header'); + + // Build header content + let avatarHtml = ''; + if (resource.avatarUrl) { + avatarHtml = `${resource.displayName}`; + } else { + // Fallback: initials + const initials = this.getInitials(resource.displayName); + const bgColor = resource.color || '#6366f1'; + avatarHtml = `${initials}`; + } + + header.innerHTML = ` +
+ ${avatarHtml} + ${resource.displayName} +
+ `; + + header.dataset.columnId = columnInfo.identifier; + header.dataset.resourceId = resource.id; + + calendarHeader.appendChild(header); + }); + } + + /** + * Get initials from display name + */ + private getInitials(name: string): string { + return name + .split(' ') + .map(part => part.charAt(0)) + .join('') + .toUpperCase() + .substring(0, 2); + } +} diff --git a/src/repositories/MockAuditRepository.ts b/src/repositories/MockAuditRepository.ts index 33448f7..753f4b4 100644 --- a/src/repositories/MockAuditRepository.ts +++ b/src/repositories/MockAuditRepository.ts @@ -11,7 +11,7 @@ import { EntityType } from '../types/CalendarTypes'; export class MockAuditRepository implements IApiRepository { readonly entityType: EntityType = 'Audit'; - async sendCreate(entity: IAuditEntry): Promise { + async sendCreate(entity: IAuditEntry): Promise { // Simulate API call delay await new Promise(resolve => setTimeout(resolve, 100)); @@ -22,9 +22,11 @@ export class MockAuditRepository implements IApiRepository { operation: entity.operation, timestamp: new Date(entity.timestamp).toISOString() }); + + return entity; } - async sendUpdate(_id: string, _entity: IAuditEntry): Promise { + async sendUpdate(_id: string, entity: IAuditEntry): Promise { // Audit entries are immutable - updates should not happen throw new Error('Audit entries cannot be updated'); } diff --git a/src/storage/IndexedDBContext.ts b/src/storage/IndexedDBContext.ts index be51585..da2d6fe 100644 --- a/src/storage/IndexedDBContext.ts +++ b/src/storage/IndexedDBContext.ts @@ -19,7 +19,7 @@ import { IStore } from './IStore'; */ export class IndexedDBContext { private static readonly DB_NAME = 'CalendarDB'; - private static readonly DB_VERSION = 3; // Bumped for audit store + private static readonly DB_VERSION = 5; // Bumped to add syncStatus index to resources static readonly QUEUE_STORE = 'operationQueue'; static readonly SYNC_STATE_STORE = 'syncState'; diff --git a/src/storage/resources/ResourceService.ts b/src/storage/resources/ResourceService.ts index e59cef9..8fd868e 100644 --- a/src/storage/resources/ResourceService.ts +++ b/src/storage/resources/ResourceService.ts @@ -31,68 +31,25 @@ export class ResourceService extends BaseEntityService { /** * Get resources by type - * - * @param type - Resource type (person, room, equipment, etc.) - * @returns Array of resources of this type */ async getByType(type: string): Promise { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([this.storeName], 'readonly'); - const store = transaction.objectStore(this.storeName); - const index = store.index('type'); - const request = index.getAll(type); - - request.onsuccess = () => { - resolve(request.result as IResource[]); - }; - - request.onerror = () => { - reject(new Error(`Failed to get resources by type ${type}: ${request.error}`)); - }; - }); + const all = await this.getAll(); + return all.filter(r => r.type === type); } /** * Get active resources only - * - * @returns Array of active resources (isActive = true) */ async getActive(): Promise { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([this.storeName], 'readonly'); - const store = transaction.objectStore(this.storeName); - const index = store.index('isActive'); - const request = index.getAll(IDBKeyRange.only(true)); - - request.onsuccess = () => { - resolve(request.result as IResource[]); - }; - - request.onerror = () => { - reject(new Error(`Failed to get active resources: ${request.error}`)); - }; - }); + const all = await this.getAll(); + return all.filter(r => r.isActive === true); } /** * Get inactive resources - * - * @returns Array of inactive resources (isActive = false) */ async getInactive(): Promise { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([this.storeName], 'readonly'); - const store = transaction.objectStore(this.storeName); - const index = store.index('isActive'); - const request = index.getAll(IDBKeyRange.only(false)); - - request.onsuccess = () => { - resolve(request.result as IResource[]); - }; - - request.onerror = () => { - reject(new Error(`Failed to get inactive resources: ${request.error}`)); - }; - }); + const all = await this.getAll(); + return all.filter(r => r.isActive === false); } } diff --git a/src/storage/resources/ResourceStore.ts b/src/storage/resources/ResourceStore.ts index 1725777..05ed171 100644 --- a/src/storage/resources/ResourceStore.ts +++ b/src/storage/resources/ResourceStore.ts @@ -20,16 +20,7 @@ export class ResourceStore implements IStore { * @param db - IDBDatabase instance */ create(db: IDBDatabase): void { - // Create ObjectStore with 'id' as keyPath const store = db.createObjectStore(ResourceStore.STORE_NAME, { keyPath: 'id' }); - - // Index: type (for filtering by resource category) - store.createIndex('type', 'type', { unique: false }); - - // Index: isActive (for showing/hiding inactive resources) - store.createIndex('isActive', 'isActive', { unique: false }); - - // Index: syncStatus (for querying by sync status - used by SyncPlugin) store.createIndex('syncStatus', 'syncStatus', { unique: false }); } } diff --git a/src/types/ColumnDataSource.ts b/src/types/ColumnDataSource.ts index f933574..1b38a7d 100644 --- a/src/types/ColumnDataSource.ts +++ b/src/types/ColumnDataSource.ts @@ -21,7 +21,7 @@ export interface IColumnDataSource { * Get the list of columns to render * @returns Array of column information */ - getColumns(): IColumnInfo[]; + getColumns(): Promise; /** * Get the type of columns this datasource provides diff --git a/wwwroot/data/bookings.json b/wwwroot/data/bookings.json deleted file mode 100644 index a4c0eec..0000000 --- a/wwwroot/data/bookings.json +++ /dev/null @@ -1,306 +0,0 @@ -[ - { - "id": "BOOK001", - "customerId": "CUST001", - "status": "arrived", - "createdAt": "2025-08-05T08:00:00Z", - "services": [ - { - "serviceId": "SRV001", - "serviceName": "Klipning og styling", - "baseDuration": 60, - "basePrice": 500, - "customPrice": 500, - "resourceId": "EMP001" - } - ], - "totalPrice": 500, - "notes": "Kunde ønsker lidt kortere" - }, - { - "id": "BOOK002", - "customerId": "CUST002", - "status": "paid", - "createdAt": "2025-08-05T09:00:00Z", - "services": [ - { - "serviceId": "SRV002", - "serviceName": "Hårvask", - "baseDuration": 30, - "basePrice": 100, - "customPrice": 100, - "resourceId": "STUDENT001" - }, - { - "serviceId": "SRV003", - "serviceName": "Bundfarve", - "baseDuration": 90, - "basePrice": 800, - "customPrice": 800, - "resourceId": "EMP001" - } - ], - "totalPrice": 900, - "notes": "Split booking: Elev laver hårvask, master laver farve" - }, - { - "id": "BOOK003", - "customerId": "CUST003", - "status": "created", - "createdAt": "2025-08-05T07:00:00Z", - "services": [ - { - "serviceId": "SRV004A", - "serviceName": "Bryllupsfrisure - Del 1", - "baseDuration": 60, - "basePrice": 750, - "customPrice": 750, - "resourceId": "EMP001" - }, - { - "serviceId": "SRV004B", - "serviceName": "Bryllupsfrisure - Del 2", - "baseDuration": 60, - "basePrice": 750, - "customPrice": 750, - "resourceId": "EMP002" - } - ], - "totalPrice": 1500, - "notes": "Equal-split: To master stylister arbejder sammen" - }, - { - "id": "BOOK004", - "customerId": "CUST004", - "status": "arrived", - "createdAt": "2025-08-05T10:00:00Z", - "services": [ - { - "serviceId": "SRV005", - "serviceName": "Herreklipning", - "baseDuration": 30, - "basePrice": 350, - "customPrice": 350, - "resourceId": "EMP003" - } - ], - "totalPrice": 350 - }, - { - "id": "BOOK005", - "customerId": "CUST005", - "status": "paid", - "createdAt": "2025-08-05T11:00:00Z", - "services": [ - { - "serviceId": "SRV006", - "serviceName": "Balayage langt hår", - "baseDuration": 120, - "basePrice": 1200, - "customPrice": 1200, - "resourceId": "EMP002" - } - ], - "totalPrice": 1200, - "notes": "Kunde ønsker naturlig blond tone" - }, - { - "id": "BOOK006", - "customerId": "CUST006", - "status": "created", - "createdAt": "2025-08-06T08:00:00Z", - "services": [ - { - "serviceId": "SRV007", - "serviceName": "Permanent", - "baseDuration": 90, - "basePrice": 900, - "customPrice": 900, - "resourceId": "EMP004" - } - ], - "totalPrice": 900 - }, - { - "id": "BOOK007", - "customerId": "CUST007", - "status": "arrived", - "createdAt": "2025-08-06T09:00:00Z", - "services": [ - { - "serviceId": "SRV008", - "serviceName": "Highlights", - "baseDuration": 90, - "basePrice": 850, - "customPrice": 850, - "resourceId": "EMP001" - }, - { - "serviceId": "SRV009", - "serviceName": "Styling", - "baseDuration": 30, - "basePrice": 200, - "customPrice": 200, - "resourceId": "EMP001" - } - ], - "totalPrice": 1050, - "notes": "Highlights + styling samme stylist" - }, - { - "id": "BOOK008", - "customerId": "CUST008", - "status": "paid", - "createdAt": "2025-08-06T10:00:00Z", - "services": [ - { - "serviceId": "SRV010", - "serviceName": "Klipning", - "baseDuration": 45, - "basePrice": 450, - "customPrice": 450, - "resourceId": "EMP004" - } - ], - "totalPrice": 450 - }, - { - "id": "BOOK009", - "customerId": "CUST001", - "status": "created", - "createdAt": "2025-08-07T08:00:00Z", - "services": [ - { - "serviceId": "SRV011", - "serviceName": "Farve behandling", - "baseDuration": 120, - "basePrice": 950, - "customPrice": 950, - "resourceId": "EMP002" - } - ], - "totalPrice": 950 - }, - { - "id": "BOOK010", - "customerId": "CUST002", - "status": "arrived", - "createdAt": "2025-08-07T09:00:00Z", - "services": [ - { - "serviceId": "SRV012", - "serviceName": "Skæg trimning", - "baseDuration": 20, - "basePrice": 200, - "customPrice": 200, - "resourceId": "EMP003" - } - ], - "totalPrice": 200 - }, - { - "id": "BOOK011", - "customerId": "CUST003", - "status": "paid", - "createdAt": "2025-08-07T10:00:00Z", - "services": [ - { - "serviceId": "SRV002", - "serviceName": "Hårvask", - "baseDuration": 30, - "basePrice": 100, - "customPrice": 100, - "resourceId": "STUDENT002" - }, - { - "serviceId": "SRV013", - "serviceName": "Ombré", - "baseDuration": 100, - "basePrice": 1100, - "customPrice": 1100, - "resourceId": "EMP002" - } - ], - "totalPrice": 1200, - "notes": "Split booking: Student hårvask, master ombré" - }, - { - "id": "BOOK012", - "customerId": "CUST004", - "status": "created", - "createdAt": "2025-08-08T08:00:00Z", - "services": [ - { - "serviceId": "SRV014", - "serviceName": "Føntørring", - "baseDuration": 30, - "basePrice": 250, - "customPrice": 250, - "resourceId": "STUDENT001" - } - ], - "totalPrice": 250 - }, - { - "id": "BOOK013", - "customerId": "CUST005", - "status": "arrived", - "createdAt": "2025-08-08T09:00:00Z", - "services": [ - { - "serviceId": "SRV015", - "serviceName": "Opsætning", - "baseDuration": 60, - "basePrice": 700, - "customPrice": 700, - "resourceId": "EMP004" - } - ], - "totalPrice": 700, - "notes": "Fest opsætning" - }, - { - "id": "BOOK014", - "customerId": "CUST006", - "status": "created", - "createdAt": "2025-08-09T08:00:00Z", - "services": [ - { - "serviceId": "SRV016A", - "serviceName": "Ekstensions - Del 1", - "baseDuration": 90, - "basePrice": 1250, - "customPrice": 1250, - "resourceId": "EMP001" - }, - { - "serviceId": "SRV016B", - "serviceName": "Ekstensions - Del 2", - "baseDuration": 90, - "basePrice": 1250, - "customPrice": 1250, - "resourceId": "EMP004" - } - ], - "totalPrice": 2500, - "notes": "Equal-split: To stylister arbejder sammen om extensions" - }, - { - "id": "BOOK015", - "customerId": "CUST007", - "status": "noshow", - "createdAt": "2025-08-09T09:00:00Z", - "services": [ - { - "serviceId": "SRV001", - "serviceName": "Klipning og styling", - "baseDuration": 60, - "basePrice": 500, - "customPrice": 500, - "resourceId": "EMP002" - } - ], - "totalPrice": 500, - "notes": "Kunde mødte ikke op" - } -] diff --git a/wwwroot/data/customers.json b/wwwroot/data/customers.json deleted file mode 100644 index 28997bf..0000000 --- a/wwwroot/data/customers.json +++ /dev/null @@ -1,49 +0,0 @@ -[ - { - "id": "CUST001", - "name": "Sofie Nielsen", - "phone": "+45 23 45 67 89", - "email": "sofie.nielsen@email.dk" - }, - { - "id": "CUST002", - "name": "Emma Andersen", - "phone": "+45 31 24 56 78", - "email": "emma.andersen@email.dk" - }, - { - "id": "CUST003", - "name": "Freja Christensen", - "phone": "+45 42 67 89 12", - "email": "freja.christensen@email.dk" - }, - { - "id": "CUST004", - "name": "Laura Pedersen", - "phone": "+45 51 98 76 54" - }, - { - "id": "CUST005", - "name": "Ida Larsen", - "phone": "+45 29 87 65 43", - "email": "ida.larsen@email.dk" - }, - { - "id": "CUST006", - "name": "Caroline Jensen", - "phone": "+45 38 76 54 32", - "email": "caroline.jensen@email.dk" - }, - { - "id": "CUST007", - "name": "Mathilde Hansen", - "phone": "+45 47 65 43 21", - "email": "mathilde.hansen@email.dk" - }, - { - "id": "CUST008", - "name": "Olivia Sørensen", - "phone": "+45 56 54 32 10", - "email": "olivia.sorensen@email.dk" - } -] diff --git a/wwwroot/data/events.json b/wwwroot/data/events.json deleted file mode 100644 index 498cbe5..0000000 --- a/wwwroot/data/events.json +++ /dev/null @@ -1,485 +0,0 @@ -[ - { - "id": "EVT001", - "title": "Sofie Nielsen - Klipning og styling", - "start": "2025-08-05T10:00:00Z", - "end": "2025-08-05T11:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK001", - "resourceId": "EMP001", - "customerId": "CUST001" - }, - { - "id": "EVT002", - "title": "Emma Andersen - Hårvask", - "start": "2025-08-05T11:00:00Z", - "end": "2025-08-05T11:30:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK002", - "resourceId": "STUDENT001", - "customerId": "CUST002" - }, - { - "id": "EVT003", - "title": "Emma Andersen - Bundfarve", - "start": "2025-08-05T11:30:00Z", - "end": "2025-08-05T13:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK002", - "resourceId": "EMP001", - "customerId": "CUST002" - }, - { - "id": "EVT004", - "title": "Freja Christensen - Bryllupsfrisure (Camilla)", - "start": "2025-08-05T08:00:00Z", - "end": "2025-08-05T10:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK003", - "resourceId": "EMP001", - "customerId": "CUST003", - "metadata": { - "note": "To stylister arbejder sammen" - } - }, - { - "id": "EVT005", - "title": "Freja Christensen - Bryllupsfrisure (Isabella)", - "start": "2025-08-05T08:00:00Z", - "end": "2025-08-05T10:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK003", - "resourceId": "EMP002", - "customerId": "CUST003", - "metadata": { - "note": "To stylister arbejder sammen" - } - }, - { - "id": "EVT006", - "title": "Laura Pedersen - Herreklipning", - "start": "2025-08-05T11:00:00Z", - "end": "2025-08-05T11:30:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK004", - "resourceId": "EMP003", - "customerId": "CUST004" - }, - { - "id": "EVT007", - "title": "Ida Larsen - Balayage langt hår", - "start": "2025-08-05T13:00:00Z", - "end": "2025-08-05T15:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK005", - "resourceId": "EMP002", - "customerId": "CUST005" - }, - { - "id": "EVT008", - "title": "Frokostpause", - "start": "2025-08-05T12:00:00Z", - "end": "2025-08-05T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP003" - }, - { - "id": "EVT009", - "title": "Caroline Jensen - Permanent", - "start": "2025-08-06T09:00:00Z", - "end": "2025-08-06T10:30:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK006", - "resourceId": "EMP004", - "customerId": "CUST006" - }, - { - "id": "EVT010", - "title": "Mathilde Hansen - Highlights", - "start": "2025-08-06T10:00:00Z", - "end": "2025-08-06T11:30:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK007", - "resourceId": "EMP001", - "customerId": "CUST007" - }, - { - "id": "EVT011", - "title": "Mathilde Hansen - Styling", - "start": "2025-08-06T11:30:00Z", - "end": "2025-08-06T12:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK007", - "resourceId": "EMP001", - "customerId": "CUST007" - }, - { - "id": "EVT012", - "title": "Olivia Sørensen - Klipning", - "start": "2025-08-06T13:00:00Z", - "end": "2025-08-06T13:45:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK008", - "resourceId": "EMP004", - "customerId": "CUST008" - }, - { - "id": "EVT013", - "title": "Team møde - Salgsmål", - "start": "2025-08-06T08:00:00Z", - "end": "2025-08-06T08:30:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP001", - "metadata": { - "attendees": ["EMP001", "EMP002", "EMP003", "EMP004"] - } - }, - { - "id": "EVT014", - "title": "Frokostpause", - "start": "2025-08-06T12:00:00Z", - "end": "2025-08-06T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP002" - }, - { - "id": "EVT015", - "title": "Sofie Nielsen - Farve behandling", - "start": "2025-08-07T10:00:00Z", - "end": "2025-08-07T12:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK009", - "resourceId": "EMP002", - "customerId": "CUST001" - }, - { - "id": "EVT016", - "title": "Emma Andersen - Skæg trimning", - "start": "2025-08-07T09:00:00Z", - "end": "2025-08-07T09:20:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK010", - "resourceId": "EMP003", - "customerId": "CUST002" - }, - { - "id": "EVT017", - "title": "Freja Christensen - Hårvask", - "start": "2025-08-07T11:00:00Z", - "end": "2025-08-07T11:30:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK011", - "resourceId": "STUDENT002", - "customerId": "CUST003" - }, - { - "id": "EVT018", - "title": "Freja Christensen - Ombré", - "start": "2025-08-07T11:30:00Z", - "end": "2025-08-07T13:10:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK011", - "resourceId": "EMP002", - "customerId": "CUST003" - }, - { - "id": "EVT019", - "title": "Frokostpause", - "start": "2025-08-07T12:00:00Z", - "end": "2025-08-07T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP001" - }, - { - "id": "EVT020", - "title": "Laura Pedersen - Føntørring", - "start": "2025-08-08T09:00:00Z", - "end": "2025-08-08T09:30:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK012", - "resourceId": "STUDENT001", - "customerId": "CUST004" - }, - { - "id": "EVT021", - "title": "Ida Larsen - Opsætning", - "start": "2025-08-08T10:00:00Z", - "end": "2025-08-08T11:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK013", - "resourceId": "EMP004", - "customerId": "CUST005" - }, - { - "id": "EVT022", - "title": "Produktleverance møde", - "start": "2025-08-08T08:00:00Z", - "end": "2025-08-08T08:30:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP001", - "metadata": { - "attendees": ["EMP001", "EMP004"] - } - }, - { - "id": "EVT023", - "title": "Frokostpause", - "start": "2025-08-08T12:00:00Z", - "end": "2025-08-08T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP004" - }, - { - "id": "EVT024", - "title": "Caroline Jensen - Ekstensions (Camilla)", - "start": "2025-08-09T09:00:00Z", - "end": "2025-08-09T12:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK014", - "resourceId": "EMP001", - "customerId": "CUST006", - "metadata": { - "note": "To stylister arbejder sammen" - } - }, - { - "id": "EVT025", - "title": "Caroline Jensen - Ekstensions (Viktor)", - "start": "2025-08-09T09:00:00Z", - "end": "2025-08-09T12:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK014", - "resourceId": "EMP004", - "customerId": "CUST006", - "metadata": { - "note": "To stylister arbejder sammen" - } - }, - { - "id": "EVT026", - "title": "Mathilde Hansen - Klipning og styling", - "start": "2025-08-09T10:00:00Z", - "end": "2025-08-09T11:00:00Z", - "type": "customer", - "allDay": false, - "syncStatus": "synced", - "bookingId": "BOOK015", - "resourceId": "EMP002", - "customerId": "CUST007", - "metadata": { - "note": "NOSHOW - kunde mødte ikke op" - } - }, - { - "id": "EVT027", - "title": "Ferie - Spanien", - "start": "2025-08-10T00:00:00Z", - "end": "2025-08-17T23:59:59Z", - "type": "vacation", - "allDay": true, - "syncStatus": "synced", - "resourceId": "EMP003", - "metadata": { - "destination": "Mallorca" - } - }, - { - "id": "EVT028", - "title": "Frokostpause", - "start": "2025-08-09T12:00:00Z", - "end": "2025-08-09T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP002" - }, - { - "id": "EVT029", - "title": "Kaffepause", - "start": "2025-08-05T14:00:00Z", - "end": "2025-08-05T14:15:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP004" - }, - { - "id": "EVT030", - "title": "Kursus - Nye farvningsteknikker", - "start": "2025-08-11T09:00:00Z", - "end": "2025-08-11T16:00:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP001", - "metadata": { - "location": "København", - "type": "external_course" - } - }, - { - "id": "EVT031", - "title": "Supervision - Elev", - "start": "2025-08-05T15:00:00Z", - "end": "2025-08-05T15:30:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP001", - "metadata": { - "attendees": ["EMP001", "STUDENT001"] - } - }, - { - "id": "EVT032", - "title": "Aftensmad pause", - "start": "2025-08-06T17:00:00Z", - "end": "2025-08-06T17:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP001" - }, - { - "id": "EVT033", - "title": "Supervision - Elev", - "start": "2025-08-07T15:00:00Z", - "end": "2025-08-07T15:30:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP002", - "metadata": { - "attendees": ["EMP002", "STUDENT002"] - } - }, - { - "id": "EVT034", - "title": "Rengøring af arbejdsstation", - "start": "2025-08-08T16:00:00Z", - "end": "2025-08-08T16:30:00Z", - "type": "blocked", - "allDay": false, - "syncStatus": "synced", - "resourceId": "STUDENT001" - }, - { - "id": "EVT035", - "title": "Rengøring af arbejdsstation", - "start": "2025-08-08T16:00:00Z", - "end": "2025-08-08T16:30:00Z", - "type": "blocked", - "allDay": false, - "syncStatus": "synced", - "resourceId": "STUDENT002" - }, - { - "id": "EVT036", - "title": "Leverandør møde", - "start": "2025-08-09T14:00:00Z", - "end": "2025-08-09T15:00:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP004", - "metadata": { - "attendees": ["EMP004"] - } - }, - { - "id": "EVT037", - "title": "Sygedag", - "start": "2025-08-12T00:00:00Z", - "end": "2025-08-12T23:59:59Z", - "type": "vacation", - "allDay": true, - "syncStatus": "synced", - "resourceId": "STUDENT001", - "metadata": { - "reason": "sick_leave" - } - }, - { - "id": "EVT038", - "title": "Frokostpause", - "start": "2025-08-05T12:00:00Z", - "end": "2025-08-05T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "STUDENT001" - }, - { - "id": "EVT039", - "title": "Frokostpause", - "start": "2025-08-05T12:00:00Z", - "end": "2025-08-05T12:30:00Z", - "type": "break", - "allDay": false, - "syncStatus": "synced", - "resourceId": "STUDENT002" - }, - { - "id": "EVT040", - "title": "Morgen briefing", - "start": "2025-08-05T08:30:00Z", - "end": "2025-08-05T08:45:00Z", - "type": "meeting", - "allDay": false, - "syncStatus": "synced", - "resourceId": "EMP004", - "metadata": { - "attendees": ["EMP001", "EMP002", "EMP003", "EMP004", "STUDENT001", "STUDENT002"] - } - } -] diff --git a/wwwroot/data/mock-bookings.json b/wwwroot/data/mock-bookings.json index a4c0eec..985bda0 100644 --- a/wwwroot/data/mock-bookings.json +++ b/wwwroot/data/mock-bookings.json @@ -302,5 +302,213 @@ ], "totalPrice": 500, "notes": "Kunde mødte ikke op" + }, + { + "id": "BOOK-NOV22-001", + "customerId": "CUST001", + "status": "arrived", + "createdAt": "2025-11-20T10:00:00Z", + "services": [ + { "serviceId": "SRV-WASH", "serviceName": "Hårvask", "baseDuration": 30, "basePrice": 100, "resourceId": "STUDENT001" }, + { "serviceId": "SRV-BAL", "serviceName": "Balayage", "baseDuration": 90, "basePrice": 1200, "resourceId": "EMP001" } + ], + "totalPrice": 1300, + "notes": "Split: Elev vasker, Camilla farver" + }, + { + "id": "BOOK-NOV22-002", + "customerId": "CUST002", + "status": "arrived", + "createdAt": "2025-11-20T11:00:00Z", + "services": [ + { "serviceId": "SRV-HERREKLIP", "serviceName": "Herreklipning", "baseDuration": 30, "basePrice": 350, "resourceId": "EMP003" } + ], + "totalPrice": 350 + }, + { + "id": "BOOK-NOV22-003", + "customerId": "CUST003", + "status": "created", + "createdAt": "2025-11-20T12:00:00Z", + "services": [ + { "serviceId": "SRV-FARVE", "serviceName": "Farvning", "baseDuration": 120, "basePrice": 900, "resourceId": "EMP002" } + ], + "totalPrice": 900 + }, + { + "id": "BOOK-NOV22-004", + "customerId": "CUST004", + "status": "arrived", + "createdAt": "2025-11-20T13:00:00Z", + "services": [ + { "serviceId": "SRV-KLIP", "serviceName": "Dameklipning", "baseDuration": 60, "basePrice": 450, "resourceId": "EMP004" } + ], + "totalPrice": 450 + }, + { + "id": "BOOK-NOV22-005", + "customerId": "CUST005", + "status": "created", + "createdAt": "2025-11-20T14:00:00Z", + "services": [ + { "serviceId": "SRV-STYLE", "serviceName": "Styling", "baseDuration": 60, "basePrice": 400, "resourceId": "EMP001" } + ], + "totalPrice": 400 + }, + { + "id": "BOOK-NOV23-001", + "customerId": "CUST006", + "status": "created", + "createdAt": "2025-11-21T09:00:00Z", + "services": [ + { "serviceId": "SRV-PERM", "serviceName": "Permanent", "baseDuration": 150, "basePrice": 1100, "resourceId": "EMP002" } + ], + "totalPrice": 1100 + }, + { + "id": "BOOK-NOV23-002", + "customerId": "CUST007", + "status": "created", + "createdAt": "2025-11-21T10:00:00Z", + "services": [ + { "serviceId": "SRV-SKAEG", "serviceName": "Skæg trimning", "baseDuration": 30, "basePrice": 200, "resourceId": "EMP003" } + ], + "totalPrice": 200 + }, + { + "id": "BOOK-NOV23-003", + "customerId": "CUST008", + "status": "created", + "createdAt": "2025-11-21T11:00:00Z", + "services": [ + { "serviceId": "SRV-WASH", "serviceName": "Hårvask", "baseDuration": 30, "basePrice": 100, "resourceId": "STUDENT002" }, + { "serviceId": "SRV-HIGH", "serviceName": "Highlights", "baseDuration": 120, "basePrice": 1000, "resourceId": "EMP001" } + ], + "totalPrice": 1100, + "notes": "Split: Elev vasker, Camilla laver highlights" + }, + { + "id": "BOOK-NOV24-001", + "customerId": "CUST001", + "status": "created", + "createdAt": "2025-11-22T08:00:00Z", + "services": [ + { "serviceId": "SRV-BRYLLUP1", "serviceName": "Bryllupsfrisure Del 1", "baseDuration": 60, "basePrice": 750, "resourceId": "EMP001" }, + { "serviceId": "SRV-BRYLLUP2", "serviceName": "Bryllupsfrisure Del 2", "baseDuration": 60, "basePrice": 750, "resourceId": "EMP002" } + ], + "totalPrice": 1500, + "notes": "Equal split: Camilla og Isabella arbejder sammen" + }, + { + "id": "BOOK-NOV24-002", + "customerId": "CUST002", + "status": "created", + "createdAt": "2025-11-22T09:00:00Z", + "services": [ + { "serviceId": "SRV-FADE", "serviceName": "Fade klipning", "baseDuration": 45, "basePrice": 400, "resourceId": "EMP003" } + ], + "totalPrice": 400 + }, + { + "id": "BOOK-NOV24-003", + "customerId": "CUST003", + "status": "created", + "createdAt": "2025-11-22T10:00:00Z", + "services": [ + { "serviceId": "SRV-KLIPVASK", "serviceName": "Klipning og vask", "baseDuration": 60, "basePrice": 500, "resourceId": "EMP004" } + ], + "totalPrice": 500 + }, + { + "id": "BOOK-NOV25-001", + "customerId": "CUST004", + "status": "created", + "createdAt": "2025-11-23T08:00:00Z", + "services": [ + { "serviceId": "SRV-BALKORT", "serviceName": "Balayage kort hår", "baseDuration": 90, "basePrice": 900, "resourceId": "EMP001" } + ], + "totalPrice": 900 + }, + { + "id": "BOOK-NOV25-002", + "customerId": "CUST005", + "status": "created", + "createdAt": "2025-11-23T09:00:00Z", + "services": [ + { "serviceId": "SRV-EXT", "serviceName": "Extensions", "baseDuration": 180, "basePrice": 2500, "resourceId": "EMP002" } + ], + "totalPrice": 2500 + }, + { + "id": "BOOK-NOV25-003", + "customerId": "CUST006", + "status": "created", + "createdAt": "2025-11-23T10:00:00Z", + "services": [ + { "serviceId": "SRV-HERRESKAEG", "serviceName": "Herreklipning + skæg", "baseDuration": 60, "basePrice": 500, "resourceId": "EMP003" } + ], + "totalPrice": 500 + }, + { + "id": "BOOK-NOV26-001", + "customerId": "CUST007", + "status": "created", + "createdAt": "2025-11-24T08:00:00Z", + "services": [ + { "serviceId": "SRV-FARVKOR", "serviceName": "Farvekorrektion", "baseDuration": 180, "basePrice": 1800, "resourceId": "EMP001" } + ], + "totalPrice": 1800 + }, + { + "id": "BOOK-NOV26-002", + "customerId": "CUST008", + "status": "created", + "createdAt": "2025-11-24T09:00:00Z", + "services": [ + { "serviceId": "SRV-KERATIN", "serviceName": "Keratinbehandling", "baseDuration": 150, "basePrice": 1400, "resourceId": "EMP002" } + ], + "totalPrice": 1400 + }, + { + "id": "BOOK-NOV26-003", + "customerId": "CUST001", + "status": "created", + "createdAt": "2025-11-24T10:00:00Z", + "services": [ + { "serviceId": "SRV-SKINFADE", "serviceName": "Skin fade", "baseDuration": 45, "basePrice": 450, "resourceId": "EMP003" } + ], + "totalPrice": 450 + }, + { + "id": "BOOK-NOV27-001", + "customerId": "CUST002", + "status": "created", + "createdAt": "2025-11-25T08:00:00Z", + "services": [ + { "serviceId": "SRV-FULLCOLOR", "serviceName": "Full color", "baseDuration": 120, "basePrice": 1000, "resourceId": "EMP001" } + ], + "totalPrice": 1000 + }, + { + "id": "BOOK-NOV27-002", + "customerId": "CUST003", + "status": "created", + "createdAt": "2025-11-25T09:00:00Z", + "services": [ + { "serviceId": "SRV-WASH", "serviceName": "Hårvask", "baseDuration": 30, "basePrice": 100, "resourceId": "STUDENT001" }, + { "serviceId": "SRV-BABY", "serviceName": "Babylights", "baseDuration": 180, "basePrice": 1500, "resourceId": "EMP002" } + ], + "totalPrice": 1600, + "notes": "Split: Elev vasker, Isabella laver babylights" + }, + { + "id": "BOOK-NOV27-003", + "customerId": "CUST004", + "status": "created", + "createdAt": "2025-11-25T10:00:00Z", + "services": [ + { "serviceId": "SRV-KLASSISK", "serviceName": "Klassisk herreklip", "baseDuration": 30, "basePrice": 300, "resourceId": "EMP003" } + ], + "totalPrice": 300 } ] diff --git a/wwwroot/data/mock-events.json b/wwwroot/data/mock-events.json index b20ceab..de34e27 100644 --- a/wwwroot/data/mock-events.json +++ b/wwwroot/data/mock-events.json @@ -3965,5 +3965,354 @@ "duration": 1440, "color": "#795548" } + }, + { + "id": "RES-NOV22-001", + "title": "Balayage", + "start": "2025-11-22T09:00:00Z", + "end": "2025-11-22T11:00:00Z", + "type": "customer", + "allDay": false, + "bookingId": "BOOK-NOV22-001", + "resourceId": "EMP001", + "customerId": "CUST001", + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#9c27b0" } + }, + { + "id": "RES-NOV22-002", + "title": "Herreklipning", + "start": "2025-11-22T09:30:00Z", + "end": "2025-11-22T10:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP003", + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#3f51b5" } + }, + { + "id": "RES-NOV22-003", + "title": "Farvning", + "start": "2025-11-22T10:00:00Z", + "end": "2025-11-22T12:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP002", + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#e91e63" } + }, + { + "id": "RES-NOV22-004", + "title": "Styling", + "start": "2025-11-22T13:00:00Z", + "end": "2025-11-22T14:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP001", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#9c27b0" } + }, + { + "id": "RES-NOV22-005", + "title": "Vask og føn", + "start": "2025-11-22T11:00:00Z", + "end": "2025-11-22T11:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT001", + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#8bc34a" } + }, + { + "id": "RES-NOV22-006", + "title": "Klipning dame", + "start": "2025-11-22T14:00:00Z", + "end": "2025-11-22T15:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP004", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#009688" } + }, + { + "id": "RES-NOV23-001", + "title": "Permanent", + "start": "2025-11-23T09:00:00Z", + "end": "2025-11-23T11:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP002", + "syncStatus": "synced", + "metadata": { "duration": 150, "color": "#e91e63" } + }, + { + "id": "RES-NOV23-002", + "title": "Skæg trimning", + "start": "2025-11-23T10:00:00Z", + "end": "2025-11-23T10:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP003", + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#3f51b5" } + }, + { + "id": "RES-NOV23-003", + "title": "Highlights", + "start": "2025-11-23T12:00:00Z", + "end": "2025-11-23T14:00:00Z", + "type": "customer", + "allDay": false, + "bookingId": "BOOK-NOV22-001", + "resourceId": "EMP001", + "customerId": "CUST001", + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#9c27b0" } + }, + { + "id": "RES-NOV23-004", + "title": "Assistance", + "start": "2025-11-23T13:00:00Z", + "end": "2025-11-23T14:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT002", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#ff9800" } + }, + { + "id": "RES-NOV24-001", + "title": "Bryllupsfrisure", + "start": "2025-11-24T08:00:00Z", + "end": "2025-11-24T10:00:00Z", + "type": "customer", + "allDay": false, + "bookingId": "BOOK-NOV22-001", + "resourceId": "EMP001", + "customerId": "CUST001", + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#9c27b0" } + }, + { + "id": "RES-NOV24-002", + "title": "Ombre", + "start": "2025-11-24T10:00:00Z", + "end": "2025-11-24T12:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP002", + "syncStatus": "synced", + "metadata": { "duration": 150, "color": "#e91e63" } + }, + { + "id": "RES-NOV24-003", + "title": "Fade klipning", + "start": "2025-11-24T11:00:00Z", + "end": "2025-11-24T11:45:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP003", + "syncStatus": "synced", + "metadata": { "duration": 45, "color": "#3f51b5" } + }, + { + "id": "RES-NOV24-004", + "title": "Klipning og vask", + "start": "2025-11-24T14:00:00Z", + "end": "2025-11-24T15:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP004", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#009688" } + }, + { + "id": "RES-NOV24-005", + "title": "Grundklipning elev", + "start": "2025-11-24T13:00:00Z", + "end": "2025-11-24T14:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT001", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#8bc34a" } + }, + { + "id": "RES-NOV25-001", + "title": "Balayage kort hår", + "start": "2025-11-25T09:00:00Z", + "end": "2025-11-25T10:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP001", + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#9c27b0" } + }, + { + "id": "RES-NOV25-002", + "title": "Extensions", + "start": "2025-11-25T11:00:00Z", + "end": "2025-11-25T14:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP002", + "syncStatus": "synced", + "metadata": { "duration": 180, "color": "#e91e63" } + }, + { + "id": "RES-NOV25-003", + "title": "Herreklipning + skæg", + "start": "2025-11-25T09:00:00Z", + "end": "2025-11-25T10:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP003", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#3f51b5" } + }, + { + "id": "RES-NOV25-004", + "title": "Styling special", + "start": "2025-11-25T15:00:00Z", + "end": "2025-11-25T16:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP004", + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#009688" } + }, + { + "id": "RES-NOV25-005", + "title": "Praktik vask", + "start": "2025-11-25T10:00:00Z", + "end": "2025-11-25T10:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT002", + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff9800" } + }, + { + "id": "RES-NOV26-001", + "title": "Farvekorrektion", + "start": "2025-11-26T09:00:00Z", + "end": "2025-11-26T12:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP001", + "syncStatus": "synced", + "metadata": { "duration": 180, "color": "#9c27b0" } + }, + { + "id": "RES-NOV26-002", + "title": "Keratinbehandling", + "start": "2025-11-26T10:00:00Z", + "end": "2025-11-26T12:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP002", + "syncStatus": "synced", + "metadata": { "duration": 150, "color": "#e91e63" } + }, + { + "id": "RES-NOV26-003", + "title": "Skin fade", + "start": "2025-11-26T13:00:00Z", + "end": "2025-11-26T13:45:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP003", + "syncStatus": "synced", + "metadata": { "duration": 45, "color": "#3f51b5" } + }, + { + "id": "RES-NOV26-004", + "title": "Dameklipning lang", + "start": "2025-11-26T14:00:00Z", + "end": "2025-11-26T15:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP004", + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#009688" } + }, + { + "id": "RES-NOV26-005", + "title": "Føntørring træning", + "start": "2025-11-26T11:00:00Z", + "end": "2025-11-26T12:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT001", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#8bc34a" } + }, + { + "id": "RES-NOV27-001", + "title": "Full color", + "start": "2025-11-27T09:00:00Z", + "end": "2025-11-27T11:00:00Z", + "type": "customer", + "allDay": false, + "bookingId": "BOOK-NOV22-001", + "resourceId": "EMP001", + "customerId": "CUST001", + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#9c27b0" } + }, + { + "id": "RES-NOV27-002", + "title": "Babylights", + "start": "2025-11-27T12:00:00Z", + "end": "2025-11-27T15:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP002", + "syncStatus": "synced", + "metadata": { "duration": 180, "color": "#e91e63" } + }, + { + "id": "RES-NOV27-003", + "title": "Klassisk herreklip", + "start": "2025-11-27T10:00:00Z", + "end": "2025-11-27T10:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP003", + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#3f51b5" } + }, + { + "id": "RES-NOV27-004", + "title": "Klipning + styling", + "start": "2025-11-27T11:00:00Z", + "end": "2025-11-27T12:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "EMP004", + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#009688" } + }, + { + "id": "RES-NOV27-005", + "title": "Vask assistance", + "start": "2025-11-27T14:00:00Z", + "end": "2025-11-27T14:30:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT001", + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#8bc34a" } + }, + { + "id": "RES-NOV27-006", + "title": "Observation", + "start": "2025-11-27T15:00:00Z", + "end": "2025-11-27T16:00:00Z", + "type": "customer", + "allDay": false, + "resourceId": "STUDENT002", + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#ff9800" } } ] \ No newline at end of file diff --git a/wwwroot/data/resources.json b/wwwroot/data/resources.json deleted file mode 100644 index 3600c0a..0000000 --- a/wwwroot/data/resources.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "id": "EMP001", - "name": "camilla.jensen", - "displayName": "Camilla Jensen", - "type": "person", - "avatarUrl": "/avatars/camilla.jpg", - "color": "#9c27b0", - "isActive": true, - "metadata": { - "role": "master stylist", - "specialties": ["balayage", "color", "bridal"] - } - }, - { - "id": "EMP002", - "name": "isabella.hansen", - "displayName": "Isabella Hansen", - "type": "person", - "avatarUrl": "/avatars/isabella.jpg", - "color": "#e91e63", - "isActive": true, - "metadata": { - "role": "master stylist", - "specialties": ["highlights", "ombre", "styling"] - } - }, - { - "id": "EMP003", - "name": "alexander.nielsen", - "displayName": "Alexander Nielsen", - "type": "person", - "avatarUrl": "/avatars/alexander.jpg", - "color": "#3f51b5", - "isActive": true, - "metadata": { - "role": "master stylist", - "specialties": ["men's cuts", "beard", "fade"] - } - }, - { - "id": "EMP004", - "name": "viktor.andersen", - "displayName": "Viktor Andersen", - "type": "person", - "avatarUrl": "/avatars/viktor.jpg", - "color": "#009688", - "isActive": true, - "metadata": { - "role": "stylist", - "specialties": ["cuts", "styling", "perms"] - } - }, - { - "id": "STUDENT001", - "name": "line.pedersen", - "displayName": "Line Pedersen (Elev)", - "type": "person", - "avatarUrl": "/avatars/line.jpg", - "color": "#8bc34a", - "isActive": true, - "metadata": { - "role": "student", - "specialties": ["wash", "blow-dry", "basic cuts"] - } - }, - { - "id": "STUDENT002", - "name": "mads.larsen", - "displayName": "Mads Larsen (Elev)", - "type": "person", - "avatarUrl": "/avatars/mads.jpg", - "color": "#ff9800", - "isActive": true, - "metadata": { - "role": "student", - "specialties": ["wash", "styling assistance"] - } - } -]