diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 84b43fa..3200db8 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -21,15 +21,7 @@
"WebFetch(domain:unpkg.com)",
"Bash(node -e:*)",
"Bash(ls:*)",
- "Bash(find:*)",
- "WebFetch(domain:www.elegantthemes.com)",
- "Bash(npm publish:*)",
- "Bash(npm init:*)",
- "Bash(node dist/bundle.js:*)",
- "Bash(node build.js:*)",
- "Bash(npm ls:*)",
- "Bash(npm view:*)",
- "Bash(npm update:*)"
+ "Bash(find:*)"
],
"deny": [],
"ask": []
diff --git a/.gitignore b/.gitignore
index 9bbe200..a0905c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Build outputs
bin/
obj/
+wwwroot/js/
# Node modules
node_modules/
@@ -29,5 +30,4 @@ Thumbs.db
*.suo
*.userosscache
*.sln.docstates
-
-packages/calendar/dist/
+js/
diff --git a/.workbench/image.png b/.workbench/image.png
index 196aaeb..cbecbe4 100644
Binary files a/.workbench/image.png and b/.workbench/image.png differ
diff --git a/.workbench/projectstructure.txt b/.workbench/projectstructure.txt
deleted file mode 100644
index 8507e45..0000000
--- a/.workbench/projectstructure.txt
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-Organizing Project Folder Structure: Function-Based vs Feature-Based
-Ina Lopez
-Ina Lopez
-
-Follow
-2 min read
-·
-Sep 3, 2024
-12
-
-
-
-
-When setting up a project, one of the most crucial decisions is how to organize the folder structure. The structure you choose can significantly impact productivity, scalability, and collaboration among developers. Two common approaches are function-based and feature-based organization. Both have their advantages, and the choice between them often depends on the size of the project and the number of developers involved.
-
-Function-Based Organization
-
-In a function-based folder structure, directories are organized based on the functions they provide. This is a popular approach, especially for smaller projects or teams. The idea is to group similar functionalities together, making it easy to locate specific files or components.
-
-For example, in a React project, the src directory might look like this:
-
-src/
-├── components/
-├── hooks/
-├── utils/
-
-Each folder contains files related to a specific function. Components, hooks, reducers, and utilities are neatly separated, making it easy to find and manage related code. This structure works well when the codebase is relatively small and developers need to quickly find and reuse functions.
-
-Pros:
-
-Easy to find similar functions.
-Encourages reuse of components and utilities.
-Clean and straightforward structure.
-Cons:
-
-As the project grows, it can become difficult to manage.
-Dependencies between different folders can increase complexity.
-Not ideal for teams working on different features simultaneously.
-Feature-Based Organization
-In larger projects with many developers, a feature-based folder structure can be more effective. Instead of organizing files by function, the top-level directories in the src folder are based on features or modules of the application. This approach allows teams to work on separate features independently without interfering with other parts of the codebase.
-
-Get Ina Lopez’s stories in your inbox
-Join Medium for free to get updates from this writer.
-
-Enter your email
-Subscribe
-For example, a feature-based structure might look like this:
-
-src/
-├─ signup/
-│ ├── components/
-│ ├── hooks/
-│ └── utils/
-├─ checkout/
-│ ├── components/
-│ ├── hooks/
-│ └── utils/
-├─ dashboard/
-│ ├── components/
-│ ├── hooks/
-│ └── utils/
-└─ profile/
-├── components/
-├── hooks/
-└── utils/
-
-Each folder contains all the components, hooks, reducers, and utilities specific to that feature. This structure makes it easier for developers to focus on specific features, reduces conflicts, and simplifies the onboarding process for new team members.
-
-Pros:
-
-Better suited for larger projects with multiple developers.
-Encourages modularity and separation of concerns.
-Easier to manage and scale as the project grows.
-Reduces the risk of conflicts between different teams.
-Cons:
-
-Some duplication of code across features is possible.
-Finding reusable components can be more challenging.
-Can be overwhelming if the project has too many small features.
-Conclusion
-Choosing the right folder structure depends on your project’s size and team dynamics. Function-based organization is ideal for small to medium projects with fewer developers, offering simplicity and ease of reuse. However, as your project grows and more developers are involved, a feature-based approach becomes more effective, enabling modularity, better collaboration, and easier scaling.
-
-For some projects, a hybrid approach might work best, combining both methods to balance flexibility and organization. Ultimately, the key is to select a structure that supports the current and future needs of your project and team.
-
\ No newline at end of file
diff --git a/.workbench/spec-salary.html b/.workbench/spec-salary.html
deleted file mode 100644
index 41ee387..0000000
--- a/.workbench/spec-salary.html
+++ /dev/null
@@ -1,564 +0,0 @@
-
-
-
-
-
- Lønspecifikation – Januar 2026 (2 sider)
-
-
-
-
-
-
-
-
-
-
Lønspecifikation
-
Periode: Januar 2026
-
-
-
-
-
-
-
-
Bruttoløn (Januar 2026)
-
34.063,50 kr
-
-
-
Side 1: Overblik
-
- Kort opsummering til udlevering.
- Detaljer findes på side 2.
-
-
-
-
-
-
-
-
Samlet lønopgørelse
- Alle beløb i DKK
-
-
-
-
-
- Løndel
- Beløb
-
-
-
-
- Grundløn inkl. overarbejde
- 29.322,50 kr
-
-
- Provision i alt
- 3.685,00 kr
-
-
- Tillæg i alt
- 1.056,00 kr
-
-
- Bruttoløn
- 34.063,50 kr
-
-
-
-
- (Hvis du senere vil have skat/AM-bidrag/nettoløn med, kan det tilføjes som ekstra blok her.)
-
-
-
-
-
-
-
Saldi
- Ved periodens slut
-
-
-
-
-
- Type
- Optjent
- Afholdt
- Rest
-
-
-
-
- Ferie (dage)
- 18,5
- 6,0
- 12,5
-
-
- Afspadsering (timer)
- 12,0
- 4,0
- 8,0
-
-
-
-
Saldi er opgjort som angivet på lønspecifikationen.
-
-
-
-
-
-
-
Hurtigt resumé
- Det vigtigste
-
-
-
-
-
- Nøglepunkt
- Værdi
-
-
-
-
- Normaltimer
- 148,0 t
-
-
- Overarbejde
- 7,0 t
-
-
- Provision (services + produkter)
- 3.685,00 kr
-
-
- Tillæg (aften + lørdag + søndag)
- 1.056,00 kr
-
-
-
-
-
-
-
-
Side 1/2 · Overblik
-
Lønspecifikation · Januar 2026
-
-
-
-
-
-
-
-
Lønspecifikation – Detaljer
-
Periode: Januar 2026 · Medarbejder: Emma Larsen
-
-
-
-
-
-
-
-
Arbejdstid pr. uge
- Normal + overtid
-
-
-
-
-
- Uge
- Normaltimer
- Overtid
- Beløb
-
-
-
-
- Uge 1 (30. dec – 5. jan)
- 37,0 t
- 2,0 t
- 7.400,00 kr
-
-
- Uge 2 (6. – 12. jan)
- 37,0 t
- 3,5 t
- 7.816,25 kr
-
-
- Uge 3 (13. – 19. jan)
- 37,0 t
- 0,0 t
- 6.845,00 kr
-
-
- Uge 4 (20. – 26. jan)
- 37,0 t
- 1,5 t
- 7.261,25 kr
-
-
- I alt
- 148,0 t
- 7,0 t
- 29.322,50 kr
-
-
-
-
- Satser: Normal 185,00 kr/time. Overtid (50%) 277,50 kr/time.
-
-
-
-
-
-
-
Provision
- Services & produkter
-
-
-
- Services: 15% af omsætning over minimum (220 kr/time).
- Produkter: 10% af salg.
-
-
-
-
- Uge
- Service prov.
- Produkt prov.
- I alt
-
-
-
-
- Uge 1
- 573,00 kr
- 210,00 kr
- 783,00 kr
-
-
- Uge 2
- 883,50 kr
- 320,00 kr
- 1.203,50 kr
-
-
- Uge 3
- 459,00 kr
- 180,00 kr
- 639,00 kr
-
-
- Uge 4
- 769,50 kr
- 290,00 kr
- 1.059,50 kr
-
-
- I alt
- 2.685,00 kr
- 1.000,00 kr
- 3.685,00 kr
-
-
-
-
-
-
-
-
-
Tillæg & fravær
- Opsummering
-
-
-
-
-
-
-
- Tillæg
- Timer
- Beløb
-
-
-
-
- Aftentillæg (hverdage 18–21)
- 12,0
- 336,00 kr
-
-
- Lørdagstillæg (før kl. 14)
- 16,0
- 720,00 kr
-
-
- Søndagstillæg
- 0,0
- 0,00 kr
-
-
- Tillæg i alt
-
- 1.056,00 kr
-
-
-
-
-
-
-
-
-
- Fravær
- Dage
- Beløb
-
-
-
-
- Ferie med løn
- 0
- 0,00 kr
-
-
- Sygdom
- 0
- 0,00 kr
-
-
- Barns sygedag
- 0
- 0,00 kr
-
-
-
-
Ingen fravær registreret i perioden.
-
-
-
-
-
-
-
Side 2/2 · Detaljer
-
Lønspecifikation · Januar 2026
-
-
-
- Tip: I Chrome/Edge: Ctrl/Cmd + P → Destination: Gem som PDF → slå “Headers and footers” fra.
-
-
-
-
diff --git a/CalendarServer.csproj b/CalendarServer.csproj
new file mode 100644
index 0000000..2447542
--- /dev/null
+++ b/CalendarServer.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..10cbdaa
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.FileProviders;
+
+var builder = WebApplication.CreateBuilder(args);
+
+var app = builder.Build();
+
+// Configure static files to serve from current directory
+app.UseStaticFiles(new StaticFileOptions
+{
+ FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")),
+ RequestPath = ""
+});
+
+// Fallback to index.html for SPA routing
+app.MapFallbackToFile("index.html");
+
+app.Run("http://localhost:8000");
\ No newline at end of file
diff --git a/docs/design-system.md b/docs/design-system.md
deleted file mode 100644
index 6acead2..0000000
--- a/docs/design-system.md
+++ /dev/null
@@ -1,221 +0,0 @@
-# SWP Design System - UI/UX Dokumentation
-
-## Oversigt
-
-Dette dokument beskriver det komponent-baserede design system udviklet til Salon OS SaaS platformen gennem POC-udvikling. Systemet består af **150+ custom HTML elementer** med `swp-` prefix.
-
----
-
-## Design Principper
-
-- **Custom Elements**: Alle komponenter bruger semantiske `swp-*` tags
-- **CSS Variables**: Theming via `--color-*` variabler (light/dark mode)
-- **Responsive**: Mobile-first med grid breakpoints
-- **Konsistent spacing**: 4px base unit (4, 8, 12, 16, 20, 24px)
-
----
-
-## Farvepalette (CSS Variables)
-
-```css
---color-surface: #fff / #1e1e1e
---color-background: #f5f5f5 / #121212
---color-border: #e0e0e0 / #333
---color-text: #333 / #e0e0e0
---color-text-secondary: #666 / #999
---color-teal: #00897b (primary)
---color-blue: #1976d2
---color-green: #43a047 (success)
---color-amber: #f59e0b (warning)
---color-red: #e53935 (error)
---color-purple: #8b5cf6
-```
-
----
-
-## Typografi
-
-```css
---font-family: 'Poppins', sans-serif
---font-mono: 'JetBrains Mono', monospace
-```
-
-| Størrelse | Brug |
-|-----------|------|
-| 22px | Stat values |
-| 16px | Page titles |
-| 14px | Body text |
-| 13px | Table cells, inputs |
-| 12px | Labels, hints |
-| 11px | Table headers (uppercase) |
-
----
-
-## Komponent-katalog
-
-### 1. Layout & Container
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-page` | Hovedpage wrapper |
-| `swp-page-container` | Max-width container (1400px) |
-| `swp-card` | Kortkomponent med border og shadow |
-| `swp-card-header` | Kortheader med titel |
-| `swp-drawer` | Slide-in panel fra højre |
-| `swp-drawer-overlay` | Mørk overlay bag drawer |
-| `swp-two-columns` | To-kolonne layout |
-
-### 2. Navigation
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-topbar` | Sticky header bar |
-| `swp-topbar-left/right` | Flex containers i topbar |
-| `swp-page-title` | Sidetitel i topbar |
-| `swp-back-link` | Tilbage-navigation med ikon |
-| `swp-tabs` | Tab navigation |
-| `swp-tab` | Enkelt tab |
-
-### 3. Formularer
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-form-field` | Wrapper for label + input |
-| `swp-form-label` | Form label |
-| `swp-form-input` | Input wrapper |
-| `swp-form-row` | Horisontal gruppe af felter |
-| `swp-form-hint` | Hjælpetekst under felt |
-| `swp-toggle-slider` | On/off toggle |
-| `swp-edit-section` | Redigerbar sektion |
-| `swp-edit-row` | Label + værdi row |
-
-### 4. Tabeller
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-table` | Hovedtabel container |
-| `swp-table-header` | Header row (grid) |
-| `swp-table-body` | Body container |
-| `swp-table-row` | Data row (grid) |
-| `swp-th` | Header cell |
-| `swp-td` | Data cell |
-| `swp-table-footer` | Footer med pagination |
-| `swp-row-arrow` | Klik-indikator pil |
-
-### 5. Statistik
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-stats-bar` | Grid af stat cards |
-| `swp-stat-card` | Enkelt statistik kort |
-| `swp-stat-value` | Stor tal (mono font) |
-| `swp-stat-label` | Beskrivelse under tal |
-| `swp-stat-change` | Ændring indikator (+/-) |
-
-### 6. Søgning & Filtrering
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-filter-bar` | Filterpanel |
-| `swp-search-input` | Søgefelt med ikon |
-| `swp-filter-group` | Label + select/input |
-| `swp-filter-label` | Filter label |
-
-### 7. Badges & Status
-
-| Element | Beskrivelse | Klasser |
-|---------|-------------|---------|
-| `swp-status-badge` | Status indikator | `.paid`, `.pending`, `.credited` |
-| `swp-payment-badge` | Betalingsmetode | `.card`, `.cash`, `.mobilepay` |
-| `swp-tag` | Inline tag | `.vip`, `.new` |
-
-### 8. Buttons
-
-| Element | Beskrivelse | Klasser |
-|---------|-------------|---------|
-| `swp-btn` | Standard button | `.primary`, `.secondary`, `.danger` |
-
-### 9. Charts (swp-charting)
-
-| Element | Beskrivelse |
-|---------|-------------|
-| `swp-chart-card` | Chart wrapper kort |
-| `swp-chart-header` | Titel + hint |
-| `swp-chart-container` | Canvas container |
-
-### 10. Specialiserede Komponenter
-
-#### Kunde
-- `swp-customer-avatar` - Rund avatar
-- `swp-customer-cell` - Navn + telefon
-- `swp-customer-header` - Profil header
-
-#### Medarbejder
-- `swp-employee-avatar` - Medarbejder billede
-- `swp-employee-name` - Navn display
-
-#### Faktura
-- `swp-invoice-cell` - Fakturanummer
-- `swp-datetime-cell` - Dato + tid
-- `swp-amount-cell` - Beløb (højre-justeret)
-
-#### Produkt
-- `swp-variants-table` - Variant liste
-- `swp-stock-display` - Lagerstatus
-- `swp-margin-display` - Avance visning
-
-#### Booking/AI
-- `swp-gap-card` - Ledigt hul kort
-- `swp-suggestion-item` - AI forslag
-- `swp-optimization-score` - Score cirkel
-
----
-
-## Pagination
-
-```html
-
-
- 1
- 2
- ...
-
-
-```
-
----
-
-## Ikoner
-
-Bruger **Phosphor Icons** via CDN:
-```html
-
-
-
-
-```
-
----
-
-## POC Filer (Reference)
-
-| Fil | Domæne |
-|-----|--------|
-| `poc-salg.html` | Salgsoversigt, fakturaer |
-| `poc-customer-list.html` | Kundeliste |
-| `poc-customer-detail.html` | Kundeprofil |
-| `poc-employee.html` | Medarbejderprofil |
-| `poc-produkt.html` | Produktdetaljer |
-| `poc-gavekort.html` | Fordelskort |
-| `poc-kasseafstemninger.html` | Kasseafstemning |
-| `poc-rapport.html` | Rapporter |
-| `poc-indstillinger.html` | Indstillinger |
-
----
-
-## Næste Skridt: .NET Core Implementation
-
-Når design systemet er dokumenteret, er næste fase at implementere:
-1. Backend API endpoints
-2. Database modeller
-3. Razor/Blazor komponenter baseret på swp-* elementer
diff --git a/package-lock.json b/package-lock.json
index 6bc1f15..d0ad451 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@novadi/core": "^0.6.0",
"@rollup/rollup-win32-x64-msvc": "^4.52.2",
- "@sevenweirdpeople/swp-charting": "^0.2.2",
+ "@sevenweirdpeople/swp-charting": "^0.2.1",
"dayjs": "^1.11.19",
"fuse.js": "^7.1.0",
"json-diff-ts": "^4.8.2",
@@ -1177,9 +1177,9 @@
]
},
"node_modules/@sevenweirdpeople/swp-charting": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.2.tgz",
- "integrity": "sha512-q9p7TOSMAq6I0t6jGEWpmjR7l2H8q8G0TnXbIpDutCz5a2JEqMDFe0NGBGcCwze2rvvRnRvCz8P2zGMQlHmphw==",
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.1.tgz",
+ "integrity": "sha512-QtY77Dyv4Vs/rWfBVSDTmuxgD4L8tGu4pmTF0l3i8HDwK6qtT2wEtH35UHD1RDFE1VtOGcnU0/dTdqjNWCqzxA==",
"license": "MIT"
},
"node_modules/@types/chai": {
diff --git a/package.json b/package.json
index 8e2f69f..36c19f2 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"dependencies": {
"@novadi/core": "^0.6.0",
"@rollup/rollup-win32-x64-msvc": "^4.52.2",
- "@sevenweirdpeople/swp-charting": "^0.2.2",
+ "@sevenweirdpeople/swp-charting": "^0.2.1",
"dayjs": "^1.11.19",
"fuse.js": "^7.1.0",
"json-diff-ts": "^4.8.2",
diff --git a/packages/calendar/README.md b/packages/calendar/README.md
deleted file mode 100644
index 5fecd34..0000000
--- a/packages/calendar/README.md
+++ /dev/null
@@ -1,620 +0,0 @@
-# Calendar
-
-Professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities.
-
-## Features
-
-- **Multiple View Modes**: Date-based (day/week/month) and resource-based (people, rooms) views
-- **Drag & Drop**: Smooth event dragging with snap-to-grid, cross-column movement, and timed/all-day conversion
-- **Event Resizing**: Intuitive resize handles for adjusting event duration
-- **Offline-First**: IndexedDB storage with automatic background sync
-- **Event-Driven Architecture**: Decoupled components via centralized EventBus
-- **Dependency Injection**: Built on NovaDI for clean, testable architecture
-- **Extensions**: Modular extensions for teams, departments, bookings, customers, schedules, and audit logging
-
-## Installation
-
-```bash
-npm install calendar
-```
-
-## Quick Start (AI-Friendly Setup Guide)
-
-### Step 1: Create HTML Structure
-
-```html
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-### Step 2: Initialize Calendar
-
-```typescript
-import { Container } from '@novadi/core';
-import {
- registerCoreServices,
- CalendarApp,
- IndexedDBContext,
- SettingsService,
- ViewConfigService,
- EventService,
- EventBus,
- CalendarEvents
-} from 'calendar';
-
-async function init() {
- // 1. Create DI container and register services
- const container = new Container();
- const builder = container.builder();
- registerCoreServices(builder, {
- dbConfig: { dbName: 'MyCalendarDB', dbVersion: 1 }
- });
- const app = builder.build();
-
- // 2. Initialize IndexedDB
- const dbContext = app.resolveType();
- await dbContext.initialize();
-
- // 3. Seed required settings (first time only)
- const settingsService = app.resolveType();
- const viewConfigService = app.resolveType();
-
- await settingsService.save({
- id: 'grid',
- dayStartHour: 8,
- dayEndHour: 17,
- workStartHour: 9,
- workEndHour: 16,
- hourHeight: 64,
- snapInterval: 15,
- syncStatus: 'synced'
- });
-
- await settingsService.save({
- id: 'workweek',
- presets: {
- standard: { id: 'standard', label: 'Standard', workDays: [1, 2, 3, 4, 5], periodDays: 7 }
- },
- defaultPreset: 'standard',
- firstDayOfWeek: 1,
- syncStatus: 'synced'
- });
-
- await viewConfigService.save({
- id: 'simple',
- groupings: [{ type: 'date', values: [], idProperty: 'date', derivedFrom: 'start' }],
- syncStatus: 'synced'
- });
-
- // 4. Initialize CalendarApp
- const calendarApp = app.resolveType();
- const containerEl = document.querySelector('swp-calendar-container') as HTMLElement;
- await calendarApp.init(containerEl);
-
- // 5. Render a view
- const eventBus = app.resolveType();
- eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: 'simple' });
-}
-
-init().catch(console.error);
-```
-
-### Step 3: Add Events
-
-```typescript
-const eventService = app.resolveType();
-
-await eventService.save({
- id: crypto.randomUUID(),
- title: 'Meeting',
- start: new Date('2024-01-15T09:00:00'),
- end: new Date('2024-01-15T10:00:00'),
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced'
-});
-```
-
----
-
-## Architecture
-
-### Core Components
-
-| Component | Description |
-|-----------|-------------|
-| `CalendarApp` | Main application entry point |
-| `CalendarOrchestrator` | Coordinates rendering pipeline |
-| `EventBus` | Central event dispatcher for all inter-component communication |
-| `DateService` | Date calculations and formatting |
-| `IndexedDBContext` | Offline storage infrastructure |
-
-### Managers
-
-| Manager | Description |
-|---------|-------------|
-| `DragDropManager` | Event drag-drop with smooth animations and snap-to-grid |
-| `EdgeScrollManager` | Automatic scrolling at viewport edges during drag |
-| `ResizeManager` | Event resizing with visual feedback |
-| `ScrollManager` | Scroll behavior and position management |
-| `HeaderDrawerManager` | All-day events drawer toggle |
-| `EventPersistenceManager` | Saves drag/resize changes to storage |
-
-### Renderers
-
-| Renderer | Description |
-|----------|-------------|
-| `DateRenderer` | Renders date-based column groupings |
-| `ResourceRenderer` | Renders resource-based column groupings |
-| `EventRenderer` | Renders timed events in columns |
-| `ScheduleRenderer` | Renders working hours backgrounds |
-| `HeaderDrawerRenderer` | Renders all-day events in header |
-| `TimeAxisRenderer` | Renders time labels on the left axis |
-
-### Storage
-
-| Service | Description |
-|---------|-------------|
-| `EventService` / `EventStore` | Calendar event CRUD |
-| `ResourceService` / `ResourceStore` | Resource management |
-| `SettingsService` / `SettingsStore` | Tenant settings |
-| `ViewConfigService` / `ViewConfigStore` | View configurations |
-
----
-
-## Events Reference
-
-### Lifecycle Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `core:initialized` | `CoreEvents.INITIALIZED` | - | Calendar core initialized |
-| `core:ready` | `CoreEvents.READY` | - | Calendar ready for interaction |
-| `core:destroyed` | `CoreEvents.DESTROYED` | - | Calendar destroyed |
-
-### View Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `view:changed` | `CoreEvents.VIEW_CHANGED` | `{ viewId: string }` | View type changed |
-| `view:rendered` | `CoreEvents.VIEW_RENDERED` | - | View finished rendering |
-
-### Navigation Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `nav:date-changed` | `CoreEvents.DATE_CHANGED` | `{ date: Date }` | Current date changed |
-| `nav:navigation-completed` | `CoreEvents.NAVIGATION_COMPLETED` | - | Navigation animation completed |
-
-### Data Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `data:loading` | `CoreEvents.DATA_LOADING` | - | Data loading started |
-| `data:loaded` | `CoreEvents.DATA_LOADED` | - | Data loading completed |
-| `data:error` | `CoreEvents.DATA_ERROR` | `{ error: Error }` | Data loading error |
-
-### Grid Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `grid:rendered` | `CoreEvents.GRID_RENDERED` | - | Grid finished rendering |
-| `grid:clicked` | `CoreEvents.GRID_CLICKED` | `{ time: Date, columnKey: string }` | Grid area clicked |
-
-### Event Management
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `event:created` | `CoreEvents.EVENT_CREATED` | `ICalendarEvent` | Event created |
-| `event:updated` | `CoreEvents.EVENT_UPDATED` | `IEventUpdatedPayload` | Event updated |
-| `event:deleted` | `CoreEvents.EVENT_DELETED` | `{ eventId: string }` | Event deleted |
-| `event:selected` | `CoreEvents.EVENT_SELECTED` | `{ eventId: string }` | Event selected |
-
-### Drag-Drop Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `event:drag-start` | `CoreEvents.EVENT_DRAG_START` | `IDragStartPayload` | Drag started |
-| `event:drag-move` | `CoreEvents.EVENT_DRAG_MOVE` | `IDragMovePayload` | Dragging (throttled) |
-| `event:drag-end` | `CoreEvents.EVENT_DRAG_END` | `IDragEndPayload` | Drag completed |
-| `event:drag-cancel` | `CoreEvents.EVENT_DRAG_CANCEL` | `IDragCancelPayload` | Drag cancelled |
-| `event:drag-column-change` | `CoreEvents.EVENT_DRAG_COLUMN_CHANGE` | `IDragColumnChangePayload` | Moved to different column |
-
-### Header Drag Events (Timed to All-Day Conversion)
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `event:drag-enter-header` | `CoreEvents.EVENT_DRAG_ENTER_HEADER` | `IDragEnterHeaderPayload` | Entered header area |
-| `event:drag-move-header` | `CoreEvents.EVENT_DRAG_MOVE_HEADER` | `IDragMoveHeaderPayload` | Moving in header area |
-| `event:drag-leave-header` | `CoreEvents.EVENT_DRAG_LEAVE_HEADER` | `IDragLeaveHeaderPayload` | Left header area |
-
-### Resize Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `event:resize-start` | `CoreEvents.EVENT_RESIZE_START` | `IResizeStartPayload` | Resize started |
-| `event:resize-end` | `CoreEvents.EVENT_RESIZE_END` | `IResizeEndPayload` | Resize completed |
-
-### Edge Scroll Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `edge-scroll:tick` | `CoreEvents.EDGE_SCROLL_TICK` | `{ deltaY: number }` | Scroll tick during edge scroll |
-| `edge-scroll:started` | `CoreEvents.EDGE_SCROLL_STARTED` | - | Edge scrolling started |
-| `edge-scroll:stopped` | `CoreEvents.EDGE_SCROLL_STOPPED` | - | Edge scrolling stopped |
-
-### Sync Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `sync:started` | `CoreEvents.SYNC_STARTED` | - | Background sync started |
-| `sync:completed` | `CoreEvents.SYNC_COMPLETED` | - | Background sync completed |
-| `sync:failed` | `CoreEvents.SYNC_FAILED` | `{ error: Error }` | Background sync failed |
-
-### Entity Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `entity:saved` | `CoreEvents.ENTITY_SAVED` | `IEntitySavedPayload` | Entity saved to storage |
-| `entity:deleted` | `CoreEvents.ENTITY_DELETED` | `IEntityDeletedPayload` | Entity deleted from storage |
-
-### Audit Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `audit:logged` | `CoreEvents.AUDIT_LOGGED` | `IAuditLoggedPayload` | Audit entry logged |
-
-### Rendering Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `events:rendered` | `CoreEvents.EVENTS_RENDERED` | - | Events finished rendering |
-
-### System Events
-
-| Event | Constant | Payload | Description |
-|-------|----------|---------|-------------|
-| `system:error` | `CoreEvents.ERROR` | `{ error: Error, context?: string }` | System error occurred |
-
----
-
-## Command Events (Host to Calendar)
-
-Use these to control the calendar from your application:
-
-```typescript
-import { EventBus, CalendarEvents } from 'calendar';
-
-const eventBus = app.resolveType();
-
-// Navigate
-eventBus.emit(CalendarEvents.CMD_NAVIGATE_PREV);
-eventBus.emit(CalendarEvents.CMD_NAVIGATE_NEXT);
-
-// Render a view
-eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: 'simple' });
-
-// Toggle header drawer
-eventBus.emit(CalendarEvents.CMD_DRAWER_TOGGLE);
-
-// Change workweek preset
-eventBus.emit(CalendarEvents.CMD_WORKWEEK_CHANGE, { presetId: 'standard' });
-
-// Update view grouping
-eventBus.emit(CalendarEvents.CMD_VIEW_UPDATE, { type: 'resource', values: ['r1', 'r2'] });
-```
-
-| Command | Constant | Payload | Description |
-|---------|----------|---------|-------------|
-| Navigate Previous | `CalendarEvents.CMD_NAVIGATE_PREV` | - | Go to previous period |
-| Navigate Next | `CalendarEvents.CMD_NAVIGATE_NEXT` | - | Go to next period |
-| Render View | `CalendarEvents.CMD_RENDER` | `{ viewId: string }` | Render specified view |
-| Toggle Drawer | `CalendarEvents.CMD_DRAWER_TOGGLE` | - | Toggle all-day drawer |
-| Change Workweek | `CalendarEvents.CMD_WORKWEEK_CHANGE` | `{ presetId: string }` | Change workweek preset |
-| Update View | `CalendarEvents.CMD_VIEW_UPDATE` | `{ type: string, values: string[] }` | Update grouping values |
-
----
-
-## Types
-
-### Core Types
-
-```typescript
-// Event types
-type CalendarEventType = 'customer' | 'vacation' | 'break' | 'meeting' | 'blocked';
-
-interface ICalendarEvent {
- id: string;
- title: string;
- description?: string;
- start: Date;
- end: Date;
- type: CalendarEventType;
- allDay: boolean;
- bookingId?: string;
- resourceId?: string;
- customerId?: string;
- recurringId?: string;
- syncStatus: SyncStatus;
- metadata?: Record;
-}
-
-// Resource types
-type ResourceType = 'person' | 'room' | 'equipment' | 'vehicle' | 'custom';
-
-interface IResource {
- id: string;
- name: string;
- displayName: string;
- type: ResourceType;
- avatarUrl?: string;
- color?: string;
- isActive?: boolean;
- defaultSchedule?: IWeekSchedule;
- syncStatus: SyncStatus;
-}
-
-// Sync status
-type SyncStatus = 'synced' | 'pending' | 'error';
-```
-
-### Settings Types
-
-```typescript
-interface IGridSettings {
- id: 'grid';
- dayStartHour: number;
- dayEndHour: number;
- workStartHour: number;
- workEndHour: number;
- hourHeight: number;
- snapInterval: number;
-}
-
-interface IWorkweekPreset {
- id: string;
- workDays: number[]; // ISO weekdays: 1=Monday, 7=Sunday
- label: string;
- periodDays: number; // Navigation step (1=day, 7=week)
-}
-
-interface IWeekSchedule {
- [day: number]: ITimeSlot | null; // null = off that day
-}
-
-interface ITimeSlot {
- start: string; // "HH:mm"
- end: string; // "HH:mm"
-}
-```
-
-### Drag-Drop Payloads
-
-```typescript
-interface IDragStartPayload {
- eventId: string;
- element: HTMLElement;
- ghostElement: HTMLElement;
- startY: number;
- mouseOffset: { x: number; y: number };
- columnElement: HTMLElement;
-}
-
-interface IDragEndPayload {
- swpEvent: SwpEvent;
- sourceColumnKey: string;
- target: 'grid' | 'header';
-}
-```
-
----
-
-## Extensions
-
-Import extensions separately to keep bundle size minimal:
-
-```typescript
-// Teams extension
-import { registerTeams, TeamService, TeamStore, TeamRenderer } from 'calendar/teams';
-
-// Departments extension
-import { registerDepartments, DepartmentService, DepartmentStore } from 'calendar/departments';
-
-// Bookings extension
-import { registerBookings, BookingService, BookingStore } from 'calendar/bookings';
-
-// Customers extension
-import { registerCustomers, CustomerService, CustomerStore } from 'calendar/customers';
-
-// Schedules extension (working hours)
-import { registerSchedules, ResourceScheduleService, ScheduleOverrideService } from 'calendar/schedules';
-
-// Audit extension
-import { registerAudit, AuditService, AuditStore } from 'calendar/audit';
-
-// Register with container builder
-const builder = container.builder();
-registerCoreServices(builder);
-registerTeams(builder);
-registerSchedules(builder);
-// ... etc
-```
-
----
-
-## Configuration
-
-### Calendar Options
-
-```typescript
-interface ICalendarOptions {
- timeConfig?: ITimeFormatConfig;
- gridConfig?: IGridConfig;
- dbConfig?: IDBConfig;
-}
-
-// Time format configuration
-interface ITimeFormatConfig {
- timezone: string; // e.g., 'Europe/Copenhagen'
- use24HourFormat: boolean;
- locale: string; // e.g., 'da-DK'
- dateFormat: string;
- showSeconds: boolean;
-}
-
-// Grid configuration
-interface IGridConfig {
- hourHeight: number; // Pixels per hour (default: 64)
- dayStartHour: number; // Grid start hour (default: 6)
- dayEndHour: number; // Grid end hour (default: 18)
- snapInterval: number; // Minutes to snap to (default: 15)
- gridStartThresholdMinutes: number;
-}
-
-// Database configuration
-interface IDBConfig {
- dbName: string; // IndexedDB database name
- dbVersion: number; // Schema version
-}
-```
-
-### Default Configuration
-
-```typescript
-import {
- defaultTimeFormatConfig,
- defaultGridConfig,
- defaultDBConfig
-} from 'calendar';
-
-// Defaults:
-// timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
-// use24HourFormat: true
-// locale: 'da-DK'
-// hourHeight: 64
-// dayStartHour: 6
-// dayEndHour: 18
-// snapInterval: 15
-```
-
----
-
-## Utilities
-
-### Position Utilities
-
-```typescript
-import {
- calculateEventPosition,
- minutesToPixels,
- pixelsToMinutes,
- snapToGrid
-} from 'calendar';
-
-// Convert time to pixels
-const pixels = minutesToPixels(120, 64); // 120 mins at 64px/hour = 128px
-
-// Snap to 15-minute grid
-const snapped = snapToGrid(new Date(), 15);
-```
-
-### Event Layout Engine
-
-```typescript
-import { eventsOverlap, calculateColumnLayout } from 'calendar';
-
-// Check if two events overlap
-const overlap = eventsOverlap(event1, event2);
-
-// Calculate layout for overlapping events
-const layout = calculateColumnLayout(events);
-```
-
----
-
-## Listening to Events
-
-```typescript
-import { EventBus, CoreEvents } from 'calendar';
-
-const eventBus = app.resolveType();
-
-// Subscribe to event updates
-eventBus.on(CoreEvents.EVENT_UPDATED, (e: Event) => {
- const { eventId, sourceColumnKey, targetColumnKey } = (e as CustomEvent).detail;
- console.log(`Event ${eventId} moved from ${sourceColumnKey} to ${targetColumnKey}`);
-});
-
-// Subscribe to drag events
-eventBus.on(CoreEvents.EVENT_DRAG_END, (e: Event) => {
- const { swpEvent, target } = (e as CustomEvent).detail;
- console.log(`Dropped on ${target}:`, swpEvent);
-});
-
-// One-time listener
-eventBus.once(CoreEvents.READY, () => {
- console.log('Calendar is ready!');
-});
-```
-
----
-
-## CSS Customization
-
-The calendar uses CSS custom properties for theming. Override these in your CSS:
-
-```css
-:root {
- --calendar-hour-height: 64px;
- --calendar-header-height: 48px;
- --calendar-column-min-width: 120px;
- --calendar-event-border-radius: 4px;
-}
-```
-
----
-
-## Dependencies
-
-- `@novadi/core` - Dependency injection framework (peer dependency)
-- `dayjs` - Date manipulation and formatting
-
----
-
-## License
-
-Proprietary - SWP
diff --git a/packages/calendar/build.js b/packages/calendar/build.js
deleted file mode 100644
index 0570894..0000000
--- a/packages/calendar/build.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import * as esbuild from 'esbuild';
-import { NovadiUnplugin } from '@novadi/core/unplugin';
-import * as fs from 'fs';
-import * as path from 'path';
-
-const entryPoints = [
- 'src/index.ts',
- 'src/extensions/teams/index.ts',
- 'src/extensions/departments/index.ts',
- 'src/extensions/bookings/index.ts',
- 'src/extensions/customers/index.ts',
- 'src/extensions/schedules/index.ts',
- 'src/extensions/audit/index.ts'
-];
-
-async function build() {
- await esbuild.build({
- entryPoints,
- bundle: true,
- outdir: 'dist',
- format: 'esm',
- platform: 'browser',
- external: ['@novadi/core', 'dayjs'],
- splitting: true,
- sourcemap: true,
- target: 'es2020',
- plugins: [NovadiUnplugin.esbuild({ debug: false, enableAutowiring: true })]
- });
-
- console.log('Build complete: dist/');
-
- // Bundle CSS
- const cssDir = 'dist/css';
- if (!fs.existsSync(cssDir)) {
- fs.mkdirSync(cssDir, { recursive: true });
- }
- const cssFiles = [
- '../../wwwroot/css/calendar-base.css',
- '../../wwwroot/css/calendar-layout.css',
- '../../wwwroot/css/calendar-events.css'
- ];
- const bundledCss = cssFiles.map(f => fs.readFileSync(f, 'utf8')).join('\n');
- fs.writeFileSync(path.join(cssDir, 'calendar.css'), bundledCss);
- console.log('CSS bundled: dist/css/calendar.css');
-}
-
-build();
diff --git a/packages/calendar/package-lock.json b/packages/calendar/package-lock.json
deleted file mode 100644
index c00dabb..0000000
--- a/packages/calendar/package-lock.json
+++ /dev/null
@@ -1,167 +0,0 @@
-{
- "name": "@plantempus/calendar",
- "version": "0.1.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "@plantempus/calendar",
- "version": "0.1.0",
- "dependencies": {
- "dayjs": "^1.11.0"
- },
- "peerDependencies": {
- "@novadi/core": "^0.6.0"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@novadi/core": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@novadi/core/-/core-0.6.0.tgz",
- "integrity": "sha512-CU1134Nd7ULMg9OQbID5oP+FLtrMkNiLJ17+dmy4jjmPDcPK/dVzKTFxvJmbBvEfZEc9WtmkmJjqw11ABU7Jxw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "unplugin": "^2.3.10"
- },
- "optionalDependencies": {
- "@rollup/rollup-win32-x64-msvc": "^4.52.5"
- },
- "peerDependencies": {
- "typescript": ">=5.0.0"
- }
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
- "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true
- },
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "license": "MIT",
- "peer": true,
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/dayjs": {
- "version": "1.11.19",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
- "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
- "license": "MIT"
- },
- "node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "license": "Apache-2.0",
- "peer": true,
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/unplugin": {
- "version": "2.3.11",
- "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
- "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "acorn": "^8.15.0",
- "picomatch": "^4.0.3",
- "webpack-virtual-modules": "^0.6.2"
- },
- "engines": {
- "node": ">=18.12.0"
- }
- },
- "node_modules/webpack-virtual-modules": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
- "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
- "license": "MIT",
- "peer": true
- }
- }
-}
diff --git a/packages/calendar/package.json b/packages/calendar/package.json
deleted file mode 100644
index 9a7374b..0000000
--- a/packages/calendar/package.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
- "name": "calendar",
- "version": "0.1.7",
- "description": "Calendar library",
- "author": "SWP",
- "type": "module",
- "main": "./dist/index.js",
- "types": "./dist/index.d.ts",
- "exports": {
- ".": {
- "import": "./dist/index.js",
- "types": "./dist/index.d.ts"
- },
- "./teams": {
- "import": "./dist/extensions/teams/index.js",
- "types": "./dist/extensions/teams/index.d.ts"
- },
- "./departments": {
- "import": "./dist/extensions/departments/index.js",
- "types": "./dist/extensions/departments/index.d.ts"
- },
- "./bookings": {
- "import": "./dist/extensions/bookings/index.js",
- "types": "./dist/extensions/bookings/index.d.ts"
- },
- "./customers": {
- "import": "./dist/extensions/customers/index.js",
- "types": "./dist/extensions/customers/index.d.ts"
- },
- "./schedules": {
- "import": "./dist/extensions/schedules/index.js",
- "types": "./dist/extensions/schedules/index.d.ts"
- },
- "./audit": {
- "import": "./dist/extensions/audit/index.js",
- "types": "./dist/extensions/audit/index.d.ts"
- },
- "./styles": "./dist/css/calendar.css"
- },
- "files": [
- "dist"
- ],
- "scripts": {
- "build": "node build.js && npm run build:types",
- "build:types": "tsc --emitDeclarationOnly --outDir dist"
- },
- "peerDependencies": {
- "@novadi/core": "^0.6.0"
- },
- "dependencies": {
- "dayjs": "^1.11.0"
- },
- "devDependencies": {
- "esbuild": "^0.24.0",
- "typescript": "^5.0.0"
- }
-}
diff --git a/packages/calendar/src/CompositionRoot.ts b/packages/calendar/src/CompositionRoot.ts
deleted file mode 100644
index e2b429a..0000000
--- a/packages/calendar/src/CompositionRoot.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { Container, Builder } from '@novadi/core';
-
-// Core
-import { EventBus } from './core/EventBus';
-import { DateService } from './core/DateService';
-import { CalendarOrchestrator } from './core/CalendarOrchestrator';
-import { CalendarApp } from './core/CalendarApp';
-import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
-import { ScrollManager } from './core/ScrollManager';
-import { HeaderDrawerManager } from './core/HeaderDrawerManager';
-import { ITimeFormatConfig } from './core/ITimeFormatConfig';
-import { IGridConfig } from './core/IGridConfig';
-
-// Types
-import { IEventBus, ICalendarEvent, ISync, IResource } from './types/CalendarTypes';
-import { TenantSetting } from './types/SettingsTypes';
-import { ViewConfig } from './core/ViewConfig';
-
-// Renderers
-import { IRenderer } from './core/IGroupingRenderer';
-import { DateRenderer } from './features/date/DateRenderer';
-import { ResourceRenderer } from './features/resource/ResourceRenderer';
-import { EventRenderer } from './features/event/EventRenderer';
-import { ScheduleRenderer } from './features/schedule/ScheduleRenderer';
-import { HeaderDrawerRenderer } from './features/headerdrawer/HeaderDrawerRenderer';
-
-// Storage
-import { IndexedDBContext, IDBConfig, defaultDBConfig } from './storage/IndexedDBContext';
-import { IStore } from './storage/IStore';
-import { IEntityService } from './storage/IEntityService';
-import { EventStore } from './storage/events/EventStore';
-import { EventService } from './storage/events/EventService';
-import { ResourceStore } from './storage/resources/ResourceStore';
-import { ResourceService } from './storage/resources/ResourceService';
-import { SettingsStore } from './storage/settings/SettingsStore';
-import { SettingsService } from './storage/settings/SettingsService';
-import { ViewConfigStore } from './storage/viewconfigs/ViewConfigStore';
-import { ViewConfigService } from './storage/viewconfigs/ViewConfigService';
-
-// Managers
-import { DragDropManager } from './managers/DragDropManager';
-import { EdgeScrollManager } from './managers/EdgeScrollManager';
-import { ResizeManager } from './managers/ResizeManager';
-import { EventPersistenceManager } from './managers/EventPersistenceManager';
-
-/**
- * Default configuration values
- */
-export const defaultTimeFormatConfig: ITimeFormatConfig = {
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- use24HourFormat: true,
- locale: 'da-DK',
- dateFormat: 'locale',
- showSeconds: false
-};
-
-export const defaultGridConfig: IGridConfig = {
- hourHeight: 64,
- dayStartHour: 6,
- dayEndHour: 18,
- snapInterval: 15,
- gridStartThresholdMinutes: 30
-};
-
-/**
- * Calendar configuration options
- */
-export interface ICalendarOptions {
- timeConfig?: ITimeFormatConfig;
- gridConfig?: IGridConfig;
- dbConfig?: IDBConfig;
-}
-
-/**
- * Creates a configured DI container with all core calendar services registered.
- * Call this to get a ready-to-use container for the calendar.
- *
- * @param options - Optional calendar configuration options
- * @returns Configured Container instance
- */
-export function createCalendarContainer(options?: ICalendarOptions): Container {
- const container = new Container();
- const builder = container.builder();
-
- registerCoreServices(builder, options);
-
- return builder.build();
-}
-
-/**
- * Registers all core calendar services with the DI container builder.
- * Use this when you need to customize the container or add extensions.
- *
- * @param builder - ContainerBuilder to register services with
- * @param options - Optional calendar configuration options
- */
-export function registerCoreServices(
- builder: Builder,
- options?: ICalendarOptions
-): void {
- const timeConfig = options?.timeConfig ?? defaultTimeFormatConfig;
- const gridConfig = options?.gridConfig ?? defaultGridConfig;
- const dbConfig = options?.dbConfig ?? defaultDBConfig;
- // Configuration instances
- builder.registerInstance(timeConfig).as();
- builder.registerInstance(gridConfig).as();
- builder.registerInstance(dbConfig).as();
-
- // Core - EventBus (singleton pattern via dual registration)
- builder.registerType(EventBus).as();
- builder.registerType(EventBus).as();
-
- // Core Services
- builder.registerType(DateService).as();
-
- // Storage infrastructure
- builder.registerType(IndexedDBContext).as();
-
- // Core Stores (for IndexedDB schema creation via IStore[] array injection)
- builder.registerType(EventStore).as();
- builder.registerType(ResourceStore).as();
- builder.registerType(SettingsStore).as();
- builder.registerType(ViewConfigStore).as();
-
- // Core Entity Services (polymorphic via IEntityService)
- builder.registerType(EventService).as>();
- builder.registerType(EventService).as>();
- builder.registerType(EventService).as();
-
- builder.registerType(ResourceService).as>();
- builder.registerType(ResourceService).as>();
- builder.registerType(ResourceService).as();
-
- builder.registerType(SettingsService).as>();
- builder.registerType(SettingsService).as>();
- builder.registerType(SettingsService).as();
-
- builder.registerType(ViewConfigService).as>();
- builder.registerType(ViewConfigService).as>();
- builder.registerType(ViewConfigService).as();
-
- // Core Renderers
- builder.registerType(EventRenderer).as();
- builder.registerType(ScheduleRenderer).as();
- builder.registerType(HeaderDrawerRenderer).as();
- builder.registerType(TimeAxisRenderer).as();
-
- // Grouping Renderers (registered as IRenderer[] for CalendarOrchestrator)
- builder.registerType(DateRenderer).as();
- builder.registerType(ResourceRenderer).as();
-
- // Core Managers
- builder.registerType(ScrollManager).as();
- builder.registerType(HeaderDrawerManager).as();
- builder.registerType(DragDropManager).as();
- builder.registerType(EdgeScrollManager).as();
- builder.registerType(ResizeManager).as();
- builder.registerType(EventPersistenceManager).as();
-
- // Orchestrator and App
- builder.registerType(CalendarOrchestrator).as();
- builder.registerType(CalendarApp).as();
-}
diff --git a/packages/calendar/src/constants/CoreEvents.ts b/packages/calendar/src/constants/CoreEvents.ts
deleted file mode 100644
index 7363138..0000000
--- a/packages/calendar/src/constants/CoreEvents.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * CoreEvents - Consolidated essential events for the calendar
- */
-export const CoreEvents = {
- // Lifecycle events
- INITIALIZED: 'core:initialized',
- READY: 'core:ready',
- DESTROYED: 'core:destroyed',
-
- // View events
- VIEW_CHANGED: 'view:changed',
- VIEW_RENDERED: 'view:rendered',
-
- // Navigation events
- DATE_CHANGED: 'nav:date-changed',
- NAVIGATION_COMPLETED: 'nav:navigation-completed',
-
- // Data events
- DATA_LOADING: 'data:loading',
- DATA_LOADED: 'data:loaded',
- DATA_ERROR: 'data:error',
-
- // Grid events
- GRID_RENDERED: 'grid:rendered',
- GRID_CLICKED: 'grid:clicked',
-
- // Event management
- EVENT_CREATED: 'event:created',
- EVENT_UPDATED: 'event:updated',
- EVENT_DELETED: 'event:deleted',
- EVENT_SELECTED: 'event:selected',
-
- // Event drag-drop
- EVENT_DRAG_START: 'event:drag-start',
- EVENT_DRAG_MOVE: 'event:drag-move',
- EVENT_DRAG_END: 'event:drag-end',
- EVENT_DRAG_CANCEL: 'event:drag-cancel',
- EVENT_DRAG_COLUMN_CHANGE: 'event:drag-column-change',
-
- // Header drag (timed → header conversion)
- EVENT_DRAG_ENTER_HEADER: 'event:drag-enter-header',
- EVENT_DRAG_MOVE_HEADER: 'event:drag-move-header',
- EVENT_DRAG_LEAVE_HEADER: 'event:drag-leave-header',
-
- // Event resize
- EVENT_RESIZE_START: 'event:resize-start',
- EVENT_RESIZE_END: 'event:resize-end',
-
- // Edge scroll
- EDGE_SCROLL_TICK: 'edge-scroll:tick',
- EDGE_SCROLL_STARTED: 'edge-scroll:started',
- EDGE_SCROLL_STOPPED: 'edge-scroll:stopped',
-
- // System events
- ERROR: 'system:error',
-
- // Sync events
- SYNC_STARTED: 'sync:started',
- SYNC_COMPLETED: 'sync:completed',
- SYNC_FAILED: 'sync:failed',
-
- // Entity events - for audit and sync
- ENTITY_SAVED: 'entity:saved',
- ENTITY_DELETED: 'entity:deleted',
-
- // Audit events
- AUDIT_LOGGED: 'audit:logged',
-
- // Rendering events
- EVENTS_RENDERED: 'events:rendered'
-} as const;
diff --git a/packages/calendar/src/core/BaseGroupingRenderer.ts b/packages/calendar/src/core/BaseGroupingRenderer.ts
deleted file mode 100644
index 60c9abf..0000000
--- a/packages/calendar/src/core/BaseGroupingRenderer.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { IRenderer, IRenderContext } from './IGroupingRenderer';
-
-/**
- * Entity must have id
- */
-export interface IGroupingEntity {
- id: string;
-}
-
-/**
- * Configuration for a grouping renderer
- */
-export interface IGroupingRendererConfig {
- elementTag: string; // e.g., 'swp-team-header'
- idAttribute: string; // e.g., 'teamId' -> data-team-id
- colspanVar: string; // e.g., '--team-cols'
-}
-
-/**
- * Abstract base class for grouping renderers
- *
- * Handles:
- * - Fetching entities by IDs
- * - Calculating colspan from parentChildMap
- * - Creating header elements
- * - Appending to container
- *
- * Subclasses override:
- * - renderHeader() for custom content
- * - getDisplayName() for entity display text
- */
-export abstract class BaseGroupingRenderer implements IRenderer {
- abstract readonly type: string;
- protected abstract readonly config: IGroupingRendererConfig;
-
- /**
- * Fetch entities from service
- */
- protected abstract getEntities(ids: string[]): Promise;
-
- /**
- * Get display name for entity
- */
- protected abstract getDisplayName(entity: T): string;
-
- /**
- * Main render method - handles common logic
- */
- async render(context: IRenderContext): Promise {
- const allowedIds = context.filter[this.type] || [];
- if (allowedIds.length === 0) return;
-
- const entities = await this.getEntities(allowedIds);
- const dateCount = context.filter['date']?.length || 1;
- const childIds = context.childType ? context.filter[context.childType] || [] : [];
-
- for (const entity of entities) {
- const entityChildIds = context.parentChildMap?.[entity.id] || [];
- const childCount = entityChildIds.filter(id => childIds.includes(id)).length;
- const colspan = childCount * dateCount;
-
- const header = document.createElement(this.config.elementTag);
- header.dataset[this.config.idAttribute] = entity.id;
- header.style.setProperty(this.config.colspanVar, String(colspan));
-
- // Allow subclass to customize header content
- this.renderHeader(entity, header, context);
-
- context.headerContainer.appendChild(header);
- }
- }
-
- /**
- * Override this method for custom header rendering
- * Default: just sets textContent to display name
- */
- protected renderHeader(entity: T, header: HTMLElement, _context: IRenderContext): void {
- header.textContent = this.getDisplayName(entity);
- }
-
- /**
- * Helper to render a single entity header.
- * Can be used by subclasses that override render() but want consistent header creation.
- */
- protected createHeader(entity: T, context: IRenderContext): HTMLElement {
- const header = document.createElement(this.config.elementTag);
- header.dataset[this.config.idAttribute] = entity.id;
- this.renderHeader(entity, header, context);
- return header;
- }
-}
diff --git a/packages/calendar/src/core/CalendarApp.ts b/packages/calendar/src/core/CalendarApp.ts
deleted file mode 100644
index 246b257..0000000
--- a/packages/calendar/src/core/CalendarApp.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { CalendarOrchestrator } from './CalendarOrchestrator';
-import { TimeAxisRenderer } from '../features/timeaxis/TimeAxisRenderer';
-import { NavigationAnimator } from './NavigationAnimator';
-import { DateService } from './DateService';
-import { ScrollManager } from './ScrollManager';
-import { HeaderDrawerManager } from './HeaderDrawerManager';
-import { ViewConfig } from './ViewConfig';
-import { DragDropManager } from '../managers/DragDropManager';
-import { EdgeScrollManager } from '../managers/EdgeScrollManager';
-import { ResizeManager } from '../managers/ResizeManager';
-import { EventPersistenceManager } from '../managers/EventPersistenceManager';
-import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer';
-import { SettingsService } from '../storage/settings/SettingsService';
-import { ViewConfigService } from '../storage/viewconfigs/ViewConfigService';
-import { IWorkweekPreset } from '../types/SettingsTypes';
-import { IEventBus } from '../types/CalendarTypes';
-import {
- CalendarEvents,
- RenderPayload,
- WorkweekChangePayload,
- ViewUpdatePayload
-} from './CalendarEvents';
-
-export class CalendarApp {
- private animator!: NavigationAnimator;
- private container!: HTMLElement;
- private dayOffset = 0;
- private currentViewId = 'simple';
- private workweekPreset: IWorkweekPreset | null = null;
- private groupingOverrides: Map = new Map();
-
- constructor(
- private orchestrator: CalendarOrchestrator,
- private timeAxisRenderer: TimeAxisRenderer,
- private dateService: DateService,
- private scrollManager: ScrollManager,
- private headerDrawerManager: HeaderDrawerManager,
- private dragDropManager: DragDropManager,
- private edgeScrollManager: EdgeScrollManager,
- private resizeManager: ResizeManager,
- private headerDrawerRenderer: HeaderDrawerRenderer,
- private eventPersistenceManager: EventPersistenceManager,
- private settingsService: SettingsService,
- private viewConfigService: ViewConfigService,
- private eventBus: IEventBus
- ) {}
-
- async init(container: HTMLElement): Promise {
- this.container = container;
-
- // Load settings
- const gridSettings = await this.settingsService.getGridSettings();
- if (!gridSettings) {
- throw new Error('GridSettings not found');
- }
-
- this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset();
-
- // Create NavigationAnimator with DOM elements
- this.animator = new NavigationAnimator(
- container.querySelector('swp-header-track') as HTMLElement,
- container.querySelector('swp-content-track') as HTMLElement,
- container.querySelector('swp-header-drawer')
- );
-
- // Render time axis from settings
- this.timeAxisRenderer.render(
- container.querySelector('#time-axis') as HTMLElement,
- gridSettings.dayStartHour,
- gridSettings.dayEndHour
- );
-
- // Init managers
- this.scrollManager.init(container);
- this.headerDrawerManager.init(container);
- this.dragDropManager.init(container);
- this.resizeManager.init(container);
-
- const scrollableContent = container.querySelector('swp-scrollable-content') as HTMLElement;
- this.edgeScrollManager.init(scrollableContent);
-
- // Setup command event listeners
- this.setupEventListeners();
-
- // Emit ready status
- this.emitStatus('ready');
- }
-
- private setupEventListeners(): void {
- // Navigation commands via EventBus
- this.eventBus.on(CalendarEvents.CMD_NAVIGATE_PREV, () => {
- this.handleNavigatePrev();
- });
-
- this.eventBus.on(CalendarEvents.CMD_NAVIGATE_NEXT, () => {
- this.handleNavigateNext();
- });
-
- // Drawer toggle via EventBus
- this.eventBus.on(CalendarEvents.CMD_DRAWER_TOGGLE, () => {
- this.headerDrawerManager.toggle();
- });
-
- // Render command via EventBus
- this.eventBus.on(CalendarEvents.CMD_RENDER, (e: Event) => {
- const { viewId } = (e as CustomEvent).detail;
- this.handleRenderCommand(viewId);
- });
-
- // Workweek change via EventBus
- this.eventBus.on(CalendarEvents.CMD_WORKWEEK_CHANGE, (e: Event) => {
- const { presetId } = (e as CustomEvent).detail;
- this.handleWorkweekChange(presetId);
- });
-
- // View update via EventBus
- this.eventBus.on(CalendarEvents.CMD_VIEW_UPDATE, (e: Event) => {
- const { type, values } = (e as CustomEvent).detail;
- this.handleViewUpdate(type, values);
- });
- }
-
- private async handleRenderCommand(viewId: string): Promise {
- this.currentViewId = viewId;
- await this.render();
- this.emitStatus('rendered', { viewId });
- }
-
- private async handleNavigatePrev(): Promise {
- const step = this.workweekPreset?.periodDays ?? 7;
- this.dayOffset -= step;
- await this.animator.slide('right', () => this.render());
- this.emitStatus('rendered', { viewId: this.currentViewId });
- }
-
- private async handleNavigateNext(): Promise {
- const step = this.workweekPreset?.periodDays ?? 7;
- this.dayOffset += step;
- await this.animator.slide('left', () => this.render());
- this.emitStatus('rendered', { viewId: this.currentViewId });
- }
-
- private async handleWorkweekChange(presetId: string): Promise {
- const preset = await this.settingsService.getWorkweekPreset(presetId);
- if (preset) {
- this.workweekPreset = preset;
- await this.render();
- this.emitStatus('rendered', { viewId: this.currentViewId });
- }
- }
-
- private async handleViewUpdate(type: string, values: string[]): Promise {
- this.groupingOverrides.set(type, values);
- await this.render();
- this.emitStatus('rendered', { viewId: this.currentViewId });
- }
-
- private async render(): Promise {
- const storedConfig = await this.viewConfigService.getById(this.currentViewId);
- if (!storedConfig) {
- this.emitStatus('error', { message: `ViewConfig not found: ${this.currentViewId}` });
- return;
- }
-
- // Populate date values based on workweek preset and day offset
- const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5];
- const periodDays = this.workweekPreset?.periodDays ?? 7;
-
- // For single-day navigation (periodDays=1), show consecutive days from offset
- // For week navigation (periodDays=7), show workDays from the week containing offset
- const dates = periodDays === 1
- ? this.dateService.getDatesFromOffset(this.dayOffset, workDays.length)
- : this.dateService.getWorkDaysFromOffset(this.dayOffset, workDays);
-
- // Clone config and apply overrides
- const viewConfig: ViewConfig = {
- ...storedConfig,
- groupings: storedConfig.groupings.map(g => {
- // Apply date values
- if (g.type === 'date') {
- return { ...g, values: dates };
- }
- // Apply grouping overrides
- const override = this.groupingOverrides.get(g.type);
- if (override) {
- return { ...g, values: override };
- }
- return g;
- })
- };
-
- await this.orchestrator.render(viewConfig, this.container);
- }
-
- private emitStatus(status: string, detail?: object): void {
- this.container.dispatchEvent(new CustomEvent(`calendar:status:${status}`, {
- detail,
- bubbles: true
- }));
- }
-}
diff --git a/packages/calendar/src/core/CalendarEvents.ts b/packages/calendar/src/core/CalendarEvents.ts
deleted file mode 100644
index 4cf553e..0000000
--- a/packages/calendar/src/core/CalendarEvents.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * CalendarEvents - Command and status events for CalendarApp
- */
-export const CalendarEvents = {
- // Command events (host → calendar)
- CMD_NAVIGATE_PREV: 'calendar:cmd:navigate:prev',
- CMD_NAVIGATE_NEXT: 'calendar:cmd:navigate:next',
- CMD_DRAWER_TOGGLE: 'calendar:cmd:drawer:toggle',
- CMD_RENDER: 'calendar:cmd:render',
- CMD_WORKWEEK_CHANGE: 'calendar:cmd:workweek:change',
- CMD_VIEW_UPDATE: 'calendar:cmd:view:update'
-} as const;
-
-/**
- * Payload interfaces for CalendarEvents
- */
-export interface RenderPayload {
- viewId: string;
-}
-
-export interface WorkweekChangePayload {
- presetId: string;
-}
-
-export interface ViewUpdatePayload {
- type: string;
- values: string[];
-}
diff --git a/packages/calendar/src/core/CalendarOrchestrator.ts b/packages/calendar/src/core/CalendarOrchestrator.ts
deleted file mode 100644
index 933e8a5..0000000
--- a/packages/calendar/src/core/CalendarOrchestrator.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { IRenderer, IRenderContext } from './IGroupingRenderer';
-import { buildPipeline } from './RenderBuilder';
-import { EventRenderer } from '../features/event/EventRenderer';
-import { ScheduleRenderer } from '../features/schedule/ScheduleRenderer';
-import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer';
-import { ViewConfig, GroupingConfig } from './ViewConfig';
-import { FilterTemplate } from './FilterTemplate';
-import { DateService } from './DateService';
-import { IEntityService } from '../storage/IEntityService';
-import { ISync } from '../types/CalendarTypes';
-
-export class CalendarOrchestrator {
- constructor(
- private allRenderers: IRenderer[],
- private eventRenderer: EventRenderer,
- private scheduleRenderer: ScheduleRenderer,
- private headerDrawerRenderer: HeaderDrawerRenderer,
- private dateService: DateService,
- private entityServices: IEntityService[]
- ) {}
-
- async render(viewConfig: ViewConfig, container: HTMLElement): Promise {
- const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
- const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
- if (!headerContainer || !columnContainer) {
- throw new Error('Missing swp-calendar-header or swp-day-columns');
- }
-
- // Byg filter fra viewConfig
- const filter: Record = {};
- for (const grouping of viewConfig.groupings) {
- filter[grouping.type] = grouping.values;
- }
-
- // Byg FilterTemplate fra viewConfig groupings (kun de med idProperty)
- const filterTemplate = new FilterTemplate(this.dateService);
- for (const grouping of viewConfig.groupings) {
- if (grouping.idProperty) {
- filterTemplate.addField(grouping.idProperty, grouping.derivedFrom);
- }
- }
-
- // Resolve belongsTo relations (e.g., team.resourceIds)
- const { parentChildMap, childType } = await this.resolveBelongsTo(viewConfig.groupings, filter);
-
- const context: IRenderContext = { headerContainer, columnContainer, filter, groupings: viewConfig.groupings, parentChildMap, childType };
-
- // Clear
- headerContainer.innerHTML = '';
- columnContainer.innerHTML = '';
-
- // Sæt data-levels attribut for CSS grid-row styling
- const levels = viewConfig.groupings.map(g => g.type).join(' ');
- headerContainer.dataset.levels = levels;
-
- // Vælg renderers baseret på groupings types
- const activeRenderers = this.selectRenderers(viewConfig);
-
- // Byg og kør pipeline
- const pipeline = buildPipeline(activeRenderers);
- await pipeline.run(context);
-
- // Render schedule unavailable zones (før events)
- await this.scheduleRenderer.render(container, filter);
-
- // Render timed events in grid (med filterTemplate til matching)
- await this.eventRenderer.render(container, filter, filterTemplate);
-
- // Render allDay events in header drawer (med filterTemplate til matching)
- await this.headerDrawerRenderer.render(container, filter, filterTemplate);
- }
-
- private selectRenderers(viewConfig: ViewConfig): IRenderer[] {
- const types = viewConfig.groupings.map(g => g.type);
- // Sortér renderers i samme rækkefølge som viewConfig.groupings
- return types
- .map(type => this.allRenderers.find(r => r.type === type))
- .filter((r): r is IRenderer => r !== undefined);
- }
-
- /**
- * Resolve belongsTo relations to build parent-child map
- * e.g., belongsTo: 'team.resourceIds' → { team1: ['EMP001', 'EMP002'], team2: [...] }
- * Also returns the childType (the grouping type that has belongsTo)
- */
- private async resolveBelongsTo(
- groupings: GroupingConfig[],
- filter: Record
- ): Promise<{ parentChildMap?: Record; childType?: string }> {
- // Find grouping with belongsTo
- const childGrouping = groupings.find(g => g.belongsTo);
- if (!childGrouping?.belongsTo) return {};
-
- // Parse belongsTo: 'team.resourceIds'
- const [entityType, property] = childGrouping.belongsTo.split('.');
- if (!entityType || !property) return {};
-
- // Get parent IDs from filter
- const parentIds = filter[entityType] || [];
- if (parentIds.length === 0) return {};
-
- // Find service dynamisk baseret på entityType (ingen hardcoded type check)
- const service = this.entityServices.find(s =>
- s.entityType.toLowerCase() === entityType
- );
- if (!service) return {};
-
- // Hent alle entities og filtrer på parentIds
- const allEntities = await service.getAll();
- const entities = allEntities.filter(e =>
- parentIds.includes((e as unknown as Record).id as string)
- );
-
- // Byg parent-child map
- const map: Record = {};
- for (const entity of entities) {
- const entityRecord = entity as unknown as Record;
- const children = (entityRecord[property] as string[]) || [];
- map[entityRecord.id as string] = children;
- }
-
- return { parentChildMap: map, childType: childGrouping.type };
- }
-}
diff --git a/packages/calendar/src/core/DateService.ts b/packages/calendar/src/core/DateService.ts
deleted file mode 100644
index 1d3c44d..0000000
--- a/packages/calendar/src/core/DateService.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import dayjs from 'dayjs';
-import utc from 'dayjs/plugin/utc';
-import timezone from 'dayjs/plugin/timezone';
-import isoWeek from 'dayjs/plugin/isoWeek';
-import { ITimeFormatConfig } from './ITimeFormatConfig';
-
-// Enable dayjs plugins
-dayjs.extend(utc);
-dayjs.extend(timezone);
-dayjs.extend(isoWeek);
-
-export class DateService {
- private timezone: string;
- private baseDate: dayjs.Dayjs;
-
- constructor(private config: ITimeFormatConfig, baseDate?: Date) {
- this.timezone = config.timezone;
- // Allow setting a fixed base date for demo/testing purposes
- this.baseDate = baseDate ? dayjs(baseDate) : dayjs();
- }
-
- /**
- * Set a fixed base date (useful for demos with static mock data)
- */
- setBaseDate(date: Date): void {
- this.baseDate = dayjs(date);
- }
-
- /**
- * Get the current base date (either fixed or today)
- */
- getBaseDate(): Date {
- return this.baseDate.toDate();
- }
-
- parseISO(isoString: string): Date {
- return dayjs(isoString).toDate();
- }
-
- getDayName(date: Date, format: 'short' | 'long' = 'short'): string {
- return new Intl.DateTimeFormat(this.config.locale, { weekday: format }).format(date);
- }
-
- /**
- * Get dates starting from a day offset
- * @param dayOffset - Day offset from base date
- * @param count - Number of consecutive days to return
- * @returns Array of date strings in YYYY-MM-DD format
- */
- getDatesFromOffset(dayOffset: number, count: number): string[] {
- const startDate = this.baseDate.add(dayOffset, 'day');
- return Array.from({ length: count }, (_, i) =>
- startDate.add(i, 'day').format('YYYY-MM-DD')
- );
- }
-
- /**
- * Get specific weekdays from the week containing the offset date
- * @param dayOffset - Day offset from base date
- * @param workDays - Array of ISO weekday numbers (1=Monday, 7=Sunday)
- * @returns Array of date strings in YYYY-MM-DD format
- */
- getWorkDaysFromOffset(dayOffset: number, workDays: number[]): string[] {
- // Get the date at offset, then find its week's Monday
- const targetDate = this.baseDate.add(dayOffset, 'day');
- const monday = targetDate.startOf('week').add(1, 'day');
-
- return workDays.map(isoDay => {
- // ISO: 1=Monday, 7=Sunday → days from Monday: 0-6
- const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
- return monday.add(daysFromMonday, 'day').format('YYYY-MM-DD');
- });
- }
-
- // Legacy methods for backwards compatibility
- getWeekDates(weekOffset = 0, days = 7): string[] {
- return this.getDatesFromOffset(weekOffset * 7, days);
- }
-
- getWorkWeekDates(weekOffset: number, workDays: number[]): string[] {
- return this.getWorkDaysFromOffset(weekOffset * 7, workDays);
- }
-
- // ============================================
- // FORMATTING
- // ============================================
-
- formatTime(date: Date, showSeconds = false): string {
- const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
- return dayjs(date).format(pattern);
- }
-
- formatTimeRange(start: Date, end: Date): string {
- return `${this.formatTime(start)} - ${this.formatTime(end)}`;
- }
-
- formatDate(date: Date): string {
- return dayjs(date).format('YYYY-MM-DD');
- }
-
- getDateKey(date: Date): string {
- return this.formatDate(date);
- }
-
- // ============================================
- // COLUMN KEY
- // ============================================
-
- /**
- * Build a uniform columnKey from grouping segments
- * Handles any combination of date, resource, team, etc.
- *
- * @example
- * buildColumnKey({ date: '2025-12-09' }) → "2025-12-09"
- * buildColumnKey({ date: '2025-12-09', resource: 'EMP001' }) → "2025-12-09:EMP001"
- */
- buildColumnKey(segments: Record): string {
- // Always put date first if present, then other segments alphabetically
- const date = segments.date;
- const others = Object.entries(segments)
- .filter(([k]) => k !== 'date')
- .sort(([a], [b]) => a.localeCompare(b))
- .map(([, v]) => v);
-
- return date ? [date, ...others].join(':') : others.join(':');
- }
-
- /**
- * Parse a columnKey back into segments
- * Assumes format: "date:resource:..." or just "date"
- */
- parseColumnKey(columnKey: string): { date: string; resource?: string } {
- const parts = columnKey.split(':');
- return {
- date: parts[0],
- resource: parts[1]
- };
- }
-
- /**
- * Extract dateKey from columnKey (first segment)
- */
- getDateFromColumnKey(columnKey: string): string {
- return columnKey.split(':')[0];
- }
-
- // ============================================
- // TIME CALCULATIONS
- // ============================================
-
- timeToMinutes(timeString: string): number {
- const parts = timeString.split(':').map(Number);
- const hours = parts[0] || 0;
- const minutes = parts[1] || 0;
- return hours * 60 + minutes;
- }
-
- minutesToTime(totalMinutes: number): string {
- const hours = Math.floor(totalMinutes / 60);
- const minutes = totalMinutes % 60;
- return dayjs().hour(hours).minute(minutes).format('HH:mm');
- }
-
- getMinutesSinceMidnight(date: Date): number {
- const d = dayjs(date);
- return d.hour() * 60 + d.minute();
- }
-
- // ============================================
- // UTC CONVERSIONS
- // ============================================
-
- toUTC(localDate: Date): string {
- return dayjs.tz(localDate, this.timezone).utc().toISOString();
- }
-
- fromUTC(utcString: string): Date {
- return dayjs.utc(utcString).tz(this.timezone).toDate();
- }
-
- // ============================================
- // DATE CREATION
- // ============================================
-
- createDateAtTime(baseDate: Date | string, timeString: string): Date {
- const totalMinutes = this.timeToMinutes(timeString);
- const hours = Math.floor(totalMinutes / 60);
- const minutes = totalMinutes % 60;
- return dayjs(baseDate).startOf('day').hour(hours).minute(minutes).toDate();
- }
-
- getISOWeekDay(date: Date | string): number {
- return dayjs(date).isoWeekday(); // 1=Monday, 7=Sunday
- }
-}
diff --git a/packages/calendar/src/core/EntityResolver.ts b/packages/calendar/src/core/EntityResolver.ts
deleted file mode 100644
index 7161c30..0000000
--- a/packages/calendar/src/core/EntityResolver.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { IEntityResolver } from './IEntityResolver';
-
-/**
- * EntityResolver - Resolves entities from pre-loaded cache
- *
- * Entities must be loaded before use (typically at render time).
- * This allows synchronous lookups during filtering.
- */
-export class EntityResolver implements IEntityResolver {
- private cache: Map>> = new Map();
-
- /**
- * Load entities into cache for a given type
- * @param entityType - The entity type (e.g., 'resource')
- * @param entities - Array of entities with 'id' property
- */
- load(entityType: string, entities: T[]): void {
- const typeCache = new Map>();
- for (const entity of entities) {
- // Cast to Record for storage while preserving original data
- typeCache.set(entity.id, entity as unknown as Record);
- }
- this.cache.set(entityType, typeCache);
- }
-
- /**
- * Resolve an entity by type and ID
- */
- resolve(entityType: string, id: string): Record | undefined {
- const typeCache = this.cache.get(entityType);
- if (!typeCache) return undefined;
- return typeCache.get(id);
- }
-
- /**
- * Clear all cached entities
- */
- clear(): void {
- this.cache.clear();
- }
-
- /**
- * Clear cache for a specific entity type
- */
- clearType(entityType: string): void {
- this.cache.delete(entityType);
- }
-}
diff --git a/packages/calendar/src/core/EventBus.ts b/packages/calendar/src/core/EventBus.ts
deleted file mode 100644
index 469a73e..0000000
--- a/packages/calendar/src/core/EventBus.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { IEventLogEntry, IListenerEntry, IEventBus } from '../types/CalendarTypes';
-
-/**
- * Central event dispatcher for calendar using DOM CustomEvents
- * Provides logging and debugging capabilities
- */
-export class EventBus implements IEventBus {
- private eventLog: IEventLogEntry[] = [];
- private debug: boolean = false;
- private listeners: Set = new Set();
-
- // Log configuration for different categories
- private logConfig: { [key: string]: boolean } = {
- calendar: true,
- grid: true,
- event: true,
- scroll: true,
- navigation: true,
- view: true,
- default: true
- };
-
- /**
- * Subscribe to an event via DOM addEventListener
- */
- on(eventType: string, handler: EventListener, options?: AddEventListenerOptions): () => void {
- document.addEventListener(eventType, handler, options);
-
- // Track for cleanup
- this.listeners.add({ eventType, handler, options });
-
- // Return unsubscribe function
- return () => this.off(eventType, handler);
- }
-
- /**
- * Subscribe to an event once
- */
- once(eventType: string, handler: EventListener): () => void {
- return this.on(eventType, handler, { once: true });
- }
-
- /**
- * Unsubscribe from an event
- */
- off(eventType: string, handler: EventListener): void {
- document.removeEventListener(eventType, handler);
-
- // Remove from tracking
- for (const listener of this.listeners) {
- if (listener.eventType === eventType && listener.handler === handler) {
- this.listeners.delete(listener);
- break;
- }
- }
- }
-
- /**
- * Emit an event via DOM CustomEvent
- */
- emit(eventType: string, detail: unknown = {}): boolean {
- // Validate eventType
- if (!eventType) {
- return false;
- }
-
- const event = new CustomEvent(eventType, {
- detail: detail ?? {},
- bubbles: true,
- cancelable: true
- });
-
- // Log event with grouping
- if (this.debug) {
- this.logEventWithGrouping(eventType, detail);
- }
-
- this.eventLog.push({
- type: eventType,
- detail: detail ?? {},
- timestamp: Date.now()
- });
-
- // Emit on document (only DOM events now)
- return !document.dispatchEvent(event);
- }
-
- /**
- * Log event with console grouping
- */
- private logEventWithGrouping(eventType: string, _detail: unknown): void {
- // Extract category from event type (e.g., 'calendar:datechanged' → 'calendar')
- const category = this.extractCategory(eventType);
-
- // Only log if category is enabled
- if (!this.logConfig[category]) {
- return;
- }
-
- // Get category emoji and color (used for future console styling)
- this.getCategoryStyle(category);
- }
-
- /**
- * Extract category from event type
- */
- private extractCategory(eventType: string): string {
- if (!eventType) {
- return 'unknown';
- }
-
- if (eventType.includes(':')) {
- return eventType.split(':')[0];
- }
-
- // Fallback: try to detect category from event name patterns
- const lowerType = eventType.toLowerCase();
- if (lowerType.includes('grid') || lowerType.includes('rendered')) return 'grid';
- if (lowerType.includes('event') || lowerType.includes('sync')) return 'event';
- if (lowerType.includes('scroll')) return 'scroll';
- if (lowerType.includes('nav') || lowerType.includes('date')) return 'navigation';
- if (lowerType.includes('view')) return 'view';
-
- return 'default';
- }
-
- /**
- * Get styling for different categories
- */
- private getCategoryStyle(category: string): { emoji: string; color: string } {
- const styles: { [key: string]: { emoji: string; color: string } } = {
- calendar: { emoji: '📅', color: '#2196F3' },
- grid: { emoji: '📊', color: '#4CAF50' },
- event: { emoji: '📌', color: '#FF9800' },
- scroll: { emoji: '📜', color: '#9C27B0' },
- navigation: { emoji: '🧭', color: '#F44336' },
- view: { emoji: '👁', color: '#00BCD4' },
- default: { emoji: '📢', color: '#607D8B' }
- };
-
- return styles[category] || styles.default;
- }
-
- /**
- * Configure logging for specific categories
- */
- setLogConfig(config: { [key: string]: boolean }): void {
- this.logConfig = { ...this.logConfig, ...config };
- }
-
- /**
- * Get current log configuration
- */
- getLogConfig(): { [key: string]: boolean } {
- return { ...this.logConfig };
- }
-
- /**
- * Get event history
- */
- getEventLog(eventType?: string): IEventLogEntry[] {
- if (eventType) {
- return this.eventLog.filter(e => e.type === eventType);
- }
- return this.eventLog;
- }
-
- /**
- * Enable/disable debug mode
- */
- setDebug(enabled: boolean): void {
- this.debug = enabled;
- }
-}
diff --git a/packages/calendar/src/core/FilterTemplate.ts b/packages/calendar/src/core/FilterTemplate.ts
deleted file mode 100644
index 00451b1..0000000
--- a/packages/calendar/src/core/FilterTemplate.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { ICalendarEvent } from '../types/CalendarTypes';
-import { DateService } from './DateService';
-import { IEntityResolver } from './IEntityResolver';
-
-/**
- * Field definition for FilterTemplate
- */
-interface IFilterField {
- idProperty: string;
- derivedFrom?: string;
-}
-
-/**
- * Parsed dot-notation reference
- */
-interface IDotNotation {
- entityType: string; // e.g., 'resource'
- property: string; // e.g., 'teamId'
- foreignKey: string; // e.g., 'resourceId'
-}
-
-/**
- * FilterTemplate - Bygger nøgler til event-kolonne matching
- *
- * ViewConfig definerer hvilke felter (idProperties) der indgår i kolonnens nøgle.
- * Samme template bruges til at bygge nøgle for både kolonne og event.
- *
- * Supports dot-notation for hierarchical relations:
- * - 'resource.teamId' → looks up event.resourceId → resource entity → teamId
- *
- * Princip: Kolonnens nøgle-template bestemmer hvad der matches på.
- *
- * @see docs/filter-template.md
- */
-export class FilterTemplate {
- private fields: IFilterField[] = [];
-
- constructor(
- private dateService: DateService,
- private entityResolver?: IEntityResolver
- ) {}
-
- /**
- * Tilføj felt til template
- * @param idProperty - Property-navn (bruges på både event og column.dataset)
- * @param derivedFrom - Hvis feltet udledes fra anden property (f.eks. date fra start)
- */
- addField(idProperty: string, derivedFrom?: string): this {
- this.fields.push({ idProperty, derivedFrom });
- return this;
- }
-
- /**
- * Parse dot-notation string into components
- * @example 'resource.teamId' → { entityType: 'resource', property: 'teamId', foreignKey: 'resourceId' }
- */
- private parseDotNotation(idProperty: string): IDotNotation | null {
- if (!idProperty.includes('.')) return null;
- const [entityType, property] = idProperty.split('.');
- return {
- entityType,
- property,
- foreignKey: entityType + 'Id' // Convention: resource → resourceId
- };
- }
-
- /**
- * Get dataset key for column lookup
- * For dot-notation 'resource.teamId', we look for 'teamId' in dataset
- */
- private getDatasetKey(idProperty: string): string {
- const dotNotation = this.parseDotNotation(idProperty);
- if (dotNotation) {
- return dotNotation.property; // 'teamId'
- }
- return idProperty;
- }
-
- /**
- * Byg nøgle fra kolonne
- * Læser værdier fra column.dataset[idProperty]
- * For dot-notation, uses the property part (resource.teamId → teamId)
- */
- buildKeyFromColumn(column: HTMLElement): string {
- return this.fields
- .map(f => {
- const key = this.getDatasetKey(f.idProperty);
- return column.dataset[key] || '';
- })
- .join(':');
- }
-
- /**
- * Byg nøgle fra event
- * Læser værdier fra event[idProperty] eller udleder fra derivedFrom
- * For dot-notation, resolves via EntityResolver
- */
- buildKeyFromEvent(event: ICalendarEvent): string {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const eventRecord = event as any;
- return this.fields
- .map(f => {
- // Check for dot-notation (e.g., 'resource.teamId')
- const dotNotation = this.parseDotNotation(f.idProperty);
- if (dotNotation) {
- return this.resolveDotNotation(eventRecord, dotNotation);
- }
-
- if (f.derivedFrom) {
- // Udled værdi (f.eks. date fra start)
- const sourceValue = eventRecord[f.derivedFrom];
- if (sourceValue instanceof Date) {
- return this.dateService.getDateKey(sourceValue);
- }
- return String(sourceValue || '');
- }
- return String(eventRecord[f.idProperty] || '');
- })
- .join(':');
- }
-
- /**
- * Resolve dot-notation reference via EntityResolver
- */
- private resolveDotNotation(eventRecord: Record, dotNotation: IDotNotation): string {
- if (!this.entityResolver) {
- console.warn(`FilterTemplate: EntityResolver required for dot-notation '${dotNotation.entityType}.${dotNotation.property}'`);
- return '';
- }
-
- // Get foreign key value from event (e.g., resourceId)
- const foreignId = eventRecord[dotNotation.foreignKey];
- if (!foreignId) return '';
-
- // Resolve entity
- const entity = this.entityResolver.resolve(dotNotation.entityType, String(foreignId));
- if (!entity) return '';
-
- // Return property value from entity
- return String(entity[dotNotation.property] || '');
- }
-
- /**
- * Match event mod kolonne
- */
- matches(event: ICalendarEvent, column: HTMLElement): boolean {
- return this.buildKeyFromEvent(event) === this.buildKeyFromColumn(column);
- }
-}
diff --git a/packages/calendar/src/core/HeaderDrawerManager.ts b/packages/calendar/src/core/HeaderDrawerManager.ts
deleted file mode 100644
index 445bb23..0000000
--- a/packages/calendar/src/core/HeaderDrawerManager.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-export class HeaderDrawerManager {
- private drawer!: HTMLElement;
- private expanded = false;
- private currentRows = 0;
- private readonly rowHeight = 25;
- private readonly duration = 200;
-
- init(container: HTMLElement): void {
- this.drawer = container.querySelector('swp-header-drawer')!;
-
- if (!this.drawer) console.error('HeaderDrawerManager: swp-header-drawer not found');
- }
-
- toggle(): void {
- this.expanded ? this.collapse() : this.expand();
- }
-
- /**
- * Expand drawer to single row (legacy support)
- */
- expand(): void {
- this.expandToRows(1);
- }
-
- /**
- * Expand drawer to fit specified number of rows
- */
- expandToRows(rowCount: number): void {
- const targetHeight = rowCount * this.rowHeight;
- const currentHeight = this.expanded ? this.currentRows * this.rowHeight : 0;
-
- // Skip if already at target
- if (this.expanded && this.currentRows === rowCount) return;
-
- this.currentRows = rowCount;
- this.expanded = true;
- this.animate(currentHeight, targetHeight);
- }
-
- collapse(): void {
- if (!this.expanded) return;
- const currentHeight = this.currentRows * this.rowHeight;
- this.expanded = false;
- this.currentRows = 0;
- this.animate(currentHeight, 0);
- }
-
- private animate(from: number, to: number): void {
- const keyframes = [
- { height: `${from}px` },
- { height: `${to}px` }
- ];
- const options: KeyframeAnimationOptions = {
- duration: this.duration,
- easing: 'ease',
- fill: 'forwards'
- };
-
- // Kun animér drawer - ScrollManager synkroniserer header-spacer via ResizeObserver
- this.drawer.animate(keyframes, options);
- }
-
- isExpanded(): boolean {
- return this.expanded;
- }
-
- getRowCount(): number {
- return this.currentRows;
- }
-}
diff --git a/packages/calendar/src/core/IEntityResolver.ts b/packages/calendar/src/core/IEntityResolver.ts
deleted file mode 100644
index b825c0f..0000000
--- a/packages/calendar/src/core/IEntityResolver.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * IEntityResolver - Resolves entities by type and ID
- *
- * Used by FilterTemplate to resolve dot-notation references like 'resource.teamId'
- * where the value needs to be looked up from a related entity.
- */
-export interface IEntityResolver {
- /**
- * Resolve an entity by type and ID
- * @param entityType - The entity type (e.g., 'resource', 'booking', 'customer')
- * @param id - The entity ID
- * @returns The entity record or undefined if not found
- */
- resolve(entityType: string, id: string): Record | undefined;
-}
diff --git a/packages/calendar/src/core/IGridConfig.ts b/packages/calendar/src/core/IGridConfig.ts
deleted file mode 100644
index 03c6a2f..0000000
--- a/packages/calendar/src/core/IGridConfig.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface IGridConfig {
- hourHeight: number; // pixels per hour
- dayStartHour: number; // e.g. 6
- dayEndHour: number; // e.g. 18
- snapInterval: number; // minutes, e.g. 15
- gridStartThresholdMinutes?: number; // threshold for GRID grouping (default 10)
-}
diff --git a/packages/calendar/src/core/IGroupingRenderer.ts b/packages/calendar/src/core/IGroupingRenderer.ts
deleted file mode 100644
index a1bc507..0000000
--- a/packages/calendar/src/core/IGroupingRenderer.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { GroupingConfig } from './ViewConfig';
-
-export interface IRenderContext {
- headerContainer: HTMLElement;
- columnContainer: HTMLElement;
- filter: Record; // { team: ['alpha'], resource: ['alice', 'bob'], date: [...] }
- groupings?: GroupingConfig[]; // Full grouping configs (for hideHeader etc.)
- parentChildMap?: Record; // { team1: ['EMP001', 'EMP002'], team2: ['EMP003', 'EMP004'] }
- childType?: string; // The type of the child grouping (e.g., 'resource' when team has belongsTo)
-}
-
-export interface IRenderer {
- readonly type: string;
- render(context: IRenderContext): void | Promise;
-}
diff --git a/packages/calendar/src/core/IGroupingStore.ts b/packages/calendar/src/core/IGroupingStore.ts
deleted file mode 100644
index 8abc837..0000000
--- a/packages/calendar/src/core/IGroupingStore.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface IGroupingStore {
- readonly type: string;
- getByIds(ids: string[]): T[];
-}
diff --git a/packages/calendar/src/core/ITimeFormatConfig.ts b/packages/calendar/src/core/ITimeFormatConfig.ts
deleted file mode 100644
index 1a401d5..0000000
--- a/packages/calendar/src/core/ITimeFormatConfig.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface ITimeFormatConfig {
- timezone: string;
- use24HourFormat: boolean;
- locale: string;
- dateFormat: 'locale' | 'technical';
- showSeconds: boolean;
-}
diff --git a/packages/calendar/src/core/NavigationAnimator.ts b/packages/calendar/src/core/NavigationAnimator.ts
deleted file mode 100644
index cf173ad..0000000
--- a/packages/calendar/src/core/NavigationAnimator.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-export class NavigationAnimator {
- constructor(
- private headerTrack: HTMLElement,
- private contentTrack: HTMLElement,
- private headerDrawer: HTMLElement | null
- ) {}
-
- async slide(direction: 'left' | 'right', renderFn: () => Promise): Promise {
- const out = direction === 'left' ? '-100%' : '100%';
- const into = direction === 'left' ? '100%' : '-100%';
-
- await this.animateOut(out);
- await renderFn();
- await this.animateIn(into);
- }
-
- private async animateOut(translate: string): Promise {
- const animations = [
- this.headerTrack.animate(
- [{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
- { duration: 200, easing: 'ease-in' }
- ).finished,
- this.contentTrack.animate(
- [{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
- { duration: 200, easing: 'ease-in' }
- ).finished
- ];
-
- if (this.headerDrawer) {
- animations.push(
- this.headerDrawer.animate(
- [{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
- { duration: 200, easing: 'ease-in' }
- ).finished
- );
- }
-
- await Promise.all(animations);
- }
-
- private async animateIn(translate: string): Promise {
- const animations = [
- this.headerTrack.animate(
- [{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
- { duration: 200, easing: 'ease-out' }
- ).finished,
- this.contentTrack.animate(
- [{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
- { duration: 200, easing: 'ease-out' }
- ).finished
- ];
-
- if (this.headerDrawer) {
- animations.push(
- this.headerDrawer.animate(
- [{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
- { duration: 200, easing: 'ease-out' }
- ).finished
- );
- }
-
- await Promise.all(animations);
- }
-}
diff --git a/packages/calendar/src/core/RenderBuilder.ts b/packages/calendar/src/core/RenderBuilder.ts
deleted file mode 100644
index 68f0ee3..0000000
--- a/packages/calendar/src/core/RenderBuilder.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { IRenderer, IRenderContext } from './IGroupingRenderer';
-
-export interface Pipeline {
- run(context: IRenderContext): Promise;
-}
-
-export function buildPipeline(renderers: IRenderer[]): Pipeline {
- return {
- async run(context: IRenderContext) {
- for (const renderer of renderers) {
- await renderer.render(context);
- }
- }
- };
-}
diff --git a/packages/calendar/src/core/ScrollManager.ts b/packages/calendar/src/core/ScrollManager.ts
deleted file mode 100644
index bb4f490..0000000
--- a/packages/calendar/src/core/ScrollManager.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-export class ScrollManager {
- private scrollableContent!: HTMLElement;
- private timeAxisContent!: HTMLElement;
- private calendarHeader!: HTMLElement;
- private headerDrawer!: HTMLElement;
- private headerViewport!: HTMLElement;
- private headerSpacer!: HTMLElement;
- private resizeObserver!: ResizeObserver;
-
- init(container: HTMLElement): void {
- this.scrollableContent = container.querySelector('swp-scrollable-content')!;
- this.timeAxisContent = container.querySelector('swp-time-axis-content')!;
- this.calendarHeader = container.querySelector('swp-calendar-header')!;
- this.headerDrawer = container.querySelector('swp-header-drawer')!;
- this.headerViewport = container.querySelector('swp-header-viewport')!;
- this.headerSpacer = container.querySelector('swp-header-spacer')!;
-
- this.scrollableContent.addEventListener('scroll', () => this.onScroll());
-
- // Synkroniser header-spacer højde med header-viewport
- this.resizeObserver = new ResizeObserver(() => this.syncHeaderSpacerHeight());
- this.resizeObserver.observe(this.headerViewport);
- this.syncHeaderSpacerHeight();
- }
-
- private syncHeaderSpacerHeight(): void {
- // Kopier den faktiske computed height direkte fra header-viewport
- const computedHeight = getComputedStyle(this.headerViewport).height;
- this.headerSpacer.style.height = computedHeight;
- }
-
- private onScroll(): void {
- const { scrollTop, scrollLeft } = this.scrollableContent;
-
- // Synkroniser time-axis vertikalt
- this.timeAxisContent.style.transform = `translateY(-${scrollTop}px)`;
-
- // Synkroniser header og drawer horisontalt
- this.calendarHeader.style.transform = `translateX(-${scrollLeft}px)`;
- this.headerDrawer.style.transform = `translateX(-${scrollLeft}px)`;
- }
-}
diff --git a/packages/calendar/src/core/ViewConfig.ts b/packages/calendar/src/core/ViewConfig.ts
deleted file mode 100644
index 8ecd79b..0000000
--- a/packages/calendar/src/core/ViewConfig.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ISync } from '../types/CalendarTypes';
-
-export interface ViewTemplate {
- id: string;
- name: string;
- groupingTypes: string[];
-}
-
-export interface ViewConfig extends ISync {
- id: string; // templateId (e.g. 'day', 'simple', 'resource')
- groupings: GroupingConfig[];
-}
-
-export interface GroupingConfig {
- type: string;
- values: string[];
- idProperty?: string; // Property-navn på event (f.eks. 'resourceId') - kun for event matching
- derivedFrom?: string; // Hvis feltet udledes fra anden property (f.eks. 'date' fra 'start')
- belongsTo?: string; // Parent-child relation (f.eks. 'team.resourceIds')
- hideHeader?: boolean; // Skjul header-rækken for denne grouping (f.eks. dato i day-view)
-}
diff --git a/packages/calendar/src/extensions/audit/AuditService.ts b/packages/calendar/src/extensions/audit/AuditService.ts
deleted file mode 100644
index ccdb2b4..0000000
--- a/packages/calendar/src/extensions/audit/AuditService.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import { BaseEntityService } from '../../storage/BaseEntityService';
-import { IndexedDBContext } from '../../storage/IndexedDBContext';
-import { IAuditEntry, IAuditLoggedPayload } from '../../types/AuditTypes';
-import { EntityType, IEventBus, IEntitySavedPayload, IEntityDeletedPayload } from '../../types/CalendarTypes';
-import { CoreEvents } from '../../constants/CoreEvents';
-
-/**
- * AuditService - Entity service for audit entries
- *
- * RESPONSIBILITIES:
- * - Store audit entries in IndexedDB
- * - Listen for ENTITY_SAVED/ENTITY_DELETED events
- * - Create audit entries for all entity changes
- * - Emit AUDIT_LOGGED after saving (for SyncManager to listen)
- *
- * OVERRIDE PATTERN:
- * - Overrides save() to NOT emit events (prevents infinite loops)
- * - AuditService saves audit entries without triggering more audits
- *
- * EVENT CHAIN:
- * Entity change → ENTITY_SAVED/DELETED → AuditService → AUDIT_LOGGED → SyncManager
- */
-export class AuditService extends BaseEntityService {
- readonly storeName = 'audit';
- readonly entityType: EntityType = 'Audit';
-
- // Hardcoded userId for now - will come from session later
- private static readonly DEFAULT_USER_ID = '00000000-0000-0000-0000-000000000001';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- this.setupEventListeners();
- }
-
- /**
- * Setup listeners for ENTITY_SAVED and ENTITY_DELETED events
- */
- private setupEventListeners(): void {
- // Listen for entity saves (create/update)
- this.eventBus.on(CoreEvents.ENTITY_SAVED, (event: Event) => {
- const detail = (event as CustomEvent).detail;
- this.handleEntitySaved(detail);
- });
-
- // Listen for entity deletes
- this.eventBus.on(CoreEvents.ENTITY_DELETED, (event: Event) => {
- const detail = (event as CustomEvent).detail;
- this.handleEntityDeleted(detail);
- });
- }
-
- /**
- * Handle ENTITY_SAVED event - create audit entry
- */
- private async handleEntitySaved(payload: IEntitySavedPayload): Promise {
- // Don't audit audit entries (prevent infinite loops)
- if (payload.entityType === 'Audit') return;
-
- const auditEntry: IAuditEntry = {
- id: crypto.randomUUID(),
- entityType: payload.entityType,
- entityId: payload.entityId,
- operation: payload.operation,
- userId: AuditService.DEFAULT_USER_ID,
- timestamp: payload.timestamp,
- changes: payload.changes,
- synced: false,
- syncStatus: 'pending'
- };
-
- await this.save(auditEntry);
- }
-
- /**
- * Handle ENTITY_DELETED event - create audit entry
- */
- private async handleEntityDeleted(payload: IEntityDeletedPayload): Promise {
- // Don't audit audit entries (prevent infinite loops)
- if (payload.entityType === 'Audit') return;
-
- const auditEntry: IAuditEntry = {
- id: crypto.randomUUID(),
- entityType: payload.entityType,
- entityId: payload.entityId,
- operation: 'delete',
- userId: AuditService.DEFAULT_USER_ID,
- timestamp: payload.timestamp,
- changes: { id: payload.entityId }, // For delete, just store the ID
- synced: false,
- syncStatus: 'pending'
- };
-
- await this.save(auditEntry);
- }
-
- /**
- * Override save to NOT trigger ENTITY_SAVED event
- * Instead, emits AUDIT_LOGGED for SyncManager to listen
- *
- * This prevents infinite loops:
- * - BaseEntityService.save() emits ENTITY_SAVED
- * - AuditService listens to ENTITY_SAVED and creates audit
- * - If AuditService.save() also emitted ENTITY_SAVED, it would loop
- */
- async save(entity: IAuditEntry): Promise {
- const serialized = this.serialize(entity);
-
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readwrite');
- const store = transaction.objectStore(this.storeName);
- const request = store.put(serialized);
-
- request.onsuccess = () => {
- // Emit AUDIT_LOGGED instead of ENTITY_SAVED
- const payload: IAuditLoggedPayload = {
- auditId: entity.id,
- entityType: entity.entityType,
- entityId: entity.entityId,
- operation: entity.operation,
- timestamp: entity.timestamp
- };
- this.eventBus.emit(CoreEvents.AUDIT_LOGGED, payload);
- resolve();
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to save audit entry ${entity.id}: ${request.error}`));
- };
- });
- }
-
- /**
- * Override delete to NOT trigger ENTITY_DELETED event
- * Audit entries should never be deleted (compliance requirement)
- */
- async delete(_id: string): Promise {
- throw new Error('Audit entries cannot be deleted (compliance requirement)');
- }
-
- /**
- * Get pending audit entries (for sync)
- */
- async getPendingAudits(): Promise {
- return this.getBySyncStatus('pending');
- }
-
- /**
- * Get audit entries for a specific entity
- */
- async getByEntityId(entityId: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('entityId');
- const request = index.getAll(entityId);
-
- request.onsuccess = () => {
- const entries = request.result as IAuditEntry[];
- resolve(entries);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get audit entries for entity ${entityId}: ${request.error}`));
- };
- });
- }
-}
diff --git a/packages/calendar/src/extensions/audit/AuditStore.ts b/packages/calendar/src/extensions/audit/AuditStore.ts
deleted file mode 100644
index 769b3b9..0000000
--- a/packages/calendar/src/extensions/audit/AuditStore.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { IStore } from '../../storage/IStore';
-
-/**
- * AuditStore - IndexedDB store configuration for audit entries
- *
- * Stores all entity changes for:
- * - Compliance and audit trail
- * - Sync tracking with backend
- * - Change history
- *
- * Indexes:
- * - syncStatus: For finding pending entries to sync
- * - synced: Boolean flag for quick sync queries
- * - entityId: For getting all audits for a specific entity
- * - timestamp: For chronological queries
- */
-export class AuditStore implements IStore {
- readonly storeName = 'audit';
-
- create(db: IDBDatabase): void {
- const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
- store.createIndex('syncStatus', 'syncStatus', { unique: false });
- store.createIndex('synced', 'synced', { unique: false });
- store.createIndex('entityId', 'entityId', { unique: false });
- store.createIndex('timestamp', 'timestamp', { unique: false });
- }
-}
diff --git a/packages/calendar/src/extensions/audit/index.ts b/packages/calendar/src/extensions/audit/index.ts
deleted file mode 100644
index 48100e1..0000000
--- a/packages/calendar/src/extensions/audit/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export { AuditService } from './AuditService';
-export { AuditStore } from './AuditStore';
-export type { IAuditEntry, IAuditLoggedPayload } from '../../types/AuditTypes';
-
-// DI registration helper
-import type { Builder } from '@novadi/core';
-import { IStore } from '../../storage/IStore';
-import { AuditStore } from './AuditStore';
-import { AuditService } from './AuditService';
-
-export function registerAudit(builder: Builder): void {
- builder.registerType(AuditStore).as();
- builder.registerType(AuditService).as();
-}
diff --git a/packages/calendar/src/extensions/bookings/BookingService.ts b/packages/calendar/src/extensions/bookings/BookingService.ts
deleted file mode 100644
index d12eb57..0000000
--- a/packages/calendar/src/extensions/bookings/BookingService.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { IBooking, EntityType, IEventBus, BookingStatus } from '../../types/CalendarTypes';
-import { BookingStore } from './BookingStore';
-import { BaseEntityService } from '../../storage/BaseEntityService';
-import { IndexedDBContext } from '../../storage/IndexedDBContext';
-
-/**
- * BookingService - CRUD operations for bookings in IndexedDB
- */
-export class BookingService extends BaseEntityService {
- readonly storeName = BookingStore.STORE_NAME;
- readonly entityType: EntityType = 'Booking';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- protected serialize(booking: IBooking): unknown {
- return {
- ...booking,
- createdAt: booking.createdAt.toISOString()
- };
- }
-
- protected deserialize(data: unknown): IBooking {
- const raw = data as Record;
- return {
- ...raw,
- createdAt: new Date(raw.createdAt as string)
- } as IBooking;
- }
-
- /**
- * Get bookings for a customer
- */
- async getByCustomer(customerId: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('customerId');
- const request = index.getAll(customerId);
-
- request.onsuccess = () => {
- const data = request.result as unknown[];
- const bookings = data.map(item => this.deserialize(item));
- resolve(bookings);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get bookings for customer ${customerId}: ${request.error}`));
- };
- });
- }
-
- /**
- * Get bookings by status
- */
- async getByStatus(status: BookingStatus): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('status');
- const request = index.getAll(status);
-
- request.onsuccess = () => {
- const data = request.result as unknown[];
- const bookings = data.map(item => this.deserialize(item));
- resolve(bookings);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get bookings with status ${status}: ${request.error}`));
- };
- });
- }
-}
diff --git a/packages/calendar/src/extensions/bookings/BookingStore.ts b/packages/calendar/src/extensions/bookings/BookingStore.ts
deleted file mode 100644
index 5e64ad3..0000000
--- a/packages/calendar/src/extensions/bookings/BookingStore.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { IStore } from '../../storage/IStore';
-
-/**
- * BookingStore - IndexedDB ObjectStore definition for bookings
- */
-export class BookingStore implements IStore {
- static readonly STORE_NAME = 'bookings';
- readonly storeName = BookingStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- const store = db.createObjectStore(BookingStore.STORE_NAME, { keyPath: 'id' });
-
- store.createIndex('customerId', 'customerId', { unique: false });
- store.createIndex('status', 'status', { unique: false });
- store.createIndex('syncStatus', 'syncStatus', { unique: false });
- store.createIndex('createdAt', 'createdAt', { unique: false });
- }
-}
diff --git a/packages/calendar/src/extensions/bookings/index.ts b/packages/calendar/src/extensions/bookings/index.ts
deleted file mode 100644
index 4bfe7d2..0000000
--- a/packages/calendar/src/extensions/bookings/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export { BookingService } from './BookingService';
-export { BookingStore } from './BookingStore';
-export type { IBooking, BookingStatus, IBookingService } from '../../types/CalendarTypes';
-
-// DI registration helper
-import type { Builder } from '@novadi/core';
-import { IStore } from '../../storage/IStore';
-import { IEntityService } from '../../storage/IEntityService';
-import type { IBooking, ISync } from '../../types/CalendarTypes';
-import { BookingStore } from './BookingStore';
-import { BookingService } from './BookingService';
-
-export function registerBookings(builder: Builder): void {
- builder.registerType(BookingStore).as();
- builder.registerType(BookingService).as>();
- builder.registerType(BookingService).as>();
- builder.registerType(BookingService).as();
-}
diff --git a/packages/calendar/src/extensions/customers/CustomerService.ts b/packages/calendar/src/extensions/customers/CustomerService.ts
deleted file mode 100644
index d1225e1..0000000
--- a/packages/calendar/src/extensions/customers/CustomerService.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { ICustomer, EntityType, IEventBus } from '../../types/CalendarTypes';
-import { CustomerStore } from './CustomerStore';
-import { BaseEntityService } from '../../storage/BaseEntityService';
-import { IndexedDBContext } from '../../storage/IndexedDBContext';
-
-/**
- * CustomerService - CRUD operations for customers in IndexedDB
- */
-export class CustomerService extends BaseEntityService {
- readonly storeName = CustomerStore.STORE_NAME;
- readonly entityType: EntityType = 'Customer';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- /**
- * Search customers by name (case-insensitive contains)
- */
- async searchByName(query: string): Promise {
- const all = await this.getAll();
- const lowerQuery = query.toLowerCase();
- return all.filter(c => c.name.toLowerCase().includes(lowerQuery));
- }
-
- /**
- * Find customer by phone
- */
- async getByPhone(phone: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('phone');
- const request = index.get(phone);
-
- request.onsuccess = () => {
- const data = request.result;
- resolve(data ? (data as ICustomer) : null);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to find customer by phone ${phone}: ${request.error}`));
- };
- });
- }
-}
diff --git a/packages/calendar/src/extensions/customers/CustomerStore.ts b/packages/calendar/src/extensions/customers/CustomerStore.ts
deleted file mode 100644
index b53cd7e..0000000
--- a/packages/calendar/src/extensions/customers/CustomerStore.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { IStore } from '../../storage/IStore';
-
-/**
- * CustomerStore - IndexedDB ObjectStore definition for customers
- */
-export class CustomerStore implements IStore {
- static readonly STORE_NAME = 'customers';
- readonly storeName = CustomerStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- const store = db.createObjectStore(CustomerStore.STORE_NAME, { keyPath: 'id' });
-
- store.createIndex('name', 'name', { unique: false });
- store.createIndex('phone', 'phone', { unique: false });
- store.createIndex('syncStatus', 'syncStatus', { unique: false });
- }
-}
diff --git a/packages/calendar/src/extensions/customers/index.ts b/packages/calendar/src/extensions/customers/index.ts
deleted file mode 100644
index 6d47df5..0000000
--- a/packages/calendar/src/extensions/customers/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export { CustomerService } from './CustomerService';
-export { CustomerStore } from './CustomerStore';
-export type { ICustomer } from '../../types/CalendarTypes';
-
-// DI registration helper
-import type { Builder } from '@novadi/core';
-import { IStore } from '../../storage/IStore';
-import { IEntityService } from '../../storage/IEntityService';
-import type { ICustomer, ISync } from '../../types/CalendarTypes';
-import { CustomerStore } from './CustomerStore';
-import { CustomerService } from './CustomerService';
-
-export function registerCustomers(builder: Builder): void {
- builder.registerType(CustomerStore).as();
- builder.registerType(CustomerService).as>();
- builder.registerType(CustomerService).as>();
- builder.registerType(CustomerService).as();
-}
diff --git a/packages/calendar/src/extensions/departments/DepartmentRenderer.ts b/packages/calendar/src/extensions/departments/DepartmentRenderer.ts
deleted file mode 100644
index 16bb161..0000000
--- a/packages/calendar/src/extensions/departments/DepartmentRenderer.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BaseGroupingRenderer, IGroupingRendererConfig } from '../../core/BaseGroupingRenderer';
-import { DepartmentService } from './DepartmentService';
-import { IDepartment } from '../../types/CalendarTypes';
-
-export class DepartmentRenderer extends BaseGroupingRenderer {
- readonly type = 'department';
-
- protected readonly config: IGroupingRendererConfig = {
- elementTag: 'swp-department-header',
- idAttribute: 'departmentId',
- colspanVar: '--department-cols'
- };
-
- constructor(private departmentService: DepartmentService) {
- super();
- }
-
- protected getEntities(ids: string[]): Promise {
- return this.departmentService.getByIds(ids);
- }
-
- protected getDisplayName(entity: IDepartment): string {
- return entity.name;
- }
-}
diff --git a/packages/calendar/src/extensions/departments/DepartmentService.ts b/packages/calendar/src/extensions/departments/DepartmentService.ts
deleted file mode 100644
index 4b4c8b9..0000000
--- a/packages/calendar/src/extensions/departments/DepartmentService.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { IDepartment, EntityType, IEventBus } from '../../types/CalendarTypes';
-import { DepartmentStore } from './DepartmentStore';
-import { BaseEntityService } from '../../storage/BaseEntityService';
-import { IndexedDBContext } from '../../storage/IndexedDBContext';
-
-/**
- * DepartmentService - CRUD operations for departments in IndexedDB
- */
-export class DepartmentService extends BaseEntityService {
- readonly storeName = DepartmentStore.STORE_NAME;
- readonly entityType: EntityType = 'Department';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- /**
- * Get departments by IDs
- */
- async getByIds(ids: string[]): Promise {
- if (ids.length === 0) return [];
- const results = await Promise.all(ids.map(id => this.get(id)));
- return results.filter((d): d is IDepartment => d !== null);
- }
-}
diff --git a/packages/calendar/src/extensions/departments/DepartmentStore.ts b/packages/calendar/src/extensions/departments/DepartmentStore.ts
deleted file mode 100644
index 0e9c6a3..0000000
--- a/packages/calendar/src/extensions/departments/DepartmentStore.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { IStore } from '../../storage/IStore';
-
-/**
- * DepartmentStore - IndexedDB ObjectStore definition for departments
- */
-export class DepartmentStore implements IStore {
- static readonly STORE_NAME = 'departments';
- readonly storeName = DepartmentStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- db.createObjectStore(DepartmentStore.STORE_NAME, { keyPath: 'id' });
- }
-}
diff --git a/packages/calendar/src/extensions/departments/index.ts b/packages/calendar/src/extensions/departments/index.ts
deleted file mode 100644
index d50130a..0000000
--- a/packages/calendar/src/extensions/departments/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export { DepartmentRenderer } from './DepartmentRenderer';
-export { DepartmentService } from './DepartmentService';
-export { DepartmentStore } from './DepartmentStore';
-export type { IDepartment } from '../../types/CalendarTypes';
-
-// DI registration helper
-import type { Builder } from '@novadi/core';
-import { IRenderer } from '../../core/IGroupingRenderer';
-import { IStore } from '../../storage/IStore';
-import { IEntityService } from '../../storage/IEntityService';
-import type { IDepartment, ISync } from '../../types/CalendarTypes';
-import { DepartmentStore } from './DepartmentStore';
-import { DepartmentService } from './DepartmentService';
-import { DepartmentRenderer } from './DepartmentRenderer';
-
-export function registerDepartments(builder: Builder): void {
- builder.registerType(DepartmentStore).as();
- builder.registerType(DepartmentService).as>();
- builder.registerType(DepartmentService).as>();
- builder.registerType(DepartmentService).as();
- builder.registerType(DepartmentRenderer).as();
-}
diff --git a/packages/calendar/src/extensions/schedules/ResourceScheduleService.ts b/packages/calendar/src/extensions/schedules/ResourceScheduleService.ts
deleted file mode 100644
index a548865..0000000
--- a/packages/calendar/src/extensions/schedules/ResourceScheduleService.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { ITimeSlot } from '../../types/ScheduleTypes';
-import { ResourceService } from '../../storage/resources/ResourceService';
-import { ScheduleOverrideService } from './ScheduleOverrideService';
-import { DateService } from '../../core/DateService';
-
-/**
- * ResourceScheduleService - Get effective schedule for a resource on a date
- *
- * Logic:
- * 1. Check for override on this date
- * 2. Fall back to default schedule for the weekday
- */
-export class ResourceScheduleService {
- constructor(
- private resourceService: ResourceService,
- private overrideService: ScheduleOverrideService,
- private dateService: DateService
- ) {}
-
- /**
- * Get effective schedule for a resource on a specific date
- *
- * @param resourceId - Resource ID
- * @param date - Date string "YYYY-MM-DD"
- * @returns ITimeSlot or null (fri/closed)
- */
- async getScheduleForDate(resourceId: string, date: string): Promise {
- // 1. Check for override
- const override = await this.overrideService.getOverride(resourceId, date);
- if (override) {
- return override.schedule;
- }
-
- // 2. Use default schedule for weekday
- const resource = await this.resourceService.get(resourceId);
- if (!resource || !resource.defaultSchedule) {
- return null;
- }
-
- const weekDay = this.dateService.getISOWeekDay(date);
- return resource.defaultSchedule[weekDay] || null;
- }
-
- /**
- * Get schedules for multiple dates
- *
- * @param resourceId - Resource ID
- * @param dates - Array of date strings "YYYY-MM-DD"
- * @returns Map of date -> ITimeSlot | null
- */
- async getSchedulesForDates(resourceId: string, dates: string[]): Promise> {
- const result = new Map();
-
- // Get resource once
- const resource = await this.resourceService.get(resourceId);
-
- // Get all overrides in date range
- const overrides = dates.length > 0
- ? await this.overrideService.getByDateRange(resourceId, dates[0], dates[dates.length - 1])
- : [];
-
- // Build override map
- const overrideMap = new Map(overrides.map(o => [o.date, o.schedule]));
-
- // Resolve each date
- for (const date of dates) {
- // Check override first
- if (overrideMap.has(date)) {
- result.set(date, overrideMap.get(date)!);
- continue;
- }
-
- // Fall back to default
- if (resource?.defaultSchedule) {
- const weekDay = this.dateService.getISOWeekDay(date);
- result.set(date, resource.defaultSchedule[weekDay] || null);
- } else {
- result.set(date, null);
- }
- }
-
- return result;
- }
-}
diff --git a/packages/calendar/src/extensions/schedules/ScheduleOverrideService.ts b/packages/calendar/src/extensions/schedules/ScheduleOverrideService.ts
deleted file mode 100644
index 184e354..0000000
--- a/packages/calendar/src/extensions/schedules/ScheduleOverrideService.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { IScheduleOverride } from '../../types/ScheduleTypes';
-import { IndexedDBContext } from '../../storage/IndexedDBContext';
-import { ScheduleOverrideStore } from './ScheduleOverrideStore';
-
-/**
- * ScheduleOverrideService - CRUD for schedule overrides
- *
- * Provides access to date-specific schedule overrides for resources.
- */
-export class ScheduleOverrideService {
- private context: IndexedDBContext;
-
- constructor(context: IndexedDBContext) {
- this.context = context;
- }
-
- private get db(): IDBDatabase {
- return this.context.getDatabase();
- }
-
- /**
- * Get override for a specific resource and date
- */
- async getOverride(resourceId: string, date: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], 'readonly');
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const index = store.index('resourceId_date');
- const request = index.get([resourceId, date]);
-
- request.onsuccess = () => {
- resolve(request.result || null);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get override for ${resourceId} on ${date}: ${request.error}`));
- };
- });
- }
-
- /**
- * Get all overrides for a resource
- */
- async getByResource(resourceId: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], 'readonly');
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const index = store.index('resourceId');
- const request = index.getAll(resourceId);
-
- request.onsuccess = () => {
- resolve(request.result || []);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get overrides for ${resourceId}: ${request.error}`));
- };
- });
- }
-
- /**
- * Get overrides for a date range
- */
- async getByDateRange(resourceId: string, startDate: string, endDate: string): Promise {
- const all = await this.getByResource(resourceId);
- return all.filter(o => o.date >= startDate && o.date <= endDate);
- }
-
- /**
- * Save an override
- */
- async save(override: IScheduleOverride): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], 'readwrite');
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const request = store.put(override);
-
- request.onsuccess = () => resolve();
- request.onerror = () => {
- reject(new Error(`Failed to save override ${override.id}: ${request.error}`));
- };
- });
- }
-
- /**
- * Delete an override
- */
- async delete(id: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], 'readwrite');
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const request = store.delete(id);
-
- request.onsuccess = () => resolve();
- request.onerror = () => {
- reject(new Error(`Failed to delete override ${id}: ${request.error}`));
- };
- });
- }
-}
diff --git a/packages/calendar/src/extensions/schedules/ScheduleOverrideStore.ts b/packages/calendar/src/extensions/schedules/ScheduleOverrideStore.ts
deleted file mode 100644
index 0fe4f09..0000000
--- a/packages/calendar/src/extensions/schedules/ScheduleOverrideStore.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { IStore } from '../../storage/IStore';
-
-/**
- * ScheduleOverrideStore - IndexedDB ObjectStore for schedule overrides
- *
- * Stores date-specific schedule overrides for resources.
- * Indexes: resourceId, date, compound (resourceId + date)
- */
-export class ScheduleOverrideStore implements IStore {
- static readonly STORE_NAME = 'scheduleOverrides';
- readonly storeName = ScheduleOverrideStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- const store = db.createObjectStore(ScheduleOverrideStore.STORE_NAME, { keyPath: 'id' });
-
- store.createIndex('resourceId', 'resourceId', { unique: false });
- store.createIndex('date', 'date', { unique: false });
- store.createIndex('resourceId_date', ['resourceId', 'date'], { unique: true });
- store.createIndex('syncStatus', 'syncStatus', { unique: false });
- }
-}
diff --git a/packages/calendar/src/extensions/schedules/index.ts b/packages/calendar/src/extensions/schedules/index.ts
deleted file mode 100644
index e8a94f0..0000000
--- a/packages/calendar/src/extensions/schedules/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export { ScheduleOverrideService } from './ScheduleOverrideService';
-export { ScheduleOverrideStore } from './ScheduleOverrideStore';
-export { ResourceScheduleService } from './ResourceScheduleService';
-export type { IScheduleOverride, ITimeSlot, IWeekSchedule, WeekDay } from '../../types/ScheduleTypes';
-
-// DI registration helper
-import type { Builder } from '@novadi/core';
-import { IStore } from '../../storage/IStore';
-import { ScheduleOverrideStore } from './ScheduleOverrideStore';
-import { ScheduleOverrideService } from './ScheduleOverrideService';
-import { ResourceScheduleService } from './ResourceScheduleService';
-
-export function registerSchedules(builder: Builder): void {
- builder.registerType(ScheduleOverrideStore).as();
- builder.registerType(ScheduleOverrideService).as();
- builder.registerType(ResourceScheduleService).as();
-}
diff --git a/packages/calendar/src/extensions/teams/TeamRenderer.ts b/packages/calendar/src/extensions/teams/TeamRenderer.ts
deleted file mode 100644
index 090d8c9..0000000
--- a/packages/calendar/src/extensions/teams/TeamRenderer.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BaseGroupingRenderer, IGroupingRendererConfig } from '../../core/BaseGroupingRenderer';
-import { TeamService } from './TeamService';
-import { ITeam } from '../../types/CalendarTypes';
-
-export class TeamRenderer extends BaseGroupingRenderer {
- readonly type = 'team';
-
- protected readonly config: IGroupingRendererConfig = {
- elementTag: 'swp-team-header',
- idAttribute: 'teamId',
- colspanVar: '--team-cols'
- };
-
- constructor(private teamService: TeamService) {
- super();
- }
-
- protected getEntities(ids: string[]): Promise {
- return this.teamService.getByIds(ids);
- }
-
- protected getDisplayName(entity: ITeam): string {
- return entity.name;
- }
-}
diff --git a/packages/calendar/src/extensions/teams/TeamService.ts b/packages/calendar/src/extensions/teams/TeamService.ts
deleted file mode 100644
index 655dd1f..0000000
--- a/packages/calendar/src/extensions/teams/TeamService.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { ITeam, EntityType, IEventBus } from '../../types/CalendarTypes';
-import { TeamStore } from './TeamStore';
-import { BaseEntityService } from '../../storage/BaseEntityService';
-import { IndexedDBContext } from '../../storage/IndexedDBContext';
-
-/**
- * TeamService - CRUD operations for teams in IndexedDB
- *
- * Teams define which resources belong together for hierarchical grouping.
- * Extends BaseEntityService for standard entity operations.
- */
-export class TeamService extends BaseEntityService {
- readonly storeName = TeamStore.STORE_NAME;
- readonly entityType: EntityType = 'Team';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- /**
- * Get teams by IDs
- */
- async getByIds(ids: string[]): Promise {
- if (ids.length === 0) return [];
- const results = await Promise.all(ids.map(id => this.get(id)));
- return results.filter((t): t is ITeam => t !== null);
- }
-
- /**
- * Build reverse lookup: resourceId → teamId
- */
- async buildResourceToTeamMap(): Promise> {
- const teams = await this.getAll();
- const map: Record = {};
-
- for (const team of teams) {
- for (const resourceId of team.resourceIds) {
- map[resourceId] = team.id;
- }
- }
-
- return map;
- }
-}
diff --git a/packages/calendar/src/extensions/teams/TeamStore.ts b/packages/calendar/src/extensions/teams/TeamStore.ts
deleted file mode 100644
index af515e0..0000000
--- a/packages/calendar/src/extensions/teams/TeamStore.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { IStore } from '../../storage/IStore';
-
-/**
- * TeamStore - IndexedDB ObjectStore definition for teams
- */
-export class TeamStore implements IStore {
- static readonly STORE_NAME = 'teams';
- readonly storeName = TeamStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- db.createObjectStore(TeamStore.STORE_NAME, { keyPath: 'id' });
- }
-}
diff --git a/packages/calendar/src/extensions/teams/index.ts b/packages/calendar/src/extensions/teams/index.ts
deleted file mode 100644
index f9d231e..0000000
--- a/packages/calendar/src/extensions/teams/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export { TeamRenderer } from './TeamRenderer';
-export { TeamService } from './TeamService';
-export { TeamStore } from './TeamStore';
-export type { ITeam } from '../../types/CalendarTypes';
-
-// DI registration helper
-import type { Builder } from '@novadi/core';
-import { IRenderer } from '../../core/IGroupingRenderer';
-import { IStore } from '../../storage/IStore';
-import { IEntityService } from '../../storage/IEntityService';
-import type { ITeam, ISync } from '../../types/CalendarTypes';
-import { TeamStore } from './TeamStore';
-import { TeamService } from './TeamService';
-import { TeamRenderer } from './TeamRenderer';
-
-export function registerTeams(builder: Builder): void {
- builder.registerType(TeamStore).as();
- builder.registerType(TeamService).as>();
- builder.registerType(TeamService).as>();
- builder.registerType(TeamService).as();
- builder.registerType(TeamRenderer).as();
-}
diff --git a/packages/calendar/src/features/date/DateRenderer.ts b/packages/calendar/src/features/date/DateRenderer.ts
deleted file mode 100644
index 4f1cfad..0000000
--- a/packages/calendar/src/features/date/DateRenderer.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { IRenderer, IRenderContext } from '../../core/IGroupingRenderer';
-import { DateService } from '../../core/DateService';
-
-export class DateRenderer implements IRenderer {
- readonly type = 'date';
-
- constructor(private dateService: DateService) {}
-
- render(context: IRenderContext): void {
- const dates = context.filter['date'] || [];
- const resourceIds = context.filter['resource'] || [];
-
- // Check if date headers should be hidden (e.g., in day view)
- const dateGrouping = context.groupings?.find(g => g.type === 'date');
- const hideHeader = dateGrouping?.hideHeader === true;
-
- // Render dates for HVER resource (eller 1 gang hvis ingen resources)
- const iterations = resourceIds.length || 1;
- let columnCount = 0;
-
- for (let r = 0; r < iterations; r++) {
- const resourceId = resourceIds[r]; // undefined hvis ingen resources
-
- for (const dateStr of dates) {
- const date = this.dateService.parseISO(dateStr);
-
- // Build columnKey for uniform identification
- const segments: Record = { date: dateStr };
- if (resourceId) segments.resource = resourceId;
- const columnKey = this.dateService.buildColumnKey(segments);
-
- // Header
- const header = document.createElement('swp-day-header');
- header.dataset.date = dateStr;
- header.dataset.columnKey = columnKey;
- if (resourceId) {
- header.dataset.resourceId = resourceId;
- }
- if (hideHeader) {
- header.dataset.hidden = 'true';
- }
- header.innerHTML = `
- ${this.dateService.getDayName(date, 'short')}
- ${date.getDate()}
- `;
- context.headerContainer.appendChild(header);
-
- // Column
- const column = document.createElement('swp-day-column');
- column.dataset.date = dateStr;
- column.dataset.columnKey = columnKey;
- if (resourceId) {
- column.dataset.resourceId = resourceId;
- }
- column.innerHTML = ' ';
- context.columnContainer.appendChild(column);
-
- columnCount++;
- }
- }
-
- // Set grid columns on container
- const container = context.columnContainer.closest('swp-calendar-container');
- if (container) {
- (container as HTMLElement).style.setProperty('--grid-columns', String(columnCount));
- }
- }
-}
diff --git a/packages/calendar/src/features/date/index.ts b/packages/calendar/src/features/date/index.ts
deleted file mode 100644
index 7bf37b3..0000000
--- a/packages/calendar/src/features/date/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { DateRenderer } from './DateRenderer';
diff --git a/packages/calendar/src/features/event/EventLayoutEngine.ts b/packages/calendar/src/features/event/EventLayoutEngine.ts
deleted file mode 100644
index 0b10905..0000000
--- a/packages/calendar/src/features/event/EventLayoutEngine.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-/**
- * EventLayoutEngine - Simplified stacking/grouping algorithm
- *
- * Supports two layout modes:
- * - GRID: Events starting at same time rendered side-by-side
- * - STACKING: Overlapping events with margin-left offset (15px per level)
- *
- * No prev/next chains, single-pass greedy algorithm
- */
-
-import { ICalendarEvent } from '../../types/CalendarTypes';
-import { IGridConfig } from '../../core/IGridConfig';
-import { calculateEventPosition } from '../../utils/PositionUtils';
-import { IColumnLayout, IGridGroupLayout, IStackedEventLayout } from './EventLayoutTypes';
-
-/**
- * Check if two events overlap (strict - touching at boundary = NOT overlapping)
- * This matches Scenario 8: end===start is NOT overlap
- */
-export function eventsOverlap(a: ICalendarEvent, b: ICalendarEvent): boolean {
- return a.start < b.end && a.end > b.start;
-}
-
-/**
- * Check if two events are within threshold for grid grouping.
- * This includes:
- * 1. Start-to-start: Events start within threshold of each other
- * 2. End-to-start: One event starts within threshold before another ends
- */
-function eventsWithinThreshold(a: ICalendarEvent, b: ICalendarEvent, thresholdMinutes: number): boolean {
- const thresholdMs = thresholdMinutes * 60 * 1000;
-
- // Start-to-start: both events start within threshold
- const startToStartDiff = Math.abs(a.start.getTime() - b.start.getTime());
- if (startToStartDiff <= thresholdMs) return true;
-
- // End-to-start: one event starts within threshold before the other ends
- // B starts within threshold before A ends
- const bStartsBeforeAEnds = a.end.getTime() - b.start.getTime();
- if (bStartsBeforeAEnds > 0 && bStartsBeforeAEnds <= thresholdMs) return true;
-
- // A starts within threshold before B ends
- const aStartsBeforeBEnds = b.end.getTime() - a.start.getTime();
- if (aStartsBeforeBEnds > 0 && aStartsBeforeBEnds <= thresholdMs) return true;
-
- return false;
-}
-
-/**
- * Check if all events in a group start within threshold of each other
- */
-function allStartWithinThreshold(events: ICalendarEvent[], thresholdMinutes: number): boolean {
- if (events.length <= 1) return true;
-
- // Find earliest and latest start times
- let earliest = events[0].start.getTime();
- let latest = events[0].start.getTime();
-
- for (const event of events) {
- const time = event.start.getTime();
- if (time < earliest) earliest = time;
- if (time > latest) latest = time;
- }
-
- const diffMinutes = (latest - earliest) / (1000 * 60);
- return diffMinutes <= thresholdMinutes;
-}
-
-/**
- * Find groups of overlapping events (connected by overlap chain)
- * Events are grouped if they overlap with any event in the group
- */
-function findOverlapGroups(events: ICalendarEvent[]): ICalendarEvent[][] {
- if (events.length === 0) return [];
-
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- const used = new Set();
- const groups: ICalendarEvent[][] = [];
-
- for (const event of sorted) {
- if (used.has(event.id)) continue;
-
- // Start a new group with this event
- const group: ICalendarEvent[] = [event];
- used.add(event.id);
-
- // Expand group by finding all connected events (via overlap)
- let expanded = true;
- while (expanded) {
- expanded = false;
- for (const candidate of sorted) {
- if (used.has(candidate.id)) continue;
-
- // Check if candidate overlaps with any event in group
- const connects = group.some(member => eventsOverlap(member, candidate));
-
- if (connects) {
- group.push(candidate);
- used.add(candidate.id);
- expanded = true;
- }
- }
- }
-
- groups.push(group);
- }
-
- return groups;
-}
-
-/**
- * Find grid candidates within a group - events connected via threshold chain
- * Uses V1 logic: events are connected if within threshold (no overlap requirement)
- */
-function findGridCandidates(
- events: ICalendarEvent[],
- thresholdMinutes: number
-): ICalendarEvent[][] {
- if (events.length === 0) return [];
-
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- const used = new Set();
- const groups: ICalendarEvent[][] = [];
-
- for (const event of sorted) {
- if (used.has(event.id)) continue;
-
- const group: ICalendarEvent[] = [event];
- used.add(event.id);
-
- // Expand by threshold chain (V1 logic: no overlap requirement, just threshold)
- let expanded = true;
- while (expanded) {
- expanded = false;
- for (const candidate of sorted) {
- if (used.has(candidate.id)) continue;
-
- const connects = group.some(member =>
- eventsWithinThreshold(member, candidate, thresholdMinutes)
- );
-
- if (connects) {
- group.push(candidate);
- used.add(candidate.id);
- expanded = true;
- }
- }
- }
-
- groups.push(group);
- }
-
- return groups;
-}
-
-/**
- * Calculate stack levels for overlapping events using greedy algorithm
- * For each event: level = max(overlapping already-processed events) + 1
- */
-function calculateStackLevels(events: ICalendarEvent[]): Map {
- const levels = new Map();
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
-
- for (const event of sorted) {
- let maxOverlappingLevel = -1;
-
- // Find max level among overlapping events already processed
- for (const [id, level] of levels) {
- const other = events.find(e => e.id === id);
- if (other && eventsOverlap(event, other)) {
- maxOverlappingLevel = Math.max(maxOverlappingLevel, level);
- }
- }
-
- levels.set(event.id, maxOverlappingLevel + 1);
- }
-
- return levels;
-}
-
-/**
- * Allocate events to columns for GRID layout using greedy algorithm
- * Non-overlapping events can share a column to minimize total columns
- */
-function allocateColumns(events: ICalendarEvent[]): ICalendarEvent[][] {
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- const columns: ICalendarEvent[][] = [];
-
- for (const event of sorted) {
- // Find first column where event doesn't overlap with existing events
- let placed = false;
- for (const column of columns) {
- const canFit = !column.some(e => eventsOverlap(event, e));
- if (canFit) {
- column.push(event);
- placed = true;
- break;
- }
- }
-
- // No suitable column found, create new one
- if (!placed) {
- columns.push([event]);
- }
- }
-
- return columns;
-}
-
-/**
- * Main entry point: Calculate complete layout for a column's events
- *
- * Algorithm:
- * 1. Find overlap groups (events connected by overlap chain)
- * 2. For each overlap group, find grid candidates (events within threshold chain)
- * 3. If all events in overlap group form a single grid candidate → GRID mode
- * 4. Otherwise → STACKING mode with calculated levels
- */
-export function calculateColumnLayout(
- events: ICalendarEvent[],
- config: IGridConfig
-): IColumnLayout {
- const thresholdMinutes = config.gridStartThresholdMinutes ?? 10;
-
- const result: IColumnLayout = {
- grids: [],
- stacked: []
- };
-
- if (events.length === 0) return result;
-
- // Find all overlapping event groups
- const overlapGroups = findOverlapGroups(events);
-
- for (const overlapGroup of overlapGroups) {
- if (overlapGroup.length === 1) {
- // Single event - no grouping needed
- result.stacked.push({
- event: overlapGroup[0],
- stackLevel: 0
- });
- continue;
- }
-
- // Within this overlap group, find grid candidates (threshold-connected subgroups)
- const gridSubgroups = findGridCandidates(overlapGroup, thresholdMinutes);
-
- // Check if the ENTIRE overlap group forms a single grid candidate
- // This happens when all events are connected via threshold chain
- const largestGridCandidate = gridSubgroups.reduce((max, g) =>
- g.length > max.length ? g : max, gridSubgroups[0]);
-
- if (largestGridCandidate.length === overlapGroup.length) {
- // All events in overlap group are connected via threshold chain → GRID mode
- const columns = allocateColumns(overlapGroup);
- const earliest = overlapGroup.reduce((min, e) =>
- e.start < min.start ? e : min, overlapGroup[0]);
- const position = calculateEventPosition(earliest.start, earliest.end, config);
-
- result.grids.push({
- events: overlapGroup,
- columns,
- stackLevel: 0,
- position: { top: position.top }
- });
- } else {
- // Not all events connected via threshold → STACKING mode
- const levels = calculateStackLevels(overlapGroup);
- for (const event of overlapGroup) {
- result.stacked.push({
- event,
- stackLevel: levels.get(event.id) ?? 0
- });
- }
- }
- }
-
- return result;
-}
diff --git a/packages/calendar/src/features/event/EventLayoutTypes.ts b/packages/calendar/src/features/event/EventLayoutTypes.ts
deleted file mode 100644
index c887eaf..0000000
--- a/packages/calendar/src/features/event/EventLayoutTypes.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ICalendarEvent } from '../../types/CalendarTypes';
-
-/**
- * Stack link metadata stored on event elements
- * Simplified from V1: No prev/next chains - only stackLevel needed for rendering
- */
-export interface IStackLink {
- stackLevel: number;
-}
-
-/**
- * Layout result for a stacked event (overlapping events with margin offset)
- */
-export interface IStackedEventLayout {
- event: ICalendarEvent;
- stackLevel: number;
-}
-
-/**
- * Layout result for a grid group (simultaneous events side-by-side)
- */
-export interface IGridGroupLayout {
- events: ICalendarEvent[];
- columns: ICalendarEvent[][]; // Events grouped by column (non-overlapping within column)
- stackLevel: number; // Stack level for entire group (if nested in another event)
- position: { top: number }; // Top position of earliest event in pixels
-}
-
-/**
- * Complete layout result for a column's events
- */
-export interface IColumnLayout {
- grids: IGridGroupLayout[];
- stacked: IStackedEventLayout[];
-}
diff --git a/packages/calendar/src/features/event/EventRenderer.ts b/packages/calendar/src/features/event/EventRenderer.ts
deleted file mode 100644
index 6cc1b96..0000000
--- a/packages/calendar/src/features/event/EventRenderer.ts
+++ /dev/null
@@ -1,434 +0,0 @@
-import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../../types/CalendarTypes';
-import { EventService } from '../../storage/events/EventService';
-import { DateService } from '../../core/DateService';
-import { IGridConfig } from '../../core/IGridConfig';
-import { calculateEventPosition, snapToGrid, pixelsToMinutes } from '../../utils/PositionUtils';
-import { CoreEvents } from '../../constants/CoreEvents';
-import { IDragColumnChangePayload, IDragMovePayload, IDragEndPayload, IDragLeaveHeaderPayload } from '../../types/DragTypes';
-import { calculateColumnLayout } from './EventLayoutEngine';
-import { IGridGroupLayout } from './EventLayoutTypes';
-import { FilterTemplate } from '../../core/FilterTemplate';
-
-/**
- * EventRenderer - Renders calendar events to the DOM
- *
- * CLEAN approach:
- * - Only data-id attribute on event element
- * - innerHTML contains only visible content
- * - Event data retrieved via EventService when needed
- */
-export class EventRenderer {
- private container: HTMLElement | null = null;
-
- constructor(
- private eventService: EventService,
- private dateService: DateService,
- private gridConfig: IGridConfig,
- private eventBus: IEventBus
- ) {
- this.setupListeners();
- }
-
- /**
- * Setup listeners for drag-drop and update events
- */
- private setupListeners(): void {
- this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleColumnChange(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE, (e) => {
- const payload = (e as CustomEvent).detail;
- this.updateDragTimestamp(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_UPDATED, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleEventUpdated(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleDragEnd(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleDragLeaveHeader(payload);
- });
- }
-
- /**
- * Handle EVENT_DRAG_END - remove element if dropped in header
- */
- private handleDragEnd(payload: IDragEndPayload): void {
- if (payload.target === 'header') {
- // Event was dropped in header drawer - remove from grid
- const element = this.container?.querySelector(`swp-content-viewport swp-event[data-event-id="${payload.swpEvent.eventId}"]`);
- element?.remove();
- }
- }
-
- /**
- * Handle header item leaving header - create swp-event in grid
- */
- private handleDragLeaveHeader(payload: IDragLeaveHeaderPayload): void {
- // Only handle when source is header (header item dragged to grid)
- if (payload.source !== 'header') return;
- if (!payload.targetColumn || !payload.start || !payload.end) return;
-
- // Turn header item into ghost (stays visible but faded)
- if (payload.element) {
- payload.element.classList.add('drag-ghost');
- payload.element.style.opacity = '0.3';
- payload.element.style.pointerEvents = 'none';
- }
-
- // Create event object from header item data
- const event: ICalendarEvent = {
- id: payload.eventId,
- title: payload.title || '',
- description: '',
- start: payload.start,
- end: payload.end,
- type: 'customer',
- allDay: false,
- syncStatus: 'pending'
- };
-
- // Create swp-event element using existing method
- const element = this.createEventElement(event);
-
- // Add to target column
- let eventsLayer = payload.targetColumn.querySelector('swp-events-layer');
- if (!eventsLayer) {
- eventsLayer = document.createElement('swp-events-layer');
- payload.targetColumn.appendChild(eventsLayer);
- }
- eventsLayer.appendChild(element);
-
- // Mark as dragging so DragDropManager can continue with it
- element.classList.add('dragging');
- }
-
- /**
- * Handle EVENT_UPDATED - re-render affected columns
- */
- private async handleEventUpdated(payload: IEventUpdatedPayload): Promise {
- // Re-render source column (if different from target)
- if (payload.sourceColumnKey !== payload.targetColumnKey) {
- await this.rerenderColumn(payload.sourceColumnKey);
- }
-
- // Re-render target column
- await this.rerenderColumn(payload.targetColumnKey);
- }
-
- /**
- * Re-render a single column with fresh data from IndexedDB
- */
- private async rerenderColumn(columnKey: string): Promise {
- const column = this.findColumn(columnKey);
- if (!column) return;
-
- // Read date and resourceId directly from column attributes (columnKey is opaque)
- const date = column.dataset.date;
- const resourceId = column.dataset.resourceId;
-
- if (!date) return;
-
- // Get date range for this day
- const startDate = new Date(date);
- const endDate = new Date(date);
- endDate.setHours(23, 59, 59, 999);
-
- // Fetch events from IndexedDB
- const events = resourceId
- ? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate)
- : await this.eventService.getByDateRange(startDate, endDate);
-
- // Filter to timed events and match date exactly
- const timedEvents = events.filter(event =>
- !event.allDay && this.dateService.getDateKey(event.start) === date
- );
-
- // Get or create events layer
- let eventsLayer = column.querySelector('swp-events-layer');
- if (!eventsLayer) {
- eventsLayer = document.createElement('swp-events-layer');
- column.appendChild(eventsLayer);
- }
-
- // Clear existing events
- eventsLayer.innerHTML = '';
-
- // Calculate layout with stacking/grouping
- const layout = calculateColumnLayout(timedEvents, this.gridConfig);
-
- // Render GRID groups
- layout.grids.forEach(grid => {
- const groupEl = this.renderGridGroup(grid);
- eventsLayer!.appendChild(groupEl);
- });
-
- // Render STACKED events
- layout.stacked.forEach(item => {
- const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
- eventsLayer!.appendChild(eventEl);
- });
- }
-
- /**
- * Find a column element by columnKey
- */
- private findColumn(columnKey: string): HTMLElement | null {
- if (!this.container) return null;
- return this.container.querySelector(`swp-day-column[data-column-key="${columnKey}"]`) as HTMLElement;
- }
-
- /**
- * Handle event moving to a new column during drag
- */
- private handleColumnChange(payload: IDragColumnChangePayload): void {
- const eventsLayer = payload.newColumn.querySelector('swp-events-layer');
- if (!eventsLayer) return;
-
- // Move element to new column
- eventsLayer.appendChild(payload.element);
-
- // Preserve Y position
- payload.element.style.top = `${payload.currentY}px`;
- }
-
- /**
- * Update timestamp display during drag (snapped to grid)
- */
- private updateDragTimestamp(payload: IDragMovePayload): void {
- const timeEl = payload.element.querySelector('swp-event-time');
- if (!timeEl) return;
-
- // Snap position to grid interval
- const snappedY = snapToGrid(payload.currentY, this.gridConfig);
-
- // Calculate new start time
- const minutesFromGridStart = pixelsToMinutes(snappedY, this.gridConfig);
- const startMinutes = (this.gridConfig.dayStartHour * 60) + minutesFromGridStart;
-
- // Keep original duration (from element height)
- const height = parseFloat(payload.element.style.height) || this.gridConfig.hourHeight;
- const durationMinutes = pixelsToMinutes(height, this.gridConfig);
-
- // Create Date objects for consistent formatting via DateService
- const start = this.minutesToDate(startMinutes);
- const end = this.minutesToDate(startMinutes + durationMinutes);
-
- timeEl.textContent = this.dateService.formatTimeRange(start, end);
- }
-
- /**
- * Convert minutes since midnight to a Date object (today)
- */
- private minutesToDate(minutes: number): Date {
- const date = new Date();
- date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0);
- return date;
- }
-
- /**
- * Render events for visible dates into day columns
- * @param container - Calendar container element
- * @param filter - Filter with 'date' and optionally 'resource' arrays
- * @param filterTemplate - Template for matching events to columns
- */
- async render(container: HTMLElement, filter: Record, filterTemplate: FilterTemplate): Promise {
- // Store container reference for later re-renders
- this.container = container;
-
- const visibleDates = filter['date'] || [];
-
- if (visibleDates.length === 0) return;
-
- // Get date range for query
- const startDate = new Date(visibleDates[0]);
- const endDate = new Date(visibleDates[visibleDates.length - 1]);
- endDate.setHours(23, 59, 59, 999);
-
- // Fetch events from IndexedDB
- const events = await this.eventService.getByDateRange(startDate, endDate);
-
- // Find day columns
- const dayColumns = container.querySelector('swp-day-columns');
- if (!dayColumns) return;
-
- const columns = dayColumns.querySelectorAll('swp-day-column');
-
- // Render events into each column based on FilterTemplate matching
- columns.forEach(column => {
- const columnEl = column as HTMLElement;
-
- // Use FilterTemplate for matching - only fields in template are checked
- const columnEvents = events.filter(event => filterTemplate.matches(event, columnEl));
-
- // Get or create events layer
- let eventsLayer = column.querySelector('swp-events-layer');
- if (!eventsLayer) {
- eventsLayer = document.createElement('swp-events-layer');
- column.appendChild(eventsLayer);
- }
-
- // Clear existing events
- eventsLayer.innerHTML = '';
-
- // Filter to timed events only
- const timedEvents = columnEvents.filter(event => !event.allDay);
-
- // Calculate layout with stacking/grouping
- const layout = calculateColumnLayout(timedEvents, this.gridConfig);
-
- // Render GRID groups (simultaneous events side-by-side)
- layout.grids.forEach(grid => {
- const groupEl = this.renderGridGroup(grid);
- eventsLayer!.appendChild(groupEl);
- });
-
- // Render STACKED events (overlapping with margin offset)
- layout.stacked.forEach(item => {
- const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
- eventsLayer!.appendChild(eventEl);
- });
- });
- }
-
- /**
- * Create a single event element
- *
- * CLEAN approach:
- * - Only data-id for lookup
- * - Visible content in innerHTML only
- */
- private createEventElement(event: ICalendarEvent): HTMLElement {
- const element = document.createElement('swp-event');
-
- // Data attributes for SwpEvent compatibility
- element.dataset.eventId = event.id;
- if (event.resourceId) {
- element.dataset.resourceId = event.resourceId;
- }
-
- // Calculate position
- const position = calculateEventPosition(event.start, event.end, this.gridConfig);
- element.style.top = `${position.top}px`;
- element.style.height = `${position.height}px`;
-
- // Color class based on event type
- const colorClass = this.getColorClass(event);
- if (colorClass) {
- element.classList.add(colorClass);
- }
-
- // Visible content only
- element.innerHTML = `
- ${this.dateService.formatTimeRange(event.start, event.end)}
- ${this.escapeHtml(event.title)}
- ${event.description ? `${this.escapeHtml(event.description)} ` : ''}
- `;
-
- return element;
- }
-
- /**
- * Get color class based on metadata.color or event type
- */
- private getColorClass(event: ICalendarEvent): string {
- // Check metadata.color first
- if (event.metadata?.color) {
- return `is-${event.metadata.color}`;
- }
-
- // Fallback to type-based color
- const typeColors: Record = {
- 'customer': 'is-blue',
- 'vacation': 'is-green',
- 'break': 'is-amber',
- 'meeting': 'is-purple',
- 'blocked': 'is-red'
- };
- return typeColors[event.type] || 'is-blue';
- }
-
- /**
- * Escape HTML to prevent XSS
- */
- private escapeHtml(text: string): string {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
-
- /**
- * Render a GRID group with side-by-side columns
- * Used when multiple events start at the same time
- */
- private renderGridGroup(layout: IGridGroupLayout): HTMLElement {
- const group = document.createElement('swp-event-group');
- group.classList.add(`cols-${layout.columns.length}`);
- group.style.top = `${layout.position.top}px`;
-
- // Stack level styling for entire group (if nested in another event)
- if (layout.stackLevel > 0) {
- group.style.marginLeft = `${layout.stackLevel * 15}px`;
- group.style.zIndex = `${100 + layout.stackLevel}`;
- }
-
- // Calculate the height needed for the group (tallest event)
- let maxBottom = 0;
- for (const event of layout.events) {
- const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
- const eventBottom = pos.top + pos.height;
- if (eventBottom > maxBottom) maxBottom = eventBottom;
- }
- const groupHeight = maxBottom - layout.position.top;
- group.style.height = `${groupHeight}px`;
-
- // Create wrapper div for each column
- layout.columns.forEach(columnEvents => {
- const wrapper = document.createElement('div');
- wrapper.style.position = 'relative';
-
- columnEvents.forEach(event => {
- const eventEl = this.createEventElement(event);
- // Position relative to group top
- const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
- eventEl.style.top = `${pos.top - layout.position.top}px`;
- eventEl.style.position = 'absolute';
- eventEl.style.left = '0';
- eventEl.style.right = '0';
- wrapper.appendChild(eventEl);
- });
-
- group.appendChild(wrapper);
- });
-
- return group;
- }
-
- /**
- * Render a STACKED event with margin-left offset
- * Used for overlapping events that don't start at the same time
- */
- private renderStackedEvent(event: ICalendarEvent, stackLevel: number): HTMLElement {
- const element = this.createEventElement(event);
-
- // Add stack metadata for drag-drop and other features
- element.dataset.stackLink = JSON.stringify({ stackLevel });
-
- // Visual styling based on stack level
- if (stackLevel > 0) {
- element.style.marginLeft = `${stackLevel * 15}px`;
- element.style.zIndex = `${100 + stackLevel}`;
- }
-
- return element;
- }
-}
diff --git a/packages/calendar/src/features/event/index.ts b/packages/calendar/src/features/event/index.ts
deleted file mode 100644
index 7b8f118..0000000
--- a/packages/calendar/src/features/event/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { EventRenderer } from './EventRenderer';
diff --git a/packages/calendar/src/features/headerdrawer/HeaderDrawerLayoutEngine.ts b/packages/calendar/src/features/headerdrawer/HeaderDrawerLayoutEngine.ts
deleted file mode 100644
index b407a58..0000000
--- a/packages/calendar/src/features/headerdrawer/HeaderDrawerLayoutEngine.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * HeaderDrawerLayoutEngine - Calculates row placement for header items
- *
- * Prevents visual overlap by assigning items to different rows when
- * they occupy the same columns. Uses a track-based algorithm similar
- * to V1's AllDayLayoutEngine.
- *
- * Each row can hold multiple items as long as they don't overlap in columns.
- * When an item spans columns that are already occupied, it's placed in the
- * next available row.
- */
-
-export interface IHeaderItemLayout {
- itemId: string;
- gridArea: string; // "row / col-start / row+1 / col-end"
- startColumn: number;
- endColumn: number;
- row: number;
-}
-
-export interface IHeaderItemInput {
- id: string;
- columnStart: number; // 0-based column index
- columnEnd: number; // 0-based end column (inclusive)
-}
-
-export class HeaderDrawerLayoutEngine {
- private tracks: boolean[][] = [];
- private columnCount: number;
-
- constructor(columnCount: number) {
- this.columnCount = columnCount;
- this.reset();
- }
-
- /**
- * Reset tracks for new layout calculation
- */
- reset(): void {
- this.tracks = [new Array(this.columnCount).fill(false)];
- }
-
- /**
- * Calculate layout for all items
- * Items should be sorted by start column for optimal packing
- */
- calculateLayout(items: IHeaderItemInput[]): IHeaderItemLayout[] {
- this.reset();
- const layouts: IHeaderItemLayout[] = [];
-
- for (const item of items) {
- const row = this.findAvailableRow(item.columnStart, item.columnEnd);
-
- // Mark columns as occupied in this row
- for (let col = item.columnStart; col <= item.columnEnd; col++) {
- this.tracks[row][col] = true;
- }
-
- // gridArea format: "row / col-start / row+1 / col-end"
- // CSS grid uses 1-based indices
- layouts.push({
- itemId: item.id,
- gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`,
- startColumn: item.columnStart,
- endColumn: item.columnEnd,
- row: row + 1 // 1-based for CSS
- });
- }
-
- return layouts;
- }
-
- /**
- * Calculate layout for a single new item
- * Useful for real-time drag operations
- */
- calculateSingleLayout(item: IHeaderItemInput): IHeaderItemLayout {
- const row = this.findAvailableRow(item.columnStart, item.columnEnd);
-
- // Mark columns as occupied
- for (let col = item.columnStart; col <= item.columnEnd; col++) {
- this.tracks[row][col] = true;
- }
-
- return {
- itemId: item.id,
- gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`,
- startColumn: item.columnStart,
- endColumn: item.columnEnd,
- row: row + 1
- };
- }
-
- /**
- * Find the first row where all columns in range are available
- */
- private findAvailableRow(startCol: number, endCol: number): number {
- for (let row = 0; row < this.tracks.length; row++) {
- if (this.isRowAvailable(row, startCol, endCol)) {
- return row;
- }
- }
-
- // Add new row if all existing rows are occupied
- this.tracks.push(new Array(this.columnCount).fill(false));
- return this.tracks.length - 1;
- }
-
- /**
- * Check if columns in range are all available in given row
- */
- private isRowAvailable(row: number, startCol: number, endCol: number): boolean {
- for (let col = startCol; col <= endCol; col++) {
- if (this.tracks[row][col]) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Get the number of rows currently in use
- */
- getRowCount(): number {
- return this.tracks.length;
- }
-
- /**
- * Update column count (e.g., when view changes)
- */
- setColumnCount(count: number): void {
- this.columnCount = count;
- this.reset();
- }
-}
diff --git a/packages/calendar/src/features/headerdrawer/HeaderDrawerRenderer.ts b/packages/calendar/src/features/headerdrawer/HeaderDrawerRenderer.ts
deleted file mode 100644
index 1b528cd..0000000
--- a/packages/calendar/src/features/headerdrawer/HeaderDrawerRenderer.ts
+++ /dev/null
@@ -1,419 +0,0 @@
-import { IEventBus, ICalendarEvent } from '../../types/CalendarTypes';
-import { IGridConfig } from '../../core/IGridConfig';
-import { CoreEvents } from '../../constants/CoreEvents';
-import { HeaderDrawerManager } from '../../core/HeaderDrawerManager';
-import { EventService } from '../../storage/events/EventService';
-import { DateService } from '../../core/DateService';
-import { FilterTemplate } from '../../core/FilterTemplate';
-import {
- IDragEnterHeaderPayload,
- IDragMoveHeaderPayload,
- IDragLeaveHeaderPayload,
- IDragEndPayload
-} from '../../types/DragTypes';
-
-/**
- * Layout information for a header item
- */
-interface IHeaderItemLayout {
- event: ICalendarEvent;
- columnKey: string; // Opaque column identifier
- row: number; // 1-indexed
- colStart: number; // 1-indexed
- colEnd: number; // exclusive
-}
-
-/**
- * HeaderDrawerRenderer - Handles rendering of items in the header drawer
- *
- * Listens to drag events from DragDropManager and creates/manages
- * swp-header-item elements in the header drawer.
- *
- * Uses subgrid for column alignment with parent swp-calendar-header.
- * Position items via gridArea for explicit row/column placement.
- */
-export class HeaderDrawerRenderer {
- private currentItem: HTMLElement | null = null;
- private container: HTMLElement | null = null;
- private sourceElement: HTMLElement | null = null;
- private wasExpandedBeforeDrag = false;
- private filterTemplate: FilterTemplate | null = null;
-
- constructor(
- private eventBus: IEventBus,
- private gridConfig: IGridConfig,
- private headerDrawerManager: HeaderDrawerManager,
- private eventService: EventService,
- private dateService: DateService
- ) {
- this.setupListeners();
- }
-
- /**
- * Render allDay events into the header drawer with row stacking
- * @param filterTemplate - Template for matching events to columns
- */
- async render(container: HTMLElement, filter: Record, filterTemplate: FilterTemplate): Promise {
- // Store filterTemplate for buildColumnKeyFromEvent
- this.filterTemplate = filterTemplate;
-
- const drawer = container.querySelector('swp-header-drawer');
- if (!drawer) return;
-
- const visibleDates = filter['date'] || [];
- if (visibleDates.length === 0) return;
-
- // Get column keys from DOM for correct multi-resource positioning
- const visibleColumnKeys = this.getVisibleColumnKeysFromDOM();
- if (visibleColumnKeys.length === 0) return;
-
- // Fetch events for date range
- const startDate = new Date(visibleDates[0]);
- const endDate = new Date(visibleDates[visibleDates.length - 1]);
- endDate.setHours(23, 59, 59, 999);
-
- const events = await this.eventService.getByDateRange(startDate, endDate);
-
- // Filter to allDay events only (allDay !== false)
- const allDayEvents = events.filter(event => event.allDay !== false);
-
- // Clear existing items
- drawer.innerHTML = '';
-
- if (allDayEvents.length === 0) return;
-
- // Calculate layout with row stacking using columnKeys
- const layouts = this.calculateLayout(allDayEvents, visibleColumnKeys);
- const rowCount = Math.max(1, ...layouts.map(l => l.row));
-
- // Render each item with layout
- layouts.forEach(layout => {
- const item = this.createHeaderItem(layout);
- drawer.appendChild(item);
- });
-
- // Expand drawer to fit all rows
- this.headerDrawerManager.expandToRows(rowCount);
- }
-
- /**
- * Create a header item element from layout
- */
- private createHeaderItem(layout: IHeaderItemLayout): HTMLElement {
- const { event, columnKey, row, colStart, colEnd } = layout;
-
- const item = document.createElement('swp-header-item');
- item.dataset.eventId = event.id;
- item.dataset.itemType = 'event';
- item.dataset.start = event.start.toISOString();
- item.dataset.end = event.end.toISOString();
- item.dataset.columnKey = columnKey;
- item.textContent = event.title;
-
- // Color class
- const colorClass = this.getColorClass(event);
- if (colorClass) item.classList.add(colorClass);
-
- // Grid position from layout
- item.style.gridArea = `${row} / ${colStart} / ${row + 1} / ${colEnd}`;
-
- return item;
- }
-
- /**
- * Calculate layout for all events with row stacking
- * Uses track-based algorithm to find available rows for overlapping events
- */
- private calculateLayout(events: ICalendarEvent[], visibleColumnKeys: string[]): IHeaderItemLayout[] {
- // tracks[row][col] = occupied
- const tracks: boolean[][] = [new Array(visibleColumnKeys.length).fill(false)];
- const layouts: IHeaderItemLayout[] = [];
-
- for (const event of events) {
- // Build columnKey from event fields (only place we need to construct it)
- const columnKey = this.buildColumnKeyFromEvent(event);
- const startCol = visibleColumnKeys.indexOf(columnKey);
- const endColumnKey = this.buildColumnKeyFromEvent(event, event.end);
- const endCol = visibleColumnKeys.indexOf(endColumnKey);
- if (startCol === -1 && endCol === -1) continue;
-
- // Clamp til synlige kolonner
- const colStart = Math.max(0, startCol);
- const colEnd = (endCol !== -1 ? endCol : visibleColumnKeys.length - 1) + 1;
-
- // Find ledig række
- const row = this.findAvailableRow(tracks, colStart, colEnd);
-
- // Marker som optaget
- for (let c = colStart; c < colEnd; c++) {
- tracks[row][c] = true;
- }
-
- layouts.push({ event, columnKey, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 });
- }
-
- return layouts;
- }
-
- /**
- * Build columnKey from event using FilterTemplate
- * Uses the same template that columns use for matching
- */
- private buildColumnKeyFromEvent(event: ICalendarEvent, date?: Date): string {
- if (!this.filterTemplate) {
- // Fallback if no template - shouldn't happen in normal flow
- const dateStr = this.dateService.getDateKey(date || event.start);
- return dateStr;
- }
-
- // For multi-day events, we need to override the date in the event
- if (date && date.getTime() !== event.start.getTime()) {
- // Create temporary event with overridden start for key generation
- const tempEvent = { ...event, start: date };
- return this.filterTemplate.buildKeyFromEvent(tempEvent);
- }
-
- return this.filterTemplate.buildKeyFromEvent(event);
- }
-
- /**
- * Find available row for event spanning columns [colStart, colEnd)
- */
- private findAvailableRow(tracks: boolean[][], colStart: number, colEnd: number): number {
- for (let row = 0; row < tracks.length; row++) {
- let available = true;
- for (let c = colStart; c < colEnd; c++) {
- if (tracks[row][c]) { available = false; break; }
- }
- if (available) return row;
- }
- // Ny række
- tracks.push(new Array(tracks[0].length).fill(false));
- return tracks.length - 1;
- }
-
- /**
- * Get color class based on event metadata or type
- */
- private getColorClass(event: ICalendarEvent): string {
- if (event.metadata?.color) {
- return `is-${event.metadata.color}`;
- }
- const typeColors: Record = {
- 'customer': 'is-blue',
- 'vacation': 'is-green',
- 'break': 'is-amber',
- 'meeting': 'is-purple',
- 'blocked': 'is-red'
- };
- return typeColors[event.type] || 'is-blue';
- }
-
- /**
- * Setup event listeners for drag events
- */
- private setupListeners(): void {
- this.eventBus.on(CoreEvents.EVENT_DRAG_ENTER_HEADER, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleDragEnter(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE_HEADER, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleDragMove(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleDragLeave(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => {
- const payload = (e as CustomEvent).detail;
- this.handleDragEnd(payload);
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => {
- this.cleanup();
- });
- }
-
- /**
- * Handle drag entering header zone - create preview item
- */
- private handleDragEnter(payload: IDragEnterHeaderPayload): void {
- this.container = document.querySelector('swp-header-drawer');
- if (!this.container) return;
-
- // Remember if drawer was already expanded
- this.wasExpandedBeforeDrag = this.headerDrawerManager.isExpanded();
-
- // Expand to at least 1 row if collapsed, otherwise keep current height
- if (!this.wasExpandedBeforeDrag) {
- this.headerDrawerManager.expandToRows(1);
- }
-
- // Store reference to source element
- this.sourceElement = payload.element;
-
- // Create header item
- const item = document.createElement('swp-header-item');
- item.dataset.eventId = payload.eventId;
- item.dataset.itemType = payload.itemType;
- item.dataset.duration = String(payload.duration);
- item.dataset.columnKey = payload.sourceColumnKey;
- item.textContent = payload.title;
-
- // Apply color class if present
- if (payload.colorClass) {
- item.classList.add(payload.colorClass);
- }
-
- // Add dragging state
- item.classList.add('dragging');
-
- // Initial placement (duration determines column span)
- // gridArea format: "row / col-start / row+1 / col-end"
- const col = payload.sourceColumnIndex + 1;
- const endCol = col + payload.duration;
- item.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
-
- this.container.appendChild(item);
- this.currentItem = item;
-
- // Hide original element while in header
- payload.element.style.visibility = 'hidden';
- }
-
- /**
- * Handle drag moving within header - update column position
- */
- private handleDragMove(payload: IDragMoveHeaderPayload): void {
- if (!this.currentItem) return;
-
- // Update column position
- const col = payload.columnIndex + 1;
- const duration = parseInt(this.currentItem.dataset.duration || '1', 10);
- const endCol = col + duration;
-
- this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
-
- // Update columnKey to new position
- this.currentItem.dataset.columnKey = payload.columnKey;
- }
-
- /**
- * Handle drag leaving header - cleanup for grid→header drag only
- */
- private handleDragLeave(payload: IDragLeaveHeaderPayload): void {
- // Only cleanup for grid→header drag (when grid event leaves header back to grid)
- // For header→grid drag, the header item stays as ghost until drop
- if (payload.source === 'grid') {
- this.cleanup();
- }
- // For header source, do nothing - ghost stays until EVENT_DRAG_END
- }
-
- /**
- * Handle drag end - finalize based on drop target
- */
- private handleDragEnd(payload: IDragEndPayload): void {
- if (payload.target === 'header') {
- // Grid→Header: Finalize the header item (it stays in header)
- if (this.currentItem) {
- this.currentItem.classList.remove('dragging');
- this.recalculateDrawerLayout();
- this.currentItem = null;
- this.sourceElement = null;
- }
- } else {
- // Header→Grid: Remove ghost header item and recalculate
- const ghost = document.querySelector(`swp-header-item.drag-ghost[data-event-id="${payload.swpEvent.eventId}"]`);
- ghost?.remove();
- this.recalculateDrawerLayout();
- }
- }
-
- /**
- * Recalculate layout for all items currently in the drawer
- * Called after drop to reposition items and adjust height
- */
- private recalculateDrawerLayout(): void {
- const drawer = document.querySelector('swp-header-drawer');
- if (!drawer) return;
-
- const items = Array.from(drawer.querySelectorAll('swp-header-item')) as HTMLElement[];
- if (items.length === 0) return;
-
- // Get visible column keys for correct multi-resource positioning
- const visibleColumnKeys = this.getVisibleColumnKeysFromDOM();
- if (visibleColumnKeys.length === 0) return;
-
- // Build layout data from DOM items - use columnKey directly (opaque matching)
- const itemData = items.map(item => ({
- element: item,
- columnKey: item.dataset.columnKey || '',
- duration: parseInt(item.dataset.duration || '1', 10)
- }));
-
- // Calculate new layout using track algorithm
- const tracks: boolean[][] = [new Array(visibleColumnKeys.length).fill(false)];
-
- for (const item of itemData) {
- // Direct columnKey matching - no parsing or construction needed
- const startCol = visibleColumnKeys.indexOf(item.columnKey);
- if (startCol === -1) continue;
-
- const colStart = startCol;
- const colEnd = Math.min(startCol + item.duration, visibleColumnKeys.length);
-
- const row = this.findAvailableRow(tracks, colStart, colEnd);
-
- for (let c = colStart; c < colEnd; c++) {
- tracks[row][c] = true;
- }
-
- // Update element position
- item.element.style.gridArea = `${row + 1} / ${colStart + 1} / ${row + 2} / ${colEnd + 1}`;
- }
-
- // Update drawer height
- const rowCount = tracks.length;
- this.headerDrawerManager.expandToRows(rowCount);
- }
-
- /**
- * Get visible column keys from DOM (preserves order for multi-resource views)
- * Uses filterTemplate.buildKeyFromColumn() for consistent key format with events
- */
- private getVisibleColumnKeysFromDOM(): string[] {
- if (!this.filterTemplate) return [];
- const columns = document.querySelectorAll('swp-day-column');
- const columnKeys: string[] = [];
- columns.forEach(col => {
- const columnKey = this.filterTemplate!.buildKeyFromColumn(col as HTMLElement);
- if (columnKey) columnKeys.push(columnKey);
- });
- return columnKeys;
- }
-
- /**
- * Cleanup preview item and restore source visibility
- */
- private cleanup(): void {
- // Remove preview item
- this.currentItem?.remove();
- this.currentItem = null;
-
- // Restore source element visibility
- if (this.sourceElement) {
- this.sourceElement.style.visibility = '';
- this.sourceElement = null;
- }
-
- // Collapse drawer if it wasn't expanded before drag
- if (!this.wasExpandedBeforeDrag) {
- this.headerDrawerManager.collapse();
- }
- }
-}
diff --git a/packages/calendar/src/features/headerdrawer/index.ts b/packages/calendar/src/features/headerdrawer/index.ts
deleted file mode 100644
index 7b19757..0000000
--- a/packages/calendar/src/features/headerdrawer/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { HeaderDrawerRenderer } from './HeaderDrawerRenderer';
-export { HeaderDrawerLayoutEngine, type IHeaderItemLayout, type IHeaderItemInput } from './HeaderDrawerLayoutEngine';
diff --git a/packages/calendar/src/features/resource/ResourceRenderer.ts b/packages/calendar/src/features/resource/ResourceRenderer.ts
deleted file mode 100644
index 2bf565f..0000000
--- a/packages/calendar/src/features/resource/ResourceRenderer.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { IRenderContext } from '../../core/IGroupingRenderer';
-import { BaseGroupingRenderer, IGroupingRendererConfig } from '../../core/BaseGroupingRenderer';
-import { ResourceService } from '../../storage/resources/ResourceService';
-import { IResource } from '../../types/CalendarTypes';
-
-export class ResourceRenderer extends BaseGroupingRenderer {
- readonly type = 'resource';
-
- protected readonly config: IGroupingRendererConfig = {
- elementTag: 'swp-resource-header',
- idAttribute: 'resourceId',
- colspanVar: '--resource-cols'
- };
-
- constructor(private resourceService: ResourceService) {
- super();
- }
-
- protected getEntities(ids: string[]): Promise {
- return this.resourceService.getByIds(ids);
- }
-
- protected getDisplayName(entity: IResource): string {
- return entity.displayName;
- }
-
- /**
- * Override render to handle:
- * 1. Special ordering when parentChildMap exists (resources grouped by parent)
- * 2. Different colspan calculation (just dateCount, not childCount * dateCount)
- */
- async render(context: IRenderContext): Promise {
- const resourceIds = context.filter['resource'] || [];
- const dateCount = context.filter['date']?.length || 1;
-
- // Determine render order based on parentChildMap
- // If parentChildMap exists, render resources grouped by parent (e.g., team)
- // Otherwise, render in filter order
- let orderedResourceIds: string[];
-
- if (context.parentChildMap) {
- // Render resources in parent-child order
- orderedResourceIds = [];
- for (const childIds of Object.values(context.parentChildMap)) {
- for (const childId of childIds) {
- if (resourceIds.includes(childId)) {
- orderedResourceIds.push(childId);
- }
- }
- }
- } else {
- orderedResourceIds = resourceIds;
- }
-
- const resources = await this.getEntities(orderedResourceIds);
-
- // Create a map for quick lookup to preserve order
- const resourceMap = new Map(resources.map(r => [r.id, r]));
-
- for (const resourceId of orderedResourceIds) {
- const resource = resourceMap.get(resourceId);
- if (!resource) continue;
-
- const header = this.createHeader(resource, context);
- header.style.gridColumn = `span ${dateCount}`;
- context.headerContainer.appendChild(header);
- }
- }
-}
diff --git a/packages/calendar/src/features/resource/index.ts b/packages/calendar/src/features/resource/index.ts
deleted file mode 100644
index 3bbd0d9..0000000
--- a/packages/calendar/src/features/resource/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { ResourceRenderer } from './ResourceRenderer';
diff --git a/packages/calendar/src/features/schedule/ScheduleRenderer.ts b/packages/calendar/src/features/schedule/ScheduleRenderer.ts
deleted file mode 100644
index d1d3349..0000000
--- a/packages/calendar/src/features/schedule/ScheduleRenderer.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { ResourceScheduleService } from '../../extensions/schedules/ResourceScheduleService';
-import { DateService } from '../../core/DateService';
-import { IGridConfig } from '../../core/IGridConfig';
-import { ITimeSlot } from '../../types/ScheduleTypes';
-
-/**
- * ScheduleRenderer - Renders unavailable time zones in day columns
- *
- * Creates visual indicators for times outside the resource's working hours:
- * - Before work start (e.g., 06:00 - 09:00)
- * - After work end (e.g., 17:00 - 18:00)
- * - Full day if resource is off (schedule = null)
- */
-export class ScheduleRenderer {
- constructor(
- private scheduleService: ResourceScheduleService,
- private dateService: DateService,
- private gridConfig: IGridConfig
- ) {}
-
- /**
- * Render unavailable zones for visible columns
- * @param container - Calendar container element
- * @param filter - Filter with 'date' and 'resource' arrays
- */
- async render(container: HTMLElement, filter: Record): Promise {
- const dates = filter['date'] || [];
- const resourceIds = filter['resource'] || [];
-
- if (dates.length === 0) return;
-
- // Find day columns
- const dayColumns = container.querySelector('swp-day-columns');
- if (!dayColumns) return;
-
- const columns = dayColumns.querySelectorAll('swp-day-column');
-
- for (const column of columns) {
- const date = (column as HTMLElement).dataset.date;
- const resourceId = (column as HTMLElement).dataset.resourceId;
-
- if (!date || !resourceId) continue;
-
- // Get or create unavailable layer
- let unavailableLayer = column.querySelector('swp-unavailable-layer');
- if (!unavailableLayer) {
- unavailableLayer = document.createElement('swp-unavailable-layer');
- column.insertBefore(unavailableLayer, column.firstChild);
- }
-
- // Clear existing
- unavailableLayer.innerHTML = '';
-
- // Get schedule for this resource/date
- const schedule = await this.scheduleService.getScheduleForDate(resourceId, date);
-
- // Render unavailable zones
- this.renderUnavailableZones(unavailableLayer as HTMLElement, schedule);
- }
- }
-
- /**
- * Render unavailable time zones based on schedule
- */
- private renderUnavailableZones(layer: HTMLElement, schedule: ITimeSlot | null): void {
- const dayStartMinutes = this.gridConfig.dayStartHour * 60;
- const dayEndMinutes = this.gridConfig.dayEndHour * 60;
- const minuteHeight = this.gridConfig.hourHeight / 60;
-
- if (schedule === null) {
- // Full day unavailable
- const zone = this.createUnavailableZone(0, (dayEndMinutes - dayStartMinutes) * minuteHeight);
- layer.appendChild(zone);
- return;
- }
-
- const workStartMinutes = this.dateService.timeToMinutes(schedule.start);
- const workEndMinutes = this.dateService.timeToMinutes(schedule.end);
-
- // Before work start
- if (workStartMinutes > dayStartMinutes) {
- const top = 0;
- const height = (workStartMinutes - dayStartMinutes) * minuteHeight;
- const zone = this.createUnavailableZone(top, height);
- layer.appendChild(zone);
- }
-
- // After work end
- if (workEndMinutes < dayEndMinutes) {
- const top = (workEndMinutes - dayStartMinutes) * minuteHeight;
- const height = (dayEndMinutes - workEndMinutes) * minuteHeight;
- const zone = this.createUnavailableZone(top, height);
- layer.appendChild(zone);
- }
- }
-
- /**
- * Create an unavailable zone element
- */
- private createUnavailableZone(top: number, height: number): HTMLElement {
- const zone = document.createElement('swp-unavailable-zone');
- zone.style.top = `${top}px`;
- zone.style.height = `${height}px`;
- return zone;
- }
-}
diff --git a/packages/calendar/src/features/schedule/index.ts b/packages/calendar/src/features/schedule/index.ts
deleted file mode 100644
index c6ca514..0000000
--- a/packages/calendar/src/features/schedule/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { ScheduleRenderer } from './ScheduleRenderer';
diff --git a/packages/calendar/src/features/timeaxis/TimeAxisRenderer.ts b/packages/calendar/src/features/timeaxis/TimeAxisRenderer.ts
deleted file mode 100644
index 80279be..0000000
--- a/packages/calendar/src/features/timeaxis/TimeAxisRenderer.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export class TimeAxisRenderer {
- render(container: HTMLElement, startHour = 6, endHour = 20): void {
- container.innerHTML = '';
- for (let hour = startHour; hour <= endHour; hour++) {
- const marker = document.createElement('swp-hour-marker');
- marker.textContent = `${hour.toString().padStart(2, '0')}:00`;
- container.appendChild(marker);
- }
- }
-}
diff --git a/packages/calendar/src/index.ts b/packages/calendar/src/index.ts
deleted file mode 100644
index a0bee9c..0000000
--- a/packages/calendar/src/index.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-// === CORE ===
-
-// App
-export { CalendarApp } from './core/CalendarApp';
-export { CalendarOrchestrator } from './core/CalendarOrchestrator';
-export { CalendarEvents } from './core/CalendarEvents';
-export type {
- RenderPayload,
- WorkweekChangePayload,
- ViewUpdatePayload
-} from './core/CalendarEvents';
-
-// Infrastructure
-export { EventBus } from './core/EventBus';
-export { DateService } from './core/DateService';
-export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
-export { IRenderer, IRenderContext } from './core/IGroupingRenderer';
-export { IGroupingStore } from './core/IGroupingStore';
-export { BaseGroupingRenderer, IGroupingRendererConfig } from './core/BaseGroupingRenderer';
-export { buildPipeline, Pipeline } from './core/RenderBuilder';
-export { NavigationAnimator } from './core/NavigationAnimator';
-export { ScrollManager } from './core/ScrollManager';
-export { HeaderDrawerManager } from './core/HeaderDrawerManager';
-export { FilterTemplate } from './core/FilterTemplate';
-export { EntityResolver } from './core/EntityResolver';
-export type { IEntityResolver } from './core/IEntityResolver';
-
-// Configuration interfaces
-export type { ITimeFormatConfig } from './core/ITimeFormatConfig';
-export type { IGridConfig } from './core/IGridConfig';
-
-// Core Features
-export { DateRenderer } from './features/date';
-export { ResourceRenderer } from './features/resource';
-export { EventRenderer } from './features/event';
-export { eventsOverlap, calculateColumnLayout } from './features/event/EventLayoutEngine';
-export type {
- IStackLink,
- IStackedEventLayout,
- IGridGroupLayout,
- IColumnLayout
-} from './features/event/EventLayoutTypes';
-export { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
-export { HeaderDrawerRenderer, HeaderDrawerLayoutEngine } from './features/headerdrawer';
-export { ScheduleRenderer } from './features/schedule';
-
-// Core Storage
-export { IndexedDBContext, defaultDBConfig } from './storage/IndexedDBContext';
-export type { IDBConfig } from './storage/IndexedDBContext';
-export { BaseEntityService } from './storage/BaseEntityService';
-export type { IEntityService } from './storage/IEntityService';
-export type { IStore } from './storage/IStore';
-export { SyncPlugin } from './storage/SyncPlugin';
-
-// Event storage
-export { EventService } from './storage/events/EventService';
-export { EventStore } from './storage/events/EventStore';
-export { EventSerialization } from './storage/events/EventSerialization';
-
-// Resource storage
-export { ResourceService } from './storage/resources/ResourceService';
-export { ResourceStore } from './storage/resources/ResourceStore';
-
-// Settings storage
-export { SettingsService } from './storage/settings/SettingsService';
-export { SettingsStore } from './storage/settings/SettingsStore';
-
-// ViewConfig storage
-export { ViewConfigService } from './storage/viewconfigs/ViewConfigService';
-export { ViewConfigStore } from './storage/viewconfigs/ViewConfigStore';
-
-// Core Managers
-export { DragDropManager } from './managers/DragDropManager';
-export { EdgeScrollManager } from './managers/EdgeScrollManager';
-export { ResizeManager } from './managers/ResizeManager';
-export { EventPersistenceManager } from './managers/EventPersistenceManager';
-
-// Position utilities
-export {
- calculateEventPosition,
- minutesToPixels,
- pixelsToMinutes,
- snapToGrid
-} from './utils/PositionUtils';
-export type { EventPosition } from './utils/PositionUtils';
-
-// Types
-export type {
- ICalendarEvent,
- IResource,
- IEventBus,
- ISync,
- SyncStatus,
- EntityType,
- CalendarEventType,
- ResourceType,
- ITeam,
- IDepartment,
- IBooking,
- BookingStatus,
- IBookingService,
- ICustomer,
- IDataEntity,
- IEventLogEntry,
- IListenerEntry,
- IEntitySavedPayload,
- IEntityDeletedPayload,
- IEventUpdatedPayload
-} from './types/CalendarTypes';
-
-export type {
- TenantSetting,
- IGridSettings,
- IWorkweekPreset
-} from './types/SettingsTypes';
-
-export type {
- IScheduleOverride,
- ITimeSlot,
- IWeekSchedule,
- WeekDay
-} from './types/ScheduleTypes';
-
-// Drag types
-export type {
- IMousePosition,
- IDragStartPayload,
- IDragMovePayload,
- IDragEndPayload,
- IDragCancelPayload,
- IDragColumnChangePayload,
- IDragEnterHeaderPayload,
- IDragMoveHeaderPayload,
- IDragLeaveHeaderPayload
-} from './types/DragTypes';
-
-// Resize types
-export type {
- IResizeStartPayload,
- IResizeEndPayload
-} from './types/ResizeTypes';
-
-// Audit types
-export type {
- IAuditEntry,
- IAuditLoggedPayload
-} from './types/AuditTypes';
-
-export { SwpEvent } from './types/SwpEvent';
-
-// Core Events constants
-export { CoreEvents } from './constants/CoreEvents';
-
-// Repository interface (for custom implementations)
-export type { IApiRepository } from './repositories/IApiRepository';
-
-// DI helpers
-export {
- createCalendarContainer,
- registerCoreServices,
- defaultTimeFormatConfig,
- defaultGridConfig
-} from './CompositionRoot';
-export type { ICalendarOptions } from './CompositionRoot';
diff --git a/packages/calendar/src/managers/DragDropManager.ts b/packages/calendar/src/managers/DragDropManager.ts
deleted file mode 100644
index 4bf50b9..0000000
--- a/packages/calendar/src/managers/DragDropManager.ts
+++ /dev/null
@@ -1,581 +0,0 @@
-import { IEventBus } from '../types/CalendarTypes';
-import { IGridConfig } from '../core/IGridConfig';
-import { CoreEvents } from '../constants/CoreEvents';
-import { snapToGrid } from '../utils/PositionUtils';
-import {
- IMousePosition,
- IDragStartPayload,
- IDragMovePayload,
- IDragEndPayload,
- IDragCancelPayload,
- IDragColumnChangePayload,
- IDragEnterHeaderPayload,
- IDragMoveHeaderPayload,
- IDragLeaveHeaderPayload
-} from '../types/DragTypes';
-import { SwpEvent } from '../types/SwpEvent';
-
-interface DragState {
- eventId: string;
- element: HTMLElement;
- ghostElement: HTMLElement | null; // Null for header items
- startY: number;
- mouseOffset: IMousePosition;
- columnElement: HTMLElement | null; // Null when starting from header
- currentColumn: HTMLElement | null; // Null when in header
- targetY: number;
- currentY: number;
- animationId: number;
- sourceColumnKey: string; // Source column key (where drag started)
- dragSource: 'grid' | 'header'; // Where drag originated
-}
-
-/**
- * DragDropManager - Handles drag-drop for calendar events
- *
- * Strategy: Drag original element, leave ghost-clone in place
- * - mousedown: Store initial state, wait for movement
- * - mousemove (>5px): Create ghost, start dragging original
- * - mouseup: Snap to grid, remove ghost, emit drag:end
- * - cancel: Animate back to startY, remove ghost
- */
-export class DragDropManager {
- private dragState: DragState | null = null;
- private mouseDownPosition: IMousePosition | null = null;
- private pendingElement: HTMLElement | null = null;
- private pendingMouseOffset: IMousePosition | null = null;
- private container: HTMLElement | null = null;
- private inHeader = false;
-
- private readonly DRAG_THRESHOLD = 5;
- private readonly INTERPOLATION_FACTOR = 0.3;
-
- constructor(
- private eventBus: IEventBus,
- private gridConfig: IGridConfig
- ) {
- this.setupScrollListener();
- }
-
- private setupScrollListener(): void {
- this.eventBus.on(CoreEvents.EDGE_SCROLL_TICK, (e) => {
- if (!this.dragState) return;
- const { scrollDelta } = (e as CustomEvent<{ scrollDelta: number }>).detail;
-
- // Element skal flytte med scroll for at forblive under musen
- // (elementets top er relativ til kolonnen, som scroller med viewport)
- this.dragState.targetY += scrollDelta;
- this.dragState.currentY += scrollDelta;
- this.dragState.element.style.top = `${this.dragState.currentY}px`;
- });
- }
-
- /**
- * Initialize drag-drop on a container element
- */
- init(container: HTMLElement): void {
- this.container = container;
- container.addEventListener('pointerdown', this.handlePointerDown);
- document.addEventListener('pointermove', this.handlePointerMove);
- document.addEventListener('pointerup', this.handlePointerUp);
- }
-
- private handlePointerDown = (e: PointerEvent): void => {
- const target = e.target as HTMLElement;
-
- // Ignore if clicking on resize handle
- if (target.closest('swp-resize-handle')) return;
-
- // Match both swp-event and swp-header-item
- const eventElement = target.closest('swp-event') as HTMLElement;
- const headerItem = target.closest('swp-header-item') as HTMLElement;
- const draggable = eventElement || headerItem;
-
- if (!draggable) return;
-
- // Store for potential drag
- this.mouseDownPosition = { x: e.clientX, y: e.clientY };
- this.pendingElement = draggable;
-
- // Calculate mouse offset within element
- const rect = draggable.getBoundingClientRect();
- this.pendingMouseOffset = {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top
- };
-
- // Capture pointer for reliable tracking
- draggable.setPointerCapture(e.pointerId);
- };
-
- private handlePointerMove = (e: PointerEvent): void => {
- // Not in potential drag state
- if (!this.mouseDownPosition || !this.pendingElement) {
- // Already dragging - update target
- if (this.dragState) {
- this.updateDragTarget(e);
- }
- return;
- }
-
- // Check threshold
- const deltaX = Math.abs(e.clientX - this.mouseDownPosition.x);
- const deltaY = Math.abs(e.clientY - this.mouseDownPosition.y);
- const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
-
- if (distance < this.DRAG_THRESHOLD) return;
-
- // Start drag
- this.initializeDrag(this.pendingElement, this.pendingMouseOffset!, e);
- this.mouseDownPosition = null;
- this.pendingElement = null;
- this.pendingMouseOffset = null;
- };
-
- private handlePointerUp = (_e: PointerEvent): void => {
- // Clear pending state
- this.mouseDownPosition = null;
- this.pendingElement = null;
- this.pendingMouseOffset = null;
-
- if (!this.dragState) return;
-
- // Stop animation
- cancelAnimationFrame(this.dragState.animationId);
-
- // Handle based on drag source and target
- if (this.dragState.dragSource === 'header') {
- // Header item drag end
- this.handleHeaderItemDragEnd();
- } else {
- // Grid event drag end
- this.handleGridEventDragEnd();
- }
-
- // Cleanup
- this.dragState.element.classList.remove('dragging');
- this.dragState = null;
- this.inHeader = false;
- };
-
- /**
- * Handle drag end for header items
- */
- private handleHeaderItemDragEnd(): void {
- if (!this.dragState) return;
-
- // If dropped in grid (not in header), the swp-event was already created
- // by EventRenderer listening to EVENT_DRAG_LEAVE_HEADER
- // Just emit drag:end for persistence
-
- if (!this.inHeader && this.dragState.currentColumn) {
- // Dropped in grid - emit drag:end with the new swp-event element
- const gridEvent = this.dragState.currentColumn.querySelector(
- `swp-event[data-event-id="${this.dragState.eventId}"]`
- ) as HTMLElement;
-
- if (gridEvent) {
- const columnKey = this.dragState.currentColumn.dataset.columnKey || '';
- const date = this.dragState.currentColumn.dataset.date || '';
- const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, date, this.gridConfig);
-
- const payload: IDragEndPayload = {
- swpEvent,
- sourceColumnKey: this.dragState.sourceColumnKey,
- target: 'grid'
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
- }
- }
- // If still in header, no persistence needed (stayed in header)
- }
-
- /**
- * Handle drag end for grid events
- */
- private handleGridEventDragEnd(): void {
- if (!this.dragState || !this.dragState.columnElement) return;
-
- // Snap to grid
- const snappedY = snapToGrid(this.dragState.currentY, this.gridConfig);
- this.dragState.element.style.top = `${snappedY}px`;
-
- // Remove ghost
- this.dragState.ghostElement?.remove();
-
- // Get columnKey and date from target column
- const columnKey = this.dragState.columnElement.dataset.columnKey || '';
- const date = this.dragState.columnElement.dataset.date || '';
-
- // Create SwpEvent from element (reads top/height/eventId from element)
- const swpEvent = SwpEvent.fromElement(
- this.dragState.element,
- columnKey,
- date,
- this.gridConfig
- );
-
- // Emit drag:end
- const payload: IDragEndPayload = {
- swpEvent,
- sourceColumnKey: this.dragState.sourceColumnKey,
- target: this.inHeader ? 'header' : 'grid'
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
- }
-
- private initializeDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent): void {
- const eventId = element.dataset.eventId || '';
- const isHeaderItem = element.tagName.toLowerCase() === 'swp-header-item';
- const columnElement = element.closest('swp-day-column') as HTMLElement;
-
- // For grid events, we need a column
- if (!isHeaderItem && !columnElement) return;
-
- if (isHeaderItem) {
- // Header item drag initialization
- this.initializeHeaderItemDrag(element, mouseOffset, eventId);
- } else {
- // Grid event drag initialization
- this.initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId);
- }
- }
-
- /**
- * Initialize drag for a header item (allDay event)
- */
- private initializeHeaderItemDrag(element: HTMLElement, mouseOffset: IMousePosition, eventId: string): void {
- // Mark as dragging
- element.classList.add('dragging');
-
- // Initialize drag state for header item
- this.dragState = {
- eventId,
- element,
- ghostElement: null, // No ghost for header items
- startY: 0,
- mouseOffset,
- columnElement: null,
- currentColumn: null,
- targetY: 0,
- currentY: 0,
- animationId: 0,
- sourceColumnKey: '', // Will be set from header item data
- dragSource: 'header'
- };
-
- // Start in header mode
- this.inHeader = true;
- }
-
- /**
- * Initialize drag for a grid event
- */
- private initializeGridEventDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent, columnElement: HTMLElement, eventId: string): void {
- // Calculate absolute Y position using getBoundingClientRect
- const elementRect = element.getBoundingClientRect();
- const columnRect = columnElement.getBoundingClientRect();
- const startY = elementRect.top - columnRect.top;
-
- // If event is inside a group, move it to events-layer for correct positioning during drag
- const group = element.closest('swp-event-group');
- if (group) {
- const eventsLayer = columnElement.querySelector('swp-events-layer');
- if (eventsLayer) {
- eventsLayer.appendChild(element);
- }
- }
-
- // Set consistent positioning for drag (works for both grouped and stacked events)
- element.style.position = 'absolute';
- element.style.top = `${startY}px`;
- element.style.left = '2px';
- element.style.right = '2px';
- element.style.marginLeft = '0'; // Reset stacking margin
-
- // Create ghost clone
- const ghostElement = element.cloneNode(true) as HTMLElement;
- ghostElement.classList.add('drag-ghost');
- ghostElement.style.opacity = '0.3';
- ghostElement.style.pointerEvents = 'none';
-
- // Insert ghost before original
- element.parentNode?.insertBefore(ghostElement, element);
-
- // Setup element for dragging
- element.classList.add('dragging');
-
- // Calculate initial target from mouse position
- const targetY = e.clientY - columnRect.top - mouseOffset.y;
-
- // Initialize drag state
- this.dragState = {
- eventId,
- element,
- ghostElement,
- startY,
- mouseOffset,
- columnElement,
- currentColumn: columnElement,
- targetY: Math.max(0, targetY),
- currentY: startY,
- animationId: 0,
- sourceColumnKey: columnElement.dataset.columnKey || '',
- dragSource: 'grid'
- };
-
- // Emit drag:start
- const payload: IDragStartPayload = {
- eventId,
- element,
- ghostElement,
- startY,
- mouseOffset,
- columnElement
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_START, payload);
-
- // Start animation loop
- this.animateDrag();
- }
-
- private updateDragTarget(e: PointerEvent): void {
- if (!this.dragState) return;
-
- // Check header zone first
- this.checkHeaderZone(e);
-
- // Skip normal grid handling if in header
- if (this.inHeader) return;
-
- // Check for column change
- const columnAtPoint = this.getColumnAtPoint(e.clientX);
-
- // For header items entering grid, set initial column
- if (this.dragState.dragSource === 'header' && columnAtPoint && !this.dragState.currentColumn) {
- this.dragState.currentColumn = columnAtPoint;
- this.dragState.columnElement = columnAtPoint;
- }
-
- if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn && this.dragState.currentColumn) {
- const payload: IDragColumnChangePayload = {
- eventId: this.dragState.eventId,
- element: this.dragState.element,
- previousColumn: this.dragState.currentColumn,
- newColumn: columnAtPoint,
- currentY: this.dragState.currentY
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, payload);
- this.dragState.currentColumn = columnAtPoint;
- this.dragState.columnElement = columnAtPoint;
- }
-
- // Skip grid position updates if no column yet
- if (!this.dragState.columnElement) return;
-
- const columnRect = this.dragState.columnElement.getBoundingClientRect();
- const targetY = e.clientY - columnRect.top - this.dragState.mouseOffset.y;
-
- this.dragState.targetY = Math.max(0, targetY);
-
- // Start animation if not running
- if (!this.dragState.animationId) {
- this.animateDrag();
- }
- }
-
- /**
- * Check if pointer is in header zone and emit appropriate events
- */
- private checkHeaderZone(e: PointerEvent): void {
- if (!this.dragState) return;
-
- const headerViewport = document.querySelector('swp-header-viewport');
- if (!headerViewport) return;
-
- const rect = headerViewport.getBoundingClientRect();
- const isInHeader = e.clientY < rect.bottom;
-
- if (isInHeader && !this.inHeader) {
- // Entered header (from grid)
- this.inHeader = true;
-
- if (this.dragState.dragSource === 'grid' && this.dragState.columnElement) {
- const payload: IDragEnterHeaderPayload = {
- eventId: this.dragState.eventId,
- element: this.dragState.element,
- sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
- sourceColumnKey: this.dragState.columnElement.dataset.columnKey || '',
- title: this.dragState.element.querySelector('swp-event-title')?.textContent || '',
- colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')),
- itemType: 'event',
- duration: 1
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload);
- }
- // For header source re-entering header, just update inHeader flag
- } else if (!isInHeader && this.inHeader) {
- // Left header (entering grid)
- this.inHeader = false;
-
- const targetColumn = this.getColumnAtPoint(e.clientX);
-
- if (this.dragState.dragSource === 'header') {
- // Header item leaving header → create swp-event in grid
- const payload: IDragLeaveHeaderPayload = {
- eventId: this.dragState.eventId,
- source: 'header',
- element: this.dragState.element,
- targetColumn: targetColumn || undefined,
- start: this.dragState.element.dataset.start ? new Date(this.dragState.element.dataset.start) : undefined,
- end: this.dragState.element.dataset.end ? new Date(this.dragState.element.dataset.end) : undefined,
- title: this.dragState.element.textContent || '',
- colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-'))
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
-
- // Re-attach to the new swp-event created by EventRenderer
- if (targetColumn) {
- const newElement = targetColumn.querySelector(
- `swp-event[data-event-id="${this.dragState.eventId}"]`
- ) as HTMLElement;
-
- if (newElement) {
- this.dragState.element = newElement;
- this.dragState.columnElement = targetColumn;
- this.dragState.currentColumn = targetColumn;
-
- // Start animation for the new element
- this.animateDrag();
- }
- }
- } else {
- // Grid event leaving header → restore to grid
- const payload: IDragLeaveHeaderPayload = {
- eventId: this.dragState.eventId,
- source: 'grid'
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
- }
- } else if (isInHeader) {
- // Moving within header
- const column = this.getColumnAtX(e.clientX);
- if (column) {
- const payload: IDragMoveHeaderPayload = {
- eventId: this.dragState.eventId,
- columnIndex: this.getColumnIndex(column),
- columnKey: column.dataset.columnKey || ''
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload);
- }
- }
- }
-
- /**
- * Get column index (0-based) for a column element
- */
- private getColumnIndex(column: HTMLElement | null): number {
- if (!this.container || !column) return 0;
- const columns = Array.from(this.container.querySelectorAll('swp-day-column'));
- return columns.indexOf(column);
- }
-
- /**
- * Get column at X coordinate (alias for getColumnAtPoint)
- */
- private getColumnAtX(clientX: number): HTMLElement | null {
- return this.getColumnAtPoint(clientX);
- }
-
- /**
- * Find column element at given X coordinate
- */
- private getColumnAtPoint(clientX: number): HTMLElement | null {
- if (!this.container) return null;
-
- const columns = this.container.querySelectorAll('swp-day-column');
- for (const col of columns) {
- const rect = col.getBoundingClientRect();
- if (clientX >= rect.left && clientX <= rect.right) {
- return col as HTMLElement;
- }
- }
- return null;
- }
-
- private animateDrag = (): void => {
- if (!this.dragState) return;
-
- const diff = this.dragState.targetY - this.dragState.currentY;
-
- // Stop animation when close enough to target
- if (Math.abs(diff) <= 0.5) {
- this.dragState.animationId = 0;
- return;
- }
-
- // Interpolate towards target
- this.dragState.currentY += diff * this.INTERPOLATION_FACTOR;
-
- // Update element position
- this.dragState.element.style.top = `${this.dragState.currentY}px`;
-
- // Emit drag:move (only if we have a column)
- if (this.dragState.columnElement) {
- const payload: IDragMovePayload = {
- eventId: this.dragState.eventId,
- element: this.dragState.element,
- currentY: this.dragState.currentY,
- columnElement: this.dragState.columnElement
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE, payload);
- }
-
- // Continue animation
- this.dragState.animationId = requestAnimationFrame(this.animateDrag);
- };
-
- /**
- * Cancel drag and animate back to start position
- */
- cancelDrag(): void {
- if (!this.dragState) return;
-
- // Stop animation
- cancelAnimationFrame(this.dragState.animationId);
-
- const { element, ghostElement, startY, eventId } = this.dragState;
-
- // Animate back to start
- element.style.transition = 'top 200ms ease-out';
- element.style.top = `${startY}px`;
-
- // Remove ghost after animation (if exists)
- setTimeout(() => {
- ghostElement?.remove();
- element.style.transition = '';
- element.classList.remove('dragging');
- }, 200);
-
- // Emit drag:cancel
- const payload: IDragCancelPayload = {
- eventId,
- element,
- startY
- };
-
- this.eventBus.emit(CoreEvents.EVENT_DRAG_CANCEL, payload);
-
- this.dragState = null;
- this.inHeader = false;
- }
-}
diff --git a/packages/calendar/src/managers/EdgeScrollManager.ts b/packages/calendar/src/managers/EdgeScrollManager.ts
deleted file mode 100644
index d1b5584..0000000
--- a/packages/calendar/src/managers/EdgeScrollManager.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * EdgeScrollManager - Auto-scroll when dragging near viewport edges
- *
- * 2-zone system:
- * - Inner zone (0-50px): Fast scroll (640 px/sec)
- * - Outer zone (50-100px): Slow scroll (140 px/sec)
- */
-
-import { IEventBus } from '../types/CalendarTypes';
-import { CoreEvents } from '../constants/CoreEvents';
-
-export class EdgeScrollManager {
- private scrollableContent: HTMLElement | null = null;
- private timeGrid: HTMLElement | null = null;
- private draggedElement: HTMLElement | null = null;
- private scrollRAF: number | null = null;
- private mouseY = 0;
- private isDragging = false;
- private isScrolling = false;
- private lastTs = 0;
- private rect: DOMRect | null = null;
- private initialScrollTop = 0;
-
- private readonly OUTER_ZONE = 100;
- private readonly INNER_ZONE = 50;
- private readonly SLOW_SPEED = 140;
- private readonly FAST_SPEED = 640;
-
- constructor(private eventBus: IEventBus) {
- this.subscribeToEvents();
- document.addEventListener('pointermove', this.trackMouse);
- }
-
- init(scrollableContent: HTMLElement): void {
- this.scrollableContent = scrollableContent;
- this.timeGrid = scrollableContent.querySelector('swp-time-grid');
- this.scrollableContent.style.scrollBehavior = 'auto';
- }
-
- private trackMouse = (e: PointerEvent): void => {
- if (this.isDragging) {
- this.mouseY = e.clientY;
- }
- };
-
- private subscribeToEvents(): void {
- this.eventBus.on(CoreEvents.EVENT_DRAG_START, (event: Event) => {
- const payload = (event as CustomEvent).detail;
- this.draggedElement = payload.element;
- this.startDrag();
- });
-
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => this.stopDrag());
- this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => this.stopDrag());
- }
-
- private startDrag(): void {
- this.isDragging = true;
- this.isScrolling = false;
- this.lastTs = 0;
- this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
-
- if (this.scrollRAF === null) {
- this.scrollRAF = requestAnimationFrame(this.scrollTick);
- }
- }
-
- private stopDrag(): void {
- this.isDragging = false;
- this.setScrollingState(false);
-
- if (this.scrollRAF !== null) {
- cancelAnimationFrame(this.scrollRAF);
- this.scrollRAF = null;
- }
-
- this.rect = null;
- this.lastTs = 0;
- this.initialScrollTop = 0;
- }
-
- private calculateVelocity(): number {
- if (!this.rect) return 0;
-
- const distTop = this.mouseY - this.rect.top;
- const distBot = this.rect.bottom - this.mouseY;
-
- if (distTop < this.INNER_ZONE) return -this.FAST_SPEED;
- if (distTop < this.OUTER_ZONE) return -this.SLOW_SPEED;
- if (distBot < this.INNER_ZONE) return this.FAST_SPEED;
- if (distBot < this.OUTER_ZONE) return this.SLOW_SPEED;
-
- return 0;
- }
-
- private isAtBoundary(velocity: number): boolean {
- if (!this.scrollableContent || !this.timeGrid || !this.draggedElement) return false;
-
- const atTop = this.scrollableContent.scrollTop <= 0 && velocity < 0;
- const atBottom = velocity > 0 &&
- this.draggedElement.getBoundingClientRect().bottom >=
- this.timeGrid.getBoundingClientRect().bottom;
-
- return atTop || atBottom;
- }
-
- private setScrollingState(scrolling: boolean): void {
- if (this.isScrolling === scrolling) return;
-
- this.isScrolling = scrolling;
- if (scrolling) {
- this.eventBus.emit(CoreEvents.EDGE_SCROLL_STARTED, {});
- } else {
- this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
- this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
- }
- }
-
- private scrollTick = (ts: number): void => {
- if (!this.isDragging || !this.scrollableContent) return;
-
- const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
- this.lastTs = ts;
- this.rect ??= this.scrollableContent.getBoundingClientRect();
-
- const velocity = this.calculateVelocity();
-
- if (velocity !== 0 && !this.isAtBoundary(velocity)) {
- const scrollDelta = velocity * dt;
- this.scrollableContent.scrollTop += scrollDelta;
- this.rect = null;
- this.eventBus.emit(CoreEvents.EDGE_SCROLL_TICK, { scrollDelta });
- this.setScrollingState(true);
- } else {
- this.setScrollingState(false);
- }
-
- this.scrollRAF = requestAnimationFrame(this.scrollTick);
- };
-}
diff --git a/packages/calendar/src/managers/EventPersistenceManager.ts b/packages/calendar/src/managers/EventPersistenceManager.ts
deleted file mode 100644
index ae59df9..0000000
--- a/packages/calendar/src/managers/EventPersistenceManager.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * EventPersistenceManager - Persists event changes to IndexedDB
- *
- * Listens to drag/resize events and updates IndexedDB via EventService.
- * This bridges the gap between UI interactions and data persistence.
- */
-
-import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../types/CalendarTypes';
-import { EventService } from '../storage/events/EventService';
-import { DateService } from '../core/DateService';
-import { CoreEvents } from '../constants/CoreEvents';
-import { IDragEndPayload } from '../types/DragTypes';
-import { IResizeEndPayload } from '../types/ResizeTypes';
-
-export class EventPersistenceManager {
- constructor(
- private eventService: EventService,
- private eventBus: IEventBus,
- private dateService: DateService
- ) {
- this.setupListeners();
- }
-
- private setupListeners(): void {
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, this.handleDragEnd);
- this.eventBus.on(CoreEvents.EVENT_RESIZE_END, this.handleResizeEnd);
- }
-
- /**
- * Handle drag end - update event position in IndexedDB
- */
- private handleDragEnd = async (e: Event): Promise => {
- const payload = (e as CustomEvent).detail;
- const { swpEvent } = payload;
-
- // Get existing event to merge with
- const event = await this.eventService.get(swpEvent.eventId);
- if (!event) {
- console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`);
- return;
- }
-
- // Parse resourceId from columnKey if present
- const { resource } = this.dateService.parseColumnKey(swpEvent.columnKey);
-
- // Update and save - start/end already calculated in SwpEvent
- // Set allDay based on drop target:
- // - header: allDay = true
- // - grid: allDay = false (converts allDay event to timed)
- const updatedEvent: ICalendarEvent = {
- ...event,
- start: swpEvent.start,
- end: swpEvent.end,
- resourceId: resource ?? event.resourceId,
- allDay: payload.target === 'header',
- syncStatus: 'pending'
- };
-
- await this.eventService.save(updatedEvent);
-
- // Emit EVENT_UPDATED for EventRenderer to re-render affected columns
- const updatePayload: IEventUpdatedPayload = {
- eventId: updatedEvent.id,
- sourceColumnKey: payload.sourceColumnKey,
- targetColumnKey: swpEvent.columnKey
- };
- this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
- };
-
- /**
- * Handle resize end - update event duration in IndexedDB
- */
- private handleResizeEnd = async (e: Event): Promise => {
- const payload = (e as CustomEvent).detail;
- const { swpEvent } = payload;
-
- // Get existing event to merge with
- const event = await this.eventService.get(swpEvent.eventId);
- if (!event) {
- console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`);
- return;
- }
-
- // Update and save - end already calculated in SwpEvent
- const updatedEvent: ICalendarEvent = {
- ...event,
- end: swpEvent.end,
- syncStatus: 'pending'
- };
-
- await this.eventService.save(updatedEvent);
-
- // Emit EVENT_UPDATED for EventRenderer to re-render the column
- // Resize stays in same column, so source and target are the same
- const updatePayload: IEventUpdatedPayload = {
- eventId: updatedEvent.id,
- sourceColumnKey: swpEvent.columnKey,
- targetColumnKey: swpEvent.columnKey
- };
- this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
- };
-}
diff --git a/packages/calendar/src/managers/ResizeManager.ts b/packages/calendar/src/managers/ResizeManager.ts
deleted file mode 100644
index 5448def..0000000
--- a/packages/calendar/src/managers/ResizeManager.ts
+++ /dev/null
@@ -1,290 +0,0 @@
-import { IEventBus } from '../types/CalendarTypes';
-import { IGridConfig } from '../core/IGridConfig';
-import { pixelsToMinutes, minutesToPixels, snapToGrid } from '../utils/PositionUtils';
-import { DateService } from '../core/DateService';
-import { CoreEvents } from '../constants/CoreEvents';
-import { IResizeStartPayload, IResizeEndPayload } from '../types/ResizeTypes';
-import { SwpEvent } from '../types/SwpEvent';
-
-/**
- * ResizeManager - Handles resize of calendar events
- *
- * Step 1: Handle creation on mouseover (CSS handles visibility)
- * Step 2: Pointer events + resize start
- * Step 3: RAF animation for smooth height update
- * Step 4: Grid snapping + timestamp update
- */
-
-interface ResizeState {
- eventId: string;
- element: HTMLElement;
- handleElement: HTMLElement;
- startY: number;
- startHeight: number;
- startDurationMinutes: number;
- pointerId: number;
- prevZIndex: string;
- // Animation state
- currentHeight: number;
- targetHeight: number;
- animationId: number | null;
-}
-
-export class ResizeManager {
- private container: HTMLElement | null = null;
- private resizeState: ResizeState | null = null;
-
- private readonly Z_INDEX_RESIZING = '1000';
- private readonly ANIMATION_SPEED = 0.35;
- private readonly MIN_HEIGHT_MINUTES = 15;
-
- constructor(
- private eventBus: IEventBus,
- private gridConfig: IGridConfig,
- private dateService: DateService
- ) {}
-
- /**
- * Initialize resize functionality on container
- */
- init(container: HTMLElement): void {
- this.container = container;
-
- // Mouseover listener for handle creation (capture phase like V1)
- container.addEventListener('mouseover', this.handleMouseOver, true);
-
- // Pointer listeners for resize (capture phase like V1)
- document.addEventListener('pointerdown', this.handlePointerDown, true);
- document.addEventListener('pointermove', this.handlePointerMove, true);
- document.addEventListener('pointerup', this.handlePointerUp, true);
- }
-
- /**
- * Create resize handle element
- */
- private createResizeHandle(): HTMLElement {
- const handle = document.createElement('swp-resize-handle');
- handle.setAttribute('aria-label', 'Resize event');
- handle.setAttribute('role', 'separator');
- return handle;
- }
-
- /**
- * Handle mouseover - create resize handle if not exists
- */
- private handleMouseOver = (e: Event): void => {
- const target = e.target as HTMLElement;
- const eventElement = target.closest('swp-event') as HTMLElement;
-
- if (!eventElement || this.resizeState) return;
-
- // Check if handle already exists
- if (!eventElement.querySelector(':scope > swp-resize-handle')) {
- const handle = this.createResizeHandle();
- eventElement.appendChild(handle);
- }
- };
-
- /**
- * Handle pointerdown - start resize if on handle
- */
- private handlePointerDown = (e: PointerEvent): void => {
- const handle = (e.target as HTMLElement).closest('swp-resize-handle') as HTMLElement;
- if (!handle) return;
-
- const element = handle.parentElement as HTMLElement;
- if (!element) return;
-
- const eventId = element.dataset.eventId || '';
- const startHeight = element.offsetHeight;
- const startDurationMinutes = pixelsToMinutes(startHeight, this.gridConfig);
-
- // Store previous z-index
- const container = element.closest('swp-event-group') as HTMLElement ?? element;
- const prevZIndex = container.style.zIndex;
-
- // Set resize state
- this.resizeState = {
- eventId,
- element,
- handleElement: handle,
- startY: e.clientY,
- startHeight,
- startDurationMinutes,
- pointerId: e.pointerId,
- prevZIndex,
- // Animation state
- currentHeight: startHeight,
- targetHeight: startHeight,
- animationId: null
- };
-
- // Elevate z-index
- container.style.zIndex = this.Z_INDEX_RESIZING;
-
- // Capture pointer for smooth tracking
- try {
- handle.setPointerCapture(e.pointerId);
- } catch (err) {
- console.warn('Pointer capture failed:', err);
- }
-
- // Add global resizing class
- document.documentElement.classList.add('swp--resizing');
-
- // Emit resize start event
- this.eventBus.emit(CoreEvents.EVENT_RESIZE_START, {
- eventId,
- element,
- startHeight
- } as IResizeStartPayload);
-
- e.preventDefault();
- };
-
- /**
- * Handle pointermove - update target height during resize
- */
- private handlePointerMove = (e: PointerEvent): void => {
- if (!this.resizeState) return;
-
- const deltaY = e.clientY - this.resizeState.startY;
- const minHeight = (this.MIN_HEIGHT_MINUTES / 60) * this.gridConfig.hourHeight;
- const newHeight = Math.max(minHeight, this.resizeState.startHeight + deltaY);
-
- // Set target height for animation
- this.resizeState.targetHeight = newHeight;
-
- // Start animation if not running
- if (this.resizeState.animationId === null) {
- this.animateHeight();
- }
- };
-
- /**
- * RAF animation loop for smooth height interpolation
- */
- private animateHeight = (): void => {
- if (!this.resizeState) return;
-
- const diff = this.resizeState.targetHeight - this.resizeState.currentHeight;
-
- // Stop animation when close enough
- if (Math.abs(diff) < 0.5) {
- this.resizeState.animationId = null;
- return;
- }
-
- // Interpolate towards target (35% per frame like V1)
- this.resizeState.currentHeight += diff * this.ANIMATION_SPEED;
- this.resizeState.element.style.height = `${this.resizeState.currentHeight}px`;
-
- // Update timestamp display (snapped)
- this.updateTimestampDisplay();
-
- // Continue animation
- this.resizeState.animationId = requestAnimationFrame(this.animateHeight);
- };
-
- /**
- * Update timestamp display with snapped end time
- */
- private updateTimestampDisplay(): void {
- if (!this.resizeState) return;
-
- const timeEl = this.resizeState.element.querySelector('swp-event-time');
- if (!timeEl) return;
-
- // Get start time from element position
- const top = parseFloat(this.resizeState.element.style.top) || 0;
- const startMinutesFromGrid = pixelsToMinutes(top, this.gridConfig);
- const startMinutes = (this.gridConfig.dayStartHour * 60) + startMinutesFromGrid;
-
- // Calculate snapped end time from current height
- const snappedHeight = snapToGrid(this.resizeState.currentHeight, this.gridConfig);
- const durationMinutes = pixelsToMinutes(snappedHeight, this.gridConfig);
- const endMinutes = startMinutes + durationMinutes;
-
- // Format and update
- const start = this.minutesToDate(startMinutes);
- const end = this.minutesToDate(endMinutes);
- timeEl.textContent = this.dateService.formatTimeRange(start, end);
- }
-
- /**
- * Convert minutes since midnight to Date
- */
- private minutesToDate(minutes: number): Date {
- const date = new Date();
- date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0);
- return date;
- };
-
- /**
- * Handle pointerup - finish resize
- */
- private handlePointerUp = (e: PointerEvent): void => {
- if (!this.resizeState) return;
-
- // Cancel any pending animation
- if (this.resizeState.animationId !== null) {
- cancelAnimationFrame(this.resizeState.animationId);
- }
-
- // Release pointer capture
- try {
- this.resizeState.handleElement.releasePointerCapture(e.pointerId);
- } catch (err) {
- console.warn('Pointer release failed:', err);
- }
-
- // Snap final height to grid
- this.snapToGridFinal();
-
- // Update timestamp one final time
- this.updateTimestampDisplay();
-
- // Restore z-index
- const container = this.resizeState.element.closest('swp-event-group') as HTMLElement ?? this.resizeState.element;
- container.style.zIndex = this.resizeState.prevZIndex;
-
- // Remove global resizing class
- document.documentElement.classList.remove('swp--resizing');
-
- // Get columnKey and date from parent column
- const column = this.resizeState.element.closest('swp-day-column') as HTMLElement;
- const columnKey = column?.dataset.columnKey || '';
- const date = column?.dataset.date || '';
-
- // Create SwpEvent from element (reads top/height/eventId from element)
- const swpEvent = SwpEvent.fromElement(
- this.resizeState.element,
- columnKey,
- date,
- this.gridConfig
- );
-
- // Emit resize end event
- this.eventBus.emit(CoreEvents.EVENT_RESIZE_END, {
- swpEvent
- } as IResizeEndPayload);
-
- // Reset state
- this.resizeState = null;
- };
-
- /**
- * Snap final height to grid interval
- */
- private snapToGridFinal(): void {
- if (!this.resizeState) return;
-
- const currentHeight = this.resizeState.element.offsetHeight;
- const snappedHeight = snapToGrid(currentHeight, this.gridConfig);
- const minHeight = minutesToPixels(this.MIN_HEIGHT_MINUTES, this.gridConfig);
- const finalHeight = Math.max(minHeight, snappedHeight);
-
- this.resizeState.element.style.height = `${finalHeight}px`;
- this.resizeState.currentHeight = finalHeight;
- }
-}
diff --git a/packages/calendar/src/repositories/IApiRepository.ts b/packages/calendar/src/repositories/IApiRepository.ts
deleted file mode 100644
index a50791f..0000000
--- a/packages/calendar/src/repositories/IApiRepository.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { EntityType } from '../types/CalendarTypes';
-
-/**
- * IApiRepository - Generic interface for backend API communication
- *
- * Used by DataSeeder to fetch initial data and by SyncManager for sync operations.
- */
-export interface IApiRepository {
- /**
- * Entity type discriminator - used for runtime routing
- */
- readonly entityType: EntityType;
-
- /**
- * Send create operation to backend API
- */
- sendCreate(data: T): Promise;
-
- /**
- * Send update operation to backend API
- */
- sendUpdate(id: string, updates: Partial): Promise;
-
- /**
- * Send delete operation to backend API
- */
- sendDelete(id: string): Promise;
-
- /**
- * Fetch all entities from backend API
- */
- fetchAll(): Promise;
-}
diff --git a/packages/calendar/src/storage/BaseEntityService.ts b/packages/calendar/src/storage/BaseEntityService.ts
deleted file mode 100644
index ed8d3a1..0000000
--- a/packages/calendar/src/storage/BaseEntityService.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import { ISync, EntityType, SyncStatus, IEventBus, IEntitySavedPayload, IEntityDeletedPayload } from '../types/CalendarTypes';
-import { IEntityService } from './IEntityService';
-import { SyncPlugin } from './SyncPlugin';
-import { IndexedDBContext } from './IndexedDBContext';
-import { CoreEvents } from '../constants/CoreEvents';
-import { diff } from 'json-diff-ts';
-
-/**
- * BaseEntityService - Abstract base class for all entity services
- *
- * PROVIDES:
- * - Generic CRUD operations (get, getAll, save, delete)
- * - Sync status management (delegates to SyncPlugin)
- * - Serialization hooks (override in subclass if needed)
- */
-export abstract class BaseEntityService implements IEntityService {
- abstract readonly storeName: string;
- abstract readonly entityType: EntityType;
-
- private syncPlugin: SyncPlugin;
- private context: IndexedDBContext;
- protected eventBus: IEventBus;
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- this.context = context;
- this.eventBus = eventBus;
- this.syncPlugin = new SyncPlugin(this);
- }
-
- protected get db(): IDBDatabase {
- return this.context.getDatabase();
- }
-
- /**
- * Serialize entity before storing in IndexedDB
- */
- protected serialize(entity: T): unknown {
- return entity;
- }
-
- /**
- * Deserialize data from IndexedDB back to entity
- */
- protected deserialize(data: unknown): T {
- return data as T;
- }
-
- /**
- * Get a single entity by ID
- */
- async get(id: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const request = store.get(id);
-
- request.onsuccess = () => {
- const data = request.result;
- resolve(data ? this.deserialize(data) : null);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`));
- };
- });
- }
-
- /**
- * Get all entities
- */
- async getAll(): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const request = store.getAll();
-
- request.onsuccess = () => {
- const data = request.result as unknown[];
- const entities = data.map(item => this.deserialize(item));
- resolve(entities);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`));
- };
- });
- }
-
- /**
- * Save an entity (create or update)
- * Emits ENTITY_SAVED event with operation type and changes (diff for updates)
- * @param entity - Entity to save
- * @param silent - If true, skip event emission (used for seeding)
- */
- async save(entity: T, silent = false): Promise {
- const entityId = (entity as unknown as { id: string }).id;
- const existingEntity = await this.get(entityId);
- const isCreate = existingEntity === null;
-
- // Calculate changes: full entity for create, diff for update
- let changes: unknown;
- if (isCreate) {
- changes = entity;
- } else {
- const existingSerialized = this.serialize(existingEntity);
- const newSerialized = this.serialize(entity);
- changes = diff(existingSerialized, newSerialized);
- }
-
- const serialized = this.serialize(entity);
-
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readwrite');
- const store = transaction.objectStore(this.storeName);
- const request = store.put(serialized);
-
- request.onsuccess = () => {
- // Only emit event if not silent (silent used for seeding)
- if (!silent) {
- const payload: IEntitySavedPayload = {
- entityType: this.entityType,
- entityId,
- operation: isCreate ? 'create' : 'update',
- changes,
- timestamp: Date.now()
- };
- this.eventBus.emit(CoreEvents.ENTITY_SAVED, payload);
- }
- resolve();
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`));
- };
- });
- }
-
- /**
- * Delete an entity
- * Emits ENTITY_DELETED event
- */
- async delete(id: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readwrite');
- const store = transaction.objectStore(this.storeName);
- const request = store.delete(id);
-
- request.onsuccess = () => {
- const payload: IEntityDeletedPayload = {
- entityType: this.entityType,
- entityId: id,
- operation: 'delete',
- timestamp: Date.now()
- };
- this.eventBus.emit(CoreEvents.ENTITY_DELETED, payload);
- resolve();
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`));
- };
- });
- }
-
- // Sync methods - delegate to SyncPlugin
- async markAsSynced(id: string): Promise {
- return this.syncPlugin.markAsSynced(id);
- }
-
- async markAsError(id: string): Promise {
- return this.syncPlugin.markAsError(id);
- }
-
- async getSyncStatus(id: string): Promise {
- return this.syncPlugin.getSyncStatus(id);
- }
-
- async getBySyncStatus(syncStatus: string): Promise {
- return this.syncPlugin.getBySyncStatus(syncStatus);
- }
-}
diff --git a/packages/calendar/src/storage/IEntityService.ts b/packages/calendar/src/storage/IEntityService.ts
deleted file mode 100644
index 800ea62..0000000
--- a/packages/calendar/src/storage/IEntityService.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { ISync, EntityType, SyncStatus } from '../types/CalendarTypes';
-
-/**
- * IEntityService - Generic interface for entity services with sync capabilities
- *
- * All entity services implement this interface to enable polymorphic operations.
- */
-export interface IEntityService {
- /**
- * Entity type discriminator for runtime routing
- */
- readonly entityType: EntityType;
-
- /**
- * Get all entities from IndexedDB
- */
- getAll(): Promise;
-
- /**
- * Save an entity (create or update) to IndexedDB
- * @param entity - Entity to save
- * @param silent - If true, skip event emission (used for seeding)
- */
- save(entity: T, silent?: boolean): Promise;
-
- /**
- * Mark entity as successfully synced
- */
- markAsSynced(id: string): Promise;
-
- /**
- * Mark entity as sync error
- */
- markAsError(id: string): Promise;
-
- /**
- * Get current sync status for an entity
- */
- getSyncStatus(id: string): Promise;
-}
diff --git a/packages/calendar/src/storage/IStore.ts b/packages/calendar/src/storage/IStore.ts
deleted file mode 100644
index 91ac873..0000000
--- a/packages/calendar/src/storage/IStore.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * IStore - Interface for IndexedDB ObjectStore definitions
- *
- * Each entity store implements this interface to define its schema.
- * Enables Open/Closed Principle: IndexedDBContext works with any IStore.
- */
-export interface IStore {
- /**
- * The name of the ObjectStore in IndexedDB
- */
- readonly storeName: string;
-
- /**
- * Create the ObjectStore with its schema (indexes, keyPath, etc.)
- * Called during database upgrade (onupgradeneeded event)
- */
- create(db: IDBDatabase): void;
-}
diff --git a/packages/calendar/src/storage/IndexedDBContext.ts b/packages/calendar/src/storage/IndexedDBContext.ts
deleted file mode 100644
index ab9e9ab..0000000
--- a/packages/calendar/src/storage/IndexedDBContext.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { IStore } from './IStore';
-
-/**
- * Database configuration
- */
-export interface IDBConfig {
- dbName: string;
- dbVersion?: number;
-}
-
-export const defaultDBConfig: IDBConfig = {
- dbName: 'CalendarDB',
- dbVersion: 4
-};
-
-/**
- * IndexedDBContext - Database connection manager
- *
- * RESPONSIBILITY:
- * - Opens and manages IDBDatabase connection lifecycle
- * - Creates object stores via injected IStore implementations
- * - Provides shared IDBDatabase instance to all services
- */
-export class IndexedDBContext {
- private db: IDBDatabase | null = null;
- private initialized: boolean = false;
- private stores: IStore[];
- private config: IDBConfig;
-
- constructor(stores: IStore[], config: IDBConfig) {
- this.stores = stores;
- this.config = config;
- }
-
- get dbName(): string {
- return this.config.dbName;
- }
-
- /**
- * Initialize and open the database
- */
- async initialize(): Promise {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.config.dbName, this.config.dbVersion);
-
- request.onerror = () => {
- reject(new Error(`Failed to open IndexedDB: ${request.error}`));
- };
-
- request.onsuccess = () => {
- this.db = request.result;
- this.initialized = true;
- resolve();
- };
-
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result;
-
- // Create all entity stores via injected IStore implementations
- this.stores.forEach(store => {
- if (!db.objectStoreNames.contains(store.storeName)) {
- store.create(db);
- }
- });
- };
- });
- }
-
- /**
- * Check if database is initialized
- */
- public isInitialized(): boolean {
- return this.initialized;
- }
-
- /**
- * Get IDBDatabase instance
- */
- public getDatabase(): IDBDatabase {
- if (!this.db) {
- throw new Error('IndexedDB not initialized. Call initialize() first.');
- }
- return this.db;
- }
-
- /**
- * Close database connection
- */
- close(): void {
- if (this.db) {
- this.db.close();
- this.db = null;
- this.initialized = false;
- }
- }
-
- /**
- * Delete entire database (for testing/reset)
- */
- static async deleteDatabase(dbName: string = defaultDBConfig.dbName): Promise {
- return new Promise((resolve, reject) => {
- const request = indexedDB.deleteDatabase(dbName);
-
- request.onsuccess = () => resolve();
- request.onerror = () => reject(new Error(`Failed to delete database: ${request.error}`));
- });
- }
-}
diff --git a/packages/calendar/src/storage/SyncPlugin.ts b/packages/calendar/src/storage/SyncPlugin.ts
deleted file mode 100644
index 7774da6..0000000
--- a/packages/calendar/src/storage/SyncPlugin.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { ISync, SyncStatus } from '../types/CalendarTypes';
-
-/**
- * SyncPlugin - Pluggable sync functionality for entity services
- *
- * COMPOSITION PATTERN:
- * - Encapsulates all sync-related logic in separate class
- * - Composed into BaseEntityService (not inheritance)
- */
-export class SyncPlugin {
- constructor(private service: any) {}
-
- /**
- * Mark entity as successfully synced
- */
- async markAsSynced(id: string): Promise {
- const entity = await this.service.get(id);
- if (entity) {
- entity.syncStatus = 'synced';
- await this.service.save(entity);
- }
- }
-
- /**
- * Mark entity as sync error
- */
- async markAsError(id: string): Promise {
- const entity = await this.service.get(id);
- if (entity) {
- entity.syncStatus = 'error';
- await this.service.save(entity);
- }
- }
-
- /**
- * Get current sync status for an entity
- */
- async getSyncStatus(id: string): Promise {
- const entity = await this.service.get(id);
- return entity ? entity.syncStatus : null;
- }
-
- /**
- * Get entities by sync status using IndexedDB index
- */
- async getBySyncStatus(syncStatus: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.service.db.transaction([this.service.storeName], 'readonly');
- const store = transaction.objectStore(this.service.storeName);
- const index = store.index('syncStatus');
- const request = index.getAll(syncStatus);
-
- request.onsuccess = () => {
- const data = request.result as unknown[];
- const entities = data.map(item => this.service.deserialize(item));
- resolve(entities);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get by sync status ${syncStatus}: ${request.error}`));
- };
- });
- }
-}
diff --git a/packages/calendar/src/storage/events/EventSerialization.ts b/packages/calendar/src/storage/events/EventSerialization.ts
deleted file mode 100644
index 583fa79..0000000
--- a/packages/calendar/src/storage/events/EventSerialization.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ICalendarEvent } from '../../types/CalendarTypes';
-
-/**
- * EventSerialization - Handles Date field serialization for IndexedDB
- *
- * IndexedDB doesn't store Date objects directly, so we convert:
- * - Date → ISO string (serialize) when writing
- * - ISO string → Date (deserialize) when reading
- */
-export class EventSerialization {
- /**
- * Serialize event for IndexedDB storage
- */
- static serialize(event: ICalendarEvent): unknown {
- return {
- ...event,
- start: event.start instanceof Date ? event.start.toISOString() : event.start,
- end: event.end instanceof Date ? event.end.toISOString() : event.end
- };
- }
-
- /**
- * Deserialize event from IndexedDB storage
- */
- static deserialize(data: Record): ICalendarEvent {
- return {
- ...data,
- start: typeof data.start === 'string' ? new Date(data.start) : data.start,
- end: typeof data.end === 'string' ? new Date(data.end) : data.end
- } as ICalendarEvent;
- }
-}
diff --git a/packages/calendar/src/storage/events/EventService.ts b/packages/calendar/src/storage/events/EventService.ts
deleted file mode 100644
index 0ccd5a5..0000000
--- a/packages/calendar/src/storage/events/EventService.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { ICalendarEvent, EntityType, IEventBus } from '../../types/CalendarTypes';
-import { EventStore } from './EventStore';
-import { EventSerialization } from './EventSerialization';
-import { BaseEntityService } from '../BaseEntityService';
-import { IndexedDBContext } from '../IndexedDBContext';
-
-/**
- * EventService - CRUD operations for calendar events in IndexedDB
- *
- * Extends BaseEntityService for shared CRUD and sync logic.
- * Provides event-specific query methods.
- */
-export class EventService extends BaseEntityService {
- readonly storeName = EventStore.STORE_NAME;
- readonly entityType: EntityType = 'Event';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- protected serialize(event: ICalendarEvent): unknown {
- return EventSerialization.serialize(event);
- }
-
- protected deserialize(data: unknown): ICalendarEvent {
- return EventSerialization.deserialize(data as Record);
- }
-
- /**
- * Get events within a date range
- */
- async getByDateRange(start: Date, end: Date): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('start');
-
- const range = IDBKeyRange.lowerBound(start.toISOString());
- const request = index.getAll(range);
-
- request.onsuccess = () => {
- const data = request.result as unknown[];
- const events = data
- .map(item => this.deserialize(item))
- .filter(event => event.start <= end);
- resolve(events);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get events by date range: ${request.error}`));
- };
- });
- }
-
- /**
- * Get events for a specific resource
- */
- async getByResource(resourceId: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('resourceId');
- const request = index.getAll(resourceId);
-
- request.onsuccess = () => {
- const data = request.result as unknown[];
- const events = data.map(item => this.deserialize(item));
- resolve(events);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get events for resource ${resourceId}: ${request.error}`));
- };
- });
- }
-
- /**
- * Get events for a resource within a date range
- */
- async getByResourceAndDateRange(resourceId: string, start: Date, end: Date): Promise {
- const resourceEvents = await this.getByResource(resourceId);
- return resourceEvents.filter(event => event.start >= start && event.start <= end);
- }
-}
diff --git a/packages/calendar/src/storage/events/EventStore.ts b/packages/calendar/src/storage/events/EventStore.ts
deleted file mode 100644
index 21c7be0..0000000
--- a/packages/calendar/src/storage/events/EventStore.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { IStore } from '../IStore';
-
-/**
- * EventStore - IndexedDB ObjectStore definition for calendar events
- */
-export class EventStore implements IStore {
- static readonly STORE_NAME = 'events';
- readonly storeName = EventStore.STORE_NAME;
-
- /**
- * Create the events ObjectStore with indexes
- */
- create(db: IDBDatabase): void {
- const store = db.createObjectStore(EventStore.STORE_NAME, { keyPath: 'id' });
-
- // Index: start (for date range queries)
- store.createIndex('start', 'start', { unique: false });
-
- // Index: end (for date range queries)
- store.createIndex('end', 'end', { unique: false });
-
- // Index: syncStatus (for filtering by sync state)
- store.createIndex('syncStatus', 'syncStatus', { unique: false });
-
- // Index: resourceId (for resource-mode filtering)
- store.createIndex('resourceId', 'resourceId', { unique: false });
-
- // Index: customerId (for customer-centric queries)
- store.createIndex('customerId', 'customerId', { unique: false });
-
- // Index: bookingId (for event-to-booking lookups)
- store.createIndex('bookingId', 'bookingId', { unique: false });
-
- // Compound index: startEnd (for optimized range queries)
- store.createIndex('startEnd', ['start', 'end'], { unique: false });
- }
-}
diff --git a/packages/calendar/src/storage/resources/ResourceService.ts b/packages/calendar/src/storage/resources/ResourceService.ts
deleted file mode 100644
index 769210c..0000000
--- a/packages/calendar/src/storage/resources/ResourceService.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { IResource, EntityType, IEventBus } from '../../types/CalendarTypes';
-import { ResourceStore } from './ResourceStore';
-import { BaseEntityService } from '../BaseEntityService';
-import { IndexedDBContext } from '../IndexedDBContext';
-
-/**
- * ResourceService - CRUD operations for resources in IndexedDB
- */
-export class ResourceService extends BaseEntityService {
- readonly storeName = ResourceStore.STORE_NAME;
- readonly entityType: EntityType = 'Resource';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- /**
- * Get all active resources
- */
- async getActive(): Promise {
- const all = await this.getAll();
- return all.filter(r => r.isActive !== false);
- }
-
- /**
- * Get resources by IDs
- */
- async getByIds(ids: string[]): Promise {
- if (ids.length === 0) return [];
-
- const results = await Promise.all(ids.map(id => this.get(id)));
- return results.filter((r): r is IResource => r !== null);
- }
-
- /**
- * Get resources by type
- */
- async getByType(type: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], 'readonly');
- const store = transaction.objectStore(this.storeName);
- const index = store.index('type');
- const request = index.getAll(type);
-
- request.onsuccess = () => {
- const data = request.result as IResource[];
- resolve(data);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get resources by type ${type}: ${request.error}`));
- };
- });
- }
-}
diff --git a/packages/calendar/src/storage/resources/ResourceStore.ts b/packages/calendar/src/storage/resources/ResourceStore.ts
deleted file mode 100644
index 38e39b6..0000000
--- a/packages/calendar/src/storage/resources/ResourceStore.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { IStore } from '../IStore';
-
-/**
- * ResourceStore - IndexedDB ObjectStore definition for resources
- */
-export class ResourceStore implements IStore {
- static readonly STORE_NAME = 'resources';
- readonly storeName = ResourceStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- const store = db.createObjectStore(ResourceStore.STORE_NAME, { keyPath: 'id' });
-
- store.createIndex('type', 'type', { unique: false });
- store.createIndex('syncStatus', 'syncStatus', { unique: false });
- store.createIndex('isActive', 'isActive', { unique: false });
- }
-}
diff --git a/packages/calendar/src/storage/settings/SettingsService.ts b/packages/calendar/src/storage/settings/SettingsService.ts
deleted file mode 100644
index 5bc57b4..0000000
--- a/packages/calendar/src/storage/settings/SettingsService.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { EntityType, IEventBus } from '../../types/CalendarTypes';
-import {
- TenantSetting,
- IWorkweekSettings,
- IGridSettings,
- ITimeFormatSettings,
- IViewSettings,
- IWorkweekPreset,
- SettingsIds
-} from '../../types/SettingsTypes';
-import { SettingsStore } from './SettingsStore';
-import { BaseEntityService } from '../BaseEntityService';
-import { IndexedDBContext } from '../IndexedDBContext';
-
-/**
- * SettingsService - CRUD operations for tenant settings
- *
- * Settings are stored as separate records per section.
- * This service provides typed methods for accessing specific settings.
- */
-export class SettingsService extends BaseEntityService {
- readonly storeName = SettingsStore.STORE_NAME;
- readonly entityType: EntityType = 'Settings';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- /**
- * Get workweek settings
- */
- async getWorkweekSettings(): Promise {
- return this.get(SettingsIds.WORKWEEK) as Promise;
- }
-
- /**
- * Get grid settings
- */
- async getGridSettings(): Promise {
- return this.get(SettingsIds.GRID) as Promise;
- }
-
- /**
- * Get time format settings
- */
- async getTimeFormatSettings(): Promise {
- return this.get(SettingsIds.TIME_FORMAT) as Promise;
- }
-
- /**
- * Get view settings
- */
- async getViewSettings(): Promise {
- return this.get(SettingsIds.VIEWS) as Promise;
- }
-
- /**
- * Get workweek preset by ID
- */
- async getWorkweekPreset(presetId: string): Promise {
- const settings = await this.getWorkweekSettings();
- if (!settings) return null;
- return settings.presets[presetId] || null;
- }
-
- /**
- * Get the default workweek preset
- */
- async getDefaultWorkweekPreset(): Promise {
- const settings = await this.getWorkweekSettings();
- if (!settings) return null;
- return settings.presets[settings.defaultPreset] || null;
- }
-
- /**
- * Get all available workweek presets
- */
- async getWorkweekPresets(): Promise {
- const settings = await this.getWorkweekSettings();
- if (!settings) return [];
- return Object.values(settings.presets);
- }
-}
diff --git a/packages/calendar/src/storage/settings/SettingsStore.ts b/packages/calendar/src/storage/settings/SettingsStore.ts
deleted file mode 100644
index a28cc79..0000000
--- a/packages/calendar/src/storage/settings/SettingsStore.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { IStore } from '../IStore';
-
-/**
- * SettingsStore - IndexedDB ObjectStore definition for tenant settings
- *
- * Single store for all settings sections. Settings are stored as one document
- * per tenant with id='tenant-settings'.
- */
-export class SettingsStore implements IStore {
- static readonly STORE_NAME = 'settings';
- readonly storeName = SettingsStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- db.createObjectStore(SettingsStore.STORE_NAME, { keyPath: 'id' });
- }
-}
diff --git a/packages/calendar/src/storage/viewconfigs/ViewConfigService.ts b/packages/calendar/src/storage/viewconfigs/ViewConfigService.ts
deleted file mode 100644
index 03a42f7..0000000
--- a/packages/calendar/src/storage/viewconfigs/ViewConfigService.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { EntityType, IEventBus } from '../../types/CalendarTypes';
-import { ViewConfig } from '../../core/ViewConfig';
-import { ViewConfigStore } from './ViewConfigStore';
-import { BaseEntityService } from '../BaseEntityService';
-import { IndexedDBContext } from '../IndexedDBContext';
-
-export class ViewConfigService extends BaseEntityService {
- readonly storeName = ViewConfigStore.STORE_NAME;
- readonly entityType: EntityType = 'ViewConfig';
-
- constructor(context: IndexedDBContext, eventBus: IEventBus) {
- super(context, eventBus);
- }
-
- async getById(id: string): Promise {
- return this.get(id);
- }
-}
diff --git a/packages/calendar/src/storage/viewconfigs/ViewConfigStore.ts b/packages/calendar/src/storage/viewconfigs/ViewConfigStore.ts
deleted file mode 100644
index fb02d07..0000000
--- a/packages/calendar/src/storage/viewconfigs/ViewConfigStore.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { IStore } from '../IStore';
-
-export class ViewConfigStore implements IStore {
- static readonly STORE_NAME = 'viewconfigs';
- readonly storeName = ViewConfigStore.STORE_NAME;
-
- create(db: IDBDatabase): void {
- db.createObjectStore(ViewConfigStore.STORE_NAME, { keyPath: 'id' });
- }
-}
diff --git a/packages/calendar/src/types/AuditTypes.ts b/packages/calendar/src/types/AuditTypes.ts
deleted file mode 100644
index 3c0eb9f..0000000
--- a/packages/calendar/src/types/AuditTypes.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { ISync, EntityType } from './CalendarTypes';
-
-/**
- * IAuditEntry - Audit log entry for tracking all entity changes
- *
- * Used for:
- * - Compliance and audit trail
- * - Sync tracking with backend
- * - Change history
- */
-export interface IAuditEntry extends ISync {
- /** Unique audit entry ID */
- id: string;
-
- /** Type of entity that was changed */
- entityType: EntityType;
-
- /** ID of the entity that was changed */
- entityId: string;
-
- /** Type of operation performed */
- operation: 'create' | 'update' | 'delete';
-
- /** User who made the change */
- userId: string;
-
- /** Timestamp when change was made */
- timestamp: number;
-
- /** Changes made (full entity for create, diff for update, { id } for delete) */
- changes: unknown;
-
- /** Whether this audit entry has been synced to backend */
- synced: boolean;
-}
-
-/**
- * IAuditLoggedPayload - Event payload when audit entry is logged
- */
-export interface IAuditLoggedPayload {
- auditId: string;
- entityType: EntityType;
- entityId: string;
- operation: 'create' | 'update' | 'delete';
- timestamp: number;
-}
diff --git a/packages/calendar/src/types/CalendarTypes.ts b/packages/calendar/src/types/CalendarTypes.ts
deleted file mode 100644
index c7aa21c..0000000
--- a/packages/calendar/src/types/CalendarTypes.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * Calendar Type Definitions
- */
-
-import { IWeekSchedule } from './ScheduleTypes';
-
-export type SyncStatus = 'synced' | 'pending' | 'error';
-
-export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit' | 'Settings' | 'ViewConfig';
-
-/**
- * CalendarEventType - Used by ICalendarEvent.type
- * Note: Only 'customer' events have associated IBooking
- */
-export type CalendarEventType =
- | 'customer' // Customer appointment (HAS booking)
- | 'vacation' // Vacation/time off (NO booking)
- | 'break' // Lunch/break (NO booking)
- | 'meeting' // Meeting (NO booking)
- | 'blocked'; // Blocked time (NO booking)
-
-/**
- * ISync - Interface for sync status tracking
- * All syncable entities should extend this interface
- */
-export interface ISync {
- syncStatus: SyncStatus;
-}
-
-/**
- * IDataEntity - Wrapper for entity data with typename discriminator
- */
-export interface IDataEntity {
- typename: EntityType;
- data: unknown;
-}
-
-export interface ICalendarEvent extends ISync {
- id: string;
- title: string;
- description?: string;
- start: Date;
- end: Date;
- type: CalendarEventType;
- allDay: boolean;
-
- // References (denormalized for IndexedDB performance)
- bookingId?: string; // Reference to booking (only if type = 'customer')
- resourceId?: string; // Resource who owns this time slot
- customerId?: string; // Denormalized from Booking.customerId
-
- recurringId?: string;
- metadata?: Record;
-}
-
-// EventBus types
-export interface IEventLogEntry {
- type: string;
- detail: unknown;
- timestamp: number;
-}
-
-export interface IListenerEntry {
- eventType: string;
- handler: EventListener;
- options?: AddEventListenerOptions;
-}
-
-export interface IEventBus {
- on(eventType: string, handler: EventListener, options?: AddEventListenerOptions): () => void;
- once(eventType: string, handler: EventListener): () => void;
- off(eventType: string, handler: EventListener): void;
- emit(eventType: string, detail?: unknown): boolean;
- getEventLog(eventType?: string): IEventLogEntry[];
- setDebug(enabled: boolean): void;
-}
-
-// Entity event payloads
-export interface IEntitySavedPayload {
- entityType: EntityType;
- entityId: string;
- operation: 'create' | 'update';
- changes: unknown;
- timestamp: number;
-}
-
-export interface IEntityDeletedPayload {
- entityType: EntityType;
- entityId: string;
- operation: 'delete';
- timestamp: number;
-}
-
-// Event update payload (for re-rendering columns after drag/resize)
-export interface IEventUpdatedPayload {
- eventId: string;
- sourceColumnKey: string; // Source column key (where event came from)
- targetColumnKey: string; // Target column key (where event landed)
-}
-
-// Resource types
-export type ResourceType =
- | 'person'
- | 'room'
- | 'equipment'
- | 'vehicle'
- | 'custom';
-
-export interface IResource extends ISync {
- id: string;
- name: string;
- displayName: string;
- type: ResourceType;
- avatarUrl?: string;
- color?: string;
- isActive?: boolean;
- defaultSchedule?: IWeekSchedule; // Default arbejdstider per ugedag
- metadata?: Record;
-}
-
-// Team types
-export interface ITeam extends ISync {
- id: string;
- name: string;
- resourceIds: string[];
-}
-
-// Department types
-export interface IDepartment extends ISync {
- id: string;
- name: string;
- resourceIds: string[];
-}
-
-// Booking types
-export type BookingStatus =
- | 'created'
- | 'arrived'
- | 'paid'
- | 'noshow'
- | 'cancelled';
-
-export interface IBookingService {
- serviceId: string;
- serviceName: string;
- baseDuration: number;
- basePrice: number;
- customPrice?: number;
- resourceId: string;
-}
-
-export interface IBooking extends ISync {
- id: string;
- customerId: string;
- status: BookingStatus;
- createdAt: Date;
- services: IBookingService[];
- totalPrice?: number;
- tags?: string[];
- notes?: string;
-}
-
-// Customer types
-export interface ICustomer extends ISync {
- id: string;
- name: string;
- phone: string;
- email?: string;
- metadata?: Record;
-}
diff --git a/packages/calendar/src/types/DragTypes.ts b/packages/calendar/src/types/DragTypes.ts
deleted file mode 100644
index d63c1a8..0000000
--- a/packages/calendar/src/types/DragTypes.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * DragTypes - Event payloads for drag-drop operations
- */
-
-import { SwpEvent } from './SwpEvent';
-
-export interface IMousePosition {
- x: number;
- y: number;
-}
-
-export interface IDragStartPayload {
- eventId: string;
- element: HTMLElement; // Original element (being dragged)
- ghostElement: HTMLElement; // Ghost clone (stays in place)
- startY: number; // Original Y position
- mouseOffset: IMousePosition; // Click position within element
- columnElement: HTMLElement;
-}
-
-export interface IDragMovePayload {
- eventId: string;
- element: HTMLElement;
- currentY: number; // Interpolated Y position (smooth)
- columnElement: HTMLElement;
-}
-
-export interface IDragEndPayload {
- swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, columnKey
- sourceColumnKey: string; // Source column key (where drag started)
- target: 'grid' | 'header'; // Where the event was dropped
-}
-
-export interface IDragCancelPayload {
- eventId: string;
- element: HTMLElement;
- startY: number; // Position to animate back to
-}
-
-export interface IDragColumnChangePayload {
- eventId: string;
- element: HTMLElement;
- previousColumn: HTMLElement;
- newColumn: HTMLElement;
- currentY: number;
-}
-
-// Header drag payloads
-export interface IDragEnterHeaderPayload {
- eventId: string;
- element: HTMLElement; // Original dragged element
- sourceColumnIndex: number;
- sourceColumnKey: string; // Opaque column identifier (for matching only)
- title: string;
- colorClass?: string;
- itemType: 'event' | 'reminder';
- duration: number; // Antal dage (default 1)
-}
-
-export interface IDragMoveHeaderPayload {
- eventId: string;
- columnIndex: number;
- columnKey: string; // Opaque column identifier (for matching only)
-}
-
-export interface IDragLeaveHeaderPayload {
- eventId: string;
- source: 'grid' | 'header'; // Where drag originated
- // Header→grid fields (when source === 'header')
- element?: HTMLElement; // Header item element
- targetColumn?: HTMLElement; // Target column in grid
- start?: Date; // Event start from header item
- end?: Date; // Event end from header item
- title?: string;
- colorClass?: string;
-}
diff --git a/packages/calendar/src/types/ResizeTypes.ts b/packages/calendar/src/types/ResizeTypes.ts
deleted file mode 100644
index 75caf6b..0000000
--- a/packages/calendar/src/types/ResizeTypes.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * ResizeTypes - Event payloads for resize operations
- */
-
-import { SwpEvent } from './SwpEvent';
-
-export interface IResizeStartPayload {
- eventId: string;
- element: HTMLElement;
- startHeight: number;
-}
-
-export interface IResizeEndPayload {
- swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, resourceId
-}
diff --git a/packages/calendar/src/types/ScheduleTypes.ts b/packages/calendar/src/types/ScheduleTypes.ts
deleted file mode 100644
index 65bfee7..0000000
--- a/packages/calendar/src/types/ScheduleTypes.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Schedule Types - Resource arbejdstider
- */
-
-// Genbrugelig tidsslot
-export interface ITimeSlot {
- start: string; // "HH:mm"
- end: string; // "HH:mm"
-}
-
-// Ugedag: 1=mandag, 7=søndag (ISO 8601)
-export type WeekDay = 1 | 2 | 3 | 4 | 5 | 6 | 7;
-
-// Default arbejdstider per ugedag
-export interface IWeekSchedule {
- [day: number]: ITimeSlot | null; // null = fri den dag
-}
-
-// Override for specifik dato
-export interface IScheduleOverride {
- id: string;
- resourceId: string;
- date: string; // "YYYY-MM-DD"
- schedule: ITimeSlot | null; // null = fri den dag
- breaks?: ITimeSlot[];
- syncStatus?: 'synced' | 'pending' | 'error';
-}
diff --git a/packages/calendar/src/types/SettingsTypes.ts b/packages/calendar/src/types/SettingsTypes.ts
deleted file mode 100644
index 5ae47aa..0000000
--- a/packages/calendar/src/types/SettingsTypes.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Tenant Settings Type Definitions
- *
- * Settings are tenant-specific configuration that comes from the backend
- * and is stored in IndexedDB for offline access.
- *
- * Each settings section is stored as a separate record with its own id.
- */
-
-import { ISync } from './CalendarTypes';
-
-/**
- * Workweek preset - defines which ISO weekdays to display
- * ISO: 1=Monday, 7=Sunday
- */
-export interface IWorkweekPreset {
- id: string;
- workDays: number[];
- label: string;
- periodDays: number; // Navigation step in days (1 = day, 7 = week)
-}
-
-/**
- * Workweek settings - stored as separate record
- */
-export interface IWorkweekSettings extends ISync {
- id: 'workweek';
- presets: Record;
- defaultPreset: string;
- firstDayOfWeek: number; // ISO: 1=Monday
-}
-
-/**
- * Grid display settings - stored as separate record
- */
-export interface IGridSettings extends ISync {
- id: 'grid';
- dayStartHour: number;
- dayEndHour: number;
- workStartHour: number;
- workEndHour: number;
- hourHeight: number;
- snapInterval: number;
-}
-
-/**
- * Time format settings - stored as separate record
- */
-export interface ITimeFormatSettings extends ISync {
- id: 'timeFormat';
- timezone: string;
- locale: string;
- use24HourFormat: boolean;
-}
-
-/**
- * View settings - stored as separate record
- */
-export interface IViewSettings extends ISync {
- id: 'views';
- availableViews: string[];
- defaultView: string;
-}
-
-/**
- * Union type for all tenant settings records
- */
-export type TenantSetting = IWorkweekSettings | IGridSettings | ITimeFormatSettings | IViewSettings;
-
-/**
- * Settings IDs as const for type safety
- */
-export const SettingsIds = {
- WORKWEEK: 'workweek',
- GRID: 'grid',
- TIME_FORMAT: 'timeFormat',
- VIEWS: 'views'
-} as const;
diff --git a/packages/calendar/src/types/SwpEvent.ts b/packages/calendar/src/types/SwpEvent.ts
deleted file mode 100644
index ff8373b..0000000
--- a/packages/calendar/src/types/SwpEvent.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { IGridConfig } from '../core/IGridConfig';
-
-/**
- * SwpEvent - Wrapper class for calendar event elements
- *
- * Encapsulates an HTMLElement and provides computed properties
- * for start/end times based on element position and grid config.
- *
- * Usage:
- * - eventId is read from element.dataset
- * - columnKey identifies the column uniformly
- * - Position (top, height) is read from element.style
- * - Factory method `fromElement()` calculates Date objects
- */
-export class SwpEvent {
- readonly element: HTMLElement;
- readonly columnKey: string;
- private _start: Date;
- private _end: Date;
-
- constructor(element: HTMLElement, columnKey: string, start: Date, end: Date) {
- this.element = element;
- this.columnKey = columnKey;
- this._start = start;
- this._end = end;
- }
-
- /** Event ID from element.dataset.eventId */
- get eventId(): string {
- return this.element.dataset.eventId || '';
- }
-
- get start(): Date {
- return this._start;
- }
-
- get end(): Date {
- return this._end;
- }
-
- /** Duration in minutes */
- get durationMinutes(): number {
- return (this._end.getTime() - this._start.getTime()) / (1000 * 60);
- }
-
- /** Duration in milliseconds */
- get durationMs(): number {
- return this._end.getTime() - this._start.getTime();
- }
-
- /**
- * Factory: Create SwpEvent from element + columnKey
- * Reads top/height from element.style to calculate start/end
- * @param columnKey - Opaque column identifier (do NOT parse - use only for matching)
- * @param date - Date string (YYYY-MM-DD) for time calculations
- */
- static fromElement(
- element: HTMLElement,
- columnKey: string,
- date: string,
- gridConfig: IGridConfig
- ): SwpEvent {
- const topPixels = parseFloat(element.style.top) || 0;
- const heightPixels = parseFloat(element.style.height) || 0;
-
- // Calculate start from top position
- const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60;
- const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid;
-
- const start = new Date(date);
- start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0);
-
- // Calculate end from height
- const durationMinutes = (heightPixels / gridConfig.hourHeight) * 60;
- const end = new Date(start.getTime() + durationMinutes * 60 * 1000);
-
- return new SwpEvent(element, columnKey, start, end);
- }
-}
diff --git a/packages/calendar/src/utils/PositionUtils.ts b/packages/calendar/src/utils/PositionUtils.ts
deleted file mode 100644
index 5c99e4b..0000000
--- a/packages/calendar/src/utils/PositionUtils.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * PositionUtils - Pixel/position calculations for calendar grid
- *
- * RESPONSIBILITY: Convert between time and pixel positions
- * NOTE: Date formatting belongs in DateService, not here
- */
-
-import { IGridConfig } from '../core/IGridConfig';
-
-export interface EventPosition {
- top: number; // pixels from day start
- height: number; // pixels
-}
-
-/**
- * Calculate pixel position for an event based on its times
- */
-export function calculateEventPosition(
- start: Date,
- end: Date,
- config: IGridConfig
-): EventPosition {
- const startMinutes = start.getHours() * 60 + start.getMinutes();
- const endMinutes = end.getHours() * 60 + end.getMinutes();
-
- const dayStartMinutes = config.dayStartHour * 60;
- const minuteHeight = config.hourHeight / 60;
-
- const top = (startMinutes - dayStartMinutes) * minuteHeight;
- const height = (endMinutes - startMinutes) * minuteHeight;
-
- return { top, height };
-}
-
-/**
- * Convert minutes to pixels
- */
-export function minutesToPixels(minutes: number, config: IGridConfig): number {
- return (minutes / 60) * config.hourHeight;
-}
-
-/**
- * Convert pixels to minutes
- */
-export function pixelsToMinutes(pixels: number, config: IGridConfig): number {
- return (pixels / config.hourHeight) * 60;
-}
-
-/**
- * Snap pixel position to grid interval
- */
-export function snapToGrid(pixels: number, config: IGridConfig): number {
- const snapPixels = minutesToPixels(config.snapInterval, config);
- return Math.round(pixels / snapPixels) * snapPixels;
-}
diff --git a/packages/calendar/tsconfig.json b/packages/calendar/tsconfig.json
deleted file mode 100644
index c1c4978..0000000
--- a/packages/calendar/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "ESNext",
- "moduleResolution": "node",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "declaration": true,
- "declarationDir": "./dist",
- "outDir": "./dist",
- "rootDir": "./src",
- "sourceMap": true,
- "lib": ["ES2024", "DOM", "DOM.Iterable"],
- "experimentalDecorators": true,
- "emitDecoratorMetadata": true
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules", "dist"]
-}
diff --git a/test-package/build.js b/test-package/build.js
deleted file mode 100644
index 07cf9e7..0000000
--- a/test-package/build.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import esbuild from 'esbuild';
-import { NovadiUnplugin } from '@novadi/core/unplugin';
-import { copyFileSync, mkdirSync } from 'fs';
-
-// Ensure dist/css directory exists
-mkdirSync('dist/css', { recursive: true });
-
-// Copy calendar CSS
-copyFileSync(
- 'node_modules/calendar/dist/css/calendar.css',
- 'dist/css/calendar.css'
-);
-
-await esbuild.build({
- entryPoints: ['src/index.ts'],
- bundle: true,
- outfile: 'dist/bundle.js',
- format: 'esm',
- platform: 'browser',
- plugins: [NovadiUnplugin.esbuild()]
-});
-
-console.log('Build complete');
diff --git a/test-package/dist/bundle.js b/test-package/dist/bundle.js
deleted file mode 100644
index 71cc1e8..0000000
--- a/test-package/dist/bundle.js
+++ /dev/null
@@ -1,5289 +0,0 @@
-var __create = Object.create;
-var __defProp = Object.defineProperty;
-var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
-var __getOwnPropNames = Object.getOwnPropertyNames;
-var __getProtoOf = Object.getPrototypeOf;
-var __hasOwnProp = Object.prototype.hasOwnProperty;
-var __commonJS = (cb, mod) => function __require() {
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
-};
-var __copyProps = (to, from, except, desc) => {
- if (from && typeof from === "object" || typeof from === "function") {
- for (let key of __getOwnPropNames(from))
- if (!__hasOwnProp.call(to, key) && key !== except)
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
- }
- return to;
-};
-var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
- // If the importer is in node compatibility mode or this is not an ESM
- // file that has been converted to a CommonJS file using a Babel-
- // compatible transform (i.e. "__esModule" has not been set), then set
- // "default" to the CommonJS "module.exports" for node compatibility.
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
- mod
-));
-
-// node_modules/dayjs/dayjs.min.js
-var require_dayjs_min = __commonJS({
- "node_modules/dayjs/dayjs.min.js"(exports, module) {
- !(function(t, e) {
- "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
- })(exports, (function() {
- "use strict";
- var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", c = "month", f = "quarter", h = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(t2) {
- var e2 = ["th", "st", "nd", "rd"], n2 = t2 % 100;
- return "[" + t2 + (e2[(n2 - 20) % 10] || e2[n2] || e2[0]) + "]";
- } }, m = function(t2, e2, n2) {
- var r2 = String(t2);
- return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2;
- }, v = { s: m, z: function(t2) {
- var e2 = -t2.utcOffset(), n2 = Math.abs(e2), r2 = Math.floor(n2 / 60), i2 = n2 % 60;
- return (e2 <= 0 ? "+" : "-") + m(r2, 2, "0") + ":" + m(i2, 2, "0");
- }, m: function t2(e2, n2) {
- if (e2.date() < n2.date()) return -t2(n2, e2);
- var r2 = 12 * (n2.year() - e2.year()) + (n2.month() - e2.month()), i2 = e2.clone().add(r2, c), s2 = n2 - i2 < 0, u2 = e2.clone().add(r2 + (s2 ? -1 : 1), c);
- return +(-(r2 + (n2 - i2) / (s2 ? i2 - u2 : u2 - i2)) || 0);
- }, a: function(t2) {
- return t2 < 0 ? Math.ceil(t2) || 0 : Math.floor(t2);
- }, p: function(t2) {
- return { M: c, y: h, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: f }[t2] || String(t2 || "").toLowerCase().replace(/s$/, "");
- }, u: function(t2) {
- return void 0 === t2;
- } }, g = "en", D = {};
- D[g] = M;
- var p = "$isDayjsObject", S = function(t2) {
- return t2 instanceof _ || !(!t2 || !t2[p]);
- }, w = function t2(e2, n2, r2) {
- var i2;
- if (!e2) return g;
- if ("string" == typeof e2) {
- var s2 = e2.toLowerCase();
- D[s2] && (i2 = s2), n2 && (D[s2] = n2, i2 = s2);
- var u2 = e2.split("-");
- if (!i2 && u2.length > 1) return t2(u2[0]);
- } else {
- var a2 = e2.name;
- D[a2] = e2, i2 = a2;
- }
- return !r2 && i2 && (g = i2), i2 || !r2 && g;
- }, O = function(t2, e2) {
- if (S(t2)) return t2.clone();
- var n2 = "object" == typeof e2 ? e2 : {};
- return n2.date = t2, n2.args = arguments, new _(n2);
- }, b = v;
- b.l = w, b.i = S, b.w = function(t2, e2) {
- return O(t2, { locale: e2.$L, utc: e2.$u, x: e2.$x, $offset: e2.$offset });
- };
- var _ = (function() {
- function M2(t2) {
- this.$L = w(t2.locale, null, true), this.parse(t2), this.$x = this.$x || t2.x || {}, this[p] = true;
- }
- var m2 = M2.prototype;
- return m2.parse = function(t2) {
- this.$d = (function(t3) {
- var e2 = t3.date, n2 = t3.utc;
- if (null === e2) return /* @__PURE__ */ new Date(NaN);
- if (b.u(e2)) return /* @__PURE__ */ new Date();
- if (e2 instanceof Date) return new Date(e2);
- if ("string" == typeof e2 && !/Z$/i.test(e2)) {
- var r2 = e2.match($);
- if (r2) {
- var i2 = r2[2] - 1 || 0, s2 = (r2[7] || "0").substring(0, 3);
- return n2 ? new Date(Date.UTC(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2)) : new Date(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2);
- }
- }
- return new Date(e2);
- })(t2), this.init();
- }, m2.init = function() {
- var t2 = this.$d;
- this.$y = t2.getFullYear(), this.$M = t2.getMonth(), this.$D = t2.getDate(), this.$W = t2.getDay(), this.$H = t2.getHours(), this.$m = t2.getMinutes(), this.$s = t2.getSeconds(), this.$ms = t2.getMilliseconds();
- }, m2.$utils = function() {
- return b;
- }, m2.isValid = function() {
- return !(this.$d.toString() === l);
- }, m2.isSame = function(t2, e2) {
- var n2 = O(t2);
- return this.startOf(e2) <= n2 && n2 <= this.endOf(e2);
- }, m2.isAfter = function(t2, e2) {
- return O(t2) < this.startOf(e2);
- }, m2.isBefore = function(t2, e2) {
- return this.endOf(e2) < O(t2);
- }, m2.$g = function(t2, e2, n2) {
- return b.u(t2) ? this[e2] : this.set(n2, t2);
- }, m2.unix = function() {
- return Math.floor(this.valueOf() / 1e3);
- }, m2.valueOf = function() {
- return this.$d.getTime();
- }, m2.startOf = function(t2, e2) {
- var n2 = this, r2 = !!b.u(e2) || e2, f2 = b.p(t2), l2 = function(t3, e3) {
- var i2 = b.w(n2.$u ? Date.UTC(n2.$y, e3, t3) : new Date(n2.$y, e3, t3), n2);
- return r2 ? i2 : i2.endOf(a);
- }, $2 = function(t3, e3) {
- return b.w(n2.toDate()[t3].apply(n2.toDate("s"), (r2 ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e3)), n2);
- }, y2 = this.$W, M3 = this.$M, m3 = this.$D, v2 = "set" + (this.$u ? "UTC" : "");
- switch (f2) {
- case h:
- return r2 ? l2(1, 0) : l2(31, 11);
- case c:
- return r2 ? l2(1, M3) : l2(0, M3 + 1);
- case o:
- var g2 = this.$locale().weekStart || 0, D2 = (y2 < g2 ? y2 + 7 : y2) - g2;
- return l2(r2 ? m3 - D2 : m3 + (6 - D2), M3);
- case a:
- case d:
- return $2(v2 + "Hours", 0);
- case u:
- return $2(v2 + "Minutes", 1);
- case s:
- return $2(v2 + "Seconds", 2);
- case i:
- return $2(v2 + "Milliseconds", 3);
- default:
- return this.clone();
- }
- }, m2.endOf = function(t2) {
- return this.startOf(t2, false);
- }, m2.$set = function(t2, e2) {
- var n2, o2 = b.p(t2), f2 = "set" + (this.$u ? "UTC" : ""), l2 = (n2 = {}, n2[a] = f2 + "Date", n2[d] = f2 + "Date", n2[c] = f2 + "Month", n2[h] = f2 + "FullYear", n2[u] = f2 + "Hours", n2[s] = f2 + "Minutes", n2[i] = f2 + "Seconds", n2[r] = f2 + "Milliseconds", n2)[o2], $2 = o2 === a ? this.$D + (e2 - this.$W) : e2;
- if (o2 === c || o2 === h) {
- var y2 = this.clone().set(d, 1);
- y2.$d[l2]($2), y2.init(), this.$d = y2.set(d, Math.min(this.$D, y2.daysInMonth())).$d;
- } else l2 && this.$d[l2]($2);
- return this.init(), this;
- }, m2.set = function(t2, e2) {
- return this.clone().$set(t2, e2);
- }, m2.get = function(t2) {
- return this[b.p(t2)]();
- }, m2.add = function(r2, f2) {
- var d2, l2 = this;
- r2 = Number(r2);
- var $2 = b.p(f2), y2 = function(t2) {
- var e2 = O(l2);
- return b.w(e2.date(e2.date() + Math.round(t2 * r2)), l2);
- };
- if ($2 === c) return this.set(c, this.$M + r2);
- if ($2 === h) return this.set(h, this.$y + r2);
- if ($2 === a) return y2(1);
- if ($2 === o) return y2(7);
- var M3 = (d2 = {}, d2[s] = e, d2[u] = n, d2[i] = t, d2)[$2] || 1, m3 = this.$d.getTime() + r2 * M3;
- return b.w(m3, this);
- }, m2.subtract = function(t2, e2) {
- return this.add(-1 * t2, e2);
- }, m2.format = function(t2) {
- var e2 = this, n2 = this.$locale();
- if (!this.isValid()) return n2.invalidDate || l;
- var r2 = t2 || "YYYY-MM-DDTHH:mm:ssZ", i2 = b.z(this), s2 = this.$H, u2 = this.$m, a2 = this.$M, o2 = n2.weekdays, c2 = n2.months, f2 = n2.meridiem, h2 = function(t3, n3, i3, s3) {
- return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3);
- }, d2 = function(t3) {
- return b.s(s2 % 12 || 12, t3, "0");
- }, $2 = f2 || function(t3, e3, n3) {
- var r3 = t3 < 12 ? "AM" : "PM";
- return n3 ? r3.toLowerCase() : r3;
- };
- return r2.replace(y, (function(t3, r3) {
- return r3 || (function(t4) {
- switch (t4) {
- case "YY":
- return String(e2.$y).slice(-2);
- case "YYYY":
- return b.s(e2.$y, 4, "0");
- case "M":
- return a2 + 1;
- case "MM":
- return b.s(a2 + 1, 2, "0");
- case "MMM":
- return h2(n2.monthsShort, a2, c2, 3);
- case "MMMM":
- return h2(c2, a2);
- case "D":
- return e2.$D;
- case "DD":
- return b.s(e2.$D, 2, "0");
- case "d":
- return String(e2.$W);
- case "dd":
- return h2(n2.weekdaysMin, e2.$W, o2, 2);
- case "ddd":
- return h2(n2.weekdaysShort, e2.$W, o2, 3);
- case "dddd":
- return o2[e2.$W];
- case "H":
- return String(s2);
- case "HH":
- return b.s(s2, 2, "0");
- case "h":
- return d2(1);
- case "hh":
- return d2(2);
- case "a":
- return $2(s2, u2, true);
- case "A":
- return $2(s2, u2, false);
- case "m":
- return String(u2);
- case "mm":
- return b.s(u2, 2, "0");
- case "s":
- return String(e2.$s);
- case "ss":
- return b.s(e2.$s, 2, "0");
- case "SSS":
- return b.s(e2.$ms, 3, "0");
- case "Z":
- return i2;
- }
- return null;
- })(t3) || i2.replace(":", "");
- }));
- }, m2.utcOffset = function() {
- return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
- }, m2.diff = function(r2, d2, l2) {
- var $2, y2 = this, M3 = b.p(d2), m3 = O(r2), v2 = (m3.utcOffset() - this.utcOffset()) * e, g2 = this - m3, D2 = function() {
- return b.m(y2, m3);
- };
- switch (M3) {
- case h:
- $2 = D2() / 12;
- break;
- case c:
- $2 = D2();
- break;
- case f:
- $2 = D2() / 3;
- break;
- case o:
- $2 = (g2 - v2) / 6048e5;
- break;
- case a:
- $2 = (g2 - v2) / 864e5;
- break;
- case u:
- $2 = g2 / n;
- break;
- case s:
- $2 = g2 / e;
- break;
- case i:
- $2 = g2 / t;
- break;
- default:
- $2 = g2;
- }
- return l2 ? $2 : b.a($2);
- }, m2.daysInMonth = function() {
- return this.endOf(c).$D;
- }, m2.$locale = function() {
- return D[this.$L];
- }, m2.locale = function(t2, e2) {
- if (!t2) return this.$L;
- var n2 = this.clone(), r2 = w(t2, e2, true);
- return r2 && (n2.$L = r2), n2;
- }, m2.clone = function() {
- return b.w(this.$d, this);
- }, m2.toDate = function() {
- return new Date(this.valueOf());
- }, m2.toJSON = function() {
- return this.isValid() ? this.toISOString() : null;
- }, m2.toISOString = function() {
- return this.$d.toISOString();
- }, m2.toString = function() {
- return this.$d.toUTCString();
- }, M2;
- })(), k = _.prototype;
- return O.prototype = k, [["$ms", r], ["$s", i], ["$m", s], ["$H", u], ["$W", a], ["$M", c], ["$y", h], ["$D", d]].forEach((function(t2) {
- k[t2[1]] = function(e2) {
- return this.$g(e2, t2[0], t2[1]);
- };
- })), O.extend = function(t2, e2) {
- return t2.$i || (t2(e2, _, O), t2.$i = true), O;
- }, O.locale = w, O.isDayjs = S, O.unix = function(t2) {
- return O(1e3 * t2);
- }, O.en = D[g], O.Ls = D, O.p = {}, O;
- }));
- }
-});
-
-// node_modules/dayjs/plugin/utc.js
-var require_utc = __commonJS({
- "node_modules/dayjs/plugin/utc.js"(exports, module) {
- !(function(t, i) {
- "object" == typeof exports && "undefined" != typeof module ? module.exports = i() : "function" == typeof define && define.amd ? define(i) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs_plugin_utc = i();
- })(exports, (function() {
- "use strict";
- var t = "minute", i = /[+-]\d\d(?::?\d\d)?/g, e = /([+-]|\d\d)/g;
- return function(s, f, n) {
- var u = f.prototype;
- n.utc = function(t2) {
- var i2 = { date: t2, utc: true, args: arguments };
- return new f(i2);
- }, u.utc = function(i2) {
- var e2 = n(this.toDate(), { locale: this.$L, utc: true });
- return i2 ? e2.add(this.utcOffset(), t) : e2;
- }, u.local = function() {
- return n(this.toDate(), { locale: this.$L, utc: false });
- };
- var r = u.parse;
- u.parse = function(t2) {
- t2.utc && (this.$u = true), this.$utils().u(t2.$offset) || (this.$offset = t2.$offset), r.call(this, t2);
- };
- var o = u.init;
- u.init = function() {
- if (this.$u) {
- var t2 = this.$d;
- this.$y = t2.getUTCFullYear(), this.$M = t2.getUTCMonth(), this.$D = t2.getUTCDate(), this.$W = t2.getUTCDay(), this.$H = t2.getUTCHours(), this.$m = t2.getUTCMinutes(), this.$s = t2.getUTCSeconds(), this.$ms = t2.getUTCMilliseconds();
- } else o.call(this);
- };
- var a = u.utcOffset;
- u.utcOffset = function(s2, f2) {
- var n2 = this.$utils().u;
- if (n2(s2)) return this.$u ? 0 : n2(this.$offset) ? a.call(this) : this.$offset;
- if ("string" == typeof s2 && (s2 = (function(t2) {
- void 0 === t2 && (t2 = "");
- var s3 = t2.match(i);
- if (!s3) return null;
- var f3 = ("" + s3[0]).match(e) || ["-", 0, 0], n3 = f3[0], u3 = 60 * +f3[1] + +f3[2];
- return 0 === u3 ? 0 : "+" === n3 ? u3 : -u3;
- })(s2), null === s2)) return this;
- var u2 = Math.abs(s2) <= 16 ? 60 * s2 : s2;
- if (0 === u2) return this.utc(f2);
- var r2 = this.clone();
- if (f2) return r2.$offset = u2, r2.$u = false, r2;
- var o2 = this.$u ? this.toDate().getTimezoneOffset() : -1 * this.utcOffset();
- return (r2 = this.local().add(u2 + o2, t)).$offset = u2, r2.$x.$localOffset = o2, r2;
- };
- var h = u.format;
- u.format = function(t2) {
- var i2 = t2 || (this.$u ? "YYYY-MM-DDTHH:mm:ss[Z]" : "");
- return h.call(this, i2);
- }, u.valueOf = function() {
- var t2 = this.$utils().u(this.$offset) ? 0 : this.$offset + (this.$x.$localOffset || this.$d.getTimezoneOffset());
- return this.$d.valueOf() - 6e4 * t2;
- }, u.isUTC = function() {
- return !!this.$u;
- }, u.toISOString = function() {
- return this.toDate().toISOString();
- }, u.toString = function() {
- return this.toDate().toUTCString();
- };
- var l = u.toDate;
- u.toDate = function(t2) {
- return "s" === t2 && this.$offset ? n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate() : l.call(this);
- };
- var c = u.diff;
- u.diff = function(t2, i2, e2) {
- if (t2 && this.$u === t2.$u) return c.call(this, t2, i2, e2);
- var s2 = this.local(), f2 = n(t2).local();
- return c.call(s2, f2, i2, e2);
- };
- };
- }));
- }
-});
-
-// node_modules/dayjs/plugin/timezone.js
-var require_timezone = __commonJS({
- "node_modules/dayjs/plugin/timezone.js"(exports, module) {
- !(function(t, e) {
- "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs_plugin_timezone = e();
- })(exports, (function() {
- "use strict";
- var t = { year: 0, month: 1, day: 2, hour: 3, minute: 4, second: 5 }, e = {};
- return function(n, i, o) {
- var r, a = function(t2, n2, i2) {
- void 0 === i2 && (i2 = {});
- var o2 = new Date(t2), r2 = (function(t3, n3) {
- void 0 === n3 && (n3 = {});
- var i3 = n3.timeZoneName || "short", o3 = t3 + "|" + i3, r3 = e[o3];
- return r3 || (r3 = new Intl.DateTimeFormat("en-US", { hour12: false, timeZone: t3, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: i3 }), e[o3] = r3), r3;
- })(n2, i2);
- return r2.formatToParts(o2);
- }, u = function(e2, n2) {
- for (var i2 = a(e2, n2), r2 = [], u2 = 0; u2 < i2.length; u2 += 1) {
- var f2 = i2[u2], s2 = f2.type, m = f2.value, c = t[s2];
- c >= 0 && (r2[c] = parseInt(m, 10));
- }
- var d = r2[3], l = 24 === d ? 0 : d, h = r2[0] + "-" + r2[1] + "-" + r2[2] + " " + l + ":" + r2[4] + ":" + r2[5] + ":000", v = +e2;
- return (o.utc(h).valueOf() - (v -= v % 1e3)) / 6e4;
- }, f = i.prototype;
- f.tz = function(t2, e2) {
- void 0 === t2 && (t2 = r);
- var n2, i2 = this.utcOffset(), a2 = this.toDate(), u2 = a2.toLocaleString("en-US", { timeZone: t2 }), f2 = Math.round((a2 - new Date(u2)) / 1e3 / 60), s2 = 15 * -Math.round(a2.getTimezoneOffset() / 15) - f2;
- if (!Number(s2)) n2 = this.utcOffset(0, e2);
- else if (n2 = o(u2, { locale: this.$L }).$set("millisecond", this.$ms).utcOffset(s2, true), e2) {
- var m = n2.utcOffset();
- n2 = n2.add(i2 - m, "minute");
- }
- return n2.$x.$timezone = t2, n2;
- }, f.offsetName = function(t2) {
- var e2 = this.$x.$timezone || o.tz.guess(), n2 = a(this.valueOf(), e2, { timeZoneName: t2 }).find((function(t3) {
- return "timezonename" === t3.type.toLowerCase();
- }));
- return n2 && n2.value;
- };
- var s = f.startOf;
- f.startOf = function(t2, e2) {
- if (!this.$x || !this.$x.$timezone) return s.call(this, t2, e2);
- var n2 = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"), { locale: this.$L });
- return s.call(n2, t2, e2).tz(this.$x.$timezone, true);
- }, o.tz = function(t2, e2, n2) {
- var i2 = n2 && e2, a2 = n2 || e2 || r, f2 = u(+o(), a2);
- if ("string" != typeof t2) return o(t2).tz(a2);
- var s2 = (function(t3, e3, n3) {
- var i3 = t3 - 60 * e3 * 1e3, o2 = u(i3, n3);
- if (e3 === o2) return [i3, e3];
- var r2 = u(i3 -= 60 * (o2 - e3) * 1e3, n3);
- return o2 === r2 ? [i3, o2] : [t3 - 60 * Math.min(o2, r2) * 1e3, Math.max(o2, r2)];
- })(o.utc(t2, i2).valueOf(), f2, a2), m = s2[0], c = s2[1], d = o(m).utcOffset(c);
- return d.$x.$timezone = a2, d;
- }, o.tz.guess = function() {
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
- }, o.tz.setDefault = function(t2) {
- r = t2;
- };
- };
- }));
- }
-});
-
-// node_modules/dayjs/plugin/isoWeek.js
-var require_isoWeek = __commonJS({
- "node_modules/dayjs/plugin/isoWeek.js"(exports, module) {
- !(function(e, t) {
- "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = "undefined" != typeof globalThis ? globalThis : e || self).dayjs_plugin_isoWeek = t();
- })(exports, (function() {
- "use strict";
- var e = "day";
- return function(t, i, s) {
- var a = function(t2) {
- return t2.add(4 - t2.isoWeekday(), e);
- }, d = i.prototype;
- d.isoWeekYear = function() {
- return a(this).year();
- }, d.isoWeek = function(t2) {
- if (!this.$utils().u(t2)) return this.add(7 * (t2 - this.isoWeek()), e);
- var i2, d2, n2, o, r = a(this), u = (i2 = this.isoWeekYear(), d2 = this.$u, n2 = (d2 ? s.utc : s)().year(i2).startOf("year"), o = 4 - n2.isoWeekday(), n2.isoWeekday() > 4 && (o += 7), n2.add(o, e));
- return r.diff(u, "week") + 1;
- }, d.isoWeekday = function(e2) {
- return this.$utils().u(e2) ? this.day() || 7 : this.day(this.day() % 7 ? e2 : e2 - 7);
- };
- var n = d.startOf;
- d.startOf = function(e2, t2) {
- var i2 = this.$utils(), s2 = !!i2.u(t2) || t2;
- return "isoweek" === i2.p(e2) ? s2 ? this.date(this.date() - (this.isoWeekday() - 1)).startOf("day") : this.date(this.date() - 1 - (this.isoWeekday() - 1) + 7).endOf("day") : n.bind(this)(e2, t2);
- };
- };
- }));
- }
-});
-
-// node_modules/@novadi/core/dist/token.js
-var tokenCounter = 0;
-function Token(description) {
- const id = ++tokenCounter;
- const sym = /* @__PURE__ */ Symbol(description ? `Token(${description})` : `Token#${id}`);
- const token2 = {
- symbol: sym,
- description,
- toString() {
- return description ? `Token<${description}>` : `Token<#${id}>`;
- }
- };
- return token2;
-}
-
-// node_modules/@novadi/core/dist/errors.js
-var ContainerError = class extends Error {
- constructor(message) {
- super(message);
- this.name = "ContainerError";
- }
-};
-var BindingNotFoundError = class extends ContainerError {
- constructor(tokenDescription, path = []) {
- const pathStr = path.length > 0 ? `
- Dependency path: ${path.join(" -> ")}` : "";
- super(`Token "${tokenDescription}" is not bound or registered in the container.${pathStr}`);
- this.name = "BindingNotFoundError";
- }
-};
-var CircularDependencyError = class extends ContainerError {
- constructor(path) {
- super(`Circular dependency detected: ${path.join(" -> ")}`);
- this.name = "CircularDependencyError";
- }
-};
-
-// node_modules/@novadi/core/dist/autowire.js
-var paramNameCache = /* @__PURE__ */ new WeakMap();
-function extractParameterNames(constructor) {
- const cached = paramNameCache.get(constructor);
- if (cached) {
- return cached;
- }
- const fnStr = constructor.toString();
- const match = fnStr.match(/constructor\s*\(([^)]*)\)/) || fnStr.match(/^[^(]*\(([^)]*)\)/);
- if (!match || !match[1]) {
- return [];
- }
- const params = match[1].split(",").map((param) => param.trim()).filter((param) => param.length > 0).map((param) => {
- let name = param.split(/[:=]/)[0].trim();
- name = name.replace(/^((public|private|protected|readonly)\s+)+/, "");
- if (name.includes("{") || name.includes("[")) {
- return null;
- }
- return name;
- }).filter((name) => name !== null);
- paramNameCache.set(constructor, params);
- return params;
-}
-function resolveByMap(constructor, container, options) {
- if (!options.map) {
- throw new Error("AutoWire map strategy requires options.map to be defined");
- }
- const paramNames = extractParameterNames(constructor);
- const resolvedDeps = [];
- for (const paramName of paramNames) {
- const resolver = options.map[paramName];
- if (resolver === void 0) {
- if (options.strict) {
- throw new Error(`Cannot resolve parameter "${paramName}" on ${constructor.name}. Not found in autowire map. Add it to the map: .autoWire({ map: { ${paramName}: ... } })`);
- } else {
- resolvedDeps.push(void 0);
- }
- continue;
- }
- if (typeof resolver === "function") {
- resolvedDeps.push(resolver(container));
- } else {
- resolvedDeps.push(container.resolve(resolver));
- }
- }
- return resolvedDeps;
-}
-function resolveByMapResolvers(_constructor, container, options) {
- if (!options.mapResolvers || options.mapResolvers.length === 0) {
- return [];
- }
- const resolvedDeps = [];
- for (let i = 0; i < options.mapResolvers.length; i++) {
- const resolver = options.mapResolvers[i];
- if (resolver === void 0) {
- resolvedDeps.push(void 0);
- } else if (typeof resolver === "function") {
- resolvedDeps.push(resolver(container));
- } else {
- resolvedDeps.push(container.resolve(resolver));
- }
- }
- return resolvedDeps;
-}
-function autowire(constructor, container, options) {
- const opts = {
- by: "paramName",
- strict: false,
- ...options
- };
- if (opts.mapResolvers && opts.mapResolvers.length > 0) {
- return resolveByMapResolvers(constructor, container, opts);
- }
- if (opts.map && Object.keys(opts.map).length > 0) {
- return resolveByMap(constructor, container, opts);
- }
- return [];
-}
-
-// node_modules/@novadi/core/dist/builder.js
-var RegistrationBuilder = class {
- constructor(pending, registrations) {
- this.registrations = registrations;
- this.configs = [];
- this.defaultLifetime = "singleton";
- this.pending = pending;
- }
- /**
- * Bind this registration to a token or interface type
- *
- * @overload
- * @param {Token} token - Explicit token for binding
- *
- * @overload
- * @param {string} typeName - Interface type name (auto-generated by transformer)
- */
- as(tokenOrTypeName) {
- if (tokenOrTypeName && typeof tokenOrTypeName === "object" && "symbol" in tokenOrTypeName) {
- const config = {
- token: tokenOrTypeName,
- type: this.pending.type,
- value: this.pending.value,
- factory: this.pending.factory,
- constructor: this.pending.constructor,
- lifetime: this.defaultLifetime
- };
- this.configs.push(config);
- this.registrations.push(config);
- return this;
- } else {
- const config = {
- token: null,
- // Will be set during build()
- type: this.pending.type,
- value: this.pending.value,
- factory: this.pending.factory,
- constructor: this.pending.constructor,
- lifetime: this.defaultLifetime,
- interfaceType: tokenOrTypeName
- };
- this.configs.push(config);
- this.registrations.push(config);
- return this;
- }
- }
- /**
- * Register as default implementation for an interface
- * Combines as() + asDefault()
- */
- asDefaultInterface(typeName) {
- this.as("TInterface", typeName);
- return this.asDefault();
- }
- /**
- * Register as a keyed interface implementation
- * Combines as() + keyed()
- */
- asKeyedInterface(key, typeName) {
- this.as("TInterface", typeName);
- return this.keyed(key);
- }
- /**
- * Register as multiple implemented interfaces
- */
- asImplementedInterfaces(tokens) {
- if (tokens.length === 0) {
- return this;
- }
- if (this.configs.length > 0) {
- for (const config of this.configs) {
- config.lifetime = "singleton";
- config.additionalTokens = config.additionalTokens || [];
- config.additionalTokens.push(...tokens);
- }
- return this;
- }
- const firstConfig = {
- token: tokens[0],
- type: this.pending.type,
- value: this.pending.value,
- factory: this.pending.factory,
- constructor: this.pending.constructor,
- lifetime: "singleton"
- };
- this.configs.push(firstConfig);
- this.registrations.push(firstConfig);
- for (let i = 1; i < tokens.length; i++) {
- firstConfig.additionalTokens = firstConfig.additionalTokens || [];
- firstConfig.additionalTokens.push(tokens[i]);
- }
- return this;
- }
- /**
- * Set singleton lifetime (one instance for entire container)
- */
- singleInstance() {
- for (const config of this.configs) {
- config.lifetime = "singleton";
- }
- return this;
- }
- /**
- * Set per-request lifetime (one instance per resolve call tree)
- */
- instancePerRequest() {
- for (const config of this.configs) {
- config.lifetime = "per-request";
- }
- return this;
- }
- /**
- * Set transient lifetime (new instance every time)
- * Alias for default behavior
- */
- instancePerDependency() {
- for (const config of this.configs) {
- config.lifetime = "transient";
- }
- return this;
- }
- /**
- * Name this registration for named resolution
- */
- named(name) {
- for (const config of this.configs) {
- config.name = name;
- }
- return this;
- }
- /**
- * Key this registration for keyed resolution
- */
- keyed(key) {
- for (const config of this.configs) {
- config.key = key;
- }
- return this;
- }
- /**
- * Mark this as default registration
- * Default registrations don't override existing ones
- */
- asDefault() {
- for (const config of this.configs) {
- config.isDefault = true;
- }
- return this;
- }
- /**
- * Only register if token not already registered
- */
- ifNotRegistered() {
- for (const config of this.configs) {
- config.ifNotRegistered = true;
- }
- return this;
- }
- /**
- * Specify parameter values for constructor (primitives and constants)
- * Use this for non-DI parameters like strings, numbers, config values
- */
- withParameters(parameters) {
- for (const config of this.configs) {
- config.parameterValues = parameters;
- }
- return this;
- }
- /**
- * Enable automatic dependency injection (autowiring)
- * Supports three strategies: paramName (default), map, and class
- *
- * @example
- * ```ts
- * // Strategy 1: paramName (default, requires non-minified code in dev)
- * builder.registerType(EventBus).as().autoWire()
- *
- * // Strategy 2: map (minify-safe, explicit)
- * builder.registerType(EventBus).as().autoWire({
- * map: {
- * logger: (c) => c.resolveType()
- * }
- * })
- *
- * // Strategy 3: class (requires build-time codegen)
- * builder.registerType(EventBus).as().autoWire({ by: 'class' })
- * ```
- */
- autoWire(options) {
- for (const config of this.configs) {
- config.autowireOptions = options || { by: "paramName", strict: false };
- }
- return this;
- }
-};
-var Builder = class {
- constructor(baseContainer) {
- this.baseContainer = baseContainer;
- this.registrations = [];
- }
- /**
- * Register a class constructor
- */
- registerType(constructor) {
- const pending = {
- type: "type",
- value: null,
- constructor
- };
- return new RegistrationBuilder(pending, this.registrations);
- }
- /**
- * Register a pre-created instance
- */
- registerInstance(instance) {
- const pending = {
- type: "instance",
- value: instance,
- constructor: void 0
- };
- return new RegistrationBuilder(pending, this.registrations);
- }
- /**
- * Register a factory function
- */
- register(factory) {
- const pending = {
- type: "factory",
- value: null,
- factory,
- constructor: void 0
- };
- return new RegistrationBuilder(pending, this.registrations);
- }
- /**
- * Register a module (function that adds multiple registrations)
- */
- module(moduleFunc) {
- moduleFunc(this);
- return this;
- }
- /**
- * Resolve interface type names to tokens
- * @internal
- */
- resolveInterfaceTokens(container) {
- for (const config of this.registrations) {
- if (config.interfaceType !== void 0 && !config.token) {
- config.token = container.interfaceToken(config.interfaceType);
- }
- }
- }
- /**
- * Identify tokens that have non-default registrations
- * @internal
- */
- identifyNonDefaultTokens() {
- const tokensWithNonDefaults = /* @__PURE__ */ new Set();
- for (const config of this.registrations) {
- if (!config.isDefault && !config.name && config.key === void 0) {
- tokensWithNonDefaults.add(config.token);
- }
- }
- return tokensWithNonDefaults;
- }
- /**
- * Check if registration should be skipped
- * @internal
- */
- shouldSkipRegistration(config, tokensWithNonDefaults, registeredTokens) {
- if (config.isDefault && !config.name && config.key === void 0 && tokensWithNonDefaults.has(config.token)) {
- return true;
- }
- if (config.ifNotRegistered && registeredTokens.has(config.token)) {
- return true;
- }
- if (config.isDefault && registeredTokens.has(config.token)) {
- return true;
- }
- return false;
- }
- /**
- * Create binding token for registration (named, keyed, or multi)
- * @internal
- */
- createBindingToken(config, namedRegistrations, keyedRegistrations, multiRegistrations) {
- if (config.name) {
- const bindingToken = Token(`__named_${config.name}`);
- namedRegistrations.set(config.name, { ...config, token: bindingToken });
- return bindingToken;
- } else if (config.key !== void 0) {
- const keyStr = typeof config.key === "symbol" ? config.key.toString() : config.key;
- const bindingToken = Token(`__keyed_${keyStr}`);
- keyedRegistrations.set(config.key, { ...config, token: bindingToken });
- return bindingToken;
- } else {
- if (multiRegistrations.has(config.token)) {
- const bindingToken = Token(`__multi_${config.token.toString()}_${multiRegistrations.get(config.token).length}`);
- multiRegistrations.get(config.token).push(bindingToken);
- return bindingToken;
- } else {
- multiRegistrations.set(config.token, [config.token]);
- return config.token;
- }
- }
- }
- /**
- * Register additional interfaces for a config
- * @internal
- */
- registerAdditionalInterfaces(container, config, bindingToken, registeredTokens) {
- if (config.additionalTokens) {
- for (const additionalToken of config.additionalTokens) {
- container.bindFactory(additionalToken, (c) => c.resolve(bindingToken), { lifetime: config.lifetime });
- registeredTokens.add(additionalToken);
- }
- }
- }
- /**
- * Build the container with all registered bindings
- */
- build() {
- const container = this.baseContainer.createChild();
- this.resolveInterfaceTokens(container);
- const registeredTokens = /* @__PURE__ */ new Set();
- const namedRegistrations = /* @__PURE__ */ new Map();
- const keyedRegistrations = /* @__PURE__ */ new Map();
- const multiRegistrations = /* @__PURE__ */ new Map();
- const tokensWithNonDefaults = this.identifyNonDefaultTokens();
- for (const config of this.registrations) {
- if (this.shouldSkipRegistration(config, tokensWithNonDefaults, registeredTokens)) {
- continue;
- }
- const bindingToken = this.createBindingToken(config, namedRegistrations, keyedRegistrations, multiRegistrations);
- this.applyRegistration(container, { ...config, token: bindingToken });
- registeredTokens.add(config.token);
- this.registerAdditionalInterfaces(container, config, bindingToken, registeredTokens);
- }
- ;
- container.__namedRegistrations = namedRegistrations;
- container.__keyedRegistrations = keyedRegistrations;
- container.__multiRegistrations = multiRegistrations;
- return container;
- }
- /**
- * Analyze constructor to detect dependencies
- * @internal
- */
- analyzeConstructor(constructor) {
- const constructorStr = constructor.toString();
- const hasDependencies = /constructor\s*\([^)]+\)/.test(constructorStr);
- return { hasDependencies };
- }
- /**
- * Create optimized factory for zero-dependency constructors
- * @internal
- */
- createOptimizedFactory(container, config, options) {
- if (config.lifetime === "singleton") {
- const instance = new config.constructor();
- container.bindValue(config.token, instance);
- } else if (config.lifetime === "transient") {
- const ctor = config.constructor;
- const fastFactory = () => new ctor();
- container.fastTransientCache.set(config.token, fastFactory);
- container.bindFactory(config.token, fastFactory, options);
- } else {
- const factory = () => new config.constructor();
- container.bindFactory(config.token, factory, options);
- }
- }
- /**
- * Create autowire factory
- * @internal
- */
- createAutoWireFactory(container, config, options) {
- const factory = (c) => {
- const resolvedDeps = autowire(config.constructor, c, config.autowireOptions);
- return new config.constructor(...resolvedDeps);
- };
- container.bindFactory(config.token, factory, options);
- }
- /**
- * Create withParameters factory
- * @internal
- */
- createParameterFactory(container, config, options) {
- const factory = () => {
- const values = Object.values(config.parameterValues);
- return new config.constructor(...values);
- };
- container.bindFactory(config.token, factory, options);
- }
- /**
- * Apply type registration (class constructor)
- * @internal
- */
- applyTypeRegistration(container, config, options) {
- const { hasDependencies } = this.analyzeConstructor(config.constructor);
- if (!hasDependencies && !config.autowireOptions && !config.parameterValues) {
- this.createOptimizedFactory(container, config, options);
- return;
- }
- if (config.autowireOptions) {
- this.createAutoWireFactory(container, config, options);
- return;
- }
- if (config.parameterValues) {
- this.createParameterFactory(container, config, options);
- return;
- }
- if (hasDependencies) {
- const className = config.constructor.name || "UnnamedClass";
- throw new Error(`Service "${className}" has constructor dependencies but no autowiring configuration.
-
-Solutions:
- 1. \u2B50 Use the NovaDI transformer (recommended):
- - Add "@novadi/core/unplugin" to your build config
- - Transformer automatically generates .autoWire() for all dependencies
-
- 2. Add manual autowiring:
- .autoWire({ map: { /* param: resolver */ } })
-
- 3. Use a factory function:
- .register((c) => new ${className}(...))
-
-See docs: https://github.com/janus007/NovaDI#autowire`);
- }
- const factory = () => new config.constructor();
- container.bindFactory(config.token, factory, options);
- }
- applyRegistration(container, config) {
- const options = { lifetime: config.lifetime };
- switch (config.type) {
- case "instance":
- container.bindValue(config.token, config.value);
- break;
- case "factory":
- container.bindFactory(config.token, config.factory, options);
- break;
- case "type":
- this.applyTypeRegistration(container, config, options);
- break;
- }
- }
-};
-
-// node_modules/@novadi/core/dist/container.js
-function isDisposable(obj) {
- return obj && typeof obj.dispose === "function";
-}
-var ResolutionContext = class {
- constructor() {
- this.resolvingStack = /* @__PURE__ */ new Set();
- this.perRequestCache = /* @__PURE__ */ new Map();
- }
- isResolving(token2) {
- return this.resolvingStack.has(token2);
- }
- enterResolve(token2) {
- this.resolvingStack.add(token2);
- }
- exitResolve(token2) {
- this.resolvingStack.delete(token2);
- this.path = void 0;
- }
- getPath() {
- if (!this.path) {
- this.path = Array.from(this.resolvingStack).map((t) => t.toString());
- }
- return [...this.path];
- }
- cachePerRequest(token2, instance) {
- this.perRequestCache.set(token2, instance);
- }
- getPerRequest(token2) {
- return this.perRequestCache.get(token2);
- }
- hasPerRequest(token2) {
- return this.perRequestCache.has(token2);
- }
- /**
- * Reset context for reuse in object pool
- * Performance: Reusing contexts avoids heap allocations
- */
- reset() {
- this.resolvingStack.clear();
- this.perRequestCache.clear();
- this.path = void 0;
- }
-};
-var ResolutionContextPool = class {
- constructor() {
- this.pool = [];
- this.maxSize = 10;
- }
- acquire() {
- const context = this.pool.pop();
- if (context) {
- context.reset();
- return context;
- }
- return new ResolutionContext();
- }
- release(context) {
- if (this.pool.length < this.maxSize) {
- this.pool.push(context);
- }
- }
-};
-var Container = class _Container {
- constructor(parent) {
- this.bindings = /* @__PURE__ */ new Map();
- this.singletonCache = /* @__PURE__ */ new Map();
- this.singletonOrder = [];
- this.interfaceRegistry = /* @__PURE__ */ new Map();
- this.interfaceTokenCache = /* @__PURE__ */ new Map();
- this.fastTransientCache = /* @__PURE__ */ new Map();
- this.ultraFastSingletonCache = /* @__PURE__ */ new Map();
- this.parent = parent;
- }
- /**
- * Bind a pre-created value to a token
- */
- bindValue(token2, value) {
- this.bindings.set(token2, {
- type: "value",
- lifetime: "singleton",
- value,
- constructor: void 0
- });
- this.invalidateBindingCache();
- }
- /**
- * Bind a factory function to a token
- */
- bindFactory(token2, factory, options) {
- this.bindings.set(token2, {
- type: "factory",
- lifetime: options?.lifetime || "transient",
- factory,
- dependencies: options?.dependencies,
- constructor: void 0
- });
- this.invalidateBindingCache();
- }
- /**
- * Bind a class constructor to a token
- */
- bindClass(token2, constructor, options) {
- const binding = {
- type: "class",
- lifetime: options?.lifetime || "transient",
- constructor,
- dependencies: options?.dependencies
- };
- this.bindings.set(token2, binding);
- this.invalidateBindingCache();
- if (binding.lifetime === "transient" && (!binding.dependencies || binding.dependencies.length === 0)) {
- this.fastTransientCache.set(token2, () => new constructor());
- }
- }
- /**
- * Resolve a dependency synchronously
- * Performance optimized with multiple fast paths
- */
- resolve(token2) {
- const cached = this.tryGetFromCaches(token2);
- if (cached !== void 0) {
- return cached;
- }
- if (this.currentContext) {
- return this.resolveWithContext(token2, this.currentContext);
- }
- const context = _Container.contextPool.acquire();
- this.currentContext = context;
- try {
- return this.resolveWithContext(token2, context);
- } finally {
- this.currentContext = void 0;
- _Container.contextPool.release(context);
- }
- }
- /**
- * SPECIALIZED: Ultra-fast singleton resolve (no safety checks)
- * Use ONLY when you're 100% sure the token is a registered singleton
- * @internal For performance-critical paths only
- */
- resolveSingletonUnsafe(token2) {
- return this.ultraFastSingletonCache.get(token2) ?? this.singletonCache.get(token2);
- }
- /**
- * SPECIALIZED: Fast transient resolve for zero-dependency classes
- * Skips all context creation and circular dependency checks
- * @internal For performance-critical paths only
- */
- resolveTransientSimple(token2) {
- const factory = this.fastTransientCache.get(token2);
- if (factory) {
- return factory();
- }
- return this.resolve(token2);
- }
- /**
- * SPECIALIZED: Batch resolve multiple dependencies at once
- * More efficient than multiple individual resolves
- */
- resolveBatch(tokens) {
- const wasResolving = !!this.currentContext;
- const context = this.currentContext || _Container.contextPool.acquire();
- if (!wasResolving) {
- this.currentContext = context;
- }
- try {
- const results = tokens.map((token2) => {
- const cached = this.tryGetFromCaches(token2);
- if (cached !== void 0)
- return cached;
- return this.resolveWithContext(token2, context);
- });
- return results;
- } finally {
- if (!wasResolving) {
- this.currentContext = void 0;
- _Container.contextPool.release(context);
- }
- }
- }
- /**
- * Resolve a dependency asynchronously (supports async factories)
- */
- async resolveAsync(token2) {
- if (this.currentContext) {
- return this.resolveAsyncWithContext(token2, this.currentContext);
- }
- const context = _Container.contextPool.acquire();
- this.currentContext = context;
- try {
- return await this.resolveAsyncWithContext(token2, context);
- } finally {
- this.currentContext = void 0;
- _Container.contextPool.release(context);
- }
- }
- /**
- * Try to get instance from all cache levels
- * Returns undefined if not cached
- * @internal
- */
- tryGetFromCaches(token2) {
- const ultraFast = this.ultraFastSingletonCache.get(token2);
- if (ultraFast !== void 0) {
- return ultraFast;
- }
- if (this.singletonCache.has(token2)) {
- const cached = this.singletonCache.get(token2);
- this.ultraFastSingletonCache.set(token2, cached);
- return cached;
- }
- const fastFactory = this.fastTransientCache.get(token2);
- if (fastFactory) {
- return fastFactory();
- }
- return void 0;
- }
- /**
- * Cache instance based on lifetime strategy
- * @internal
- */
- cacheInstance(token2, instance, lifetime, context) {
- if (lifetime === "singleton") {
- this.singletonCache.set(token2, instance);
- this.singletonOrder.push(token2);
- this.ultraFastSingletonCache.set(token2, instance);
- } else if (lifetime === "per-request" && context) {
- context.cachePerRequest(token2, instance);
- }
- }
- /**
- * Validate and get binding with circular dependency check
- * Returns binding or throws error
- * @internal
- */
- validateAndGetBinding(token2, context) {
- if (context.isResolving(token2)) {
- throw new CircularDependencyError([...context.getPath(), token2.toString()]);
- }
- const binding = this.getBinding(token2);
- if (!binding) {
- throw new BindingNotFoundError(token2.toString(), context.getPath());
- }
- return binding;
- }
- /**
- * Instantiate from binding synchronously
- * @internal
- */
- instantiateBindingSync(binding, token2, context) {
- switch (binding.type) {
- case "value":
- return binding.value;
- case "factory":
- const result = binding.factory(this);
- if (result instanceof Promise) {
- throw new Error(`Async factory detected for ${token2.toString()}. Use resolveAsync() instead.`);
- }
- return result;
- case "class":
- const deps = binding.dependencies || [];
- const resolvedDeps = deps.map((dep) => this.resolveWithContext(dep, context));
- return new binding.constructor(...resolvedDeps);
- case "inline-class":
- return new binding.constructor();
- default:
- throw new Error(`Unknown binding type: ${binding.type}`);
- }
- }
- /**
- * Instantiate from binding asynchronously
- * @internal
- */
- async instantiateBindingAsync(binding, context) {
- switch (binding.type) {
- case "value":
- return binding.value;
- case "factory":
- return await Promise.resolve(binding.factory(this));
- case "class":
- const deps = binding.dependencies || [];
- const resolvedDeps = await Promise.all(deps.map((dep) => this.resolveAsyncWithContext(dep, context)));
- return new binding.constructor(...resolvedDeps);
- case "inline-class":
- return new binding.constructor();
- default:
- throw new Error(`Unknown binding type: ${binding.type}`);
- }
- }
- /**
- * Create a child container that inherits bindings from this container
- */
- createChild() {
- return new _Container(this);
- }
- /**
- * Dispose all singleton instances in reverse registration order
- */
- async dispose() {
- const errors = [];
- for (let i = this.singletonOrder.length - 1; i >= 0; i--) {
- const token2 = this.singletonOrder[i];
- const instance = this.singletonCache.get(token2);
- if (instance && isDisposable(instance)) {
- try {
- await instance.dispose();
- } catch (error) {
- errors.push(error);
- }
- }
- }
- this.singletonCache.clear();
- this.singletonOrder.length = 0;
- }
- /**
- * Create a fluent builder for registering dependencies
- */
- builder() {
- return new Builder(this);
- }
- /**
- * Resolve a named service
- */
- resolveNamed(name) {
- const namedRegistrations = this.__namedRegistrations;
- if (!namedRegistrations) {
- throw new Error(`Named service "${name}" not found. No named registrations exist.`);
- }
- const config = namedRegistrations.get(name);
- if (!config) {
- throw new Error(`Named service "${name}" not found`);
- }
- return this.resolve(config.token);
- }
- /**
- * Resolve a keyed service
- */
- resolveKeyed(key) {
- const keyedRegistrations = this.__keyedRegistrations;
- if (!keyedRegistrations) {
- throw new Error(`Keyed service not found. No keyed registrations exist.`);
- }
- const config = keyedRegistrations.get(key);
- if (!config) {
- const keyStr = typeof key === "symbol" ? key.toString() : `"${key}"`;
- throw new Error(`Keyed service ${keyStr} not found`);
- }
- return this.resolve(config.token);
- }
- /**
- * Resolve all registrations for a token
- */
- resolveAll(token2) {
- const multiRegistrations = this.__multiRegistrations;
- if (!multiRegistrations) {
- return [];
- }
- const tokens = multiRegistrations.get(token2);
- if (!tokens || tokens.length === 0) {
- return [];
- }
- return tokens.map((t) => this.resolve(t));
- }
- /**
- * Get registry information for debugging/visualization
- * Returns array of binding information
- */
- getRegistry() {
- const registry = [];
- this.bindings.forEach((binding, token2) => {
- registry.push({
- token: token2.description || token2.symbol.toString(),
- type: binding.type,
- lifetime: binding.lifetime,
- dependencies: binding.dependencies?.map((d) => d.description || d.symbol.toString())
- });
- });
- return registry;
- }
- /**
- * Get or create a token for an interface type
- * Uses a type name hash as key for the interface registry
- */
- interfaceToken(typeName) {
- const key = typeName || `Interface_${Math.random().toString(36).substr(2, 9)}`;
- if (this.interfaceRegistry.has(key)) {
- return this.interfaceRegistry.get(key);
- }
- if (this.parent) {
- const parentToken = this.parent.interfaceToken(key);
- return parentToken;
- }
- const token2 = Token(key);
- this.interfaceRegistry.set(key, token2);
- return token2;
- }
- /**
- * Resolve a dependency by interface type without explicit token
- */
- resolveType(typeName) {
- const key = typeName || "";
- let token2 = this.interfaceTokenCache.get(key);
- if (!token2) {
- token2 = this.interfaceToken(typeName);
- this.interfaceTokenCache.set(key, token2);
- }
- return this.resolve(token2);
- }
- /**
- * Resolve a keyed interface
- */
- resolveTypeKeyed(key, _typeName) {
- return this.resolveKeyed(key);
- }
- /**
- * Resolve all registrations for an interface type
- */
- resolveTypeAll(typeName) {
- const token2 = this.interfaceToken(typeName);
- return this.resolveAll(token2);
- }
- /**
- * Internal: Resolve with context for circular dependency detection
- */
- resolveWithContext(token2, context) {
- const binding = this.validateAndGetBinding(token2, context);
- if (binding.lifetime === "per-request" && context.hasPerRequest(token2)) {
- return context.getPerRequest(token2);
- }
- if (binding.lifetime === "singleton" && this.singletonCache.has(token2)) {
- return this.singletonCache.get(token2);
- }
- context.enterResolve(token2);
- try {
- const instance = this.instantiateBindingSync(binding, token2, context);
- this.cacheInstance(token2, instance, binding.lifetime, context);
- return instance;
- } finally {
- context.exitResolve(token2);
- }
- }
- /**
- * Internal: Async resolve with context
- */
- async resolveAsyncWithContext(token2, context) {
- const binding = this.validateAndGetBinding(token2, context);
- if (binding.lifetime === "per-request" && context.hasPerRequest(token2)) {
- return context.getPerRequest(token2);
- }
- if (binding.lifetime === "singleton" && this.singletonCache.has(token2)) {
- return this.singletonCache.get(token2);
- }
- context.enterResolve(token2);
- try {
- const instance = await this.instantiateBindingAsync(binding, context);
- this.cacheInstance(token2, instance, binding.lifetime, context);
- return instance;
- } finally {
- context.exitResolve(token2);
- }
- }
- /**
- * Get binding from this container or parent chain
- * Performance optimized: Uses flat cache to avoid recursive parent lookups
- */
- getBinding(token2) {
- if (!this.bindingCache) {
- this.buildBindingCache();
- }
- return this.bindingCache.get(token2);
- }
- /**
- * Build flat cache of all bindings including parent chain
- * This converts O(n) parent chain traversal to O(1) lookup
- */
- buildBindingCache() {
- this.bindingCache = /* @__PURE__ */ new Map();
- let current = this;
- while (current) {
- current.bindings.forEach((binding, token2) => {
- if (!this.bindingCache.has(token2)) {
- this.bindingCache.set(token2, binding);
- }
- });
- current = current.parent;
- }
- }
- /**
- * Invalidate binding cache when new bindings are added
- * Called by bindValue, bindFactory, bindClass
- */
- invalidateBindingCache() {
- this.bindingCache = void 0;
- this.ultraFastSingletonCache.clear();
- }
-};
-Container.contextPool = new ResolutionContextPool();
-
-// node_modules/calendar/dist/chunk-OPIZ4QQE.js
-var BaseGroupingRenderer = class {
- /**
- * Main render method - handles common logic
- */
- async render(context) {
- const allowedIds = context.filter[this.type] || [];
- if (allowedIds.length === 0)
- return;
- const entities = await this.getEntities(allowedIds);
- const dateCount = context.filter["date"]?.length || 1;
- const childIds = context.childType ? context.filter[context.childType] || [] : [];
- for (const entity of entities) {
- const entityChildIds = context.parentChildMap?.[entity.id] || [];
- const childCount = entityChildIds.filter((id) => childIds.includes(id)).length;
- const colspan = childCount * dateCount;
- const header = document.createElement(this.config.elementTag);
- header.dataset[this.config.idAttribute] = entity.id;
- header.style.setProperty(this.config.colspanVar, String(colspan));
- this.renderHeader(entity, header, context);
- context.headerContainer.appendChild(header);
- }
- }
- /**
- * Override this method for custom header rendering
- * Default: just sets textContent to display name
- */
- renderHeader(entity, header, _context) {
- header.textContent = this.getDisplayName(entity);
- }
- /**
- * Helper to render a single entity header.
- * Can be used by subclasses that override render() but want consistent header creation.
- */
- createHeader(entity, context) {
- const header = document.createElement(this.config.elementTag);
- header.dataset[this.config.idAttribute] = entity.id;
- this.renderHeader(entity, header, context);
- return header;
- }
-};
-
-// node_modules/calendar/dist/chunk-CMOI3H5F.js
-var CoreEvents = {
- // Lifecycle events
- INITIALIZED: "core:initialized",
- READY: "core:ready",
- DESTROYED: "core:destroyed",
- // View events
- VIEW_CHANGED: "view:changed",
- VIEW_RENDERED: "view:rendered",
- // Navigation events
- DATE_CHANGED: "nav:date-changed",
- NAVIGATION_COMPLETED: "nav:navigation-completed",
- // Data events
- DATA_LOADING: "data:loading",
- DATA_LOADED: "data:loaded",
- DATA_ERROR: "data:error",
- // Grid events
- GRID_RENDERED: "grid:rendered",
- GRID_CLICKED: "grid:clicked",
- // Event management
- EVENT_CREATED: "event:created",
- EVENT_UPDATED: "event:updated",
- EVENT_DELETED: "event:deleted",
- EVENT_SELECTED: "event:selected",
- // Event drag-drop
- EVENT_DRAG_START: "event:drag-start",
- EVENT_DRAG_MOVE: "event:drag-move",
- EVENT_DRAG_END: "event:drag-end",
- EVENT_DRAG_CANCEL: "event:drag-cancel",
- EVENT_DRAG_COLUMN_CHANGE: "event:drag-column-change",
- // Header drag (timed → header conversion)
- EVENT_DRAG_ENTER_HEADER: "event:drag-enter-header",
- EVENT_DRAG_MOVE_HEADER: "event:drag-move-header",
- EVENT_DRAG_LEAVE_HEADER: "event:drag-leave-header",
- // Event resize
- EVENT_RESIZE_START: "event:resize-start",
- EVENT_RESIZE_END: "event:resize-end",
- // Edge scroll
- EDGE_SCROLL_TICK: "edge-scroll:tick",
- EDGE_SCROLL_STARTED: "edge-scroll:started",
- EDGE_SCROLL_STOPPED: "edge-scroll:stopped",
- // System events
- ERROR: "system:error",
- // Sync events
- SYNC_STARTED: "sync:started",
- SYNC_COMPLETED: "sync:completed",
- SYNC_FAILED: "sync:failed",
- // Entity events - for audit and sync
- ENTITY_SAVED: "entity:saved",
- ENTITY_DELETED: "entity:deleted",
- // Audit events
- AUDIT_LOGGED: "audit:logged",
- // Rendering events
- EVENTS_RENDERED: "events:rendered"
-};
-var SyncPlugin = class {
- constructor(service) {
- this.service = service;
- }
- /**
- * Mark entity as successfully synced
- */
- async markAsSynced(id) {
- const entity = await this.service.get(id);
- if (entity) {
- entity.syncStatus = "synced";
- await this.service.save(entity);
- }
- }
- /**
- * Mark entity as sync error
- */
- async markAsError(id) {
- const entity = await this.service.get(id);
- if (entity) {
- entity.syncStatus = "error";
- await this.service.save(entity);
- }
- }
- /**
- * Get current sync status for an entity
- */
- async getSyncStatus(id) {
- const entity = await this.service.get(id);
- return entity ? entity.syncStatus : null;
- }
- /**
- * Get entities by sync status using IndexedDB index
- */
- async getBySyncStatus(syncStatus) {
- return new Promise((resolve, reject) => {
- const transaction = this.service.db.transaction([this.service.storeName], "readonly");
- const store = transaction.objectStore(this.service.storeName);
- const index = store.index("syncStatus");
- const request = index.getAll(syncStatus);
- request.onsuccess = () => {
- const data = request.result;
- const entities = data.map((item) => this.service.deserialize(item));
- resolve(entities);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get by sync status ${syncStatus}: ${request.error}`));
- };
- });
- }
-};
-function arrayDifference(first, second) {
- const secondSet = new Set(second);
- return first.filter((item) => !secondSet.has(item));
-}
-function arrayIntersection(first, second) {
- const secondSet = new Set(second);
- return first.filter((item) => secondSet.has(item));
-}
-function keyBy(arr, getKey2) {
- const result = {};
- for (const item of arr) {
- result[String(getKey2(item))] = item;
- }
- return result;
-}
-function diff(oldObj, newObj, options = {}) {
- let { embeddedObjKeys } = options;
- const { keysToSkip, treatTypeChangeAsReplace } = options;
- if (embeddedObjKeys instanceof Map) {
- embeddedObjKeys = new Map(
- Array.from(embeddedObjKeys.entries()).map(([key, value]) => [
- key instanceof RegExp ? key : key.replace(/^\./, ""),
- value
- ])
- );
- } else if (embeddedObjKeys) {
- embeddedObjKeys = Object.fromEntries(
- Object.entries(embeddedObjKeys).map(([key, value]) => [key.replace(/^\./, ""), value])
- );
- }
- return compare(oldObj, newObj, [], [], {
- embeddedObjKeys,
- keysToSkip: keysToSkip ?? [],
- treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true
- });
-}
-var getTypeOfObj = (obj) => {
- if (typeof obj === "undefined") {
- return "undefined";
- }
- if (obj === null) {
- return null;
- }
- return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
-};
-var getKey = (path) => {
- const left = path[path.length - 1];
- return left != null ? left : "$root";
-};
-var compare = (oldObj, newObj, path, keyPath, options) => {
- let changes = [];
- const currentPath = keyPath.join(".");
- if (options.keysToSkip?.some((skipPath) => {
- if (currentPath === skipPath) {
- return true;
- }
- if (skipPath.includes(".") && skipPath.startsWith(currentPath + ".")) {
- return false;
- }
- if (skipPath.includes(".")) {
- const skipParts = skipPath.split(".");
- const currentParts = currentPath.split(".");
- if (currentParts.length >= skipParts.length) {
- for (let i = 0; i < skipParts.length; i++) {
- if (skipParts[i] !== currentParts[i]) {
- return false;
- }
- }
- return true;
- }
- }
- return false;
- })) {
- return changes;
- }
- const typeOfOldObj = getTypeOfObj(oldObj);
- const typeOfNewObj = getTypeOfObj(newObj);
- if (options.treatTypeChangeAsReplace && typeOfOldObj !== typeOfNewObj) {
- if (typeOfOldObj !== "undefined") {
- changes.push({ type: "REMOVE", key: getKey(path), value: oldObj });
- }
- if (typeOfNewObj !== "undefined") {
- changes.push({ type: "ADD", key: getKey(path), value: newObj });
- }
- return changes;
- }
- if (typeOfNewObj === "undefined" && typeOfOldObj !== "undefined") {
- changes.push({ type: "REMOVE", key: getKey(path), value: oldObj });
- return changes;
- }
- if (typeOfNewObj === "Object" && typeOfOldObj === "Array") {
- changes.push({ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj });
- return changes;
- }
- if (typeOfNewObj === null) {
- if (typeOfOldObj !== null) {
- changes.push({ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj });
- }
- return changes;
- }
- switch (typeOfOldObj) {
- case "Date":
- if (typeOfNewObj === "Date") {
- changes = changes.concat(
- comparePrimitives(oldObj.getTime(), newObj.getTime(), path).map((x) => ({
- ...x,
- value: new Date(x.value),
- oldValue: new Date(x.oldValue)
- }))
- );
- } else {
- changes = changes.concat(comparePrimitives(oldObj, newObj, path));
- }
- break;
- case "Object": {
- const diffs = compareObject(oldObj, newObj, path, keyPath, false, options);
- if (diffs.length) {
- if (path.length) {
- changes.push({
- type: "UPDATE",
- key: getKey(path),
- changes: diffs
- });
- } else {
- changes = changes.concat(diffs);
- }
- }
- break;
- }
- case "Array":
- changes = changes.concat(compareArray(oldObj, newObj, path, keyPath, options));
- break;
- case "Function":
- break;
- default:
- changes = changes.concat(comparePrimitives(oldObj, newObj, path));
- }
- return changes;
-};
-var compareObject = (oldObj, newObj, path, keyPath, skipPath = false, options = {}) => {
- let k;
- let newKeyPath;
- let newPath;
- if (skipPath == null) {
- skipPath = false;
- }
- let changes = [];
- const oldObjKeys = Object.keys(oldObj);
- const newObjKeys = Object.keys(newObj);
- const intersectionKeys = arrayIntersection(oldObjKeys, newObjKeys);
- for (k of intersectionKeys) {
- newPath = path.concat([k]);
- newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
- const diffs = compare(oldObj[k], newObj[k], newPath, newKeyPath, options);
- if (diffs.length) {
- changes = changes.concat(diffs);
- }
- }
- const addedKeys = arrayDifference(newObjKeys, oldObjKeys);
- for (k of addedKeys) {
- newPath = path.concat([k]);
- newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
- const currentPath = newKeyPath.join(".");
- if (options.keysToSkip?.some((skipPath2) => currentPath === skipPath2 || currentPath.startsWith(skipPath2 + "."))) {
- continue;
- }
- changes.push({
- type: "ADD",
- key: getKey(newPath),
- value: newObj[k]
- });
- }
- const deletedKeys = arrayDifference(oldObjKeys, newObjKeys);
- for (k of deletedKeys) {
- newPath = path.concat([k]);
- newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
- const currentPath = newKeyPath.join(".");
- if (options.keysToSkip?.some((skipPath2) => currentPath === skipPath2 || currentPath.startsWith(skipPath2 + "."))) {
- continue;
- }
- changes.push({
- type: "REMOVE",
- key: getKey(newPath),
- value: oldObj[k]
- });
- }
- return changes;
-};
-var compareArray = (oldObj, newObj, path, keyPath, options) => {
- if (getTypeOfObj(newObj) !== "Array") {
- return [{ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj }];
- }
- const left = getObjectKey(options.embeddedObjKeys, keyPath);
- const uniqKey = left != null ? left : "$index";
- const indexedOldObj = convertArrayToObj(oldObj, uniqKey);
- const indexedNewObj = convertArrayToObj(newObj, uniqKey);
- const diffs = compareObject(indexedOldObj, indexedNewObj, path, keyPath, true, options);
- if (diffs.length) {
- return [
- {
- type: "UPDATE",
- key: getKey(path),
- embeddedKey: typeof uniqKey === "function" && uniqKey.length === 2 ? uniqKey(newObj[0], true) : uniqKey,
- changes: diffs
- }
- ];
- } else {
- return [];
- }
-};
-var getObjectKey = (embeddedObjKeys, keyPath) => {
- if (embeddedObjKeys != null) {
- const path = keyPath.join(".");
- if (embeddedObjKeys instanceof Map) {
- for (const [key2, value] of embeddedObjKeys.entries()) {
- if (key2 instanceof RegExp) {
- if (path.match(key2)) {
- return value;
- }
- } else if (path === key2) {
- return value;
- }
- }
- }
- const key = embeddedObjKeys[path];
- if (key != null) {
- return key;
- }
- }
- return void 0;
-};
-var convertArrayToObj = (arr, uniqKey) => {
- let obj = {};
- if (uniqKey === "$value") {
- arr.forEach((value) => {
- obj[value] = value;
- });
- } else if (uniqKey !== "$index") {
- const keyFunction = typeof uniqKey === "string" ? (item) => item[uniqKey] : uniqKey;
- obj = keyBy(arr, keyFunction);
- } else {
- for (let i = 0; i < arr.length; i++) {
- const value = arr[i];
- obj[i] = value;
- }
- }
- return obj;
-};
-var comparePrimitives = (oldObj, newObj, path) => {
- const changes = [];
- if (oldObj !== newObj) {
- changes.push({
- type: "UPDATE",
- key: getKey(path),
- value: newObj,
- oldValue: oldObj
- });
- }
- return changes;
-};
-var BaseEntityService = class {
- constructor(context, eventBus) {
- this.context = context;
- this.eventBus = eventBus;
- this.syncPlugin = new SyncPlugin(this);
- }
- get db() {
- return this.context.getDatabase();
- }
- /**
- * Serialize entity before storing in IndexedDB
- */
- serialize(entity) {
- return entity;
- }
- /**
- * Deserialize data from IndexedDB back to entity
- */
- deserialize(data) {
- return data;
- }
- /**
- * Get a single entity by ID
- */
- async get(id) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
- const request = store.get(id);
- request.onsuccess = () => {
- const data = request.result;
- resolve(data ? this.deserialize(data) : null);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`));
- };
- });
- }
- /**
- * Get all entities
- */
- async getAll() {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
- const request = store.getAll();
- request.onsuccess = () => {
- const data = request.result;
- const entities = data.map((item) => this.deserialize(item));
- resolve(entities);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`));
- };
- });
- }
- /**
- * Save an entity (create or update)
- * Emits ENTITY_SAVED event with operation type and changes (diff for updates)
- * @param entity - Entity to save
- * @param silent - If true, skip event emission (used for seeding)
- */
- async save(entity, silent = false) {
- const entityId = entity.id;
- const existingEntity = await this.get(entityId);
- const isCreate = existingEntity === null;
- let changes;
- if (isCreate) {
- changes = entity;
- } else {
- const existingSerialized = this.serialize(existingEntity);
- const newSerialized = this.serialize(entity);
- changes = diff(existingSerialized, newSerialized);
- }
- const serialized = this.serialize(entity);
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
- const request = store.put(serialized);
- request.onsuccess = () => {
- if (!silent) {
- const payload = {
- entityType: this.entityType,
- entityId,
- operation: isCreate ? "create" : "update",
- changes,
- timestamp: Date.now()
- };
- this.eventBus.emit(CoreEvents.ENTITY_SAVED, payload);
- }
- resolve();
- };
- request.onerror = () => {
- reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`));
- };
- });
- }
- /**
- * Delete an entity
- * Emits ENTITY_DELETED event
- */
- async delete(id) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
- const request = store.delete(id);
- request.onsuccess = () => {
- const payload = {
- entityType: this.entityType,
- entityId: id,
- operation: "delete",
- timestamp: Date.now()
- };
- this.eventBus.emit(CoreEvents.ENTITY_DELETED, payload);
- resolve();
- };
- request.onerror = () => {
- reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`));
- };
- });
- }
- // Sync methods - delegate to SyncPlugin
- async markAsSynced(id) {
- return this.syncPlugin.markAsSynced(id);
- }
- async markAsError(id) {
- return this.syncPlugin.markAsError(id);
- }
- async getSyncStatus(id) {
- return this.syncPlugin.getSyncStatus(id);
- }
- async getBySyncStatus(syncStatus) {
- return this.syncPlugin.getBySyncStatus(syncStatus);
- }
-};
-
-// node_modules/calendar/dist/index.js
-var import_dayjs = __toESM(require_dayjs_min(), 1);
-var import_utc = __toESM(require_utc(), 1);
-var import_timezone = __toESM(require_timezone(), 1);
-var import_isoWeek = __toESM(require_isoWeek(), 1);
-var NavigationAnimator = class {
- constructor(headerTrack, contentTrack, headerDrawer) {
- this.headerTrack = headerTrack;
- this.contentTrack = contentTrack;
- this.headerDrawer = headerDrawer;
- }
- async slide(direction, renderFn) {
- const out = direction === "left" ? "-100%" : "100%";
- const into = direction === "left" ? "100%" : "-100%";
- await this.animateOut(out);
- await renderFn();
- await this.animateIn(into);
- }
- async animateOut(translate) {
- const animations = [
- this.headerTrack.animate([{ transform: "translateX(0)" }, { transform: `translateX(${translate})` }], { duration: 200, easing: "ease-in" }).finished,
- this.contentTrack.animate([{ transform: "translateX(0)" }, { transform: `translateX(${translate})` }], { duration: 200, easing: "ease-in" }).finished
- ];
- if (this.headerDrawer) {
- animations.push(this.headerDrawer.animate([{ transform: "translateX(0)" }, { transform: `translateX(${translate})` }], { duration: 200, easing: "ease-in" }).finished);
- }
- await Promise.all(animations);
- }
- async animateIn(translate) {
- const animations = [
- this.headerTrack.animate([{ transform: `translateX(${translate})` }, { transform: "translateX(0)" }], { duration: 200, easing: "ease-out" }).finished,
- this.contentTrack.animate([{ transform: `translateX(${translate})` }, { transform: "translateX(0)" }], { duration: 200, easing: "ease-out" }).finished
- ];
- if (this.headerDrawer) {
- animations.push(this.headerDrawer.animate([{ transform: `translateX(${translate})` }, { transform: "translateX(0)" }], { duration: 200, easing: "ease-out" }).finished);
- }
- await Promise.all(animations);
- }
-};
-var CalendarEvents = {
- // Command events (host → calendar)
- CMD_NAVIGATE_PREV: "calendar:cmd:navigate:prev",
- CMD_NAVIGATE_NEXT: "calendar:cmd:navigate:next",
- CMD_DRAWER_TOGGLE: "calendar:cmd:drawer:toggle",
- CMD_RENDER: "calendar:cmd:render",
- CMD_WORKWEEK_CHANGE: "calendar:cmd:workweek:change",
- CMD_VIEW_UPDATE: "calendar:cmd:view:update"
-};
-var CalendarApp = class {
- constructor(orchestrator, timeAxisRenderer, dateService, scrollManager, headerDrawerManager, dragDropManager, edgeScrollManager, resizeManager, headerDrawerRenderer, eventPersistenceManager, settingsService, viewConfigService, eventBus) {
- this.orchestrator = orchestrator;
- this.timeAxisRenderer = timeAxisRenderer;
- this.dateService = dateService;
- this.scrollManager = scrollManager;
- this.headerDrawerManager = headerDrawerManager;
- this.dragDropManager = dragDropManager;
- this.edgeScrollManager = edgeScrollManager;
- this.resizeManager = resizeManager;
- this.headerDrawerRenderer = headerDrawerRenderer;
- this.eventPersistenceManager = eventPersistenceManager;
- this.settingsService = settingsService;
- this.viewConfigService = viewConfigService;
- this.eventBus = eventBus;
- this.dayOffset = 0;
- this.currentViewId = "simple";
- this.workweekPreset = null;
- this.groupingOverrides = /* @__PURE__ */ new Map();
- }
- async init(container) {
- this.container = container;
- const gridSettings = await this.settingsService.getGridSettings();
- if (!gridSettings) {
- throw new Error("GridSettings not found");
- }
- this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset();
- this.animator = new NavigationAnimator(container.querySelector("swp-header-track"), container.querySelector("swp-content-track"), container.querySelector("swp-header-drawer"));
- this.timeAxisRenderer.render(container.querySelector("#time-axis"), gridSettings.dayStartHour, gridSettings.dayEndHour);
- this.scrollManager.init(container);
- this.headerDrawerManager.init(container);
- this.dragDropManager.init(container);
- this.resizeManager.init(container);
- const scrollableContent = container.querySelector("swp-scrollable-content");
- this.edgeScrollManager.init(scrollableContent);
- this.setupEventListeners();
- this.emitStatus("ready");
- }
- setupEventListeners() {
- this.eventBus.on(CalendarEvents.CMD_NAVIGATE_PREV, () => {
- this.handleNavigatePrev();
- });
- this.eventBus.on(CalendarEvents.CMD_NAVIGATE_NEXT, () => {
- this.handleNavigateNext();
- });
- this.eventBus.on(CalendarEvents.CMD_DRAWER_TOGGLE, () => {
- this.headerDrawerManager.toggle();
- });
- this.eventBus.on(CalendarEvents.CMD_RENDER, (e) => {
- const { viewId } = e.detail;
- this.handleRenderCommand(viewId);
- });
- this.eventBus.on(CalendarEvents.CMD_WORKWEEK_CHANGE, (e) => {
- const { presetId } = e.detail;
- this.handleWorkweekChange(presetId);
- });
- this.eventBus.on(CalendarEvents.CMD_VIEW_UPDATE, (e) => {
- const { type, values } = e.detail;
- this.handleViewUpdate(type, values);
- });
- }
- async handleRenderCommand(viewId) {
- this.currentViewId = viewId;
- await this.render();
- this.emitStatus("rendered", { viewId });
- }
- async handleNavigatePrev() {
- const step = this.workweekPreset?.periodDays ?? 7;
- this.dayOffset -= step;
- await this.animator.slide("right", () => this.render());
- this.emitStatus("rendered", { viewId: this.currentViewId });
- }
- async handleNavigateNext() {
- const step = this.workweekPreset?.periodDays ?? 7;
- this.dayOffset += step;
- await this.animator.slide("left", () => this.render());
- this.emitStatus("rendered", { viewId: this.currentViewId });
- }
- async handleWorkweekChange(presetId) {
- const preset = await this.settingsService.getWorkweekPreset(presetId);
- if (preset) {
- this.workweekPreset = preset;
- await this.render();
- this.emitStatus("rendered", { viewId: this.currentViewId });
- }
- }
- async handleViewUpdate(type, values) {
- this.groupingOverrides.set(type, values);
- await this.render();
- this.emitStatus("rendered", { viewId: this.currentViewId });
- }
- async render() {
- const storedConfig = await this.viewConfigService.getById(this.currentViewId);
- if (!storedConfig) {
- this.emitStatus("error", { message: `ViewConfig not found: ${this.currentViewId}` });
- return;
- }
- const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5];
- const periodDays = this.workweekPreset?.periodDays ?? 7;
- const dates = periodDays === 1 ? this.dateService.getDatesFromOffset(this.dayOffset, workDays.length) : this.dateService.getWorkDaysFromOffset(this.dayOffset, workDays);
- const viewConfig = {
- ...storedConfig,
- groupings: storedConfig.groupings.map((g) => {
- if (g.type === "date") {
- return { ...g, values: dates };
- }
- const override = this.groupingOverrides.get(g.type);
- if (override) {
- return { ...g, values: override };
- }
- return g;
- })
- };
- await this.orchestrator.render(viewConfig, this.container);
- }
- emitStatus(status, detail) {
- this.container.dispatchEvent(new CustomEvent(`calendar:status:${status}`, {
- detail,
- bubbles: true
- }));
- }
-};
-function buildPipeline(renderers) {
- return {
- async run(context) {
- for (const renderer of renderers) {
- await renderer.render(context);
- }
- }
- };
-}
-var FilterTemplate = class {
- constructor(dateService, entityResolver) {
- this.dateService = dateService;
- this.entityResolver = entityResolver;
- this.fields = [];
- }
- /**
- * Tilføj felt til template
- * @param idProperty - Property-navn (bruges på både event og column.dataset)
- * @param derivedFrom - Hvis feltet udledes fra anden property (f.eks. date fra start)
- */
- addField(idProperty, derivedFrom) {
- this.fields.push({ idProperty, derivedFrom });
- return this;
- }
- /**
- * Parse dot-notation string into components
- * @example 'resource.teamId' → { entityType: 'resource', property: 'teamId', foreignKey: 'resourceId' }
- */
- parseDotNotation(idProperty) {
- if (!idProperty.includes("."))
- return null;
- const [entityType, property] = idProperty.split(".");
- return {
- entityType,
- property,
- foreignKey: entityType + "Id"
- // Convention: resource → resourceId
- };
- }
- /**
- * Get dataset key for column lookup
- * For dot-notation 'resource.teamId', we look for 'teamId' in dataset
- */
- getDatasetKey(idProperty) {
- const dotNotation = this.parseDotNotation(idProperty);
- if (dotNotation) {
- return dotNotation.property;
- }
- return idProperty;
- }
- /**
- * Byg nøgle fra kolonne
- * Læser værdier fra column.dataset[idProperty]
- * For dot-notation, uses the property part (resource.teamId → teamId)
- */
- buildKeyFromColumn(column) {
- return this.fields.map((f) => {
- const key = this.getDatasetKey(f.idProperty);
- return column.dataset[key] || "";
- }).join(":");
- }
- /**
- * Byg nøgle fra event
- * Læser værdier fra event[idProperty] eller udleder fra derivedFrom
- * For dot-notation, resolves via EntityResolver
- */
- buildKeyFromEvent(event) {
- const eventRecord = event;
- return this.fields.map((f) => {
- const dotNotation = this.parseDotNotation(f.idProperty);
- if (dotNotation) {
- return this.resolveDotNotation(eventRecord, dotNotation);
- }
- if (f.derivedFrom) {
- const sourceValue = eventRecord[f.derivedFrom];
- if (sourceValue instanceof Date) {
- return this.dateService.getDateKey(sourceValue);
- }
- return String(sourceValue || "");
- }
- return String(eventRecord[f.idProperty] || "");
- }).join(":");
- }
- /**
- * Resolve dot-notation reference via EntityResolver
- */
- resolveDotNotation(eventRecord, dotNotation) {
- if (!this.entityResolver) {
- console.warn(`FilterTemplate: EntityResolver required for dot-notation '${dotNotation.entityType}.${dotNotation.property}'`);
- return "";
- }
- const foreignId = eventRecord[dotNotation.foreignKey];
- if (!foreignId)
- return "";
- const entity = this.entityResolver.resolve(dotNotation.entityType, String(foreignId));
- if (!entity)
- return "";
- return String(entity[dotNotation.property] || "");
- }
- /**
- * Match event mod kolonne
- */
- matches(event, column) {
- return this.buildKeyFromEvent(event) === this.buildKeyFromColumn(column);
- }
-};
-var CalendarOrchestrator = class {
- constructor(allRenderers, eventRenderer, scheduleRenderer, headerDrawerRenderer, dateService, entityServices) {
- this.allRenderers = allRenderers;
- this.eventRenderer = eventRenderer;
- this.scheduleRenderer = scheduleRenderer;
- this.headerDrawerRenderer = headerDrawerRenderer;
- this.dateService = dateService;
- this.entityServices = entityServices;
- }
- async render(viewConfig, container) {
- const headerContainer = container.querySelector("swp-calendar-header");
- const columnContainer = container.querySelector("swp-day-columns");
- if (!headerContainer || !columnContainer) {
- throw new Error("Missing swp-calendar-header or swp-day-columns");
- }
- const filter = {};
- for (const grouping of viewConfig.groupings) {
- filter[grouping.type] = grouping.values;
- }
- const filterTemplate = new FilterTemplate(this.dateService);
- for (const grouping of viewConfig.groupings) {
- if (grouping.idProperty) {
- filterTemplate.addField(grouping.idProperty, grouping.derivedFrom);
- }
- }
- const { parentChildMap, childType } = await this.resolveBelongsTo(viewConfig.groupings, filter);
- const context = { headerContainer, columnContainer, filter, groupings: viewConfig.groupings, parentChildMap, childType };
- headerContainer.innerHTML = "";
- columnContainer.innerHTML = "";
- const levels = viewConfig.groupings.map((g) => g.type).join(" ");
- headerContainer.dataset.levels = levels;
- const activeRenderers = this.selectRenderers(viewConfig);
- const pipeline = buildPipeline(activeRenderers);
- await pipeline.run(context);
- await this.scheduleRenderer.render(container, filter);
- await this.eventRenderer.render(container, filter, filterTemplate);
- await this.headerDrawerRenderer.render(container, filter, filterTemplate);
- }
- selectRenderers(viewConfig) {
- const types = viewConfig.groupings.map((g) => g.type);
- return types.map((type) => this.allRenderers.find((r) => r.type === type)).filter((r) => r !== void 0);
- }
- /**
- * Resolve belongsTo relations to build parent-child map
- * e.g., belongsTo: 'team.resourceIds' → { team1: ['EMP001', 'EMP002'], team2: [...] }
- * Also returns the childType (the grouping type that has belongsTo)
- */
- async resolveBelongsTo(groupings, filter) {
- const childGrouping = groupings.find((g) => g.belongsTo);
- if (!childGrouping?.belongsTo)
- return {};
- const [entityType, property] = childGrouping.belongsTo.split(".");
- if (!entityType || !property)
- return {};
- const parentIds = filter[entityType] || [];
- if (parentIds.length === 0)
- return {};
- const service = this.entityServices.find((s) => s.entityType.toLowerCase() === entityType);
- if (!service)
- return {};
- const allEntities = await service.getAll();
- const entities = allEntities.filter((e) => parentIds.includes(e.id));
- const map = {};
- for (const entity of entities) {
- const entityRecord = entity;
- const children = entityRecord[property] || [];
- map[entityRecord.id] = children;
- }
- return { parentChildMap: map, childType: childGrouping.type };
- }
-};
-var EventBus = class {
- constructor() {
- this.eventLog = [];
- this.debug = false;
- this.listeners = /* @__PURE__ */ new Set();
- this.logConfig = {
- calendar: true,
- grid: true,
- event: true,
- scroll: true,
- navigation: true,
- view: true,
- default: true
- };
- }
- /**
- * Subscribe to an event via DOM addEventListener
- */
- on(eventType, handler, options) {
- document.addEventListener(eventType, handler, options);
- this.listeners.add({ eventType, handler, options });
- return () => this.off(eventType, handler);
- }
- /**
- * Subscribe to an event once
- */
- once(eventType, handler) {
- return this.on(eventType, handler, { once: true });
- }
- /**
- * Unsubscribe from an event
- */
- off(eventType, handler) {
- document.removeEventListener(eventType, handler);
- for (const listener of this.listeners) {
- if (listener.eventType === eventType && listener.handler === handler) {
- this.listeners.delete(listener);
- break;
- }
- }
- }
- /**
- * Emit an event via DOM CustomEvent
- */
- emit(eventType, detail = {}) {
- if (!eventType) {
- return false;
- }
- const event = new CustomEvent(eventType, {
- detail: detail ?? {},
- bubbles: true,
- cancelable: true
- });
- if (this.debug) {
- this.logEventWithGrouping(eventType, detail);
- }
- this.eventLog.push({
- type: eventType,
- detail: detail ?? {},
- timestamp: Date.now()
- });
- return !document.dispatchEvent(event);
- }
- /**
- * Log event with console grouping
- */
- logEventWithGrouping(eventType, _detail) {
- const category = this.extractCategory(eventType);
- if (!this.logConfig[category]) {
- return;
- }
- this.getCategoryStyle(category);
- }
- /**
- * Extract category from event type
- */
- extractCategory(eventType) {
- if (!eventType) {
- return "unknown";
- }
- if (eventType.includes(":")) {
- return eventType.split(":")[0];
- }
- const lowerType = eventType.toLowerCase();
- if (lowerType.includes("grid") || lowerType.includes("rendered"))
- return "grid";
- if (lowerType.includes("event") || lowerType.includes("sync"))
- return "event";
- if (lowerType.includes("scroll"))
- return "scroll";
- if (lowerType.includes("nav") || lowerType.includes("date"))
- return "navigation";
- if (lowerType.includes("view"))
- return "view";
- return "default";
- }
- /**
- * Get styling for different categories
- */
- getCategoryStyle(category) {
- const styles = {
- calendar: { emoji: "\u{1F4C5}", color: "#2196F3" },
- grid: { emoji: "\u{1F4CA}", color: "#4CAF50" },
- event: { emoji: "\u{1F4CC}", color: "#FF9800" },
- scroll: { emoji: "\u{1F4DC}", color: "#9C27B0" },
- navigation: { emoji: "\u{1F9ED}", color: "#F44336" },
- view: { emoji: "\u{1F441}", color: "#00BCD4" },
- default: { emoji: "\u{1F4E2}", color: "#607D8B" }
- };
- return styles[category] || styles.default;
- }
- /**
- * Configure logging for specific categories
- */
- setLogConfig(config) {
- this.logConfig = { ...this.logConfig, ...config };
- }
- /**
- * Get current log configuration
- */
- getLogConfig() {
- return { ...this.logConfig };
- }
- /**
- * Get event history
- */
- getEventLog(eventType) {
- if (eventType) {
- return this.eventLog.filter((e) => e.type === eventType);
- }
- return this.eventLog;
- }
- /**
- * Enable/disable debug mode
- */
- setDebug(enabled) {
- this.debug = enabled;
- }
-};
-import_dayjs.default.extend(import_utc.default);
-import_dayjs.default.extend(import_timezone.default);
-import_dayjs.default.extend(import_isoWeek.default);
-var DateService = class {
- constructor(config, baseDate) {
- this.config = config;
- this.timezone = config.timezone;
- this.baseDate = baseDate ? (0, import_dayjs.default)(baseDate) : (0, import_dayjs.default)();
- }
- /**
- * Set a fixed base date (useful for demos with static mock data)
- */
- setBaseDate(date) {
- this.baseDate = (0, import_dayjs.default)(date);
- }
- /**
- * Get the current base date (either fixed or today)
- */
- getBaseDate() {
- return this.baseDate.toDate();
- }
- parseISO(isoString) {
- return (0, import_dayjs.default)(isoString).toDate();
- }
- getDayName(date, format = "short") {
- return new Intl.DateTimeFormat(this.config.locale, { weekday: format }).format(date);
- }
- /**
- * Get dates starting from a day offset
- * @param dayOffset - Day offset from base date
- * @param count - Number of consecutive days to return
- * @returns Array of date strings in YYYY-MM-DD format
- */
- getDatesFromOffset(dayOffset, count) {
- const startDate = this.baseDate.add(dayOffset, "day");
- return Array.from({ length: count }, (_, i) => startDate.add(i, "day").format("YYYY-MM-DD"));
- }
- /**
- * Get specific weekdays from the week containing the offset date
- * @param dayOffset - Day offset from base date
- * @param workDays - Array of ISO weekday numbers (1=Monday, 7=Sunday)
- * @returns Array of date strings in YYYY-MM-DD format
- */
- getWorkDaysFromOffset(dayOffset, workDays) {
- const targetDate = this.baseDate.add(dayOffset, "day");
- const monday = targetDate.startOf("week").add(1, "day");
- return workDays.map((isoDay) => {
- const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
- return monday.add(daysFromMonday, "day").format("YYYY-MM-DD");
- });
- }
- // Legacy methods for backwards compatibility
- getWeekDates(weekOffset = 0, days = 7) {
- return this.getDatesFromOffset(weekOffset * 7, days);
- }
- getWorkWeekDates(weekOffset, workDays) {
- return this.getWorkDaysFromOffset(weekOffset * 7, workDays);
- }
- // ============================================
- // FORMATTING
- // ============================================
- formatTime(date, showSeconds = false) {
- const pattern = showSeconds ? "HH:mm:ss" : "HH:mm";
- return (0, import_dayjs.default)(date).format(pattern);
- }
- formatTimeRange(start, end) {
- return `${this.formatTime(start)} - ${this.formatTime(end)}`;
- }
- formatDate(date) {
- return (0, import_dayjs.default)(date).format("YYYY-MM-DD");
- }
- getDateKey(date) {
- return this.formatDate(date);
- }
- // ============================================
- // COLUMN KEY
- // ============================================
- /**
- * Build a uniform columnKey from grouping segments
- * Handles any combination of date, resource, team, etc.
- *
- * @example
- * buildColumnKey({ date: '2025-12-09' }) → "2025-12-09"
- * buildColumnKey({ date: '2025-12-09', resource: 'EMP001' }) → "2025-12-09:EMP001"
- */
- buildColumnKey(segments) {
- const date = segments.date;
- const others = Object.entries(segments).filter(([k]) => k !== "date").sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
- return date ? [date, ...others].join(":") : others.join(":");
- }
- /**
- * Parse a columnKey back into segments
- * Assumes format: "date:resource:..." or just "date"
- */
- parseColumnKey(columnKey) {
- const parts = columnKey.split(":");
- return {
- date: parts[0],
- resource: parts[1]
- };
- }
- /**
- * Extract dateKey from columnKey (first segment)
- */
- getDateFromColumnKey(columnKey) {
- return columnKey.split(":")[0];
- }
- // ============================================
- // TIME CALCULATIONS
- // ============================================
- timeToMinutes(timeString) {
- const parts = timeString.split(":").map(Number);
- const hours = parts[0] || 0;
- const minutes = parts[1] || 0;
- return hours * 60 + minutes;
- }
- minutesToTime(totalMinutes) {
- const hours = Math.floor(totalMinutes / 60);
- const minutes = totalMinutes % 60;
- return (0, import_dayjs.default)().hour(hours).minute(minutes).format("HH:mm");
- }
- getMinutesSinceMidnight(date) {
- const d = (0, import_dayjs.default)(date);
- return d.hour() * 60 + d.minute();
- }
- // ============================================
- // UTC CONVERSIONS
- // ============================================
- toUTC(localDate) {
- return import_dayjs.default.tz(localDate, this.timezone).utc().toISOString();
- }
- fromUTC(utcString) {
- return import_dayjs.default.utc(utcString).tz(this.timezone).toDate();
- }
- // ============================================
- // DATE CREATION
- // ============================================
- createDateAtTime(baseDate, timeString) {
- const totalMinutes = this.timeToMinutes(timeString);
- const hours = Math.floor(totalMinutes / 60);
- const minutes = totalMinutes % 60;
- return (0, import_dayjs.default)(baseDate).startOf("day").hour(hours).minute(minutes).toDate();
- }
- getISOWeekDay(date) {
- return (0, import_dayjs.default)(date).isoWeekday();
- }
-};
-var ScrollManager = class {
- init(container) {
- this.scrollableContent = container.querySelector("swp-scrollable-content");
- this.timeAxisContent = container.querySelector("swp-time-axis-content");
- this.calendarHeader = container.querySelector("swp-calendar-header");
- this.headerDrawer = container.querySelector("swp-header-drawer");
- this.headerViewport = container.querySelector("swp-header-viewport");
- this.headerSpacer = container.querySelector("swp-header-spacer");
- this.scrollableContent.addEventListener("scroll", () => this.onScroll());
- this.resizeObserver = new ResizeObserver(() => this.syncHeaderSpacerHeight());
- this.resizeObserver.observe(this.headerViewport);
- this.syncHeaderSpacerHeight();
- }
- syncHeaderSpacerHeight() {
- const computedHeight = getComputedStyle(this.headerViewport).height;
- this.headerSpacer.style.height = computedHeight;
- }
- onScroll() {
- const { scrollTop, scrollLeft } = this.scrollableContent;
- this.timeAxisContent.style.transform = `translateY(-${scrollTop}px)`;
- this.calendarHeader.style.transform = `translateX(-${scrollLeft}px)`;
- this.headerDrawer.style.transform = `translateX(-${scrollLeft}px)`;
- }
-};
-var HeaderDrawerManager = class {
- constructor() {
- this.expanded = false;
- this.currentRows = 0;
- this.rowHeight = 25;
- this.duration = 200;
- }
- init(container) {
- this.drawer = container.querySelector("swp-header-drawer");
- if (!this.drawer)
- console.error("HeaderDrawerManager: swp-header-drawer not found");
- }
- toggle() {
- this.expanded ? this.collapse() : this.expand();
- }
- /**
- * Expand drawer to single row (legacy support)
- */
- expand() {
- this.expandToRows(1);
- }
- /**
- * Expand drawer to fit specified number of rows
- */
- expandToRows(rowCount) {
- const targetHeight = rowCount * this.rowHeight;
- const currentHeight = this.expanded ? this.currentRows * this.rowHeight : 0;
- if (this.expanded && this.currentRows === rowCount)
- return;
- this.currentRows = rowCount;
- this.expanded = true;
- this.animate(currentHeight, targetHeight);
- }
- collapse() {
- if (!this.expanded)
- return;
- const currentHeight = this.currentRows * this.rowHeight;
- this.expanded = false;
- this.currentRows = 0;
- this.animate(currentHeight, 0);
- }
- animate(from, to) {
- const keyframes = [
- { height: `${from}px` },
- { height: `${to}px` }
- ];
- const options = {
- duration: this.duration,
- easing: "ease",
- fill: "forwards"
- };
- this.drawer.animate(keyframes, options);
- }
- isExpanded() {
- return this.expanded;
- }
- getRowCount() {
- return this.currentRows;
- }
-};
-var DateRenderer = class {
- constructor(dateService) {
- this.dateService = dateService;
- this.type = "date";
- }
- render(context) {
- const dates = context.filter["date"] || [];
- const resourceIds = context.filter["resource"] || [];
- const dateGrouping = context.groupings?.find((g) => g.type === "date");
- const hideHeader = dateGrouping?.hideHeader === true;
- const iterations = resourceIds.length || 1;
- let columnCount = 0;
- for (let r = 0; r < iterations; r++) {
- const resourceId = resourceIds[r];
- for (const dateStr of dates) {
- const date = this.dateService.parseISO(dateStr);
- const segments = { date: dateStr };
- if (resourceId)
- segments.resource = resourceId;
- const columnKey = this.dateService.buildColumnKey(segments);
- const header = document.createElement("swp-day-header");
- header.dataset.date = dateStr;
- header.dataset.columnKey = columnKey;
- if (resourceId) {
- header.dataset.resourceId = resourceId;
- }
- if (hideHeader) {
- header.dataset.hidden = "true";
- }
- header.innerHTML = `
- ${this.dateService.getDayName(date, "short")}
- ${date.getDate()}
- `;
- context.headerContainer.appendChild(header);
- const column = document.createElement("swp-day-column");
- column.dataset.date = dateStr;
- column.dataset.columnKey = columnKey;
- if (resourceId) {
- column.dataset.resourceId = resourceId;
- }
- column.innerHTML = " ";
- context.columnContainer.appendChild(column);
- columnCount++;
- }
- }
- const container = context.columnContainer.closest("swp-calendar-container");
- if (container) {
- container.style.setProperty("--grid-columns", String(columnCount));
- }
- }
-};
-var ResourceRenderer = class extends BaseGroupingRenderer {
- constructor(resourceService) {
- super();
- this.resourceService = resourceService;
- this.type = "resource";
- this.config = {
- elementTag: "swp-resource-header",
- idAttribute: "resourceId",
- colspanVar: "--resource-cols"
- };
- }
- getEntities(ids) {
- return this.resourceService.getByIds(ids);
- }
- getDisplayName(entity) {
- return entity.displayName;
- }
- /**
- * Override render to handle:
- * 1. Special ordering when parentChildMap exists (resources grouped by parent)
- * 2. Different colspan calculation (just dateCount, not childCount * dateCount)
- */
- async render(context) {
- const resourceIds = context.filter["resource"] || [];
- const dateCount = context.filter["date"]?.length || 1;
- let orderedResourceIds;
- if (context.parentChildMap) {
- orderedResourceIds = [];
- for (const childIds of Object.values(context.parentChildMap)) {
- for (const childId of childIds) {
- if (resourceIds.includes(childId)) {
- orderedResourceIds.push(childId);
- }
- }
- }
- } else {
- orderedResourceIds = resourceIds;
- }
- const resources = await this.getEntities(orderedResourceIds);
- const resourceMap = new Map(resources.map((r) => [r.id, r]));
- for (const resourceId of orderedResourceIds) {
- const resource = resourceMap.get(resourceId);
- if (!resource)
- continue;
- const header = this.createHeader(resource, context);
- header.style.gridColumn = `span ${dateCount}`;
- context.headerContainer.appendChild(header);
- }
- }
-};
-function calculateEventPosition(start, end, config) {
- const startMinutes = start.getHours() * 60 + start.getMinutes();
- const endMinutes = end.getHours() * 60 + end.getMinutes();
- const dayStartMinutes = config.dayStartHour * 60;
- const minuteHeight = config.hourHeight / 60;
- const top = (startMinutes - dayStartMinutes) * minuteHeight;
- const height = (endMinutes - startMinutes) * minuteHeight;
- return { top, height };
-}
-function minutesToPixels(minutes, config) {
- return minutes / 60 * config.hourHeight;
-}
-function pixelsToMinutes(pixels, config) {
- return pixels / config.hourHeight * 60;
-}
-function snapToGrid(pixels, config) {
- const snapPixels = minutesToPixels(config.snapInterval, config);
- return Math.round(pixels / snapPixels) * snapPixels;
-}
-function eventsOverlap(a, b) {
- return a.start < b.end && a.end > b.start;
-}
-function eventsWithinThreshold(a, b, thresholdMinutes) {
- const thresholdMs = thresholdMinutes * 60 * 1e3;
- const startToStartDiff = Math.abs(a.start.getTime() - b.start.getTime());
- if (startToStartDiff <= thresholdMs)
- return true;
- const bStartsBeforeAEnds = a.end.getTime() - b.start.getTime();
- if (bStartsBeforeAEnds > 0 && bStartsBeforeAEnds <= thresholdMs)
- return true;
- const aStartsBeforeBEnds = b.end.getTime() - a.start.getTime();
- if (aStartsBeforeBEnds > 0 && aStartsBeforeBEnds <= thresholdMs)
- return true;
- return false;
-}
-function findOverlapGroups(events) {
- if (events.length === 0)
- return [];
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- const used = /* @__PURE__ */ new Set();
- const groups = [];
- for (const event of sorted) {
- if (used.has(event.id))
- continue;
- const group = [event];
- used.add(event.id);
- let expanded = true;
- while (expanded) {
- expanded = false;
- for (const candidate of sorted) {
- if (used.has(candidate.id))
- continue;
- const connects = group.some((member) => eventsOverlap(member, candidate));
- if (connects) {
- group.push(candidate);
- used.add(candidate.id);
- expanded = true;
- }
- }
- }
- groups.push(group);
- }
- return groups;
-}
-function findGridCandidates(events, thresholdMinutes) {
- if (events.length === 0)
- return [];
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- const used = /* @__PURE__ */ new Set();
- const groups = [];
- for (const event of sorted) {
- if (used.has(event.id))
- continue;
- const group = [event];
- used.add(event.id);
- let expanded = true;
- while (expanded) {
- expanded = false;
- for (const candidate of sorted) {
- if (used.has(candidate.id))
- continue;
- const connects = group.some((member) => eventsWithinThreshold(member, candidate, thresholdMinutes));
- if (connects) {
- group.push(candidate);
- used.add(candidate.id);
- expanded = true;
- }
- }
- }
- groups.push(group);
- }
- return groups;
-}
-function calculateStackLevels(events) {
- const levels = /* @__PURE__ */ new Map();
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- for (const event of sorted) {
- let maxOverlappingLevel = -1;
- for (const [id, level] of levels) {
- const other = events.find((e) => e.id === id);
- if (other && eventsOverlap(event, other)) {
- maxOverlappingLevel = Math.max(maxOverlappingLevel, level);
- }
- }
- levels.set(event.id, maxOverlappingLevel + 1);
- }
- return levels;
-}
-function allocateColumns(events) {
- const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
- const columns = [];
- for (const event of sorted) {
- let placed = false;
- for (const column of columns) {
- const canFit = !column.some((e) => eventsOverlap(event, e));
- if (canFit) {
- column.push(event);
- placed = true;
- break;
- }
- }
- if (!placed) {
- columns.push([event]);
- }
- }
- return columns;
-}
-function calculateColumnLayout(events, config) {
- const thresholdMinutes = config.gridStartThresholdMinutes ?? 10;
- const result = {
- grids: [],
- stacked: []
- };
- if (events.length === 0)
- return result;
- const overlapGroups = findOverlapGroups(events);
- for (const overlapGroup of overlapGroups) {
- if (overlapGroup.length === 1) {
- result.stacked.push({
- event: overlapGroup[0],
- stackLevel: 0
- });
- continue;
- }
- const gridSubgroups = findGridCandidates(overlapGroup, thresholdMinutes);
- const largestGridCandidate = gridSubgroups.reduce((max, g) => g.length > max.length ? g : max, gridSubgroups[0]);
- if (largestGridCandidate.length === overlapGroup.length) {
- const columns = allocateColumns(overlapGroup);
- const earliest = overlapGroup.reduce((min, e) => e.start < min.start ? e : min, overlapGroup[0]);
- const position = calculateEventPosition(earliest.start, earliest.end, config);
- result.grids.push({
- events: overlapGroup,
- columns,
- stackLevel: 0,
- position: { top: position.top }
- });
- } else {
- const levels = calculateStackLevels(overlapGroup);
- for (const event of overlapGroup) {
- result.stacked.push({
- event,
- stackLevel: levels.get(event.id) ?? 0
- });
- }
- }
- }
- return result;
-}
-var EventRenderer = class {
- constructor(eventService, dateService, gridConfig, eventBus) {
- this.eventService = eventService;
- this.dateService = dateService;
- this.gridConfig = gridConfig;
- this.eventBus = eventBus;
- this.container = null;
- this.setupListeners();
- }
- /**
- * Setup listeners for drag-drop and update events
- */
- setupListeners() {
- this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => {
- const payload = e.detail;
- this.handleColumnChange(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE, (e) => {
- const payload = e.detail;
- this.updateDragTimestamp(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_UPDATED, (e) => {
- const payload = e.detail;
- this.handleEventUpdated(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => {
- const payload = e.detail;
- this.handleDragEnd(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
- const payload = e.detail;
- this.handleDragLeaveHeader(payload);
- });
- }
- /**
- * Handle EVENT_DRAG_END - remove element if dropped in header
- */
- handleDragEnd(payload) {
- if (payload.target === "header") {
- const element = this.container?.querySelector(`swp-content-viewport swp-event[data-event-id="${payload.swpEvent.eventId}"]`);
- element?.remove();
- }
- }
- /**
- * Handle header item leaving header - create swp-event in grid
- */
- handleDragLeaveHeader(payload) {
- if (payload.source !== "header")
- return;
- if (!payload.targetColumn || !payload.start || !payload.end)
- return;
- if (payload.element) {
- payload.element.classList.add("drag-ghost");
- payload.element.style.opacity = "0.3";
- payload.element.style.pointerEvents = "none";
- }
- const event = {
- id: payload.eventId,
- title: payload.title || "",
- description: "",
- start: payload.start,
- end: payload.end,
- type: "customer",
- allDay: false,
- syncStatus: "pending"
- };
- const element = this.createEventElement(event);
- let eventsLayer = payload.targetColumn.querySelector("swp-events-layer");
- if (!eventsLayer) {
- eventsLayer = document.createElement("swp-events-layer");
- payload.targetColumn.appendChild(eventsLayer);
- }
- eventsLayer.appendChild(element);
- element.classList.add("dragging");
- }
- /**
- * Handle EVENT_UPDATED - re-render affected columns
- */
- async handleEventUpdated(payload) {
- if (payload.sourceColumnKey !== payload.targetColumnKey) {
- await this.rerenderColumn(payload.sourceColumnKey);
- }
- await this.rerenderColumn(payload.targetColumnKey);
- }
- /**
- * Re-render a single column with fresh data from IndexedDB
- */
- async rerenderColumn(columnKey) {
- const column = this.findColumn(columnKey);
- if (!column)
- return;
- const date = column.dataset.date;
- const resourceId = column.dataset.resourceId;
- if (!date)
- return;
- const startDate = new Date(date);
- const endDate = new Date(date);
- endDate.setHours(23, 59, 59, 999);
- const events = resourceId ? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate) : await this.eventService.getByDateRange(startDate, endDate);
- const timedEvents = events.filter((event) => !event.allDay && this.dateService.getDateKey(event.start) === date);
- let eventsLayer = column.querySelector("swp-events-layer");
- if (!eventsLayer) {
- eventsLayer = document.createElement("swp-events-layer");
- column.appendChild(eventsLayer);
- }
- eventsLayer.innerHTML = "";
- const layout = calculateColumnLayout(timedEvents, this.gridConfig);
- layout.grids.forEach((grid) => {
- const groupEl = this.renderGridGroup(grid);
- eventsLayer.appendChild(groupEl);
- });
- layout.stacked.forEach((item) => {
- const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
- eventsLayer.appendChild(eventEl);
- });
- }
- /**
- * Find a column element by columnKey
- */
- findColumn(columnKey) {
- if (!this.container)
- return null;
- return this.container.querySelector(`swp-day-column[data-column-key="${columnKey}"]`);
- }
- /**
- * Handle event moving to a new column during drag
- */
- handleColumnChange(payload) {
- const eventsLayer = payload.newColumn.querySelector("swp-events-layer");
- if (!eventsLayer)
- return;
- eventsLayer.appendChild(payload.element);
- payload.element.style.top = `${payload.currentY}px`;
- }
- /**
- * Update timestamp display during drag (snapped to grid)
- */
- updateDragTimestamp(payload) {
- const timeEl = payload.element.querySelector("swp-event-time");
- if (!timeEl)
- return;
- const snappedY = snapToGrid(payload.currentY, this.gridConfig);
- const minutesFromGridStart = pixelsToMinutes(snappedY, this.gridConfig);
- const startMinutes = this.gridConfig.dayStartHour * 60 + minutesFromGridStart;
- const height = parseFloat(payload.element.style.height) || this.gridConfig.hourHeight;
- const durationMinutes = pixelsToMinutes(height, this.gridConfig);
- const start = this.minutesToDate(startMinutes);
- const end = this.minutesToDate(startMinutes + durationMinutes);
- timeEl.textContent = this.dateService.formatTimeRange(start, end);
- }
- /**
- * Convert minutes since midnight to a Date object (today)
- */
- minutesToDate(minutes) {
- const date = /* @__PURE__ */ new Date();
- date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0);
- return date;
- }
- /**
- * Render events for visible dates into day columns
- * @param container - Calendar container element
- * @param filter - Filter with 'date' and optionally 'resource' arrays
- * @param filterTemplate - Template for matching events to columns
- */
- async render(container, filter, filterTemplate) {
- this.container = container;
- const visibleDates = filter["date"] || [];
- if (visibleDates.length === 0)
- return;
- const startDate = new Date(visibleDates[0]);
- const endDate = new Date(visibleDates[visibleDates.length - 1]);
- endDate.setHours(23, 59, 59, 999);
- const events = await this.eventService.getByDateRange(startDate, endDate);
- const dayColumns = container.querySelector("swp-day-columns");
- if (!dayColumns)
- return;
- const columns = dayColumns.querySelectorAll("swp-day-column");
- columns.forEach((column) => {
- const columnEl = column;
- const columnEvents = events.filter((event) => filterTemplate.matches(event, columnEl));
- let eventsLayer = column.querySelector("swp-events-layer");
- if (!eventsLayer) {
- eventsLayer = document.createElement("swp-events-layer");
- column.appendChild(eventsLayer);
- }
- eventsLayer.innerHTML = "";
- const timedEvents = columnEvents.filter((event) => !event.allDay);
- const layout = calculateColumnLayout(timedEvents, this.gridConfig);
- layout.grids.forEach((grid) => {
- const groupEl = this.renderGridGroup(grid);
- eventsLayer.appendChild(groupEl);
- });
- layout.stacked.forEach((item) => {
- const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
- eventsLayer.appendChild(eventEl);
- });
- });
- }
- /**
- * Create a single event element
- *
- * CLEAN approach:
- * - Only data-id for lookup
- * - Visible content in innerHTML only
- */
- createEventElement(event) {
- const element = document.createElement("swp-event");
- element.dataset.eventId = event.id;
- if (event.resourceId) {
- element.dataset.resourceId = event.resourceId;
- }
- const position = calculateEventPosition(event.start, event.end, this.gridConfig);
- element.style.top = `${position.top}px`;
- element.style.height = `${position.height}px`;
- const colorClass = this.getColorClass(event);
- if (colorClass) {
- element.classList.add(colorClass);
- }
- element.innerHTML = `
- ${this.dateService.formatTimeRange(event.start, event.end)}
- ${this.escapeHtml(event.title)}
- ${event.description ? `${this.escapeHtml(event.description)} ` : ""}
- `;
- return element;
- }
- /**
- * Get color class based on metadata.color or event type
- */
- getColorClass(event) {
- if (event.metadata?.color) {
- return `is-${event.metadata.color}`;
- }
- const typeColors = {
- "customer": "is-blue",
- "vacation": "is-green",
- "break": "is-amber",
- "meeting": "is-purple",
- "blocked": "is-red"
- };
- return typeColors[event.type] || "is-blue";
- }
- /**
- * Escape HTML to prevent XSS
- */
- escapeHtml(text) {
- const div = document.createElement("div");
- div.textContent = text;
- return div.innerHTML;
- }
- /**
- * Render a GRID group with side-by-side columns
- * Used when multiple events start at the same time
- */
- renderGridGroup(layout) {
- const group = document.createElement("swp-event-group");
- group.classList.add(`cols-${layout.columns.length}`);
- group.style.top = `${layout.position.top}px`;
- if (layout.stackLevel > 0) {
- group.style.marginLeft = `${layout.stackLevel * 15}px`;
- group.style.zIndex = `${100 + layout.stackLevel}`;
- }
- let maxBottom = 0;
- for (const event of layout.events) {
- const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
- const eventBottom = pos.top + pos.height;
- if (eventBottom > maxBottom)
- maxBottom = eventBottom;
- }
- const groupHeight = maxBottom - layout.position.top;
- group.style.height = `${groupHeight}px`;
- layout.columns.forEach((columnEvents) => {
- const wrapper = document.createElement("div");
- wrapper.style.position = "relative";
- columnEvents.forEach((event) => {
- const eventEl = this.createEventElement(event);
- const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
- eventEl.style.top = `${pos.top - layout.position.top}px`;
- eventEl.style.position = "absolute";
- eventEl.style.left = "0";
- eventEl.style.right = "0";
- wrapper.appendChild(eventEl);
- });
- group.appendChild(wrapper);
- });
- return group;
- }
- /**
- * Render a STACKED event with margin-left offset
- * Used for overlapping events that don't start at the same time
- */
- renderStackedEvent(event, stackLevel) {
- const element = this.createEventElement(event);
- element.dataset.stackLink = JSON.stringify({ stackLevel });
- if (stackLevel > 0) {
- element.style.marginLeft = `${stackLevel * 15}px`;
- element.style.zIndex = `${100 + stackLevel}`;
- }
- return element;
- }
-};
-var TimeAxisRenderer = class {
- render(container, startHour = 6, endHour = 20) {
- container.innerHTML = "";
- for (let hour = startHour; hour <= endHour; hour++) {
- const marker = document.createElement("swp-hour-marker");
- marker.textContent = `${hour.toString().padStart(2, "0")}:00`;
- container.appendChild(marker);
- }
- }
-};
-var HeaderDrawerRenderer = class {
- constructor(eventBus, gridConfig, headerDrawerManager, eventService, dateService) {
- this.eventBus = eventBus;
- this.gridConfig = gridConfig;
- this.headerDrawerManager = headerDrawerManager;
- this.eventService = eventService;
- this.dateService = dateService;
- this.currentItem = null;
- this.container = null;
- this.sourceElement = null;
- this.wasExpandedBeforeDrag = false;
- this.filterTemplate = null;
- this.setupListeners();
- }
- /**
- * Render allDay events into the header drawer with row stacking
- * @param filterTemplate - Template for matching events to columns
- */
- async render(container, filter, filterTemplate) {
- this.filterTemplate = filterTemplate;
- const drawer = container.querySelector("swp-header-drawer");
- if (!drawer)
- return;
- const visibleDates = filter["date"] || [];
- if (visibleDates.length === 0)
- return;
- const visibleColumnKeys = this.getVisibleColumnKeysFromDOM();
- if (visibleColumnKeys.length === 0)
- return;
- const startDate = new Date(visibleDates[0]);
- const endDate = new Date(visibleDates[visibleDates.length - 1]);
- endDate.setHours(23, 59, 59, 999);
- const events = await this.eventService.getByDateRange(startDate, endDate);
- const allDayEvents = events.filter((event) => event.allDay !== false);
- drawer.innerHTML = "";
- if (allDayEvents.length === 0)
- return;
- const layouts = this.calculateLayout(allDayEvents, visibleColumnKeys);
- const rowCount = Math.max(1, ...layouts.map((l) => l.row));
- layouts.forEach((layout) => {
- const item = this.createHeaderItem(layout);
- drawer.appendChild(item);
- });
- this.headerDrawerManager.expandToRows(rowCount);
- }
- /**
- * Create a header item element from layout
- */
- createHeaderItem(layout) {
- const { event, columnKey, row, colStart, colEnd } = layout;
- const item = document.createElement("swp-header-item");
- item.dataset.eventId = event.id;
- item.dataset.itemType = "event";
- item.dataset.start = event.start.toISOString();
- item.dataset.end = event.end.toISOString();
- item.dataset.columnKey = columnKey;
- item.textContent = event.title;
- const colorClass = this.getColorClass(event);
- if (colorClass)
- item.classList.add(colorClass);
- item.style.gridArea = `${row} / ${colStart} / ${row + 1} / ${colEnd}`;
- return item;
- }
- /**
- * Calculate layout for all events with row stacking
- * Uses track-based algorithm to find available rows for overlapping events
- */
- calculateLayout(events, visibleColumnKeys) {
- const tracks = [new Array(visibleColumnKeys.length).fill(false)];
- const layouts = [];
- for (const event of events) {
- const columnKey = this.buildColumnKeyFromEvent(event);
- const startCol = visibleColumnKeys.indexOf(columnKey);
- const endColumnKey = this.buildColumnKeyFromEvent(event, event.end);
- const endCol = visibleColumnKeys.indexOf(endColumnKey);
- if (startCol === -1 && endCol === -1)
- continue;
- const colStart = Math.max(0, startCol);
- const colEnd = (endCol !== -1 ? endCol : visibleColumnKeys.length - 1) + 1;
- const row = this.findAvailableRow(tracks, colStart, colEnd);
- for (let c = colStart; c < colEnd; c++) {
- tracks[row][c] = true;
- }
- layouts.push({ event, columnKey, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 });
- }
- return layouts;
- }
- /**
- * Build columnKey from event using FilterTemplate
- * Uses the same template that columns use for matching
- */
- buildColumnKeyFromEvent(event, date) {
- if (!this.filterTemplate) {
- const dateStr = this.dateService.getDateKey(date || event.start);
- return dateStr;
- }
- if (date && date.getTime() !== event.start.getTime()) {
- const tempEvent = { ...event, start: date };
- return this.filterTemplate.buildKeyFromEvent(tempEvent);
- }
- return this.filterTemplate.buildKeyFromEvent(event);
- }
- /**
- * Find available row for event spanning columns [colStart, colEnd)
- */
- findAvailableRow(tracks, colStart, colEnd) {
- for (let row = 0; row < tracks.length; row++) {
- let available = true;
- for (let c = colStart; c < colEnd; c++) {
- if (tracks[row][c]) {
- available = false;
- break;
- }
- }
- if (available)
- return row;
- }
- tracks.push(new Array(tracks[0].length).fill(false));
- return tracks.length - 1;
- }
- /**
- * Get color class based on event metadata or type
- */
- getColorClass(event) {
- if (event.metadata?.color) {
- return `is-${event.metadata.color}`;
- }
- const typeColors = {
- "customer": "is-blue",
- "vacation": "is-green",
- "break": "is-amber",
- "meeting": "is-purple",
- "blocked": "is-red"
- };
- return typeColors[event.type] || "is-blue";
- }
- /**
- * Setup event listeners for drag events
- */
- setupListeners() {
- this.eventBus.on(CoreEvents.EVENT_DRAG_ENTER_HEADER, (e) => {
- const payload = e.detail;
- this.handleDragEnter(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE_HEADER, (e) => {
- const payload = e.detail;
- this.handleDragMove(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
- const payload = e.detail;
- this.handleDragLeave(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => {
- const payload = e.detail;
- this.handleDragEnd(payload);
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => {
- this.cleanup();
- });
- }
- /**
- * Handle drag entering header zone - create preview item
- */
- handleDragEnter(payload) {
- this.container = document.querySelector("swp-header-drawer");
- if (!this.container)
- return;
- this.wasExpandedBeforeDrag = this.headerDrawerManager.isExpanded();
- if (!this.wasExpandedBeforeDrag) {
- this.headerDrawerManager.expandToRows(1);
- }
- this.sourceElement = payload.element;
- const item = document.createElement("swp-header-item");
- item.dataset.eventId = payload.eventId;
- item.dataset.itemType = payload.itemType;
- item.dataset.duration = String(payload.duration);
- item.dataset.columnKey = payload.sourceColumnKey;
- item.textContent = payload.title;
- if (payload.colorClass) {
- item.classList.add(payload.colorClass);
- }
- item.classList.add("dragging");
- const col = payload.sourceColumnIndex + 1;
- const endCol = col + payload.duration;
- item.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
- this.container.appendChild(item);
- this.currentItem = item;
- payload.element.style.visibility = "hidden";
- }
- /**
- * Handle drag moving within header - update column position
- */
- handleDragMove(payload) {
- if (!this.currentItem)
- return;
- const col = payload.columnIndex + 1;
- const duration = parseInt(this.currentItem.dataset.duration || "1", 10);
- const endCol = col + duration;
- this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
- this.currentItem.dataset.columnKey = payload.columnKey;
- }
- /**
- * Handle drag leaving header - cleanup for grid→header drag only
- */
- handleDragLeave(payload) {
- if (payload.source === "grid") {
- this.cleanup();
- }
- }
- /**
- * Handle drag end - finalize based on drop target
- */
- handleDragEnd(payload) {
- if (payload.target === "header") {
- if (this.currentItem) {
- this.currentItem.classList.remove("dragging");
- this.recalculateDrawerLayout();
- this.currentItem = null;
- this.sourceElement = null;
- }
- } else {
- const ghost = document.querySelector(`swp-header-item.drag-ghost[data-event-id="${payload.swpEvent.eventId}"]`);
- ghost?.remove();
- this.recalculateDrawerLayout();
- }
- }
- /**
- * Recalculate layout for all items currently in the drawer
- * Called after drop to reposition items and adjust height
- */
- recalculateDrawerLayout() {
- const drawer = document.querySelector("swp-header-drawer");
- if (!drawer)
- return;
- const items = Array.from(drawer.querySelectorAll("swp-header-item"));
- if (items.length === 0)
- return;
- const visibleColumnKeys = this.getVisibleColumnKeysFromDOM();
- if (visibleColumnKeys.length === 0)
- return;
- const itemData = items.map((item) => ({
- element: item,
- columnKey: item.dataset.columnKey || "",
- duration: parseInt(item.dataset.duration || "1", 10)
- }));
- const tracks = [new Array(visibleColumnKeys.length).fill(false)];
- for (const item of itemData) {
- const startCol = visibleColumnKeys.indexOf(item.columnKey);
- if (startCol === -1)
- continue;
- const colStart = startCol;
- const colEnd = Math.min(startCol + item.duration, visibleColumnKeys.length);
- const row = this.findAvailableRow(tracks, colStart, colEnd);
- for (let c = colStart; c < colEnd; c++) {
- tracks[row][c] = true;
- }
- item.element.style.gridArea = `${row + 1} / ${colStart + 1} / ${row + 2} / ${colEnd + 1}`;
- }
- const rowCount = tracks.length;
- this.headerDrawerManager.expandToRows(rowCount);
- }
- /**
- * Get visible column keys from DOM (preserves order for multi-resource views)
- * Uses filterTemplate.buildKeyFromColumn() for consistent key format with events
- */
- getVisibleColumnKeysFromDOM() {
- if (!this.filterTemplate)
- return [];
- const columns = document.querySelectorAll("swp-day-column");
- const columnKeys = [];
- columns.forEach((col) => {
- const columnKey = this.filterTemplate.buildKeyFromColumn(col);
- if (columnKey)
- columnKeys.push(columnKey);
- });
- return columnKeys;
- }
- /**
- * Cleanup preview item and restore source visibility
- */
- cleanup() {
- this.currentItem?.remove();
- this.currentItem = null;
- if (this.sourceElement) {
- this.sourceElement.style.visibility = "";
- this.sourceElement = null;
- }
- if (!this.wasExpandedBeforeDrag) {
- this.headerDrawerManager.collapse();
- }
- }
-};
-var ScheduleRenderer = class {
- constructor(scheduleService, dateService, gridConfig) {
- this.scheduleService = scheduleService;
- this.dateService = dateService;
- this.gridConfig = gridConfig;
- }
- /**
- * Render unavailable zones for visible columns
- * @param container - Calendar container element
- * @param filter - Filter with 'date' and 'resource' arrays
- */
- async render(container, filter) {
- const dates = filter["date"] || [];
- const resourceIds = filter["resource"] || [];
- if (dates.length === 0)
- return;
- const dayColumns = container.querySelector("swp-day-columns");
- if (!dayColumns)
- return;
- const columns = dayColumns.querySelectorAll("swp-day-column");
- for (const column of columns) {
- const date = column.dataset.date;
- const resourceId = column.dataset.resourceId;
- if (!date || !resourceId)
- continue;
- let unavailableLayer = column.querySelector("swp-unavailable-layer");
- if (!unavailableLayer) {
- unavailableLayer = document.createElement("swp-unavailable-layer");
- column.insertBefore(unavailableLayer, column.firstChild);
- }
- unavailableLayer.innerHTML = "";
- const schedule = await this.scheduleService.getScheduleForDate(resourceId, date);
- this.renderUnavailableZones(unavailableLayer, schedule);
- }
- }
- /**
- * Render unavailable time zones based on schedule
- */
- renderUnavailableZones(layer, schedule) {
- const dayStartMinutes = this.gridConfig.dayStartHour * 60;
- const dayEndMinutes = this.gridConfig.dayEndHour * 60;
- const minuteHeight = this.gridConfig.hourHeight / 60;
- if (schedule === null) {
- const zone = this.createUnavailableZone(0, (dayEndMinutes - dayStartMinutes) * minuteHeight);
- layer.appendChild(zone);
- return;
- }
- const workStartMinutes = this.dateService.timeToMinutes(schedule.start);
- const workEndMinutes = this.dateService.timeToMinutes(schedule.end);
- if (workStartMinutes > dayStartMinutes) {
- const top = 0;
- const height = (workStartMinutes - dayStartMinutes) * minuteHeight;
- const zone = this.createUnavailableZone(top, height);
- layer.appendChild(zone);
- }
- if (workEndMinutes < dayEndMinutes) {
- const top = (workEndMinutes - dayStartMinutes) * minuteHeight;
- const height = (dayEndMinutes - workEndMinutes) * minuteHeight;
- const zone = this.createUnavailableZone(top, height);
- layer.appendChild(zone);
- }
- }
- /**
- * Create an unavailable zone element
- */
- createUnavailableZone(top, height) {
- const zone = document.createElement("swp-unavailable-zone");
- zone.style.top = `${top}px`;
- zone.style.height = `${height}px`;
- return zone;
- }
-};
-var defaultDBConfig = {
- dbName: "CalendarDB",
- dbVersion: 4
-};
-var IndexedDBContext = class {
- constructor(stores, config) {
- this.db = null;
- this.initialized = false;
- this.stores = stores;
- this.config = config;
- }
- get dbName() {
- return this.config.dbName;
- }
- /**
- * Initialize and open the database
- */
- async initialize() {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.config.dbName, this.config.dbVersion);
- request.onerror = () => {
- reject(new Error(`Failed to open IndexedDB: ${request.error}`));
- };
- request.onsuccess = () => {
- this.db = request.result;
- this.initialized = true;
- resolve();
- };
- request.onupgradeneeded = (event) => {
- const db = event.target.result;
- this.stores.forEach((store) => {
- if (!db.objectStoreNames.contains(store.storeName)) {
- store.create(db);
- }
- });
- };
- });
- }
- /**
- * Check if database is initialized
- */
- isInitialized() {
- return this.initialized;
- }
- /**
- * Get IDBDatabase instance
- */
- getDatabase() {
- if (!this.db) {
- throw new Error("IndexedDB not initialized. Call initialize() first.");
- }
- return this.db;
- }
- /**
- * Close database connection
- */
- close() {
- if (this.db) {
- this.db.close();
- this.db = null;
- this.initialized = false;
- }
- }
- /**
- * Delete entire database (for testing/reset)
- */
- static async deleteDatabase(dbName = defaultDBConfig.dbName) {
- return new Promise((resolve, reject) => {
- const request = indexedDB.deleteDatabase(dbName);
- request.onsuccess = () => resolve();
- request.onerror = () => reject(new Error(`Failed to delete database: ${request.error}`));
- });
- }
-};
-var EventStore = class _EventStore {
- constructor() {
- this.storeName = _EventStore.STORE_NAME;
- }
- /**
- * Create the events ObjectStore with indexes
- */
- create(db) {
- const store = db.createObjectStore(_EventStore.STORE_NAME, { keyPath: "id" });
- store.createIndex("start", "start", { unique: false });
- store.createIndex("end", "end", { unique: false });
- store.createIndex("syncStatus", "syncStatus", { unique: false });
- store.createIndex("resourceId", "resourceId", { unique: false });
- store.createIndex("customerId", "customerId", { unique: false });
- store.createIndex("bookingId", "bookingId", { unique: false });
- store.createIndex("startEnd", ["start", "end"], { unique: false });
- }
-};
-EventStore.STORE_NAME = "events";
-var EventSerialization = class {
- /**
- * Serialize event for IndexedDB storage
- */
- static serialize(event) {
- return {
- ...event,
- start: event.start instanceof Date ? event.start.toISOString() : event.start,
- end: event.end instanceof Date ? event.end.toISOString() : event.end
- };
- }
- /**
- * Deserialize event from IndexedDB storage
- */
- static deserialize(data) {
- return {
- ...data,
- start: typeof data.start === "string" ? new Date(data.start) : data.start,
- end: typeof data.end === "string" ? new Date(data.end) : data.end
- };
- }
-};
-var EventService = class extends BaseEntityService {
- constructor(context, eventBus) {
- super(context, eventBus);
- this.storeName = EventStore.STORE_NAME;
- this.entityType = "Event";
- }
- serialize(event) {
- return EventSerialization.serialize(event);
- }
- deserialize(data) {
- return EventSerialization.deserialize(data);
- }
- /**
- * Get events within a date range
- */
- async getByDateRange(start, end) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
- const index = store.index("start");
- const range = IDBKeyRange.lowerBound(start.toISOString());
- const request = index.getAll(range);
- request.onsuccess = () => {
- const data = request.result;
- const events = data.map((item) => this.deserialize(item)).filter((event) => event.start <= end);
- resolve(events);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get events by date range: ${request.error}`));
- };
- });
- }
- /**
- * Get events for a specific resource
- */
- async getByResource(resourceId) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
- const index = store.index("resourceId");
- const request = index.getAll(resourceId);
- request.onsuccess = () => {
- const data = request.result;
- const events = data.map((item) => this.deserialize(item));
- resolve(events);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get events for resource ${resourceId}: ${request.error}`));
- };
- });
- }
- /**
- * Get events for a resource within a date range
- */
- async getByResourceAndDateRange(resourceId, start, end) {
- const resourceEvents = await this.getByResource(resourceId);
- return resourceEvents.filter((event) => event.start >= start && event.start <= end);
- }
-};
-var ResourceStore = class _ResourceStore {
- constructor() {
- this.storeName = _ResourceStore.STORE_NAME;
- }
- create(db) {
- const store = db.createObjectStore(_ResourceStore.STORE_NAME, { keyPath: "id" });
- store.createIndex("type", "type", { unique: false });
- store.createIndex("syncStatus", "syncStatus", { unique: false });
- store.createIndex("isActive", "isActive", { unique: false });
- }
-};
-ResourceStore.STORE_NAME = "resources";
-var ResourceService = class extends BaseEntityService {
- constructor(context, eventBus) {
- super(context, eventBus);
- this.storeName = ResourceStore.STORE_NAME;
- this.entityType = "Resource";
- }
- /**
- * Get all active resources
- */
- async getActive() {
- const all = await this.getAll();
- return all.filter((r) => r.isActive !== false);
- }
- /**
- * Get resources by IDs
- */
- async getByIds(ids) {
- if (ids.length === 0)
- return [];
- const results = await Promise.all(ids.map((id) => this.get(id)));
- return results.filter((r) => r !== null);
- }
- /**
- * Get resources by type
- */
- async getByType(type) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
- const index = store.index("type");
- const request = index.getAll(type);
- request.onsuccess = () => {
- const data = request.result;
- resolve(data);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get resources by type ${type}: ${request.error}`));
- };
- });
- }
-};
-var SettingsIds = {
- WORKWEEK: "workweek",
- GRID: "grid",
- TIME_FORMAT: "timeFormat",
- VIEWS: "views"
-};
-var SettingsStore = class _SettingsStore {
- constructor() {
- this.storeName = _SettingsStore.STORE_NAME;
- }
- create(db) {
- db.createObjectStore(_SettingsStore.STORE_NAME, { keyPath: "id" });
- }
-};
-SettingsStore.STORE_NAME = "settings";
-var SettingsService = class extends BaseEntityService {
- constructor(context, eventBus) {
- super(context, eventBus);
- this.storeName = SettingsStore.STORE_NAME;
- this.entityType = "Settings";
- }
- /**
- * Get workweek settings
- */
- async getWorkweekSettings() {
- return this.get(SettingsIds.WORKWEEK);
- }
- /**
- * Get grid settings
- */
- async getGridSettings() {
- return this.get(SettingsIds.GRID);
- }
- /**
- * Get time format settings
- */
- async getTimeFormatSettings() {
- return this.get(SettingsIds.TIME_FORMAT);
- }
- /**
- * Get view settings
- */
- async getViewSettings() {
- return this.get(SettingsIds.VIEWS);
- }
- /**
- * Get workweek preset by ID
- */
- async getWorkweekPreset(presetId) {
- const settings = await this.getWorkweekSettings();
- if (!settings)
- return null;
- return settings.presets[presetId] || null;
- }
- /**
- * Get the default workweek preset
- */
- async getDefaultWorkweekPreset() {
- const settings = await this.getWorkweekSettings();
- if (!settings)
- return null;
- return settings.presets[settings.defaultPreset] || null;
- }
- /**
- * Get all available workweek presets
- */
- async getWorkweekPresets() {
- const settings = await this.getWorkweekSettings();
- if (!settings)
- return [];
- return Object.values(settings.presets);
- }
-};
-var ViewConfigStore = class _ViewConfigStore {
- constructor() {
- this.storeName = _ViewConfigStore.STORE_NAME;
- }
- create(db) {
- db.createObjectStore(_ViewConfigStore.STORE_NAME, { keyPath: "id" });
- }
-};
-ViewConfigStore.STORE_NAME = "viewconfigs";
-var ViewConfigService = class extends BaseEntityService {
- constructor(context, eventBus) {
- super(context, eventBus);
- this.storeName = ViewConfigStore.STORE_NAME;
- this.entityType = "ViewConfig";
- }
- async getById(id) {
- return this.get(id);
- }
-};
-var SwpEvent = class _SwpEvent {
- constructor(element, columnKey, start, end) {
- this.element = element;
- this.columnKey = columnKey;
- this._start = start;
- this._end = end;
- }
- /** Event ID from element.dataset.eventId */
- get eventId() {
- return this.element.dataset.eventId || "";
- }
- get start() {
- return this._start;
- }
- get end() {
- return this._end;
- }
- /** Duration in minutes */
- get durationMinutes() {
- return (this._end.getTime() - this._start.getTime()) / (1e3 * 60);
- }
- /** Duration in milliseconds */
- get durationMs() {
- return this._end.getTime() - this._start.getTime();
- }
- /**
- * Factory: Create SwpEvent from element + columnKey
- * Reads top/height from element.style to calculate start/end
- * @param columnKey - Opaque column identifier (do NOT parse - use only for matching)
- * @param date - Date string (YYYY-MM-DD) for time calculations
- */
- static fromElement(element, columnKey, date, gridConfig) {
- const topPixels = parseFloat(element.style.top) || 0;
- const heightPixels = parseFloat(element.style.height) || 0;
- const startMinutesFromGrid = topPixels / gridConfig.hourHeight * 60;
- const totalMinutes = gridConfig.dayStartHour * 60 + startMinutesFromGrid;
- const start = new Date(date);
- start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0);
- const durationMinutes = heightPixels / gridConfig.hourHeight * 60;
- const end = new Date(start.getTime() + durationMinutes * 60 * 1e3);
- return new _SwpEvent(element, columnKey, start, end);
- }
-};
-var DragDropManager = class {
- constructor(eventBus, gridConfig) {
- this.eventBus = eventBus;
- this.gridConfig = gridConfig;
- this.dragState = null;
- this.mouseDownPosition = null;
- this.pendingElement = null;
- this.pendingMouseOffset = null;
- this.container = null;
- this.inHeader = false;
- this.DRAG_THRESHOLD = 5;
- this.INTERPOLATION_FACTOR = 0.3;
- this.handlePointerDown = (e) => {
- const target = e.target;
- if (target.closest("swp-resize-handle"))
- return;
- const eventElement = target.closest("swp-event");
- const headerItem = target.closest("swp-header-item");
- const draggable = eventElement || headerItem;
- if (!draggable)
- return;
- this.mouseDownPosition = { x: e.clientX, y: e.clientY };
- this.pendingElement = draggable;
- const rect = draggable.getBoundingClientRect();
- this.pendingMouseOffset = {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top
- };
- draggable.setPointerCapture(e.pointerId);
- };
- this.handlePointerMove = (e) => {
- if (!this.mouseDownPosition || !this.pendingElement) {
- if (this.dragState) {
- this.updateDragTarget(e);
- }
- return;
- }
- const deltaX = Math.abs(e.clientX - this.mouseDownPosition.x);
- const deltaY = Math.abs(e.clientY - this.mouseDownPosition.y);
- const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
- if (distance < this.DRAG_THRESHOLD)
- return;
- this.initializeDrag(this.pendingElement, this.pendingMouseOffset, e);
- this.mouseDownPosition = null;
- this.pendingElement = null;
- this.pendingMouseOffset = null;
- };
- this.handlePointerUp = (_e) => {
- this.mouseDownPosition = null;
- this.pendingElement = null;
- this.pendingMouseOffset = null;
- if (!this.dragState)
- return;
- cancelAnimationFrame(this.dragState.animationId);
- if (this.dragState.dragSource === "header") {
- this.handleHeaderItemDragEnd();
- } else {
- this.handleGridEventDragEnd();
- }
- this.dragState.element.classList.remove("dragging");
- this.dragState = null;
- this.inHeader = false;
- };
- this.animateDrag = () => {
- if (!this.dragState)
- return;
- const diff2 = this.dragState.targetY - this.dragState.currentY;
- if (Math.abs(diff2) <= 0.5) {
- this.dragState.animationId = 0;
- return;
- }
- this.dragState.currentY += diff2 * this.INTERPOLATION_FACTOR;
- this.dragState.element.style.top = `${this.dragState.currentY}px`;
- if (this.dragState.columnElement) {
- const payload = {
- eventId: this.dragState.eventId,
- element: this.dragState.element,
- currentY: this.dragState.currentY,
- columnElement: this.dragState.columnElement
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE, payload);
- }
- this.dragState.animationId = requestAnimationFrame(this.animateDrag);
- };
- this.setupScrollListener();
- }
- setupScrollListener() {
- this.eventBus.on(CoreEvents.EDGE_SCROLL_TICK, (e) => {
- if (!this.dragState)
- return;
- const { scrollDelta } = e.detail;
- this.dragState.targetY += scrollDelta;
- this.dragState.currentY += scrollDelta;
- this.dragState.element.style.top = `${this.dragState.currentY}px`;
- });
- }
- /**
- * Initialize drag-drop on a container element
- */
- init(container) {
- this.container = container;
- container.addEventListener("pointerdown", this.handlePointerDown);
- document.addEventListener("pointermove", this.handlePointerMove);
- document.addEventListener("pointerup", this.handlePointerUp);
- }
- /**
- * Handle drag end for header items
- */
- handleHeaderItemDragEnd() {
- if (!this.dragState)
- return;
- if (!this.inHeader && this.dragState.currentColumn) {
- const gridEvent = this.dragState.currentColumn.querySelector(`swp-event[data-event-id="${this.dragState.eventId}"]`);
- if (gridEvent) {
- const columnKey = this.dragState.currentColumn.dataset.columnKey || "";
- const date = this.dragState.currentColumn.dataset.date || "";
- const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, date, this.gridConfig);
- const payload = {
- swpEvent,
- sourceColumnKey: this.dragState.sourceColumnKey,
- target: "grid"
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
- }
- }
- }
- /**
- * Handle drag end for grid events
- */
- handleGridEventDragEnd() {
- if (!this.dragState || !this.dragState.columnElement)
- return;
- const snappedY = snapToGrid(this.dragState.currentY, this.gridConfig);
- this.dragState.element.style.top = `${snappedY}px`;
- this.dragState.ghostElement?.remove();
- const columnKey = this.dragState.columnElement.dataset.columnKey || "";
- const date = this.dragState.columnElement.dataset.date || "";
- const swpEvent = SwpEvent.fromElement(this.dragState.element, columnKey, date, this.gridConfig);
- const payload = {
- swpEvent,
- sourceColumnKey: this.dragState.sourceColumnKey,
- target: this.inHeader ? "header" : "grid"
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
- }
- initializeDrag(element, mouseOffset, e) {
- const eventId = element.dataset.eventId || "";
- const isHeaderItem = element.tagName.toLowerCase() === "swp-header-item";
- const columnElement = element.closest("swp-day-column");
- if (!isHeaderItem && !columnElement)
- return;
- if (isHeaderItem) {
- this.initializeHeaderItemDrag(element, mouseOffset, eventId);
- } else {
- this.initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId);
- }
- }
- /**
- * Initialize drag for a header item (allDay event)
- */
- initializeHeaderItemDrag(element, mouseOffset, eventId) {
- element.classList.add("dragging");
- this.dragState = {
- eventId,
- element,
- ghostElement: null,
- // No ghost for header items
- startY: 0,
- mouseOffset,
- columnElement: null,
- currentColumn: null,
- targetY: 0,
- currentY: 0,
- animationId: 0,
- sourceColumnKey: "",
- // Will be set from header item data
- dragSource: "header"
- };
- this.inHeader = true;
- }
- /**
- * Initialize drag for a grid event
- */
- initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId) {
- const elementRect = element.getBoundingClientRect();
- const columnRect = columnElement.getBoundingClientRect();
- const startY = elementRect.top - columnRect.top;
- const group = element.closest("swp-event-group");
- if (group) {
- const eventsLayer = columnElement.querySelector("swp-events-layer");
- if (eventsLayer) {
- eventsLayer.appendChild(element);
- }
- }
- element.style.position = "absolute";
- element.style.top = `${startY}px`;
- element.style.left = "2px";
- element.style.right = "2px";
- element.style.marginLeft = "0";
- const ghostElement = element.cloneNode(true);
- ghostElement.classList.add("drag-ghost");
- ghostElement.style.opacity = "0.3";
- ghostElement.style.pointerEvents = "none";
- element.parentNode?.insertBefore(ghostElement, element);
- element.classList.add("dragging");
- const targetY = e.clientY - columnRect.top - mouseOffset.y;
- this.dragState = {
- eventId,
- element,
- ghostElement,
- startY,
- mouseOffset,
- columnElement,
- currentColumn: columnElement,
- targetY: Math.max(0, targetY),
- currentY: startY,
- animationId: 0,
- sourceColumnKey: columnElement.dataset.columnKey || "",
- dragSource: "grid"
- };
- const payload = {
- eventId,
- element,
- ghostElement,
- startY,
- mouseOffset,
- columnElement
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_START, payload);
- this.animateDrag();
- }
- updateDragTarget(e) {
- if (!this.dragState)
- return;
- this.checkHeaderZone(e);
- if (this.inHeader)
- return;
- const columnAtPoint = this.getColumnAtPoint(e.clientX);
- if (this.dragState.dragSource === "header" && columnAtPoint && !this.dragState.currentColumn) {
- this.dragState.currentColumn = columnAtPoint;
- this.dragState.columnElement = columnAtPoint;
- }
- if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn && this.dragState.currentColumn) {
- const payload = {
- eventId: this.dragState.eventId,
- element: this.dragState.element,
- previousColumn: this.dragState.currentColumn,
- newColumn: columnAtPoint,
- currentY: this.dragState.currentY
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, payload);
- this.dragState.currentColumn = columnAtPoint;
- this.dragState.columnElement = columnAtPoint;
- }
- if (!this.dragState.columnElement)
- return;
- const columnRect = this.dragState.columnElement.getBoundingClientRect();
- const targetY = e.clientY - columnRect.top - this.dragState.mouseOffset.y;
- this.dragState.targetY = Math.max(0, targetY);
- if (!this.dragState.animationId) {
- this.animateDrag();
- }
- }
- /**
- * Check if pointer is in header zone and emit appropriate events
- */
- checkHeaderZone(e) {
- if (!this.dragState)
- return;
- const headerViewport = document.querySelector("swp-header-viewport");
- if (!headerViewport)
- return;
- const rect = headerViewport.getBoundingClientRect();
- const isInHeader = e.clientY < rect.bottom;
- if (isInHeader && !this.inHeader) {
- this.inHeader = true;
- if (this.dragState.dragSource === "grid" && this.dragState.columnElement) {
- const payload = {
- eventId: this.dragState.eventId,
- element: this.dragState.element,
- sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
- sourceColumnKey: this.dragState.columnElement.dataset.columnKey || "",
- title: this.dragState.element.querySelector("swp-event-title")?.textContent || "",
- colorClass: [...this.dragState.element.classList].find((c) => c.startsWith("is-")),
- itemType: "event",
- duration: 1
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload);
- }
- } else if (!isInHeader && this.inHeader) {
- this.inHeader = false;
- const targetColumn = this.getColumnAtPoint(e.clientX);
- if (this.dragState.dragSource === "header") {
- const payload = {
- eventId: this.dragState.eventId,
- source: "header",
- element: this.dragState.element,
- targetColumn: targetColumn || void 0,
- start: this.dragState.element.dataset.start ? new Date(this.dragState.element.dataset.start) : void 0,
- end: this.dragState.element.dataset.end ? new Date(this.dragState.element.dataset.end) : void 0,
- title: this.dragState.element.textContent || "",
- colorClass: [...this.dragState.element.classList].find((c) => c.startsWith("is-"))
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
- if (targetColumn) {
- const newElement = targetColumn.querySelector(`swp-event[data-event-id="${this.dragState.eventId}"]`);
- if (newElement) {
- this.dragState.element = newElement;
- this.dragState.columnElement = targetColumn;
- this.dragState.currentColumn = targetColumn;
- this.animateDrag();
- }
- }
- } else {
- const payload = {
- eventId: this.dragState.eventId,
- source: "grid"
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
- }
- } else if (isInHeader) {
- const column = this.getColumnAtX(e.clientX);
- if (column) {
- const payload = {
- eventId: this.dragState.eventId,
- columnIndex: this.getColumnIndex(column),
- columnKey: column.dataset.columnKey || ""
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload);
- }
- }
- }
- /**
- * Get column index (0-based) for a column element
- */
- getColumnIndex(column) {
- if (!this.container || !column)
- return 0;
- const columns = Array.from(this.container.querySelectorAll("swp-day-column"));
- return columns.indexOf(column);
- }
- /**
- * Get column at X coordinate (alias for getColumnAtPoint)
- */
- getColumnAtX(clientX) {
- return this.getColumnAtPoint(clientX);
- }
- /**
- * Find column element at given X coordinate
- */
- getColumnAtPoint(clientX) {
- if (!this.container)
- return null;
- const columns = this.container.querySelectorAll("swp-day-column");
- for (const col of columns) {
- const rect = col.getBoundingClientRect();
- if (clientX >= rect.left && clientX <= rect.right) {
- return col;
- }
- }
- return null;
- }
- /**
- * Cancel drag and animate back to start position
- */
- cancelDrag() {
- if (!this.dragState)
- return;
- cancelAnimationFrame(this.dragState.animationId);
- const { element, ghostElement, startY, eventId } = this.dragState;
- element.style.transition = "top 200ms ease-out";
- element.style.top = `${startY}px`;
- setTimeout(() => {
- ghostElement?.remove();
- element.style.transition = "";
- element.classList.remove("dragging");
- }, 200);
- const payload = {
- eventId,
- element,
- startY
- };
- this.eventBus.emit(CoreEvents.EVENT_DRAG_CANCEL, payload);
- this.dragState = null;
- this.inHeader = false;
- }
-};
-var EdgeScrollManager = class {
- constructor(eventBus) {
- this.eventBus = eventBus;
- this.scrollableContent = null;
- this.timeGrid = null;
- this.draggedElement = null;
- this.scrollRAF = null;
- this.mouseY = 0;
- this.isDragging = false;
- this.isScrolling = false;
- this.lastTs = 0;
- this.rect = null;
- this.initialScrollTop = 0;
- this.OUTER_ZONE = 100;
- this.INNER_ZONE = 50;
- this.SLOW_SPEED = 140;
- this.FAST_SPEED = 640;
- this.trackMouse = (e) => {
- if (this.isDragging) {
- this.mouseY = e.clientY;
- }
- };
- this.scrollTick = (ts) => {
- if (!this.isDragging || !this.scrollableContent)
- return;
- const dt = this.lastTs ? (ts - this.lastTs) / 1e3 : 0;
- this.lastTs = ts;
- this.rect ?? (this.rect = this.scrollableContent.getBoundingClientRect());
- const velocity = this.calculateVelocity();
- if (velocity !== 0 && !this.isAtBoundary(velocity)) {
- const scrollDelta = velocity * dt;
- this.scrollableContent.scrollTop += scrollDelta;
- this.rect = null;
- this.eventBus.emit(CoreEvents.EDGE_SCROLL_TICK, { scrollDelta });
- this.setScrollingState(true);
- } else {
- this.setScrollingState(false);
- }
- this.scrollRAF = requestAnimationFrame(this.scrollTick);
- };
- this.subscribeToEvents();
- document.addEventListener("pointermove", this.trackMouse);
- }
- init(scrollableContent) {
- this.scrollableContent = scrollableContent;
- this.timeGrid = scrollableContent.querySelector("swp-time-grid");
- this.scrollableContent.style.scrollBehavior = "auto";
- }
- subscribeToEvents() {
- this.eventBus.on(CoreEvents.EVENT_DRAG_START, (event) => {
- const payload = event.detail;
- this.draggedElement = payload.element;
- this.startDrag();
- });
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => this.stopDrag());
- this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => this.stopDrag());
- }
- startDrag() {
- this.isDragging = true;
- this.isScrolling = false;
- this.lastTs = 0;
- this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
- if (this.scrollRAF === null) {
- this.scrollRAF = requestAnimationFrame(this.scrollTick);
- }
- }
- stopDrag() {
- this.isDragging = false;
- this.setScrollingState(false);
- if (this.scrollRAF !== null) {
- cancelAnimationFrame(this.scrollRAF);
- this.scrollRAF = null;
- }
- this.rect = null;
- this.lastTs = 0;
- this.initialScrollTop = 0;
- }
- calculateVelocity() {
- if (!this.rect)
- return 0;
- const distTop = this.mouseY - this.rect.top;
- const distBot = this.rect.bottom - this.mouseY;
- if (distTop < this.INNER_ZONE)
- return -this.FAST_SPEED;
- if (distTop < this.OUTER_ZONE)
- return -this.SLOW_SPEED;
- if (distBot < this.INNER_ZONE)
- return this.FAST_SPEED;
- if (distBot < this.OUTER_ZONE)
- return this.SLOW_SPEED;
- return 0;
- }
- isAtBoundary(velocity) {
- if (!this.scrollableContent || !this.timeGrid || !this.draggedElement)
- return false;
- const atTop = this.scrollableContent.scrollTop <= 0 && velocity < 0;
- const atBottom = velocity > 0 && this.draggedElement.getBoundingClientRect().bottom >= this.timeGrid.getBoundingClientRect().bottom;
- return atTop || atBottom;
- }
- setScrollingState(scrolling) {
- if (this.isScrolling === scrolling)
- return;
- this.isScrolling = scrolling;
- if (scrolling) {
- this.eventBus.emit(CoreEvents.EDGE_SCROLL_STARTED, {});
- } else {
- this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
- this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
- }
- }
-};
-var ResizeManager = class {
- constructor(eventBus, gridConfig, dateService) {
- this.eventBus = eventBus;
- this.gridConfig = gridConfig;
- this.dateService = dateService;
- this.container = null;
- this.resizeState = null;
- this.Z_INDEX_RESIZING = "1000";
- this.ANIMATION_SPEED = 0.35;
- this.MIN_HEIGHT_MINUTES = 15;
- this.handleMouseOver = (e) => {
- const target = e.target;
- const eventElement = target.closest("swp-event");
- if (!eventElement || this.resizeState)
- return;
- if (!eventElement.querySelector(":scope > swp-resize-handle")) {
- const handle = this.createResizeHandle();
- eventElement.appendChild(handle);
- }
- };
- this.handlePointerDown = (e) => {
- const handle = e.target.closest("swp-resize-handle");
- if (!handle)
- return;
- const element = handle.parentElement;
- if (!element)
- return;
- const eventId = element.dataset.eventId || "";
- const startHeight = element.offsetHeight;
- const startDurationMinutes = pixelsToMinutes(startHeight, this.gridConfig);
- const container = element.closest("swp-event-group") ?? element;
- const prevZIndex = container.style.zIndex;
- this.resizeState = {
- eventId,
- element,
- handleElement: handle,
- startY: e.clientY,
- startHeight,
- startDurationMinutes,
- pointerId: e.pointerId,
- prevZIndex,
- // Animation state
- currentHeight: startHeight,
- targetHeight: startHeight,
- animationId: null
- };
- container.style.zIndex = this.Z_INDEX_RESIZING;
- try {
- handle.setPointerCapture(e.pointerId);
- } catch (err) {
- console.warn("Pointer capture failed:", err);
- }
- document.documentElement.classList.add("swp--resizing");
- this.eventBus.emit(CoreEvents.EVENT_RESIZE_START, {
- eventId,
- element,
- startHeight
- });
- e.preventDefault();
- };
- this.handlePointerMove = (e) => {
- if (!this.resizeState)
- return;
- const deltaY = e.clientY - this.resizeState.startY;
- const minHeight = this.MIN_HEIGHT_MINUTES / 60 * this.gridConfig.hourHeight;
- const newHeight = Math.max(minHeight, this.resizeState.startHeight + deltaY);
- this.resizeState.targetHeight = newHeight;
- if (this.resizeState.animationId === null) {
- this.animateHeight();
- }
- };
- this.animateHeight = () => {
- if (!this.resizeState)
- return;
- const diff2 = this.resizeState.targetHeight - this.resizeState.currentHeight;
- if (Math.abs(diff2) < 0.5) {
- this.resizeState.animationId = null;
- return;
- }
- this.resizeState.currentHeight += diff2 * this.ANIMATION_SPEED;
- this.resizeState.element.style.height = `${this.resizeState.currentHeight}px`;
- this.updateTimestampDisplay();
- this.resizeState.animationId = requestAnimationFrame(this.animateHeight);
- };
- this.handlePointerUp = (e) => {
- if (!this.resizeState)
- return;
- if (this.resizeState.animationId !== null) {
- cancelAnimationFrame(this.resizeState.animationId);
- }
- try {
- this.resizeState.handleElement.releasePointerCapture(e.pointerId);
- } catch (err) {
- console.warn("Pointer release failed:", err);
- }
- this.snapToGridFinal();
- this.updateTimestampDisplay();
- const container = this.resizeState.element.closest("swp-event-group") ?? this.resizeState.element;
- container.style.zIndex = this.resizeState.prevZIndex;
- document.documentElement.classList.remove("swp--resizing");
- const column = this.resizeState.element.closest("swp-day-column");
- const columnKey = column?.dataset.columnKey || "";
- const date = column?.dataset.date || "";
- const swpEvent = SwpEvent.fromElement(this.resizeState.element, columnKey, date, this.gridConfig);
- this.eventBus.emit(CoreEvents.EVENT_RESIZE_END, {
- swpEvent
- });
- this.resizeState = null;
- };
- }
- /**
- * Initialize resize functionality on container
- */
- init(container) {
- this.container = container;
- container.addEventListener("mouseover", this.handleMouseOver, true);
- document.addEventListener("pointerdown", this.handlePointerDown, true);
- document.addEventListener("pointermove", this.handlePointerMove, true);
- document.addEventListener("pointerup", this.handlePointerUp, true);
- }
- /**
- * Create resize handle element
- */
- createResizeHandle() {
- const handle = document.createElement("swp-resize-handle");
- handle.setAttribute("aria-label", "Resize event");
- handle.setAttribute("role", "separator");
- return handle;
- }
- /**
- * Update timestamp display with snapped end time
- */
- updateTimestampDisplay() {
- if (!this.resizeState)
- return;
- const timeEl = this.resizeState.element.querySelector("swp-event-time");
- if (!timeEl)
- return;
- const top = parseFloat(this.resizeState.element.style.top) || 0;
- const startMinutesFromGrid = pixelsToMinutes(top, this.gridConfig);
- const startMinutes = this.gridConfig.dayStartHour * 60 + startMinutesFromGrid;
- const snappedHeight = snapToGrid(this.resizeState.currentHeight, this.gridConfig);
- const durationMinutes = pixelsToMinutes(snappedHeight, this.gridConfig);
- const endMinutes = startMinutes + durationMinutes;
- const start = this.minutesToDate(startMinutes);
- const end = this.minutesToDate(endMinutes);
- timeEl.textContent = this.dateService.formatTimeRange(start, end);
- }
- /**
- * Convert minutes since midnight to Date
- */
- minutesToDate(minutes) {
- const date = /* @__PURE__ */ new Date();
- date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0);
- return date;
- }
- /**
- * Snap final height to grid interval
- */
- snapToGridFinal() {
- if (!this.resizeState)
- return;
- const currentHeight = this.resizeState.element.offsetHeight;
- const snappedHeight = snapToGrid(currentHeight, this.gridConfig);
- const minHeight = minutesToPixels(this.MIN_HEIGHT_MINUTES, this.gridConfig);
- const finalHeight = Math.max(minHeight, snappedHeight);
- this.resizeState.element.style.height = `${finalHeight}px`;
- this.resizeState.currentHeight = finalHeight;
- }
-};
-var EventPersistenceManager = class {
- constructor(eventService, eventBus, dateService) {
- this.eventService = eventService;
- this.eventBus = eventBus;
- this.dateService = dateService;
- this.handleDragEnd = async (e) => {
- const payload = e.detail;
- const { swpEvent } = payload;
- const event = await this.eventService.get(swpEvent.eventId);
- if (!event) {
- console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`);
- return;
- }
- const { resource } = this.dateService.parseColumnKey(swpEvent.columnKey);
- const updatedEvent = {
- ...event,
- start: swpEvent.start,
- end: swpEvent.end,
- resourceId: resource ?? event.resourceId,
- allDay: payload.target === "header",
- syncStatus: "pending"
- };
- await this.eventService.save(updatedEvent);
- const updatePayload = {
- eventId: updatedEvent.id,
- sourceColumnKey: payload.sourceColumnKey,
- targetColumnKey: swpEvent.columnKey
- };
- this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
- };
- this.handleResizeEnd = async (e) => {
- const payload = e.detail;
- const { swpEvent } = payload;
- const event = await this.eventService.get(swpEvent.eventId);
- if (!event) {
- console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`);
- return;
- }
- const updatedEvent = {
- ...event,
- end: swpEvent.end,
- syncStatus: "pending"
- };
- await this.eventService.save(updatedEvent);
- const updatePayload = {
- eventId: updatedEvent.id,
- sourceColumnKey: swpEvent.columnKey,
- targetColumnKey: swpEvent.columnKey
- };
- this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
- };
- this.setupListeners();
- }
- setupListeners() {
- this.eventBus.on(CoreEvents.EVENT_DRAG_END, this.handleDragEnd);
- this.eventBus.on(CoreEvents.EVENT_RESIZE_END, this.handleResizeEnd);
- }
-};
-var defaultTimeFormatConfig = {
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- use24HourFormat: true,
- locale: "da-DK",
- dateFormat: "locale",
- showSeconds: false
-};
-var defaultGridConfig = {
- hourHeight: 64,
- dayStartHour: 6,
- dayEndHour: 18,
- snapInterval: 15,
- gridStartThresholdMinutes: 30
-};
-function registerCoreServices(builder, options) {
- const timeConfig = options?.timeConfig ?? defaultTimeFormatConfig;
- const gridConfig = options?.gridConfig ?? defaultGridConfig;
- const dbConfig = options?.dbConfig ?? defaultDBConfig;
- builder.registerInstance(timeConfig).as("ITimeFormatConfig");
- builder.registerInstance(gridConfig).as("IGridConfig");
- builder.registerInstance(dbConfig).as("IDBConfig");
- builder.registerType(EventBus).as("EventBus");
- builder.registerType(EventBus).as("IEventBus");
- builder.registerType(DateService).as("DateService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("ITimeFormatConfig"),
- void 0
- ]
- });
- builder.registerType(IndexedDBContext).as("IndexedDBContext").autoWire({
- mapResolvers: [
- (c) => c.resolveTypeAll("IStore"),
- (c) => c.resolveType("IDBConfig")
- ]
- });
- builder.registerType(EventStore).as("IStore");
- builder.registerType(ResourceStore).as("IStore");
- builder.registerType(SettingsStore).as("IStore");
- builder.registerType(ViewConfigStore).as("IStore");
- builder.registerType(EventService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(EventService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(EventService).as("EventService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ResourceService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ResourceService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ResourceService).as("ResourceService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(SettingsService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(SettingsService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(SettingsService).as("SettingsService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ViewConfigService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ViewConfigService).as("IEntityService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ViewConfigService).as("ViewConfigService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(EventRenderer).as("EventRenderer").autoWire({
- mapResolvers: [
- (c) => c.resolveType("EventService"),
- (c) => c.resolveType("DateService"),
- (c) => c.resolveType("IGridConfig"),
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ScheduleRenderer).as("ScheduleRenderer").autoWire({
- mapResolvers: [
- (c) => c.resolveType("ResourceScheduleService"),
- (c) => c.resolveType("DateService"),
- (c) => c.resolveType("IGridConfig")
- ]
- });
- builder.registerType(HeaderDrawerRenderer).as("HeaderDrawerRenderer").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IEventBus"),
- (c) => c.resolveType("IGridConfig"),
- (c) => c.resolveType("HeaderDrawerManager"),
- (c) => c.resolveType("EventService"),
- (c) => c.resolveType("DateService")
- ]
- });
- builder.registerType(TimeAxisRenderer).as("TimeAxisRenderer");
- builder.registerType(DateRenderer).as("IRenderer").autoWire({
- mapResolvers: [
- (c) => c.resolveType("DateService")
- ]
- });
- builder.registerType(ResourceRenderer).as("IRenderer").autoWire({
- mapResolvers: [
- (c) => c.resolveType("ResourceService")
- ]
- });
- builder.registerType(ScrollManager).as("ScrollManager");
- builder.registerType(HeaderDrawerManager).as("HeaderDrawerManager");
- builder.registerType(DragDropManager).as("DragDropManager").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IEventBus"),
- (c) => c.resolveType("IGridConfig")
- ]
- });
- builder.registerType(EdgeScrollManager).as("EdgeScrollManager").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IEventBus")
- ]
- });
- builder.registerType(ResizeManager).as("ResizeManager").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IEventBus"),
- (c) => c.resolveType("IGridConfig"),
- (c) => c.resolveType("DateService")
- ]
- });
- builder.registerType(EventPersistenceManager).as("EventPersistenceManager").autoWire({
- mapResolvers: [
- (c) => c.resolveType("EventService"),
- (c) => c.resolveType("IEventBus"),
- (c) => c.resolveType("DateService")
- ]
- });
- builder.registerType(CalendarOrchestrator).as("CalendarOrchestrator").autoWire({
- mapResolvers: [
- (c) => c.resolveTypeAll("IRenderer"),
- (c) => c.resolveType("EventRenderer"),
- (c) => c.resolveType("ScheduleRenderer"),
- (c) => c.resolveType("HeaderDrawerRenderer"),
- (c) => c.resolveType("DateService"),
- (c) => c.resolveTypeAll("IEntityService")
- ]
- });
- builder.registerType(CalendarApp).as("CalendarApp").autoWire({
- mapResolvers: [
- (c) => c.resolveType("CalendarOrchestrator"),
- (c) => c.resolveType("TimeAxisRenderer"),
- (c) => c.resolveType("DateService"),
- (c) => c.resolveType("ScrollManager"),
- (c) => c.resolveType("HeaderDrawerManager"),
- (c) => c.resolveType("DragDropManager"),
- (c) => c.resolveType("EdgeScrollManager"),
- (c) => c.resolveType("ResizeManager"),
- (c) => c.resolveType("HeaderDrawerRenderer"),
- (c) => c.resolveType("EventPersistenceManager"),
- (c) => c.resolveType("SettingsService"),
- (c) => c.resolveType("ViewConfigService"),
- (c) => c.resolveType("IEventBus")
- ]
- });
-}
-
-// node_modules/calendar/dist/extensions/schedules/index.js
-var ScheduleOverrideStore = class _ScheduleOverrideStore {
- constructor() {
- this.storeName = _ScheduleOverrideStore.STORE_NAME;
- }
- create(db) {
- const store = db.createObjectStore(_ScheduleOverrideStore.STORE_NAME, { keyPath: "id" });
- store.createIndex("resourceId", "resourceId", { unique: false });
- store.createIndex("date", "date", { unique: false });
- store.createIndex("resourceId_date", ["resourceId", "date"], { unique: true });
- store.createIndex("syncStatus", "syncStatus", { unique: false });
- }
-};
-ScheduleOverrideStore.STORE_NAME = "scheduleOverrides";
-var ScheduleOverrideService = class {
- constructor(context) {
- this.context = context;
- }
- get db() {
- return this.context.getDatabase();
- }
- /**
- * Get override for a specific resource and date
- */
- async getOverride(resourceId, date) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readonly");
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const index = store.index("resourceId_date");
- const request = index.get([resourceId, date]);
- request.onsuccess = () => {
- resolve(request.result || null);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get override for ${resourceId} on ${date}: ${request.error}`));
- };
- });
- }
- /**
- * Get all overrides for a resource
- */
- async getByResource(resourceId) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readonly");
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const index = store.index("resourceId");
- const request = index.getAll(resourceId);
- request.onsuccess = () => {
- resolve(request.result || []);
- };
- request.onerror = () => {
- reject(new Error(`Failed to get overrides for ${resourceId}: ${request.error}`));
- };
- });
- }
- /**
- * Get overrides for a date range
- */
- async getByDateRange(resourceId, startDate, endDate) {
- const all = await this.getByResource(resourceId);
- return all.filter((o) => o.date >= startDate && o.date <= endDate);
- }
- /**
- * Save an override
- */
- async save(override) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readwrite");
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const request = store.put(override);
- request.onsuccess = () => resolve();
- request.onerror = () => {
- reject(new Error(`Failed to save override ${override.id}: ${request.error}`));
- };
- });
- }
- /**
- * Delete an override
- */
- async delete(id) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readwrite");
- const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME);
- const request = store.delete(id);
- request.onsuccess = () => resolve();
- request.onerror = () => {
- reject(new Error(`Failed to delete override ${id}: ${request.error}`));
- };
- });
- }
-};
-var ResourceScheduleService = class {
- constructor(resourceService, overrideService, dateService) {
- this.resourceService = resourceService;
- this.overrideService = overrideService;
- this.dateService = dateService;
- }
- /**
- * Get effective schedule for a resource on a specific date
- *
- * @param resourceId - Resource ID
- * @param date - Date string "YYYY-MM-DD"
- * @returns ITimeSlot or null (fri/closed)
- */
- async getScheduleForDate(resourceId, date) {
- const override = await this.overrideService.getOverride(resourceId, date);
- if (override) {
- return override.schedule;
- }
- const resource = await this.resourceService.get(resourceId);
- if (!resource || !resource.defaultSchedule) {
- return null;
- }
- const weekDay = this.dateService.getISOWeekDay(date);
- return resource.defaultSchedule[weekDay] || null;
- }
- /**
- * Get schedules for multiple dates
- *
- * @param resourceId - Resource ID
- * @param dates - Array of date strings "YYYY-MM-DD"
- * @returns Map of date -> ITimeSlot | null
- */
- async getSchedulesForDates(resourceId, dates) {
- const result = /* @__PURE__ */ new Map();
- const resource = await this.resourceService.get(resourceId);
- const overrides = dates.length > 0 ? await this.overrideService.getByDateRange(resourceId, dates[0], dates[dates.length - 1]) : [];
- const overrideMap = new Map(overrides.map((o) => [o.date, o.schedule]));
- for (const date of dates) {
- if (overrideMap.has(date)) {
- result.set(date, overrideMap.get(date));
- continue;
- }
- if (resource?.defaultSchedule) {
- const weekDay = this.dateService.getISOWeekDay(date);
- result.set(date, resource.defaultSchedule[weekDay] || null);
- } else {
- result.set(date, null);
- }
- }
- return result;
- }
-};
-function registerSchedules(builder) {
- builder.registerType(ScheduleOverrideStore).as("IStore");
- builder.registerType(ScheduleOverrideService).as("ScheduleOverrideService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("IndexedDBContext")
- ]
- });
- builder.registerType(ResourceScheduleService).as("ResourceScheduleService").autoWire({
- mapResolvers: [
- (c) => c.resolveType("ResourceService"),
- (c) => c.resolveType("ScheduleOverrideService"),
- (c) => c.resolveType("DateService")
- ]
- });
-}
-
-// src/index.ts
-async function init() {
- const databases = await indexedDB.databases();
- const dbExists = databases.some((db) => db.name === "CalendarTestDB");
- const container = new Container();
- const builder = container.builder();
- registerCoreServices(builder, {
- dbConfig: { dbName: "CalendarTestDB", dbVersion: 4 }
- });
- registerSchedules(builder);
- const app = builder.build();
- console.log("Container created");
- const dbContext = app.resolveType("IndexedDBContext");
- await dbContext.initialize();
- console.log("IndexedDB initialized");
- const settingsService = app.resolveType("SettingsService");
- const viewConfigService = app.resolveType("ViewConfigService");
- const eventService = app.resolveType("EventService");
- if (dbExists) {
- console.log("Database exists, skipping seed");
- } else {
- console.log("Seeding data...");
- await seedData(settingsService, viewConfigService, eventService);
- }
- const calendarApp = app.resolveType("CalendarApp");
- const containerEl = document.querySelector("swp-calendar-container");
- await calendarApp.init(containerEl);
- const eventBus = app.resolveType("EventBus");
- eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: "simple" });
- console.log("Calendar rendered");
- document.addEventListener("event:drag-end", (e) => {
- console.log("event:drag-end:", e.detail);
- });
- document.addEventListener("event:updated", (e) => {
- console.log("event:updated:", e.detail);
- });
- const persistenceManager = app.resolveType("EventPersistenceManager");
- console.log("EventPersistenceManager resolved:", persistenceManager);
-}
-async function seedData(settingsService, viewConfigService, eventService) {
- await settingsService.save({
- id: "grid",
- dayStartHour: 8,
- dayEndHour: 17,
- workStartHour: 9,
- workEndHour: 16,
- hourHeight: 64,
- snapInterval: 15,
- syncStatus: "synced"
- });
- await settingsService.save({
- id: "workweek",
- presets: {
- standard: { id: "standard", label: "Standard", workDays: [1, 2, 3, 4, 5], periodDays: 7 }
- },
- defaultPreset: "standard",
- firstDayOfWeek: 1,
- syncStatus: "synced"
- });
- await viewConfigService.save({
- id: "simple",
- groupings: [{ type: "date", values: [], idProperty: "date", derivedFrom: "start" }],
- syncStatus: "synced"
- });
- const today = /* @__PURE__ */ new Date();
- today.setHours(0, 0, 0, 0);
- console.log("Event date:", today.toISOString());
- const start1 = new Date(today);
- start1.setHours(9, 0, 0, 0);
- const end1 = new Date(today);
- end1.setHours(10, 0, 0, 0);
- await eventService.save({
- id: "1",
- title: "Morgenm\xF8de",
- start: start1,
- end: end1,
- type: "meeting",
- allDay: false,
- syncStatus: "synced"
- });
- const start2 = new Date(today);
- start2.setHours(12, 0, 0, 0);
- const end2 = new Date(today);
- end2.setHours(13, 0, 0, 0);
- await eventService.save({
- id: "2",
- title: "Frokost",
- start: start2,
- end: end2,
- type: "break",
- allDay: false,
- syncStatus: "synced"
- });
-}
-init().catch(console.error);
diff --git a/test-package/dist/css/calendar.css b/test-package/dist/css/calendar.css
deleted file mode 100644
index 5a06eed..0000000
--- a/test-package/dist/css/calendar.css
+++ /dev/null
@@ -1,877 +0,0 @@
-/* V2 Base - Shared variables */
-
-:root {
- /* Grid measurements */
- --hour-height: 64px;
- --time-axis-width: 60px;
- --grid-columns: 7;
- --day-column-min-width: 200px;
- --day-start-hour: 6;
- --day-end-hour: 18;
- --header-height: 70px;
-
- /* Colors - UI */
- --color-border: #e0e0e0;
- --color-surface: #fff;
- --color-background: #f5f5f5;
- --color-background-hover: #f0f0f0;
- --color-background-alt: #fafafa;
- --color-text: #333333;
- --color-text-secondary: #666;
- --color-primary: #1976d2;
- --color-team-bg: #e3f2fd;
- --color-team-text: #1565c0;
-
- /* Colors - Grid */
- --color-hour-line: rgba(0, 0, 0, 0.2);
- --color-grid-line-light: rgba(0, 0, 0, 0.05);
- --color-unavailable: rgba(0, 0, 0, 0.02);
-
- /* Named color palette for events (fra V1) */
- --b-color-red: #e53935;
- --b-color-pink: #d81b60;
- --b-color-magenta: #c200c2;
- --b-color-purple: #8e24aa;
- --b-color-violet: #5e35b1;
- --b-color-deep-purple: #4527a0;
- --b-color-indigo: #3949ab;
- --b-color-blue: #1e88e5;
- --b-color-light-blue: #03a9f4;
- --b-color-cyan: #3bc9db;
- --b-color-teal: #00897b;
- --b-color-green: #43a047;
- --b-color-light-green: #8bc34a;
- --b-color-lime: #c0ca33;
- --b-color-yellow: #fdd835;
- --b-color-amber: #ffb300;
- --b-color-orange: #fb8c00;
- --b-color-deep-orange: #f4511e;
-
- /* Base mix for color-mix() function */
- --b-mix: #fff;
-
- /* Shadows */
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
- --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
-
- /* Transitions */
- --transition-fast: 150ms ease;
-}
-
-* { box-sizing: border-box; margin: 0; padding: 0; }
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: var(--color-background);
-}
-
-/* V2 Layout - Calendar structure, grid, navigation */
-
-.calendar-wrapper {
- height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-swp-calendar {
- display: grid;
- grid-template-rows: auto 1fr;
- height: 100%;
- background: var(--color-surface);
-}
-
-/* Nav */
-swp-calendar-nav {
- display: flex;
- gap: 12px;
- padding: 8px 16px;
- border-bottom: 1px solid var(--color-border);
- align-items: center;
- font-size: 13px;
-}
-
-/* View switcher - small chips */
-swp-view-switcher {
- display: flex;
- gap: 4px;
- background: var(--color-background-alt);
- padding: 3px;
- border-radius: 6px;
-}
-
-.view-chip {
- padding: 4px 10px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- background: transparent;
- color: var(--color-text-secondary);
- font-size: 12px;
- font-weight: 500;
- transition: all 0.15s ease;
-
- &:hover {
- background: var(--color-surface);
- color: var(--color-text);
- }
-
- &.active {
- background: var(--color-surface);
- color: var(--color-text);
- box-shadow: 0 1px 2px rgba(0,0,0,0.1);
- }
-}
-
-/* Workweek dropdown */
-.workweek-dropdown {
- padding: 4px 8px;
- border: 1px solid var(--color-border);
- border-radius: 4px;
- background: var(--color-surface);
- font-size: 12px;
- cursor: pointer;
-
- &:hover { border-color: var(--color-text-secondary); }
- &:focus { outline: 2px solid var(--color-primary); outline-offset: 1px; }
-}
-
-/* Resource selector (picker view) */
-swp-resource-selector {
- &.hidden { display: none; }
-
- fieldset {
- border: 1px solid var(--color-border);
- border-radius: 6px;
- padding: 6px 12px;
- margin: 0;
- }
-
- legend {
- font-size: 11px;
- font-weight: 500;
- color: var(--color-text-secondary);
- padding: 0 6px;
- }
-
- .resource-checkboxes {
- display: flex;
- flex-wrap: wrap;
- gap: 4px 16px;
- }
-
- label {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 12px;
- cursor: pointer;
- white-space: nowrap;
-
- &:hover { color: var(--color-primary); }
- }
-
- input[type="checkbox"] {
- width: 14px;
- height: 14px;
- cursor: pointer;
- }
-}
-
-/* Navigation group */
-swp-nav-group {
- display: flex;
- gap: 2px;
-}
-
-swp-nav-button {
- padding: 6px 12px;
- border: 1px solid var(--color-border);
- border-radius: 4px;
- cursor: pointer;
- background: var(--color-surface);
- font-size: 12px;
-
- &:hover { background: var(--color-background-hover); }
-
- &.btn-small {
- padding: 4px 8px;
- font-size: 11px;
- }
-}
-
-swp-week-info {
- margin-left: auto;
- text-align: right;
-
- swp-week-number {
- font-weight: 600;
- font-size: 12px;
- display: block;
- }
-
- swp-date-range {
- font-size: 11px;
- color: var(--color-text-secondary);
- }
-}
-
-/* Container */
-swp-calendar-container {
- display: grid;
- grid-template-columns: var(--time-axis-width) 1fr;
- grid-template-rows: auto 1fr;
- overflow: hidden;
- height: 100%;
-}
-
-/* Time axis */
-swp-time-axis {
- grid-column: 1;
- grid-row: 1 / 3;
- display: grid;
- grid-template-rows: auto 1fr;
- border-right: 1px solid var(--color-border);
- background: var(--color-surface);
- overflow: hidden;
- user-select: none;
-}
-
-swp-header-spacer {
- border-bottom: 1px solid var(--color-border);
- background: var(--color-surface);
- z-index: 1;
-}
-
-swp-header-drawer {
- display: grid;
- grid-template-columns: subgrid;
- grid-column: 1 / -1;
- grid-row: 2;
- overflow: hidden;
- background: var(--color-background-alt);
- border-bottom: 1px solid var(--color-border);
-}
-
-swp-time-axis-content {
- display: flex;
- flex-direction: column;
- position: relative;
-}
-
-swp-hour-marker {
- height: var(--hour-height);
- padding: 4px 8px;
- font-size: 11px;
- color: var(--color-text-secondary);
- text-align: right;
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- top: -1px;
- right: 0;
- width: 5px;
- height: 1px;
- background: var(--color-hour-line);
- }
-
- &:first-child::after {
- display: none;
- }
-}
-
-/* Grid container */
-swp-grid-container {
- grid-column: 2;
- grid-row: 1 / 3;
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- grid-template-rows: subgrid;
- overflow: hidden;
-}
-
-/* Viewport/Track for slide animation */
-swp-header-viewport {
- display: grid;
- grid-template-columns: repeat(var(--grid-columns), minmax(var(--day-column-min-width), 1fr));
- grid-template-rows: auto auto;
- min-width: calc(var(--grid-columns) * var(--day-column-min-width));
- overflow-y: scroll;
- overflow-x: hidden;
-
- &::-webkit-scrollbar { background: transparent; }
- &::-webkit-scrollbar-thumb { background: transparent; }
-}
-
-swp-content-viewport {
- overflow: hidden;
- min-height: 0;
- width: 100%;
-}
-
-swp-header-track {
- display: grid;
- grid-template-columns: subgrid;
- grid-column: 1 / -1;
- grid-row: 1;
-}
-
-swp-content-track {
- display: flex;
- height: 100%;
-
- > swp-scrollable-content {
- flex: 0 0 100%;
- height: 100%;
- }
-}
-
-/* Header */
-swp-calendar-header {
- display: grid;
- grid-template-columns: subgrid;
- grid-column: 1 / -1;
- grid-auto-rows: auto;
- background: var(--color-surface);
-
- &[data-levels="date"] > swp-day-header { grid-row: 1; }
-
- &[data-levels="resource date"] {
- > swp-resource-header { grid-row: 1; }
- > swp-day-header { grid-row: 2; }
- }
-
- &[data-levels="team resource date"] {
- > swp-team-header { grid-row: 1; }
- > swp-resource-header { grid-row: 2; }
- > swp-day-header { grid-row: 3; }
- }
-
- &[data-levels="department resource date"] {
- > swp-department-header { grid-row: 1; }
- > swp-resource-header { grid-row: 2; }
- > swp-day-header { grid-row: 3; }
- }
-}
-
-swp-day-header,
-swp-resource-header,
-swp-team-header,
-swp-department-header {
- padding: 8px;
- text-align: center;
- border-right: 1px solid var(--color-border);
- border-bottom: 1px solid var(--color-border);
- user-select: none;
-}
-
-swp-team-header {
- background: var(--color-team-bg);
- color: var(--color-team-text);
- font-weight: 500;
- grid-column: span var(--team-cols, 1);
-}
-
-swp-department-header {
- background: var(--color-team-bg);
- color: var(--color-team-text);
- font-weight: 500;
- grid-column: span var(--department-cols, 1);
-}
-
-swp-resource-header {
- background: var(--color-background-alt);
- font-size: 13px;
-}
-
-swp-day-header {
- swp-day-name {
- display: block;
- font-size: 11px;
- color: var(--color-text-secondary);
- text-transform: uppercase;
- }
-
- swp-day-date {
- display: block;
- font-size: 24px;
- font-weight: 300;
- }
-
- &[data-hidden="true"] {
- display: none;
- }
-}
-
-/* Scrollable content */
-swp-scrollable-content {
- display: block;
- overflow: auto;
-}
-
-swp-time-grid {
- display: block;
- position: relative;
- min-height: calc((var(--day-end-hour) - var(--day-start-hour)) * var(--hour-height));
- min-width: calc(var(--grid-columns) * var(--day-column-min-width));
-
- /* Timelinjer */
- &::after {
- content: '';
- position: absolute;
- inset: 0;
- z-index: 2;
- background-image: repeating-linear-gradient(
- to bottom,
- transparent,
- transparent calc(var(--hour-height) - 1px),
- var(--color-hour-line) calc(var(--hour-height) - 1px),
- var(--color-hour-line) var(--hour-height)
- );
- pointer-events: none;
- }
-}
-
-/* Kvarterlinjer - 3 linjer per time (15, 30, 45 min) */
-swp-grid-lines {
- display: block;
- position: absolute;
- inset: 0;
- z-index: 1;
- background-image: repeating-linear-gradient(
- to bottom,
- transparent 0,
- transparent calc(var(--hour-height) / 4 - 1px),
- var(--color-grid-line-light) calc(var(--hour-height) / 4 - 1px),
- var(--color-grid-line-light) calc(var(--hour-height) / 4),
- transparent calc(var(--hour-height) / 4),
- transparent calc(var(--hour-height) / 2 - 1px),
- var(--color-grid-line-light) calc(var(--hour-height) / 2 - 1px),
- var(--color-grid-line-light) calc(var(--hour-height) / 2),
- transparent calc(var(--hour-height) / 2),
- transparent calc(var(--hour-height) * 3 / 4 - 1px),
- var(--color-grid-line-light) calc(var(--hour-height) * 3 / 4 - 1px),
- var(--color-grid-line-light) calc(var(--hour-height) * 3 / 4),
- transparent calc(var(--hour-height) * 3 / 4),
- transparent var(--hour-height)
- );
-}
-
-swp-day-columns {
- position: absolute;
- inset: 0;
- display: grid;
- grid-template-columns: repeat(var(--grid-columns), minmax(var(--day-column-min-width), 1fr));
- min-width: calc(var(--grid-columns) * var(--day-column-min-width));
-}
-
-swp-day-column {
- position: relative;
- border-right: 1px solid var(--color-border);
-}
-
-swp-events-layer {
- position: absolute;
- inset: 0;
- z-index: 10;
-}
-
-/* Unavailable time zones (outside working hours) */
-swp-unavailable-layer {
- position: absolute;
- inset: 0;
- z-index: 5;
- pointer-events: none;
-}
-
-swp-unavailable-zone {
- position: absolute;
- left: 0;
- right: 0;
- background: var(--color-unavailable, rgba(0, 0, 0, 0.05));
- pointer-events: none;
-}
-
-/* V2 Events - Event styling (from V1 calendar-events-css.css) */
-
-/* Event base styles */
-swp-day-columns swp-event {
- --b-text: var(--color-text);
-
- position: absolute;
- border-radius: 3px;
- overflow: hidden;
- cursor: pointer;
- transition: background-color 200ms ease, box-shadow 150ms ease, transform 150ms ease;
- z-index: 10;
- left: 2px;
- right: 2px;
- font-size: 12px;
- padding: 4px 6px;
- user-select: none;
-
- /* Color system using color-mix() */
- background-color: color-mix(in srgb, var(--b-primary) 10%, var(--b-mix));
- color: var(--b-text);
- border-left: 4px solid var(--b-primary);
-
- /* Enable container queries for responsive layout */
- container-type: size;
- container-name: event;
-
- /* CSS Grid layout for time, title, and description */
- display: grid;
- grid-template-columns: auto 1fr;
- grid-template-rows: auto 1fr;
- gap: 2px 4px;
- align-items: start;
-
- /* Dragging state */
- &.dragging {
- position: absolute;
- z-index: 999999;
- left: 2px;
- right: 2px;
- width: auto;
- cursor: grabbing;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
-
- /* Ghost clone (stays in original position during drag) */
- &.drag-ghost {
- opacity: 0.3;
- pointer-events: none;
- }
-
- /* Hover state */
- &:hover {
- background-color: color-mix(in srgb, var(--b-primary) 15%, var(--b-mix));
- }
-}
-
-swp-day-columns swp-event:hover {
- z-index: 20;
-}
-
-/* Resize handle - actual draggable element */
-swp-resize-handle {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 15px;
- cursor: ns-resize;
- z-index: 25;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- transition: opacity 150ms ease;
-}
-
-/* Show handle on hover */
-swp-day-columns swp-event:hover swp-resize-handle {
- opacity: 1;
-}
-
-/* Handle visual indicator (grip lines) */
-swp-resize-handle::before {
- content: '';
- width: 30px;
- height: 4px;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 2px;
- box-shadow:
- 0 -2px 0 rgba(255, 255, 255, 0.9),
- 0 2px 0 rgba(255, 255, 255, 0.9),
- 0 0 4px rgba(0, 0, 0, 0.2);
-}
-
-/* Global resizing state */
-.swp--resizing {
- user-select: none !important;
- cursor: ns-resize !important;
-}
-
-.swp--resizing * {
- cursor: ns-resize !important;
-}
-
-swp-day-columns swp-event-time {
- grid-column: 1;
- grid-row: 1;
- font-size: 0.875rem;
- font-weight: 500;
- white-space: nowrap;
-}
-
-swp-day-columns swp-event-title {
- grid-column: 2;
- grid-row: 1;
- font-size: 0.875rem;
- font-weight: 500;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-swp-day-columns swp-event-description {
- grid-column: 1 / -1;
- grid-row: 2;
- display: block;
- font-size: 0.875rem;
- opacity: 0.8;
- line-height: 1.3;
- overflow: hidden;
- word-wrap: break-word;
-
- /* Ensure description fills available height for gradient effect */
- min-height: 100%;
- align-self: stretch;
-
- /* Fade-out effect for long descriptions */
- -webkit-mask-image: linear-gradient(to bottom, black 70%, transparent 100%);
- mask-image: linear-gradient(to bottom, black 70%, transparent 100%);
-}
-
-/* Container queries for height-based layout */
-
-/* Hide description when event is too short (< 30px) */
-@container event (height < 30px) {
- swp-day-columns swp-event-description {
- display: none;
- }
-}
-
-/* Full description for tall events (>= 100px) */
-@container event (height >= 100px) {
- swp-day-columns swp-event-description {
- max-height: none;
- }
-}
-
-/* Multi-day events */
-swp-multi-day-event {
- position: relative;
- height: 28px;
- margin: 2px 4px;
- padding: 0 8px;
- border-radius: 4px;
- display: flex;
- align-items: center;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: all var(--transition-fast);
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- user-select: none;
-
- /* Color system using color-mix() */
- --b-text: var(--color-text);
- background-color: color-mix(in srgb, var(--b-primary) 10%, var(--b-mix));
- color: var(--b-text);
- border-left: 4px solid var(--b-primary);
-
- &:hover {
- background-color: color-mix(in srgb, var(--b-primary) 15%, var(--b-mix));
- }
-
- /* Continuation indicators */
- &[data-continues-before="true"] {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- margin-left: 0;
- padding-left: 20px;
-
- &::before {
- content: '\25C0';
- position: absolute;
- left: 4px;
- opacity: 0.6;
- font-size: 0.75rem;
- }
- }
-
- &[data-continues-after="true"] {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- margin-right: 0;
- padding-right: 20px;
-
- &::after {
- content: '\25B6';
- position: absolute;
- right: 4px;
- opacity: 0.6;
- font-size: 0.75rem;
- }
- }
-
- &:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
- }
-}
-
-/* All-day events */
-swp-allday-event {
- --b-text: var(--color-text);
- background-color: color-mix(in srgb, var(--b-primary) 10%, var(--b-mix));
- color: var(--b-text);
- border-left: 4px solid var(--b-primary);
- cursor: pointer;
- transition: background-color 200ms ease;
- user-select: none;
-
- &:hover {
- background-color: color-mix(in srgb, var(--b-primary) 15%, var(--b-mix));
- }
-}
-
-/* Event creation preview */
-swp-event-preview {
- position: absolute;
- left: 8px;
- right: 8px;
- background: rgba(33, 150, 243, 0.1);
- border: 2px dashed var(--color-primary);
- border-radius: 4px;
-
- /* Position via CSS variables */
- top: calc(var(--preview-start) * var(--minute-height));
- height: calc(var(--preview-duration) * var(--minute-height));
-}
-
-/* Event filtering styles */
-/* When filter is active, all events are dimmed by default */
-swp-events-layer[data-filter-active="true"] swp-event {
- opacity: 0.2;
- transition: opacity 200ms ease;
-}
-
-/* Events that match the filter stay normal */
-swp-events-layer[data-filter-active="true"] swp-event[data-matches="true"] {
- opacity: 1;
-}
-
-/* Event overlap styling */
-/* Event group container for column sharing */
-swp-event-group {
- position: absolute;
- display: grid;
- gap: 2px;
- left: 2px;
- right: 2px;
- z-index: 10;
-}
-
-/* Grid column configurations */
-swp-event-group.cols-2 {
- grid-template-columns: 1fr 1fr;
-}
-
-swp-event-group.cols-3 {
- grid-template-columns: 1fr 1fr 1fr;
-}
-
-swp-event-group.cols-4 {
- grid-template-columns: 1fr 1fr 1fr 1fr;
-}
-
-/* Stack levels using margin-left */
-swp-event-group.stack-level-0 {
- margin-left: 0px;
-}
-
-swp-event-group.stack-level-1 {
- margin-left: 15px;
-}
-
-swp-event-group.stack-level-2 {
- margin-left: 30px;
-}
-
-swp-event-group.stack-level-3 {
- margin-left: 45px;
-}
-
-swp-event-group.stack-level-4 {
- margin-left: 60px;
-}
-
-/* Shadow for stacked events (level 1+) */
-swp-event[data-stack-link]:not([data-stack-link*='"stackLevel":0']),
-swp-event-group[data-stack-link]:not([data-stack-link*='"stackLevel":0']) swp-event {
- box-shadow:
- 0 -1px 2px rgba(0, 0, 0, 0.1),
- 0 1px 2px rgba(0, 0, 0, 0.1);
-}
-
-/* Child events within grid */
-swp-event-group swp-event {
- position: relative;
- left: 0;
- right: 0;
-}
-
-/* All-day event transition for smooth repositioning */
-swp-allday-container swp-event.transitioning {
- transition: grid-area 200ms ease-out, grid-row 200ms ease-out, grid-column 200ms ease-out;
-}
-
-/* Color utility classes */
-.is-red { --b-primary: var(--b-color-red); }
-.is-pink { --b-primary: var(--b-color-pink); }
-.is-magenta { --b-primary: var(--b-color-magenta); }
-.is-purple { --b-primary: var(--b-color-purple); }
-.is-violet { --b-primary: var(--b-color-violet); }
-.is-deep-purple { --b-primary: var(--b-color-deep-purple); }
-.is-indigo { --b-primary: var(--b-color-indigo); }
-.is-blue { --b-primary: var(--b-color-blue); }
-.is-light-blue { --b-primary: var(--b-color-light-blue); }
-.is-cyan { --b-primary: var(--b-color-cyan); }
-.is-teal { --b-primary: var(--b-color-teal); }
-.is-green { --b-primary: var(--b-color-green); }
-.is-light-green { --b-primary: var(--b-color-light-green); }
-.is-lime { --b-primary: var(--b-color-lime); }
-.is-yellow { --b-primary: var(--b-color-yellow); }
-.is-amber { --b-primary: var(--b-color-amber); }
-.is-orange { --b-primary: var(--b-color-orange); }
-.is-deep-orange { --b-primary: var(--b-color-deep-orange); }
-
-/* Header drawer items */
-swp-header-item {
- --b-text: var(--color-text);
-
- /* Positioneres via style.gridArea */
- height: 22px;
- margin: 1px 4px;
- padding: 2px 8px;
- border-radius: 3px;
- font-size: 0.75rem;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: flex;
- align-items: center;
- cursor: pointer;
- user-select: none;
- transition: background-color 200ms ease;
-
- /* Color system - inverted from swp-event */
- background-color: color-mix(in srgb, var(--b-primary) 15%, var(--b-mix));
- color: var(--b-text);
-
- &:hover {
- background-color: color-mix(in srgb, var(--b-primary) 10%, var(--b-mix));
- }
-
- /* Dragging state */
- &.dragging {
- opacity: 0.7;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- }
-}
diff --git a/test-package/index.html b/test-package/index.html
deleted file mode 100644
index fcc8084..0000000
--- a/test-package/index.html
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
- Calendar Package Test
-
-
-
- Calendar Package Test
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test-package/package-lock.json b/test-package/package-lock.json
deleted file mode 100644
index c458173..0000000
--- a/test-package/package-lock.json
+++ /dev/null
@@ -1,654 +0,0 @@
-{
- "name": "test-package",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "test-package",
- "version": "1.0.0",
- "license": "ISC",
- "dependencies": {
- "@novadi/core": "^0.6.0",
- "calendar": "^0.1.6",
- "dayjs": "^1.11.19"
- },
- "devDependencies": {
- "esbuild": "^0.27.2",
- "typescript": "^5.9.3"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
- "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
- "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
- "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
- "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
- "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
- "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
- "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
- "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
- "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
- "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
- "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
- "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
- "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
- "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
- "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
- "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
- "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
- "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
- "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
- "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
- "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
- "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
- "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
- "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@novadi/core": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@novadi/core/-/core-0.6.0.tgz",
- "integrity": "sha512-CU1134Nd7ULMg9OQbID5oP+FLtrMkNiLJ17+dmy4jjmPDcPK/dVzKTFxvJmbBvEfZEc9WtmkmJjqw11ABU7Jxw==",
- "license": "MIT",
- "dependencies": {
- "unplugin": "^2.3.10"
- },
- "optionalDependencies": {
- "@rollup/rollup-win32-x64-msvc": "^4.52.5"
- },
- "peerDependencies": {
- "typescript": ">=5.0.0"
- }
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
- "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/calendar": {
- "version": "0.1.6",
- "resolved": "http://npm.jarjarbinks:4873/calendar/-/calendar-0.1.6.tgz",
- "integrity": "sha512-dZKOg6gHTAexklxsBGnszTWDi0rkV68XXV9epaHxZP6RlMvys155dpitq6q3aWCGbSw8xKeTF7FTHaz5yJoT6A==",
- "dependencies": {
- "dayjs": "^1.11.0"
- },
- "peerDependencies": {
- "@novadi/core": "^0.6.0"
- }
- },
- "node_modules/dayjs": {
- "version": "1.11.19",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
- "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
- "license": "MIT"
- },
- "node_modules/esbuild": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
- "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.2",
- "@esbuild/android-arm": "0.27.2",
- "@esbuild/android-arm64": "0.27.2",
- "@esbuild/android-x64": "0.27.2",
- "@esbuild/darwin-arm64": "0.27.2",
- "@esbuild/darwin-x64": "0.27.2",
- "@esbuild/freebsd-arm64": "0.27.2",
- "@esbuild/freebsd-x64": "0.27.2",
- "@esbuild/linux-arm": "0.27.2",
- "@esbuild/linux-arm64": "0.27.2",
- "@esbuild/linux-ia32": "0.27.2",
- "@esbuild/linux-loong64": "0.27.2",
- "@esbuild/linux-mips64el": "0.27.2",
- "@esbuild/linux-ppc64": "0.27.2",
- "@esbuild/linux-riscv64": "0.27.2",
- "@esbuild/linux-s390x": "0.27.2",
- "@esbuild/linux-x64": "0.27.2",
- "@esbuild/netbsd-arm64": "0.27.2",
- "@esbuild/netbsd-x64": "0.27.2",
- "@esbuild/openbsd-arm64": "0.27.2",
- "@esbuild/openbsd-x64": "0.27.2",
- "@esbuild/openharmony-arm64": "0.27.2",
- "@esbuild/sunos-x64": "0.27.2",
- "@esbuild/win32-arm64": "0.27.2",
- "@esbuild/win32-ia32": "0.27.2",
- "@esbuild/win32-x64": "0.27.2"
- }
- },
- "node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/unplugin": {
- "version": "2.3.11",
- "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
- "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "acorn": "^8.15.0",
- "picomatch": "^4.0.3",
- "webpack-virtual-modules": "^0.6.2"
- },
- "engines": {
- "node": ">=18.12.0"
- }
- },
- "node_modules/webpack-virtual-modules": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
- "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
- "license": "MIT"
- }
- }
-}
diff --git a/test-package/package.json b/test-package/package.json
deleted file mode 100644
index c0d1011..0000000
--- a/test-package/package.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "test-package",
- "version": "1.0.0",
- "main": "index.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "description": "",
- "dependencies": {
- "@novadi/core": "^0.6.0",
- "calendar": "^0.1.6",
- "dayjs": "^1.11.19"
- },
- "devDependencies": {
- "esbuild": "^0.27.2",
- "typescript": "^5.9.3"
- }
-}
diff --git a/test-package/src/index.ts b/test-package/src/index.ts
deleted file mode 100644
index 43e78c8..0000000
--- a/test-package/src/index.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { Container } from '@novadi/core';
-import {
- registerCoreServices,
- CalendarApp,
- IndexedDBContext,
- SettingsService,
- ViewConfigService,
- EventService,
- EventBus,
- CalendarEvents,
- EventPersistenceManager
-} from 'calendar';
-import { registerSchedules } from 'calendar/schedules';
-
-async function init() {
- // Check if database already exists
- const databases = await indexedDB.databases();
- const dbExists = databases.some(db => db.name === 'CalendarTestDB');
-
- const container = new Container();
- const builder = container.builder();
- registerCoreServices(builder, {
- dbConfig: { dbName: 'CalendarTestDB', dbVersion: 4 }
- });
- registerSchedules(builder);
- const app = builder.build();
-
- console.log('Container created');
-
- // Initialize IndexedDB
- const dbContext = app.resolveType();
- await dbContext.initialize();
- console.log('IndexedDB initialized');
-
- const settingsService = app.resolveType();
- const viewConfigService = app.resolveType();
- const eventService = app.resolveType();
-
- if (dbExists) {
- console.log('Database exists, skipping seed');
- } else {
- console.log('Seeding data...');
- await seedData(settingsService, viewConfigService, eventService);
- }
-
- // Initialize and render
- const calendarApp = app.resolveType();
- const containerEl = document.querySelector('swp-calendar-container') as HTMLElement;
- await calendarApp.init(containerEl);
-
- const eventBus = app.resolveType();
- eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: 'simple' });
-
- console.log('Calendar rendered');
-
- // Debug listeners
- document.addEventListener('event:drag-end', (e) => {
- console.log('event:drag-end:', (e as CustomEvent).detail);
- });
- document.addEventListener('event:updated', (e) => {
- console.log('event:updated:', (e as CustomEvent).detail);
- });
-
- const persistenceManager = app.resolveType();
- console.log('EventPersistenceManager resolved:', persistenceManager);
-}
-
-async function seedData(
- settingsService: SettingsService,
- viewConfigService: ViewConfigService,
- eventService: EventService
-) {
- // Grid settings
- await settingsService.save({
- id: 'grid',
- dayStartHour: 8,
- dayEndHour: 17,
- workStartHour: 9,
- workEndHour: 16,
- hourHeight: 64,
- snapInterval: 15,
- syncStatus: 'synced'
- });
-
- // Workweek settings
- await settingsService.save({
- id: 'workweek',
- presets: {
- standard: { id: 'standard', label: 'Standard', workDays: [1, 2, 3, 4, 5], periodDays: 7 }
- },
- defaultPreset: 'standard',
- firstDayOfWeek: 1,
- syncStatus: 'synced'
- });
-
- // Simple view config
- await viewConfigService.save({
- id: 'simple',
- groupings: [{ type: 'date', values: [], idProperty: 'date', derivedFrom: 'start' }],
- syncStatus: 'synced'
- });
-
- // Add test events
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- console.log('Event date:', today.toISOString());
-
- const start1 = new Date(today);
- start1.setHours(9, 0, 0, 0);
- const end1 = new Date(today);
- end1.setHours(10, 0, 0, 0);
-
- await eventService.save({
- id: '1',
- title: 'Morgenmøde',
- start: start1,
- end: end1,
- type: 'meeting',
- allDay: false,
- syncStatus: 'synced'
- });
-
- const start2 = new Date(today);
- start2.setHours(12, 0, 0, 0);
- const end2 = new Date(today);
- end2.setHours(13, 0, 0, 0);
-
- await eventService.save({
- id: '2',
- title: 'Frokost',
- start: start2,
- end: end2,
- type: 'break',
- allDay: false,
- syncStatus: 'synced'
- });
-}
-
-init().catch(console.error);
diff --git a/test-package/tsconfig.json b/test-package/tsconfig.json
deleted file mode 100644
index ebbec2c..0000000
--- a/test-package/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "outDir": "./dist",
- "rootDir": "./src",
- "lib": ["ES2024", "DOM", "DOM.Iterable"],
- "experimentalDecorators": true,
- "emitDecoratorMetadata": true
- },
- "include": ["src/**/*"]
-}
diff --git a/wwwroot/css/dashboard.css b/wwwroot/css/dashboard.css
index b03b55f..81b78bf 100644
--- a/wwwroot/css/dashboard.css
+++ b/wwwroot/css/dashboard.css
@@ -867,291 +867,3 @@ swp-date-display i {
grid-template-columns: 1fr;
}
}
-
-/* ==========================================
- WAITLIST MINI CARD
- ========================================== */
-swp-waitlist-card {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px;
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: 8px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-swp-waitlist-card:hover {
- border-color: var(--color-teal);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-swp-waitlist-icon {
- position: relative;
- font-size: 24px;
- color: var(--color-text-secondary);
-}
-
-swp-waitlist-badge {
- position: absolute;
- top: -8px;
- right: -8px;
- min-width: 20px;
- height: 20px;
- padding: 0 6px;
- background: var(--color-teal);
- color: white;
- font-size: 11px;
- font-weight: 600;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-swp-waitlist-label {
- font-size: 14px;
- font-weight: 500;
- color: var(--color-text);
-}
-
-/* ==========================================
- WAITLIST DRAWER
- ========================================== */
-swp-drawer-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.3);
- opacity: 0;
- visibility: hidden;
- transition: all 0.3s ease;
- z-index: 999;
-}
-
-swp-drawer-overlay.visible {
- opacity: 1;
- visibility: visible;
-}
-
-swp-waitlist-drawer {
- position: fixed;
- top: 0;
- right: 0;
- width: 420px;
- height: 100vh;
- background: var(--color-surface);
- border-left: 1px solid var(--color-border);
- box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
- transform: translateX(100%);
- transition: transform 0.3s ease;
- z-index: 1000;
- display: flex;
- flex-direction: column;
-}
-
-swp-waitlist-drawer.open {
- transform: translateX(0);
-}
-
-swp-drawer-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20px 24px;
- border-bottom: 1px solid var(--color-border);
-}
-
-swp-drawer-title {
- font-size: 16px;
- font-weight: 600;
- color: var(--color-text);
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-swp-drawer-title swp-count {
- font-size: 14px;
- font-weight: 500;
- color: var(--color-text-secondary);
-}
-
-swp-drawer-close {
- width: 32px;
- height: 32px;
- border-radius: 6px;
- background: var(--color-background-alt);
- border: none;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--color-text-secondary);
- transition: all 0.15s ease;
-}
-
-swp-drawer-close:hover {
- background: var(--color-background-hover);
- color: var(--color-text);
-}
-
-swp-drawer-close i {
- font-size: 18px;
-}
-
-swp-drawer-body {
- flex: 1;
- overflow-y: auto;
- padding: 16px;
-}
-
-swp-waitlist-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-swp-waitlist-item {
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 16px;
- background: var(--color-background-alt);
- border-radius: 8px;
- border: 1px solid var(--color-border);
-}
-
-swp-waitlist-customer {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-swp-waitlist-customer swp-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: var(--color-teal);
- color: white;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- font-weight: 600;
- flex-shrink: 0;
-}
-
-swp-waitlist-customer-info {
- flex: 1;
-}
-
-swp-waitlist-name {
- font-size: 14px;
- font-weight: 600;
- color: var(--color-text);
-}
-
-swp-waitlist-phone {
- font-size: 12px;
- color: var(--color-text-secondary);
-}
-
-swp-waitlist-service {
- font-size: 13px;
- font-weight: 500;
- color: var(--color-teal);
- padding: 6px 10px;
- background: color-mix(in srgb, var(--color-teal) 10%, transparent);
- border-radius: 4px;
- display: inline-block;
-}
-
-swp-waitlist-meta {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-swp-waitlist-periods {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
-}
-
-swp-waitlist-periods swp-label {
- font-size: 12px;
- color: var(--color-text-secondary);
-}
-
-swp-waitlist-period-tag {
- font-size: 11px;
- padding: 3px 8px;
- background: var(--color-background);
- border-radius: 4px;
- color: var(--color-text);
-}
-
-swp-waitlist-dates {
- display: flex;
- align-items: center;
- gap: 16px;
-}
-
-swp-waitlist-date {
- font-size: 11px;
- color: var(--color-text-secondary);
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-swp-waitlist-date i {
- font-size: 12px;
-}
-
-swp-waitlist-date.expires {
- color: var(--color-text-secondary);
-}
-
-swp-waitlist-date.expires.soon {
- color: var(--color-amber);
- font-weight: 500;
-}
-
-swp-waitlist-actions {
- display: flex;
- gap: 8px;
- padding-top: 8px;
- border-top: 1px solid var(--color-border);
-}
-
-swp-waitlist-actions swp-btn {
- flex: 1;
- justify-content: center;
- padding: 8px 12px;
- font-size: 12px;
-}
-
-swp-waitlist-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 48px 24px;
- text-align: center;
-}
-
-swp-waitlist-empty i {
- font-size: 48px;
- color: var(--color-border);
- margin-bottom: 16px;
-}
-
-swp-waitlist-empty span {
- font-size: 14px;
- color: var(--color-text-secondary);
-}
diff --git a/wwwroot/js/calendar-min.js b/wwwroot/js/calendar-min.js
deleted file mode 100644
index 322988f..0000000
--- a/wwwroot/js/calendar-min.js
+++ /dev/null
@@ -1,26 +0,0 @@
-var Lt=Object.create;var vt=Object.defineProperty;var $t=Object.getOwnPropertyDescriptor;var Ht=Object.getOwnPropertyNames;var Bt=Object.getPrototypeOf,Wt=Object.prototype.hasOwnProperty;var se=(o,e)=>()=>(e||o((e={exports:{}}).exports,e),e.exports);var Pt=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Ht(e))!Wt.call(o,r)&&r!==t&&vt(o,r,{get:()=>e[r],enumerable:!(n=$t(e,r))||n.enumerable});return o};var ie=(o,e,t)=>(t=o!=null?Lt(Bt(o)):{},Pt(e||!o||!o.__esModule?vt(t,"default",{value:o,enumerable:!0}):t,o));var Et=se((rt,st)=>{(function(o,e){typeof rt=="object"&&typeof st<"u"?st.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs=e()})(rt,function(){"use strict";var o=1e3,e=6e4,t=36e5,n="millisecond",r="second",s="minute",i="hour",a="day",c="week",d="month",g="quarter",w="year",E="date",m="Invalid Date",u=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,D=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,C={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(S){var f=["th","st","nd","rd"],h=S%100;return"["+S+(f[(h-20)%10]||f[h]||f[0])+"]"}},k=function(S,f,h){var y=String(S);return!y||y.length>=f?S:""+Array(f+1-y.length).join(h)+S},l={s:k,z:function(S){var f=-S.utcOffset(),h=Math.abs(f),y=Math.floor(h/60),p=h%60;return(f<=0?"+":"-")+k(y,2,"0")+":"+k(p,2,"0")},m:function S(f,h){if(f.date()1)return S(M[0])}else{var R=f.name;W[R]=f,p=R}return!y&&p&&(O=p),p||!y&&O},x=function(S,f){if(N(S))return S.clone();var h=typeof f=="object"?f:{};return h.date=S,h.args=arguments,new Y(h)},A=l;A.l=$,A.i=N,A.w=function(S,f){return x(S,{locale:f.$L,utc:f.$u,x:f.$x,$offset:f.$offset})};var Y=function(){function S(h){this.$L=$(h.locale,null,!0),this.parse(h),this.$x=this.$x||h.x||{},this[P]=!0}var f=S.prototype;return f.parse=function(h){this.$d=function(y){var p=y.date,T=y.utc;if(p===null)return new Date(NaN);if(A.u(p))return new Date;if(p instanceof Date)return new Date(p);if(typeof p=="string"&&!/Z$/i.test(p)){var M=p.match(u);if(M){var R=M[2]-1||0,L=(M[7]||"0").substring(0,3);return T?new Date(Date.UTC(M[1],R,M[3]||1,M[4]||0,M[5]||0,M[6]||0,L)):new Date(M[1],R,M[3]||1,M[4]||0,M[5]||0,M[6]||0,L)}}return new Date(p)}(h),this.init()},f.init=function(){var h=this.$d;this.$y=h.getFullYear(),this.$M=h.getMonth(),this.$D=h.getDate(),this.$W=h.getDay(),this.$H=h.getHours(),this.$m=h.getMinutes(),this.$s=h.getSeconds(),this.$ms=h.getMilliseconds()},f.$utils=function(){return A},f.isValid=function(){return this.$d.toString()!==m},f.isSame=function(h,y){var p=x(h);return this.startOf(y)<=p&&p<=this.endOf(y)},f.isAfter=function(h,y){return x(h){(function(o,e){typeof it=="object"&&typeof at<"u"?at.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs_plugin_utc=e()})(it,function(){"use strict";var o="minute",e=/[+-]\d\d(?::?\d\d)?/g,t=/([+-]|\d\d)/g;return function(n,r,s){var i=r.prototype;s.utc=function(m){var u={date:m,utc:!0,args:arguments};return new r(u)},i.utc=function(m){var u=s(this.toDate(),{locale:this.$L,utc:!0});return m?u.add(this.utcOffset(),o):u},i.local=function(){return s(this.toDate(),{locale:this.$L,utc:!1})};var a=i.parse;i.parse=function(m){m.utc&&(this.$u=!0),this.$utils().u(m.$offset)||(this.$offset=m.$offset),a.call(this,m)};var c=i.init;i.init=function(){if(this.$u){var m=this.$d;this.$y=m.getUTCFullYear(),this.$M=m.getUTCMonth(),this.$D=m.getUTCDate(),this.$W=m.getUTCDay(),this.$H=m.getUTCHours(),this.$m=m.getUTCMinutes(),this.$s=m.getUTCSeconds(),this.$ms=m.getUTCMilliseconds()}else c.call(this)};var d=i.utcOffset;i.utcOffset=function(m,u){var D=this.$utils().u;if(D(m))return this.$u?0:D(this.$offset)?d.call(this):this.$offset;if(typeof m=="string"&&(m=function(O){O===void 0&&(O="");var W=O.match(e);if(!W)return null;var P=(""+W[0]).match(t)||["-",0,0],N=P[0],$=60*+P[1]+ +P[2];return $===0?0:N==="+"?$:-$}(m),m===null))return this;var C=Math.abs(m)<=16?60*m:m;if(C===0)return this.utc(u);var k=this.clone();if(u)return k.$offset=C,k.$u=!1,k;var l=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();return(k=this.local().add(C+l,o)).$offset=C,k.$x.$localOffset=l,k};var g=i.format;i.format=function(m){var u=m||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return g.call(this,u)},i.valueOf=function(){var m=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*m},i.isUTC=function(){return!!this.$u},i.toISOString=function(){return this.toDate().toISOString()},i.toString=function(){return this.toDate().toUTCString()};var w=i.toDate;i.toDate=function(m){return m==="s"&&this.$offset?s(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():w.call(this)};var E=i.diff;i.diff=function(m,u,D){if(m&&this.$u===m.$u)return E.call(this,m,u,D);var C=this.local(),k=s(m).local();return E.call(C,k,u,D)}}})});var St=se((ot,lt)=>{(function(o,e){typeof ot=="object"&&typeof lt<"u"?lt.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs_plugin_timezone=e()})(ot,function(){"use strict";var o={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(t,n,r){var s,i=function(g,w,E){E===void 0&&(E={});var m=new Date(g),u=function(D,C){C===void 0&&(C={});var k=C.timeZoneName||"short",l=D+"|"+k,O=e[l];return O||(O=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:D,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:k}),e[l]=O),O}(w,E);return u.formatToParts(m)},a=function(g,w){for(var E=i(g,w),m=[],u=0;u=0&&(m[l]=parseInt(k,10))}var O=m[3],W=O===24?0:O,P=m[0]+"-"+m[1]+"-"+m[2]+" "+W+":"+m[4]+":"+m[5]+":000",N=+g;return(r.utc(P).valueOf()-(N-=N%1e3))/6e4},c=n.prototype;c.tz=function(g,w){g===void 0&&(g=s);var E,m=this.utcOffset(),u=this.toDate(),D=u.toLocaleString("en-US",{timeZone:g}),C=Math.round((u-new Date(D))/1e3/60),k=15*-Math.round(u.getTimezoneOffset()/15)-C;if(!Number(k))E=this.utcOffset(0,w);else if(E=r(D,{locale:this.$L}).$set("millisecond",this.$ms).utcOffset(k,!0),w){var l=E.utcOffset();E=E.add(m-l,"minute")}return E.$x.$timezone=g,E},c.offsetName=function(g){var w=this.$x.$timezone||r.tz.guess(),E=i(this.valueOf(),w,{timeZoneName:g}).find(function(m){return m.type.toLowerCase()==="timezonename"});return E&&E.value};var d=c.startOf;c.startOf=function(g,w){if(!this.$x||!this.$x.$timezone)return d.call(this,g,w);var E=r(this.format("YYYY-MM-DD HH:mm:ss:SSS"),{locale:this.$L});return d.call(E,g,w).tz(this.$x.$timezone,!0)},r.tz=function(g,w,E){var m=E&&w,u=E||w||s,D=a(+r(),u);if(typeof g!="string")return r(g).tz(u);var C=function(W,P,N){var $=W-60*P*1e3,x=a($,N);if(P===x)return[$,P];var A=a($-=60*(x-P)*1e3,N);return x===A?[$,x]:[W-60*Math.min(x,A)*1e3,Math.max(x,A)]}(r.utc(g,m).valueOf(),D,u),k=C[0],l=C[1],O=r(k).utcOffset(l);return O.$x.$timezone=u,O},r.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},r.tz.setDefault=function(g){s=g}}})});var wt=se((ct,dt)=>{(function(o,e){typeof ct=="object"&&typeof dt<"u"?dt.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs_plugin_isoWeek=e()})(ct,function(){"use strict";var o="day";return function(e,t,n){var r=function(a){return a.add(4-a.isoWeekday(),o)},s=t.prototype;s.isoWeekYear=function(){return r(this).year()},s.isoWeek=function(a){if(!this.$utils().u(a))return this.add(7*(a-this.isoWeek()),o);var c,d,g,w,E=r(this),m=(c=this.isoWeekYear(),d=this.$u,g=(d?n.utc:n)().year(c).startOf("year"),w=4-g.isoWeekday(),g.isoWeekday()>4&&(w+=7),g.add(w,o));return E.diff(m,"week")+1},s.isoWeekday=function(a){return this.$utils().u(a)?this.day()||7:this.day(this.day()%7?a:a-7)};var i=s.startOf;s.startOf=function(a,c){var d=this.$utils(),g=!!d.u(c)||c;return d.p(a)==="isoweek"?g?this.date(this.date()-(this.isoWeekday()-1)).startOf("day"):this.date(this.date()-1-(this.isoWeekday()-1)+7).endOf("day"):i.bind(this)(a,c)}}})});var Ct=se((ut,ht)=>{(function(o,e){typeof ut=="object"&&typeof ht<"u"?ht.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs_plugin_customParseFormat=e()})(ut,function(){"use strict";var o={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},e=/(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|Q|YYYY|YY?|ww?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,t=/\d/,n=/\d\d/,r=/\d\d?/,s=/\d*[^-_:/,()\s\d]+/,i={},a=function(u){return(u=+u)+(u>68?1900:2e3)},c=function(u){return function(D){this[u]=+D}},d=[/[+-]\d\d:?(\d\d)?|Z/,function(u){(this.zone||(this.zone={})).offset=function(D){if(!D||D==="Z")return 0;var C=D.match(/([+-]|\d\d)/g),k=60*C[1]+(+C[2]||0);return k===0?0:C[0]==="+"?-k:k}(u)}],g=function(u){var D=i[u];return D&&(D.indexOf?D:D.s.concat(D.f))},w=function(u,D){var C,k=i.meridiem;if(k){for(var l=1;l<=24;l+=1)if(u.indexOf(k(l,0,D))>-1){C=l>12;break}}else C=u===(D?"pm":"PM");return C},E={A:[s,function(u){this.afternoon=w(u,!1)}],a:[s,function(u){this.afternoon=w(u,!0)}],Q:[t,function(u){this.month=3*(u-1)+1}],S:[t,function(u){this.milliseconds=100*+u}],SS:[n,function(u){this.milliseconds=10*+u}],SSS:[/\d{3}/,function(u){this.milliseconds=+u}],s:[r,c("seconds")],ss:[r,c("seconds")],m:[r,c("minutes")],mm:[r,c("minutes")],H:[r,c("hours")],h:[r,c("hours")],HH:[r,c("hours")],hh:[r,c("hours")],D:[r,c("day")],DD:[n,c("day")],Do:[s,function(u){var D=i.ordinal,C=u.match(/\d+/);if(this.day=C[0],D)for(var k=1;k<=31;k+=1)D(k).replace(/\[|\]/g,"")===u&&(this.day=k)}],w:[r,c("week")],ww:[n,c("week")],M:[r,c("month")],MM:[n,c("month")],MMM:[s,function(u){var D=g("months"),C=(g("monthsShort")||D.map(function(k){return k.slice(0,3)})).indexOf(u)+1;if(C<1)throw new Error;this.month=C%12||C}],MMMM:[s,function(u){var D=g("months").indexOf(u)+1;if(D<1)throw new Error;this.month=D%12||D}],Y:[/[+-]?\d+/,c("year")],YY:[n,function(u){this.year=a(u)}],YYYY:[/\d{4}/,c("year")],Z:d,ZZ:d};function m(u){var D,C;D=u,C=i&&i.formats;for(var k=(u=D.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,function(x,A,Y){var z=Y&&Y.toUpperCase();return A||C[Y]||o[Y]||C[z].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(S,f,h){return f||h.slice(1)})})).match(e),l=k.length,O=0;O-1)return new Date((p==="X"?1e3:1)*y);var R=m(p)(y),L=R.year,H=R.month,F=R.day,q=R.hours,ee=R.minutes,Z=R.seconds,re=R.milliseconds,j=R.zone,_=R.week,G=new Date,X=F||(L||H?1:G.getDate()),te=L||G.getFullYear(),ve=0;L&&!H||(ve=H>0?H-1:G.getMonth());var ye,Qe=q||0,Ze=ee||0,Xe=Z||0,Ke=re||0;return j?new Date(Date.UTC(te,ve,X,Qe,Ze,Xe,Ke+60*j.offset*1e3)):T?new Date(Date.UTC(te,ve,X,Qe,Ze,Xe,Ke)):(ye=new Date(te,ve,X,Qe,Ze,Xe,Ke),_&&(ye=M(ye).week(_).toDate()),ye)}catch{return new Date("")}}(W,$,P,C),this.init(),z&&z!==!0&&(this.$L=this.locale(z).$L),Y&&W!=this.format($)&&(this.$d=new Date("")),i={}}else if($ instanceof Array)for(var S=$.length,f=1;f<=S;f+=1){N[1]=$[f-1];var h=C.apply(this,N);if(h.isValid()){this.$d=h.$d,this.$L=h.$L,this.init();break}f===S&&(this.$d=new Date(""))}else l.call(this,O)}}})});var Tt=se((gt,mt)=>{(function(o,e){typeof gt=="object"&&typeof mt<"u"?mt.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs_plugin_isSameOrAfter=e()})(gt,function(){"use strict";return function(o,e){e.prototype.isSameOrAfter=function(t,n){return this.isSame(t,n)||this.isAfter(t,n)}}})});var kt=se((ft,pt)=>{(function(o,e){typeof ft=="object"&&typeof pt<"u"?pt.exports=e():typeof define=="function"&&define.amd?define(e):(o=typeof globalThis<"u"?globalThis:o||self).dayjs_plugin_isSameOrBefore=e()})(ft,function(){"use strict";return function(o,e){e.prototype.isSameOrBefore=function(t,n){return this.isSame(t,n)||this.isBefore(t,n)}}})});var Nt=0;function ae(o){let e=++Nt;return{symbol:Symbol(o?`Token(${o})`:`Token#${e}`),description:o,toString(){return o?`Token<${o}>`:`Token<#${e}>`}}}var de=class extends Error{constructor(e){super(e),this.name="ContainerError"}},ue=class extends de{constructor(e,t=[]){let n=t.length>0?`
- Dependency path: ${t.join(" -> ")}`:"";super(`Token "${e}" is not bound or registered in the container.${n}`),this.name="BindingNotFoundError"}},he=class extends de{constructor(e){super(`Circular dependency detected: ${e.join(" -> ")}`),this.name="CircularDependencyError"}};var yt=new WeakMap;function Ft(o){let e=yt.get(o);if(e)return e;let t=o.toString(),n=t.match(/constructor\s*\(([^)]*)\)/)||t.match(/^[^(]*\(([^)]*)\)/);if(!n||!n[1])return[];let r=n[1].split(",").map(s=>s.trim()).filter(s=>s.length>0).map(s=>{let i=s.split(/[:=]/)[0].trim();return i=i.replace(/^((public|private|protected|readonly)\s+)+/,""),i.includes("{")||i.includes("[")?null:i}).filter(s=>s!==null);return yt.set(o,r),r}function _t(o,e,t){if(!t.map)throw new Error("AutoWire map strategy requires options.map to be defined");let n=Ft(o),r=[];for(let s of n){let i=t.map[s];if(i===void 0){if(t.strict)throw new Error(`Cannot resolve parameter "${s}" on ${o.name}. Not found in autowire map. Add it to the map: .autoWire({ map: { ${s}: ... } })`);r.push(void 0);continue}typeof i=="function"?r.push(i(e)):r.push(e.resolve(i))}return r}function Yt(o,e,t){if(!t.mapResolvers||t.mapResolvers.length===0)return[];let n=[];for(let r=0;r0?Yt(o,e,n):n.map&&Object.keys(n.map).length>0?_t(o,e,n):[]}var le=class{constructor(e,t){this.registrations=t,this.configs=[],this.defaultLifetime="singleton",this.pending=e}as(e){if(e&&typeof e=="object"&&"symbol"in e){let t={token:e,type:this.pending.type,value:this.pending.value,factory:this.pending.factory,constructor:this.pending.constructor,lifetime:this.defaultLifetime};return this.configs.push(t),this.registrations.push(t),this}else{let t={token:null,type:this.pending.type,value:this.pending.value,factory:this.pending.factory,constructor:this.pending.constructor,lifetime:this.defaultLifetime,interfaceType:e};return this.configs.push(t),this.registrations.push(t),this}}asDefaultInterface(e){return this.as("TInterface",e),this.asDefault()}asKeyedInterface(e,t){return this.as("TInterface",t),this.keyed(e)}asImplementedInterfaces(e){if(e.length===0)return this;if(this.configs.length>0){for(let n of this.configs)n.lifetime="singleton",n.additionalTokens=n.additionalTokens||[],n.additionalTokens.push(...e);return this}let t={token:e[0],type:this.pending.type,value:this.pending.value,factory:this.pending.factory,constructor:this.pending.constructor,lifetime:"singleton"};this.configs.push(t),this.registrations.push(t);for(let n=1;n