739 lines
17 KiB
Markdown
739 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)
|