diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx index 02c1c01..f1db89a 100644 --- a/src/pages/CalendarPage.tsx +++ b/src/pages/CalendarPage.tsx @@ -3,19 +3,23 @@ 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: 'work' | 'on_call' | 'entertainment' | 'system' + slot_type: SlotType estimated_duration: number scheduled_at: string // HH:mm started_at: string | null attended: boolean actual_duration: number | null - event_type: 'job' | 'entertainment' | 'system_event' | null + event_type: EventType | null event_data: Record | null priority: number - status: 'not_started' | 'ongoing' | 'deferred' | 'skipped' | 'paused' | 'finished' | 'aborted' + status: SlotStatus plan_id: number | null is_virtual: boolean created_at: string | null @@ -24,7 +28,7 @@ interface TimeSlotResponse { interface SchedulePlanResponse { id: number - slot_type: 'work' | 'on_call' | 'entertainment' | 'system' + slot_type: SlotType estimated_duration: number event_type: string | null event_data: Record | null @@ -37,6 +41,32 @@ interface SchedulePlanResponse { updated_at: string | null } +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 SLOT_TYPE_ICONS: Record = { work: '๐Ÿ’ผ', on_call: '๐Ÿ“ž', @@ -62,6 +92,21 @@ export default function CalendarPage() { 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 fetchSlots = async (date: string) => { setLoading(true) setError('') @@ -100,17 +145,9 @@ export default function CalendarPage() { 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 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}`] @@ -120,6 +157,121 @@ export default function CalendarPage() { 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 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 + payload.date = selectedDate + response = await api.post(`/calendar/edit?date=${selectedDate}&slot_id=${editingSlot.slot_id}`, payload) + } else { + // Create new slot + payload.date = selectedDate + response = await api.post('/calendar/schedule', 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 { + 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 { + await api.post(`/calendar/cancel?date=${selectedDate}&slot_id=${slot.slot_id}`) + 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 (
@@ -144,6 +296,18 @@ export default function CalendarPage() { {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 */} @@ -157,8 +321,18 @@ export default function CalendarPage() { /> + {!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...
@@ -196,6 +370,25 @@ export default function CalendarPage() { ๐Ÿ“ฃ {slot.event_data.event}
)} + {/* Action buttons for non-past, modifiable slots */} + {!isPastDate && (slot.status === 'not_started' || slot.status === 'deferred') && ( +
+ + +
+ )} ))} @@ -223,11 +416,135 @@ export default function CalendarPage() {
โฑ {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' && ( + + )} + +
+ + +
+
+
+ )} ) }