Moving away from Azure Devops #1

Merged
Janus007 merged 113 commits from refac into master 2026-02-03 00:04:27 +01:00
13 changed files with 751 additions and 38 deletions
Showing only changes of commit ad2df353b5 - Show all commits

View file

@ -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
<!-- Simple view (kun date) -->
<swp-day-column data-date="2025-12-09"></swp-day-column>
<!-- Resource view (resource + date) -->
<swp-day-column
data-date="2025-12-09"
data-resource-id="EMP001">
</swp-day-column>
<!-- Team view (team + resource + date) -->
<swp-day-column
data-date="2025-12-09"
data-resource-id="EMP001"
data-team-id="team-1">
</swp-day-column>
```
### 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 |

View file

@ -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<IStore>();
builder.registerType(ScheduleOverrideStore).as<IStore>();
builder.registerType(AuditStore).as<IStore>();
builder.registerType(SettingsStore).as<IStore>();
// Entity services (for DataSeeder polymorphic array)
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
@ -139,6 +144,10 @@ export function createV2Container(): Container {
builder.registerType(DepartmentService).as<IEntityService<ISync>>();
builder.registerType(DepartmentService).as<DepartmentService>();
builder.registerType(SettingsService).as<IEntityService<ITenantSettings>>();
builder.registerType(SettingsService).as<IEntityService<ISync>>();
builder.registerType(SettingsService).as<SettingsService>();
// Repositories (for DataSeeder polymorphic array)
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
builder.registerType(MockEventRepository).as<IApiRepository<ISync>>();
@ -161,6 +170,9 @@ export function createV2Container(): Container {
builder.registerType(MockDepartmentRepository).as<IApiRepository<IDepartment>>();
builder.registerType(MockDepartmentRepository).as<IApiRepository<ISync>>();
builder.registerType(MockSettingsRepository).as<IApiRepository<ITenantSettings>>();
builder.registerType(MockSettingsRepository).as<IApiRepository<ISync>>();
// Audit service (listens to ENTITY_SAVED/DELETED events automatically)
builder.registerType(AuditService).as<AuditService>();

View file

@ -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
// ============================================

View file

@ -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<void> {
@ -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';
// 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';
// 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();
}
});
}

View file

@ -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<ITenantSettings> {
public readonly entityType: EntityType = 'Settings';
private readonly dataUrl = 'data/tenant-settings.json';
public async fetchAll(): Promise<ITenantSettings[]> {
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<ITenantSettings> {
throw new Error('MockSettingsRepository does not support sendCreate. Mock data is read-only.');
}
public async sendUpdate(_id: string, _updates: Partial<ITenantSettings>): Promise<ITenantSettings> {
throw new Error('MockSettingsRepository does not support sendUpdate. Mock data is read-only.');
}
public async sendDelete(_id: string): Promise<void> {
throw new Error('MockSettingsRepository does not support sendDelete. Mock data is read-only.');
}
}

View file

@ -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;

View file

@ -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<ITenantSettings> {
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<ITenantSettings | null> {
return this.get(TENANT_SETTINGS_ID);
}
/**
* Get workweek preset by ID
*/
async getWorkweekPreset(presetId: string): Promise<IWorkweekPreset | null> {
const settings = await this.getSettings();
if (!settings) return null;
return settings.workweek.presets[presetId] || null;
}
/**
* Get the default workweek preset
*/
async getDefaultWorkweekPreset(): Promise<IWorkweekPreset | null> {
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<IWorkweekPreset[]> {
const settings = await this.getSettings();
if (!settings) return [];
return Object.values(settings.workweek.presets);
}
}

View file

@ -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' });
}
}

View file

@ -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

View file

@ -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<string, IWorkweekPreset>;
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;
}

View file

@ -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 {

View file

@ -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"
}
}

View file

@ -10,18 +10,36 @@
<div class="calendar-wrapper">
<swp-calendar>
<swp-calendar-nav>
<swp-nav-button id="btn-day">Dag</swp-nav-button>
<swp-nav-button id="btn-simple">Datoer</swp-nav-button>
<swp-nav-button id="btn-resource">Resources</swp-nav-button>
<swp-nav-button id="btn-team">Teams</swp-nav-button>
<swp-nav-button id="btn-department">Departments</swp-nav-button>
<swp-week-info>
<swp-week-number>V2</swp-week-number>
<swp-date-range id="view-info"></swp-date-range>
</swp-week-info>
<!-- View switcher (small chips) -->
<swp-view-switcher>
<button class="view-chip active" data-view="simple">Datoer</button>
<button class="view-chip" data-view="day">Dag</button>
<button class="view-chip" data-view="resource">Resource</button>
<button class="view-chip" data-view="team">Team</button>
<button class="view-chip" data-view="department">Dept</button>
</swp-view-switcher>
<!-- Workweek preset dropdown -->
<select id="workweek-select" class="workweek-dropdown">
<option value="standard">Man-Fre</option>
<option value="compressed">Man-Tor</option>
<option value="midweek">Ons-Fre</option>
<option value="weekend">Weekend</option>
<option value="fullweek">Alle dage</option>
</select>
<!-- Navigation -->
<swp-nav-group>
<swp-nav-button id="btn-prev"></swp-nav-button>
<swp-nav-button id="btn-next"></swp-nav-button>
<swp-nav-button id="btn-drawer">Toggle</swp-nav-button>
</swp-nav-group>
<swp-week-info>
<swp-week-number>V2 Demo</swp-week-number>
<swp-date-range id="view-info"></swp-date-range>
</swp-week-info>
<swp-nav-button id="btn-drawer" class="btn-small">Drawer</swp-nav-button>
</swp-calendar-nav>
<swp-calendar-container>