Documents fundamental redesign of booking and calendar event architecture, focusing on: - Service-level resource assignment - Split-resource and equal-split booking scenarios - Denormalized event data structure - Clear separation between booking and calendar events Provides detailed migration strategy and type references for future implementation
17 KiB
Mock Data Migration Guide
Purpose: This document explains the changes required to existing mock data files to support the new booking architecture with service-level resource assignment.
Date: 2025-11-14 Related: Booking & Resource Architecture Redesign
Overview of Changes
What Changed?
The booking architecture has been fundamentally redesigned to support:
- Split-resource bookings (student + master working on same booking)
- Equal-split bookings (two masters, no primary resource)
- Service-level resource assignment (resources assigned per service, not per booking)
- Clear separation between Booking (business) and CalendarEvent (scheduling)
Key Architectural Principles
-
Booking = Customer Services ONLY
- Bookings are ALWAYS for customers
- Bookings ALWAYS have services
- Vacation/break/meeting are NOT bookings
-
Resources Assigned Per Service
- No
Booking.resourceId(removed) - Each
IBookingServicehasresourceId - Enables split-resource and equal-split scenarios
- No
-
CalendarEvent.type Determines Booking Existence
type: 'customer'→ hasbookingIdtype: 'vacation' | 'break' | 'meeting' | 'blocked'→ nobookingId
-
Denormalization for Performance
CalendarEventdenormalizesresourceId,customerIdfor fast IndexedDB queries- Backend performs JOIN, frontend receives denormalized data
Data Structure Changes
1. CalendarEvent Changes
Before:
{
"id": "1",
"title": "Balayage langt hår",
"start": "2025-08-05T10:00:00",
"end": "2025-08-05T11:00:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 60 }
}
After:
{
"id": "EVT001",
"title": "Balayage langt hår",
"start": "2025-08-05T10:00:00",
"end": "2025-08-05T11:00:00",
"type": "customer",
"allDay": false,
"syncStatus": "synced",
"bookingId": "BOOK001",
"resourceId": "EMP001",
"customerId": "CUST001",
"metadata": { "duration": 60 }
}
Changes:
type: Changed from"work"to"customer"(using CalendarEventType)- NEW:
bookingId- Reference to booking (required if type = 'customer') - NEW:
resourceId- Resource who owns this time slot (denormalized) - NEW:
customerId- Customer for this event (denormalized from Booking)
2. New Entity: Booking
Structure:
{
"id": "BOOK001",
"customerId": "CUST001",
"status": "created",
"createdAt": "2025-08-05T09:00:00",
"services": [
{
"serviceId": "SRV001",
"serviceName": "Balayage langt hår",
"baseDuration": 60,
"basePrice": 800,
"customPrice": 800,
"resourceId": "EMP001"
}
],
"totalPrice": 800,
"notes": "Kunde ønsker lys blond"
}
Key Points:
- NO
resourceIdat booking level (moved to service level) - NO
typefield (always customer-related) customerIdis REQUIREDservicesis REQUIRED (array with at least one service)
3. New Entity: Customer
Structure:
{
"id": "CUST001",
"name": "Maria Jensen",
"phone": "+45 12 34 56 78",
"email": "maria.jensen@example.com",
"metadata": {
"preferredStylist": "EMP001",
"allergies": ["ammonia"]
}
}
4. New Entity: Resource
Structure:
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"avatarUrl": "/avatars/karina.jpg",
"color": "#9c27b0",
"isActive": true,
"metadata": {
"role": "master stylist",
"specialties": ["balayage", "color", "bridal"]
}
}
Complete JSON Examples
Example 1: Simple Customer Booking (Single Resource)
Scenario: One customer, one service, one resource, one time slot.
Customer:
{
"id": "CUST001",
"name": "Maria Jensen",
"phone": "+45 12 34 56 78",
"email": "maria.jensen@example.com"
}
Resource:
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"avatarUrl": "/avatars/karina.jpg",
"isActive": true
}
Booking:
{
"id": "BOOK001",
"customerId": "CUST001",
"status": "created",
"createdAt": "2025-08-05T09:00:00",
"services": [
{
"serviceId": "SRV001",
"serviceName": "Klipning og styling",
"baseDuration": 60,
"basePrice": 500,
"customPrice": 500,
"resourceId": "EMP001"
}
],
"totalPrice": 500
}
CalendarEvent:
{
"id": "EVT001",
"title": "Maria Jensen - Klipning og styling",
"start": "2025-08-05T10:00:00",
"end": "2025-08-05T11:00:00",
"type": "customer",
"allDay": false,
"syncStatus": "synced",
"bookingId": "BOOK001",
"resourceId": "EMP001",
"customerId": "CUST001"
}
Relationship:
- 1 Customer → 1 Booking → 1 Event → 1 Resource
Example 2: Split-Resource Booking (Student + Master)
Scenario: One customer, two services split between student (hair wash) and master (color treatment).
Resources:
[
{
"id": "STUDENT001",
"name": "anne.elev",
"displayName": "Anne (Elev)",
"type": "person",
"isActive": true,
"metadata": { "role": "student" }
},
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"isActive": true,
"metadata": { "role": "master stylist" }
}
]
Customer:
{
"id": "CUST002",
"name": "Louise Andersen",
"phone": "+45 23 45 67 89",
"email": "louise@example.com"
}
Booking (Single booking with 2 services, 2 different resources):
{
"id": "BOOK002",
"customerId": "CUST002",
"status": "created",
"createdAt": "2025-08-05T12:00:00",
"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": "Hårvask udføres af elev, farve af master stylist"
}
CalendarEvents (2 events, same booking, different resources):
[
{
"id": "EVT002",
"title": "Louise Andersen - Hårvask",
"start": "2025-08-05T13:00:00",
"end": "2025-08-05T13:30:00",
"type": "customer",
"allDay": false,
"syncStatus": "synced",
"bookingId": "BOOK002",
"resourceId": "STUDENT001",
"customerId": "CUST002"
},
{
"id": "EVT003",
"title": "Louise Andersen - Bundfarve",
"start": "2025-08-05T13:30:00",
"end": "2025-08-05T15:00:00",
"type": "customer",
"allDay": false,
"syncStatus": "synced",
"bookingId": "BOOK002",
"resourceId": "EMP001",
"customerId": "CUST002"
}
]
Relationship:
- 1 Customer → 1 Booking → 2 Events → 2 Resources (student + master)
Key Points:
- Both events reference the SAME
bookingId - Each event has a DIFFERENT
resourceId - No "primary" resource concept - both are equal in the booking
- Student's calendar shows EVT002, Master's calendar shows EVT003
Example 3: Equal Split Booking (Two Masters)
Scenario: One customer, one large service split equally between two master stylists.
Resources:
[
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"isActive": true
},
{
"id": "EMP002",
"name": "nanna.nielsen",
"displayName": "Nanna Nielsen",
"type": "person",
"isActive": true
}
]
Customer:
{
"id": "CUST003",
"name": "Sofie Hansen",
"phone": "+45 34 56 78 90"
}
Booking:
{
"id": "BOOK003",
"customerId": "CUST003",
"status": "created",
"createdAt": "2025-08-05T08:00:00",
"services": [
{
"serviceId": "SRV004",
"serviceName": "Bryllupsfrisure - Del 1",
"baseDuration": 60,
"basePrice": 750,
"customPrice": 750,
"resourceId": "EMP001"
},
{
"serviceId": "SRV005",
"serviceName": "Bryllupsfrisure - Del 2",
"baseDuration": 60,
"basePrice": 750,
"customPrice": 750,
"resourceId": "EMP002"
}
],
"totalPrice": 1500,
"notes": "To stylister arbejder sammen om bryllupsfrisure"
}
CalendarEvents:
[
{
"id": "EVT004",
"title": "Sofie Hansen - Bryllupsfrisure (Karina)",
"start": "2025-08-05T10:00:00",
"end": "2025-08-05T11:00:00",
"type": "customer",
"allDay": false,
"syncStatus": "synced",
"bookingId": "BOOK003",
"resourceId": "EMP001",
"customerId": "CUST003"
},
{
"id": "EVT005",
"title": "Sofie Hansen - Bryllupsfrisure (Nanna)",
"start": "2025-08-05T10:00:00",
"end": "2025-08-05T11:00:00",
"type": "customer",
"allDay": false,
"syncStatus": "synced",
"bookingId": "BOOK003",
"resourceId": "EMP002",
"customerId": "CUST003"
}
]
Relationship:
- 1 Customer → 1 Booking → 2 Events (SAME time, different resources)
- Both masters work simultaneously - no "primary" resource
Example 4: Vacation Event (No Booking)
Scenario: Resource on vacation - CalendarEvent only, NO booking.
Resource:
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"isActive": true
}
CalendarEvent:
{
"id": "EVT006",
"title": "Ferie - Spanien",
"start": "2025-08-10T00:00:00",
"end": "2025-08-17T23:59:59",
"type": "vacation",
"allDay": true,
"syncStatus": "synced",
"resourceId": "EMP001",
"metadata": {
"destination": "Mallorca",
"emergency_contact": "+45 12 34 56 78"
}
}
Key Points:
type: "vacation"→ NObookingIdresourceIdpresent (who is on vacation)- NO
customerId(not customer-related) - NO Booking object exists for this event
Example 5: Break Event (No Booking)
Scenario: Lunch break - CalendarEvent only, NO booking.
CalendarEvent:
{
"id": "EVT007",
"title": "Frokostpause",
"start": "2025-08-05T12:00:00",
"end": "2025-08-05T12:30:00",
"type": "break",
"allDay": false,
"syncStatus": "synced",
"resourceId": "EMP001"
}
Key Points:
type: "break"→ NObookingId- NO
customerId - NO Booking object
Example 6: Meeting Event (No Booking)
Scenario: Team meeting - CalendarEvent only, NO booking.
CalendarEvent:
{
"id": "EVT008",
"title": "Team møde - Salgsmål Q3",
"start": "2025-08-05T16:00:00",
"end": "2025-08-05T17:00:00",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"resourceId": "EMP001",
"metadata": {
"attendees": ["EMP001", "EMP002", "EMP003"],
"location": "Mødelokale 1"
}
}
Key Points:
type: "meeting"→ NObookingId- NO
customerId - Can have multiple attendees in metadata, but
resourceIdis the "owner"
Example 7: Queued Booking (No Event Yet)
Scenario: Customer booked services but no time slot assigned yet.
Booking:
{
"id": "BOOK004",
"customerId": "CUST004",
"status": "created",
"createdAt": "2025-08-05T14:00:00",
"services": [
{
"serviceId": "SRV001",
"serviceName": "Klipning",
"baseDuration": 45,
"basePrice": 450,
"resourceId": "EMP001"
}
],
"totalPrice": 450,
"notes": "Kunde venter på ledig tid"
}
CalendarEvents:
[]
Key Points:
- Booking EXISTS without CalendarEvent
- No time slot assigned yet (queued state)
- When time slot is assigned, CalendarEvent will be created with
bookingId: "BOOK004"
Migration Checklist
For Existing mock-events.json
-
Update all
typevalues to use CalendarEventType:"work"→"customer""meeting"→"meeting"(unchanged)- Add
"vacation","break","blocked"as needed
-
Add denormalized fields to customer events:
- Add
bookingId(if type = 'customer') - Add
resourceId(which resource owns this slot) - Add
customerId(if type = 'customer')
- Add
-
Create corresponding Booking objects for all customer events
-
Create Customer objects for all unique customers
-
Create Resource objects for all unique resources
For Existing mock-resource-events.json
-
Extract resource data to separate
resources.jsonfile using IResource interface -
Update event structure (same changes as above)
-
Link events to bookings and customers
New Files to Create
customers.json- Array of ICustomer objectsresources.json- Array of IResource objectsbookings.json- Array of IBooking objectsservices.json(optional) - Array of service definitions
Type Reference
CalendarEventType
type CalendarEventType =
| 'customer' // Customer appointment (HAS booking)
| 'vacation' // Vacation/time off (NO booking)
| 'break' // Lunch/break (NO booking)
| 'meeting' // Meeting (NO booking)
| 'blocked'; // Blocked time (NO booking)
BookingStatus
type BookingStatus =
| 'created' // AftaleOprettet
| 'arrived' // Ankommet
| 'paid' // Betalt
| 'noshow' // Udeblevet
| 'cancelled'; // Aflyst
ResourceType
type ResourceType =
| 'person' // Employee, stylist, etc.
| 'room' // Meeting room, treatment room
| 'equipment' // Chair, equipment
| 'vehicle' // Company car
| 'custom'; // Custom resource type
Quick Decision Guide
Question: Where does this data belong?
| Data | Entity | Example |
|---|---|---|
| Customer name, phone, email | Customer | { "id": "CUST001", "name": "Maria" } |
| Service name, price, duration | Booking.services | { "serviceId": "SRV001", "basePrice": 500 } |
| Which resource performs service | BookingService.resourceId | { "resourceId": "EMP001" } |
| When service happens | CalendarEvent | { "start": "...", "end": "..." } |
| Resource on vacation | CalendarEvent (type: vacation) | { "type": "vacation", "resourceId": "EMP001" } |
| Break/lunch | CalendarEvent (type: break) | { "type": "break", "resourceId": "EMP001" } |
| Team meeting | CalendarEvent (type: meeting) | { "type": "meeting" } |
Common Mistakes to Avoid
❌ Wrong: Adding resourceId to Booking
{
"id": "BOOK001",
"resourceId": "EMP001", // ❌ WRONG - removed from booking level
"customerId": "CUST001",
"services": [...]
}
Why wrong: Resources are assigned per service, not per booking.
❌ Wrong: Adding type to Booking
{
"id": "BOOK001",
"type": "customer", // ❌ WRONG - Booking has no type field
"customerId": "CUST001"
}
Why wrong: Bookings are ALWAYS customer-related. Type belongs on CalendarEvent.
❌ Wrong: Creating Booking for Vacation
{
"id": "BOOK999",
"type": "vacation", // ❌ WRONG - Vacation is NOT a booking
"resourceId": "EMP001"
}
Why wrong: Only customer services are bookings. Vacation is a CalendarEvent only.
❌ Wrong: Customer Event Without bookingId
{
"id": "EVT001",
"type": "customer",
"resourceId": "EMP001",
// ❌ MISSING: bookingId
}
Why wrong: If type: 'customer', there MUST be a bookingId.
Summary
Before:
- Events had basic structure with
type: "work" | "meeting" - No separation between business (booking) and scheduling (event)
- Resources implicitly tied to events
After:
- Clean separation: Booking (business) ↔ CalendarEvent (scheduling)
- Resources assigned at service level (enables splits)
- Denormalized data in events for performance (resourceId, customerId)
- Type-safe with CalendarEventType, BookingStatus, ResourceType
- Supports all business scenarios: split resources, equal splits, queued bookings
Next Steps:
- Create new entity files (customers.json, resources.json, bookings.json)
- Update existing event files with new structure
- Test data loading with new interfaces
- Verify UI displays correctly with denormalized data
Related Documentation: