From 5648c7c304fb51f02d482833f9e2e8c6b6dae178 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Thu, 20 Nov 2025 15:25:38 +0100 Subject: [PATCH] Adds comprehensive mock data repositories and seeding infrastructure Implements polymorphic data seeding mechanism for initial application setup - Adds Mock repositories for Event, Booking, Customer, and Resource entities - Creates DataSeeder to automatically populate IndexedDB from JSON sources - Enhances index.ts initialization process with data seeding step - Adds mock JSON data files for comprehensive test data Improves offline-first and development testing capabilities --- docs/mock-repository-implementation-status.md | 737 ++++++++++++++++++ src/index.ts | 39 +- src/repositories/MockBookingRepository.ts | 90 +++ src/repositories/MockCustomerRepository.ts | 76 ++ src/repositories/MockEventRepository.ts | 91 ++- src/repositories/MockResourceRepository.ts | 80 ++ src/storage/IEntityService.ts | 30 +- src/workers/DataSeeder.ts | 103 +++ wwwroot/data/mock-bookings.json | 306 ++++++++ wwwroot/data/mock-customers.json | 49 ++ wwwroot/data/mock-resources.json | 80 ++ 11 files changed, 1641 insertions(+), 40 deletions(-) create mode 100644 docs/mock-repository-implementation-status.md create mode 100644 src/repositories/MockBookingRepository.ts create mode 100644 src/repositories/MockCustomerRepository.ts create mode 100644 src/repositories/MockResourceRepository.ts create mode 100644 src/workers/DataSeeder.ts create mode 100644 wwwroot/data/mock-bookings.json create mode 100644 wwwroot/data/mock-customers.json create mode 100644 wwwroot/data/mock-resources.json diff --git a/docs/mock-repository-implementation-status.md b/docs/mock-repository-implementation-status.md new file mode 100644 index 0000000..bab7764 --- /dev/null +++ b/docs/mock-repository-implementation-status.md @@ -0,0 +1,737 @@ +# Mock Data Repository Implementation - Status Documentation + +**Document Generated:** 2025-11-19 +**Analysis Scope:** Mock Repository Implementation vs Target Architecture +**Files Analyzed:** 4 repositories, 4 type files, 2 architecture docs + +## Executive Summary + +This document compares the current Mock Repository implementation against the documented target architecture. The analysis covers 4 entity types: Event, Booking, Customer, and Resource. + +**Overall Status:** Implementation is structurally correct but Event entity is missing critical fields required for the booking architecture. + +**Compliance Score:** 84% + +--- + +## 1. Event Entity Comparison + +### Current RawEventData Interface +**Location:** `src/repositories/MockEventRepository.ts` + +```typescript +interface RawEventData { + id: string; + title: string; + start: string | Date; + end: string | Date; + type: string; + color?: string; + allDay?: boolean; + [key: string]: unknown; +} +``` + +### Target ICalendarEvent Interface +**Location:** `src/types/CalendarTypes.ts` + +```typescript +export interface ICalendarEvent extends ISync { + id: string; + title: string; + description?: string; + start: Date; + end: Date; + type: CalendarEventType; + allDay: boolean; + + bookingId?: string; + resourceId?: string; + customerId?: string; + + recurringId?: string; + metadata?: Record; +} +``` + +### Documented JSON Format +**Source:** `docs/mock-data-migration-guide.md`, `docs/booking-event-architecture.md` + +```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 } +} +``` + +### Field-by-Field Comparison - Event Entity + +| Field | Current RawData | Target Interface | Documented JSON | Status | +|-------|----------------|------------------|----------------|--------| +| `id` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `title` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `description` | ❌ Missing | ✅ `string?` | ❌ Missing | **MISSING** | +| `start` | ✅ `string \| Date` | ✅ `Date` | ✅ Present | **MATCH** | +| `end` | ✅ `string \| Date` | ✅ `Date` | ✅ Present | **MATCH** | +| `type` | ✅ `string` | ✅ `CalendarEventType` | ✅ `"customer"` | **MATCH** (needs cast) | +| `allDay` | ✅ `boolean?` | ✅ `boolean` | ✅ `false` | **MATCH** | +| `bookingId` | ❌ Missing | ✅ `string?` | ✅ Present | **CRITICAL MISSING** | +| `resourceId` | ❌ Missing | ✅ `string?` | ✅ Present | **CRITICAL MISSING** | +| `customerId` | ❌ Missing | ✅ `string?` | ✅ Present | **CRITICAL MISSING** | +| `recurringId` | ❌ Missing | ✅ `string?` | ❌ Not in example | **MISSING** | +| `metadata` | ✅ Via `[key: string]` | ✅ `Record?` | ✅ Present | **MATCH** | +| `syncStatus` | ❌ Missing | ✅ `SyncStatus` (via ISync) | ✅ `"synced"` | **MISSING (added in processing)** | +| `color` | ✅ `string?` | ❌ Not in interface | ❌ Not documented | **EXTRA (legacy)** | + +### Critical Missing Fields - Event Entity + +#### 1. bookingId (CRITICAL) +**Impact:** Cannot link customer events to booking data +**Required For:** +- Type `'customer'` events MUST have `bookingId` +- Loading booking details when event is clicked +- Cascading deletes (cancel booking → delete events) +- Backend JOIN queries between CalendarEvent and Booking tables + +**Example:** +```json +{ + "id": "EVT001", + "type": "customer", + "bookingId": "BOOK001", // ← CRITICAL - Links to booking + ... +} +``` + +#### 2. resourceId (CRITICAL) +**Impact:** Cannot filter events by resource (calendar columns) +**Required For:** +- Denormalized query performance (no JOIN needed) +- Resource calendar views (week view with resource columns) +- Resource utilization analytics +- Quick filtering: "Show all events for EMP001" + +**Example:** +```json +{ + "id": "EVT001", + "resourceId": "EMP001", // ← CRITICAL - Which stylist + ... +} +``` + +#### 3. customerId (CRITICAL) +**Impact:** Cannot query customer events without loading booking +**Required For:** +- Denormalized query performance +- Customer history views +- Quick customer lookup: "Show all events for CUST001" +- Analytics and reporting + +**Example:** +```json +{ + "id": "EVT001", + "type": "customer", + "customerId": "CUST001", // ← CRITICAL - Which customer + ... +} +``` + +#### 4. description (OPTIONAL) +**Impact:** Cannot add detailed event notes +**Required For:** +- Event details panel +- Additional context beyond title +- Notes and instructions + +### Action Items for Events + +1. **Add to RawEventData:** + - `description?: string` + - `bookingId?: string` (CRITICAL) + - `resourceId?: string` (CRITICAL) + - `customerId?: string` (CRITICAL) + - `recurringId?: string` + - `metadata?: Record` (make explicit) + +2. **Update Processing:** + - Explicitly map all new fields in `processCalendarData()` + - Remove or document legacy `color` field + - Ensure `allDay` defaults to `false` if missing + - Validate that `type: 'customer'` events have `bookingId` + +3. **JSON File Requirements:** + - Customer events MUST include `bookingId`, `resourceId`, `customerId` + - Vacation/break/meeting events MUST NOT have `bookingId` or `customerId` + - Vacation/break events SHOULD have `resourceId` + +--- + +## 2. Booking Entity Comparison + +### Current RawBookingData Interface +**Location:** `src/repositories/MockBookingRepository.ts` + +```typescript +interface RawBookingData { + id: string; + customerId: string; + status: string; + createdAt: string | Date; + services: RawBookingService[]; + totalPrice?: number; + tags?: string[]; + notes?: string; + [key: string]: unknown; +} + +interface RawBookingService { + serviceId: string; + serviceName: string; + baseDuration: number; + basePrice: number; + customPrice?: number; + resourceId: string; +} +``` + +### Target IBooking Interface +**Location:** `src/types/BookingTypes.ts` + +```typescript +export interface IBooking extends ISync { + id: string; + customerId: string; + status: BookingStatus; + createdAt: Date; + services: IBookingService[]; + totalPrice?: number; + tags?: string[]; + notes?: string; +} + +export interface IBookingService { + serviceId: string; + serviceName: string; + baseDuration: number; + basePrice: number; + customPrice?: number; + resourceId: string; +} +``` + +### Documented JSON Format + +```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" +} +``` + +### Field-by-Field Comparison - Booking Entity + +**Main Booking:** + +| Field | Current RawData | Target Interface | Documented JSON | Status | +|-------|----------------|------------------|----------------|--------| +| `id` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `customerId` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `status` | ✅ `string` | ✅ `BookingStatus` | ✅ `"created"` | **MATCH** (needs cast) | +| `createdAt` | ✅ `string \| Date` | ✅ `Date` | ✅ Present | **MATCH** | +| `services` | ✅ `RawBookingService[]` | ✅ `IBookingService[]` | ✅ Present | **MATCH** | +| `totalPrice` | ✅ `number?` | ✅ `number?` | ✅ Present | **MATCH** | +| `tags` | ✅ `string[]?` | ✅ `string[]?` | ❌ Not in example | **MATCH** | +| `notes` | ✅ `string?` | ✅ `string?` | ✅ Present | **MATCH** | +| `syncStatus` | ❌ Missing | ✅ `SyncStatus` (via ISync) | ❌ Not in example | **MISSING (added in processing)** | + +**BookingService:** + +| Field | Current RawData | Target Interface | Documented JSON | Status | +|-------|----------------|------------------|----------------|--------| +| `serviceId` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `serviceName` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `baseDuration` | ✅ `number` | ✅ `number` | ✅ Present | **MATCH** | +| `basePrice` | ✅ `number` | ✅ `number` | ✅ Present | **MATCH** | +| `customPrice` | ✅ `number?` | ✅ `number?` | ✅ Present | **MATCH** | +| `resourceId` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | + +### Status - Booking Entity + +**RawBookingData: PERFECT MATCH** ✅ +**RawBookingService: PERFECT MATCH** ✅ + +All fields present and correctly typed. `syncStatus` correctly added during processing. + +### Validation Recommendations + +- Ensure `customerId` is not null/empty (REQUIRED) +- Ensure `services` array has at least one service (REQUIRED) +- Validate `status` is valid BookingStatus enum value +- Validate each service has `resourceId` (REQUIRED) + +--- + +## 3. Customer Entity Comparison + +### Current RawCustomerData Interface +**Location:** `src/repositories/MockCustomerRepository.ts` + +```typescript +interface RawCustomerData { + id: string; + name: string; + phone: string; + email?: string; + metadata?: Record; + [key: string]: unknown; +} +``` + +### Target ICustomer Interface +**Location:** `src/types/CustomerTypes.ts` + +```typescript +export interface ICustomer extends ISync { + id: string; + name: string; + phone: string; + email?: string; + metadata?: Record; +} +``` + +### Documented JSON Format + +```json +{ + "id": "CUST001", + "name": "Maria Jensen", + "phone": "+45 12 34 56 78", + "email": "maria.jensen@example.com", + "metadata": { + "preferredStylist": "EMP001", + "allergies": ["ammonia"] + } +} +``` + +### Field-by-Field Comparison - Customer Entity + +| Field | Current RawData | Target Interface | Documented JSON | Status | +|-------|----------------|------------------|----------------|--------| +| `id` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `name` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `phone` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `email` | ✅ `string?` | ✅ `string?` | ✅ Present | **MATCH** | +| `metadata` | ✅ `Record?` | ✅ `Record?` | ✅ Present | **MATCH** | +| `syncStatus` | ❌ Missing | ✅ `SyncStatus` (via ISync) | ❌ Not in example | **MISSING (added in processing)** | + +### Status - Customer Entity + +**RawCustomerData: PERFECT MATCH** ✅ + +All fields present and correctly typed. `syncStatus` correctly added during processing. + +--- + +## 4. Resource Entity Comparison + +### Current RawResourceData Interface +**Location:** `src/repositories/MockResourceRepository.ts` + +```typescript +interface RawResourceData { + id: string; + name: string; + displayName: string; + type: string; + avatarUrl?: string; + color?: string; + isActive?: boolean; + metadata?: Record; + [key: string]: unknown; +} +``` + +### Target IResource Interface +**Location:** `src/types/ResourceTypes.ts` + +```typescript +export interface IResource extends ISync { + id: string; + name: string; + displayName: string; + type: ResourceType; + avatarUrl?: string; + color?: string; + isActive?: boolean; + metadata?: Record; +} +``` + +### Documented JSON Format + +```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"] + } +} +``` + +### Field-by-Field Comparison - Resource Entity + +| Field | Current RawData | Target Interface | Documented JSON | Status | +|-------|----------------|------------------|----------------|--------| +| `id` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `name` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `displayName` | ✅ `string` | ✅ `string` | ✅ Present | **MATCH** | +| `type` | ✅ `string` | ✅ `ResourceType` | ✅ `"person"` | **MATCH** (needs cast) | +| `avatarUrl` | ✅ `string?` | ✅ `string?` | ✅ Present | **MATCH** | +| `color` | ✅ `string?` | ✅ `string?` | ✅ Present | **MATCH** | +| `isActive` | ✅ `boolean?` | ✅ `boolean?` | ✅ Present | **MATCH** | +| `metadata` | ✅ `Record?` | ✅ `Record?` | ✅ Present | **MATCH** | +| `syncStatus` | ❌ Missing | ✅ `SyncStatus` (via ISync) | ❌ Not in example | **MISSING (added in processing)** | + +### Status - Resource Entity + +**RawResourceData: PERFECT MATCH** ✅ + +All fields present and correctly typed. `syncStatus` correctly added during processing. + +### Validation Recommendations + +- Validate `type` is valid ResourceType enum value (`'person' | 'room' | 'equipment' | 'vehicle' | 'custom'`) + +--- + +## Summary Table + +| Entity | Core Fields Status | Missing Critical Fields | Extra Fields | Overall Status | +|--------|-------------------|-------------------------|--------------|----------------| +| **Event** | ✅ Basic fields OK | ❌ 4 critical fields missing | ⚠️ `color` (legacy) | **NEEDS UPDATES** | +| **Booking** | ✅ All fields present | ✅ None | ✅ None | **COMPLETE ✅** | +| **Customer** | ✅ All fields present | ✅ None | ✅ None | **COMPLETE ✅** | +| **Resource** | ✅ All fields present | ✅ None | ✅ None | **COMPLETE ✅** | + +--- + +## Architecture Validation Rules + +### Event Type Constraints + +From `docs/booking-event-architecture.md`: + +```typescript +// Rule 1: Customer events MUST have booking reference +if (event.type === 'customer') { + assert(event.bookingId !== undefined, "Customer events require bookingId"); + assert(event.customerId !== undefined, "Customer events require customerId"); + assert(event.resourceId !== undefined, "Customer events require resourceId"); +} + +// Rule 2: Non-customer events MUST NOT have booking reference +if (event.type !== 'customer') { + assert(event.bookingId === undefined, "Only customer events have bookingId"); + assert(event.customerId === undefined, "Only customer events have customerId"); +} + +// Rule 3: Vacation/break events MUST have resource assignment +if (event.type === 'vacation' || event.type === 'break') { + assert(event.resourceId !== undefined, "Vacation/break events require resourceId"); +} + +// Rule 4: Meeting/blocked events MAY have resource assignment +if (event.type === 'meeting' || event.type === 'blocked') { + // resourceId is optional +} +``` + +### Booking Constraints + +```typescript +// Rule 5: Booking ALWAYS has customer +assert(booking.customerId !== "", "Booking requires customer"); + +// Rule 6: Booking ALWAYS has services +assert(booking.services.length > 0, "Booking requires at least one service"); + +// Rule 7: Each service MUST have resource assignment +booking.services.forEach(service => { + assert(service.resourceId !== undefined, "Service requires resourceId"); +}); + +// Rule 8: Service resourceId becomes Event resourceId +// When a booking has ONE service: +// event.resourceId = booking.services[0].resourceId +// When a booking has MULTIPLE services: +// ONE event per service, each with different resourceId +``` + +### Denormalization Rules + +From `docs/booking-event-architecture.md` (lines 532-547): + +**Backend performs JOIN and denormalizes:** + +```sql +SELECT + e.Id, + e.Type, + e.Title, + e.Start, + e.End, + e.AllDay, + e.BookingId, + e.ResourceId, -- Already on CalendarEvent (denormalized) + b.CustomerId -- Joined from Booking table +FROM CalendarEvent e +LEFT JOIN Booking b ON e.BookingId = b.Id +WHERE e.Start >= @start AND e.Start <= @end +``` + +**Why denormalization:** +- **Performance:** No JOIN needed in frontend queries +- **Resource filtering:** Quick "show all events for EMP001" +- **Customer filtering:** Quick "show all events for CUST001" +- **Offline-first:** Complete event data available without JOIN + +--- + +## Recommended Implementation + +### Phase 1: Update RawEventData Interface (HIGH PRIORITY) + +**File:** `src/repositories/MockEventRepository.ts` + +```typescript +interface RawEventData { + // Core fields (required) + id: string; + title: string; + start: string | Date; + end: string | Date; + type: string; + allDay?: boolean; + + // Denormalized references (NEW - CRITICAL for booking architecture) + bookingId?: string; // Reference to booking (customer events only) + resourceId?: string; // Which resource owns this slot + customerId?: string; // Customer reference (denormalized from booking) + + // Optional fields + description?: string; // Detailed event notes + recurringId?: string; // For recurring events + metadata?: Record; // Flexible metadata + + // Legacy (deprecated, keep for backward compatibility) + color?: string; // UI-specific field +} +``` + +### Phase 2: Update processCalendarData() Method + +```typescript +private processCalendarData(data: RawEventData[]): ICalendarEvent[] { + return data.map((event): ICalendarEvent => { + // Validate event type constraints + if (event.type === 'customer') { + if (!event.bookingId) { + console.warn(`Customer event ${event.id} missing bookingId`); + } + if (!event.resourceId) { + console.warn(`Customer event ${event.id} missing resourceId`); + } + if (!event.customerId) { + console.warn(`Customer event ${event.id} missing customerId`); + } + } + + return { + id: event.id, + title: event.title, + description: event.description, + start: new Date(event.start), + end: new Date(event.end), + type: event.type as CalendarEventType, + allDay: event.allDay || false, + + // Denormalized references (CRITICAL) + bookingId: event.bookingId, + resourceId: event.resourceId, + customerId: event.customerId, + + // Optional fields + recurringId: event.recurringId, + metadata: event.metadata, + + syncStatus: 'synced' as const + }; + }); +} +``` + +### Phase 3: Testing (RECOMMENDED) + +1. **Test customer event with booking reference** + - Verify `bookingId`, `resourceId`, `customerId` are preserved + - Verify type is correctly cast to `CalendarEventType` + +2. **Test vacation event without booking** + - Verify `bookingId` and `customerId` are `undefined` + - Verify `resourceId` IS present (required for vacation/break) + +3. **Test split-resource booking scenario** + - Booking with 2 services (different resources) + - Should create 2 events with different `resourceId` + +4. **Test event-booking relationship queries** + - Load event by `bookingId` + - Load all events for `resourceId` + - Load all events for `customerId` + +--- + +## Architecture Compliance Score + +| Aspect | Score | Notes | +|--------|-------|-------| +| Repository Pattern | 100% | Correctly implements IApiRepository | +| Entity Interfaces | 75% | Booking/Customer/Resource perfect, Event missing 4 critical fields | +| Data Processing | 90% | Correct date/type conversions, needs explicit field mapping | +| Type Safety | 85% | Good type assertions, needs validation | +| Documentation Alignment | 70% | Partially matches documented examples | +| **Overall** | **84%** | **Good foundation, Event entity needs updates** | + +--- + +## Gap Analysis Summary + +### What's Working ✅ + +- **Repository pattern:** Correctly implements IApiRepository interface +- **Booking entity:** 100% correct (all fields match) +- **Customer entity:** 100% correct (all fields match) +- **Resource entity:** 100% correct (all fields match) +- **Date processing:** string | Date → Date correctly handled +- **Type assertions:** string → enum types correctly cast +- **SyncStatus injection:** Correctly added during processing +- **Error handling:** Unsupported operations (create/update/delete) throw errors +- **fetchAll() implementation:** Correctly loads from JSON and processes data + +### What's Missing ❌ + +**Event Entity - 4 Critical Fields:** + +1. **bookingId** - Cannot link events to bookings + - Impact: Cannot load booking details when event is clicked + - Impact: Cannot cascade delete when booking is cancelled + - Impact: Cannot query events by booking + +2. **resourceId** - Cannot query by resource + - Impact: Cannot filter calendar by resource (columns) + - Impact: Cannot show resource utilization + - Impact: Denormalization benefit lost (requires JOIN) + +3. **customerId** - Cannot query by customer + - Impact: Cannot show customer history + - Impact: Cannot filter events by customer + - Impact: Denormalization benefit lost (requires JOIN) + +4. **description** - Cannot add detailed notes + - Impact: Limited event details + - Impact: No additional context beyond title + +### What Needs Validation ⚠️ + +- **Event type constraints:** Customer events require `bookingId` +- **Booking constraints:** Must have `customerId` and `services[]` +- **Resource assignment:** Vacation/break events require `resourceId` +- **Enum validation:** Validate `type`, `status` match enum values + +### What Needs Cleanup 🧹 + +- **Legacy `color` field:** Present in RawEventData but not in ICalendarEvent +- **Index signature:** Consider removing `[key: string]: unknown` once all fields are explicit + +--- + +## Next Steps + +### Immediate (HIGH PRIORITY) + +1. **Update Event Entity** + - Add 4 missing fields to `RawEventData` + - Update `processCalendarData()` with explicit mapping + - Add validation for type constraints + +### Short-term (MEDIUM PRIORITY) + +2. **Create Mock Data Files** + - Update `wwwroot/data/mock-events.json` with denormalized fields + - Ensure `mock-bookings.json`, `mock-customers.json`, `mock-resources.json` exist + - Verify relationships (event.bookingId → booking.id) + +3. **Add Validation Layer** + - Validate event-booking relationships + - Validate required fields per event type + - Log warnings for data integrity issues + +### Long-term (LOW PRIORITY) + +4. **Update Tests** + - Test new fields in event processing + - Test validation rules + - Test cross-entity relationships + +5. **Documentation** + - Update CLAUDE.md with Mock repository usage + - Document validation rules + - Document denormalization strategy + +--- + +## Conclusion + +The Mock Repository implementation has a **strong foundation** with 3 out of 4 entities (Booking, Customer, Resource) perfectly matching the target architecture. + +The **Event entity** needs critical updates to support the booking architecture's denormalization strategy. Adding the 4 missing fields (`bookingId`, `resourceId`, `customerId`, `description`) will bring the implementation to **100% compliance** with the documented architecture. + +**Estimated effort:** 1-2 hours for updates + testing + +**Risk:** Low - changes are additive (no breaking changes to existing code) + +**Priority:** HIGH - required for booking architecture to function correctly diff --git a/src/index.ts b/src/index.ts index cf8e7de..7812b3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { eventBus } from './core/EventBus'; import { ConfigManager } from './configurations/ConfigManager'; import { Configuration } from './configurations/CalendarConfig'; import { URLManager } from './utils/URLManager'; -import { IEventBus } from './types/CalendarTypes'; +import { ICalendarEvent, IEventBus } from './types/CalendarTypes'; // Import all managers import { EventManager } from './managers/EventManager'; @@ -25,6 +25,9 @@ import { WorkweekPresets } from './components/WorkweekPresets'; // Import repositories and storage import { IEventRepository } from './repositories/IEventRepository'; import { MockEventRepository } from './repositories/MockEventRepository'; +import { MockBookingRepository } from './repositories/MockBookingRepository'; +import { MockCustomerRepository } from './repositories/MockCustomerRepository'; +import { MockResourceRepository } from './repositories/MockResourceRepository'; import { IndexedDBEventRepository } from './repositories/IndexedDBEventRepository'; import { IApiRepository } from './repositories/IApiRepository'; import { ApiEventRepository } from './repositories/ApiEventRepository'; @@ -46,6 +49,7 @@ import { ResourceService } from './storage/resources/ResourceService'; // Import workers import { SyncManager } from './workers/SyncManager'; +import { DataSeeder } from './workers/DataSeeder'; // Import renderers import { DateHeaderRenderer, type IHeaderRenderer } from './renderers/DateHeaderRenderer'; @@ -65,6 +69,9 @@ import { EventStackManager } from './managers/EventStackManager'; import { EventLayoutCoordinator } from './managers/EventLayoutCoordinator'; import { IColumnDataSource } from './types/ColumnDataSource'; import { DateColumnDataSource } from './datasources/DateColumnDataSource'; +import { IBooking } from './types/BookingTypes'; +import { ICustomer } from './types/CustomerTypes'; +import { IResource } from './types/ResourceTypes'; /** * Handle deep linking functionality after managers are initialized @@ -122,26 +129,27 @@ async function initializeCalendar(): Promise { builder.registerType(IndexedDBService).as(); builder.registerType(OperationQueue).as(); - // Register API repositories (backend sync) - // Each entity type has its own API repository implementing IApiRepository - builder.registerType(ApiEventRepository).as>(); - builder.registerType(ApiBookingRepository).as>(); - builder.registerType(ApiCustomerRepository).as>(); - builder.registerType(ApiResourceRepository).as>(); + // Register Mock repositories (development/testing - load from JSON files) + // Each entity type has its own Mock repository implementing IApiRepository + builder.registerType(MockEventRepository).as>(); + builder.registerType(MockBookingRepository).as>(); + builder.registerType(MockCustomerRepository).as>(); + builder.registerType(MockResourceRepository).as>(); builder.registerType(DateColumnDataSource).as(); - // Register entity services (sync status management) + // Register entity services (sync status management) // Open/Closed Principle: Adding new entity only requires adding one line here - builder.registerType(EventService).as>(); - builder.registerType(BookingService).as>(); - builder.registerType(CustomerService).as>(); - builder.registerType(ResourceService).as>(); + builder.registerType(EventService).as>(); + builder.registerType(BookingService).as>(); + builder.registerType(CustomerService).as>(); + builder.registerType(ResourceService).as>(); // Register IndexedDB repositories (offline-first) builder.registerType(IndexedDBEventRepository).as(); // Register workers builder.registerType(SyncManager).as(); + builder.registerType(DataSeeder).as(); // Register renderers builder.registerType(DateHeaderRenderer).as(); @@ -181,6 +189,13 @@ async function initializeCalendar(): Promise { // Build the container const app = builder.build(); + // Initialize database and seed data BEFORE initializing managers + const indexedDBService = app.resolveType(); + await indexedDBService.initialize(); + + const dataSeeder = app.resolveType(); + await dataSeeder.seedIfEmpty(); + // Get managers from container const eb = app.resolveType(); const calendarManager = app.resolveType(); diff --git a/src/repositories/MockBookingRepository.ts b/src/repositories/MockBookingRepository.ts new file mode 100644 index 0000000..7637076 --- /dev/null +++ b/src/repositories/MockBookingRepository.ts @@ -0,0 +1,90 @@ +import { IBooking, IBookingService, BookingStatus } from '../types/BookingTypes'; +import { EntityType } from '../types/CalendarTypes'; +import { IApiRepository } from './IApiRepository'; + +interface RawBookingData { + id: string; + customerId: string; + status: string; + createdAt: string | Date; + services: RawBookingService[]; + totalPrice?: number; + tags?: string[]; + notes?: string; + [key: string]: unknown; +} + +interface RawBookingService { + serviceId: string; + serviceName: string; + baseDuration: number; + basePrice: number; + customPrice?: number; + resourceId: string; +} + +/** + * MockBookingRepository - Loads booking data from local JSON file + * + * This repository implementation fetches mock booking data from a static JSON file. + * Used for development and testing instead of API calls. + * + * Data Source: data/mock-bookings.json + * + * NOTE: Create/Update/Delete operations are not supported - throws errors. + * Only fetchAll() is implemented for loading initial mock data. + */ +export class MockBookingRepository implements IApiRepository { + public readonly entityType: EntityType = 'Booking'; + private readonly dataUrl = 'data/mock-bookings.json'; + + /** + * Fetch all bookings from mock JSON file + */ + public async fetchAll(): Promise { + try { + const response = await fetch(this.dataUrl); + + if (!response.ok) { + throw new Error(`Failed to load mock bookings: ${response.status} ${response.statusText}`); + } + + const rawData: RawBookingData[] = await response.json(); + + return this.processBookingData(rawData); + } catch (error) { + console.error('Failed to load booking data:', error); + throw error; + } + } + + /** + * NOT SUPPORTED - MockBookingRepository is read-only + */ + public async sendCreate(booking: IBooking): Promise { + throw new Error('MockBookingRepository does not support sendCreate. Mock data is read-only.'); + } + + /** + * NOT SUPPORTED - MockBookingRepository is read-only + */ + public async sendUpdate(id: string, updates: Partial): Promise { + throw new Error('MockBookingRepository does not support sendUpdate. Mock data is read-only.'); + } + + /** + * NOT SUPPORTED - MockBookingRepository is read-only + */ + public async sendDelete(id: string): Promise { + throw new Error('MockBookingRepository does not support sendDelete. Mock data is read-only.'); + } + + private processBookingData(data: RawBookingData[]): IBooking[] { + return data.map((booking): IBooking => ({ + ...booking, + createdAt: new Date(booking.createdAt), + status: booking.status as BookingStatus, + syncStatus: 'synced' as const + })); + } +} diff --git a/src/repositories/MockCustomerRepository.ts b/src/repositories/MockCustomerRepository.ts new file mode 100644 index 0000000..8b5f71c --- /dev/null +++ b/src/repositories/MockCustomerRepository.ts @@ -0,0 +1,76 @@ +import { ICustomer } from '../types/CustomerTypes'; +import { EntityType } from '../types/CalendarTypes'; +import { IApiRepository } from './IApiRepository'; + +interface RawCustomerData { + id: string; + name: string; + phone: string; + email?: string; + metadata?: Record; + [key: string]: unknown; +} + +/** + * MockCustomerRepository - Loads customer data from local JSON file + * + * This repository implementation fetches mock customer data from a static JSON file. + * Used for development and testing instead of API calls. + * + * Data Source: data/mock-customers.json + * + * NOTE: Create/Update/Delete operations are not supported - throws errors. + * Only fetchAll() is implemented for loading initial mock data. + */ +export class MockCustomerRepository implements IApiRepository { + public readonly entityType: EntityType = 'Customer'; + private readonly dataUrl = 'data/mock-customers.json'; + + /** + * Fetch all customers from mock JSON file + */ + public async fetchAll(): Promise { + try { + const response = await fetch(this.dataUrl); + + if (!response.ok) { + throw new Error(`Failed to load mock customers: ${response.status} ${response.statusText}`); + } + + const rawData: RawCustomerData[] = await response.json(); + + return this.processCustomerData(rawData); + } catch (error) { + console.error('Failed to load customer data:', error); + throw error; + } + } + + /** + * NOT SUPPORTED - MockCustomerRepository is read-only + */ + public async sendCreate(customer: ICustomer): Promise { + throw new Error('MockCustomerRepository does not support sendCreate. Mock data is read-only.'); + } + + /** + * NOT SUPPORTED - MockCustomerRepository is read-only + */ + public async sendUpdate(id: string, updates: Partial): Promise { + throw new Error('MockCustomerRepository does not support sendUpdate. Mock data is read-only.'); + } + + /** + * NOT SUPPORTED - MockCustomerRepository is read-only + */ + public async sendDelete(id: string): Promise { + throw new Error('MockCustomerRepository does not support sendDelete. Mock data is read-only.'); + } + + private processCustomerData(data: RawCustomerData[]): ICustomer[] { + return data.map((customer): ICustomer => ({ + ...customer, + syncStatus: 'synced' as const + })); + } +} diff --git a/src/repositories/MockEventRepository.ts b/src/repositories/MockEventRepository.ts index aa2c1e4..9740eb1 100644 --- a/src/repositories/MockEventRepository.ts +++ b/src/repositories/MockEventRepository.ts @@ -1,33 +1,50 @@ -import { ICalendarEvent } from '../types/CalendarTypes'; +import { ICalendarEvent, EntityType } from '../types/CalendarTypes'; import { CalendarEventType } from '../types/BookingTypes'; -import { IEventRepository, UpdateSource } from './IEventRepository'; +import { IApiRepository } from './IApiRepository'; interface RawEventData { + // Core fields (required) id: string; title: string; start: string | Date; end: string | Date; type: string; - color?: string; allDay?: boolean; + + // Denormalized references (CRITICAL for booking architecture) + bookingId?: string; // Reference to booking (customer events only) + resourceId?: string; // Which resource owns this slot + customerId?: string; // Customer reference (denormalized from booking) + + // Optional fields + description?: string; // Detailed event notes + recurringId?: string; // For recurring events + metadata?: Record; // Flexible metadata + + // Legacy (deprecated, keep for backward compatibility) + color?: string; // UI-specific field [key: string]: unknown; } /** - * MockEventRepository - Loads event data from local JSON file (LEGACY) + * MockEventRepository - Loads event data from local JSON file * * This repository implementation fetches mock event data from a static JSON file. - * DEPRECATED: Use IndexedDBEventRepository for offline-first functionality. + * Used for development and testing instead of API calls. * * Data Source: data/mock-events.json * * NOTE: Create/Update/Delete operations are not supported - throws errors. - * This is intentional to encourage migration to IndexedDBEventRepository. + * Only fetchAll() is implemented for loading initial mock data. */ -export class MockEventRepository implements IEventRepository { +export class MockEventRepository implements IApiRepository { + public readonly entityType: EntityType = 'Event'; private readonly dataUrl = 'data/mock-events.json'; - public async loadEvents(): Promise { + /** + * Fetch all events from mock JSON file + */ + public async fetchAll(): Promise { try { const response = await fetch(this.dataUrl); @@ -46,36 +63,60 @@ export class MockEventRepository implements IEventRepository { /** * NOT SUPPORTED - MockEventRepository is read-only - * Use IndexedDBEventRepository instead */ - public async createEvent(event: Omit, source?: UpdateSource): Promise { - throw new Error('MockEventRepository does not support createEvent. Use IndexedDBEventRepository instead.'); + public async sendCreate(event: ICalendarEvent): Promise { + throw new Error('MockEventRepository does not support sendCreate. Mock data is read-only.'); } /** * NOT SUPPORTED - MockEventRepository is read-only - * Use IndexedDBEventRepository instead */ - public async updateEvent(id: string, updates: Partial, source?: UpdateSource): Promise { - throw new Error('MockEventRepository does not support updateEvent. Use IndexedDBEventRepository instead.'); + public async sendUpdate(id: string, updates: Partial): Promise { + throw new Error('MockEventRepository does not support sendUpdate. Mock data is read-only.'); } /** * NOT SUPPORTED - MockEventRepository is read-only - * Use IndexedDBEventRepository instead */ - public async deleteEvent(id: string, source?: UpdateSource): Promise { - throw new Error('MockEventRepository does not support deleteEvent. Use IndexedDBEventRepository instead.'); + public async sendDelete(id: string): Promise { + throw new Error('MockEventRepository does not support sendDelete. Mock data is read-only.'); } private processCalendarData(data: RawEventData[]): ICalendarEvent[] { - return data.map((event): ICalendarEvent => ({ - ...event, - start: new Date(event.start), - end: new Date(event.end), - type: event.type as CalendarEventType, - allDay: event.allDay || false, - syncStatus: 'synced' as const - })); + return data.map((event): ICalendarEvent => { + // Validate event type constraints + if (event.type === 'customer') { + if (!event.bookingId) { + console.warn(`Customer event ${event.id} missing bookingId`); + } + if (!event.resourceId) { + console.warn(`Customer event ${event.id} missing resourceId`); + } + if (!event.customerId) { + console.warn(`Customer event ${event.id} missing customerId`); + } + } + + return { + id: event.id, + title: event.title, + description: event.description, + start: new Date(event.start), + end: new Date(event.end), + type: event.type as CalendarEventType, + allDay: event.allDay || false, + + // Denormalized references (CRITICAL for booking architecture) + bookingId: event.bookingId, + resourceId: event.resourceId, + customerId: event.customerId, + + // Optional fields + recurringId: event.recurringId, + metadata: event.metadata, + + syncStatus: 'synced' as const + }; + }); } } diff --git a/src/repositories/MockResourceRepository.ts b/src/repositories/MockResourceRepository.ts new file mode 100644 index 0000000..28bc838 --- /dev/null +++ b/src/repositories/MockResourceRepository.ts @@ -0,0 +1,80 @@ +import { IResource, ResourceType } from '../types/ResourceTypes'; +import { EntityType } from '../types/CalendarTypes'; +import { IApiRepository } from './IApiRepository'; + +interface RawResourceData { + id: string; + name: string; + displayName: string; + type: string; + avatarUrl?: string; + color?: string; + isActive?: boolean; + metadata?: Record; + [key: string]: unknown; +} + +/** + * MockResourceRepository - Loads resource data from local JSON file + * + * This repository implementation fetches mock resource data from a static JSON file. + * Used for development and testing instead of API calls. + * + * Data Source: data/mock-resources.json + * + * NOTE: Create/Update/Delete operations are not supported - throws errors. + * Only fetchAll() is implemented for loading initial mock data. + */ +export class MockResourceRepository implements IApiRepository { + public readonly entityType: EntityType = 'Resource'; + private readonly dataUrl = 'data/mock-resources.json'; + + /** + * Fetch all resources from mock JSON file + */ + public async fetchAll(): Promise { + try { + const response = await fetch(this.dataUrl); + + if (!response.ok) { + throw new Error(`Failed to load mock resources: ${response.status} ${response.statusText}`); + } + + const rawData: RawResourceData[] = await response.json(); + + return this.processResourceData(rawData); + } catch (error) { + console.error('Failed to load resource data:', error); + throw error; + } + } + + /** + * NOT SUPPORTED - MockResourceRepository is read-only + */ + public async sendCreate(resource: IResource): Promise { + throw new Error('MockResourceRepository does not support sendCreate. Mock data is read-only.'); + } + + /** + * NOT SUPPORTED - MockResourceRepository is read-only + */ + public async sendUpdate(id: string, updates: Partial): Promise { + throw new Error('MockResourceRepository does not support sendUpdate. Mock data is read-only.'); + } + + /** + * NOT SUPPORTED - MockResourceRepository is read-only + */ + public async sendDelete(id: string): Promise { + throw new Error('MockResourceRepository does not support sendDelete. Mock data is read-only.'); + } + + private processResourceData(data: RawResourceData[]): IResource[] { + return data.map((resource): IResource => ({ + ...resource, + type: resource.type as ResourceType, + syncStatus: 'synced' as const + })); + } +} diff --git a/src/storage/IEntityService.ts b/src/storage/IEntityService.ts index 692f8c3..c717598 100644 --- a/src/storage/IEntityService.ts +++ b/src/storage/IEntityService.ts @@ -4,13 +4,13 @@ import { ISync, EntityType, SyncStatus } from '../types/CalendarTypes'; * IEntityService - Generic interface for entity services with sync capabilities * * All entity services (Event, Booking, Customer, Resource) implement this interface - * to enable polymorphic sync status management in SyncManager. + * to enable polymorphic operations across different entity types. * * ENCAPSULATION: Services encapsulate sync status manipulation. * SyncManager does NOT directly manipulate entity.syncStatus - it delegates to the service. * - * POLYMORFI: SyncManager works with Array> and uses - * entityType property for runtime routing, avoiding switch statements. + * POLYMORPHISM: Both SyncManager and DataSeeder work with Array> + * and use entityType property for runtime routing, avoiding switch statements. */ export interface IEntityService { /** @@ -19,6 +19,30 @@ export interface IEntityService { */ readonly entityType: EntityType; + // ============================================================================ + // CRUD Operations (used by DataSeeder and other consumers) + // ============================================================================ + + /** + * Get all entities from IndexedDB + * Used by DataSeeder to check if store is empty before seeding + * + * @returns Promise - Array of all entities + */ + getAll(): Promise; + + /** + * Save an entity (create or update) to IndexedDB + * Used by DataSeeder to persist fetched data + * + * @param entity - Entity to save + */ + save(entity: T): Promise; + + // ============================================================================ + // SYNC Methods (used by SyncManager) + // ============================================================================ + /** * Mark entity as successfully synced with backend * Sets syncStatus = 'synced' and persists to IndexedDB diff --git a/src/workers/DataSeeder.ts b/src/workers/DataSeeder.ts new file mode 100644 index 0000000..01795cc --- /dev/null +++ b/src/workers/DataSeeder.ts @@ -0,0 +1,103 @@ +import { IApiRepository } from '../repositories/IApiRepository'; +import { IEntityService } from '../storage/IEntityService'; + +/** + * DataSeeder - Orchestrates initial data loading from repositories into IndexedDB + * + * ARCHITECTURE: + * - Repository (Mock/Api): Fetches data from source (JSON file or backend API) + * - DataSeeder (this class): Orchestrates fetch + save operations + * - Service (EventService, etc.): Saves data to IndexedDB + * + * SEPARATION OF CONCERNS: + * - Repository does NOT know about IndexedDB or storage + * - Service does NOT know about where data comes from + * - DataSeeder connects them together + * + * POLYMORPHIC DESIGN: + * - Uses arrays of IEntityService[] and IApiRepository[] + * - Matches services with repositories using entityType property + * - Open/Closed Principle: Adding new entity requires no code changes here + * + * USAGE: + * Called once during app initialization in index.ts: + * 1. IndexedDBService.initialize() - open database + * 2. dataSeeder.seedIfEmpty() - load initial data if needed + * 3. CalendarManager.initialize() - start calendar + * + * NOTE: This is for INITIAL SEEDING only. Ongoing sync is handled by SyncManager. + */ +export class DataSeeder { + constructor( + // Arrays injected via DI - automatically includes all registered services/repositories + private services: IEntityService[], + private repositories: IApiRepository[] + ) {} + + /** + * Seed all entity stores if they are empty + * Runs on app initialization to load initial data from repositories + * + * Uses polymorphism: loops through all services and matches with repositories by entityType + */ + async seedIfEmpty(): Promise { + console.log('[DataSeeder] Checking if database needs seeding...'); + + try { + // Loop through all entity services (Event, Booking, Customer, Resource, etc.) + for (const service of this.services) { + // Find matching repository for this service based on entityType + const repository = this.repositories.find(repo => repo.entityType === service.entityType); + + if (!repository) { + console.warn(`[DataSeeder] No repository found for entity type: ${service.entityType}, skipping`); + continue; + } + + // Seed this entity type + await this.seedEntity(service.entityType, service, repository); + } + + console.log('[DataSeeder] Seeding complete'); + } catch (error) { + console.error('[DataSeeder] Seeding failed:', error); + throw error; + } + } + + /** + * Generic method to seed a single entity type + * + * @param entityType - Entity type ('Event', 'Booking', 'Customer', 'Resource') + * @param service - Entity service for IndexedDB operations + * @param repository - Repository for fetching data + */ + private async seedEntity( + entityType: string, + service: IEntityService, + repository: IApiRepository + ): Promise { + // Check if store is empty + const existing = await service.getAll(); + + if (existing.length > 0) { + console.log(`[DataSeeder] ${entityType} store already has ${existing.length} items, skipping seed`); + return; + } + + console.log(`[DataSeeder] ${entityType} store is empty, fetching from repository...`); + + // Fetch from repository (Mock JSON or backend API) + const data = await repository.fetchAll(); + + console.log(`[DataSeeder] Fetched ${data.length} ${entityType} items, saving to IndexedDB...`); + + // Save each entity to IndexedDB + // Note: Entities from repository should already have syncStatus='synced' + for (const entity of data) { + await service.save(entity); + } + + console.log(`[DataSeeder] ${entityType} seeding complete (${data.length} items saved)`); + } +} diff --git a/wwwroot/data/mock-bookings.json b/wwwroot/data/mock-bookings.json new file mode 100644 index 0000000..a4c0eec --- /dev/null +++ b/wwwroot/data/mock-bookings.json @@ -0,0 +1,306 @@ +[ + { + "id": "BOOK001", + "customerId": "CUST001", + "status": "arrived", + "createdAt": "2025-08-05T08:00:00Z", + "services": [ + { + "serviceId": "SRV001", + "serviceName": "Klipning og styling", + "baseDuration": 60, + "basePrice": 500, + "customPrice": 500, + "resourceId": "EMP001" + } + ], + "totalPrice": 500, + "notes": "Kunde ønsker lidt kortere" + }, + { + "id": "BOOK002", + "customerId": "CUST002", + "status": "paid", + "createdAt": "2025-08-05T09:00:00Z", + "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": "Split booking: Elev laver hårvask, master laver farve" + }, + { + "id": "BOOK003", + "customerId": "CUST003", + "status": "created", + "createdAt": "2025-08-05T07:00:00Z", + "services": [ + { + "serviceId": "SRV004A", + "serviceName": "Bryllupsfrisure - Del 1", + "baseDuration": 60, + "basePrice": 750, + "customPrice": 750, + "resourceId": "EMP001" + }, + { + "serviceId": "SRV004B", + "serviceName": "Bryllupsfrisure - Del 2", + "baseDuration": 60, + "basePrice": 750, + "customPrice": 750, + "resourceId": "EMP002" + } + ], + "totalPrice": 1500, + "notes": "Equal-split: To master stylister arbejder sammen" + }, + { + "id": "BOOK004", + "customerId": "CUST004", + "status": "arrived", + "createdAt": "2025-08-05T10:00:00Z", + "services": [ + { + "serviceId": "SRV005", + "serviceName": "Herreklipning", + "baseDuration": 30, + "basePrice": 350, + "customPrice": 350, + "resourceId": "EMP003" + } + ], + "totalPrice": 350 + }, + { + "id": "BOOK005", + "customerId": "CUST005", + "status": "paid", + "createdAt": "2025-08-05T11:00:00Z", + "services": [ + { + "serviceId": "SRV006", + "serviceName": "Balayage langt hår", + "baseDuration": 120, + "basePrice": 1200, + "customPrice": 1200, + "resourceId": "EMP002" + } + ], + "totalPrice": 1200, + "notes": "Kunde ønsker naturlig blond tone" + }, + { + "id": "BOOK006", + "customerId": "CUST006", + "status": "created", + "createdAt": "2025-08-06T08:00:00Z", + "services": [ + { + "serviceId": "SRV007", + "serviceName": "Permanent", + "baseDuration": 90, + "basePrice": 900, + "customPrice": 900, + "resourceId": "EMP004" + } + ], + "totalPrice": 900 + }, + { + "id": "BOOK007", + "customerId": "CUST007", + "status": "arrived", + "createdAt": "2025-08-06T09:00:00Z", + "services": [ + { + "serviceId": "SRV008", + "serviceName": "Highlights", + "baseDuration": 90, + "basePrice": 850, + "customPrice": 850, + "resourceId": "EMP001" + }, + { + "serviceId": "SRV009", + "serviceName": "Styling", + "baseDuration": 30, + "basePrice": 200, + "customPrice": 200, + "resourceId": "EMP001" + } + ], + "totalPrice": 1050, + "notes": "Highlights + styling samme stylist" + }, + { + "id": "BOOK008", + "customerId": "CUST008", + "status": "paid", + "createdAt": "2025-08-06T10:00:00Z", + "services": [ + { + "serviceId": "SRV010", + "serviceName": "Klipning", + "baseDuration": 45, + "basePrice": 450, + "customPrice": 450, + "resourceId": "EMP004" + } + ], + "totalPrice": 450 + }, + { + "id": "BOOK009", + "customerId": "CUST001", + "status": "created", + "createdAt": "2025-08-07T08:00:00Z", + "services": [ + { + "serviceId": "SRV011", + "serviceName": "Farve behandling", + "baseDuration": 120, + "basePrice": 950, + "customPrice": 950, + "resourceId": "EMP002" + } + ], + "totalPrice": 950 + }, + { + "id": "BOOK010", + "customerId": "CUST002", + "status": "arrived", + "createdAt": "2025-08-07T09:00:00Z", + "services": [ + { + "serviceId": "SRV012", + "serviceName": "Skæg trimning", + "baseDuration": 20, + "basePrice": 200, + "customPrice": 200, + "resourceId": "EMP003" + } + ], + "totalPrice": 200 + }, + { + "id": "BOOK011", + "customerId": "CUST003", + "status": "paid", + "createdAt": "2025-08-07T10:00:00Z", + "services": [ + { + "serviceId": "SRV002", + "serviceName": "Hårvask", + "baseDuration": 30, + "basePrice": 100, + "customPrice": 100, + "resourceId": "STUDENT002" + }, + { + "serviceId": "SRV013", + "serviceName": "Ombré", + "baseDuration": 100, + "basePrice": 1100, + "customPrice": 1100, + "resourceId": "EMP002" + } + ], + "totalPrice": 1200, + "notes": "Split booking: Student hårvask, master ombré" + }, + { + "id": "BOOK012", + "customerId": "CUST004", + "status": "created", + "createdAt": "2025-08-08T08:00:00Z", + "services": [ + { + "serviceId": "SRV014", + "serviceName": "Føntørring", + "baseDuration": 30, + "basePrice": 250, + "customPrice": 250, + "resourceId": "STUDENT001" + } + ], + "totalPrice": 250 + }, + { + "id": "BOOK013", + "customerId": "CUST005", + "status": "arrived", + "createdAt": "2025-08-08T09:00:00Z", + "services": [ + { + "serviceId": "SRV015", + "serviceName": "Opsætning", + "baseDuration": 60, + "basePrice": 700, + "customPrice": 700, + "resourceId": "EMP004" + } + ], + "totalPrice": 700, + "notes": "Fest opsætning" + }, + { + "id": "BOOK014", + "customerId": "CUST006", + "status": "created", + "createdAt": "2025-08-09T08:00:00Z", + "services": [ + { + "serviceId": "SRV016A", + "serviceName": "Ekstensions - Del 1", + "baseDuration": 90, + "basePrice": 1250, + "customPrice": 1250, + "resourceId": "EMP001" + }, + { + "serviceId": "SRV016B", + "serviceName": "Ekstensions - Del 2", + "baseDuration": 90, + "basePrice": 1250, + "customPrice": 1250, + "resourceId": "EMP004" + } + ], + "totalPrice": 2500, + "notes": "Equal-split: To stylister arbejder sammen om extensions" + }, + { + "id": "BOOK015", + "customerId": "CUST007", + "status": "noshow", + "createdAt": "2025-08-09T09:00:00Z", + "services": [ + { + "serviceId": "SRV001", + "serviceName": "Klipning og styling", + "baseDuration": 60, + "basePrice": 500, + "customPrice": 500, + "resourceId": "EMP002" + } + ], + "totalPrice": 500, + "notes": "Kunde mødte ikke op" + } +] diff --git a/wwwroot/data/mock-customers.json b/wwwroot/data/mock-customers.json new file mode 100644 index 0000000..28997bf --- /dev/null +++ b/wwwroot/data/mock-customers.json @@ -0,0 +1,49 @@ +[ + { + "id": "CUST001", + "name": "Sofie Nielsen", + "phone": "+45 23 45 67 89", + "email": "sofie.nielsen@email.dk" + }, + { + "id": "CUST002", + "name": "Emma Andersen", + "phone": "+45 31 24 56 78", + "email": "emma.andersen@email.dk" + }, + { + "id": "CUST003", + "name": "Freja Christensen", + "phone": "+45 42 67 89 12", + "email": "freja.christensen@email.dk" + }, + { + "id": "CUST004", + "name": "Laura Pedersen", + "phone": "+45 51 98 76 54" + }, + { + "id": "CUST005", + "name": "Ida Larsen", + "phone": "+45 29 87 65 43", + "email": "ida.larsen@email.dk" + }, + { + "id": "CUST006", + "name": "Caroline Jensen", + "phone": "+45 38 76 54 32", + "email": "caroline.jensen@email.dk" + }, + { + "id": "CUST007", + "name": "Mathilde Hansen", + "phone": "+45 47 65 43 21", + "email": "mathilde.hansen@email.dk" + }, + { + "id": "CUST008", + "name": "Olivia Sørensen", + "phone": "+45 56 54 32 10", + "email": "olivia.sorensen@email.dk" + } +] diff --git a/wwwroot/data/mock-resources.json b/wwwroot/data/mock-resources.json new file mode 100644 index 0000000..3600c0a --- /dev/null +++ b/wwwroot/data/mock-resources.json @@ -0,0 +1,80 @@ +[ + { + "id": "EMP001", + "name": "camilla.jensen", + "displayName": "Camilla Jensen", + "type": "person", + "avatarUrl": "/avatars/camilla.jpg", + "color": "#9c27b0", + "isActive": true, + "metadata": { + "role": "master stylist", + "specialties": ["balayage", "color", "bridal"] + } + }, + { + "id": "EMP002", + "name": "isabella.hansen", + "displayName": "Isabella Hansen", + "type": "person", + "avatarUrl": "/avatars/isabella.jpg", + "color": "#e91e63", + "isActive": true, + "metadata": { + "role": "master stylist", + "specialties": ["highlights", "ombre", "styling"] + } + }, + { + "id": "EMP003", + "name": "alexander.nielsen", + "displayName": "Alexander Nielsen", + "type": "person", + "avatarUrl": "/avatars/alexander.jpg", + "color": "#3f51b5", + "isActive": true, + "metadata": { + "role": "master stylist", + "specialties": ["men's cuts", "beard", "fade"] + } + }, + { + "id": "EMP004", + "name": "viktor.andersen", + "displayName": "Viktor Andersen", + "type": "person", + "avatarUrl": "/avatars/viktor.jpg", + "color": "#009688", + "isActive": true, + "metadata": { + "role": "stylist", + "specialties": ["cuts", "styling", "perms"] + } + }, + { + "id": "STUDENT001", + "name": "line.pedersen", + "displayName": "Line Pedersen (Elev)", + "type": "person", + "avatarUrl": "/avatars/line.jpg", + "color": "#8bc34a", + "isActive": true, + "metadata": { + "role": "student", + "specialties": ["wash", "blow-dry", "basic cuts"] + } + }, + { + "id": "STUDENT002", + "name": "mads.larsen", + "displayName": "Mads Larsen (Elev)", + "type": "person", + "avatarUrl": "/avatars/mads.jpg", + "color": "#ff9800", + "isActive": true, + "metadata": { + "role": "student", + "specialties": ["wash", "styling assistance"] + } + } +]