Calendar/docs/mock-data-migration-guide.md
Janus C. H. Knudsen 6174dc895e Adds comprehensive mock data migration guide for booking architecture
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
2025-11-14 23:25:50 +01:00

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

  1. Booking = Customer Services ONLY

    • Bookings are ALWAYS for customers
    • Bookings ALWAYS have services
    • Vacation/break/meeting are NOT bookings
  2. Resources Assigned Per Service

    • No Booking.resourceId (removed)
    • Each IBookingService has resourceId
    • Enables split-resource and equal-split scenarios
  3. CalendarEvent.type Determines Booking Existence

    • type: 'customer' → has bookingId
    • type: 'vacation' | 'break' | 'meeting' | 'blocked' → no bookingId
  4. Denormalization for Performance

    • CalendarEvent denormalizes resourceId, customerId for 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 resourceId at booking level (moved to service level)
  • NO type field (always customer-related)
  • customerId is REQUIRED
  • services is 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" → NO bookingId
  • resourceId present (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" → NO bookingId
  • 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" → NO bookingId
  • NO customerId
  • Can have multiple attendees in metadata, but resourceId is 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 type values 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')
  • 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.json file using IResource interface

  • Update event structure (same changes as above)

  • Link events to bookings and customers

New Files to Create

  1. customers.json - Array of ICustomer objects
  2. resources.json - Array of IResource objects
  3. bookings.json - Array of IBooking objects
  4. services.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:

  1. Create new entity files (customers.json, resources.json, bookings.json)
  2. Update existing event files with new structure
  3. Test data loading with new interfaces
  4. Verify UI displays correctly with denormalized data

Related Documentation: