diff --git a/src/App.tsx b/src/App.tsx index 0e15d9b..0f10251 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import MonitorPage from '@/pages/MonitorPage' import ProposalsPage from '@/pages/ProposalsPage' import ProposalDetailPage from '@/pages/ProposalDetailPage' import UsersPage from '@/pages/UsersPage' +import CalendarPage from '@/pages/CalendarPage' import SupportDetailPage from '@/pages/SupportDetailPage' import MeetingDetailPage from '@/pages/MeetingDetailPage' import axios from 'axios' @@ -118,6 +119,7 @@ export default function App() { } /> } /> } /> + } /> {/* Legacy routes for backward compatibility */} } /> } /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index a07e3f7..7227acf 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -33,6 +33,7 @@ export default function Sidebar({ user, onLogout }: Props) { { to: '/', icon: '๐Ÿ“Š', label: 'Dashboard' }, { to: '/projects', icon: '๐Ÿ“', label: 'Projects' }, { to: '/proposals', icon: '๐Ÿ’ก', label: 'Proposals' }, + { to: '/calendar', icon: '๐Ÿ“…', label: 'Calendar' }, { to: '/notifications', icon: '๐Ÿ””', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') }, { to: '/monitor', icon: '๐Ÿ“ก', label: 'Monitor' }, ...(user.is_admin ? [ diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx new file mode 100644 index 0000000..02c1c01 --- /dev/null +++ b/src/pages/CalendarPage.tsx @@ -0,0 +1,233 @@ +import { useState, useEffect } from 'react' +import api from '@/services/api' +import dayjs from 'dayjs' + +// Types for Calendar entities +interface TimeSlotResponse { + slot_id: string // real int id or "plan-{planId}-{date}" for virtual + date: string + slot_type: 'work' | 'on_call' | 'entertainment' | 'system' + 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_data: Record | null + priority: number + status: 'not_started' | 'ongoing' | 'deferred' | 'skipped' | 'paused' | 'finished' | 'aborted' + plan_id: number | null + is_virtual: boolean + created_at: string | null + updated_at: string | null +} + +interface SchedulePlanResponse { + id: number + slot_type: 'work' | 'on_call' | 'entertainment' | 'system' + 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 +} + +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('') + + const fetchSlots = async (date: string) => { + setLoading(true) + setError('') + try { + const { data } = await api.get(`/calendar/day?date=${date}`) + setSlots(data) + } 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(data) + } 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(' ยท ') + } + + return ( +
+
+

๐Ÿ“… Calendar

+
+ + {/* Tab switcher */} +
+ + +
+ + {error &&
{error}
} + + {activeTab === 'daily' && ( + <> + {/* Date navigation */} +
+ + + + +
+ + {/* 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} +
+ )} +
+ ))} +
+ )} + + )} + + {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('_', ' ')}
} +
+
+ )) + )} +
+ )} +
+ ) +}