/** * TimeBlockCalendarOverlay - Renders time block overlays on the scheduler calendar * * Shows blocked time periods with visual styling: * - Hard blocks: Red with diagonal stripes, 50% opacity * - Soft blocks: Yellow with dotted border, 30% opacity * - Business blocks: Full-width across the lane * - Resource blocks: Only on matching resource lane */ import React, { useMemo, useState } from 'react'; import { BlockedDate, BlockType, BlockPurpose } from '../../types'; interface TimeBlockCalendarOverlayProps { blockedDates: BlockedDate[]; resourceId: string; viewDate: Date; zoomLevel: number; pixelsPerMinute: number; startHour: number; dayWidth: number; laneHeight: number; days: Date[]; onDayClick?: (day: Date) => void; } interface TimeBlockTooltipProps { block: BlockedDate; position: { x: number; y: number }; } const TimeBlockTooltip: React.FC = ({ block, position }) => { return (
{block.title}
{block.block_type === 'HARD' ? 'Hard Block' : 'Soft Block'} {block.all_day ? ' (All Day)' : ` (${block.start_time} - ${block.end_time})`}
); }; const TimeBlockCalendarOverlay: React.FC = ({ blockedDates, resourceId, viewDate, zoomLevel, pixelsPerMinute, startHour, dayWidth, laneHeight, days, onDayClick, }) => { const [hoveredBlock, setHoveredBlock] = useState<{ block: BlockedDate; position: { x: number; y: number } } | null>(null); // Filter blocks for this resource (includes business-level blocks where resource_id is null) const relevantBlocks = useMemo(() => { return blockedDates.filter( (block) => block.resource_id === null || block.resource_id === resourceId ); }, [blockedDates, resourceId]); // Calculate block positions for each day const blockOverlays = useMemo(() => { const overlays: Array<{ block: BlockedDate; left: number; width: number; dayIndex: number; }> = []; relevantBlocks.forEach((block) => { // Parse date string as local date, not UTC // "2025-12-06" should be Dec 6 in local timezone, not UTC const [year, month, dayNum] = block.date.split('-').map(Number); const blockDate = new Date(year, month - 1, dayNum); blockDate.setHours(0, 0, 0, 0); // Find which day this block falls on days.forEach((day, dayIndex) => { const dayStart = new Date(day); dayStart.setHours(0, 0, 0, 0); if (blockDate.getTime() === dayStart.getTime()) { let left: number; let width: number; if (block.all_day) { // Full day block left = dayIndex * dayWidth; width = dayWidth; } else if (block.start_time && block.end_time) { // Partial day block const [startHours, startMins] = block.start_time.split(':').map(Number); const [endHours, endMins] = block.end_time.split(':').map(Number); const startMinutes = (startHours - startHour) * 60 + startMins; const endMinutes = (endHours - startHour) * 60 + endMins; left = dayIndex * dayWidth + startMinutes * pixelsPerMinute * zoomLevel; width = (endMinutes - startMinutes) * pixelsPerMinute * zoomLevel; } else { // Default to full day if no times specified left = dayIndex * dayWidth; width = dayWidth; } overlays.push({ block, left, width, dayIndex, }); } }); }); return overlays; }, [relevantBlocks, days, dayWidth, pixelsPerMinute, zoomLevel, startHour]); const getBlockStyle = (blockType: BlockType, purpose: BlockPurpose, isBusinessLevel: boolean): React.CSSProperties => { const baseStyle: React.CSSProperties = { position: 'absolute', top: 0, height: '100%', pointerEvents: 'auto', cursor: 'default', zIndex: 5, // Ensure overlays are visible above grid lines }; // Business-level blocks (including business hours): Simple gray background // No fancy styling - just indicates "not available for booking" if (isBusinessLevel) { return { ...baseStyle, background: 'rgba(107, 114, 128, 0.25)', // Gray-500 at 25% opacity (more visible) }; } // Resource-level blocks: Purple (hard) / Cyan (soft) if (blockType === 'HARD') { return { ...baseStyle, background: `repeating-linear-gradient( -45deg, rgba(147, 51, 234, 0.25), rgba(147, 51, 234, 0.25) 5px, rgba(147, 51, 234, 0.4) 5px, rgba(147, 51, 234, 0.4) 10px )`, borderTop: '2px solid rgba(147, 51, 234, 0.7)', borderBottom: '2px solid rgba(147, 51, 234, 0.7)', }; } else { return { ...baseStyle, background: 'rgba(6, 182, 212, 0.15)', borderTop: '2px dashed rgba(6, 182, 212, 0.7)', borderBottom: '2px dashed rgba(6, 182, 212, 0.7)', }; } }; const handleMouseEnter = (e: React.MouseEvent, block: BlockedDate) => { setHoveredBlock({ block, position: { x: e.clientX, y: e.clientY }, }); }; const handleMouseMove = (e: React.MouseEvent) => { if (hoveredBlock) { setHoveredBlock({ ...hoveredBlock, position: { x: e.clientX, y: e.clientY }, }); } }; const handleMouseLeave = () => { setHoveredBlock(null); }; return ( <> {blockOverlays.map((overlay, index) => { const isBusinessLevel = overlay.block.resource_id === null; const style = getBlockStyle(overlay.block.block_type, overlay.block.purpose, isBusinessLevel); return (
handleMouseEnter(e, overlay.block)} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} onClick={() => onDayClick?.(days[overlay.dayIndex])} > {/* Only show badge for resource-level blocks */} {!isBusinessLevel && (
R
)}
); })} {/* Tooltip */} {hoveredBlock && ( )} ); }; export default TimeBlockCalendarOverlay;