import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Typography, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, CircularProgress, Alert, Button, TextField, TableSortLabel, Select, MenuItem, FormControl, InputLabel, Chip, OutlinedInput, IconButton } from '@mui/material'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import DeleteIcon from '@mui/icons-material/Delete'; import { getMeetings, updateMeeting, deleteMeeting, createMeeting } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import NewMeetingDialog from '../components/NewMeetingDialog'; // Helper function for sorting function descendingComparator(a, b, orderBy) { if (b[orderBy] < a[orderBy]) return -1; if (b[orderBy] > a[orderBy]) return 1; return 0; } function getComparator(order, orderBy) { return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy); } const DashboardPage = () => { const navigate = useNavigate(); const { user } = useAuth(); const [meetings, setMeetings] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [deletingIds, setDeletingIds] = useState(new Set()); const [isModalOpen, setIsModalOpen] = useState(false); // State for filtering and searching const [topicSearch, setTopicSearch] = useState(''); const [ownerSearch, setOwnerSearch] = useState(''); const [statusFilter, setStatusFilter] = useState([]); // State for sorting const [order, setOrder] = useState('asc'); const [orderBy, setOrderBy] = useState('meeting_date'); const fetchMeetings = useCallback(async () => { try { const data = await getMeetings(); setMeetings(data); } catch (err) { setError('Could not fetch meetings.'); } finally { setLoading(false); } }, []); useEffect(() => { fetchMeetings(); }, [fetchMeetings]); const handleCreateMeeting = async (topic, meetingDate) => { try { const newMeeting = await createMeeting(topic, new Date(meetingDate).toISOString()); setIsModalOpen(false); navigate(`/meeting/${newMeeting.id}`); } catch (err) { throw new Error(err.response?.data?.error || "Failed to create meeting."); } }; const handleSortRequest = (property) => { const isAsc = orderBy === property && order === 'asc'; setOrder(isAsc ? 'desc' : 'asc'); setOrderBy(property); }; const handleStatusChange = async (meetingId, newStatus) => { try { await updateMeeting(meetingId, { status: newStatus }); fetchMeetings(); } catch (err) { setError(`Failed to update status for meeting ${meetingId}.`); } }; const handleDelete = async (meetingId) => { if (window.confirm('Are you sure you want to delete this meeting?')) { setDeletingIds(prev => new Set(prev).add(meetingId)); try { await deleteMeeting(meetingId); fetchMeetings(); } catch (err) { setError(`Failed to delete meeting ${meetingId}.`); } finally { setDeletingIds(prev => { const newSet = new Set(prev); newSet.delete(meetingId); return newSet; }); } } }; const uniqueStatuses = useMemo(() => { const statuses = new Set(meetings.map(m => m.status)); return Array.from(statuses); }, [meetings]); const filteredAndSortedMeetings = useMemo(() => { let filtered = meetings.filter(meeting => { const topicMatch = meeting.topic.toLowerCase().includes(topicSearch.toLowerCase()); const ownerMatch = meeting.owner_name ? meeting.owner_name.toLowerCase().includes(ownerSearch.toLowerCase()) : ownerSearch === ''; const statusMatch = statusFilter.length === 0 || statusFilter.includes(meeting.status); return topicMatch && ownerMatch && statusMatch; }); return filtered.sort(getComparator(order, orderBy)); }, [meetings, topicSearch, ownerSearch, statusFilter, order, orderBy]); if (loading) { return ; } const headCells = [ { id: 'topic', label: 'Topic' }, { id: 'owner_name', label: 'Owner' }, { id: 'meeting_date', label: 'Meeting Date' }, { id: 'status', label: 'Status' }, { id: 'action_item_count', label: 'Action Items' }, { id: 'created_at', label: 'Created At' }, ]; const statusColorMap = { 'Completed': 'success', 'In Progress': 'info', 'To Do': 'warning', 'Failed': 'error', }; const allPossibleStatuses = ['To Do', 'In Progress', 'Completed', 'Failed']; return ( Dashboard setIsModalOpen(false)} onCreate={handleCreateMeeting} /> {error && setError('')}>{error}} setTopicSearch(e.target.value)} sx={{ flex: '1 1 40%' }} /> setOwnerSearch(e.target.value)} sx={{ flex: '1 1 30%' }} /> Filter by Status {headCells.map((headCell) => ( handleSortRequest(headCell.id)} > {headCell.label} ))} Actions {filteredAndSortedMeetings.map((meeting) => { const isOwnerOrAdmin = user && (user.role === 'admin' || String(user.id) === String(meeting.created_by_id)); const isDeleting = deletingIds.has(meeting.id); let taipeiTime = 'N/A'; if (meeting.created_at) { taipeiTime = new Date(meeting.created_at).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } return ( {meeting.topic} {meeting.owner_name || 'N/A'} {meeting.meeting_date ? new Date(meeting.meeting_date).toLocaleDateString() : 'N/A'} {meeting.action_item_count} {taipeiTime} {isOwnerOrAdmin ? ( handleDelete(meeting.id)} color="error" disabled={isDeleting}> {isDeleting ? : } ) : ( // Invisible placeholder to maintain alignment )} ); })}
); }; export default DashboardPage;