diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..61c890f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,89 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+### Build and Development
+- **Build TypeScript:** `npm run build` - Compiles TypeScript sources to JavaScript using esbuild
+- **Watch mode:** `npm run watch` - Continuously compiles TypeScript on file changes
+- **Clean build:** `npm run clean` - Removes compiled JavaScript files (uses PowerShell on Windows)
+- **Start server:** `dotnet run` - Starts the ASP.NET Core server on http://localhost:8000
+
+### Typical Development Workflow
+1. Install dependencies: `npm install`
+2. Build TypeScript: `npm run build`
+3. Start server: `dotnet run`
+4. For active development, run `npm run watch` in a separate terminal
+
+## Architecture Overview
+
+Calendar Plantempus is an event-driven calendar application with the following architecture:
+
+### Core Principles
+- **Event-driven communication**: All components communicate via DOM CustomEvents through a central EventBus
+- **No global state**: Each manager maintains its own state
+- **Manager-based architecture**: Each manager has a specific responsibility
+- **Pure DOM manipulation**: No external JavaScript frameworks (React, Vue, etc.)
+- **TypeScript with esbuild**: Modern build tooling for fast compilation
+
+### Key Components
+
+#### EventBus (src/core/EventBus.ts)
+Central event dispatcher using DOM CustomEvents. All inter-component communication flows through this:
+```typescript
+// Example event emission
+eventBus.emit('calendar:view-changed', { view: 'week' });
+
+// Example event subscription
+eventBus.on('calendar:view-changed', (event) => { /* handle */ });
+```
+
+#### Manager Pattern
+Each manager is instantiated in `src/index.ts` and handles a specific domain:
+- **CalendarManager**: Main coordinator, initializes other managers
+- **ViewManager**: Handles day/week/month view switching
+- **NavigationManager**: Prev/next/today navigation
+- **EventManager**: CRUD operations for calendar events
+- **EventRenderer**: Visual rendering of events in the grid
+- **GridManager**: Creates and maintains the calendar grid structure
+- **ScrollManager**: Handles scroll position and time indicators
+- **DataManager**: Mock data loading and event data transformation
+
+### Project Structure
+```
+src/
+├── constants/ # Enums and constants (EventTypes)
+├── core/ # Core functionality (EventBus, CalendarConfig)
+├── managers/ # Manager classes (one per domain)
+├── types/ # TypeScript type definitions
+└── utils/ # Utility functions (DateUtils, PositionUtils)
+
+wwwroot/
+├── css/ # Modular CSS files
+├── js/ # Compiled JavaScript output
+└── index.html # Main HTML entry point
+```
+
+### CSS Architecture
+Modular CSS structure without external frameworks:
+- `calendar-base-css.css`: CSS custom properties and base styles
+- `calendar-components-css.css`: UI components
+- `calendar-events-css.css`: Event styling and colors
+- `calendar-layout-css.css`: Grid and layout
+- `calendar-popup-css.css`: Popup and modal styles
+
+### Event Naming Convention
+Events follow the pattern `category:action`:
+- `calendar:*` - General calendar events
+- `grid:*` - Grid-related events
+- `event:*` - Event data changes
+- `navigation:*` - Navigation actions
+- `view:*` - View changes
+
+### TypeScript Configuration
+- Target: ES2020
+- Module: ESNext
+- Strict mode enabled
+- Source maps enabled
+- Output directory: `./js`
\ No newline at end of file
diff --git a/calendar-grid-manager.js b/calendar-grid-manager.js
index a244898..7673542 100644
--- a/calendar-grid-manager.js
+++ b/calendar-grid-manager.js
@@ -28,7 +28,7 @@ export class GridManager {
findElements() {
this.container = document.querySelector('swp-calendar-container');
this.timeAxis = document.querySelector('swp-time-axis');
- this.weekHeader = document.querySelector('swp-week-header');
+ this.weekHeader = document.querySelector('swp-calendar-header');
this.timeGrid = document.querySelector('swp-time-grid');
this.scrollableContent = document.querySelector('swp-scrollable-content');
}
diff --git a/calendar-poc-single-file.html b/calendar-poc-single-file.html
index 42e163c..8c2f25f 100644
--- a/calendar-poc-single-file.html
+++ b/calendar-poc-single-file.html
@@ -64,7 +64,7 @@
swp-calendar-nav,
swp-calendar-container,
swp-time-axis,
- swp-week-header,
+ swp-calendar-header,
swp-scrollable-content,
swp-time-grid,
swp-day-columns,
@@ -228,7 +228,7 @@
}
/* Week container for sliding */
- swp-week-container {
+ swp-grid-container {
grid-column: 2;
display: grid;
grid-template-rows: auto 1fr;
@@ -237,19 +237,19 @@
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
}
- swp-week-container.slide-out-left {
+ swp-grid-container.slide-out-left {
transform: translateX(-100%);
}
- swp-week-container.slide-out-right {
+ swp-grid-container.slide-out-right {
transform: translateX(100%);
}
- swp-week-container.slide-in-left {
+ swp-grid-container.slide-in-left {
transform: translateX(-100%);
}
- swp-week-container.slide-in-right {
+ swp-grid-container.slide-in-right {
transform: translateX(100%);
}
@@ -287,7 +287,7 @@
}
/* Week header */
- swp-week-header {
+ swp-calendar-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
background: var(--color-surface);
@@ -522,8 +522,8 @@
-
-
+
+
@@ -531,7 +531,7 @@
-
+
@@ -607,7 +607,7 @@
}
renderWeekHeaders() {
- const weekHeader = document.querySelector('swp-week-header');
+ const weekHeader = document.querySelector('swp-calendar-header');
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
weekHeader.innerHTML = ''; // Clear any existing content
@@ -818,12 +818,12 @@
animateTransition(direction, targetWeek) {
const container = document.querySelector('swp-calendar-container');
- const currentWeek = document.querySelector('swp-week-container');
+ const currentWeek = document.querySelector('swp-grid-container');
// Create new week container
- const newWeek = document.createElement('swp-week-container');
+ const newWeek = document.createElement('swp-grid-container');
newWeek.innerHTML = `
-
+
@@ -844,7 +844,7 @@
container.appendChild(newWeek);
// Render new content in the new container using targetWeek
- const tempHeader = newWeek.querySelector('swp-week-header');
+ const tempHeader = newWeek.querySelector('swp-calendar-header');
const tempColumns = newWeek.querySelector('swp-day-columns');
// Clear any existing content
@@ -1027,7 +1027,7 @@
updateCalendar() {
// This method is now only used for initial render
- document.querySelector('swp-week-header').innerHTML = '';
+ document.querySelector('swp-calendar-header').innerHTML = '';
document.querySelector('swp-day-columns').innerHTML = '';
this.renderWeekHeaders();
this.renderDayColumns();
diff --git a/resource-calendar-structure.md b/resource-calendar-structure.md
new file mode 100644
index 0000000..42e7346
--- /dev/null
+++ b/resource-calendar-structure.md
@@ -0,0 +1,166 @@
+# Resource Calendar JSON Structure (Opdateret)
+
+Her er den opdaterede JSON struktur med resources som array og detaljerede resource informationer:
+
+```json
+{
+ "date": "2025-08-05",
+ "resources": [
+ {
+ "name": "karina.knudsen",
+ "displayName": "Karina Knudsen",
+ "avatarUrl": "/avatars/karina.jpg",
+ "employeeId": "EMP001",
+ "events": [
+ {
+ "id": "1",
+ "title": "Balayage langt hår",
+ "start": "2025-08-05T10:00:00",
+ "end": "2025-08-05T11:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#9c27b0" }
+ },
+ {
+ "id": "2",
+ "title": "Klipning og styling",
+ "start": "2025-08-05T14:00:00",
+ "end": "2025-08-05T15:30:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 90, "color": "#e91e63" }
+ }
+ ]
+ },
+ {
+ "name": "maria.hansen",
+ "displayName": "Maria Hansen",
+ "avatarUrl": "/avatars/maria.jpg",
+ "employeeId": "EMP002",
+ "events": [
+ {
+ "id": "3",
+ "title": "Permanent",
+ "start": "2025-08-05T09:00:00",
+ "end": "2025-08-05T11:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#3f51b5" }
+ },
+ {
+ "id": "4",
+ "title": "Farve behandling",
+ "start": "2025-08-05T13:00:00",
+ "end": "2025-08-05T15:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#ff9800" }
+ }
+ ]
+ },
+ {
+ "name": "lars.nielsen",
+ "displayName": "Lars Nielsen",
+ "avatarUrl": "/avatars/lars.jpg",
+ "employeeId": "EMP003",
+ "events": [
+ {
+ "id": "5",
+ "title": "Herreklipning",
+ "start": "2025-08-05T11:00:00",
+ "end": "2025-08-05T11:30:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#795548" }
+ },
+ {
+ "id": "6",
+ "title": "Skæg trimning",
+ "start": "2025-08-05T16:00:00",
+ "end": "2025-08-05T16:30:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#607d8b" }
+ }
+ ]
+ },
+ {
+ "name": "anna.petersen",
+ "displayName": "Anna Petersen",
+ "avatarUrl": "/avatars/anna.jpg",
+ "employeeId": "EMP004",
+ "events": [
+ {
+ "id": "7",
+ "title": "Bryllupsfrisure",
+ "start": "2025-08-05T08:00:00",
+ "end": "2025-08-05T10:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#009688" }
+ }
+ ]
+ },
+ {
+ "name": "thomas.olsen",
+ "displayName": "Thomas Olsen",
+ "avatarUrl": "/avatars/thomas.jpg",
+ "employeeId": "EMP005",
+ "events": [
+ {
+ "id": "8",
+ "title": "Highlights",
+ "start": "2025-08-05T12:00:00",
+ "end": "2025-08-05T14:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#8bc34a" }
+ },
+ {
+ "id": "9",
+ "title": "Styling konsultation",
+ "start": "2025-08-05T15:00:00",
+ "end": "2025-08-05T15:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#cddc39" }
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Struktur Forklaring
+
+- **date**: Den specifikke dato for resource calendar visningen
+- **resources**: Array af resource objekter
+- **Resource objekt**:
+ - **name**: Unikt navn/ID (kebab-case)
+ - **displayName**: Navn til visning i UI
+ - **avatarUrl**: URL til profilbillede
+ - **employeeId**: Medarbejder ID
+ - **events**: Array af events for denne resource
+
+## Fordele ved denne struktur:
+
+1. **Fleksibel**: Nemt at tilføje flere resource felter
+2. **Skalerbar**: Kan håndtere mange resources
+3. **UI-venlig**: DisplayName og avatar til visning
+4. **Struktureret**: Klar separation mellem resource info og events
+5. **Søgbar**: Name og employeeId til filtrering/søgning
+
+Denne struktur gør det nemt at:
+- Vise resource info i headers (displayName, avatar)
+- Filtrere events per resource
+- Håndtere kun én dato ad gangen i resource mode
+- Udvide med flere resource felter senere
\ No newline at end of file
diff --git a/src/constants/EventTypes.ts b/src/constants/EventTypes.ts
index 74e94e9..1bb162f 100644
--- a/src/constants/EventTypes.ts
+++ b/src/constants/EventTypes.ts
@@ -19,6 +19,7 @@ export const EventTypes = {
EVENT_RENDERED: 'calendar:eventrendered',
EVENT_SELECTED: 'calendar:eventselected',
EVENTS_LOADED: 'calendar:eventsloaded',
+ RESOURCE_DATA_LOADED: 'calendar:resourcedataloaded',
// Interaction events
DRAG_START: 'calendar:dragstart',
@@ -55,6 +56,8 @@ export const EventTypes = {
// State events
STATE_UPDATE: 'calendar:stateupdate',
CONFIG_UPDATE: 'calendar:configupdate',
+ CALENDAR_TYPE_CHANGED: 'calendar:calendartypechanged',
+ SELECTED_DATE_CHANGED: 'calendar:selecteddatechanged',
// Time events
TIME_UPDATE: 'calendar:timeupdate',
diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts
index 8eb0fe4..8e9b8c0 100644
--- a/src/core/CalendarConfig.ts
+++ b/src/core/CalendarConfig.ts
@@ -2,7 +2,7 @@
import { eventBus } from './EventBus';
import { EventTypes } from '../constants/EventTypes';
-import { CalendarConfig as ICalendarConfig, ViewType } from '../types/CalendarTypes';
+import { CalendarConfig as ICalendarConfig, ViewType, CalendarType } from '../types/CalendarTypes';
/**
* View-specific settings interface
@@ -18,6 +18,8 @@ interface ViewSettings {
*/
export class CalendarConfig {
private config: ICalendarConfig;
+ private calendarType: CalendarType = 'date';
+ private selectedDate: Date | null = null;
constructor() {
this.config = {
@@ -69,10 +71,46 @@ export class CalendarConfig {
// Set computed values
this.config.minEventDuration = this.config.snapInterval;
+ // Load calendar type from URL parameter
+ this.loadCalendarType();
+
// Load from data attributes
this.loadFromDOM();
}
+ /**
+ * Load calendar type and date from URL parameters
+ */
+ private loadCalendarType(): void {
+ const urlParams = new URLSearchParams(window.location.search);
+ const typeParam = urlParams.get('type');
+ const dateParam = urlParams.get('date');
+
+ // Set calendar type
+ if (typeParam === 'resource' || typeParam === 'date') {
+ this.calendarType = typeParam;
+ console.log(`CalendarConfig: Calendar type set to '${this.calendarType}' from URL parameter`);
+ } else {
+ this.calendarType = 'date'; // Default
+ console.log(`CalendarConfig: Calendar type defaulted to '${this.calendarType}'`);
+ }
+
+ // Set selected date
+ if (dateParam) {
+ const parsedDate = new Date(dateParam);
+ if (!isNaN(parsedDate.getTime())) {
+ this.selectedDate = parsedDate;
+ console.log(`CalendarConfig: Selected date set to '${this.selectedDate.toISOString()}' from URL parameter`);
+ } else {
+ console.warn(`CalendarConfig: Invalid date parameter '${dateParam}', using current date`);
+ this.selectedDate = new Date();
+ }
+ } else {
+ this.selectedDate = new Date(); // Default to today
+ console.log(`CalendarConfig: Selected date defaulted to today: ${this.selectedDate.toISOString()}`);
+ }
+ }
+
/**
* Load configuration from DOM data attributes
*/
@@ -194,6 +232,46 @@ export class CalendarConfig {
return settings[view] || settings.week;
}
+
+ /**
+ * Get calendar type
+ */
+ getCalendarType(): CalendarType {
+ return this.calendarType;
+ }
+
+ /**
+ * Set calendar type
+ */
+ setCalendarType(type: CalendarType): void {
+ const oldType = this.calendarType;
+ this.calendarType = type;
+
+ // Emit calendar type change event
+ eventBus.emit(EventTypes.CALENDAR_TYPE_CHANGED, {
+ oldType,
+ newType: type
+ });
+ }
+
+ /**
+ * Get selected date
+ */
+ getSelectedDate(): Date | null {
+ return this.selectedDate;
+ }
+
+ /**
+ * Set selected date
+ */
+ setSelectedDate(date: Date): void {
+ this.selectedDate = date;
+
+ // Emit date change event
+ eventBus.emit(EventTypes.SELECTED_DATE_CHANGED, {
+ date: date
+ });
+ }
}
// Create singleton instance
diff --git a/src/data/mock-events.json b/src/data/mock-events.json
new file mode 100644
index 0000000..80e1796
--- /dev/null
+++ b/src/data/mock-events.json
@@ -0,0 +1,168 @@
+[
+ {
+ "id": "1",
+ "title": "Weekend Planning",
+ "start": "2025-08-03T10:00:00",
+ "end": "2025-08-03T11:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#9c27b0" }
+ },
+ {
+ "id": "2",
+ "title": "Team Standup",
+ "start": "2025-08-04T09:00:00",
+ "end": "2025-08-04T09:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#ff5722" }
+ },
+ {
+ "id": "3",
+ "title": "Project Kickoff",
+ "start": "2025-08-04T14:00:00",
+ "end": "2025-08-04T15:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 90, "color": "#e91e63" }
+ },
+ {
+ "id": "4",
+ "title": "Deep Work Session",
+ "start": "2025-08-05T10:00:00",
+ "end": "2025-08-05T12:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#3f51b5" }
+ },
+ {
+ "id": "5",
+ "title": "Lunch Meeting",
+ "start": "2025-08-05T12:30:00",
+ "end": "2025-08-05T13:30:00",
+ "type": "meal",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#ff9800" }
+ },
+ {
+ "id": "6",
+ "title": "Client Review",
+ "start": "2025-08-06T15:00:00",
+ "end": "2025-08-06T16:00:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#795548" }
+ },
+ {
+ "id": "7",
+ "title": "Sprint Planning",
+ "start": "2025-08-07T09:00:00",
+ "end": "2025-08-07T10:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 90, "color": "#607d8b" }
+ },
+ {
+ "id": "8",
+ "title": "Code Review",
+ "start": "2025-08-07T14:00:00",
+ "end": "2025-08-07T15:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#009688" }
+ },
+ {
+ "id": "9",
+ "title": "Team Standup",
+ "start": "2025-08-08T09:00:00",
+ "end": "2025-08-08T09:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#8bc34a" }
+ },
+ {
+ "id": "10",
+ "title": "Client Meeting",
+ "start": "2025-08-08T14:00:00",
+ "end": "2025-08-08T15:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 90, "color": "#cddc39" }
+ },
+ {
+ "id": "11",
+ "title": "Weekend Project",
+ "start": "2025-08-09T10:00:00",
+ "end": "2025-08-09T12:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#f44336" }
+ },
+ {
+ "id": "12",
+ "title": "Early Morning Workout",
+ "start": "2025-08-05T06:00:00",
+ "end": "2025-08-05T07:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#00bcd4" }
+ },
+ {
+ "id": "13",
+ "title": "Late Evening Call",
+ "start": "2025-08-06T21:00:00",
+ "end": "2025-08-06T22:00:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#673ab7" }
+ },
+ {
+ "id": "14",
+ "title": "Midnight Deployment",
+ "start": "2025-08-07T23:00:00",
+ "end": "2025-08-08T01:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#ffc107" }
+ },
+ {
+ "id": "15",
+ "title": "Company Holiday",
+ "start": "2025-08-04T00:00:00",
+ "end": "2025-08-05T23:59:59",
+ "type": "milestone",
+ "allDay": true,
+ "syncStatus": "synced",
+ "metadata": {
+ "duration": 1440,
+ "color": "#4caf50"
+ }
+ },
+ {
+ "id": "16",
+ "title": "Team Building Event",
+ "start": "2025-08-06T00:00:00",
+ "end": "2025-08-06T23:59:59",
+ "type": "meeting",
+ "allDay": true,
+ "syncStatus": "synced",
+ "metadata": {
+ "duration": 1440,
+ "color": "#2196f3"
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/data/mock-resource-events.json b/src/data/mock-resource-events.json
new file mode 100644
index 0000000..a569174
--- /dev/null
+++ b/src/data/mock-resource-events.json
@@ -0,0 +1,135 @@
+{
+ "date": "2025-08-05",
+ "resources": [
+ {
+ "name": "karina.knudsen",
+ "displayName": "Karina Knudsen",
+ "avatarUrl": "/avatars/karina.jpg",
+ "employeeId": "EMP001",
+ "events": [
+ {
+ "id": "1",
+ "title": "Balayage langt hår",
+ "start": "2025-08-05T10:00:00",
+ "end": "2025-08-05T11:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 60, "color": "#9c27b0" }
+ },
+ {
+ "id": "2",
+ "title": "Klipning og styling",
+ "start": "2025-08-05T14:00:00",
+ "end": "2025-08-05T15:30:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 90, "color": "#e91e63" }
+ }
+ ]
+ },
+ {
+ "name": "maria.hansen",
+ "displayName": "Maria Hansen",
+ "avatarUrl": "/avatars/maria.jpg",
+ "employeeId": "EMP002",
+ "events": [
+ {
+ "id": "3",
+ "title": "Permanent",
+ "start": "2025-08-05T09:00:00",
+ "end": "2025-08-05T11:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#3f51b5" }
+ },
+ {
+ "id": "4",
+ "title": "Farve behandling",
+ "start": "2025-08-05T13:00:00",
+ "end": "2025-08-05T15:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#ff9800" }
+ }
+ ]
+ },
+ {
+ "name": "lars.nielsen",
+ "displayName": "Lars Nielsen",
+ "avatarUrl": "/avatars/lars.jpg",
+ "employeeId": "EMP003",
+ "events": [
+ {
+ "id": "5",
+ "title": "Herreklipning",
+ "start": "2025-08-05T11:00:00",
+ "end": "2025-08-05T11:30:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#795548" }
+ },
+ {
+ "id": "6",
+ "title": "Skæg trimning",
+ "start": "2025-08-05T16:00:00",
+ "end": "2025-08-05T16:30:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#607d8b" }
+ }
+ ]
+ },
+ {
+ "name": "anna.petersen",
+ "displayName": "Anna Petersen",
+ "avatarUrl": "/avatars/anna.jpg",
+ "employeeId": "EMP004",
+ "events": [
+ {
+ "id": "7",
+ "title": "Bryllupsfrisure",
+ "start": "2025-08-05T08:00:00",
+ "end": "2025-08-05T10:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#009688" }
+ }
+ ]
+ },
+ {
+ "name": "thomas.olsen",
+ "displayName": "Thomas Olsen",
+ "avatarUrl": "/avatars/thomas.jpg",
+ "employeeId": "EMP005",
+ "events": [
+ {
+ "id": "8",
+ "title": "Highlights",
+ "start": "2025-08-05T12:00:00",
+ "end": "2025-08-05T14:00:00",
+ "type": "work",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 120, "color": "#8bc34a" }
+ },
+ {
+ "id": "9",
+ "title": "Styling konsultation",
+ "start": "2025-08-05T15:00:00",
+ "end": "2025-08-05T15:30:00",
+ "type": "meeting",
+ "allDay": false,
+ "syncStatus": "synced",
+ "metadata": { "duration": 30, "color": "#cddc39" }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/factories/CalendarTypeFactory.ts b/src/factories/CalendarTypeFactory.ts
new file mode 100644
index 0000000..735ab16
--- /dev/null
+++ b/src/factories/CalendarTypeFactory.ts
@@ -0,0 +1,107 @@
+// Factory for creating calendar type-specific renderers
+
+import { CalendarType } from '../types/CalendarTypes';
+import { HeaderRenderer, DateHeaderRenderer, ResourceHeaderRenderer } from '../renderers/HeaderRenderer';
+import { ColumnRenderer, DateColumnRenderer, ResourceColumnRenderer } from '../renderers/ColumnRenderer';
+import { EventRendererStrategy, DateEventRenderer, ResourceEventRenderer } from '../renderers/EventRenderer';
+
+/**
+ * Renderer configuration for a calendar type
+ */
+export interface RendererConfig {
+ headerRenderer: HeaderRenderer;
+ columnRenderer: ColumnRenderer;
+ eventRenderer: EventRendererStrategy;
+}
+
+/**
+ * Factory for creating calendar type-specific renderers
+ */
+export class CalendarTypeFactory {
+ private static renderers: Map = new Map();
+
+ /**
+ * Initialize the factory with default renderers
+ */
+ static initialize(): void {
+ // Register default renderers
+ this.registerRenderers('date', {
+ headerRenderer: new DateHeaderRenderer(),
+ columnRenderer: new DateColumnRenderer(),
+ eventRenderer: new DateEventRenderer()
+ });
+
+ this.registerRenderers('resource', {
+ headerRenderer: new ResourceHeaderRenderer(),
+ columnRenderer: new ResourceColumnRenderer(),
+ eventRenderer: new ResourceEventRenderer()
+ });
+
+ console.log('CalendarTypeFactory: Initialized with default renderers', Array.from(this.renderers.keys()));
+ }
+
+ /**
+ * Register renderers for a calendar type
+ */
+ static registerRenderers(type: CalendarType, config: RendererConfig): void {
+ this.renderers.set(type, config);
+ console.log(`CalendarTypeFactory: Registered renderers for type '${type}'`);
+ }
+
+ /**
+ * Get renderers for a calendar type
+ */
+ static getRenderers(type: CalendarType): RendererConfig {
+ const renderers = this.renderers.get(type);
+
+ if (!renderers) {
+ console.warn(`CalendarTypeFactory: No renderers found for type '${type}', falling back to 'date'`);
+ return this.renderers.get('date')!;
+ }
+
+ return renderers;
+ }
+
+ /**
+ * Get header renderer for a calendar type
+ */
+ static getHeaderRenderer(type: CalendarType): HeaderRenderer {
+ return this.getRenderers(type).headerRenderer;
+ }
+
+ /**
+ * Get column renderer for a calendar type
+ */
+ static getColumnRenderer(type: CalendarType): ColumnRenderer {
+ return this.getRenderers(type).columnRenderer;
+ }
+
+ /**
+ * Get event renderer for a calendar type
+ */
+ static getEventRenderer(type: CalendarType): EventRendererStrategy {
+ return this.getRenderers(type).eventRenderer;
+ }
+
+ /**
+ * Check if a calendar type is supported
+ */
+ static isSupported(type: CalendarType): boolean {
+ return this.renderers.has(type);
+ }
+
+ /**
+ * Get all supported calendar types
+ */
+ static getSupportedTypes(): CalendarType[] {
+ return Array.from(this.renderers.keys());
+ }
+
+ /**
+ * Clear all registered renderers (useful for testing)
+ */
+ static clear(): void {
+ this.renderers.clear();
+ console.log('CalendarTypeFactory: All renderers cleared');
+ }
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index ef04ae4..63760c7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,7 +7,7 @@ import { EventManager } from './managers/EventManager.js';
import { EventRenderer } from './managers/EventRenderer.js';
import { GridManager } from './managers/GridManager.js';
import { ScrollManager } from './managers/ScrollManager.js';
-import { CalendarConfig } from './core/CalendarConfig.js';
+import { calendarConfig } from './core/CalendarConfig.js';
/**
* Initialize the calendar application
@@ -15,8 +15,8 @@ import { CalendarConfig } from './core/CalendarConfig.js';
function initializeCalendar(): void {
console.log('🗓️ Initializing Calendar Plantempus...');
- // Create calendar configuration
- const config = new CalendarConfig();
+ // Use the singleton calendar configuration
+ const config = calendarConfig;
// Initialize managers
const calendarManager = new CalendarManager(eventBus, config);
diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts
index 2300dd5..eb7c1c8 100644
--- a/src/managers/EventManager.ts
+++ b/src/managers/EventManager.ts
@@ -1,6 +1,7 @@
import { EventBus } from '../core/EventBus';
-import { IEventBus, CalendarEvent } from '../types/CalendarTypes';
+import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
import { EventTypes } from '../constants/EventTypes';
+import { calendarConfig } from '../core/CalendarConfig';
/**
* EventManager - Administrerer event lifecycle og CRUD operationer
@@ -11,9 +12,17 @@ export class EventManager {
private events: CalendarEvent[] = [];
constructor(eventBus: IEventBus) {
+ console.log('EventManager: Constructor called');
this.eventBus = eventBus;
this.setupEventListeners();
- this.loadMockData();
+ console.log('EventManager: About to call loadMockData()');
+ this.loadMockData().then(() => {
+ console.log('EventManager: loadMockData() completed, syncing events');
+ // Data loaded, sync events after loading
+ this.syncEvents();
+ }).catch(error => {
+ console.error('EventManager: loadMockData() failed:', error);
+ });
}
private setupEventListeners(): void {
@@ -30,189 +39,54 @@ export class EventManager {
});
}
- private loadMockData(): void {
- // Mock events for current week (July 27 - August 2, 2025)
- this.events = [
- // Sunday August 3, 2025
- {
- id: '1',
- title: 'Weekend Planning',
- start: '2025-08-03T10:00:00',
- end: '2025-08-03T11:00:00',
- type: 'work',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 60, color: '#9c27b0' } // Purple
- },
- // Monday August 4, 2025
- {
- id: '2',
- title: 'Team Standup',
- start: '2025-08-04T09:00:00',
- end: '2025-08-04T09:30:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 30, color: '#ff5722' } // Deep Orange
- },
- {
- id: '3',
- title: 'Project Kickoff',
- start: '2025-08-04T14:00:00',
- end: '2025-08-04T15:30:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 90, color: '#e91e63' } // Pink
- },
- // Tuesday August 5, 2025
- {
- id: '4',
- title: 'Deep Work Session',
- start: '2025-08-05T10:00:00',
- end: '2025-08-05T12:00:00',
- type: 'work',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 120, color: '#3f51b5' } // Indigo
- },
- {
- id: '5',
- title: 'Lunch Meeting',
- start: '2025-08-05T12:30:00',
- end: '2025-08-05T13:30:00',
- type: 'meal',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 60, color: '#ff9800' } // Orange
- },
- // Wednesday August 6, 2025
- {
- id: '6',
- title: 'Client Review',
- start: '2025-08-06T15:00:00',
- end: '2025-08-06T16:00:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 60, color: '#795548' } // Brown
- },
- // Thursday August 7, 2025
- {
- id: '7',
- title: 'Sprint Planning',
- start: '2025-08-07T09:00:00',
- end: '2025-08-07T10:30:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 90, color: '#607d8b' } // Blue Grey
- },
- {
- id: '8',
- title: 'Code Review',
- start: '2025-08-07T14:00:00',
- end: '2025-08-07T15:00:00',
- type: 'work',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 60, color: '#009688' } // Teal
- },
- // Friday August 8, 2025
- {
- id: '9',
- title: 'Team Standup',
- start: '2025-08-08T09:00:00',
- end: '2025-08-08T09:30:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 30, color: '#8bc34a' } // Light Green
- },
- {
- id: '10',
- title: 'Client Meeting',
- start: '2025-08-08T14:00:00',
- end: '2025-08-08T15:30:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 90, color: '#cddc39' } // Lime
- },
- // Saturday August 9, 2025
- {
- id: '11',
- title: 'Weekend Project',
- start: '2025-08-09T10:00:00',
- end: '2025-08-09T12:00:00',
- type: 'work',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 120, color: '#f44336' } // Red
- },
- // Test events for early/late hours
- {
- id: '12',
- title: 'Early Morning Workout',
- start: '2025-08-05T06:00:00',
- end: '2025-08-05T07:00:00',
- type: 'work',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 60, color: '#00bcd4' } // Cyan
- },
- {
- id: '13',
- title: 'Late Evening Call',
- start: '2025-08-06T21:00:00',
- end: '2025-08-06T22:00:00',
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 60, color: '#673ab7' } // Deep Purple
- },
- {
- id: '14',
- title: 'Midnight Deployment',
- start: '2025-08-07T23:00:00',
- end: '2025-08-08T01:00:00',
- type: 'work',
- allDay: false,
- syncStatus: 'synced',
- metadata: { duration: 120, color: '#ffc107' } // Amber
- },
- // All-day events for demo
- {
- id: '15',
- title: 'Company Holiday',
- start: '2025-08-04T00:00:00',
- end: '2025-08-05T23:59:59',
- type: 'milestone',
- allDay: true,
- syncStatus: 'synced',
- metadata: {
- duration: 1440, // Full day in minutes
- color: '#4caf50' // Green color
- }
- },
- {
- id: '16',
- title: 'Team Building Event',
- start: '2025-08-06T00:00:00',
- end: '2025-08-06T23:59:59',
- type: 'meeting',
- allDay: true,
- syncStatus: 'synced',
- metadata: {
- duration: 1440, // Full day in minutes
- color: '#2196f3' // Blue color
- }
+ private async loadMockData(): Promise {
+ try {
+ const calendarType = calendarConfig.getCalendarType();
+ let jsonFile: string;
+
+ console.log(`EventManager: Calendar type detected: '${calendarType}'`);
+
+ if (calendarType === 'resource') {
+ jsonFile = '/src/data/mock-resource-events.json';
+ } else {
+ jsonFile = '/src/data/mock-events.json';
}
- ];
-
- console.log(`EventManager: Loaded ${this.events.length} mock events`);
- console.log('EventManager: First event:', this.events[0]);
- console.log('EventManager: Last event:', this.events[this.events.length - 1]);
+
+ console.log(`EventManager: Loading ${calendarType} calendar data from ${jsonFile}`);
+
+ const response = await fetch(jsonFile);
+ if (!response.ok) {
+ throw new Error(`Failed to load mock events: ${response.status}`);
+ }
+
+ if (calendarType === 'resource') {
+ const resourceData: ResourceCalendarData = await response.json();
+ // Flatten events from all resources and add resource metadata
+ this.events = resourceData.resources.flatMap(resource =>
+ resource.events.map(event => ({
+ ...event,
+ resourceName: resource.name,
+ resourceDisplayName: resource.displayName,
+ resourceEmployeeId: resource.employeeId
+ }))
+ );
+ console.log(`EventManager: Loaded ${this.events.length} events from ${resourceData.resources.length} resources`);
+
+ // Emit resource data for GridManager
+ this.eventBus.emit(EventTypes.RESOURCE_DATA_LOADED, {
+ resourceData: resourceData
+ });
+ } else {
+ this.events = await response.json();
+ console.log(`EventManager: Loaded ${this.events.length} date calendar events`);
+ }
+
+ console.log('EventManager: First event:', this.events[0]);
+ console.log('EventManager: Last event:', this.events[this.events.length - 1]);
+ } catch (error) {
+ console.error('EventManager: Failed to load mock events:', error);
+ this.events = []; // Fallback to empty array
+ }
}
private syncEvents(): void {
diff --git a/src/managers/EventRenderer.ts b/src/managers/EventRenderer.ts
index 4ae98b1..ddcb03e 100644
--- a/src/managers/EventRenderer.ts
+++ b/src/managers/EventRenderer.ts
@@ -2,18 +2,22 @@ import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent } from '../types/CalendarTypes';
import { EventTypes } from '../constants/EventTypes';
import { calendarConfig } from '../core/CalendarConfig';
-import { DateUtils } from '../utils/DateUtils';
+import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
/**
- * EventRenderer - Render events i DOM med positionering
+ * EventRenderer - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection
*/
export class EventRenderer {
private eventBus: IEventBus;
+ private pendingEvents: CalendarEvent[] = [];
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
this.setupEventListeners();
+
+ // Initialize the factory (if not already done)
+ CalendarTypeFactory.initialize();
}
private setupEventListeners(): void {
@@ -35,17 +39,26 @@ export class EventRenderer {
// Clear existing events when view changes
this.clearEvents();
});
+
+ // Handle calendar type changes
+ this.eventBus.on(EventTypes.CALENDAR_TYPE_CHANGED, () => {
+ // Re-render events with new strategy
+ this.tryRenderEvents();
+ });
}
- private pendingEvents: CalendarEvent[] = [];
-
private tryRenderEvents(): void {
- // Only render if we have both events and grid is ready
+ // Only render if we have both events and appropriate columns are ready
console.log('EventRenderer: tryRenderEvents called, pending events:', this.pendingEvents.length);
+
if (this.pendingEvents.length > 0) {
- const dayColumns = document.querySelectorAll('swp-day-column');
- console.log('EventRenderer: Found', dayColumns.length, 'day columns');
- if (dayColumns.length > 0) {
+ const calendarType = calendarConfig.getCalendarType();
+ let columnsSelector = calendarType === 'resource' ? 'swp-resource-column' : 'swp-day-column';
+ const columns = document.querySelectorAll(columnsSelector);
+
+ console.log(`EventRenderer: Found ${columns.length} ${columnsSelector} elements for ${calendarType} calendar`);
+
+ if (columns.length > 0) {
this.renderEvents(this.pendingEvents);
this.pendingEvents = []; // Clear pending events after rendering
}
@@ -54,173 +67,34 @@ export class EventRenderer {
private renderEvents(events: CalendarEvent[]): void {
console.log('EventRenderer: renderEvents called with', events.length, 'events');
- console.log('EventRenderer: All events:', events.map(e => ({ title: e.title, allDay: e.allDay, start: e.start })));
- // Clear existing events first
- this.clearEvents();
-
- // Get current week dates for filtering
- const currentWeekDates = this.getCurrentWeekDates();
- // Filter events for current week and exclude all-day events (handled by GridManager)
- const currentWeekEvents = events.filter(event => {
- // Skip all-day events - they are handled by GridManager
- if (event.allDay) {
- return false;
- }
-
- const eventDate = new Date(event.start);
- const eventDateStr = DateUtils.formatDate(eventDate);
- const isInCurrentWeek = currentWeekDates.some(weekDate =>
- DateUtils.formatDate(weekDate) === eventDateStr
- );
- return isInCurrentWeek;
- });
-
- // Render each event in the correct day column
- currentWeekEvents.forEach(event => {
- const eventDate = new Date(event.start);
- const dayColumn = this.findDayColumn(eventDate);
-
- if (dayColumn) {
- const eventsLayer = dayColumn.querySelector('swp-events-layer');
- if (eventsLayer) {
- this.renderEvent(event, eventsLayer);
- } else {
- console.warn('EventRenderer: No events layer found in day column for', DateUtils.formatDate(eventDate));
- }
- } else {
- console.warn('EventRenderer: No day column found for event date', DateUtils.formatDate(eventDate));
- }
- });
+ // Get the appropriate event renderer strategy
+ const calendarType = calendarConfig.getCalendarType();
+ const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType);
+
+ console.log(`EventRenderer: Using ${calendarType} event renderer strategy`);
+
+ // Use strategy to render events
+ eventRenderer.renderEvents(events, calendarConfig);
// Emit event rendered
this.eventBus.emit(EventTypes.EVENT_RENDERED, {
- count: currentWeekEvents.length
+ count: events.filter(e => !e.allDay).length
});
-
- }
-
- /**
- * Get current week dates (Sunday to Saturday)
- */
- private getCurrentWeekDates(): Date[] {
- const today = new Date();
- const weekStart = DateUtils.getWeekStart(today, 0); // Sunday start
- const dates: Date[] = [];
-
- for (let i = 0; i < 7; i++) {
- const date = DateUtils.addDays(weekStart, i);
- dates.push(date);
- }
-
- return dates;
- }
-
- /**
- * Find day column for specific date
- */
- private findDayColumn(date: Date): HTMLElement | null {
- const dateStr = DateUtils.formatDate(date);
- const dayColumn = document.querySelector(`swp-day-column[data-date="${dateStr}"]`) as HTMLElement;
- console.log('EventRenderer: Looking for day column with date', dateStr, 'found:', !!dayColumn);
- return dayColumn;
- }
-
- private renderEvent(event: CalendarEvent, container: Element): void {
- const eventElement = document.createElement('swp-event');
- eventElement.dataset.eventId = event.id;
- eventElement.dataset.type = event.type;
-
- // Calculate position based on time
- const position = this.calculateEventPosition(event);
- eventElement.style.position = 'absolute';
- eventElement.style.top = `${position.top + 1}px`;
- eventElement.style.height = `${position.height - 1}px`;
-
- // Only set positioning and color - rest is in CSS
- eventElement.style.backgroundColor = event.metadata?.color || '#3498db';
-
- // Format time for display
- const startTime = this.formatTime(event.start);
- const endTime = this.formatTime(event.end);
-
- // Create event content
- eventElement.innerHTML = `
- ${startTime} - ${endTime}
- ${event.title}
- `;
-
- // Add event listeners
- this.addEventListeners(eventElement, event);
-
- container.appendChild(eventElement);
-
- // Event successfully rendered
- }
-
- private calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
- const startDate = new Date(event.start);
- const endDate = new Date(event.end);
-
- // Use dayStartHour to match time-axis positioning - this is the visible start hour (6 AM)
- const dayStartHour = calendarConfig.get('dayStartHour'); // 6 (6 AM)
- const hourHeight = calendarConfig.get('hourHeight');
-
- // Calculate minutes from visible day start (6 AM, not midnight)
- const eventHour = startDate.getHours();
- const eventMinutes = startDate.getMinutes();
- const startMinutes = (eventHour - dayStartHour) * 60 + eventMinutes;
-
- // Calculate duration in minutes
- const duration = (endDate.getTime() - startDate.getTime()) / (1000 * 60);
-
- // Convert to pixels - position relative to visible time-grid (starts at dayStartHour)
- const top = startMinutes * (hourHeight / 60);
- const height = duration * (hourHeight / 60);
-
-
- return { top, height };
- }
-
- private formatTime(isoString: string): string {
- const date = new Date(isoString);
- const hours = date.getHours();
- const minutes = date.getMinutes();
-
- const period = hours >= 12 ? 'PM' : 'AM';
- const displayHours = hours % 12 || 12;
- const displayMinutes = minutes.toString().padStart(2, '0');
-
- return `${displayHours}:${displayMinutes} ${period}`;
- }
-
- private addEventListeners(eventElement: HTMLElement, event: CalendarEvent): void {
- // Click handler
- eventElement.addEventListener('click', (e) => {
- e.stopPropagation();
- this.eventBus.emit(EventTypes.EVENT_SELECTED, {
- event,
- element: eventElement
- });
- });
-
- // Hover effects are handled by CSS
- // Hover effects are now handled by CSS
}
private clearEvents(): void {
- const eventsLayers = document.querySelectorAll('swp-events-layer');
- eventsLayers.forEach(layer => {
- layer.innerHTML = '';
- });
+ const calendarType = calendarConfig.getCalendarType();
+ const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType);
+ eventRenderer.clearEvents();
}
public refresh(): void {
- // Request fresh events from EventManager
- this.eventBus.emit(EventTypes.REFRESH_REQUESTED);
+ this.tryRenderEvents();
}
public destroy(): void {
+ this.pendingEvents = [];
this.clearEvents();
}
}
\ No newline at end of file
diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts
index f2a2c1b..b87845d 100644
--- a/src/managers/GridManager.ts
+++ b/src/managers/GridManager.ts
@@ -1,9 +1,13 @@
-// Grid structure management - Simple CSS Grid Implementation
+// Grid structure management - Simple CSS Grid Implementation with Strategy Pattern
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { EventTypes } from '../constants/EventTypes';
import { DateUtils } from '../utils/DateUtils';
+import { ResourceCalendarData } from '../types/CalendarTypes';
+import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
+import { HeaderRenderContext } from '../renderers/HeaderRenderer';
+import { ColumnRenderContext } from '../renderers/ColumnRenderer';
/**
* Grid position interface
@@ -15,19 +19,23 @@ interface GridPosition {
}
/**
- * Manages the calendar grid structure using simple CSS Grid
+ * Manages the calendar grid structure using simple CSS Grid with Strategy Pattern
*/
export class GridManager {
private container: HTMLElement | null = null;
private grid: HTMLElement | null = null;
private currentWeek: Date | null = null;
private allDayEvents: any[] = []; // Store all-day events for current week
+ private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
constructor() {
this.init();
}
private init(): void {
+ // Initialize the factory
+ CalendarTypeFactory.initialize();
+
this.findElements();
this.subscribeToEvents();
@@ -58,6 +66,11 @@ export class GridManager {
}
});
+ // Re-render on calendar type change
+ eventBus.on(EventTypes.CALENDAR_TYPE_CHANGED, () => {
+ this.render();
+ });
+
// Re-render on view change
eventBus.on(EventTypes.VIEW_CHANGE, () => {
this.render();
@@ -83,6 +96,21 @@ export class GridManager {
this.updateAllDayEvents(detail.events);
});
+ // Handle resource data loaded
+ eventBus.on(EventTypes.RESOURCE_DATA_LOADED, (e: Event) => {
+ const detail = (e as CustomEvent).detail;
+ this.resourceData = detail.resourceData;
+ console.log(`GridManager: Received resource data for ${this.resourceData!.resources.length} resources`);
+
+ // Update grid styles with new column count immediately
+ this.updateGridStyles();
+
+ // Re-render if grid is already rendered
+ if (this.grid && this.grid.children.length > 0) {
+ this.render();
+ }
+ });
+
// Handle grid clicks
this.setupGridInteractions();
}
@@ -122,11 +150,8 @@ export class GridManager {
console.log('GridManager: First render - creating grid structure');
// Create POC structure: header-spacer + time-axis + week-container + right-column + bottom spacers
this.createHeaderSpacer();
- this.createRightHeaderSpacer();
this.createTimeAxis();
this.createWeekContainer();
- this.createRightColumn();
- this.createBottomRow();
} else {
console.log('GridManager: Re-render - updating existing structure');
// Just update the week header for all-day events
@@ -146,27 +171,6 @@ export class GridManager {
this.grid.appendChild(headerSpacer);
}
- /**
- * Create right header spacer for scrollbar alignment
- */
- private createRightHeaderSpacer(): void {
- if (!this.grid) return;
-
- const rightHeaderSpacer = document.createElement('swp-right-header-spacer');
- this.grid.appendChild(rightHeaderSpacer);
- }
-
- /**
- * Create right column for scrollbar area
- */
- private createRightColumn(): void {
- if (!this.grid) return;
-
- const rightColumn = document.createElement('swp-right-column');
- this.grid.appendChild(rightColumn);
- }
-
-
/**
* Create time axis (positioned beside week container) like in POC
*/
@@ -192,15 +196,15 @@ export class GridManager {
}
/**
- * Create week container with header and scrollable content like in POC
+ * Create week container with header and scrollable content using Strategy Pattern
*/
private createWeekContainer(): void {
if (!this.grid || !this.currentWeek) return;
- const weekContainer = document.createElement('swp-week-container');
+ const weekContainer = document.createElement('swp-grid-container');
- // Create week header
- const weekHeader = document.createElement('swp-week-header');
+ // Create week header using Strategy Pattern
+ const weekHeader = document.createElement('swp-calendar-header');
this.renderWeekHeaders(weekHeader);
weekContainer.appendChild(weekHeader);
@@ -212,7 +216,7 @@ export class GridManager {
const gridLines = document.createElement('swp-grid-lines');
timeGrid.appendChild(gridLines);
- // Create day columns
+ // Create day columns using Strategy Pattern
const dayColumns = document.createElement('swp-day-columns');
this.renderDayColumns(dayColumns);
timeGrid.appendChild(dayColumns);
@@ -224,74 +228,60 @@ export class GridManager {
}
/**
- * Create bottom row with spacers
- */
- private createBottomRow(): void {
- if (!this.grid) return;
-
- // Bottom spacer (left)
- const bottomSpacer = document.createElement('swp-bottom-spacer');
- this.grid.appendChild(bottomSpacer);
-
- // Bottom middle spacer
- const bottomMiddleSpacer = document.createElement('swp-bottom-middle-spacer');
- this.grid.appendChild(bottomMiddleSpacer);
-
- // Right bottom spacer
- const rightBottomSpacer = document.createElement('swp-right-bottom-spacer');
- this.grid.appendChild(rightBottomSpacer);
- }
-
- /**
- * Render week headers like in POC
+ * Render week headers using Strategy Pattern
*/
private renderWeekHeaders(weekHeader: HTMLElement): void {
if (!this.currentWeek) return;
- const dates = this.getWeekDates(this.currentWeek);
- const weekDays = calendarConfig.get('weekDays');
- const daysToShow = dates.slice(0, weekDays);
+ const calendarType = calendarConfig.getCalendarType();
+ const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
+
+ const context: HeaderRenderContext = {
+ currentWeek: this.currentWeek,
+ config: calendarConfig,
+ allDayEvents: this.allDayEvents,
+ resourceData: this.resourceData
+ };
- daysToShow.forEach((date) => {
- const header = document.createElement('swp-day-header');
- if (this.isToday(date)) {
- (header as any).dataset.today = 'true';
- }
-
- header.innerHTML = `
- ${this.getDayName(date)}
- ${date.getDate()}
- `;
- (header as any).dataset.date = this.formatDate(date);
-
- weekHeader.appendChild(header);
- });
-
- // Render all-day events in row 2
- this.renderAllDayEvents(weekHeader);
+ headerRenderer.render(weekHeader, context);
// Update spacer heights based on all-day events
this.updateSpacerHeights();
}
+ /**
+ * Render day columns using Strategy Pattern
+ */
+ private renderDayColumns(dayColumns: HTMLElement): void {
+ if (!this.currentWeek) return;
+
+ console.log('GridManager: renderDayColumns called');
+ const calendarType = calendarConfig.getCalendarType();
+ const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
+
+ const context: ColumnRenderContext = {
+ currentWeek: this.currentWeek,
+ config: calendarConfig,
+ resourceData: this.resourceData
+ };
+
+ columnRenderer.render(dayColumns, context);
+ }
+
/**
* Update only the week header (for all-day events) without rebuilding entire grid
*/
private updateWeekHeader(): void {
if (!this.grid || !this.currentWeek) return;
- const weekHeader = this.grid.querySelector('swp-week-header');
+ const weekHeader = this.grid.querySelector('swp-calendar-header');
if (!weekHeader) return;
- // Clear existing all-day events but keep day headers
- const allDayEvents = weekHeader.querySelectorAll('swp-allday-event');
- allDayEvents.forEach(event => event.remove());
+ // Clear existing content
+ weekHeader.innerHTML = '';
- // Re-render all-day events
- this.renderAllDayEvents(weekHeader as HTMLElement);
-
- // Update spacer heights
- this.updateSpacerHeights();
+ // Re-render headers using Strategy Pattern
+ this.renderWeekHeaders(weekHeader as HTMLElement);
}
/**
@@ -320,64 +310,6 @@ export class GridManager {
}
}
- /**
- * Render all-day events in week header row 2
- */
- private renderAllDayEvents(weekHeader: HTMLElement): void {
- if (!this.currentWeek) return;
-
- const dates = this.getWeekDates(this.currentWeek);
- const weekDays = calendarConfig.get('weekDays');
- const daysToShow = dates.slice(0, weekDays);
-
- // Process each all-day event to calculate its span
- this.allDayEvents.forEach(event => {
- const startDate = new Date(event.start);
- const endDate = new Date(event.end);
-
- // Find start and end column indices
- let startColumnIndex = -1;
- let endColumnIndex = -1;
-
- daysToShow.forEach((date, index) => {
- const dateStr = this.formatDate(date);
- const startDateStr = this.formatDate(startDate);
- const endDateStr = this.formatDate(endDate);
-
- if (dateStr === startDateStr) {
- startColumnIndex = index;
- }
-
- // For end date, we need to check if the event spans to this day
- // All-day events typically end at 23:59:59, so we check if this date is <= end date
- if (date <= endDate) {
- endColumnIndex = index;
- }
- });
-
- // Only render if the event starts within the visible week
- if (startColumnIndex >= 0) {
- // If end column is not found or is before start, default to single day
- if (endColumnIndex < startColumnIndex) {
- endColumnIndex = startColumnIndex;
- }
-
- const allDayEvent = document.createElement('swp-allday-event');
- allDayEvent.textContent = event.title;
-
- // Set grid column span: start column (1-based) to end column + 1 (1-based)
- const gridColumnStart = startColumnIndex + 1;
- const gridColumnEnd = endColumnIndex + 2; // +2 because grid columns are 1-based and we want to include the end column
- allDayEvent.style.gridColumn = `${gridColumnStart} / ${gridColumnEnd}`;
- allDayEvent.style.backgroundColor = event.metadata?.color || '#ff9800';
-
- console.log(`GridManager: All-day event "${event.title}" spans columns ${gridColumnStart} to ${gridColumnEnd-1} (${endColumnIndex - startColumnIndex + 1} days)`);
-
- weekHeader.appendChild(allDayEvent);
- }
- });
- }
-
/**
* Update spacer heights based on all-day events presence
*/
@@ -393,33 +325,6 @@ export class GridManager {
console.log('GridManager: Updated --all-day-row-height to', `${allDayHeight}px`, 'for', allDayEventCount, 'events');
}
- /**
- * Render day columns like in POC
- */
- private renderDayColumns(dayColumns: HTMLElement): void {
- console.log('GridManager: renderDayColumns called');
- if (!this.currentWeek) {
- console.log('GridManager: No currentWeek, returning');
- return;
- }
-
- const dates = this.getWeekDates(this.currentWeek);
- const weekDays = calendarConfig.get('weekDays');
- const daysToShow = dates.slice(0, weekDays);
-
- console.log('GridManager: About to render', daysToShow.length, 'day columns');
-
- daysToShow.forEach((date, dayIndex) => {
- const column = document.createElement('swp-day-column');
- (column as any).dataset.date = this.formatDate(date);
-
- const eventsLayer = document.createElement('swp-events-layer');
- column.appendChild(eventsLayer);
-
- dayColumns.appendChild(column);
- });
- }
-
/**
* Update grid CSS variables
*/
@@ -427,6 +332,7 @@ export class GridManager {
const root = document.documentElement;
const config = calendarConfig.getAll();
const calendar = document.querySelector('swp-calendar') as HTMLElement;
+ const calendarType = calendarConfig.getCalendarType();
// Set CSS variables
root.style.setProperty('--hour-height', `${config.hourHeight}px`);
@@ -437,6 +343,15 @@ export class GridManager {
root.style.setProperty('--work-start-hour', config.workStartHour.toString());
root.style.setProperty('--work-end-hour', config.workEndHour.toString());
+ // Set number of columns based on calendar type
+ let columnCount = 7; // Default for date mode
+ if (calendarType === 'resource' && this.resourceData) {
+ columnCount = this.resourceData.resources.length;
+ } else if (calendarType === 'date') {
+ columnCount = config.weekDays;
+ }
+ root.style.setProperty('--grid-columns', columnCount.toString());
+
// Set day column min width based on fitToWidth setting
if (config.fitToWidth) {
root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space
@@ -448,6 +363,8 @@ export class GridManager {
if (calendar) {
calendar.setAttribute('data-fit-to-width', config.fitToWidth.toString());
}
+
+ console.log('GridManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar');
}
/**
@@ -456,18 +373,20 @@ export class GridManager {
private setupGridInteractions(): void {
if (!this.grid) return;
- // Click handler for day columns
+ // Click handler for day columns (works for both date and resource columns)
this.grid.addEventListener('click', (e: MouseEvent) => {
// Ignore if clicking on an event
if ((e.target as Element).closest('swp-event')) return;
- const dayColumn = (e.target as Element).closest('swp-day-column') as HTMLElement;
+ const dayColumn = (e.target as Element).closest('swp-day-column, swp-resource-column') as HTMLElement;
if (!dayColumn) return;
const position = this.getClickPosition(e, dayColumn);
eventBus.emit(EventTypes.GRID_CLICK, {
date: (dayColumn as any).dataset.date,
+ resource: (dayColumn as any).dataset.resource,
+ employeeId: (dayColumn as any).dataset.employeeId,
time: position.time,
minutes: position.minutes
});
@@ -478,13 +397,15 @@ export class GridManager {
// Ignore if clicking on an event
if ((e.target as Element).closest('swp-event')) return;
- const dayColumn = (e.target as Element).closest('swp-day-column') as HTMLElement;
+ const dayColumn = (e.target as Element).closest('swp-day-column, swp-resource-column') as HTMLElement;
if (!dayColumn) return;
const position = this.getClickPosition(e, dayColumn);
eventBus.emit(EventTypes.GRID_DBLCLICK, {
date: (dayColumn as any).dataset.date,
+ resource: (dayColumn as any).dataset.resource,
+ employeeId: (dayColumn as any).dataset.employeeId,
time: position.time,
minutes: position.minutes
});
@@ -537,36 +458,6 @@ export class GridManager {
* Utility methods
*/
- private formatHour(hour: number): string {
- const period = hour >= 12 ? 'PM' : 'AM';
- const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
- return `${displayHour} ${period}`;
- }
-
- private formatDate(date: Date): string {
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
- }
-
- private getDayName(date: Date): string {
- const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
- return days[date.getDay()];
- }
-
- private getWeekDates(weekStart: Date): Date[] {
- const dates: Date[] = [];
- for (let i = 0; i < 7; i++) {
- const date = new Date(weekStart);
- date.setDate(weekStart.getDate() + i);
- dates.push(date);
- }
- return dates;
- }
-
- private isToday(date: Date): boolean {
- const today = new Date();
- return date.toDateString() === today.toDateString();
- }
-
private minutesToTime(totalMinutes: number): string {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts
index c5664fc..f025a67 100644
--- a/src/managers/ScrollManager.ts
+++ b/src/managers/ScrollManager.ts
@@ -108,7 +108,7 @@ export class ScrollManager {
this.scrollableContent = document.querySelector('swp-scrollable-content');
this.calendarContainer = document.querySelector('swp-calendar-container');
this.timeAxis = document.querySelector('swp-time-axis');
- this.weekHeader = document.querySelector('swp-week-header');
+ this.weekHeader = document.querySelector('swp-calendar-header');
console.log('ScrollManager: Found elements:', {
scrollableContent: !!this.scrollableContent,
@@ -173,7 +173,7 @@ export class ScrollManager {
const navHeight = navigation ? navigation.getBoundingClientRect().height : 0;
// Find week header height
- const weekHeader = document.querySelector('swp-week-header');
+ const weekHeader = document.querySelector('swp-calendar-header');
const headerHeight = weekHeader ? weekHeader.getBoundingClientRect().height : 80;
// Calculate available height for scrollable content
diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts
new file mode 100644
index 0000000..ff862f5
--- /dev/null
+++ b/src/renderers/ColumnRenderer.ts
@@ -0,0 +1,87 @@
+// Column rendering strategy interface and implementations
+
+import { CalendarConfig } from '../core/CalendarConfig';
+import { ResourceCalendarData } from '../types/CalendarTypes';
+
+/**
+ * Interface for column rendering strategies
+ */
+export interface ColumnRenderer {
+ render(dayColumns: HTMLElement, context: ColumnRenderContext): void;
+}
+
+/**
+ * Context for column rendering
+ */
+export interface ColumnRenderContext {
+ currentWeek: Date;
+ config: CalendarConfig;
+ resourceData?: ResourceCalendarData | null;
+}
+
+/**
+ * Date-based column renderer (original functionality)
+ */
+export class DateColumnRenderer implements ColumnRenderer {
+ render(dayColumns: HTMLElement, context: ColumnRenderContext): void {
+ const { currentWeek, config } = context;
+
+ const dates = this.getWeekDates(currentWeek);
+ const weekDays = config.get('weekDays');
+ const daysToShow = dates.slice(0, weekDays);
+
+ console.log('DateColumnRenderer: About to render', daysToShow.length, 'date columns');
+
+ daysToShow.forEach((date) => {
+ const column = document.createElement('swp-day-column');
+ (column as any).dataset.date = this.formatDate(date);
+
+ const eventsLayer = document.createElement('swp-events-layer');
+ column.appendChild(eventsLayer);
+
+ dayColumns.appendChild(column);
+ });
+ }
+
+ private getWeekDates(weekStart: Date): Date[] {
+ const dates: Date[] = [];
+ for (let i = 0; i < 7; i++) {
+ const date = new Date(weekStart);
+ date.setDate(weekStart.getDate() + i);
+ dates.push(date);
+ }
+ return dates;
+ }
+
+ private formatDate(date: Date): string {
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+ }
+}
+
+/**
+ * Resource-based column renderer
+ */
+export class ResourceColumnRenderer implements ColumnRenderer {
+ render(dayColumns: HTMLElement, context: ColumnRenderContext): void {
+ const { resourceData } = context;
+
+ if (!resourceData) {
+ console.warn('ResourceColumnRenderer: No resource data available for resource columns');
+ return;
+ }
+
+ console.log('ResourceColumnRenderer: About to render', resourceData.resources.length, 'resource columns');
+
+ resourceData.resources.forEach((resource) => {
+ const column = document.createElement('swp-resource-column');
+ (column as any).dataset.resource = resource.name;
+ (column as any).dataset.employeeId = resource.employeeId;
+ (column as any).dataset.date = resourceData.date;
+
+ const eventsLayer = document.createElement('swp-events-layer');
+ column.appendChild(eventsLayer);
+
+ dayColumns.appendChild(column);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts
new file mode 100644
index 0000000..e0e2e52
--- /dev/null
+++ b/src/renderers/EventRenderer.ts
@@ -0,0 +1,141 @@
+// Event rendering strategy interface and implementations
+
+import { CalendarEvent } from '../types/CalendarTypes';
+import { CalendarConfig } from '../core/CalendarConfig';
+import { DateUtils } from '../utils/DateUtils';
+
+/**
+ * Interface for event rendering strategies
+ */
+export interface EventRendererStrategy {
+ findColumn(event: CalendarEvent): HTMLElement | null;
+ renderEvents(events: CalendarEvent[], config: CalendarConfig): void;
+ clearEvents(): void;
+}
+
+/**
+ * Base class for event renderers with common functionality
+ */
+export abstract class BaseEventRenderer implements EventRendererStrategy {
+ abstract findColumn(event: CalendarEvent): HTMLElement | null;
+
+ renderEvents(events: CalendarEvent[], config: CalendarConfig): void {
+ console.log('BaseEventRenderer: renderEvents called with', events.length, 'events');
+
+ // Clear existing events first
+ this.clearEvents();
+
+ // Filter out all-day events (handled by GridManager)
+ const nonAllDayEvents = events.filter(event => !event.allDay);
+ console.log('BaseEventRenderer: Rendering', nonAllDayEvents.length, 'non-all-day events');
+
+ // Render each event in the correct column
+ nonAllDayEvents.forEach(event => {
+ const column = this.findColumn(event);
+
+ if (column) {
+ const eventsLayer = column.querySelector('swp-events-layer');
+ if (eventsLayer) {
+ this.renderEvent(event, eventsLayer, config);
+ } else {
+ console.warn('BaseEventRenderer: No events layer found in column for event', event.title);
+ }
+ } else {
+ console.warn('BaseEventRenderer: No column found for event', event.title);
+ }
+ });
+ }
+
+ protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void {
+ const eventElement = document.createElement('swp-event');
+ eventElement.dataset.eventId = event.id;
+ eventElement.dataset.type = event.type;
+
+ // Calculate position based on time
+ const position = this.calculateEventPosition(event, config);
+ eventElement.style.position = 'absolute';
+ eventElement.style.top = `${position.top + 1}px`;
+ eventElement.style.height = `${position.height - 1}px`;
+
+ // Set color
+ eventElement.style.backgroundColor = event.metadata?.color || '#3498db';
+
+ // Format time for display
+ const startTime = this.formatTime(event.start);
+ const endTime = this.formatTime(event.end);
+
+ // Create event content
+ eventElement.innerHTML = `
+ ${startTime} - ${endTime}
+ ${event.title}
+ `;
+
+ container.appendChild(eventElement);
+ }
+
+ protected calculateEventPosition(event: CalendarEvent, config: CalendarConfig): { top: number; height: number } {
+ const startDate = new Date(event.start);
+ const endDate = new Date(event.end);
+
+ const dayStartHour = config.get('dayStartHour');
+ const hourHeight = config.get('hourHeight');
+
+ // Calculate minutes from visible day start
+ const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
+ const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
+ const dayStartMinutes = dayStartHour * 60;
+
+ // Calculate top position (subtract day start to align with time axis)
+ const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
+
+ // Calculate height
+ const durationMinutes = endMinutes - startMinutes;
+ const height = (durationMinutes / 60) * hourHeight;
+
+ return { top, height };
+ }
+
+ protected formatTime(isoString: string): string {
+ const date = new Date(isoString);
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const period = hours >= 12 ? 'PM' : 'AM';
+ const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
+
+ return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
+ }
+
+ clearEvents(): void {
+ const existingEvents = document.querySelectorAll('swp-event');
+ existingEvents.forEach(event => event.remove());
+ }
+}
+
+/**
+ * Date-based event renderer
+ */
+export class DateEventRenderer extends BaseEventRenderer {
+ findColumn(event: CalendarEvent): HTMLElement | null {
+ const eventDate = new Date(event.start);
+ const dateStr = DateUtils.formatDate(eventDate);
+ const dayColumn = document.querySelector(`swp-day-column[data-date="${dateStr}"]`) as HTMLElement;
+ console.log('DateEventRenderer: Looking for day column with date', dateStr, 'found:', !!dayColumn);
+ return dayColumn;
+ }
+}
+
+/**
+ * Resource-based event renderer
+ */
+export class ResourceEventRenderer extends BaseEventRenderer {
+ findColumn(event: CalendarEvent): HTMLElement | null {
+ if (!event.resourceName) {
+ console.warn('ResourceEventRenderer: Event has no resourceName', event);
+ return null;
+ }
+
+ const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${event.resourceName}"]`) as HTMLElement;
+ console.log('ResourceEventRenderer: Looking for resource column with name', event.resourceName, 'found:', !!resourceColumn);
+ return resourceColumn;
+ }
+}
\ No newline at end of file
diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts
new file mode 100644
index 0000000..5347083
--- /dev/null
+++ b/src/renderers/HeaderRenderer.ts
@@ -0,0 +1,158 @@
+// Header rendering strategy interface and implementations
+
+import { CalendarConfig } from '../core/CalendarConfig';
+import { ResourceCalendarData } from '../types/CalendarTypes';
+
+/**
+ * Interface for header rendering strategies
+ */
+export interface HeaderRenderer {
+ render(weekHeader: HTMLElement, context: HeaderRenderContext): void;
+}
+
+/**
+ * Context for header rendering
+ */
+export interface HeaderRenderContext {
+ currentWeek: Date;
+ config: CalendarConfig;
+ allDayEvents?: any[];
+ resourceData?: ResourceCalendarData | null;
+}
+
+/**
+ * Date-based header renderer (original functionality)
+ */
+export class DateHeaderRenderer implements HeaderRenderer {
+ render(weekHeader: HTMLElement, context: HeaderRenderContext): void {
+ const { currentWeek, config, allDayEvents = [] } = context;
+
+ const dates = this.getWeekDates(currentWeek);
+ const weekDays = config.get('weekDays');
+ const daysToShow = dates.slice(0, weekDays);
+
+ daysToShow.forEach((date) => {
+ const header = document.createElement('swp-day-header');
+ if (this.isToday(date)) {
+ (header as any).dataset.today = 'true';
+ }
+
+ header.innerHTML = `
+ ${this.getDayName(date)}
+ ${date.getDate()}
+ `;
+ (header as any).dataset.date = this.formatDate(date);
+
+ weekHeader.appendChild(header);
+ });
+
+ // Render all-day events in row 2
+ this.renderAllDayEvents(weekHeader, context);
+ }
+
+ private renderAllDayEvents(weekHeader: HTMLElement, context: HeaderRenderContext): void {
+ const { currentWeek, config, allDayEvents = [] } = context;
+
+ const dates = this.getWeekDates(currentWeek);
+ const weekDays = config.get('weekDays');
+ const daysToShow = dates.slice(0, weekDays);
+
+ // Process each all-day event to calculate its span
+ allDayEvents.forEach(event => {
+ const startDate = new Date(event.start);
+ const endDate = new Date(event.end);
+
+ // Find start and end column indices
+ let startColumnIndex = -1;
+ let endColumnIndex = -1;
+
+ daysToShow.forEach((date, index) => {
+ const dateStr = this.formatDate(date);
+ const startDateStr = this.formatDate(startDate);
+
+ if (dateStr === startDateStr) {
+ startColumnIndex = index;
+ }
+
+ // For end date, we need to check if the event spans to this day
+ if (date <= endDate) {
+ endColumnIndex = index;
+ }
+ });
+
+ // Only render if the event starts within the visible week
+ if (startColumnIndex >= 0) {
+ // If end column is not found or is before start, default to single day
+ if (endColumnIndex < startColumnIndex) {
+ endColumnIndex = startColumnIndex;
+ }
+
+ const allDayEvent = document.createElement('swp-allday-event');
+ allDayEvent.textContent = event.title;
+
+ // Set grid column span: start column (1-based) to end column + 1 (1-based)
+ const gridColumnStart = startColumnIndex + 1;
+ const gridColumnEnd = endColumnIndex + 2;
+ allDayEvent.style.gridColumn = `${gridColumnStart} / ${gridColumnEnd}`;
+ allDayEvent.style.backgroundColor = event.metadata?.color || '#ff9800';
+
+ weekHeader.appendChild(allDayEvent);
+ }
+ });
+ }
+
+ private getWeekDates(weekStart: Date): Date[] {
+ const dates: Date[] = [];
+ for (let i = 0; i < 7; i++) {
+ const date = new Date(weekStart);
+ date.setDate(weekStart.getDate() + i);
+ dates.push(date);
+ }
+ return dates;
+ }
+
+ private isToday(date: Date): boolean {
+ const today = new Date();
+ return date.toDateString() === today.toDateString();
+ }
+
+ private formatDate(date: Date): string {
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+ }
+
+ private getDayName(date: Date): string {
+ const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+ return days[date.getDay()];
+ }
+}
+
+/**
+ * Resource-based header renderer
+ */
+export class ResourceHeaderRenderer implements HeaderRenderer {
+ render(weekHeader: HTMLElement, context: HeaderRenderContext): void {
+ const { resourceData } = context;
+
+ if (!resourceData) {
+ console.warn('ResourceHeaderRenderer: No resource data available for resource headers');
+ return;
+ }
+
+ resourceData.resources.forEach((resource) => {
+ const header = document.createElement('swp-resource-header');
+ header.setAttribute('data-resource', resource.name);
+ header.setAttribute('data-employee-id', resource.employeeId);
+
+ header.innerHTML = `
+
+
+
+ ${resource.displayName}
+ `;
+
+ weekHeader.appendChild(header);
+ });
+
+ console.log(`ResourceHeaderRenderer: Rendered ${resourceData.resources.length} resource headers`);
+ }
+}
\ No newline at end of file
diff --git a/src/types/CalendarTypes.ts b/src/types/CalendarTypes.ts
index c04701c..62599b8 100644
--- a/src/types/CalendarTypes.ts
+++ b/src/types/CalendarTypes.ts
@@ -3,10 +3,25 @@
export type ViewType = 'day' | 'week' | 'month';
export type CalendarView = ViewType; // Alias for compatibility
+export type CalendarType = 'date' | 'resource';
+
export type EventType = 'meeting' | 'meal' | 'work' | 'milestone';
export type SyncStatus = 'synced' | 'pending' | 'error';
+export interface Resource {
+ name: string;
+ displayName: string;
+ avatarUrl: string;
+ employeeId: string;
+ events: CalendarEvent[];
+}
+
+export interface ResourceCalendarData {
+ date: string;
+ resources: Resource[];
+}
+
export interface CalendarEvent {
id: string;
title: string;
@@ -15,6 +30,10 @@ export interface CalendarEvent {
type: EventType;
allDay: boolean;
syncStatus: SyncStatus;
+ // Resource information (only present in resource calendar mode)
+ resourceName?: string;
+ resourceDisplayName?: string;
+ resourceEmployeeId?: string;
recurringId?: string;
resources?: string[];
metadata?: Record;
diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css
index ac7803f..59c1af7 100644
--- a/wwwroot/css/calendar-layout-css.css
+++ b/wwwroot/css/calendar-layout-css.css
@@ -62,7 +62,7 @@ swp-header-spacer {
/* Week container for sliding */
-swp-week-container {
+swp-grid-container {
grid-column: 2;
grid-row: 1 / 3;
display: grid;
@@ -127,7 +127,7 @@ swp-time-grid::after {
left: 0;
right: 0;
bottom: 0;
- min-width: calc(var(--week-days) * var(--day-column-min-width)); /* Dynamic width like swp-grid-lines */
+ min-width: calc(var(--grid-columns, 7) * var(--day-column-min-width)); /* Dynamic width like swp-grid-lines */
background-image: repeating-linear-gradient(
to bottom,
transparent,
@@ -140,11 +140,11 @@ swp-time-grid::after {
}
/* Week header - dynamic height based on content */
-swp-week-header {
+swp-calendar-header {
display: grid;
- grid-template-columns: repeat(7, minmax(var(--day-column-min-width), 1fr));
+ grid-template-columns: repeat(var(--grid-columns, 7), minmax(var(--day-column-min-width), 1fr));
grid-template-rows: var(--header-height) auto; /* Row 1: use CSS variable, Row 2: auto-sized for all-day events */
- min-width: calc(var(--week-days) * var(--day-column-min-width)); /* Dynamic width */
+ min-width: calc(var(--grid-columns, 7) * var(--day-column-min-width)); /* Dynamic width */
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
position: sticky;
@@ -169,6 +169,47 @@ swp-day-header:last-child {
border-right: none;
}
+/* Resource header styling */
+swp-resource-header {
+ padding: 12px;
+ text-align: center;
+ border-right: 1px solid var(--color-grid-line);
+ border-bottom: 1px solid var(--color-grid-line);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-surface);
+}
+
+swp-resource-header:last-child {
+ border-right: none;
+}
+
+swp-resource-avatar {
+ display: block;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-bottom: 8px;
+ background: var(--color-border);
+}
+
+swp-resource-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+swp-resource-name {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--color-text);
+ text-align: center;
+}
+
swp-day-name {
display: block;
font-weight: 500;
@@ -274,7 +315,7 @@ swp-time-grid::before {
left: 0;
right: 0;
background: var(--color-work-hours);
- min-width: calc(var(--week-days) * var(--day-column-min-width));
+ min-width: calc(var(--grid-columns, 7) * var(--day-column-min-width));
pointer-events: none;
}
@@ -285,7 +326,7 @@ swp-grid-lines {
left: 0;
right: 0;
bottom: 0;
- min-width: calc(var(--week-days) * var(--day-column-min-width)); /* Dynamic width */
+ min-width: calc(var(--grid-columns, 7) * var(--day-column-min-width)); /* Dynamic width */
pointer-events: none;
z-index: var(--z-grid);
background-image: repeating-linear-gradient(
@@ -303,8 +344,8 @@ swp-day-columns {
position: absolute;
inset: 0;
display: grid;
- grid-template-columns: repeat(7, minmax(var(--day-column-min-width), 1fr));
- min-width: calc(var(--week-days) * var(--day-column-min-width)); /* Dynamic width */
+ grid-template-columns: repeat(var(--grid-columns, 7), minmax(var(--day-column-min-width), 1fr));
+ min-width: calc(var(--grid-columns, 7) * var(--day-column-min-width)); /* Dynamic width */
}
@@ -318,6 +359,17 @@ swp-day-column:last-child {
border-right: none;
}
+/* Resource column styling */
+swp-resource-column {
+ position: relative;
+ border-right: 1px solid var(--color-grid-line);
+ min-width: var(--day-column-min-width);
+}
+
+swp-resource-column:last-child {
+ border-right: none;
+}
+
swp-events-layer {
position: absolute;
inset: 0;
diff --git a/wwwroot/css/calendar.css b/wwwroot/css/calendar.css
index a550b73..95e2017 100644
--- a/wwwroot/css/calendar.css
+++ b/wwwroot/css/calendar.css
@@ -7,7 +7,7 @@ swp-calendar,
swp-calendar-nav,
swp-calendar-container,
swp-time-axis,
-swp-week-header,
+swp-calendar-header,
swp-scrollable-content,
swp-time-grid,
swp-day-columns,
@@ -15,7 +15,7 @@ swp-day-column,
swp-events-layer,
swp-event,
swp-loading-overlay,
-swp-week-container,
+swp-grid-container,
swp-grid-lines {
display: block;
}
@@ -164,7 +164,7 @@ swp-view-button[data-active="true"] {
/* Week container for sliding */
-swp-week-container {
+swp-grid-container {
grid-column: 2;
display: grid;
grid-template-rows: auto 1fr;
@@ -173,19 +173,19 @@ swp-week-container {
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
}
-swp-week-container.slide-out-left {
+swp-grid-container.slide-out-left {
transform: translateX(-100%);
}
-swp-week-container.slide-out-right {
+swp-grid-container.slide-out-right {
transform: translateX(100%);
}
-swp-week-container.slide-in-left {
+swp-grid-container.slide-in-left {
transform: translateX(-100%);
}
-swp-week-container.slide-in-right {
+swp-grid-container.slide-in-right {
transform: translateX(100%);
}