diff --git a/client/src/App.jsx b/client/src/App.jsx index 5878520..22e7061 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -26,6 +26,7 @@ import MeetingRoom from "./pages/MeetingRoom.jsx"; import MeetingDetails from "./pages/MeetingDetails.jsx"; import TeamMembers from "./pages/TeamMembers.jsx"; import Profile from "./pages/Profile.jsx"; +import Calendar from "./pages/Calendar.jsx"; // --- Components --- import ProtectedRoute from "./components/ProtectedRoute.jsx"; @@ -240,6 +241,15 @@ const App = () => { } /> + + + + } + /> + {/* ✅ Fallback route — send unknown routes to Home */} } /> diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index 2ecb1a3..9563fa3 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -8,6 +8,7 @@ import { X, LayoutDashboard, Calendar, + CalendarDays, Building2, Search, Bell, @@ -204,6 +205,7 @@ const Navbar = () => { const appLinks = [ { label: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { label: "Meetings", href: "/meetings", icon: Calendar }, + { label: "Calendar", href: "/calendar", icon: CalendarDays }, { label: "Team Members", href: "/team-members", icon: Users }, { label: "Organizations", href: "/select-role", icon: Building2 }, { label: "AI Search", href: "/ai-search", icon: Search }, diff --git a/client/src/pages/Calendar.jsx b/client/src/pages/Calendar.jsx new file mode 100644 index 0000000..0dd5998 --- /dev/null +++ b/client/src/pages/Calendar.jsx @@ -0,0 +1,931 @@ +import React, { useState, useEffect, useContext } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import { toast } from "react-toastify"; +import AppContent from "../context/AppContent"; +import Navbar from "../components/Navbar.jsx"; +import { + Calendar as CalendarIcon, + ChevronLeft, + ChevronRight, + Filter, + Plus, + Video, + ExternalLink, + MapPin, + Clock, + Briefcase, + Layers, + CheckCircle, + HelpCircle, + AlertCircle, + Loader2, + Inbox, + X, +} from "lucide-react"; + +const Calendar = () => { + const navigate = useNavigate(); + const { backendUrl } = useContext(AppContent); + + // States + const [meetings, setMeetings] = useState([]); + const [loading, setLoading] = useState(true); + const [currentDate, setCurrentDate] = useState(new Date()); + const [view, setView] = useState("month"); // 'month' | 'week' | 'day' + const [selectedMeeting, setSelectedMeeting] = useState(null); + + // Filter States + const [statusFilter, setStatusFilter] = useState("all"); + const [typeFilter, setTypeFilter] = useState("all"); + const [orgFilter, setOrgFilter] = useState("all"); + + // Fetch Meetings + useEffect(() => { + const fetchMeetings = async () => { + setLoading(true); + try { + const { data } = await axios.get(`${backendUrl}/api/meetings/all`, { + withCredentials: true, + }); + if (data.success) { + setMeetings(data.meetings || []); + } else { + toast.error(data.message || "Failed to fetch meetings."); + } + } catch (err) { + console.error("Fetch meetings error:", err); + toast.error("Error loading calendar data."); + } finally { + setLoading(false); + } + }; + + fetchMeetings(); + }, [backendUrl]); + + // Handle outside click to close modal + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === "Escape") { + setSelectedMeeting(null); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + // Filter Logic + const filteredMeetings = meetings.filter((meeting) => { + const matchesStatus = + statusFilter === "all" || meeting.status === statusFilter; + const matchesType = + typeFilter === "all" || meeting.meetingType === typeFilter; + + // Filter by organization: if user has organization or meeting has one + let matchesOrg = true; + if (orgFilter !== "all") { + if (orgFilter === "personal") { + matchesOrg = !meeting.organization; + } else { + matchesOrg = + meeting.organization === orgFilter || + (meeting.organization?._id && + meeting.organization._id === orgFilter) || + (meeting.organization?.name && + meeting.organization.name === orgFilter); + } + } + + return matchesStatus && matchesType && matchesOrg; + }); + + // Extract unique organizations for filters + const uniqueOrgs = Array.from( + new Set( + meetings + .map((m) => m.organization?.name || m.organization) + .filter(Boolean), + ), + ); + + // Navigation helpers + const handlePrev = () => { + const newDate = new Date(currentDate); + if (view === "month") { + newDate.setMonth(currentDate.getMonth() - 1); + } else if (view === "week") { + newDate.setDate(currentDate.getDate() - 7); + } else { + newDate.setDate(currentDate.getDate() - 1); + } + setCurrentDate(newDate); + }; + + const handleNext = () => { + const newDate = new Date(currentDate); + if (view === "month") { + newDate.setMonth(currentDate.getMonth() + 1); + } else if (view === "week") { + newDate.setDate(currentDate.getDate() + 7); + } else { + newDate.setDate(currentDate.getDate() + 1); + } + setCurrentDate(newDate); + }; + + const handleToday = () => { + setCurrentDate(new Date()); + }; + + // Date Parsing Helpers + const isSameDay = (d1, d2) => { + return ( + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() + ); + }; + + // Status Styling Utilities + const getStatusStyle = (status) => { + switch (status) { + case "completed": + return { + bg: "bg-emerald-50 hover:bg-emerald-100/80 border-emerald-200 text-emerald-800", + dot: "bg-emerald-500", + badge: "bg-emerald-50 text-emerald-700 border-emerald-100", + icon: CheckCircle, + }; + case "processing": + return { + bg: "bg-amber-50 hover:bg-amber-100/80 border-amber-200 text-amber-800 animate-pulse", + dot: "bg-amber-500", + badge: "bg-amber-50 text-amber-700 border-amber-100", + icon: Loader2, + }; + case "failed": + return { + bg: "bg-rose-50 hover:bg-rose-100/80 border-rose-200 text-rose-800", + dot: "bg-rose-500", + badge: "bg-rose-50 text-rose-700 border-rose-100", + icon: AlertCircle, + }; + default: // uploaded / upcoming + return { + bg: "bg-blue-50 hover:bg-blue-100/80 border-blue-200 text-blue-800", + dot: "bg-blue-500", + badge: "bg-blue-50 text-blue-700 border-blue-100", + icon: HelpCircle, + }; + } + }; + + // Get start/end dates for calculations + const getDaysInMonth = (date) => { + const year = date.getFullYear(); + const month = date.getMonth(); + return new Date(year, month + 1, 0).getDate(); + }; + + // Week days calculator (Sunday to Saturday) + const getWeekDays = (date) => { + const startOfWeek = new Date(date); + const day = date.getDay(); + const diff = date.getDate() - day; // adjust to Sunday + startOfWeek.setDate(diff); + + const week = []; + for (let i = 0; i < 7; i++) { + const wDay = new Date(startOfWeek); + wDay.setDate(startOfWeek.getDate() + i); + week.push(wDay); + } + return week; + }; + + // Build grid of days for month view + const buildMonthGrid = () => { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + + const totalDays = getDaysInMonth(currentDate); + const firstDayIndex = new Date(year, month, 1).getDay(); // Sunday=0, Monday=1... + + const prevMonthDate = new Date(year, month, 0); + const prevMonthDays = prevMonthDate.getDate(); + + const cells = []; + + // Prepend previous month's trailing days + for (let i = firstDayIndex - 1; i >= 0; i--) { + cells.push({ + date: new Date(year, month - 1, prevMonthDays - i), + isCurrentMonth: false, + }); + } + + // Current month days + for (let i = 1; i <= totalDays; i++) { + cells.push({ + date: new Date(year, month, i), + isCurrentMonth: true, + }); + } + + // Append next month's leading days to complete grid rows + const remainingCells = 42 - cells.length; + for (let i = 1; i <= remainingCells; i++) { + cells.push({ + date: new Date(year, month + 1, i), + isCurrentMonth: false, + }); + } + + return cells; + }; + + // Convert "HH:MM" String offset to minutes + const parseTimeToMinutes = (timeStr) => { + if (!timeStr) return 9 * 60; // default 09:00 AM (540 mins) + const parts = timeStr.split(":"); + if (parts.length < 2) return 9 * 60; + return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); + }; + + // Format Time Slot helper (e.g. "09:30 AM") + const formatTimeSlot = (timeStr) => { + if (!timeStr) return "09:00 AM"; + const parts = timeStr.split(":"); + if (parts.length < 2) return timeStr; + const hours = parseInt(parts[0], 10); + const minutes = parts[1]; + const ampm = hours >= 12 ? "PM" : "AM"; + const displayHours = hours % 12 || 12; + return `${displayHours}:${minutes} ${ampm}`; + }; + + // Format Date string title + const getHeaderDateString = () => { + if (view === "month") { + return currentDate.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + } else if (view === "week") { + const week = getWeekDays(currentDate); + const start = week[0]; + const end = week[6]; + if (start.getMonth() === end.getMonth()) { + return `${start.toLocaleDateString("en-US", { month: "short" })} ${start.getFullYear()}`; + } else { + return `${start.toLocaleDateString("en-US", { month: "short" })} - ${end.toLocaleDateString("en-US", { month: "short" })} ${end.getFullYear()}`; + } + } else { + return currentDate.toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }); + } + }; + + return ( +
+ + +
+ {/* Navigation & Toolbar Header */} +
+
+

+ + Calendar & Schedule +

+

+ Visualize your upcoming schedule, review minutes, and manage + meeting timelines. +

+
+ + +
+ + {/* Calendar Nav + Filters Panel */} +
+
+ {/* Prev/Next buttons */} +
+
+ + + +
+

+ {getHeaderDateString()} +

+
+ + {/* View togglers */} +
+ {["month", "week", "day"].map((v) => ( + + ))} +
+
+ + {/* Filters Row */} +
+
+ + Filters: +
+ + {/* Date filter (Jump to Date) */} +
+ + Date: + + { + if (e.target.value) { + setCurrentDate(new Date(e.target.value + "T00:00:00")); + } + }} + className="bg-transparent border-none text-slate-700 outline-none cursor-pointer text-xs font-semibold" + /> +
+ + {/* Status filter */} + + + {/* Type Filter */} + + + {/* Org Filter */} + +
+
+ + {/* Loading Spinner */} + {loading ? ( +
+ + + Fetching meetings... + +
+ ) : filteredMeetings.length === 0 ? ( + /* Empty State */ +
+
+ +
+

+ No Scheduled Meetings +

+

+ There are no meetings scheduled matching the selected filters. + Change filters or create a new meeting. +

+ +
+ ) : ( + /* CALENDAR VIEWS */ +
+ {/* MONTH VIEW */} + {view === "month" && ( +
+ {/* Weekday headers */} +
+
Sun
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
+ + {/* Days Grid */} +
+ {buildMonthGrid().map((cell, idx) => { + const dayEvents = filteredMeetings.filter((m) => + isSameDay(new Date(m.date), cell.date), + ); + const isToday = isSameDay(new Date(), cell.date); + + return ( +
+ {/* Day Number */} +
+ + {cell.date.getDate()} + +
+ + {/* Events list */} +
+ {dayEvents.slice(0, 3).map((event) => { + const style = getStatusStyle(event.status); + return ( + + ); + })} + {dayEvents.length > 3 && ( +
+ + {dayEvents.length - 3} more +
+ )} +
+
+ ); + })} +
+
+ )} + + {/* WEEK VIEW */} + {view === "week" && ( +
+ {/* Week Headers */} +
+
Time
+ {getWeekDays(currentDate).map((day, idx) => { + const isToday = isSameDay(new Date(), day); + return ( +
+ + {day.toLocaleDateString("en-US", { + weekday: "short", + })} + + + {day.getDate()} + +
+ ); + })} +
+ + {/* Weekly Time Grid */} +
+
+ {/* Y-Axis Hourly labels */} +
+ {Array.from({ length: 13 }).map((_, h) => ( +
+ {h + 8 < 12 + ? `${h + 8}:00 AM` + : h + 8 === 12 + ? "12:00 PM" + : `${h + 8 - 12}:00 PM`} +
+ ))} +
+ + {/* Weekday columns mapping slots */} + {getWeekDays(currentDate).map((day, dIdx) => { + const dayEvents = filteredMeetings.filter((m) => + isSameDay(new Date(m.date), day), + ); + + return ( +
+ {/* Hour Dividers in background */} + {Array.from({ length: 13 }).map((_, h) => ( +
+ ))} + + {/* Placed Events */} + {dayEvents.map((event) => { + const style = getStatusStyle(event.status); + const minutesFromMidnight = parseTimeToMinutes( + event.time, + ); + + // Map to start hour (08:00 = 480 mins). Hour size = 50px (50px / 60 mins = 0.83px/min) + const startMins = 8 * 60; + const top = Math.max( + 0, + (minutesFromMidnight - startMins) * (50 / 60), + ); + const duration = event.duration || 60; + const height = Math.max(35, duration * (50 / 60)); + + return ( + + ); + })} +
+ ); + })} +
+
+
+ )} + + {/* DAY VIEW */} + {view === "day" && ( +
+ {/* Left side Timeline Column */} +
+
+

+ Timeline +

+ + Hourly Schedule + +
+ +
+ {/* Hourly scale */} +
+ {Array.from({ length: 13 }).map((_, h) => ( +
+ {h + 8 < 12 + ? `${h + 8}:00 AM` + : h + 8 === 12 + ? "12:00 PM" + : `${h + 8 - 12}:00 PM`} +
+ ))} +
+ + {/* Timeline grid content */} +
+ {/* Hour Grid Dividers */} + {Array.from({ length: 13 }).map((_, h) => ( +
+ ))} + + {/* Day events mapped */} + {filteredMeetings + .filter((m) => isSameDay(new Date(m.date), currentDate)) + .map((event) => { + const style = getStatusStyle(event.status); + const minutesFromMidnight = parseTimeToMinutes( + event.time, + ); + + const startMins = 8 * 60; + const top = Math.max( + 0, + (minutesFromMidnight - startMins) * (50 / 60), + ); + const duration = event.duration || 60; + const height = Math.max(40, duration * (50 / 60)); + + return ( + + ); + })} +
+
+
+ + {/* Right side chronological list card */} +
+

+ List Agenda +

+ +
+ {filteredMeetings + .filter((m) => isSameDay(new Date(m.date), currentDate)) + .sort( + (a, b) => + parseTimeToMinutes(a.time) - + parseTimeToMinutes(b.time), + ) + .map((event) => { + const style = getStatusStyle(event.status); + const StatusIcon = style.icon; + + return ( +
setSelectedMeeting(event)} + className="bg-white border border-slate-200/80 hover:border-slate-300 p-4 rounded-xl shadow-2xs hover:shadow-xs transition-all cursor-pointer flex items-start gap-3" + > +
+ +
+
+

+ {event.title} +

+

+ {formatTimeSlot(event.time)} +

+ {event.organization && ( +

+ + {event.organization.name || + event.organization} +

+ )} +
+
+ ); + })} +
+
+
+ )} +
+ )} +
+ + {/* QUICK DETAILS MODAL */} + {selectedMeeting && ( +
+
e.stopPropagation()} + className="w-full max-w-md bg-white border border-slate-200 rounded-2xl shadow-xl p-6 relative overflow-hidden" + > + {/* Close button */} + + + {/* Header info */} +
+ + {selectedMeeting.status} + +

+ {selectedMeeting.title} +

+
+ + {/* Detail rows */} +
+
+ +
+
+ {new Date(selectedMeeting.date).toLocaleDateString( + "en-US", + { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }, + )} +
+
+ {formatTimeSlot(selectedMeeting.time)} ( + {selectedMeeting.duration || 60} mins) +
+
+
+ + {selectedMeeting.organization && ( +
+ +
+ + Organization: + {" "} + {selectedMeeting.organization.name || + selectedMeeting.organization} +
+
+ )} + +
+ +
+ Type:{" "} + + {selectedMeeting.meetingType || "Conference"} + +
+
+ + {selectedMeeting.location && ( +
+ +
+ + Location: + {" "} + {selectedMeeting.location} +
+
+ )} + + {selectedMeeting.description && ( +
+

+ Description +

+

+ {selectedMeeting.description} +

+
+ )} +
+ + {/* Quick Actions buttons */} +
+ {selectedMeeting.recordingType === "live" && + selectedMeeting.status !== "failed" && ( + + )} + +
+
+
+ )} +
+ ); +}; + +export default Calendar; diff --git a/server/controllers/meetingController.js b/server/controllers/meetingController.js index 7e4a9b7..f534ab4 100644 --- a/server/controllers/meetingController.js +++ b/server/controllers/meetingController.js @@ -587,7 +587,8 @@ export const getAllMeetings = async (req, res) => { const meetings = await Meeting.find({ uploadedBy: userId }) .sort({ createdAt: -1 }) - .select("title summary structuredMoM createdAt date meetingType status"); + .select("title summary structuredMoM createdAt date meetingType status time duration recordingType organization") + .populate("organization", "name"); return res.status(200).json({ success: true, meetings }); } catch (error) {