diff --git a/CLAUDE.md b/CLAUDE.md index 11820b8..ff242de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Calendar Plantempus is a professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities. +Calendar Plantempus is a professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities. Supports both date-based (day/week/month) and resource-based (people, rooms) calendar views. ## Build & Development Commands @@ -21,18 +21,18 @@ npm run clean # Type check only npx tsc --noEmit -# Run all tests +# Run all tests (watch mode) npm test -# Run tests in watch mode -npm run test - # Run tests once and exit npm run test:run # Run tests with UI npm run test:ui +# Run single test file +npm test -- + # CSS Development npm run css:build # Build CSS npm run css:watch # Watch and rebuild CSS @@ -57,6 +57,21 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o - Components subscribe via `eventBus.on(CoreEvents.EVENT_NAME, handler)` - Never call methods directly between managers - always use events +### Calendar Modes: Date vs Resource + +The calendar supports two column modes, configured at initialization in `src/index.ts`: + +**Date Mode** (`DateColumnDataSource`): +- Columns represent dates (day/week/month views) +- Uses `DateHeaderRenderer` and `DateColumnRenderer` + +**Resource Mode** (`ResourceColumnDataSource`): +- Columns represent resources (people, rooms, equipment) +- Uses `ResourceHeaderRenderer` and `ResourceColumnRenderer` +- Events filtered per-resource via `IColumnInfo.events` + +Both modes implement `IColumnDataSource` interface, allowing polymorphic column handling. + ### Manager Hierarchy **CalendarManager** (`src/managers/CalendarManager.ts`) - Top-level coordinator @@ -67,7 +82,6 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o **Key Managers**: - **EventManager** - Event CRUD operations, data loading from repository - **GridManager** - Renders time grid structure -- **ViewManager** - Handles view switching (day/week/month) - **NavigationManager** - Date navigation and period calculations - **DragDropManager** - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation - **ResizeHandleManager** - Event resizing with visual feedback @@ -78,13 +92,20 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o ### Repository Pattern -Event data access is abstracted through the **IEventRepository** interface (`src/repositories/IEventRepository.ts`): -- **IndexedDBEventRepository** - Primary: Local storage with offline support -- **ApiEventRepository** - Sends changes to backend API -- **MockEventRepository** - Legacy: Loads from JSON file +Data access is abstracted through **IApiRepository** interface (`src/repositories/IApiRepository.ts`): +- **MockEventRepository**, **MockBookingRepository**, **MockCustomerRepository**, **MockResourceRepository** - Development: Load from JSON files +- **ApiEventRepository**, **ApiBookingRepository**, **ApiCustomerRepository**, **ApiResourceRepository** - Production: Backend API calls All repository methods accept an `UpdateSource` parameter ('local' | 'remote') to distinguish user actions from remote updates. +### Entity Service Pattern + +**IEntityService** (`src/storage/IEntityService.ts`) provides polymorphic entity handling: +- `EventService`, `BookingService`, `CustomerService`, `ResourceService` implement this interface +- Encapsulates sync status manipulation (SyncManager delegates, never manipulates directly) +- Uses `entityType` discriminator for runtime routing without switch statements +- Enables polymorphic operations: `Array>` works across all entity types + ### Offline-First Sync Architecture **SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization: @@ -154,16 +175,19 @@ When dropping events, snap to time grid: ### Testing with Vitest Tests use **Vitest** with **jsdom** environment: +- Config: `vitest.config.ts` - Setup file: `test/setup.ts` -- Test helpers: `test/helpers/dom-helpers.ts` +- Test helpers: `test/helpers/dom-helpers.ts`, `test/helpers/config-helpers.ts` - Run single test: `npm test -- ` ## Key Files to Know -- `src/index.ts` - DI container setup and initialization +- `src/index.ts` - DI container setup and initialization (also sets calendar mode: date vs resource) - `src/core/EventBus.ts` - Central event dispatcher -- `src/constants/CoreEvents.ts` - All event type constants +- `src/constants/CoreEvents.ts` - All event type constants (~34 core events) - `src/types/CalendarTypes.ts` - Core type definitions +- `src/types/ColumnDataSource.ts` - Column abstraction for date/resource modes +- `src/storage/IEntityService.ts` - Entity service interface for polymorphic sync - `src/managers/CalendarManager.ts` - Main coordinator - `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs - `src/configurations/CalendarConfig.ts` - Configuration schema @@ -207,12 +231,15 @@ Access debug interface in browser console: window.calendarDebug.eventBus.getEventLog() window.calendarDebug.calendarManager window.calendarDebug.eventManager +window.calendarDebug.auditService +window.calendarDebug.syncManager +window.calendarDebug.app // Full DI container ``` ## Dependencies - **@novadi/core** - Dependency injection framework -- **date-fns** / **date-fns-tz** - Date manipulation and timezone support +- **dayjs** - Date manipulation and formatting - **fuse.js** - Fuzzy search for event filtering - **esbuild** - Fast bundler for development - **vitest** - Testing framework diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index 3f28a70..30c8525 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -4,6 +4,7 @@ import { Configuration } from '../configurations/CalendarConfig'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; import { DateService } from '../utils/DateService'; +import { EventId } from '../types/EventId'; /** * Base class for event elements @@ -172,7 +173,7 @@ export class SwpEventElement extends BaseSwpEventElement { const clone = this.cloneNode(true) as SwpEventElement; // Apply "clone-" prefix to ID - clone.dataset.eventId = `clone-${this.eventId}`; + clone.dataset.eventId = EventId.toCloneId(this.eventId as EventId); // Disable pointer events on clone so it doesn't interfere with hover detection clone.style.pointerEvents = 'none'; @@ -343,7 +344,7 @@ export class SwpAllDayEventElement extends BaseSwpEventElement { const clone = this.cloneNode(true) as SwpAllDayEventElement; // Apply "clone-" prefix to ID - clone.dataset.eventId = `clone-${this.eventId}`; + clone.dataset.eventId = EventId.toCloneId(this.eventId as EventId); // Disable pointer events on clone so it doesn't interfere with hover detection clone.style.pointerEvents = 'none'; diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index 7f7c9ed..f003630 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -22,6 +22,7 @@ import { IDragOffset, IMousePosition } from '../types/DragDropTypes'; import { CoreEvents } from '../constants/CoreEvents'; import { EventManager } from './EventManager'; import { DateService } from '../utils/DateService'; +import { EventId } from '../types/EventId'; /** * AllDayManager - Handles all-day row height animations and management @@ -500,7 +501,7 @@ export class AllDayManager { if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return; const clone = dragEndEvent.draggedClone as SwpAllDayEventElement; - const eventId = clone.eventId.replace('clone-', ''); + const eventId = EventId.from(clone.eventId); const columnIdentifier = dragEndEvent.finalPosition.column.identifier; // Determine target date based on mode @@ -568,7 +569,7 @@ export class AllDayManager { if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return; const clone = dragEndEvent.draggedClone as SwpAllDayEventElement; - const eventId = clone.eventId.replace('clone-', ''); + const eventId = EventId.from(clone.eventId); const columnIdentifier = dragEndEvent.finalPosition.column.identifier; // Determine target date based on mode diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 853a982..0de20cb 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -10,6 +10,7 @@ import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPa import { DateService } from '../utils/DateService'; import { EventStackManager } from '../managers/EventStackManager'; import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator'; +import { EventId } from '../types/EventId'; /** * Interface for event rendering strategies @@ -165,22 +166,14 @@ export class DateEventRenderer implements IEventRenderer { * Handle drag end event */ public handleDragEnd(originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void { - if (!draggedClone || !originalElement) { - console.warn('Missing draggedClone or originalElement'); - return; - } - + // Only fade out and remove if it's a swp-event (not swp-allday-event) // AllDayManager handles removal of swp-allday-event elements if (originalElement.tagName === 'SWP-EVENT') { this.fadeOutAndRemove(originalElement); } - // Remove clone prefix and normalize clone to be a regular event - const cloneId = draggedClone.dataset.eventId; - if (cloneId && cloneId.startsWith('clone-')) { - draggedClone.dataset.eventId = cloneId.replace('clone-', ''); - } + draggedClone.dataset.eventId = EventId.from(draggedClone.dataset.eventId!); // Fully normalize the clone to be a regular event draggedClone.classList.remove('dragging'); @@ -192,7 +185,7 @@ export class DateEventRenderer implements IEventRenderer { // Clean up any remaining day event clones - const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${cloneId}"]`); + const dayEventClone = document.querySelector(`swp-event[data-event-id="${draggedClone.dataset.eventId}"]`); if (dayEventClone) { dayEventClone.remove(); } diff --git a/src/types/EventId.ts b/src/types/EventId.ts new file mode 100644 index 0000000..114372b --- /dev/null +++ b/src/types/EventId.ts @@ -0,0 +1,31 @@ +/** + * Branded type for Event IDs + * Ensures type-safety and centralizes ID normalization logic + */ +export type EventId = string & { readonly __brand: 'EventId' }; + +/** + * EventId utility functions + */ +export const EventId = { + /** + * Create EventId from string, normalizing clone- prefix + */ + from(id: string): EventId { + return id.replace('clone-', '') as EventId; + }, + + /** + * Check if raw ID is a clone + */ + isClone(id: string): boolean { + return id.startsWith('clone-'); + }, + + /** + * Create clone ID string from EventId + */ + toCloneId(id: EventId): string { + return `clone-${id}`; + } +};