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
This commit is contained in:
Janus C. H. Knudsen 2025-11-20 15:25:38 +01:00
parent 871f5c5682
commit 5648c7c304
11 changed files with 1641 additions and 40 deletions

View file

@ -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<IBooking> {
public readonly entityType: EntityType = 'Booking';
private readonly dataUrl = 'data/mock-bookings.json';
/**
* Fetch all bookings from mock JSON file
*/
public async fetchAll(): Promise<IBooking[]> {
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<IBooking> {
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<IBooking>): Promise<IBooking> {
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<void> {
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
}));
}
}

View file

@ -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<string, any>;
[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<ICustomer> {
public readonly entityType: EntityType = 'Customer';
private readonly dataUrl = 'data/mock-customers.json';
/**
* Fetch all customers from mock JSON file
*/
public async fetchAll(): Promise<ICustomer[]> {
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<ICustomer> {
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<ICustomer>): Promise<ICustomer> {
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<void> {
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
}));
}
}

View file

@ -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<string, any>; // 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<ICalendarEvent> {
public readonly entityType: EntityType = 'Event';
private readonly dataUrl = 'data/mock-events.json';
public async loadEvents(): Promise<ICalendarEvent[]> {
/**
* Fetch all events from mock JSON file
*/
public async fetchAll(): Promise<ICalendarEvent[]> {
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<ICalendarEvent, 'id'>, source?: UpdateSource): Promise<ICalendarEvent> {
throw new Error('MockEventRepository does not support createEvent. Use IndexedDBEventRepository instead.');
public async sendCreate(event: ICalendarEvent): Promise<ICalendarEvent> {
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<ICalendarEvent>, source?: UpdateSource): Promise<ICalendarEvent> {
throw new Error('MockEventRepository does not support updateEvent. Use IndexedDBEventRepository instead.');
public async sendUpdate(id: string, updates: Partial<ICalendarEvent>): Promise<ICalendarEvent> {
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<void> {
throw new Error('MockEventRepository does not support deleteEvent. Use IndexedDBEventRepository instead.');
public async sendDelete(id: string): Promise<void> {
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
};
});
}
}

View file

@ -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<string, any>;
[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<IResource> {
public readonly entityType: EntityType = 'Resource';
private readonly dataUrl = 'data/mock-resources.json';
/**
* Fetch all resources from mock JSON file
*/
public async fetchAll(): Promise<IResource[]> {
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<IResource> {
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<IResource>): Promise<IResource> {
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<void> {
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
}));
}
}