diff --git a/src/v2/V2CompositionRoot.ts b/src/v2/V2CompositionRoot.ts index ed34959..a71d912 100644 --- a/src/v2/V2CompositionRoot.ts +++ b/src/v2/V2CompositionRoot.ts @@ -15,7 +15,7 @@ import { DemoApp } from './demo/DemoApp'; // Event system import { EventBus } from './core/EventBus'; -import { IEventBus, ICalendarEvent, ISync } from './types/CalendarTypes'; +import { IEventBus, ICalendarEvent, ISync, IResource, IBooking, ICustomer } from './types/CalendarTypes'; // Storage import { IndexedDBContext } from './storage/IndexedDBContext'; @@ -23,10 +23,19 @@ import { IStore } from './storage/IStore'; import { IEntityService } from './storage/IEntityService'; import { EventStore } from './storage/events/EventStore'; import { EventService } from './storage/events/EventService'; +import { ResourceStore } from './storage/resources/ResourceStore'; +import { ResourceService } from './storage/resources/ResourceService'; +import { BookingStore } from './storage/bookings/BookingStore'; +import { BookingService } from './storage/bookings/BookingService'; +import { CustomerStore } from './storage/customers/CustomerStore'; +import { CustomerService } from './storage/customers/CustomerService'; // Repositories import { IApiRepository } from './repositories/IApiRepository'; import { MockEventRepository } from './repositories/MockEventRepository'; +import { MockResourceRepository } from './repositories/MockResourceRepository'; +import { MockBookingRepository } from './repositories/MockBookingRepository'; +import { MockCustomerRepository } from './repositories/MockCustomerRepository'; // Workers import { DataSeeder } from './workers/DataSeeder'; @@ -58,17 +67,43 @@ export function createV2Container(): Container { // Storage infrastructure builder.registerType(IndexedDBContext).as(); - builder.registerType(EventStore).as(); - // Entity services + // Stores (for IndexedDB schema creation) + builder.registerType(EventStore).as(); + builder.registerType(ResourceStore).as(); + builder.registerType(BookingStore).as(); + builder.registerType(CustomerStore).as(); + + // Entity services (for DataSeeder polymorphic array) builder.registerType(EventService).as>(); builder.registerType(EventService).as>(); builder.registerType(EventService).as(); - // Repositories + builder.registerType(ResourceService).as>(); + builder.registerType(ResourceService).as>(); + builder.registerType(ResourceService).as(); + + builder.registerType(BookingService).as>(); + builder.registerType(BookingService).as>(); + builder.registerType(BookingService).as(); + + builder.registerType(CustomerService).as>(); + builder.registerType(CustomerService).as>(); + builder.registerType(CustomerService).as(); + + // Repositories (for DataSeeder polymorphic array) builder.registerType(MockEventRepository).as>(); builder.registerType(MockEventRepository).as>(); + builder.registerType(MockResourceRepository).as>(); + builder.registerType(MockResourceRepository).as>(); + + builder.registerType(MockBookingRepository).as>(); + builder.registerType(MockBookingRepository).as>(); + + builder.registerType(MockCustomerRepository).as>(); + builder.registerType(MockCustomerRepository).as>(); + // Workers builder.registerType(DataSeeder).as(); diff --git a/src/v2/core/CalendarOrchestrator.ts b/src/v2/core/CalendarOrchestrator.ts index ecad40b..09f9c87 100644 --- a/src/v2/core/CalendarOrchestrator.ts +++ b/src/v2/core/CalendarOrchestrator.ts @@ -43,9 +43,8 @@ export class CalendarOrchestrator { const pipeline = buildPipeline(activeRenderers); pipeline.run(context); - // Events - const dates = filter['date'] || []; - await this.eventRenderer.render(container, dates); + // Render events med hele filter (date + resource) + await this.eventRenderer.render(container, filter); } private selectRenderers(viewConfig: ViewConfig): Renderer[] { diff --git a/src/v2/demo/DemoApp.ts b/src/v2/demo/DemoApp.ts index f18db5b..da5ec78 100644 --- a/src/v2/demo/DemoApp.ts +++ b/src/v2/demo/DemoApp.ts @@ -12,7 +12,7 @@ export class DemoApp { private animator!: NavigationAnimator; private container!: HTMLElement; private weekOffset = 0; - private currentView: 'simple' | 'resource' | 'team' = 'simple'; + private currentView: 'day' | 'simple' | 'resource' | 'team' = 'simple'; constructor( private orchestrator: CalendarOrchestrator, @@ -66,8 +66,18 @@ export class DemoApp { private buildViewConfig(): ViewConfig { const dates = this.dateService.getWeekDates(this.weekOffset, 3); + const today = this.dateService.getWeekDates(this.weekOffset, 1); switch (this.currentView) { + case 'day': + return { + templateId: 'day', + groupings: [ + { type: 'resource', values: ['res1', 'res2'] }, + { type: 'date', values: today } + ] + }; + case 'simple': return { templateId: 'simple', @@ -110,6 +120,11 @@ export class DemoApp { } private setupViewSwitching(): void { + document.getElementById('btn-day')?.addEventListener('click', () => { + this.currentView = 'day'; + this.render(); + }); + document.getElementById('btn-simple')?.addEventListener('click', () => { this.currentView = 'simple'; this.render(); diff --git a/src/v2/features/date/DateRenderer.ts b/src/v2/features/date/DateRenderer.ts index cae48bd..87a4622 100644 --- a/src/v2/features/date/DateRenderer.ts +++ b/src/v2/features/date/DateRenderer.ts @@ -8,16 +8,23 @@ export class DateRenderer implements Renderer { render(context: RenderContext): void { const dates = context.filter['date'] || []; - const resourceCount = context.filter['resource']?.length || 1; + const resourceIds = context.filter['resource'] || []; + + // Render dates for HVER resource (eller 1 gang hvis ingen resources) + const iterations = resourceIds.length || 1; + + for (let r = 0; r < iterations; r++) { + const resourceId = resourceIds[r]; // undefined hvis ingen resources - // Render dates for HVER resource (resourceCount gange) - for (let r = 0; r < resourceCount; r++) { for (const dateStr of dates) { const date = this.dateService.parseISO(dateStr); // Header const header = document.createElement('swp-day-header'); header.dataset.date = dateStr; + if (resourceId) { + header.dataset.resourceId = resourceId; + } header.innerHTML = ` ${this.dateService.getDayName(date, 'short')} ${date.getDate()} @@ -27,6 +34,9 @@ export class DateRenderer implements Renderer { // Column const column = document.createElement('swp-day-column'); column.dataset.date = dateStr; + if (resourceId) { + column.dataset.resourceId = resourceId; + } column.innerHTML = ''; context.columnContainer.appendChild(column); } diff --git a/src/v2/features/event/EventRenderer.ts b/src/v2/features/event/EventRenderer.ts index 2ec8d3b..cf6d9c7 100644 --- a/src/v2/features/event/EventRenderer.ts +++ b/src/v2/features/event/EventRenderer.ts @@ -21,8 +21,14 @@ export class EventRenderer { /** * Render events for visible dates into day columns + * @param container - Calendar container element + * @param filter - Filter with 'date' and optionally 'resource' arrays */ - async render(container: HTMLElement, visibleDates: string[]): Promise { + async render(container: HTMLElement, filter: Record): Promise { + const visibleDates = filter['date'] || []; + + if (visibleDates.length === 0) return; + // Get date range for query const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); @@ -31,19 +37,32 @@ export class EventRenderer { // Fetch events from IndexedDB const events = await this.eventService.getByDateRange(startDate, endDate); - // Group events by date - const eventsByDate = this.groupEventsByDate(events); - // Find day columns const dayColumns = container.querySelector('swp-day-columns'); if (!dayColumns) return; const columns = dayColumns.querySelectorAll('swp-day-column'); - // Render events into columns - columns.forEach((column, index) => { - const dateKey = visibleDates[index]; - const dateEvents = eventsByDate.get(dateKey) || []; + // Render events into each column based on data attributes + columns.forEach(column => { + const dateKey = (column as HTMLElement).dataset.date; + const columnResourceId = (column as HTMLElement).dataset.resourceId; + + if (!dateKey) return; + + // Filter events for this column + const columnEvents = events.filter(event => { + // Must match date + if (getDateKey(event.start) !== dateKey) return false; + + // If column has resourceId, event must match + if (columnResourceId && event.resourceId !== columnResourceId) return false; + + // If no resourceId on column but resources in filter, show all + // (this handles 'simple' view without resources) + + return true; + }); // Get or create events layer let eventsLayer = column.querySelector('swp-events-layer'); @@ -55,8 +74,8 @@ export class EventRenderer { // Clear existing events eventsLayer.innerHTML = ''; - // Render each event - dateEvents.forEach(event => { + // Render each timed event + columnEvents.forEach(event => { if (!event.allDay) { const eventElement = this.createEventElement(event); eventsLayer!.appendChild(eventElement); @@ -65,22 +84,6 @@ export class EventRenderer { }); } - /** - * Group events by their date key - */ - private groupEventsByDate(events: ICalendarEvent[]): Map { - const map = new Map(); - - events.forEach(event => { - const dateKey = getDateKey(event.start); - const existing = map.get(dateKey) || []; - existing.push(event); - map.set(dateKey, existing); - }); - - return map; - } - /** * Create a single event element * diff --git a/src/v2/repositories/MockBookingRepository.ts b/src/v2/repositories/MockBookingRepository.ts new file mode 100644 index 0000000..449d5a3 --- /dev/null +++ b/src/v2/repositories/MockBookingRepository.ts @@ -0,0 +1,73 @@ +import { IBooking, IBookingService, BookingStatus, 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 + */ +export class MockBookingRepository implements IApiRepository { + public readonly entityType: EntityType = 'Booking'; + private readonly dataUrl = 'data/mock-bookings.json'; + + 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; + } + } + + public async sendCreate(_booking: IBooking): Promise { + throw new Error('MockBookingRepository does not support sendCreate. Mock data is read-only.'); + } + + public async sendUpdate(_id: string, _updates: Partial): Promise { + throw new Error('MockBookingRepository does not support sendUpdate. Mock data 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 => ({ + id: booking.id, + customerId: booking.customerId, + status: booking.status as BookingStatus, + createdAt: new Date(booking.createdAt), + services: booking.services as IBookingService[], + totalPrice: booking.totalPrice, + tags: booking.tags, + notes: booking.notes, + syncStatus: 'synced' as const + })); + } +} diff --git a/src/v2/repositories/MockCustomerRepository.ts b/src/v2/repositories/MockCustomerRepository.ts new file mode 100644 index 0000000..4bf079c --- /dev/null +++ b/src/v2/repositories/MockCustomerRepository.ts @@ -0,0 +1,58 @@ +import { ICustomer, 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 + */ +export class MockCustomerRepository implements IApiRepository { + public readonly entityType: EntityType = 'Customer'; + private readonly dataUrl = 'data/mock-customers.json'; + + 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; + } + } + + public async sendCreate(_customer: ICustomer): Promise { + throw new Error('MockCustomerRepository does not support sendCreate. Mock data is read-only.'); + } + + public async sendUpdate(_id: string, _updates: Partial): Promise { + throw new Error('MockCustomerRepository does not support sendUpdate. Mock data 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 => ({ + id: customer.id, + name: customer.name, + phone: customer.phone, + email: customer.email, + metadata: customer.metadata, + syncStatus: 'synced' as const + })); + } +} diff --git a/src/v2/repositories/MockResourceRepository.ts b/src/v2/repositories/MockResourceRepository.ts new file mode 100644 index 0000000..eb7731e --- /dev/null +++ b/src/v2/repositories/MockResourceRepository.ts @@ -0,0 +1,64 @@ +import { IResource, ResourceType, 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 + */ +export class MockResourceRepository implements IApiRepository { + public readonly entityType: EntityType = 'Resource'; + private readonly dataUrl = 'data/mock-resources.json'; + + 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; + } + } + + public async sendCreate(_resource: IResource): Promise { + throw new Error('MockResourceRepository does not support sendCreate. Mock data is read-only.'); + } + + public async sendUpdate(_id: string, _updates: Partial): Promise { + throw new Error('MockResourceRepository does not support sendUpdate. Mock data 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 => ({ + id: resource.id, + name: resource.name, + displayName: resource.displayName, + type: resource.type as ResourceType, + avatarUrl: resource.avatarUrl, + color: resource.color, + isActive: resource.isActive, + metadata: resource.metadata, + syncStatus: 'synced' as const + })); + } +} diff --git a/src/v2/storage/IndexedDBContext.ts b/src/v2/storage/IndexedDBContext.ts index a504cf3..ea709bf 100644 --- a/src/v2/storage/IndexedDBContext.ts +++ b/src/v2/storage/IndexedDBContext.ts @@ -10,7 +10,7 @@ import { IStore } from './IStore'; */ export class IndexedDBContext { private static readonly DB_NAME = 'CalendarV2DB'; - private static readonly DB_VERSION = 1; + private static readonly DB_VERSION = 2; private db: IDBDatabase | null = null; private initialized: boolean = false; diff --git a/src/v2/storage/bookings/BookingService.ts b/src/v2/storage/bookings/BookingService.ts new file mode 100644 index 0000000..ae7a9f9 --- /dev/null +++ b/src/v2/storage/bookings/BookingService.ts @@ -0,0 +1,75 @@ +import { IBooking, EntityType, IEventBus, BookingStatus } from '../../types/CalendarTypes'; +import { BookingStore } from './BookingStore'; +import { BaseEntityService } from '../BaseEntityService'; +import { IndexedDBContext } from '../IndexedDBContext'; + +/** + * BookingService - CRUD operations for bookings in IndexedDB + */ +export class BookingService extends BaseEntityService { + readonly storeName = BookingStore.STORE_NAME; + readonly entityType: EntityType = 'Booking'; + + constructor(context: IndexedDBContext, eventBus: IEventBus) { + super(context, eventBus); + } + + protected serialize(booking: IBooking): unknown { + return { + ...booking, + createdAt: booking.createdAt.toISOString() + }; + } + + protected deserialize(data: unknown): IBooking { + const raw = data as Record; + return { + ...raw, + createdAt: new Date(raw.createdAt as string) + } as IBooking; + } + + /** + * Get bookings for a customer + */ + async getByCustomer(customerId: string): Promise { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + const index = store.index('customerId'); + const request = index.getAll(customerId); + + request.onsuccess = () => { + const data = request.result as unknown[]; + const bookings = data.map(item => this.deserialize(item)); + resolve(bookings); + }; + + request.onerror = () => { + reject(new Error(`Failed to get bookings for customer ${customerId}: ${request.error}`)); + }; + }); + } + + /** + * Get bookings by status + */ + async getByStatus(status: BookingStatus): Promise { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + const index = store.index('status'); + const request = index.getAll(status); + + request.onsuccess = () => { + const data = request.result as unknown[]; + const bookings = data.map(item => this.deserialize(item)); + resolve(bookings); + }; + + request.onerror = () => { + reject(new Error(`Failed to get bookings with status ${status}: ${request.error}`)); + }; + }); + } +} diff --git a/src/v2/storage/bookings/BookingStore.ts b/src/v2/storage/bookings/BookingStore.ts new file mode 100644 index 0000000..c412f85 --- /dev/null +++ b/src/v2/storage/bookings/BookingStore.ts @@ -0,0 +1,18 @@ +import { IStore } from '../IStore'; + +/** + * BookingStore - IndexedDB ObjectStore definition for bookings + */ +export class BookingStore implements IStore { + static readonly STORE_NAME = 'bookings'; + readonly storeName = BookingStore.STORE_NAME; + + create(db: IDBDatabase): void { + const store = db.createObjectStore(BookingStore.STORE_NAME, { keyPath: 'id' }); + + store.createIndex('customerId', 'customerId', { unique: false }); + store.createIndex('status', 'status', { unique: false }); + store.createIndex('syncStatus', 'syncStatus', { unique: false }); + store.createIndex('createdAt', 'createdAt', { unique: false }); + } +} diff --git a/src/v2/storage/customers/CustomerService.ts b/src/v2/storage/customers/CustomerService.ts new file mode 100644 index 0000000..6cfd888 --- /dev/null +++ b/src/v2/storage/customers/CustomerService.ts @@ -0,0 +1,46 @@ +import { ICustomer, EntityType, IEventBus } from '../../types/CalendarTypes'; +import { CustomerStore } from './CustomerStore'; +import { BaseEntityService } from '../BaseEntityService'; +import { IndexedDBContext } from '../IndexedDBContext'; + +/** + * CustomerService - CRUD operations for customers in IndexedDB + */ +export class CustomerService extends BaseEntityService { + readonly storeName = CustomerStore.STORE_NAME; + readonly entityType: EntityType = 'Customer'; + + constructor(context: IndexedDBContext, eventBus: IEventBus) { + super(context, eventBus); + } + + /** + * Search customers by name (case-insensitive contains) + */ + async searchByName(query: string): Promise { + const all = await this.getAll(); + const lowerQuery = query.toLowerCase(); + return all.filter(c => c.name.toLowerCase().includes(lowerQuery)); + } + + /** + * Find customer by phone + */ + async getByPhone(phone: string): Promise { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + const index = store.index('phone'); + const request = index.get(phone); + + request.onsuccess = () => { + const data = request.result; + resolve(data ? (data as ICustomer) : null); + }; + + request.onerror = () => { + reject(new Error(`Failed to find customer by phone ${phone}: ${request.error}`)); + }; + }); + } +} diff --git a/src/v2/storage/customers/CustomerStore.ts b/src/v2/storage/customers/CustomerStore.ts new file mode 100644 index 0000000..9afcf9e --- /dev/null +++ b/src/v2/storage/customers/CustomerStore.ts @@ -0,0 +1,17 @@ +import { IStore } from '../IStore'; + +/** + * CustomerStore - IndexedDB ObjectStore definition for customers + */ +export class CustomerStore implements IStore { + static readonly STORE_NAME = 'customers'; + readonly storeName = CustomerStore.STORE_NAME; + + create(db: IDBDatabase): void { + const store = db.createObjectStore(CustomerStore.STORE_NAME, { keyPath: 'id' }); + + store.createIndex('name', 'name', { unique: false }); + store.createIndex('phone', 'phone', { unique: false }); + store.createIndex('syncStatus', 'syncStatus', { unique: false }); + } +} diff --git a/src/v2/storage/resources/ResourceService.ts b/src/v2/storage/resources/ResourceService.ts new file mode 100644 index 0000000..7bd7f2e --- /dev/null +++ b/src/v2/storage/resources/ResourceService.ts @@ -0,0 +1,45 @@ +import { IResource, EntityType, IEventBus } from '../../types/CalendarTypes'; +import { ResourceStore } from './ResourceStore'; +import { BaseEntityService } from '../BaseEntityService'; +import { IndexedDBContext } from '../IndexedDBContext'; + +/** + * ResourceService - CRUD operations for resources in IndexedDB + */ +export class ResourceService extends BaseEntityService { + readonly storeName = ResourceStore.STORE_NAME; + readonly entityType: EntityType = 'Resource'; + + constructor(context: IndexedDBContext, eventBus: IEventBus) { + super(context, eventBus); + } + + /** + * Get all active resources + */ + async getActive(): Promise { + const all = await this.getAll(); + return all.filter(r => r.isActive !== false); + } + + /** + * Get resources by type + */ + async getByType(type: string): Promise { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + const index = store.index('type'); + const request = index.getAll(type); + + request.onsuccess = () => { + const data = request.result as IResource[]; + resolve(data); + }; + + request.onerror = () => { + reject(new Error(`Failed to get resources by type ${type}: ${request.error}`)); + }; + }); + } +} diff --git a/src/v2/storage/resources/ResourceStore.ts b/src/v2/storage/resources/ResourceStore.ts new file mode 100644 index 0000000..38e39b6 --- /dev/null +++ b/src/v2/storage/resources/ResourceStore.ts @@ -0,0 +1,17 @@ +import { IStore } from '../IStore'; + +/** + * ResourceStore - IndexedDB ObjectStore definition for resources + */ +export class ResourceStore implements IStore { + static readonly STORE_NAME = 'resources'; + readonly storeName = ResourceStore.STORE_NAME; + + create(db: IDBDatabase): void { + const store = db.createObjectStore(ResourceStore.STORE_NAME, { keyPath: 'id' }); + + store.createIndex('type', 'type', { unique: false }); + store.createIndex('syncStatus', 'syncStatus', { unique: false }); + store.createIndex('isActive', 'isActive', { unique: false }); + } +} diff --git a/src/v2/types/CalendarTypes.ts b/src/v2/types/CalendarTypes.ts index fdf1d97..8d032d1 100644 --- a/src/v2/types/CalendarTypes.ts +++ b/src/v2/types/CalendarTypes.ts @@ -84,3 +84,59 @@ export interface IEntityDeletedPayload { entityType: EntityType; id: string; } + +// Resource types +export type ResourceType = + | 'person' + | 'room' + | 'equipment' + | 'vehicle' + | 'custom'; + +export interface IResource extends ISync { + id: string; + name: string; + displayName: string; + type: ResourceType; + avatarUrl?: string; + color?: string; + isActive?: boolean; + metadata?: Record; +} + +// Booking types +export type BookingStatus = + | 'created' + | 'arrived' + | 'paid' + | 'noshow' + | 'cancelled'; + +export interface IBookingService { + serviceId: string; + serviceName: string; + baseDuration: number; + basePrice: number; + customPrice?: number; + resourceId: string; +} + +export interface IBooking extends ISync { + id: string; + customerId: string; + status: BookingStatus; + createdAt: Date; + services: IBookingService[]; + totalPrice?: number; + tags?: string[]; + notes?: string; +} + +// Customer types +export interface ICustomer extends ISync { + id: string; + name: string; + phone: string; + email?: string; + metadata?: Record; +} diff --git a/wwwroot/v2.html b/wwwroot/v2.html index c1fd702..bbc41e4 100644 --- a/wwwroot/v2.html +++ b/wwwroot/v2.html @@ -10,6 +10,7 @@
+ Dag Datoer Resources Teams