Adds work hours management with day-specific overrides

Introduces work hours management with weekly default and date-specific overrides to support per-column scheduling.

Calculates and applies non-work hour overlays to columns.
This allows users to visualize working and non-working times.

Adjusts event rendering to prevent overlap with horizontal timelines.
This commit is contained in:
Janus Knudsen 2025-08-22 22:57:35 +02:00
parent 18c12cd3e6
commit a3ed03ff44
5 changed files with 252 additions and 13 deletions

View file

@ -0,0 +1,160 @@
// Work hours management for per-column scheduling
import { DateCalculator } from '../utils/DateCalculator';
import { CalendarConfig } from '../core/CalendarConfig';
/**
* Work hours for a specific day
*/
export interface DayWorkHours {
start: number; // Hour (0-23)
end: number; // Hour (0-23)
}
/**
* Work schedule configuration
*/
export interface WorkScheduleConfig {
weeklyDefault: {
monday: DayWorkHours | 'off';
tuesday: DayWorkHours | 'off';
wednesday: DayWorkHours | 'off';
thursday: DayWorkHours | 'off';
friday: DayWorkHours | 'off';
saturday: DayWorkHours | 'off';
sunday: DayWorkHours | 'off';
};
dateOverrides: {
[dateString: string]: DayWorkHours | 'off'; // YYYY-MM-DD format
};
}
/**
* Manages work hours scheduling with weekly defaults and date-specific overrides
*/
export class WorkHoursManager {
private config: CalendarConfig;
private dateCalculator: DateCalculator;
private workSchedule: WorkScheduleConfig;
constructor(config: CalendarConfig) {
this.config = config;
this.dateCalculator = new DateCalculator(config);
// Default work schedule - will be loaded from JSON later
this.workSchedule = {
weeklyDefault: {
monday: { start: 9, end: 17 },
tuesday: { start: 9, end: 17 },
wednesday: { start: 9, end: 17 },
thursday: { start: 9, end: 17 },
friday: { start: 9, end: 15 },
saturday: 'off',
sunday: 'off'
},
dateOverrides: {
'2025-01-20': { start: 10, end: 16 },
'2025-01-21': { start: 8, end: 14 },
'2025-01-22': 'off'
}
};
}
/**
* Get work hours for a specific date
*/
getWorkHoursForDate(date: Date): DayWorkHours | 'off' {
const dateString = this.dateCalculator.formatISODate(date);
// Check for date-specific override first
if (this.workSchedule.dateOverrides[dateString]) {
return this.workSchedule.dateOverrides[dateString];
}
// Fall back to weekly default
const dayName = this.getDayName(date);
return this.workSchedule.weeklyDefault[dayName];
}
/**
* Get work hours for multiple dates (used by GridManager)
*/
getWorkHoursForDateRange(dates: Date[]): Map<string, DayWorkHours | 'off'> {
const workHoursMap = new Map<string, DayWorkHours | 'off'>();
dates.forEach(date => {
const dateString = this.dateCalculator.formatISODate(date);
const workHours = this.getWorkHoursForDate(date);
workHoursMap.set(dateString, workHours);
});
return workHoursMap;
}
/**
* Calculate CSS custom properties for non-work hour overlays (before and after work)
*/
calculateNonWorkHoursStyle(workHours: DayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null {
if (workHours === 'off') {
return null; // Full day will be colored via CSS background
}
const gridSettings = this.config.getGridSettings();
const dayStartHour = gridSettings.dayStartHour;
const dayEndHour = gridSettings.dayEndHour;
const hourHeight = gridSettings.hourHeight;
// Before work: from day start to work start
const beforeWorkHeight = (workHours.start - dayStartHour) * hourHeight;
// After work: from work end to day end
const afterWorkTop = (workHours.end - dayStartHour) * hourHeight;
return {
beforeWorkHeight: Math.max(0, beforeWorkHeight),
afterWorkTop: Math.max(0, afterWorkTop)
};
}
/**
* Calculate CSS custom properties for work hours overlay (legacy - for backward compatibility)
*/
calculateWorkHoursStyle(workHours: DayWorkHours | 'off'): { top: number; height: number } | null {
if (workHours === 'off') {
return null;
}
const gridSettings = this.config.getGridSettings();
const dayStartHour = gridSettings.dayStartHour;
const hourHeight = gridSettings.hourHeight;
const top = (workHours.start - dayStartHour) * hourHeight;
const height = (workHours.end - workHours.start) * hourHeight;
return { top, height };
}
/**
* Load work schedule from JSON (future implementation)
*/
async loadWorkSchedule(jsonData: WorkScheduleConfig): Promise<void> {
this.workSchedule = jsonData;
}
/**
* Get current work schedule configuration
*/
getWorkSchedule(): WorkScheduleConfig {
return this.workSchedule;
}
/**
* Convert Date to day name key
*/
private getDayName(date: Date): keyof WorkScheduleConfig['weeklyDefault'] {
const dayNames: (keyof WorkScheduleConfig['weeklyDefault'])[] = [
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'
];
return dayNames[date.getDay()];
}
}

View file

@ -3,6 +3,7 @@
import { CalendarConfig } from '../core/CalendarConfig';
import { ResourceCalendarData } from '../types/CalendarTypes';
import { DateCalculator } from '../utils/DateCalculator';
import { WorkHoursManager } from '../managers/WorkHoursManager';
/**
* Interface for column rendering strategies
@ -25,23 +26,28 @@ export interface ColumnRenderContext {
*/
export class DateColumnRenderer implements ColumnRenderer {
private dateCalculator!: DateCalculator;
private workHoursManager!: WorkHoursManager;
render(columnContainer: HTMLElement, context: ColumnRenderContext): void {
const { currentWeek, config } = context;
// Initialize date calculator
// Initialize date calculator and work hours manager
this.dateCalculator = new DateCalculator(config);
this.workHoursManager = new WorkHoursManager(config);
const dates = this.dateCalculator.getWorkWeekDates(currentWeek);
const dateSettings = config.getDateViewSettings();
const daysToShow = dates.slice(0, dateSettings.weekDays);
console.log('DateColumnRenderer: About to render', daysToShow.length, 'date columns');
console.log('DateColumnRenderer: About to render', daysToShow.length, 'date columns with work hours');
daysToShow.forEach((date) => {
const column = document.createElement('swp-day-column');
(column as any).dataset.date = this.dateCalculator.formatISODate(date);
// Apply work hours styling
this.applyWorkHoursToColumn(column, date);
const eventsLayer = document.createElement('swp-events-layer');
column.appendChild(eventsLayer);
@ -49,6 +55,28 @@ export class DateColumnRenderer implements ColumnRenderer {
});
}
private applyWorkHoursToColumn(column: HTMLElement, date: Date): void {
const workHours = this.workHoursManager.getWorkHoursForDate(date);
if (workHours === 'off') {
// No work hours - mark as off day (full day will be colored)
(column as any).dataset.workHours = 'off';
console.log(`DateColumnRenderer: ${this.dateCalculator.formatISODate(date)} is an off day`);
} else {
// Calculate and apply non-work hours overlays (before and after work)
const nonWorkStyle = this.workHoursManager.calculateNonWorkHoursStyle(workHours);
if (nonWorkStyle) {
// Before work overlay (::before pseudo-element)
column.style.setProperty('--before-work-height', `${nonWorkStyle.beforeWorkHeight}px`);
// After work overlay (::after pseudo-element)
column.style.setProperty('--after-work-top', `${nonWorkStyle.afterWorkTop}px`);
console.log(`DateColumnRenderer: ${this.dateCalculator.formatISODate(date)} non-work overlays - before: ${nonWorkStyle.beforeWorkHeight}px, after: ${nonWorkStyle.afterWorkTop}px (work hours: ${workHours.start}-${workHours.end})`);
}
}
}
}
/**

View file

@ -69,7 +69,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const position = this.calculateEventPosition(event, config);
eventElement.style.position = 'absolute';
eventElement.style.top = `${position.top + 1}px`;
eventElement.style.height = `${position.height - 1}px`;
eventElement.style.height = `${position.height - 3}px`; //adjusted so bottom does not cover horizontal time lines.
// Color is now handled by CSS classes based on data-type attribute