/** * Time Blocks Management Page * * Allows business owners/managers to manage time blocks for business closures, * holidays, and resource-specific unavailability. */ import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { TimeBlockListItem, BlockType, RecurrenceType, } from '../types'; import { useTimeBlocks, useCreateTimeBlock, useUpdateTimeBlock, useDeleteTimeBlock, useToggleTimeBlock, useHolidays, usePendingReviews, useApproveTimeBlock, useDenyTimeBlock, } from '../hooks/useTimeBlocks'; import { useResources } from '../hooks/useResources'; import Portal from '../components/Portal'; import YearlyBlockCalendar from '../components/time-blocks/YearlyBlockCalendar'; import TimeBlockCreatorModal from '../components/time-blocks/TimeBlockCreatorModal'; import { Building2, User, Plus, Pencil, Trash2, AlertTriangle, Clock, Calendar, CalendarDays, Ban, AlertCircle, Power, PowerOff, HourglassIcon, CheckCircle, XCircle, MessageSquare, ChevronDown, ChevronUp, } from 'lucide-react'; type TimeBlockTab = 'business' | 'resource' | 'calendar'; const RECURRENCE_TYPE_LABELS: Record = { NONE: 'One-time', WEEKLY: 'Weekly', MONTHLY: 'Monthly', YEARLY: 'Yearly', HOLIDAY: 'Holiday', }; const BLOCK_TYPE_LABELS: Record = { HARD: 'Hard Block', SOFT: 'Soft Block', }; const TimeBlocks: React.FC = () => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('business'); const [isModalOpen, setIsModalOpen] = useState(false); const [editingBlock, setEditingBlock] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [isPendingReviewsExpanded, setIsPendingReviewsExpanded] = useState(true); const [reviewingBlock, setReviewingBlock] = useState(null); const [reviewNotes, setReviewNotes] = useState(''); // Fetch data (include inactive blocks so users can re-enable them) const { data: businessBlocks = [], isLoading: businessLoading, } = useTimeBlocks({ level: 'business' }); const { data: resourceBlocks = [], isLoading: resourceLoading, } = useTimeBlocks({ level: 'resource' }); const { data: holidays = [] } = useHolidays('US'); const { data: resources = [] } = useResources(); const { data: pendingReviews } = usePendingReviews(); // Mutations const createBlock = useCreateTimeBlock(); const updateBlock = useUpdateTimeBlock(); const deleteBlock = useDeleteTimeBlock(); const toggleBlock = useToggleTimeBlock(); const approveBlock = useApproveTimeBlock(); const denyBlock = useDenyTimeBlock(); // Current blocks based on tab const currentBlocks = activeTab === 'business' ? businessBlocks : resourceBlocks; const isLoading = activeTab === 'business' ? businessLoading : resourceLoading; // Group resource blocks by resource const resourceBlocksGrouped = useMemo(() => { const groups: Record = {}; resourceBlocks.forEach((block) => { const resourceId = block.resource || 'unassigned'; if (!groups[resourceId]) groups[resourceId] = []; groups[resourceId].push(block); }); return groups; }, [resourceBlocks]); // Modal handlers const openCreateModal = () => { setEditingBlock(null); setIsModalOpen(true); }; const openEditModal = (block: TimeBlockListItem) => { setEditingBlock(block); setIsModalOpen(true); }; const closeModal = () => { setIsModalOpen(false); setEditingBlock(null); }; const handleDelete = async (id: string) => { try { await deleteBlock.mutateAsync(id); setDeleteConfirmId(null); } catch (error) { console.error('Failed to delete time block:', error); } }; const handleToggle = async (id: string) => { try { await toggleBlock.mutateAsync(id); } catch (error) { console.error('Failed to toggle time block:', error); } }; const handleApprove = async (id: string) => { try { await approveBlock.mutateAsync({ id, notes: reviewNotes }); setReviewingBlock(null); setReviewNotes(''); } catch (error) { console.error('Failed to approve time block:', error); } }; const handleDeny = async (id: string) => { try { await denyBlock.mutateAsync({ id, notes: reviewNotes }); setReviewingBlock(null); setReviewNotes(''); } catch (error) { console.error('Failed to deny time block:', error); } }; // Render block type badge const renderBlockTypeBadge = (type: BlockType) => ( {type === 'HARD' ? : } {BLOCK_TYPE_LABELS[type]} ); // Render recurrence type badge const renderRecurrenceBadge = (type: RecurrenceType) => ( {type === 'HOLIDAY' ? ( ) : type === 'NONE' ? ( ) : ( )} {RECURRENCE_TYPE_LABELS[type]} ); return (
{/* Header */}

{t('timeBlocks.title', 'Time Blocks')}

{t('timeBlocks.subtitle', 'Manage business closures, holidays, and resource unavailability')}

{/* Pending Reviews Section */} {pendingReviews && pendingReviews.count > 0 && (
{isPendingReviewsExpanded && (
{pendingReviews.pending_blocks.map((block) => (
setReviewingBlock(block)} >
{block.title} {renderBlockTypeBadge(block.block_type)} {renderRecurrenceBadge(block.recurrence_type)}
{block.resource_name && ( {block.resource_name} )} {block.created_by_name && ( Requested by {block.created_by_name} )} {block.pattern_display && ( {block.pattern_display} )}
))}
)}
)} {/* Tabs */}
{/* Tab Content */} {isLoading ? (
) : (
{activeTab === 'business' && ( <> {/* Business Blocks Info */}

{t('timeBlocks.businessInfo', 'Business blocks apply to all resources. Use these for holidays, company closures, and business-wide events.')}

{/* Business Blocks List */} {businessBlocks.length === 0 ? (

{t('timeBlocks.noBusinessBlocks', 'No Business Blocks')}

{t('timeBlocks.noBusinessBlocksDesc', 'Add holidays and business closures to prevent bookings during those times.')}

) : (
{businessBlocks.map((block) => ( ))}
{t('timeBlocks.titleCol', 'Title')} {t('timeBlocks.typeCol', 'Type')} {t('timeBlocks.patternCol', 'Pattern')} {t('timeBlocks.actionsCol', 'Actions')}
{block.title} {!block.is_active && ( Inactive )}
{renderBlockTypeBadge(block.block_type)}
{renderRecurrenceBadge(block.recurrence_type)} {block.pattern_display && ( {block.pattern_display} )}
)} )} {activeTab === 'resource' && ( <> {/* Resource Blocks Info */}

{t('timeBlocks.resourceInfo', 'Resource blocks apply to specific staff or equipment. Use these for vacations, maintenance, or personal time.')}

{/* Resource Blocks List */} {resourceBlocks.length === 0 ? (

{t('timeBlocks.noResourceBlocks', 'No Resource Blocks')}

{t('timeBlocks.noResourceBlocksDesc', 'Add time blocks for specific resources to manage their availability.')}

) : (
{resources.map((resource) => { const blocks = resourceBlocksGrouped[resource.id] || []; if (blocks.length === 0) return null; return (

{resource.name}

{blocks.map((block) => ( ))}
{block.title} {!block.is_active && ( Inactive )}
{renderBlockTypeBadge(block.block_type)}
{renderRecurrenceBadge(block.recurrence_type)} {block.pattern_display && ( {block.pattern_display} )}
); })}
)} )} {activeTab === 'calendar' && (
{ // Find the block and open edit modal const block = [...businessBlocks, ...resourceBlocks].find(b => b.id === blockId); if (block) { openEditModal(block); } }} />
)}
)} {/* Create/Edit Modal */} { try { if (editingBlock) { await updateBlock.mutateAsync({ id: editingBlock.id, updates: data }); } else { // Handle array of blocks (multiple holidays) const blocks = Array.isArray(data) ? data : [data]; for (const block of blocks) { await createBlock.mutateAsync(block); } } closeModal(); } catch (error) { console.error('Failed to save time block:', error); } }} isSubmitting={createBlock.isPending || updateBlock.isPending} editingBlock={editingBlock} holidays={holidays} resources={resources} isResourceLevel={activeTab === 'resource'} /> {/* Delete Confirmation Modal */} {deleteConfirmId && (

{t('timeBlocks.deleteConfirmTitle', 'Delete Time Block?')}

{t('timeBlocks.deleteConfirmDesc', 'This action cannot be undone.')}

)} {/* Review Modal */} {reviewingBlock && (

{t('timeBlocks.reviewRequest', 'Review Time Off Request')}

{t('timeBlocks.reviewRequestDesc', 'Approve or deny this time off request')}

{/* Block Details */}
{t('timeBlocks.titleCol', 'Title')} {reviewingBlock.title}
{reviewingBlock.resource_name && (
{t('timeBlocks.resource', 'Resource')} {reviewingBlock.resource_name}
)}
{t('timeBlocks.typeCol', 'Type')} {renderBlockTypeBadge(reviewingBlock.block_type)}
{/* Schedule Details Section */}
{t('timeBlocks.scheduleDetails', 'Schedule Details')} {/* Recurrence Type */}
{t('timeBlocks.patternCol', 'Pattern')} {renderRecurrenceBadge(reviewingBlock.recurrence_type)}
{/* One-time block dates */} {reviewingBlock.recurrence_type === 'NONE' && reviewingBlock.start_date && (
{t('timeBlocks.dates', 'Date(s)')} {reviewingBlock.start_date === reviewingBlock.end_date ? ( new Date(reviewingBlock.start_date + 'T00:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }) ) : ( <> {new Date(reviewingBlock.start_date + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} {' - '} {new Date((reviewingBlock.end_date || reviewingBlock.start_date) + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} )}
)} {/* Weekly: Days of week */} {reviewingBlock.recurrence_type === 'WEEKLY' && reviewingBlock.recurrence_pattern?.days_of_week && (
{t('timeBlocks.daysOfWeek', 'Days')} {reviewingBlock.recurrence_pattern.days_of_week .map(d => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d]) .join(', ')}
)} {/* Monthly: Days of month */} {reviewingBlock.recurrence_type === 'MONTHLY' && reviewingBlock.recurrence_pattern?.days_of_month && (
{t('timeBlocks.daysOfMonth', 'Days of Month')} {reviewingBlock.recurrence_pattern.days_of_month.join(', ')}
)} {/* Yearly: Month and day */} {reviewingBlock.recurrence_type === 'YEARLY' && reviewingBlock.recurrence_pattern?.month && (
{t('timeBlocks.yearlyDate', 'Annual Date')} {['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][reviewingBlock.recurrence_pattern.month - 1]} {reviewingBlock.recurrence_pattern.day}
)} {/* Recurrence period (start/end) for recurring blocks */} {reviewingBlock.recurrence_type !== 'NONE' && (reviewingBlock.recurrence_start || reviewingBlock.recurrence_end) && (
{t('timeBlocks.effectivePeriod', 'Effective Period')} {reviewingBlock.recurrence_start ? ( new Date(reviewingBlock.recurrence_start + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) ) : 'No start date'} {' - '} {reviewingBlock.recurrence_end ? ( new Date(reviewingBlock.recurrence_end + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) ) : 'Ongoing'}
)} {/* Time range if not all-day */}
{t('timeBlocks.timeRange', 'Time')} {reviewingBlock.all_day !== false ? ( t('timeBlocks.allDay', 'All Day') ) : ( <> {reviewingBlock.start_time?.slice(0, 5)} - {reviewingBlock.end_time?.slice(0, 5)} )}
{reviewingBlock.description && (
{t('timeBlocks.description', 'Description')}

{reviewingBlock.description}

)} {reviewingBlock.created_by_name && (
{t('timeBlocks.requestedBy', 'Requested by')} {reviewingBlock.created_by_name}
)}
{/* Notes */}