diff --git a/src/Modules/FileTracking/components/Archive.jsx b/src/Modules/FileTracking/components/Archive.jsx index 34ded20e2..cae3757fd 100644 --- a/src/Modules/FileTracking/components/Archive.jsx +++ b/src/Modules/FileTracking/components/Archive.jsx @@ -31,7 +31,7 @@ import { MagnifyingGlass, ArrowClockwise, FileText, - FolderNotch, + Folder, } from "@phosphor-icons/react"; import axios from "axios"; import { useSelector } from "react-redux"; @@ -224,7 +224,7 @@ export default function ArchiveFiles() { return (
- + No archived files found! @@ -336,7 +336,7 @@ export default function ArchiveFiles() { return (
- + No archived files found diff --git a/src/Modules/Mess/components/ApplyForSpecialFood.jsx b/src/Modules/Mess/components/ApplyForSpecialFood.jsx index c74e97fe4..200769644 100644 --- a/src/Modules/Mess/components/ApplyForSpecialFood.jsx +++ b/src/Modules/Mess/components/ApplyForSpecialFood.jsx @@ -1,9 +1,11 @@ import React, { useState } from "react"; import axios from "axios"; import { + Alert, Button, Select, Container, + FileInput, Paper, Title, Group, @@ -19,51 +21,67 @@ import { specialFoodRequestRoute } from "../routes"; function ApplyForSpecialFood() { const [food, setFood] = useState(""); const [timing, setTiming] = useState(""); + const [requestType, setRequestType] = useState("event"); + const [medicalProof, setMedicalProof] = useState(null); const [fromDate, setFromDate] = useState(null); const [toDate, setToDate] = useState(null); const [purpose, setPurpose] = useState(""); + const [error, setError] = useState(""); const authToken = localStorage.getItem("authToken"); const today = new Date(); const minstartdate = new Date(); minstartdate.setDate(today.getDate() + 3); - // console.log(authToken); const handleSubmit = async (event) => { event.preventDefault(); + setError(""); - const requestData = { - start_date: fromDate.toISOString().split("T")[0], - end_date: toDate.toISOString().split("T")[0], - status: "1", // Pending status - app_date: new Date().toISOString().split("T")[0], - request: purpose, - item1: food, - item2: timing, - }; - console.log(requestData); + if (!fromDate || !toDate) { + setError("Select both the start and end dates."); + return; + } + + if (requestType === "medical" && !medicalProof) { + setError("Upload medical proof for illness-based requests."); + return; + } + + const requestData = new FormData(); + requestData.append("start_date", fromDate.toISOString().split("T")[0]); + requestData.append("end_date", toDate.toISOString().split("T")[0]); + requestData.append("status", "1"); + requestData.append("app_date", new Date().toISOString().split("T")[0]); + requestData.append("request", purpose); + requestData.append("item1", food); + requestData.append("item2", timing); + requestData.append("request_type", requestType); + if (medicalProof) { + requestData.append("supporting_document", medicalProof); + } try { const response = await axios.post(specialFoodRequestRoute, requestData, { headers: { Authorization: `Token ${authToken}`, - "Content-Type": "application/json", }, }); - if (response.status === 200) { + if (response.status === 200 || response.status === 201) { alert("Special food request submitted successfully!"); setFood(""); setTiming(""); + setRequestType("event"); + setMedicalProof(null); setFromDate(null); setToDate(null); setPurpose(""); } else { console.error("Failed to submit request:", response.data); - alert(`Error: ${response.data.message || "Submission failed."}`); + setError(response.data.message || "Submission failed."); } - } catch (error) { - console.error("Error submitting request:", error); - alert(`Error: ${error.response?.data?.message || error.message}`); + } catch (submitError) { + console.error("Error submitting request:", submitError); + setError(submitError.response?.data?.message || submitError.message); } }; @@ -90,6 +108,12 @@ function ApplyForSpecialFood() {
+ {error ? ( + + {error} + + ) : null} + setRequestType(value || "event")} + required + /> + setPurpose(event.currentTarget.value)} required /> + + diff --git a/src/Modules/Mess/components/CaretakerIndex.jsx b/src/Modules/Mess/components/CaretakerIndex.jsx index 73ace94ad..453c8acc6 100644 --- a/src/Modules/Mess/components/CaretakerIndex.jsx +++ b/src/Modules/Mess/components/CaretakerIndex.jsx @@ -1,138 +1,161 @@ +import { Card, Flex, Loader } from "@mantine/core"; import { - Box, - Button, - Container, - Flex, - Loader, - Tabs, - Text, -} from "@mantine/core"; -import { CaretCircleLeft, CaretCircleRight } from "@phosphor-icons/react"; -import { useRef, useState } from "react"; -import CustomBreadcrumbs from "../../../components/Breadcrumbs.jsx"; -import classes from "../styles/messModule.module.css"; + CalendarCheck, + ChatCircleText, + ClipboardText, + ForkKnife, + Gauge, + Hamburger, + ListChecks, + UserList, +} from "@phosphor-icons/react"; +import { useState } from "react"; import UpdateSemDates from "./UpdateSemDates.jsx"; -import MessActivities from "./MessActivities.jsx"; import ViewFeedback from "./ViewFeedback.jsx"; import RespondToRebateRequest from "./RespondRebate.jsx"; import ViewSpecialFoodRequest from "./ViewSpecialFoodRequest.jsx"; import RegDeregUpdatePayment from "./RegisterDeregisterUpdateRequest.jsx"; import UpdateMenu from "./UpdateMenu.jsx"; -import MessRegistrations from "./MessRegistrations.jsx"; -import ViewMenu from "./ViewMenu.jsx"; +import ViewRegistrations from "./ViewRegistration.jsx"; +import MessAnnouncements from "./MessAnnouncements.jsx"; +import MessDashboardShell from "./MessDashboardShell.jsx"; +import ManageMenuPolls from "./ManageMenuPolls.jsx"; +import PortalAnnouncements from "./PortalAnnouncements.jsx"; +// import { CaretakerVacationSurvey } from "./VacationSurvey.jsx"; -function Caretaker() { - const [activeTab, setActiveTab] = useState("0"); - const tabsListRef = useRef(null); +const tabItems = [ + { + key: "operations", + title: "Operations Board", + description: "See the live counts that need caretaker action right now.", + icon: , + component: , + }, + { + key: "announcements", + title: "Announcements", + description: + "Publish and archive portal announcements for the mess module.", + icon: , + component: , + }, + { + key: "feedback", + title: "Feedback Inbox", + description: "Review unread student feedback and mark handled items.", + icon: , + component: , + }, + { + key: "rebate", + title: "Rebate Review", + description: "Approve, decline, and remark on student rebate requests.", + icon: , + component: , + }, + { + key: "requests", + title: "Request Desk", + description: + "Handle registration, deregistration, and payment update queues.", + icon: , + component: , + }, + { + key: "special", + title: "Special Food", + description: "Process pending special-food requests from students.", + icon: , + component: , + }, + { + key: "registrations", + title: "Registrations", + description: "Search and filter the currently registered student list.", + icon: , + component: , + }, + { + key: "menu", + title: "Menu Editor", + description: "Update the published weekly menu for both mess options.", + icon: , + component: , + }, + { + key: "polls", + title: "Menu Polls", + description: + "Create polls for upcoming dishes and track how students vote.", + icon: , + component: , + }, + // { + // key: "vacationSurvey", + // title: "Vacation Survey", + // description: "Create and review vacation food preference surveys.", + // icon: , + // // component: , + // }, + { + key: "window", + title: "Registration Window", + description: "Set the date window during which students can register.", + icon: , + component: , + }, +]; - const tabItems = [ - { title: "View Feedback" }, - { title: "Respond to Rebate" }, - { title: "Requests" }, - { title: "View Special Food Requests" }, - { title: "View Menu" }, - { title: "Mess Activities" }, - { title: "Mess Registrations" }, - { title: "Update Menu" }, - { title: "Update Semester Dates" }, +function Caretaker() { + const [activeTab, setActiveTab] = useState("operations"); + const activeItem = tabItems.find((item) => item.key === activeTab); + const summaryCards = [ + { + label: "Live workflows", + value: tabItems.length, + description: "Everything from feedback and approvals to menu publishing.", + icon: , + }, + { + label: "Menu control", + value: "2 messes", + description: + "Central Mess 1 and Central Mess 2 stay editable from one place.", + icon: , + }, + { + label: "Primary focus", + value: "Approvals", + description: + "Move pending requests forward without hunting through the UI.", + icon: , + }, ]; - const handleTabChange = (direction) => { - const newIndex = - direction === "next" - ? Math.min(+activeTab + 1, tabItems.length - 1) - : Math.max(+activeTab - 1, 0); - setActiveTab(String(newIndex)); - tabsListRef.current.scrollBy({ - left: direction === "next" ? 50 : -50, - behavior: "smooth", - }); - }; - - // Function to render content based on active tab - const renderTabContent = () => { - switch (activeTab) { - case "0": - return ; - case "1": - return ; - case "2": - return ; - case "3": - return ; - case "4": - return ; - case "5": - return ; - case "6": - return ; - case "7": - return ; - case "8": - return ; - default: - return ; - } - }; - return ( - <> - {/* Navbar contents */} - - - - - - - - - {tabItems.map((item, index) => ( - - - {item.title} - - - ))} - - - - - - - - - {renderTabContent()} - + + {activeItem ? ( + activeItem.component + ) : ( + + + + + + )} + ); } diff --git a/src/Modules/Mess/components/Deregistration.jsx b/src/Modules/Mess/components/Deregistration.jsx index 04b9c4afa..9d9dd7f7d 100644 --- a/src/Modules/Mess/components/Deregistration.jsx +++ b/src/Modules/Mess/components/Deregistration.jsx @@ -1,130 +1,256 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { + Badge, Button, Container, - Paper, Group, Title, Text, - Stack, + Alert, + Card, + Grid, + Loader, } from "@mantine/core"; -import { useSelector } from "react-redux"; -import { DateInput, Calendar } from "@mantine/dates"; +import { notifications } from "@mantine/notifications"; +import { DateInput } from "@mantine/dates"; import axios from "axios"; +import { + WarningCircle, + CalendarX, + SignOut, + CheckCircle, +} from "@phosphor-icons/react"; import { deregistrationRequestRoute } from "../routes"; function Deregistration() { - const roll_no = useSelector((state) => state.user.roll_no); const [endDate, setEndDate] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [latestRequest, setLatestRequest] = useState(null); + const [isLoadingRequest, setIsLoadingRequest] = useState(true); const today = new Date(); + const statusMeta = { + pending: { color: "yellow", label: "Pending Review" }, + escalated: { color: "blue", label: "Escalated to Warden" }, + accept: { color: "green", label: "Approved" }, + reject: { color: "red", label: "Rejected" }, + cancelled: { color: "gray", label: "Cancelled" }, + }; + const activeRequest = ["pending", "escalated"].includes( + latestRequest?.status, + ); + + const fetchLatestRequest = async () => { + try { + setIsLoadingRequest(true); + const token = localStorage.getItem("authToken"); + if (!token) { + setLatestRequest(null); + return; + } + + const response = await axios.get(deregistrationRequestRoute, { + headers: { + Authorization: `Token ${token}`, + }, + }); + const [mostRecentRequest] = response.data.payload || []; + setLatestRequest(mostRecentRequest || null); + } catch (error) { + setLatestRequest(null); + } finally { + setIsLoadingRequest(false); + } + }; + + useEffect(() => { + fetchLatestRequest(); + }, []); + const handleSubmit = async (event) => { event.preventDefault(); + if (activeRequest) { + notifications.show({ + title: "Request Already Pending", + message: + "Your previous deregistration request is still active and awaiting final review.", + color: "yellow", + icon: , + }); + return; + } + if (!endDate) { - alert("Please select an end date."); + notifications.show({ + title: "Validation Error", + message: "Please select an active deregistration end date.", + color: "red", + icon: , + }); return; } const data = { - student_id: roll_no, end_date: endDate.toISOString().split("T")[0], }; try { + setIsSubmitting(true); + const token = localStorage.getItem("authToken"); const response = await axios.post(deregistrationRequestRoute, data, { headers: { - Authorization: `Token ${localStorage.getItem("authToken")}`, + Authorization: `Token ${token}`, }, }); - if (response.status === 200) { - alert("Deregistration request submitted successfully!"); + if (response.status === 200 || response.status === 201) { + setLatestRequest(response.data.payload || null); + notifications.show({ + title: "Request Submitted", + message: + "Deregistration application has been lodged successfully and is awaiting review.", + color: "green", + icon: , + }); + setEndDate(null); + } else { + throw new Error("Failed to process deregistration"); } } catch (error) { - alert("Error submitting deregistration request"); + notifications.show({ + title: "Submission Error", + message: + error.response?.data?.message || + "Failed to submit deregistration application. Please try again.", + color: "red", + icon: , + }); + await fetchLatestRequest(); + } finally { + setIsSubmitting(false); } }; return ( - - + - - - Deregistration Request - - - Click on the Deregister Button below to request deregistration. If - your request is pending, view the status in the status bar. You will - be deregistered from the mess on the date which you fill, and you - can't eat on that day. Thus, advised to fill the next day instead of - today. -
-
- ** You can only deregister from the start of the next month. -
+ + +
+ + Opt-out & Deregistration + + + Submit a formal request to cancel your active mess subscription. + +
+
- - - } - labelProps={{ style: { marginBottom: "10px" } }} - styles={(theme) => ({ - dropdown: { - backgroundColor: theme.colors.gray[0], - boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)", - }, - day: { - "&[data-selected]": { - backgroundColor: theme.colors.blue[6], - }, - "&[data-today]": { - backgroundColor: theme.colors.gray[2], - fontWeight: "bold", - }, - }, - })} - mb="lg" - /> - + {statusMeta[latestRequest.status]?.label || + latestRequest.status} + - -
-
+ + Requested end date: {latestRequest.end_date} + + {latestRequest.deregistration_remark ? ( + + Remark: {latestRequest.deregistration_remark} + + ) : null} + {activeRequest ? ( + + A new request cannot be submitted until this one is finalized. + + ) : null} + + ) : null} + +
+ + + + } + valueFormat="MMMM D, YYYY" + /> + + + + + + +
+
); } diff --git a/src/Modules/Mess/components/ManageMenuPolls.jsx b/src/Modules/Mess/components/ManageMenuPolls.jsx new file mode 100644 index 000000000..382cb5815 --- /dev/null +++ b/src/Modules/Mess/components/ManageMenuPolls.jsx @@ -0,0 +1,490 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { + Alert, + Badge, + Button, + Card, + Divider, + Flex, + Group, + Loader, + Paper, + SegmentedControl, + SimpleGrid, + Stack, + Text, + TextInput, + Textarea, + Title, +} from "@mantine/core"; +import { + CheckCircle, + ListChecks, + LockKey, + PlusCircle, + WarningCircle, +} from "@phosphor-icons/react"; +import { notifications } from "@mantine/notifications"; +import { menuPollRoute } from "../routes"; + +const MESS_OPTIONS = [ + { label: "Central Mess 1", value: "mess1" }, + { label: "Central Mess 2", value: "mess2" }, +]; + +const MEAL_OPTIONS = [ + { label: "Any meal slot", value: "" }, + { label: "Monday Breakfast", value: "MB" }, + { label: "Monday Lunch", value: "ML" }, + { label: "Monday Dinner", value: "MD" }, + { label: "Tuesday Breakfast", value: "TB" }, + { label: "Tuesday Lunch", value: "TL" }, + { label: "Tuesday Dinner", value: "TD" }, + { label: "Wednesday Breakfast", value: "WB" }, + { label: "Wednesday Lunch", value: "WL" }, + { label: "Wednesday Dinner", value: "WD" }, + { label: "Thursday Breakfast", value: "THB" }, + { label: "Thursday Lunch", value: "THL" }, + { label: "Thursday Dinner", value: "THD" }, + { label: "Friday Breakfast", value: "FB" }, + { label: "Friday Lunch", value: "FL" }, + { label: "Friday Dinner", value: "FD" }, + { label: "Saturday Breakfast", value: "SB" }, + { label: "Saturday Lunch", value: "SL" }, + { label: "Saturday Dinner", value: "SD" }, + { label: "Sunday Breakfast", value: "SUB" }, + { label: "Sunday Lunch", value: "SUL" }, + { label: "Sunday Dinner", value: "SUD" }, +]; + +const initialForm = { + question: "", + description: "", + mess_option: "mess1", + meal_time: "", + poll_date: "", + optionsText: "", +}; + +function parseOptions(optionsText) { + return optionsText + .split(/\n|,/) + .map((option) => option.trim()) + .filter(Boolean); +} + +function formatPollMeta(poll) { + const parts = [poll.mess_option_display]; + if (poll.meal_time_display) { + parts.push(poll.meal_time_display); + } + if (poll.poll_date) { + parts.push(`For ${poll.poll_date}`); + } + return parts.join(" • "); +} + +function ManageMenuPolls() { + const [polls, setPolls] = useState([]); + const [filter, setFilter] = useState("all"); + const [form, setForm] = useState(initialForm); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [updatingPollId, setUpdatingPollId] = useState(null); + const [error, setError] = useState(""); + + const getHeaders = () => { + const token = localStorage.getItem("authToken"); + return { Authorization: `Token ${token}` }; + }; + + useEffect(() => { + const loadPolls = async () => { + try { + const response = await axios.get(menuPollRoute, { + headers: getHeaders(), + }); + setPolls(response.data.payload || []); + } catch (err) { + setError( + err.response?.data?.message || + "Unable to load the menu polls right now.", + ); + } finally { + setLoading(false); + } + }; + + loadPolls(); + }, []); + + const visiblePolls = polls.filter((poll) => { + if (filter === "all") { + return true; + } + return poll.status === filter; + }); + + const pollStats = { + total: polls.length, + open: polls.filter((poll) => poll.status === "open").length, + votes: polls.reduce((sum, poll) => sum + (poll.total_votes || 0), 0), + }; + + const handleChange = (field, value) => { + setForm((prev) => ({ ...prev, [field]: value })); + }; + + const handleCreatePoll = async () => { + const options = parseOptions(form.optionsText); + if (!form.question.trim()) { + setError("Add a clear poll question before publishing."); + return; + } + if (options.length < 2) { + setError("Add at least two menu options for students to vote on."); + return; + } + + try { + setSaving(true); + setError(""); + const response = await axios.post( + menuPollRoute, + { + question: form.question.trim(), + description: form.description.trim(), + mess_option: form.mess_option, + meal_time: form.meal_time || null, + poll_date: form.poll_date || null, + options, + }, + { headers: getHeaders() }, + ); + + setPolls((prev) => [response.data.payload, ...prev]); + setForm(initialForm); + notifications.show({ + title: "Menu poll created", + message: + response.data.message || "Students can start voting on the new poll.", + color: "green", + icon: , + }); + } catch (err) { + setError( + err.response?.data?.message || "Unable to create the menu poll.", + ); + } finally { + setSaving(false); + } + }; + + const handleToggleStatus = async (poll) => { + const nextStatus = poll.status === "open" ? "closed" : "open"; + try { + setUpdatingPollId(poll.id); + const response = await axios.put( + menuPollRoute, + { + id: poll.id, + status: nextStatus, + }, + { headers: getHeaders() }, + ); + setPolls((prev) => + prev.map((item) => + item.id === poll.id ? response.data.payload : item, + ), + ); + notifications.show({ + title: nextStatus === "open" ? "Poll reopened" : "Poll closed", + message: + response.data.message || "The poll status was updated successfully.", + color: nextStatus === "open" ? "blue" : "dark", + icon: + nextStatus === "open" ? ( + + ) : ( + + ), + }); + } catch (err) { + setError( + err.response?.data?.message || "Unable to update the poll status.", + ); + } finally { + setUpdatingPollId(null); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + {error ? ( + }> + {error} + + ) : null} + + + + +
+ + Create a Menu Poll + + + Publish a menu choice for one mess and collect student votes in + the same workspace. + +
+ + + handleChange("question", event.currentTarget.value) + } + /> + +