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

738 lines
17 KiB
Markdown

# 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](../coding-sessions/2025-11-14-booking-resource-architecture-redesign.md)
---
## 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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"id": "CUST001",
"name": "Maria Jensen",
"phone": "+45 12 34 56 78",
"email": "maria.jensen@example.com"
}
```
**Resource:**
```json
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"avatarUrl": "/avatars/karina.jpg",
"isActive": true
}
```
**Booking:**
```json
{
"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:**
```json
{
"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:**
```json
[
{
"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:**
```json
{
"id": "CUST002",
"name": "Louise Andersen",
"phone": "+45 23 45 67 89",
"email": "louise@example.com"
}
```
**Booking (Single booking with 2 services, 2 different resources):**
```json
{
"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):**
```json
[
{
"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:**
```json
[
{
"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:**
```json
{
"id": "CUST003",
"name": "Sofie Hansen",
"phone": "+45 34 56 78 90"
}
```
**Booking:**
```json
{
"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:**
```json
[
{
"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:**
```json
{
"id": "EMP001",
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"type": "person",
"isActive": true
}
```
**CalendarEvent:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
[]
```
**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
```typescript
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
```typescript
type BookingStatus =
| 'created' // AftaleOprettet
| 'arrived' // Ankommet
| 'paid' // Betalt
| 'noshow' // Udeblevet
| 'cancelled'; // Aflyst
```
### ResourceType
```typescript
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
```json
{
"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
```json
{
"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
```json
{
"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
```json
{
"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:**
- [Booking & Resource Architecture Redesign](../coding-sessions/2025-11-14-booking-resource-architecture-redesign.md)
- [Booking Event Architecture](./booking-event-architecture.md)