diff --git a/docs/mock-data-migration-guide.md b/docs/mock-data-migration-guide.md new file mode 100644 index 0000000..97e3dea --- /dev/null +++ b/docs/mock-data-migration-guide.md @@ -0,0 +1,738 @@ +# 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)