diff --git a/hab-frontend/src/App.jsx b/hab-frontend/src/App.jsx
index e158c1a3..0cad53f9 100644
--- a/hab-frontend/src/App.jsx
+++ b/hab-frontend/src/App.jsx
@@ -11,6 +11,7 @@ import MessChangePage from "./pages/MessChangePage.jsx";
import GalaDinnerPage from "./pages/GalaDinnerPage.jsx";
import GalaDinnerDetailPage from "./pages/GalaDinnerDetailPage.jsx";
import Notifications from "./pages/Notifications.jsx";
+import MessClosurePage from "./pages/MessClosurePage.jsx";
import CreateMess from "./components/CreateMess";
import MessDetails from "./components/MessDetails";
import MessMenu from "./components/MessMenu";
@@ -94,6 +95,9 @@ function App() {
path="/notifications"
element={ }
/>
+ }
+ />
diff --git a/hab-frontend/src/apis/messClosures.js b/hab-frontend/src/apis/messClosures.js
new file mode 100644
index 00000000..2d1229eb
--- /dev/null
+++ b/hab-frontend/src/apis/messClosures.js
@@ -0,0 +1,26 @@
+import axios from "axios";
+import { BACKEND_URL } from "./server";
+
+export const getAllMessClosures = async (filters = {}) => {
+ const params = new URLSearchParams(
+ Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== "" && v !== undefined))
+ ).toString();
+ const url = `${BACKEND_URL}/hostel/closure${params ? `?${params}` : ""}`;
+ const response = await axios.get(url);
+ return response.data;
+};
+
+export const createMessClosure = async (data) => {
+ const response = await axios.post(`${BACKEND_URL}/hostel/closure`, data);
+ return response.data;
+};
+
+export const updateMessClosure = async (id, data) => {
+ const response = await axios.put(`${BACKEND_URL}/hostel/closure/${id}`, data);
+ return response.data;
+};
+
+export const deleteMessClosure = async (id) => {
+ const response = await axios.delete(`${BACKEND_URL}/hostel/closure/${id}`);
+ return response.data;
+};
\ No newline at end of file
diff --git a/hab-frontend/src/components/Sidebar.jsx b/hab-frontend/src/components/Sidebar.jsx
index 8123d6cb..0d9e3989 100644
--- a/hab-frontend/src/components/Sidebar.jsx
+++ b/hab-frontend/src/components/Sidebar.jsx
@@ -11,6 +11,7 @@ import {
SettingOutlined,
BookOutlined,
NotificationOutlined,
+ CalendarOutlined,
GiftOutlined,
} from "@ant-design/icons";
@@ -40,6 +41,8 @@ const Sidebar = ({ collapsed = false, onToggle }) => {
path: "/notifications",
icon: ,
},
+ { key: "8", name: "Mess Closures", path: "/mess-closures",
+ icon: },
];
return (
diff --git a/hab-frontend/src/pages/MessClosurePage.jsx b/hab-frontend/src/pages/MessClosurePage.jsx
new file mode 100644
index 00000000..6118e436
--- /dev/null
+++ b/hab-frontend/src/pages/MessClosurePage.jsx
@@ -0,0 +1,578 @@
+// HAB Admin: Schedule / View / Edit / Delete mess closures
+// ─────────────────────────────────────────────────────────────────────────────
+
+import React, { useState, useEffect, useCallback } from "react";
+import {
+ getAllMessClosures,
+ createMessClosure,
+ updateMessClosure,
+ deleteMessClosure,
+} from "../apis/messClosures";
+import { getAllHostels } from "../apis/hostel"; // fetches list of all hostels for dropdown
+
+// ── Small reusable toast ──────────────────────────────────────────────────────
+const Toast = ({ message, type, onClose }) => {
+ useEffect(() => {
+ const t = setTimeout(onClose, 4000);
+ return () => clearTimeout(t);
+ }, [onClose]);
+
+ const colors =
+ type === "error"
+ ? "bg-red-50 border-red-200 text-red-700"
+ : "bg-green-50 border-green-200 text-green-700";
+
+ return (
+
+ {message}
+
+ ×
+
+
+ );
+};
+
+// ── Modal shell ───────────────────────────────────────────────────────────────
+const Modal = ({ title, onClose, children }) => (
+
+
+
+
{title}
+
+ ×
+
+
+
{children}
+
+
+);
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+const MONTHS = [
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December",
+];
+
+/** Returns true if a closure can still be edited/deleted (within 8 hours of creation) */
+const isEditable = (createdAt) => {
+ if (!createdAt) return false;
+ const diffMs = Date.now() - new Date(createdAt).getTime();
+ return diffMs < 8 * 60 * 60 * 1000; // 8 hours in ms
+};
+
+/** Returns minimum date string (today + 48 h) for the date input */
+const minClosureDate = () => {
+ const d = new Date(Date.now() + 48 * 60 * 60 * 1000);
+ return d.toISOString().split("T")[0];
+};
+
+const formatDate = (iso) => {
+ if (!iso) return "—";
+ return new Date(iso).toLocaleDateString("en-IN", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ });
+};
+
+// ── Main component ────────────────────────────────────────────────────────────
+const MessClosurePage = () => {
+ // ── Data state ──
+ const [closures, setClosures] = useState([]);
+ const [hostels, setHostels] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ // ── Filter state ──
+ const [filterHostel, setFilterHostel] = useState("");
+ const [filterMonth, setFilterMonth] = useState("");
+ const [filterYear, setFilterYear] = useState("");
+ const [filterUpcoming, setFilterUpcoming] = useState(false);
+
+ // ── Toast state ──
+ const [toast, setToast] = useState(null); // { message, type }
+
+ // ── Modal state ──
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [editTarget, setEditTarget] = useState(null); // closure object being edited
+ const [deleteTarget, setDeleteTarget] = useState(null); // closure object to confirm delete
+
+ // ── Form state (shared between create & edit) ──
+ const emptyForm = { hostelId: "", closureDate: ""};
+ const [form, setForm] = useState(emptyForm);
+ const [submitting, setSubmitting] = useState(false);
+
+ // ── Toast helper ──
+ const showToast = (message, type = "success") => setToast({ message, type });
+
+ // ── Fetch all closures ──
+ const fetchClosures = useCallback(async () => {
+ setLoading(true);
+ try {
+ const filters = {
+ ...(filterHostel && { hostelId: filterHostel }),
+ ...(filterMonth && { month: filterMonth }),
+ ...(filterYear && { year: filterYear }),
+ ...(filterUpcoming && { upcoming: true }),
+ };
+ const data = await getAllMessClosures(filters);
+ setClosures(data.closures || data || []);
+ } catch (err) {
+ showToast(err?.response?.data?.message || "Failed to load closures.", "error");
+ } finally {
+ setLoading(false);
+ }
+ }, [filterHostel, filterMonth, filterYear, filterUpcoming]);
+
+// const fetchClosures = useCallback(async () => {
+// setLoading(true);
+// // TEMP MOCK DATA - remove this and uncomment the real code when backend is ready
+// setClosures([
+// {
+// _id: "1",
+// hostelId: { hostel_name: "Brahmaputra" },
+// closureDate: "2026-03-15T00:00:00.000Z",
+// month: 3,
+// year: 2026,
+// createdAt: new Date().toISOString(), // recent = edit/delete buttons should be active
+// },
+// {
+// _id: "2",
+// hostelId: { hostel_name: "Lohit" },
+// closureDate: "2026-02-10T00:00:00.000Z",
+// month: 2,
+// year: 2026,
+// createdAt: "2026-02-01T00:00:00.000Z", // old = edit/delete should be locked
+// },
+// ]);
+// setLoading(false);
+// }, [filterHostel, filterMonth, filterYear, filterUpcoming]);
+
+ // ── Fetch hostel list for dropdown ──
+
+ useEffect(() => {
+ fetchHostels();
+ }, [fetchHostels]);
+
+ useEffect(() => {
+ fetchClosures();
+ }, [fetchClosures]);
+
+ // ── Open create modal ──
+ const openCreate = () => {
+ setForm(emptyForm);
+ setShowCreateModal(true);
+ };
+
+ // ── Open edit modal ──
+ const openEdit = (closure) => {
+ if (!isEditable(closure.createdAt)) {
+ showToast(
+ "This closure cannot be edited — more than 8 hours have passed since it was created.",
+ "error"
+ );
+ return;
+ }
+ setEditTarget(closure);
+ setForm({
+ hostelId: closure.hostel?._id || closure.hostelId || "",
+ closureDate: closure.closureDate?.split("T")[0] || "",
+ reason: closure.reason || "",
+ });
+ };
+
+ // ── Validate form ──
+ const validateForm = () => {
+ if (!form.hostelId) return "Please select a hostel.";
+ if (!form.closureDate) return "Please pick a closure date.";
+ const selectedDate = new Date(form.closureDate);
+ const minDate = new Date(Date.now() + 48 * 60 * 60 * 1000);
+ if (selectedDate < minDate) {
+ return "Closure must be scheduled at least 48 hours in advance.";
+ }
+ return null;
+};
+ // ── Submit create ──
+ const handleCreate = async (e) => {
+
+ const err = validateForm();
+ if (err) { showToast(err, "error"); return; }
+ setSubmitting(true);
+ try {
+ await createMessClosure({
+ hostelId: form.hostelId,
+ closureDate: form.closureDate,
+ });
+ showToast("Mess closure scheduled successfully.");
+ setShowCreateModal(false);
+ setForm(emptyForm);
+ fetchClosures();
+ } catch (err) {
+ showToast(err?.response?.data?.message || "Failed to schedule closure.", "error");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ // ── Submit edit ──
+ const handleEdit = async (e) => {
+ e.preventDefault();
+
+ const validErr = validateForm();
+ if (validErr) { showToast(validErr, "error"); return; }
+ setSubmitting(true);
+ try {
+ await updateMessClosure(editTarget._id, {
+ closureDate: form.closureDate,
+ });
+ showToast("Closure updated successfully.");
+ setEditTarget(null);
+ setForm(emptyForm);
+ fetchClosures();
+ } catch (err) {
+ showToast(err?.response?.data?.message || "Failed to update closure.", "error");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ // ── Confirm delete ──
+ const handleDelete = async () => {
+
+ if (!deleteTarget) return;
+ if (!isEditable(deleteTarget.createdAt)) {
+ showToast(
+ "This closure cannot be deleted — more than 8 hours have passed since it was created.",
+ "error"
+ );
+ setDeleteTarget(null);
+ return;
+ }
+ setSubmitting(true);
+ try {
+ await deleteMessClosure(deleteTarget._id);
+ showToast("Closure deleted.");
+ setDeleteTarget(null);
+ fetchClosures();
+ } catch (err) {
+ showToast(err?.response?.data?.message || "Failed to delete closure.", "error");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ // ── Shared form body (used in both create & edit modal) ──
+ const FormBody = ({ isEdit }) => (
+
+ );
+
+ // ── Render ─────────────────────────────────────────────────────────────────
+ return (
+
+ {/* Toast */}
+ {toast && (
+
setToast(null)} />
+ )}
+
+ {/* Create Modal */}
+ {showCreateModal && (
+ setShowCreateModal(false)}>
+
+
+ )}
+
+ {/* Edit Modal */}
+ {editTarget && (
+ { setEditTarget(null); setForm(emptyForm); }}
+ >
+
+
+ )}
+
+ {/* Delete Confirm Modal */}
+ {deleteTarget && (
+ setDeleteTarget(null)}>
+
+ Are you sure you want to delete the closure for{" "}
+
+ {deleteTarget.hostelId?.hostel_name || "this hostel"}
+ {" "}
+ on {formatDate(deleteTarget.closureDate)} ?
+ This action cannot be undone.
+
+
+ setDeleteTarget(null)}
+ className="px-4 py-2 text-sm rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50"
+ >
+ Cancel
+
+
+ {submitting ? "Deleting…" : "Delete"}
+
+
+
+ )}
+
+ {/* Page Header */}
+
+
+
Mess Closures
+
+ Manage scheduled mess closure days across all hostels.
+
+
+
+ +
+ Schedule Closure
+
+
+
+ {/* Filters */}
+
+
+ {/* Hostel filter */}
+
setFilterHostel(e.target.value)}
+ className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ All Hostels
+ {hostels.map((h) => (
+
+ {h.hostel_name || h.name}
+
+ ))}
+
+
+ {/* Month filter */}
+
setFilterMonth(e.target.value)}
+ className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ All Months
+ {MONTHS.map((m, i) => (
+
+ {m}
+
+ ))}
+
+
+ {/* Year filter */}
+
setFilterYear(e.target.value)}
+ className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+ {/* Upcoming toggle + Reset */}
+
+
+ setFilterUpcoming(e.target.checked)}
+ className="accent-blue-600"
+ />
+ Upcoming only
+
+ {
+ setFilterHostel("");
+ setFilterMonth("");
+ setFilterYear("");
+ setFilterUpcoming(false);
+ }}
+ className="text-xs text-gray-400 hover:text-gray-600 underline"
+ >
+ Reset
+
+
+
+
+
+ {/* Table */}
+
+ {loading ? (
+
+ ) : closures.length === 0 ? (
+
+ No mess closures found. Use the filters above or schedule a new one.
+
+ ) : (
+
+
+
+
+ Hostel
+ Closure Date
+ Month
+ Reason
+
+ Created
+ Actions
+
+
+
+ {closures.map((c) => {
+ const editable = isEditable(c.createdAt);
+ const closureDate = new Date(c.closureDate);
+ const isPast = closureDate < new Date();
+
+ return (
+
+
+ {c.hostelId?.hostel_name || "—"}
+
+ {formatDate(c.closureDate)}
+
+ {MONTHS[closureDate.getMonth()]} {closureDate.getFullYear()}
+
+
+ {c.reason || "—"}
+
+
+
+ {formatDate(c.createdAt)}
+
+
+
+ openEdit(c)}
+ disabled={!editable}
+ title={
+ !editable
+ ? "Can only edit within 8 hours of creation"
+ : "Edit closure"
+ }
+ className={`px-3 py-1 rounded text-xs border transition-colors ${
+ editable
+ ? "border-blue-200 text-blue-600 hover:bg-blue-50"
+ : "border-gray-200 text-gray-300 cursor-not-allowed"
+ }`}
+ >
+ Edit
+
+ editable && setDeleteTarget(c)}
+ disabled={!editable}
+ title={
+ !editable
+ ? "Can only delete within 8 hours of creation"
+ : "Delete closure"
+ }
+ className={`px-3 py-1 rounded text-xs border transition-colors ${
+ editable
+ ? "border-red-200 text-red-600 hover:bg-red-50"
+ : "border-gray-200 text-gray-300 cursor-not-allowed"
+ }`}
+ >
+ Delete
+
+
+ {!editable && !isPast && (
+ Locked (8h passed)
+ )}
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+ );
+};
+
+export default MessClosurePage;
\ No newline at end of file
diff --git a/hostel-frontend/src/components/MessClosureView.jsx b/hostel-frontend/src/components/MessClosureView.jsx
new file mode 100644
index 00000000..1c023481
--- /dev/null
+++ b/hostel-frontend/src/components/MessClosureView.jsx
@@ -0,0 +1,259 @@
+import React, { useState, useEffect, useCallback } from "react";
+import axios from "axios";
+import { API_BASE_URL } from "../apis";
+import Card from "./ui/Card";
+
+// ── Toast ──────────────────────────────────────────────────────────────────────
+const Toast = ({ message, type, onClose }) => {
+ useEffect(() => {
+ const t = setTimeout(onClose, 4000);
+ return () => clearTimeout(t);
+ }, [onClose]);
+
+ const colors =
+ type === "error"
+ ? "bg-red-50 border-red-200 text-red-700"
+ : "bg-green-50 border-green-200 text-green-700";
+
+ return (
+
+ {message}
+
+ ×
+
+
+ );
+};
+
+// ── Helpers ────────────────────────────────────────────────────────────────────
+const MONTHS = [
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December",
+];
+
+const formatDate = (iso) => {
+ if (!iso) return "—";
+ return new Date(iso).toLocaleDateString("en-IN", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ });
+};
+
+// ── Component ──────────────────────────────────────────────────────────────────
+const MessClosureView = () => {
+ const [closures, setClosures] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [toast, setToast] = useState(null);
+
+ // Filter state
+ const [filterMonth, setFilterMonth] = useState("");
+ const [filterYear, setFilterYear] = useState("");
+ const [filterUpcoming, setFilterUpcoming] = useState(false);
+
+ const showToast = (message, type = "error") => setToast({ message, type });
+
+ const fetchClosures = useCallback(async () => {
+
+setLoading(false);
+return; // add this so the real API call is skipped
+ setLoading(true);
+ try {
+ const token = localStorage.getItem("token");
+ const params = new URLSearchParams(
+ Object.fromEntries(
+ Object.entries({
+ month: filterMonth,
+ year: filterYear,
+ upcoming: filterUpcoming || undefined,
+ }).filter(([, v]) => v !== "" && v !== undefined && v !== false)
+ )
+ ).toString();
+
+ const url = `${API_BASE_URL}/hostel/closure/myHostel${params ? `?${params}` : ""}`;
+ const response = await axios.get(url, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ setClosures(response.data.closures || response.data || []);
+ } catch (err) {
+ showToast(err?.response?.data?.message || "Failed to load mess closures.");
+ } finally {
+ setLoading(false);
+ }
+ }, [filterMonth, filterYear, filterUpcoming]);
+
+ useEffect(() => {
+ fetchClosures();
+ }, [fetchClosures]);
+
+ // Find the next upcoming closure (for the highlight banner)
+ const nextClosure = closures
+ .filter((c) => new Date(c.closureDate) >= new Date())
+ .sort((a, b) => new Date(a.closureDate) - new Date(b.closureDate))[0];
+
+ return (
+
+ {/* Toast */}
+ {toast && (
+ setToast(null)} />
+ )}
+
+ {/* Header */}
+
+
Mess Closures
+
+ Scheduled mess closure days for your hostel. Contact HAB admin to make changes.
+
+
+
+
+ {/* Next upcoming closure banner */}
+ {nextClosure && (
+
+
📅
+
+
Next Scheduled Closure
+
+ {formatDate(nextClosure.closureDate)}
+ {nextClosure.reason && ` — ${nextClosure.reason}`}
+
+ {!nextClosure.isNotificationSent && (
+
+ Student notifications have not been sent yet.
+
+ )}
+
+
+ )}
+
+ {/* Filters */}
+
+
+ {/* Table */}
+ {loading ? (
+
+ ) : closures.length === 0 ? (
+
+ No mess closures found for the selected filters.
+
+ ) : (
+
+
+
+
+ Closure Date
+ Month
+ Reason
+ Scheduled By
+ Notified
+ Status
+
+
+
+ {closures.map((c) => {
+ const closureDate = new Date(c.closureDate);
+ const isPast = closureDate < new Date();
+ return (
+
+
+ {formatDate(c.closureDate)}
+
+
+ {MONTHS[closureDate.getMonth()]} {closureDate.getFullYear()}
+
+
+ {c.reason || "—"}
+
+
+ {c.scheduledBy?.name || c.scheduledBy?.email || "HAB Admin"}
+
+
+ {c.isNotificationSent ? (
+
+ Sent
+
+ ) : (
+
+ Pending
+
+ )}
+
+
+ {isPast ? (
+
+ Past
+
+ ) : (
+
+ Upcoming
+
+ )}
+
+
+ );
+ })}
+
+
+
+ )}
+
+ {/* Info note */}
+
+ * Closures are managed by the HAB Admin. Please contact HAB if you notice any discrepancy.
+
+
+
+ );
+};
+
+export default MessClosureView;
diff --git a/hostel-frontend/src/pages/Dashboard.jsx b/hostel-frontend/src/pages/Dashboard.jsx
index 31114659..7646b3e5 100644
--- a/hostel-frontend/src/pages/Dashboard.jsx
+++ b/hostel-frontend/src/pages/Dashboard.jsx
@@ -8,9 +8,13 @@ import {
UserCheck,
Building2,
LogOut,
+ Bell,
+ CalendarX,
Pencil,
X,
} from "lucide-react";
+import NotificationSender from "../components/NotificationSender";
+import MessClosureView from "../components/MessClosureView";
// NotificationSender and notification tab hidden for now
import Card from "../components/ui/Card";
import Tabs from "../components/ui/Tabs";
@@ -315,6 +319,8 @@ const Dashboard = () => {
{ label: "Boarders", value: "boarders", icon: Users },
{ label: "Mess Subscribers", value: "subscribers", icon: Building2 },
{ label: "SMC Management", value: "smc", icon: UserCheck },
+ { label: "Notifications", value: "notifications", icon: Bell },
+ { label: "Mess Closures", value: "messClosure", icon: CalendarX },
// Room cleaning configuration for this hostel
{ label: "Room Cleaners", value: "cleaners", icon: Users },
];
@@ -967,6 +973,9 @@ const Dashboard = () => {
)}
+ {activeTab === "messClosure" && }
+
+
{activeTab === "smc" && (