diff --git a/src/managers/WorkHoursManager.ts b/src/managers/WorkHoursManager.ts new file mode 100644 index 0000000..1aa8288 --- /dev/null +++ b/src/managers/WorkHoursManager.ts @@ -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 { + const workHoursMap = new Map(); + + 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 { + 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()]; + } +} \ No newline at end of file diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts index 642e67f..b1a0faa 100644 --- a/src/renderers/ColumnRenderer.ts +++ b/src/renderers/ColumnRenderer.ts @@ -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})`); + } + } + } + } /** diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index c4afe26..e0d229b 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -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 diff --git a/wwwroot/css/calendar-base-css.css b/wwwroot/css/calendar-base-css.css index dbeed1d..b033710 100644 --- a/wwwroot/css/calendar-base-css.css +++ b/wwwroot/css/calendar-base-css.css @@ -35,7 +35,7 @@ --color-grid-line: #e0e0e0; --color-grid-line-light: rgba(0, 0, 0, 0.05); --color-hour-line: rgba(0, 0, 0, 0.2); - --color-work-hours: rgba(242, 242, 242, 1); + --color-work-hours: rgba(255, 255, 255, 0.9); --color-current-time: #ff0000; /* Event colors - Updated with month-view-expanded.html color scheme */ @@ -53,6 +53,8 @@ /* UI colors */ --color-background: #ffffff; --color-surface: #f5f5f5; + --color-event-grid: #ffffff; + --color-non-work-hours: #ff980038; --color-text: #333333; --color-text-secondary: #666666; --color-border: #e0e0e0; diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index a501425..0bd2a48 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -111,7 +111,7 @@ swp-hour-marker { swp-hour-marker::before { content: ''; position: absolute; - top: -1px; + top: 0px; left: 50px; width: calc(100vw - 60px); /* Full viewport width minus time-axis width */ height: 1px; @@ -180,7 +180,6 @@ swp-calendar-header::-webkit-scrollbar-track { swp-day-header { - padding: 12px; pointer-events: auto; /* Ensure header clicks work despite parent scrollbar */ text-align: center; border-right: 1px solid var(--color-grid-line); @@ -189,6 +188,7 @@ swp-day-header { flex-direction: column; align-items: center; justify-content: center; + padding-top: 7px; } swp-day-header:last-child { @@ -239,23 +239,25 @@ swp-resource-name { swp-day-name { display: block; font-weight: 500; - font-size: 0.875rem; + font-size: 12px; color: var(--color-text-secondary); } swp-day-date { display: block; - font-size: 1.25rem; + font-size: 20px; font-weight: 600; margin-top: 4px; + height: 41px; + } swp-day-header[data-today="true"] swp-day-date { color: var(--color-primary); background: rgba(33, 150, 243, 0.1); border-radius: 50%; - width: 36px; - height: 36px; + width: 41px; + height: 41px; display: flex; align-items: center; justify-content: center; @@ -333,16 +335,18 @@ swp-time-grid { height: calc((var(--day-end-hour) - var(--day-start-hour)) * var(--hour-height)); } +/* Global work hours overlay - now disabled, replaced by per-column overlays */ swp-time-grid::before { content: ''; position: absolute; - top: calc((var(--work-start-hour) - var(--day-start-hour)) * var(--hour-height)); - height: calc((var(--work-end-hour) - var(--work-start-hour)) * var(--hour-height)); + top: 0; + height: 0; left: 0; right: 0; - background: var(--color-work-hours); + background: transparent; min-width: calc(var(--grid-columns, 7) * var(--day-column-min-width)); pointer-events: none; + display: none; /* Disabled - using per-column overlays instead */ } /* Grid lines */ @@ -379,6 +383,50 @@ swp-day-column { position: relative; border-right: 1px solid var(--color-grid-line); min-width: var(--day-column-min-width); + background: var(--color-event-grid); +} + +/* Per-column non-work hours overlays */ +/* Before work overlay */ +swp-day-column::before { + content: ''; + position: absolute; + left: 0; + right: 0; + background: var(--color-non-work-hours); + pointer-events: none; + z-index: 2; + + /* Before work period - from day start to work start */ + top: 0; + height: var(--before-work-height, 0px); + opacity: 0.3; +} + +/* After work overlay */ +swp-day-column::after { + content: ''; + position: absolute; + left: 0; + right: 0; + background: var(--color-non-work-hours); + pointer-events: none; + z-index: 2; + + /* After work period - from work end to day end */ + top: var(--after-work-top, 100%); + bottom: 0; + opacity: 0.3; +} + +/* Full day overlay when day is off */ +swp-day-column[data-work-hours="off"] { + background: var(--color-non-work-hours); +} + +swp-day-column[data-work-hours="off"]::before, +swp-day-column[data-work-hours="off"]::after { + display: none; } swp-day-column:last-child { @@ -390,6 +438,7 @@ swp-resource-column { position: relative; border-right: 1px solid var(--color-grid-line); min-width: var(--day-column-min-width); + background: var(--color-event-grid); } swp-resource-column:last-child {