diff --git a/docs/filter-template-spec.md b/docs/filter-template-spec.md new file mode 100644 index 0000000..d709bd7 --- /dev/null +++ b/docs/filter-template-spec.md @@ -0,0 +1,342 @@ +# FilterTemplate & Grouping System Specification + +> **Version:** 1.0 +> **Dato:** 2025-12-15 +> **Status:** Godkendt + +## Formål + +Dette dokument specificerer hvordan kalenderen matcher events til kolonner i alle view-typer (Simple, Dag, Resource, Team, Department). Formålet er at sikre konsistent opførsel og undgå fremtidige hacks. + +--- + +## Kerneprincipper + +### 1. Én sandhedskilde for key-format + +**FilterTemplate** er den ENESTE kilde til key-format for event-kolonne matching. + +``` +KORREKT: filterTemplate.buildKeyFromColumn(column) +KORREKT: filterTemplate.buildKeyFromEvent(event) +FORKERT: column.dataset.columnKey (bruger DateService-format) +FORKERT: Manuel key-konstruktion med string concatenation +``` + +### 2. Fields-rækkefølge bestemmer key-format + +Keys bygges fra `fields` array i samme rækkefølge som defineret i ViewConfig groupings. + +```typescript +// ViewConfig groupings: [resource, date] +// Resultat: "EMP001:2025-12-09" + +// ViewConfig groupings: [date, resource] +// Resultat: "2025-12-09:EMP001" +``` + +### 3. Kolonner og events bruger SAMME template + +```typescript +// Opret template fra ViewConfig +const filterTemplate = new FilterTemplate(dateService); +for (const grouping of viewConfig.groupings) { + if (grouping.idProperty) { + filterTemplate.addField(grouping.idProperty, grouping.derivedFrom); + } +} + +// Brug til BÅDE kolonner og events +const columnKey = filterTemplate.buildKeyFromColumn(column); +const eventKey = filterTemplate.buildKeyFromEvent(event); +const matches = columnKey === eventKey; +``` + +--- + +## API Kontrakt + +### FilterTemplate + +```typescript +class FilterTemplate { + constructor(dateService: DateService, entityResolver?: IEntityResolver) + + // Tilføj felt til template + addField(idProperty: string, derivedFrom?: string): this + + // Byg key fra kolonne (læser fra dataset) + buildKeyFromColumn(column: HTMLElement): string + + // Byg key fra event (læser fra event properties) + buildKeyFromEvent(event: ICalendarEvent): string + + // Convenience: matcher event mod kolonne + matches(event: ICalendarEvent, column: HTMLElement): boolean +} +``` + +### Field Definition + +| Parameter | Type | Beskrivelse | +|-----------|------|-------------| +| `idProperty` | `string` | Property-navn på event ELLER dot-notation | +| `derivedFrom` | `string?` | Kilde-property hvis værdi skal udledes | + +### Dot-Notation + +For hierarkiske relationer bruges dot-notation: + +```typescript +idProperty: 'resource.teamId' +// Betyder: event.resourceId → opslag i resource → teamId +``` + +**Convention:** `{entityType}.{property}` → foreignKey er `{entityType}Id` + +--- + +## Kolonne Dataset Krav + +Kolonner (`swp-day-column`) SKAL have dataset-attributter for alle felter i template: + +```html + + + + + + + + + + +``` + +### Dataset Key Mapping + +| idProperty | Dataset Key | Eksempel | +|------------|-------------|----------| +| `date` | `data-date` | `"2025-12-09"` | +| `resourceId` | `data-resource-id` | `"EMP001"` | +| `resource.teamId` | `data-team-id` | `"team-1"` | + +**Regel:** Dot-notation bruger sidste segment som dataset-key. + +--- + +## Event Property Krav + +Events SKAL have properties der matcher template fields: + +```typescript +interface ICalendarEvent { + id: string; + start: Date; // derivedFrom: 'start' → date key + end: Date; + resourceId?: string; // Direkte match + // ... andre properties +} +``` + +### Derived Values + +| idProperty | derivedFrom | Transformation | +|------------|-------------|----------------| +| `date` | `start` | `Date → "YYYY-MM-DD"` | + +--- + +## ViewConfig Groupings + +### Struktur + +```typescript +interface GroupingConfig { + type: string; // 'date', 'resource', 'team', 'department' + values: string[]; // Synlige værdier + idProperty?: string; // Felt til key-matching + derivedFrom?: string; // Kilde hvis udledt + belongsTo?: string; // Parent-child relation +} +``` + +### Eksempler + +```typescript +// Simple view +groupings: [ + { type: 'date', values: ['2025-12-09', ...], idProperty: 'date', derivedFrom: 'start' } +] + +// Resource view +groupings: [ + { type: 'resource', values: ['EMP001', 'EMP002'], idProperty: 'resourceId' }, + { type: 'date', values: ['2025-12-09', ...], idProperty: 'date', derivedFrom: 'start' } +] + +// Team view +groupings: [ + { type: 'team', values: ['team-1', 'team-2'], idProperty: 'resource.teamId' }, + { type: 'resource', values: ['EMP001', ...], idProperty: 'resourceId', belongsTo: 'team.resourceIds' }, + { type: 'date', values: ['2025-12-09', ...], idProperty: 'date', derivedFrom: 'start' } +] +``` + +--- + +## BelongsTo Resolution + +### Formål + +`belongsTo` definerer parent-child relationer for nested groupings. + +### Syntax + +``` +belongsTo: '{parentEntityType}.{childArrayProperty}' +``` + +### Eksempel + +```typescript +// Team har resourceIds array +{ type: 'resource', belongsTo: 'team.resourceIds' } + +// Resolver: +// 1. Hent team entities fra filter['team'] +// 2. For hver team, læs team.resourceIds +// 3. Byg map: { 'team-1': ['EMP001', 'EMP002'], 'team-2': ['EMP003'] } +``` + +### Implementering + +```typescript +// CalendarOrchestrator.resolveBelongsTo() +const [entityType, property] = belongsTo.split('.'); +const service = entityServices.find(s => s.entityType === entityType); +const entities = await service.getAll(); +// Byg parent-child map +``` + +--- + +## HeaderDrawerRenderer Regler + +### Key Matching for AllDay Events + +```typescript +// KORREKT: Brug FilterTemplate +private getVisibleColumnKeysFromDOM(): string[] { + const columns = document.querySelectorAll('swp-day-column'); + return Array.from(columns).map(col => + this.filterTemplate.buildKeyFromColumn(col as HTMLElement) + ); +} + +// FORKERT: Læs dataset.columnKey direkte +// (bruger DateService-format som ikke matcher FilterTemplate) +``` + +### Layout Beregning + +1. Hent synlige columnKeys via `getVisibleColumnKeysFromDOM()` +2. For hver event, byg key via `filterTemplate.buildKeyFromEvent(event)` +3. Find kolonne-index via `columnKeys.indexOf(eventKey)` +4. Beregn row via track-algoritme + +--- + +## Anti-Patterns (UNDGÅ) + +### 1. Manuel Key Konstruktion + +```typescript +// FORKERT +const key = `${resourceId}:${dateStr}`; + +// KORREKT +const key = filterTemplate.buildKeyFromEvent(event); +``` + +### 2. Direkte Dataset Læsning for Matching + +```typescript +// FORKERT +const columnKey = column.dataset.columnKey; + +// KORREKT +const columnKey = filterTemplate.buildKeyFromColumn(column); +``` + +### 3. Hardcoded Field Order + +```typescript +// FORKERT +const key = [event.resourceId, dateStr].join(':'); + +// KORREKT +// Lad FilterTemplate håndtere rækkefølge fra ViewConfig +``` + +### 4. Separate Key-Formater + +```typescript +// FORKERT: DateService til kolonner, FilterTemplate til events +DateService.buildColumnKey(segments) // "2025-12-09:EMP001" +FilterTemplate.buildKeyFromEvent(e) // "EMP001:2025-12-09" + +// KORREKT: FilterTemplate til begge +FilterTemplate.buildKeyFromColumn(col) +FilterTemplate.buildKeyFromEvent(event) +``` + +--- + +## Testcases + +### TC1: Simple View Matching + +``` +Given: ViewConfig med [date] grouping +When: Event har start=2025-12-09 +Then: Event matcher kolonne med data-date="2025-12-09" +``` + +### TC2: Resource View Matching + +``` +Given: ViewConfig med [resource, date] groupings +When: Event har resourceId=EMP001, start=2025-12-09 +Then: Event matcher kolonne med data-resource-id="EMP001" OG data-date="2025-12-09" +``` + +### TC3: Team View Matching + +``` +Given: ViewConfig med [team, resource, date] groupings + Resource EMP001 tilhører team-1 +When: Event har resourceId=EMP001, start=2025-12-09 +Then: Event matcher kolonne med data-team-id="team-1" OG data-resource-id="EMP001" OG data-date="2025-12-09" +``` + +### TC4: Multi-Day Event + +``` +Given: Event spænder 2025-12-09 til 2025-12-11 +When: HeaderDrawerRenderer beregner layout +Then: Event vises fra kolonne 09 til kolonne 11 (inclusive) +``` + +--- + +## Ændringslog + +| Version | Dato | Ændring | +|---------|------|---------| +| 1.0 | 2025-12-15 | Initial specifikation | diff --git a/src/v2/V2CompositionRoot.ts b/src/v2/V2CompositionRoot.ts index db696f0..b8728f9 100644 --- a/src/v2/V2CompositionRoot.ts +++ b/src/v2/V2CompositionRoot.ts @@ -35,6 +35,9 @@ import { TeamStore } from './storage/teams/TeamStore'; import { TeamService } from './storage/teams/TeamService'; import { DepartmentStore } from './storage/departments/DepartmentStore'; import { DepartmentService } from './storage/departments/DepartmentService'; +import { SettingsStore } from './storage/settings/SettingsStore'; +import { SettingsService } from './storage/settings/SettingsService'; +import { ITenantSettings } from './types/SettingsTypes'; // Audit import { AuditStore } from './storage/audit/AuditStore'; @@ -50,6 +53,7 @@ import { MockCustomerRepository } from './repositories/MockCustomerRepository'; import { MockAuditRepository } from './repositories/MockAuditRepository'; import { MockTeamRepository } from './repositories/MockTeamRepository'; import { MockDepartmentRepository } from './repositories/MockDepartmentRepository'; +import { MockSettingsRepository } from './repositories/MockSettingsRepository'; // Workers import { DataSeeder } from './workers/DataSeeder'; @@ -113,6 +117,7 @@ export function createV2Container(): Container { builder.registerType(DepartmentStore).as(); builder.registerType(ScheduleOverrideStore).as(); builder.registerType(AuditStore).as(); + builder.registerType(SettingsStore).as(); // Entity services (for DataSeeder polymorphic array) builder.registerType(EventService).as>(); @@ -139,6 +144,10 @@ export function createV2Container(): Container { builder.registerType(DepartmentService).as>(); builder.registerType(DepartmentService).as(); + builder.registerType(SettingsService).as>(); + builder.registerType(SettingsService).as>(); + builder.registerType(SettingsService).as(); + // Repositories (for DataSeeder polymorphic array) builder.registerType(MockEventRepository).as>(); builder.registerType(MockEventRepository).as>(); @@ -161,6 +170,9 @@ export function createV2Container(): Container { builder.registerType(MockDepartmentRepository).as>(); builder.registerType(MockDepartmentRepository).as>(); + builder.registerType(MockSettingsRepository).as>(); + builder.registerType(MockSettingsRepository).as>(); + // Audit service (listens to ENTITY_SAVED/DELETED events automatically) builder.registerType(AuditService).as(); diff --git a/src/v2/core/DateService.ts b/src/v2/core/DateService.ts index f230378..760388a 100644 --- a/src/v2/core/DateService.ts +++ b/src/v2/core/DateService.ts @@ -48,6 +48,21 @@ export class DateService { ); } + /** + * Get dates for specific weekdays within a week + * @param offset - Week offset from base date (0 = current week) + * @param workDays - Array of ISO weekday numbers (1=Monday, 7=Sunday) + * @returns Array of date strings in YYYY-MM-DD format + */ + getWorkWeekDates(offset: number, workDays: number[]): string[] { + const monday = this.baseDate.startOf('week').add(1, 'day').add(offset, 'week'); + 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'); + }); + } + // ============================================ // FORMATTING // ============================================ diff --git a/src/v2/demo/DemoApp.ts b/src/v2/demo/DemoApp.ts index 9c3dc56..47c1746 100644 --- a/src/v2/demo/DemoApp.ts +++ b/src/v2/demo/DemoApp.ts @@ -13,12 +13,15 @@ import { ResizeManager } from '../managers/ResizeManager'; import { EventPersistenceManager } from '../managers/EventPersistenceManager'; import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer'; import { AuditService } from '../storage/audit/AuditService'; +import { SettingsService } from '../storage/settings/SettingsService'; +import { IWorkweekPreset } from '../types/SettingsTypes'; export class DemoApp { private animator!: NavigationAnimator; private container!: HTMLElement; private weekOffset = 0; private currentView: 'day' | 'simple' | 'resource' | 'team' | 'department' = 'simple'; + private workweekPreset: IWorkweekPreset | null = null; constructor( private orchestrator: CalendarOrchestrator, @@ -33,7 +36,8 @@ export class DemoApp { private resizeManager: ResizeManager, private headerDrawerRenderer: HeaderDrawerRenderer, private eventPersistenceManager: EventPersistenceManager, - private auditService: AuditService + private auditService: AuditService, + private settingsService: SettingsService ) {} async init(): Promise { @@ -48,6 +52,10 @@ export class DemoApp { await this.dataSeeder.seedIfEmpty(); console.log('[DemoApp] Data seeding complete'); + // Load default workweek preset from settings + this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset(); + console.log('[DemoApp] Workweek preset loaded:', this.workweekPreset?.id); + this.container = document.querySelector('swp-calendar-container') as HTMLElement; // NavigationAnimator har DOM-dependencies - tilladt med new @@ -90,7 +98,9 @@ export class DemoApp { } private buildViewConfig(): ViewConfig { - const dates = this.dateService.getWeekDates(this.weekOffset, 3); + // Use workweek preset to determine which days to show + const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5]; // Fallback to Mon-Fri + const dates = this.dateService.getWorkWeekDates(this.weekOffset, workDays); const today = this.dateService.getWeekDates(this.weekOffset, 1); switch (this.currentView) { @@ -155,29 +165,32 @@ export class DemoApp { } private setupViewSwitching(): void { - document.getElementById('btn-day')?.addEventListener('click', () => { - this.currentView = 'day'; - this.render(); + // View chip buttons + const chips = document.querySelectorAll('.view-chip'); + chips.forEach(chip => { + chip.addEventListener('click', () => { + // Update active state + chips.forEach(c => c.classList.remove('active')); + chip.classList.add('active'); + + // Switch view + const view = (chip as HTMLElement).dataset.view as typeof this.currentView; + if (view) { + this.currentView = view; + this.render(); + } + }); }); - document.getElementById('btn-simple')?.addEventListener('click', () => { - this.currentView = 'simple'; - this.render(); - }); - - document.getElementById('btn-resource')?.addEventListener('click', () => { - this.currentView = 'resource'; - this.render(); - }); - - document.getElementById('btn-team')?.addEventListener('click', () => { - this.currentView = 'team'; - this.render(); - }); - - document.getElementById('btn-department')?.addEventListener('click', () => { - this.currentView = 'department'; - this.render(); + // Workweek preset dropdown + const workweekSelect = document.getElementById('workweek-select') as HTMLSelectElement; + workweekSelect?.addEventListener('change', async () => { + const presetId = workweekSelect.value; + const preset = await this.settingsService.getWorkweekPreset(presetId); + if (preset) { + this.workweekPreset = preset; + this.render(); + } }); } diff --git a/src/v2/repositories/MockSettingsRepository.ts b/src/v2/repositories/MockSettingsRepository.ts new file mode 100644 index 0000000..9507fec --- /dev/null +++ b/src/v2/repositories/MockSettingsRepository.ts @@ -0,0 +1,47 @@ +import { EntityType } from '../types/CalendarTypes'; +import { ITenantSettings } from '../types/SettingsTypes'; +import { IApiRepository } from './IApiRepository'; + +/** + * MockSettingsRepository - Loads tenant settings from local JSON file + * + * Settings is a single document, but we wrap it in an array to match + * the IApiRepository interface used by DataSeeder. + */ +export class MockSettingsRepository implements IApiRepository { + public readonly entityType: EntityType = 'Settings'; + private readonly dataUrl = 'data/tenant-settings.json'; + + public async fetchAll(): Promise { + try { + const response = await fetch(this.dataUrl); + + if (!response.ok) { + throw new Error(`Failed to load tenant settings: ${response.status} ${response.statusText}`); + } + + const rawData = await response.json(); + // Ensure syncStatus is set + const settings: ITenantSettings = { + ...rawData, + syncStatus: rawData.syncStatus || 'synced' + }; + return [settings]; + } catch (error) { + console.error('Failed to load tenant settings:', error); + throw error; + } + } + + public async sendCreate(_settings: ITenantSettings): Promise { + throw new Error('MockSettingsRepository does not support sendCreate. Mock data is read-only.'); + } + + public async sendUpdate(_id: string, _updates: Partial): Promise { + throw new Error('MockSettingsRepository does not support sendUpdate. Mock data is read-only.'); + } + + public async sendDelete(_id: string): Promise { + throw new Error('MockSettingsRepository does not support sendDelete. Mock data is read-only.'); + } +} diff --git a/src/v2/storage/IndexedDBContext.ts b/src/v2/storage/IndexedDBContext.ts index c399c85..a0e80f0 100644 --- a/src/v2/storage/IndexedDBContext.ts +++ b/src/v2/storage/IndexedDBContext.ts @@ -10,7 +10,7 @@ import { IStore } from './IStore'; */ export class IndexedDBContext { private static readonly DB_NAME = 'CalendarV2DB'; - private static readonly DB_VERSION = 3; + private static readonly DB_VERSION = 4; private db: IDBDatabase | null = null; private initialized: boolean = false; diff --git a/src/v2/storage/settings/SettingsService.ts b/src/v2/storage/settings/SettingsService.ts new file mode 100644 index 0000000..535fcc0 --- /dev/null +++ b/src/v2/storage/settings/SettingsService.ts @@ -0,0 +1,60 @@ +import { EntityType, IEventBus } from '../../types/CalendarTypes'; +import { ITenantSettings, IWorkweekPreset } from '../../types/SettingsTypes'; +import { SettingsStore } from './SettingsStore'; +import { BaseEntityService } from '../BaseEntityService'; +import { IndexedDBContext } from '../IndexedDBContext'; + +/** + * Default settings ID - single document per tenant + */ +const TENANT_SETTINGS_ID = 'tenant-settings'; + +/** + * SettingsService - CRUD operations for tenant settings + * + * Settings are stored as a single document with sections. + * This service provides convenience methods for accessing specific sections. + */ +export class SettingsService extends BaseEntityService { + readonly storeName = SettingsStore.STORE_NAME; + readonly entityType: EntityType = 'Settings'; + + constructor(context: IndexedDBContext, eventBus: IEventBus) { + super(context, eventBus); + } + + /** + * Get the tenant settings document + * Returns null if not yet loaded from backend + */ + async getSettings(): Promise { + return this.get(TENANT_SETTINGS_ID); + } + + /** + * Get workweek preset by ID + */ + async getWorkweekPreset(presetId: string): Promise { + const settings = await this.getSettings(); + if (!settings) return null; + return settings.workweek.presets[presetId] || null; + } + + /** + * Get the default workweek preset + */ + async getDefaultWorkweekPreset(): Promise { + const settings = await this.getSettings(); + if (!settings) return null; + return settings.workweek.presets[settings.workweek.defaultPreset] || null; + } + + /** + * Get all available workweek presets + */ + async getWorkweekPresets(): Promise { + const settings = await this.getSettings(); + if (!settings) return []; + return Object.values(settings.workweek.presets); + } +} diff --git a/src/v2/storage/settings/SettingsStore.ts b/src/v2/storage/settings/SettingsStore.ts new file mode 100644 index 0000000..a28cc79 --- /dev/null +++ b/src/v2/storage/settings/SettingsStore.ts @@ -0,0 +1,16 @@ +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/src/v2/types/CalendarTypes.ts b/src/v2/types/CalendarTypes.ts index 0f6ef6e..7c24479 100644 --- a/src/v2/types/CalendarTypes.ts +++ b/src/v2/types/CalendarTypes.ts @@ -6,7 +6,7 @@ import { IWeekSchedule } from './ScheduleTypes'; export type SyncStatus = 'synced' | 'pending' | 'error'; -export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit'; +export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit' | 'Settings'; /** * CalendarEventType - Used by ICalendarEvent.type diff --git a/src/v2/types/SettingsTypes.ts b/src/v2/types/SettingsTypes.ts new file mode 100644 index 0000000..1b10728 --- /dev/null +++ b/src/v2/types/SettingsTypes.ts @@ -0,0 +1,72 @@ +/** + * Tenant Settings Type Definitions + * + * Settings are tenant-specific configuration that comes from the backend + * and is stored in IndexedDB for offline access. + */ + +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; +} + +/** + * Workweek settings section + */ +export interface IWorkweekSettings { + presets: Record; + defaultPreset: string; + firstDayOfWeek: number; // ISO: 1=Monday +} + +/** + * Grid display settings section + */ +export interface IGridSettings { + dayStartHour: number; + dayEndHour: number; + workStartHour: number; + workEndHour: number; + hourHeight: number; + snapInterval: number; +} + +/** + * Time format settings section + */ +export interface ITimeFormatSettings { + timezone: string; + locale: string; + use24HourFormat: boolean; +} + +/** + * View settings section + */ +export interface IViewSettings { + availableViews: string[]; + defaultView: string; +} + +/** + * ITenantSettings - Complete tenant configuration + * + * Single document stored in IndexedDB 'settings' store. + * Sections can be extended as needed without schema changes. + */ +export interface ITenantSettings extends ISync { + id: string; + lastModified?: string; + + workweek: IWorkweekSettings; + grid: IGridSettings; + timeFormat: ITimeFormatSettings; + views: IViewSettings; +} diff --git a/wwwroot/css/v2/calendar-v2-layout.css b/wwwroot/css/v2/calendar-v2-layout.css index 3ef668c..5dedc79 100644 --- a/wwwroot/css/v2/calendar-v2-layout.css +++ b/wwwroot/css/v2/calendar-v2-layout.css @@ -16,20 +16,78 @@ swp-calendar { /* Nav */ swp-calendar-nav { display: flex; - gap: 16px; - padding: 12px 16px; + 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; } +} + +/* Navigation group */ +swp-nav-group { + display: flex; + gap: 2px; } swp-nav-button { - padding: 8px 16px; + 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 { @@ -38,11 +96,12 @@ swp-week-info { swp-week-number { font-weight: 600; + font-size: 12px; display: block; } swp-date-range { - font-size: 12px; + font-size: 11px; color: var(--color-text-secondary); } } @@ -70,6 +129,8 @@ swp-time-axis { swp-header-spacer { border-bottom: 1px solid var(--color-border); + background: var(--color-surface); + z-index: 1; } swp-header-drawer { diff --git a/wwwroot/data/tenant-settings.json b/wwwroot/data/tenant-settings.json new file mode 100644 index 0000000..be14e4c --- /dev/null +++ b/wwwroot/data/tenant-settings.json @@ -0,0 +1,57 @@ +{ + "id": "tenant-settings", + "syncStatus": "synced", + "lastModified": "2025-12-15T10:00:00Z", + + "workweek": { + "presets": { + "standard": { + "id": "standard", + "workDays": [1, 2, 3, 4, 5], + "label": "Man-Fre" + }, + "compressed": { + "id": "compressed", + "workDays": [1, 2, 3, 4], + "label": "Man-Tor" + }, + "midweek": { + "id": "midweek", + "workDays": [3, 4, 5], + "label": "Ons-Fre" + }, + "weekend": { + "id": "weekend", + "workDays": [6, 7], + "label": "Weekend" + }, + "fullweek": { + "id": "fullweek", + "workDays": [1, 2, 3, 4, 5, 6, 7], + "label": "Alle dage" + } + }, + "defaultPreset": "standard", + "firstDayOfWeek": 1 + }, + + "grid": { + "dayStartHour": 6, + "dayEndHour": 22, + "workStartHour": 8, + "workEndHour": 17, + "hourHeight": 80, + "snapInterval": 15 + }, + + "timeFormat": { + "timezone": "Europe/Copenhagen", + "locale": "da-DK", + "use24HourFormat": true + }, + + "views": { + "availableViews": ["simple", "resource", "team", "department"], + "defaultView": "simple" + } +} diff --git a/wwwroot/v2.html b/wwwroot/v2.html index a3f57d2..c19150c 100644 --- a/wwwroot/v2.html +++ b/wwwroot/v2.html @@ -10,18 +10,36 @@
- Dag - Datoer - Resources - Teams - Departments + + + + + + + + + + + + + + + + + + - V2 + V2 Demo - - - Toggle + + Drawer