Enhance calendar view with resource-aware rendering

Adds support for filtering events and rendering across multiple views with resource-specific context

Improves event and date rendering to handle resource-based filtering
Introduces day view and extends existing calendar infrastructure to support more flexible view configurations
This commit is contained in:
Janus C. H. Knudsen 2025-12-09 22:31:28 +01:00
parent 6fc9be9534
commit 7f6279a6f3
17 changed files with 570 additions and 38 deletions

View file

@ -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;

View file

@ -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<IBooking> {
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<string, unknown>;
return {
...raw,
createdAt: new Date(raw.createdAt as string)
} as IBooking;
}
/**
* Get bookings for a customer
*/
async getByCustomer(customerId: string): Promise<IBooking[]> {
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<IBooking[]> {
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}`));
};
});
}
}

View file

@ -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 });
}
}

View file

@ -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<ICustomer> {
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<ICustomer[]> {
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<ICustomer | null> {
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}`));
};
});
}
}

View file

@ -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 });
}
}

View file

@ -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<IResource> {
readonly storeName = ResourceStore.STORE_NAME;
readonly entityType: EntityType = 'Resource';
constructor(context: IndexedDBContext, eventBus: IEventBus) {
super(context, eventBus);
}
/**
* Get all active resources
*/
async getActive(): Promise<IResource[]> {
const all = await this.getAll();
return all.filter(r => r.isActive !== false);
}
/**
* Get resources by type
*/
async getByType(type: string): Promise<IResource[]> {
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}`));
};
});
}
}

View file

@ -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 });
}
}