import { useState, useEffect } from 'react' import api from '@/services/api' import dayjs from 'dayjs' // Types for Calendar entities type SlotType = 'work' | 'on_call' | 'entertainment' | 'system' type EventType = 'job' | 'entertainment' | 'system_event' type SlotStatus = 'not_started' | 'ongoing' | 'deferred' | 'skipped' | 'paused' | 'finished' | 'aborted' interface TimeSlotResponse { slot_id: string // real int id or "plan-{planId}-{date}" for virtual date: string slot_type: SlotType estimated_duration: number scheduled_at: string // HH:mm started_at: string | null attended: boolean actual_duration: number | null event_type: EventType | null event_data: Record | null priority: number status: SlotStatus plan_id: number | null is_virtual: boolean created_at: string | null updated_at: string | null } interface DayViewResponse { date: string user_id: number slots: TimeSlotResponse[] } interface SchedulePlanResponse { id: number slot_type: SlotType estimated_duration: number event_type: string | null event_data: Record | null at_time: string on_day: string | null on_week: number | null on_month: string | null is_active: boolean created_at: string updated_at: string | null } interface PlanListResponse { plans: SchedulePlanResponse[] } type Weekday = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' type MonthName = 'jan' | 'feb' | 'mar' | 'apr' | 'may' | 'jun' | 'jul' | 'aug' | 'sep' | 'oct' | 'nov' | 'dec' interface WorkloadWarning { period: string slot_type: string current: number minimum: number message: string } interface ScheduleResponse { slot: TimeSlotResponse warnings: WorkloadWarning[] } const SLOT_TYPES: { value: SlotType; label: string }[] = [ { value: 'work', label: 'Work' }, { value: 'on_call', label: 'On Call' }, { value: 'entertainment', label: 'Entertainment' }, { value: 'system', label: 'System' }, ] const EVENT_TYPES: { value: EventType; label: string }[] = [ { value: 'job', label: 'Job' }, { value: 'entertainment', label: 'Entertainment' }, { value: 'system_event', label: 'System Event' }, ] const WEEKDAYS: { value: Weekday; label: string }[] = [ { value: 'sun', label: 'Sunday' }, { value: 'mon', label: 'Monday' }, { value: 'tue', label: 'Tuesday' }, { value: 'wed', label: 'Wednesday' }, { value: 'thu', label: 'Thursday' }, { value: 'fri', label: 'Friday' }, { value: 'sat', label: 'Saturday' }, ] const MONTHS: { value: MonthName; label: string }[] = [ { value: 'jan', label: 'January' }, { value: 'feb', label: 'February' }, { value: 'mar', label: 'March' }, { value: 'apr', label: 'April' }, { value: 'may', label: 'May' }, { value: 'jun', label: 'June' }, { value: 'jul', label: 'July' }, { value: 'aug', label: 'August' }, { value: 'sep', label: 'September' }, { value: 'oct', label: 'October' }, { value: 'nov', label: 'November' }, { value: 'dec', label: 'December' }, ] const SLOT_TYPE_ICONS: Record = { work: '๐Ÿ’ผ', on_call: '๐Ÿ“ž', entertainment: '๐ŸŽฎ', system: 'โš™๏ธ', } const STATUS_CLASSES: Record = { not_started: 'status-open', ongoing: 'status-undergoing', deferred: 'status-pending', skipped: 'status-closed', paused: 'status-pending', finished: 'status-completed', aborted: 'status-closed', } export default function CalendarPage() { const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD')) const [slots, setSlots] = useState([]) const [plans, setPlans] = useState([]) const [loading, setLoading] = useState(false) const [activeTab, setActiveTab] = useState<'daily' | 'plans'>('daily') const [error, setError] = useState('') // Create/Edit slot modal state const [showSlotModal, setShowSlotModal] = useState(false) const [editingSlot, setEditingSlot] = useState(null) const [slotForm, setSlotForm] = useState({ slot_type: 'work' as SlotType, scheduled_at: '09:00', estimated_duration: 25, event_type: '' as string, event_data_code: '', event_data_event: '', priority: 50, }) const [slotSaving, setSlotSaving] = useState(false) const [warnings, setWarnings] = useState([]) const [showPlanModal, setShowPlanModal] = useState(false) const [editingPlan, setEditingPlan] = useState(null) const [planSaving, setPlanSaving] = useState(false) const [planForm, setPlanForm] = useState({ slot_type: 'work' as SlotType, estimated_duration: 25, at_time: '09:00', on_day: '' as Weekday | '', on_week: '' as string, on_month: '' as MonthName | '', event_type: '' as string, event_data_code: '', event_data_event: '', }) const fetchSlots = async (date: string) => { setLoading(true) setError('') try { const { data } = await api.get(`/calendar/day?date=${date}`) setSlots(Array.isArray(data?.slots) ? data.slots : []) } catch (err: any) { setError(err.response?.data?.detail || 'Failed to load calendar') setSlots([]) } finally { setLoading(false) } } const fetchPlans = async () => { try { const { data } = await api.get('/calendar/plans') setPlans(Array.isArray(data?.plans) ? data.plans : []) } catch (err: any) { console.error('Failed to load plans:', err) } } useEffect(() => { fetchSlots(selectedDate) fetchPlans() }, []) useEffect(() => { if (activeTab === 'daily') { fetchSlots(selectedDate) } }, [selectedDate]) const handleDateChange = (e: React.ChangeEvent) => { setSelectedDate(e.target.value) } const goToday = () => setSelectedDate(dayjs().format('YYYY-MM-DD')) const goPrev = () => setSelectedDate(dayjs(selectedDate).subtract(1, 'day').format('YYYY-MM-DD')) const goNext = () => setSelectedDate(dayjs(selectedDate).add(1, 'day').format('YYYY-MM-DD')) const formatPlanSchedule = (plan: SchedulePlanResponse) => { const parts: string[] = [`at ${plan.at_time}`] if (plan.on_day) parts.push(`on ${plan.on_day}`) if (plan.on_week) parts.push(`week ${plan.on_week}`) if (plan.on_month) parts.push(`in ${plan.on_month}`) return parts.join(' ยท ') } // --- Slot create/edit --- const openCreateSlotModal = () => { setEditingSlot(null) setSlotForm({ slot_type: 'work', scheduled_at: '09:00', estimated_duration: 25, event_type: '', event_data_code: '', event_data_event: '', priority: 50, }) setWarnings([]) setError('') setShowSlotModal(true) } const openEditSlotModal = (slot: TimeSlotResponse) => { setEditingSlot(slot) setSlotForm({ slot_type: slot.slot_type, scheduled_at: slot.scheduled_at, estimated_duration: slot.estimated_duration, event_type: slot.event_type || '', event_data_code: slot.event_data?.code || '', event_data_event: slot.event_data?.event || '', priority: slot.priority, }) setWarnings([]) setError('') setShowSlotModal(true) } const buildEventData = () => { if (!slotForm.event_type) return null if (slotForm.event_type === 'job') { return slotForm.event_data_code ? { type: 'Task', code: slotForm.event_data_code } : null } if (slotForm.event_type === 'system_event') { return slotForm.event_data_event ? { event: slotForm.event_data_event } : null } return null } const buildPlanEventData = () => { if (!planForm.event_type) return null if (planForm.event_type === 'job') { return planForm.event_data_code ? { type: 'Task', code: planForm.event_data_code } : null } if (planForm.event_type === 'system_event') { return planForm.event_data_event ? { event: planForm.event_data_event } : null } return null } const openCreatePlanModal = () => { setEditingPlan(null) setPlanForm({ slot_type: 'work', estimated_duration: 25, at_time: '09:00', on_day: '', on_week: '', on_month: '', event_type: '', event_data_code: '', event_data_event: '', }) setError('') setShowPlanModal(true) } const openEditPlanModal = (plan: SchedulePlanResponse) => { setEditingPlan(plan) setPlanForm({ slot_type: plan.slot_type, estimated_duration: plan.estimated_duration, at_time: plan.at_time.slice(0, 5), on_day: (plan.on_day?.toLowerCase() as Weekday | undefined) || '', on_week: plan.on_week ? String(plan.on_week) : '', on_month: (plan.on_month?.toLowerCase() as MonthName | undefined) || '', event_type: plan.event_type || '', event_data_code: plan.event_data?.code || '', event_data_event: plan.event_data?.event || '', }) setError('') setShowPlanModal(true) } const handleSavePlan = async () => { setPlanSaving(true) setError('') try { const payload: any = { slot_type: planForm.slot_type, estimated_duration: planForm.estimated_duration, at_time: planForm.at_time, on_day: planForm.on_day || null, on_week: planForm.on_week ? Number(planForm.on_week) : null, on_month: planForm.on_month || null, event_type: planForm.event_type || null, event_data: buildPlanEventData(), } if (editingPlan) { await api.patch(`/calendar/plans/${editingPlan.id}`, payload) } else { await api.post('/calendar/plans', payload) } setShowPlanModal(false) fetchPlans() if (activeTab === 'daily') { fetchSlots(selectedDate) } } catch (err: any) { setError(err.response?.data?.detail || 'Save plan failed') } finally { setPlanSaving(false) } } const handleSaveSlot = async () => { setSlotSaving(true) setError('') setWarnings([]) try { const payload: any = { slot_type: slotForm.slot_type, scheduled_at: slotForm.scheduled_at, estimated_duration: slotForm.estimated_duration, priority: slotForm.priority, event_type: slotForm.event_type || null, event_data: buildEventData(), } let response: any if (editingSlot) { // Edit existing slot if (editingSlot.is_virtual) { response = await api.patch(`/calendar/slots/virtual/${editingSlot.slot_id}`, payload) } else if (editingSlot.slot_id) { response = await api.patch(`/calendar/slots/${editingSlot.slot_id}`, payload) } else { throw new Error('Missing slot identifier for edit') } } else { // Create new slot payload.date = selectedDate response = await api.post('/calendar/slots', payload) } // Check for warnings in response if (response.data?.warnings && response.data.warnings.length > 0) { setWarnings(response.data.warnings) } setShowSlotModal(false) fetchSlots(selectedDate) } catch (err: any) { const detail = err.response?.data?.detail if (typeof detail === 'string' && detail.toLowerCase().includes('overlap')) { setError(`โš ๏ธ Overlap conflict: ${detail}`) } else if (detail && typeof detail === 'object') { const message = typeof detail.message === 'string' ? detail.message : 'Save failed' const conflicts = Array.isArray(detail.conflicts) ? detail.conflicts : [] if (conflicts.length > 0) { const summary = conflicts .map((conflict: any) => `${conflict.slot_type || 'slot'} at ${conflict.scheduled_at || 'unknown time'}`) .join(', ') setError(`โš ๏ธ ${message}: ${summary}`) } else { setError(`โš ๏ธ ${message}`) } } else { setError(detail || 'Save failed') } } finally { setSlotSaving(false) } } // --- Slot cancel --- const handleCancelSlot = async (slot: TimeSlotResponse) => { if (!confirm(`Cancel this ${slot.slot_type} slot at ${slot.scheduled_at}?`)) return setError('') try { if (slot.is_virtual) { await api.post(`/calendar/slots/virtual/${slot.slot_id}/cancel`) } else if (slot.slot_id) { await api.post(`/calendar/slots/${slot.slot_id}/cancel`) } else { throw new Error('Missing slot identifier for cancel') } fetchSlots(selectedDate) } catch (err: any) { setError(err.response?.data?.detail || 'Cancel failed') } } // --- Plan cancel --- const handleCancelPlan = async (plan: SchedulePlanResponse) => { if (!confirm(`Cancel plan #${plan.id}? This won't affect past materialized slots.`)) return setError('') try { await api.post(`/calendar/plans/${plan.id}/cancel`) fetchPlans() } catch (err: any) { setError(err.response?.data?.detail || 'Cancel plan failed') } } // Check if a date is in the past const isPastDate = dayjs(selectedDate).isBefore(dayjs().startOf('day')) return (

๐Ÿ“… Calendar

{/* Tab switcher */}
{error &&
{error}
} {/* Workload warnings banner */} {warnings.length > 0 && (
โš ๏ธ Workload Warnings:
    {warnings.map((w, i) => (
  • {w.message} ({w.period} {w.slot_type}: {w.current}/{w.minimum} min)
  • ))}
)} {activeTab === 'daily' && ( <> {/* Date navigation */}
{!isPastDate && ( )}
{/* Deferred slots notice */} {slots.some(s => s.status === 'deferred') && (
โณ Some slots are deferred โ€” they were postponed due to scheduling conflicts or agent unavailability.
)} {/* Slot list */} {loading ? (
Loading...
) : slots.length === 0 ? (
No slots scheduled for {dayjs(selectedDate).format('MMMM D, YYYY')}.
) : (
{slots.map((slot) => (
{SLOT_TYPE_ICONS[slot.slot_type] || '๐Ÿ“‹'} {slot.slot_type.replace('_', ' ')} {slot.status.replace('_', ' ')} {slot.is_virtual && plan} {slot.scheduled_at}
โฑ {slot.estimated_duration} min โšก Priority: {slot.priority} {slot.event_type && ๐Ÿ“Œ {slot.event_type.replace('_', ' ')}}
{slot.event_data && slot.event_data.code && (
๐Ÿ”— {slot.event_data.code}
)} {slot.event_data && slot.event_data.event && (
๐Ÿ“ฃ {slot.event_data.event}
)} {/* Action buttons for non-past, modifiable slots */} {!isPastDate && (slot.status === 'not_started' || slot.status === 'deferred') && (
)}
))}
)} )} {activeTab === 'plans' && ( <>
{plans.length === 0 ? (
No schedule plans configured.
) : ( plans.map((plan) => (
{SLOT_TYPE_ICONS[plan.slot_type] || '๐Ÿ“‹'} {plan.slot_type.replace('_', ' ')} {!plan.is_active && inactive} Plan #{plan.id}
๐Ÿ”„ {formatPlanSchedule(plan)}
โฑ {plan.estimated_duration} min
{plan.event_type &&
๐Ÿ“Œ {plan.event_type.replace('_', ' ')}
}
{plan.is_active && (
)}
)) )}
)} {/* Create/Edit Slot Modal */} {showSlotModal && (
setShowSlotModal(false)}>
e.stopPropagation()}>

{editingSlot ? 'Edit Slot' : 'New Slot'}

Date: {dayjs(selectedDate).format('MMMM D, YYYY')}

{slotForm.event_type === 'job' && ( )} {slotForm.event_type === 'system_event' && ( )}
)} {showPlanModal && (
setShowPlanModal(false)}>
e.stopPropagation()}>

{editingPlan ? 'Edit Plan' : 'New Plan'}

{planForm.event_type === 'job' && ( )} {planForm.event_type === 'system_event' && ( )}
)}
) }