diff --git a/src/App.tsx b/src/App.tsx index ae7218e..c2b78a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,8 @@ import MonitorPage from '@/pages/MonitorPage' import ProposesPage from '@/pages/ProposesPage' import ProposeDetailPage from '@/pages/ProposeDetailPage' import UsersPage from '@/pages/UsersPage' +import SupportDetailPage from '@/pages/SupportDetailPage' +import MeetingDetailPage from '@/pages/MeetingDetailPage' import axios from 'axios' const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080 @@ -96,6 +98,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/pages/MeetingDetailPage.tsx b/src/pages/MeetingDetailPage.tsx new file mode 100644 index 0000000..08df180 --- /dev/null +++ b/src/pages/MeetingDetailPage.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import api from '@/services/api' +import { useAuth } from '@/hooks/useAuth' +import dayjs from 'dayjs' + +interface MeetingItem { + id: number + code: string | null + meeting_code: string | null + title: string + description: string | null + status: string + priority: string + project_id: number + project_code: string | null + milestone_id: number + milestone_code: string | null + reporter_id: number + meeting_time: string | null + scheduled_at: string | null + duration_minutes: number | null + participants: string[] + created_at: string + updated_at: string | null +} + +const STATUS_OPTIONS = ['scheduled', 'in_progress', 'completed', 'cancelled'] + +export default function MeetingDetailPage() { + const { meetingId } = useParams() + const navigate = useNavigate() + const { user } = useAuth() + const [meeting, setMeeting] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + const [editMode, setEditMode] = useState(false) + const [editForm, setEditForm] = useState({ + title: '', + description: '', + meeting_time: '', + duration_minutes: '', + }) + + const fetchMeeting = async () => { + try { + const { data } = await api.get(`/meetings/${meetingId}`) + setMeeting(data) + setEditForm({ + title: data.title, + description: data.description || '', + meeting_time: data.meeting_time || data.scheduled_at || '', + duration_minutes: data.duration_minutes ? String(data.duration_minutes) : '', + }) + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to load meeting') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchMeeting() + }, [meetingId]) + + const handleAttend = async () => { + if (!meeting) return + setSaving(true) + setMessage('') + try { + const { data } = await api.post(`/meetings/${meetingId}/attend`) + setMeeting(data) + setMessage('You have joined this meeting') + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to attend meeting') + } finally { + setSaving(false) + } + } + + const handleTransition = async (newStatus: string) => { + if (!meeting) return + setSaving(true) + setMessage('') + try { + const { data } = await api.patch(`/meetings/${meetingId}`, { + status: newStatus, + }) + setMeeting(data) + setMessage(`Status changed to ${newStatus}`) + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to update meeting status') + } finally { + setSaving(false) + } + } + + const handleSave = async () => { + if (!meeting) return + setSaving(true) + setMessage('') + try { + const payload: Record = {} + if (editForm.title.trim() !== meeting.title) payload.title = editForm.title.trim() + if ((editForm.description || '') !== (meeting.description || '')) payload.description = editForm.description || null + const currentTime = meeting.meeting_time || meeting.scheduled_at || '' + if (editForm.meeting_time !== currentTime) payload.meeting_time = editForm.meeting_time || null + const currentDuration = meeting.duration_minutes ? String(meeting.duration_minutes) : '' + if (editForm.duration_minutes !== currentDuration) { + payload.duration_minutes = editForm.duration_minutes ? Number(editForm.duration_minutes) : null + } + + if (Object.keys(payload).length === 0) { + setEditMode(false) + return + } + + const { data } = await api.patch(`/meetings/${meetingId}`, payload) + setMeeting(data) + setEditMode(false) + setMessage('Meeting updated') + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to update meeting') + } finally { + setSaving(false) + } + } + + const handleDelete = async () => { + if (!meeting) return + if (!confirm(`Delete meeting ${meeting.meeting_code || meeting.id}? This cannot be undone.`)) return + setSaving(true) + try { + await api.delete(`/meetings/${meetingId}`) + navigate(-1) + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to delete meeting') + setSaving(false) + } + } + + if (loading) return
Loading meeting...
+ if (!meeting) return
{message || 'Meeting not found'}
+ + const isParticipant = user && meeting.participants.includes(user.username) + const canAttend = user && !isParticipant && meeting.status !== 'completed' && meeting.status !== 'cancelled' + const availableTransitions = STATUS_OPTIONS.filter((s) => s !== meeting.status) + const scheduledTime = meeting.meeting_time || meeting.scheduled_at + + return ( +
+ + +
+

📅 {meeting.meeting_code || `#${meeting.id}`}

+
+ {meeting.status} + {meeting.project_code && Project: {meeting.project_code}} + {meeting.milestone_code && Milestone: {meeting.milestone_code}} +
+
+ + {message && ( +
+ {message} +
+ )} + +
+ {/* Main content */} +
+ {editMode ? ( +
+ +