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
This commit is contained in:
Janus C. H. Knudsen 2025-11-22 19:42:12 +01:00
parent a7d365b186
commit eeaeddeef8
19 changed files with 765 additions and 991 deletions

View file

@ -30,7 +30,7 @@ export class DateColumnDataSource implements IColumnDataSource {
/**
* Get columns (dates) to display
*/
public getColumns(): IColumnInfo[] {
public async getColumns(): Promise<IColumnInfo[]> {
let dates: Date[];
switch (this.currentView) {

View file

@ -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<IColumnInfo[]> {
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;
}
}

View file

@ -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<void> {
builder.registerType(MockResourceRepository).as<IApiRepository<IResource>>();
builder.registerType(MockAuditRepository).as<IApiRepository<IAuditEntry>>();
builder.registerType(DateColumnDataSource).as<IColumnDataSource>();
// 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<IColumnDataSource>();
builder.registerType(ResourceHeaderRenderer).as<IHeaderRenderer>();
} else {
builder.registerType(DateColumnDataSource).as<IColumnDataSource>();
builder.registerType(DateHeaderRenderer).as<IHeaderRenderer>();
}
// Register entity services (sync status management)
// Open/Closed Principle: Adding new entity only requires adding one line here
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
builder.registerType(BookingService).as<IEntityService<IBooking>>();
builder.registerType(CustomerService).as<IEntityService<ICustomer>>();
builder.registerType(ResourceService).as<IEntityService<IResource>>();
builder.registerType(ResourceService).as<ResourceService>();
builder.registerType(AuditService).as<AuditService>();
// Register workers
@ -151,8 +166,12 @@ async function initializeCalendar(): Promise<void> {
builder.registerType(DataSeeder).as<DataSeeder>();
// Register renderers
builder.registerType(DateHeaderRenderer).as<IHeaderRenderer>();
builder.registerType(DateColumnRenderer).as<IColumnRenderer>();
// Note: IHeaderRenderer and IColumnRenderer are registered above based on calendarMode
if (calendarMode === 'resource') {
builder.registerType(ResourceColumnRenderer).as<IColumnRenderer>();
} else {
builder.registerType(DateColumnRenderer).as<IColumnRenderer>();
}
builder.registerType(DateEventRenderer).as<IEventRenderer>();
// Register core services and utilities

View file

@ -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);

View file

@ -99,7 +99,7 @@ export class HeaderManager {
/**
* Update header content for navigation
*/
private updateHeader(currentDate: Date): void {
private async updateHeader(currentDate: Date): Promise<void> {
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 = {

View file

@ -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<void> {
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);

View file

@ -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`);
}
}
}

View file

@ -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 = `<img class="swp-resource-avatar" src="${resource.avatarUrl}" alt="${resource.displayName}" />`;
} else {
// Fallback: initials
const initials = this.getInitials(resource.displayName);
const bgColor = resource.color || '#6366f1';
avatarHtml = `<span class="swp-resource-initials" style="background-color: ${bgColor}">${initials}</span>`;
}
header.innerHTML = `
<div class="swp-resource-header">
${avatarHtml}
<span class="swp-resource-name">${resource.displayName}</span>
</div>
`;
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);
}
}

View file

@ -11,7 +11,7 @@ import { EntityType } from '../types/CalendarTypes';
export class MockAuditRepository implements IApiRepository<IAuditEntry> {
readonly entityType: EntityType = 'Audit';
async sendCreate(entity: IAuditEntry): Promise<void> {
async sendCreate(entity: IAuditEntry): Promise<IAuditEntry> {
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 100));
@ -22,9 +22,11 @@ export class MockAuditRepository implements IApiRepository<IAuditEntry> {
operation: entity.operation,
timestamp: new Date(entity.timestamp).toISOString()
});
return entity;
}
async sendUpdate(_id: string, _entity: IAuditEntry): Promise<void> {
async sendUpdate(_id: string, entity: IAuditEntry): Promise<IAuditEntry> {
// Audit entries are immutable - updates should not happen
throw new Error('Audit entries cannot be updated');
}

View file

@ -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';

View file

@ -31,68 +31,25 @@ export class ResourceService extends BaseEntityService<IResource> {
/**
* Get resources by type
*
* @param type - Resource type (person, room, equipment, etc.)
* @returns Array of resources of this type
*/
async getByType(type: string): Promise<IResource[]> {
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<IResource[]> {
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<IResource[]> {
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);
}
}

View file

@ -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 });
}
}

View file

@ -21,7 +21,7 @@ export interface IColumnDataSource {
* Get the list of columns to render
* @returns Array of column information
*/
getColumns(): IColumnInfo[];
getColumns(): Promise<IColumnInfo[]>;
/**
* Get the type of columns this datasource provides

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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"]
}
}
]

View file

@ -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
}
]

View file

@ -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" }
}
]

View file

@ -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"]
}
}
]