From ee46593a5aaf9d6ed147d95f4b284bed7472a131 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 15 Dec 2025 22:53:44 +0100 Subject: [PATCH] Adds dynamic header hiding for date groupings Introduces hideHeader option for date grouping configurations Enables suppressing date headers in specific views like day view Improves calendar view flexibility by conditionally rendering headers --- src/v2/core/CalendarOrchestrator.ts | 2 +- src/v2/core/IGroupingRenderer.ts | 3 ++ src/v2/core/ViewConfig.ts | 1 + src/v2/demo/DemoApp.ts | 56 +++++++++++++++++++++++++-- src/v2/features/date/DateRenderer.ts | 7 ++++ wwwroot/css/v2/calendar-v2-layout.css | 46 ++++++++++++++++++++++ wwwroot/data/tenant-settings.json | 8 ++-- wwwroot/v2.html | 13 ++++++- 8 files changed, 126 insertions(+), 10 deletions(-) diff --git a/src/v2/core/CalendarOrchestrator.ts b/src/v2/core/CalendarOrchestrator.ts index 4fd0e56..933e8a5 100644 --- a/src/v2/core/CalendarOrchestrator.ts +++ b/src/v2/core/CalendarOrchestrator.ts @@ -43,7 +43,7 @@ export class CalendarOrchestrator { // Resolve belongsTo relations (e.g., team.resourceIds) const { parentChildMap, childType } = await this.resolveBelongsTo(viewConfig.groupings, filter); - const context: IRenderContext = { headerContainer, columnContainer, filter, parentChildMap, childType }; + const context: IRenderContext = { headerContainer, columnContainer, filter, groupings: viewConfig.groupings, parentChildMap, childType }; // Clear headerContainer.innerHTML = ''; diff --git a/src/v2/core/IGroupingRenderer.ts b/src/v2/core/IGroupingRenderer.ts index 0dd31fa..a1bc507 100644 --- a/src/v2/core/IGroupingRenderer.ts +++ b/src/v2/core/IGroupingRenderer.ts @@ -1,7 +1,10 @@ +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) } diff --git a/src/v2/core/ViewConfig.ts b/src/v2/core/ViewConfig.ts index 9b4334c..c964562 100644 --- a/src/v2/core/ViewConfig.ts +++ b/src/v2/core/ViewConfig.ts @@ -15,4 +15,5 @@ export interface GroupingConfig { 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/src/v2/demo/DemoApp.ts b/src/v2/demo/DemoApp.ts index 47c1746..f0e7e22 100644 --- a/src/v2/demo/DemoApp.ts +++ b/src/v2/demo/DemoApp.ts @@ -14,14 +14,16 @@ import { EventPersistenceManager } from '../managers/EventPersistenceManager'; import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer'; import { AuditService } from '../storage/audit/AuditService'; import { SettingsService } from '../storage/settings/SettingsService'; +import { ResourceService } from '../storage/resources/ResourceService'; 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 currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple'; private workweekPreset: IWorkweekPreset | null = null; + private selectedResourceIds: string[] = []; constructor( private orchestrator: CalendarOrchestrator, @@ -37,7 +39,8 @@ export class DemoApp { private headerDrawerRenderer: HeaderDrawerRenderer, private eventPersistenceManager: EventPersistenceManager, private auditService: AuditService, - private settingsService: SettingsService + private settingsService: SettingsService, + private resourceService: ResourceService ) {} async init(): Promise { @@ -88,6 +91,9 @@ export class DemoApp { this.setupDrawerToggle(); this.setupViewSwitching(); + // Setup resource selector for picker view + await this.setupResourceSelector(); + // Initial render this.render(); } @@ -109,7 +115,7 @@ export class DemoApp { templateId: 'day', groupings: [ { type: 'resource', values: ['EMP001', 'EMP002'], idProperty: 'resourceId' }, - { type: 'date', values: today, idProperty: 'date', derivedFrom: 'start' } + { type: 'date', values: today, idProperty: 'date', derivedFrom: 'start', hideHeader: true } ] }; @@ -149,6 +155,15 @@ export class DemoApp { { type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' } ] }; + + case 'picker': + return { + templateId: 'picker', + groupings: [ + { type: 'resource', values: this.selectedResourceIds, idProperty: 'resourceId' }, + { type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' } + ] + }; } } @@ -177,6 +192,7 @@ export class DemoApp { const view = (chip as HTMLElement).dataset.view as typeof this.currentView; if (view) { this.currentView = view; + this.updateSelectorVisibility(); this.render(); } }); @@ -199,4 +215,38 @@ export class DemoApp { this.headerDrawerManager.toggle(); }; } + + private async setupResourceSelector(): Promise { + const resources = await this.resourceService.getAll(); + const container = document.querySelector('.resource-checkboxes') as HTMLElement; + if (!container) return; + + // Clear existing + container.innerHTML = ''; + + // Create checkboxes for each resource + resources.forEach(r => { + const label = document.createElement('label'); + label.innerHTML = ` + + ${r.displayName} + `; + container.appendChild(label); + }); + + // Default: all selected + this.selectedResourceIds = resources.map(r => r.id); + + // Event listener for checkbox changes + container.addEventListener('change', () => { + const checked = container.querySelectorAll('input:checked') as NodeListOf; + this.selectedResourceIds = Array.from(checked).map(cb => cb.value); + this.render(); + }); + } + + private updateSelectorVisibility(): void { + const selector = document.querySelector('swp-resource-selector'); + selector?.classList.toggle('hidden', this.currentView !== 'picker'); + } } diff --git a/src/v2/features/date/DateRenderer.ts b/src/v2/features/date/DateRenderer.ts index 57f75f7..4f1cfad 100644 --- a/src/v2/features/date/DateRenderer.ts +++ b/src/v2/features/date/DateRenderer.ts @@ -10,6 +10,10 @@ export class DateRenderer implements IRenderer { 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; @@ -32,6 +36,9 @@ export class DateRenderer implements IRenderer { if (resourceId) { header.dataset.resourceId = resourceId; } + if (hideHeader) { + header.dataset.hidden = 'true'; + } header.innerHTML = ` ${this.dateService.getDayName(date, 'short')} ${date.getDate()} diff --git a/wwwroot/css/v2/calendar-v2-layout.css b/wwwroot/css/v2/calendar-v2-layout.css index 5dedc79..2bb9ab8 100644 --- a/wwwroot/css/v2/calendar-v2-layout.css +++ b/wwwroot/css/v2/calendar-v2-layout.css @@ -68,6 +68,48 @@ swp-view-switcher { &: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; @@ -289,6 +331,10 @@ swp-day-header { font-size: 24px; font-weight: 300; } + + &[data-hidden="true"] { + display: none; + } } /* Scrollable content */ diff --git a/wwwroot/data/tenant-settings.json b/wwwroot/data/tenant-settings.json index be14e4c..7509d65 100644 --- a/wwwroot/data/tenant-settings.json +++ b/wwwroot/data/tenant-settings.json @@ -12,13 +12,13 @@ }, "compressed": { "id": "compressed", - "workDays": [1, 2, 3, 4], - "label": "Man-Tor" + "workDays": [1, 2, 3], + "label": "Man-Ons" }, "midweek": { "id": "midweek", - "workDays": [3, 4, 5], - "label": "Ons-Fre" + "workDays": [4, 5], + "label": "Tors-Fre" }, "weekend": { "id": "weekend", diff --git a/wwwroot/v2.html b/wwwroot/v2.html index c19150c..28e558d 100644 --- a/wwwroot/v2.html +++ b/wwwroot/v2.html @@ -15,15 +15,24 @@ + + + +