Refactor entity services with hybrid sync pattern

Introduces BaseEntityService and SyncPlugin to eliminate code duplication across entity services

Improves:
- Code reusability through inheritance and composition
- Sync infrastructure for all entity types
- Polymorphic sync status management
- Reduced boilerplate code by ~75%

Supports generic sync for Event, Booking, Customer, and Resource entities
This commit is contained in:
Janus C. H. Knudsen 2025-11-18 16:37:33 +01:00
parent 2aa9d06fab
commit 8e52d670d6
30 changed files with 1960 additions and 526 deletions

View file

@ -1,108 +1,29 @@
import { ICustomer } from '../../types/CustomerTypes';
import { EntityType } from '../../types/CalendarTypes';
import { CustomerStore } from './CustomerStore';
import { BaseEntityService } from '../BaseEntityService';
/**
* CustomerService - CRUD operations for customers in IndexedDB
*
* Handles all customer-related database operations.
* Part of modular storage architecture where each entity has its own service.
* ARCHITECTURE:
* - Extends BaseEntityService for shared CRUD and sync logic
* - No serialization needed (ICustomer has no Date fields)
* - Provides customer-specific query methods (by phone, search by name)
*
* Note: No serialization needed - ICustomer has no Date fields.
* INHERITED METHODS (from BaseEntityService):
* - get(id), getAll(), save(entity), delete(id)
* - markAsSynced(id), markAsError(id), getSyncStatus(id), getBySyncStatus(status)
*
* CUSTOMER-SPECIFIC METHODS:
* - getByPhone(phone)
* - searchByName(searchTerm)
*/
export class CustomerService {
private db: IDBDatabase;
export class CustomerService extends BaseEntityService<ICustomer> {
readonly storeName = CustomerStore.STORE_NAME;
readonly entityType: EntityType = 'Customer';
/**
* @param db - IDBDatabase instance (injected dependency)
*/
constructor(db: IDBDatabase) {
this.db = db;
}
/**
* Get a single customer by ID
*
* @param id - Customer ID
* @returns ICustomer or null if not found
*/
async get(id: string): Promise<ICustomer | null> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readonly');
const store = transaction.objectStore(CustomerStore.STORE_NAME);
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result || null);
};
request.onerror = () => {
reject(new Error(`Failed to get customer ${id}: ${request.error}`));
};
});
}
/**
* Get all customers
*
* @returns Array of all customers
*/
async getAll(): Promise<ICustomer[]> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readonly');
const store = transaction.objectStore(CustomerStore.STORE_NAME);
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result as ICustomer[]);
};
request.onerror = () => {
reject(new Error(`Failed to get all customers: ${request.error}`));
};
});
}
/**
* Save a customer (create or update)
*
* @param customer - ICustomer to save
*/
async save(customer: ICustomer): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readwrite');
const store = transaction.objectStore(CustomerStore.STORE_NAME);
const request = store.put(customer);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to save customer ${customer.id}: ${request.error}`));
};
});
}
/**
* Delete a customer
*
* @param id - Customer ID to delete
*/
async delete(id: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readwrite');
const store = transaction.objectStore(CustomerStore.STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to delete customer ${id}: ${request.error}`));
};
});
}
// No serialization override needed - ICustomer has no Date fields
/**
* Get customers by phone number
@ -112,8 +33,8 @@ export class CustomerService {
*/
async getByPhone(phone: string): Promise<ICustomer[]> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readonly');
const store = transaction.objectStore(CustomerStore.STORE_NAME);
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('phone');
const request = index.getAll(phone);