Moving away from Azure Devops #1
5 changed files with 82 additions and 29 deletions
55
CLAUDE.md
55
CLAUDE.md
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project Overview
|
## 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
|
## Build & Development Commands
|
||||||
|
|
||||||
|
|
@ -21,18 +21,18 @@ npm run clean
|
||||||
# Type check only
|
# Type check only
|
||||||
npx tsc --noEmit
|
npx tsc --noEmit
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests (watch mode)
|
||||||
npm test
|
npm test
|
||||||
|
|
||||||
# Run tests in watch mode
|
|
||||||
npm run test
|
|
||||||
|
|
||||||
# Run tests once and exit
|
# Run tests once and exit
|
||||||
npm run test:run
|
npm run test:run
|
||||||
|
|
||||||
# Run tests with UI
|
# Run tests with UI
|
||||||
npm run test:ui
|
npm run test:ui
|
||||||
|
|
||||||
|
# Run single test file
|
||||||
|
npm test -- <test-file-name>
|
||||||
|
|
||||||
# CSS Development
|
# CSS Development
|
||||||
npm run css:build # Build CSS
|
npm run css:build # Build CSS
|
||||||
npm run css:watch # Watch and rebuild 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)`
|
- Components subscribe via `eventBus.on(CoreEvents.EVENT_NAME, handler)`
|
||||||
- Never call methods directly between managers - always use events
|
- 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
|
### Manager Hierarchy
|
||||||
|
|
||||||
**CalendarManager** (`src/managers/CalendarManager.ts`) - Top-level coordinator
|
**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**:
|
**Key Managers**:
|
||||||
- **EventManager** - Event CRUD operations, data loading from repository
|
- **EventManager** - Event CRUD operations, data loading from repository
|
||||||
- **GridManager** - Renders time grid structure
|
- **GridManager** - Renders time grid structure
|
||||||
- **ViewManager** - Handles view switching (day/week/month)
|
|
||||||
- **NavigationManager** - Date navigation and period calculations
|
- **NavigationManager** - Date navigation and period calculations
|
||||||
- **DragDropManager** - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation
|
- **DragDropManager** - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation
|
||||||
- **ResizeHandleManager** - Event resizing with visual feedback
|
- **ResizeHandleManager** - Event resizing with visual feedback
|
||||||
|
|
@ -78,13 +92,20 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o
|
||||||
|
|
||||||
### Repository Pattern
|
### Repository Pattern
|
||||||
|
|
||||||
Event data access is abstracted through the **IEventRepository** interface (`src/repositories/IEventRepository.ts`):
|
Data access is abstracted through **IApiRepository<T>** interface (`src/repositories/IApiRepository.ts`):
|
||||||
- **IndexedDBEventRepository** - Primary: Local storage with offline support
|
- **MockEventRepository**, **MockBookingRepository**, **MockCustomerRepository**, **MockResourceRepository** - Development: Load from JSON files
|
||||||
- **ApiEventRepository** - Sends changes to backend API
|
- **ApiEventRepository**, **ApiBookingRepository**, **ApiCustomerRepository**, **ApiResourceRepository** - Production: Backend API calls
|
||||||
- **MockEventRepository** - Legacy: Loads from JSON file
|
|
||||||
|
|
||||||
All repository methods accept an `UpdateSource` parameter ('local' | 'remote') to distinguish user actions from remote updates.
|
All repository methods accept an `UpdateSource` parameter ('local' | 'remote') to distinguish user actions from remote updates.
|
||||||
|
|
||||||
|
### Entity Service Pattern
|
||||||
|
|
||||||
|
**IEntityService<T>** (`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<IEntityService<any>>` works across all entity types
|
||||||
|
|
||||||
### Offline-First Sync Architecture
|
### Offline-First Sync Architecture
|
||||||
|
|
||||||
**SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization:
|
**SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization:
|
||||||
|
|
@ -154,16 +175,19 @@ When dropping events, snap to time grid:
|
||||||
### Testing with Vitest
|
### Testing with Vitest
|
||||||
|
|
||||||
Tests use **Vitest** with **jsdom** environment:
|
Tests use **Vitest** with **jsdom** environment:
|
||||||
|
- Config: `vitest.config.ts`
|
||||||
- Setup file: `test/setup.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 -- <test-file-name>`
|
- Run single test: `npm test -- <test-file-name>`
|
||||||
|
|
||||||
## Key Files to Know
|
## 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/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/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/CalendarManager.ts` - Main coordinator
|
||||||
- `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs
|
- `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs
|
||||||
- `src/configurations/CalendarConfig.ts` - Configuration schema
|
- `src/configurations/CalendarConfig.ts` - Configuration schema
|
||||||
|
|
@ -207,12 +231,15 @@ Access debug interface in browser console:
|
||||||
window.calendarDebug.eventBus.getEventLog()
|
window.calendarDebug.eventBus.getEventLog()
|
||||||
window.calendarDebug.calendarManager
|
window.calendarDebug.calendarManager
|
||||||
window.calendarDebug.eventManager
|
window.calendarDebug.eventManager
|
||||||
|
window.calendarDebug.auditService
|
||||||
|
window.calendarDebug.syncManager
|
||||||
|
window.calendarDebug.app // Full DI container
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **@novadi/core** - Dependency injection framework
|
- **@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
|
- **fuse.js** - Fuzzy search for event filtering
|
||||||
- **esbuild** - Fast bundler for development
|
- **esbuild** - Fast bundler for development
|
||||||
- **vitest** - Testing framework
|
- **vitest** - Testing framework
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Configuration } from '../configurations/CalendarConfig';
|
||||||
import { TimeFormatter } from '../utils/TimeFormatter';
|
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||||
import { PositionUtils } from '../utils/PositionUtils';
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
import { DateService } from '../utils/DateService';
|
import { DateService } from '../utils/DateService';
|
||||||
|
import { EventId } from '../types/EventId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for event elements
|
* Base class for event elements
|
||||||
|
|
@ -172,7 +173,7 @@ export class SwpEventElement extends BaseSwpEventElement {
|
||||||
const clone = this.cloneNode(true) as SwpEventElement;
|
const clone = this.cloneNode(true) as SwpEventElement;
|
||||||
|
|
||||||
// Apply "clone-" prefix to ID
|
// 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
|
// Disable pointer events on clone so it doesn't interfere with hover detection
|
||||||
clone.style.pointerEvents = 'none';
|
clone.style.pointerEvents = 'none';
|
||||||
|
|
@ -343,7 +344,7 @@ export class SwpAllDayEventElement extends BaseSwpEventElement {
|
||||||
const clone = this.cloneNode(true) as SwpAllDayEventElement;
|
const clone = this.cloneNode(true) as SwpAllDayEventElement;
|
||||||
|
|
||||||
// Apply "clone-" prefix to ID
|
// 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
|
// Disable pointer events on clone so it doesn't interfere with hover detection
|
||||||
clone.style.pointerEvents = 'none';
|
clone.style.pointerEvents = 'none';
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { IDragOffset, IMousePosition } from '../types/DragDropTypes';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { EventManager } from './EventManager';
|
import { EventManager } from './EventManager';
|
||||||
import { DateService } from '../utils/DateService';
|
import { DateService } from '../utils/DateService';
|
||||||
|
import { EventId } from '../types/EventId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AllDayManager - Handles all-day row height animations and management
|
* AllDayManager - Handles all-day row height animations and management
|
||||||
|
|
@ -500,7 +501,7 @@ export class AllDayManager {
|
||||||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
||||||
|
|
||||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||||
const eventId = clone.eventId.replace('clone-', '');
|
const eventId = EventId.from(clone.eventId);
|
||||||
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
||||||
|
|
||||||
// Determine target date based on mode
|
// Determine target date based on mode
|
||||||
|
|
@ -568,7 +569,7 @@ export class AllDayManager {
|
||||||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
||||||
|
|
||||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||||
const eventId = clone.eventId.replace('clone-', '');
|
const eventId = EventId.from(clone.eventId);
|
||||||
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
||||||
|
|
||||||
// Determine target date based on mode
|
// Determine target date based on mode
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPa
|
||||||
import { DateService } from '../utils/DateService';
|
import { DateService } from '../utils/DateService';
|
||||||
import { EventStackManager } from '../managers/EventStackManager';
|
import { EventStackManager } from '../managers/EventStackManager';
|
||||||
import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
|
import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
|
||||||
|
import { EventId } from '../types/EventId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for event rendering strategies
|
* Interface for event rendering strategies
|
||||||
|
|
@ -165,22 +166,14 @@ export class DateEventRenderer implements IEventRenderer {
|
||||||
* Handle drag end event
|
* Handle drag end event
|
||||||
*/
|
*/
|
||||||
public handleDragEnd(originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void {
|
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)
|
// Only fade out and remove if it's a swp-event (not swp-allday-event)
|
||||||
// AllDayManager handles removal of swp-allday-event elements
|
// AllDayManager handles removal of swp-allday-event elements
|
||||||
if (originalElement.tagName === 'SWP-EVENT') {
|
if (originalElement.tagName === 'SWP-EVENT') {
|
||||||
this.fadeOutAndRemove(originalElement);
|
this.fadeOutAndRemove(originalElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove clone prefix and normalize clone to be a regular event
|
draggedClone.dataset.eventId = EventId.from(draggedClone.dataset.eventId!);
|
||||||
const cloneId = draggedClone.dataset.eventId;
|
|
||||||
if (cloneId && cloneId.startsWith('clone-')) {
|
|
||||||
draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fully normalize the clone to be a regular event
|
// Fully normalize the clone to be a regular event
|
||||||
draggedClone.classList.remove('dragging');
|
draggedClone.classList.remove('dragging');
|
||||||
|
|
@ -192,7 +185,7 @@ export class DateEventRenderer implements IEventRenderer {
|
||||||
|
|
||||||
|
|
||||||
// Clean up any remaining day event clones
|
// 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) {
|
if (dayEventClone) {
|
||||||
dayEventClone.remove();
|
dayEventClone.remove();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/types/EventId.ts
Normal file
31
src/types/EventId.ts
Normal file
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue