From b776e4789145ebb7272a946306d1e02d50e86372 Mon Sep 17 00:00:00 2001 From: KolliKesav <162597645+KolliKesav@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:30:00 +0530 Subject: [PATCH 1/3] frontend refactoring --- src/Modules/Academic/AcademicCalendar.jsx | 12 +- src/Modules/Academic/AdminAddDashboard.jsx | 423 +- src/Modules/Academic/AdminBatchChange.jsx | 224 +- src/Modules/Academic/AdminDropDashboard.jsx | 388 +- src/Modules/Academic/AdminPromoteSemester.jsx | 31 +- .../Academic/AdminReplacementDashboard.jsx | 473 +- .../Academic/AdminStudentDashboard.jsx | 113 +- src/Modules/Academic/AllotCourses.jsx | 235 +- src/Modules/Academic/Faculty_TA_Dashboard.jsx | 90 +- .../FeedbackForm/AdminFeedbackView.jsx | 50 +- .../FeedbackForm/InstructorDashboard.jsx | 26 +- .../StudentCourseFeedbackForm.jsx | 32 +- src/Modules/Academic/FinalRegistration.jsx | 15 +- src/Modules/Academic/GenerateStudentList.jsx | 283 +- src/Modules/Academic/Hod_TA_Dashboard.jsx | 192 +- src/Modules/Academic/PreRegistration.jsx | 224 +- src/Modules/Academic/RegisteredCourses.jsx | 740 ++- .../Academic/ReplacementRequestStudent.jsx | 131 +- src/Modules/Academic/StudentAddCourse.jsx | 163 +- .../Academic/StudentAddDropReplace.jsx | 29 +- .../Academic/StudentCourseReplacement.jsx | 144 +- src/Modules/Academic/StudentCourses.jsx | 152 +- src/Modules/Academic/StudentDropCourse.jsx | 85 +- src/Modules/Academic/Student_TA_Dashboard.jsx | 33 +- src/Modules/Academic/SwayamRegistration.jsx | 32 +- .../Academic/VerifyStudentRegistration.jsx | 18 +- src/Modules/Academic/ViewRollList.jsx | 53 +- src/Modules/Academic/index.jsx | 24 +- .../Dashboard/dashboardNotifications.jsx | 2 +- src/Modules/Examination/AnnounceResult.jsx | 44 +- src/Modules/Examination/GradeStatus.jsx | 269 +- src/Modules/Examination/GradeSummary.jsx | 227 +- src/Modules/Examination/checkResult.jsx | 724 ++- src/Modules/Examination/checkResultsProf.jsx | 140 +- src/Modules/Examination/components/nav2.jsx | 26 +- .../components/studentTranscript.jsx | 784 ++- .../Examination/components/transcript.jsx | 133 +- src/Modules/Examination/examination.jsx | 16 +- .../Examination/routes/examinationRoutes.jsx | 26 +- .../Examination/routes/protectedRoutes.jsx | 10 +- src/Modules/Examination/submitGrades.jsx | 213 +- src/Modules/Examination/submitGradesProf.jsx | 275 +- src/Modules/Examination/validateDean.jsx | 92 +- src/Modules/Examination/verifyDean.jsx | 131 +- src/Modules/Examination/verifyGrades.jsx | 94 +- .../components/cards/ComplaintCard.jsx | 0 .../{students => cards}/FineCard.jsx | 0 .../LeaveApplicationCard.jsx | 0 .../{students => cards}/StudentInfoCard.jsx | 0 .../components/common/LoadingSpinner.jsx | 52 + .../SideNotifications.jsx | 0 .../components/common/StudentInfoCard.jsx | 86 + .../{students => common}/ViewAttendance.jsx | 27 +- .../components/common/index.js | 3 + .../{caretaker => forms}/AttendanceForm.jsx | 32 +- .../{warden => forms}/CreateNotice.jsx | 17 +- .../CreateNotice1.jsx} | 0 .../Hostel-Management/components/index.js | 14 + .../components/tables/Table.jsx | 149 + .../pages/all-actors/NoticeBoard.jsx | 17 +- .../all-actors/NoticeBoardWardenCaretaker.jsx | 37 +- .../pages/all-actors/StudentInfo.jsx | 15 +- .../pages/caretaker/ImposeFine.jsx | 45 +- .../pages/caretaker/ManageFine.jsx | 29 +- .../pages/caretaker/ManageLeaverequest.jsx | 39 +- .../pages/caretaker/StudentInfo.jsx | 14 +- .../pages/hostel-admin/AddHostel.jsx | 20 +- .../pages/hostel-admin/AssignBatch.jsx | 55 +- .../pages/hostel-admin/AssignCaretaker.jsx | 220 +- .../pages/hostel-admin/ViewHostel.jsx | 21 +- src/Modules/Hostel-Management/pages/index.js | 26 + .../pages/students/AllotedRooms.jsx | 20 +- .../pages/students/Complaints.jsx | 28 +- .../Hostel-Management/pages/students/Fine.jsx | 26 +- .../pages/students/LeaveForm.jsx | 19 +- .../pages/students/LeaveStatus.jsx | 16 +- .../pages/warden/AssignRoom.jsx | 43 +- .../services/adminService.js | 35 + src/Modules/Hostel-Management/services/api.js | 39 + .../services/caretakerService.js | 40 + .../services/commonService.js | 22 + .../Hostel-Management/services/index.js | 6 + .../services/studentService.js | 36 + .../services/wardenService.js | 30 + .../Hostel-Management/styles/module.css | 0 .../Hostel-Management/utils/dateFormatter.js | 47 + src/Modules/Hostel-Management/utils/index.js | 2 + .../Hostel-Management/utils/validation.js | 65 + .../Acad_admin_replicate_curriculum.jsx | 4 +- .../Acad_admin/Admin_Upcoming_Batches.jsx | 4885 +++++++++++------ .../Acad_admin/Admin_add_batch_form.jsx | 47 +- .../Admin_add_course_instructor_form.jsx | 64 +- .../Admin_add_course_proposal_form.jsx | 29 +- .../Acad_admin/Admin_add_course_slot_form.jsx | 18 +- .../Acad_admin/Admin_add_curriculum_form.jsx | 40 +- .../Acad_admin/Admin_add_discipline_form.jsx | 40 +- .../Acad_admin/Admin_add_programme_form.jsx | 70 +- .../Acad_admin/Admin_edit_batch_form.jsx | 60 +- .../Acad_admin/Admin_edit_course_form.jsx | 215 +- .../Admin_edit_course_instructor_form.jsx | 59 +- .../Admin_edit_course_slot_form.jsx | 20 +- .../Acad_admin/Admin_edit_discipline_form.jsx | 35 +- .../Acad_admin/Admin_edit_programme_form.jsx | 21 +- .../Acad_admin/Admin_view_a_course.jsx | 81 +- .../Acad_admin/Admin_view_all_batches.jsx | 1017 ++-- .../Admin_view_all_course_instructors.jsx | 175 +- .../Acad_admin/Admin_view_all_courses.jsx | 69 +- .../Acad_admin/Admin_view_all_programmes.jsx | 140 +- .../Admin_view_all_working_curriculums.jsx | 71 +- .../Acad_admin/DisciplineAcad.jsx | 71 +- .../Acad_admin/Instigate_form.jsx | 432 +- .../Acad_admin/ProgrammeCurriculumView.jsx | 116 +- .../Faculty/BreadcrumbTagsFaculty.jsx | 57 +- .../Faculty_add_course_proposal_form.jsx | 56 +- .../Faculty/Faculty_course_forward_form.jsx | 32 +- .../Faculty/Faculty_course_proposal.jsx | 11 +- .../Faculty_edit_course_proposal_form.jsx | 15 +- .../Faculty/Faculty_view_a_course.jsx | 4 +- .../Faculty_view_a_course_proposal_form.jsx | 155 +- .../Faculty/Faculty_view_all_batches.jsx | 17 +- .../Faculty/Faculty_view_all_courses.jsx | 39 +- .../Faculty/VCourseProposalForm.jsx | 4 +- .../Faculty/ViewInwardFile.jsx | 12 +- .../Program_curriculum/SemesterInfo.jsx | 2 +- .../Student/ProgrammeCurriculumStudView.jsx | 39 +- .../Program_curriculum/View_all_batches.jsx | 17 +- src/Modules/Program_curriculum/api/api.js | 17 +- src/components/FusionTable.jsx | 7 +- src/components/NotFoundPage.jsx | 26 +- src/components/header.jsx | 2 +- src/components/sidebarContent.jsx | 2 +- src/pages/login.jsx | 1092 ++-- src/utils/notifications.jsx | 68 +- 133 files changed, 11379 insertions(+), 7087 deletions(-) create mode 100644 src/Modules/Hostel-Management/components/cards/ComplaintCard.jsx rename src/Modules/Hostel-Management/components/{students => cards}/FineCard.jsx (100%) rename src/Modules/Hostel-Management/components/{students => cards}/LeaveApplicationCard.jsx (100%) rename src/Modules/Hostel-Management/components/{students => cards}/StudentInfoCard.jsx (100%) create mode 100644 src/Modules/Hostel-Management/components/common/LoadingSpinner.jsx rename src/Modules/Hostel-Management/components/{all-actors => common}/SideNotifications.jsx (100%) create mode 100644 src/Modules/Hostel-Management/components/common/StudentInfoCard.jsx rename src/Modules/Hostel-Management/components/{students => common}/ViewAttendance.jsx (88%) create mode 100644 src/Modules/Hostel-Management/components/common/index.js rename src/Modules/Hostel-Management/components/{caretaker => forms}/AttendanceForm.jsx (86%) rename src/Modules/Hostel-Management/components/{warden => forms}/CreateNotice.jsx (92%) rename src/Modules/Hostel-Management/components/{caretaker/CreateNotice.jsx => forms/CreateNotice1.jsx} (100%) create mode 100644 src/Modules/Hostel-Management/components/index.js create mode 100644 src/Modules/Hostel-Management/components/tables/Table.jsx create mode 100644 src/Modules/Hostel-Management/pages/index.js create mode 100644 src/Modules/Hostel-Management/services/adminService.js create mode 100644 src/Modules/Hostel-Management/services/api.js create mode 100644 src/Modules/Hostel-Management/services/caretakerService.js create mode 100644 src/Modules/Hostel-Management/services/commonService.js create mode 100644 src/Modules/Hostel-Management/services/index.js create mode 100644 src/Modules/Hostel-Management/services/studentService.js create mode 100644 src/Modules/Hostel-Management/services/wardenService.js create mode 100644 src/Modules/Hostel-Management/styles/module.css create mode 100644 src/Modules/Hostel-Management/utils/dateFormatter.js create mode 100644 src/Modules/Hostel-Management/utils/index.js create mode 100644 src/Modules/Hostel-Management/utils/validation.js diff --git a/src/Modules/Academic/AcademicCalendar.jsx b/src/Modules/Academic/AcademicCalendar.jsx index 6674b8f15..66460c941 100644 --- a/src/Modules/Academic/AcademicCalendar.jsx +++ b/src/Modules/Academic/AcademicCalendar.jsx @@ -61,7 +61,7 @@ function AcademicCalendar() { from_date: e.from_date ? new Date(e.from_date) : null, to_date: e.to_date ? new Date(e.to_date) : null, })) - : [] + : [], ); } catch (err) { if (mounted) setError("Failed to load events"); @@ -115,7 +115,7 @@ function AcademicCalendar() { from_date: editingEvent.from_date.toISOString().slice(0, 10), to_date: editingEvent.to_date.toISOString().slice(0, 10), }, - { headers: { Authorization: `Token ${token}` } } + { headers: { Authorization: `Token ${token}` } }, ); setEditingEvent(null); setRefreshTrigger((t) => t + 1); @@ -128,11 +128,7 @@ function AcademicCalendar() { // Create new event const handleAddEvent = async () => { - if ( - !newEvent.description || - !newEvent.from_date || - !newEvent.to_date - ) { + if (!newEvent.description || !newEvent.from_date || !newEvent.to_date) { return setError("Please fill all fields"); } setProcessing(true); @@ -145,7 +141,7 @@ function AcademicCalendar() { from_date: newEvent.from_date.toISOString().slice(0, 10), to_date: newEvent.to_date.toISOString().slice(0, 10), }, - { headers: { Authorization: `Token ${token}` } } + { headers: { Authorization: `Token ${token}` } }, ); setAddModalOpen(false); setRefreshTrigger((t) => t + 1); diff --git a/src/Modules/Academic/AdminAddDashboard.jsx b/src/Modules/Academic/AdminAddDashboard.jsx index 86d247038..9b91ebea6 100644 --- a/src/Modules/Academic/AdminAddDashboard.jsx +++ b/src/Modules/Academic/AdminAddDashboard.jsx @@ -1,28 +1,40 @@ -import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from "react"; import { - Title, Select, Group, Button, - Table, Text, Loader, Alert, - Card, Stack, Checkbox, Badge, Tabs, Modal, ActionIcon -} from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; -import { IconTrash } from '@tabler/icons-react'; -import axios from 'axios'; + Title, + Select, + Group, + Button, + Table, + Text, + Loader, + Alert, + Card, + Stack, + Checkbox, + Badge, + Tabs, + Modal, + ActionIcon, +} from "@mantine/core"; +import { showNotification } from "@mantine/notifications"; +import { IconTrash } from "@tabler/icons-react"; +import axios from "axios"; import { adminListAddRequestsRoute, approveAddRequestsRoute, deleteAddRequestsRoute, -} from '../../routes/academicRoutes'; +} from "../../routes/academicRoutes"; const SEMESTER_CHOICES = [ - { value: 'Odd Semester', label: 'Odd Semester' }, - { value: 'Even Semester', label: 'Even Semester' }, - { value: 'Summer Semester', label: 'Summer Semester' }, + { value: "Odd Semester", label: "Odd Semester" }, + { value: "Even Semester", label: "Even Semester" }, + { value: "Summer Semester", label: "Summer Semester" }, ]; const generateAcademicYears = () => { const currentYear = new Date().getFullYear(); - const yearsToShow = 5; + const yearsToShow = 5; return Array.from({ length: yearsToShow }, (_, i) => { const year = currentYear - i; const value = `${year}-${(year + 1).toString().slice(-2)}`; @@ -31,14 +43,14 @@ const generateAcademicYears = () => { }; export default function AdminAddDashboard() { - const [year, setYear] = useState(''); - const [semester, setSemester] = useState(''); + const [year, setYear] = useState(""); + const [semester, setSemester] = useState(""); const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedIds, setSelectedIds] = useState(new Set()); const [processing, setProcessing] = useState(false); - const [statusFilter, setStatusFilter] = useState(''); + const [statusFilter, setStatusFilter] = useState(""); const [deleteModal, setDeleteModal] = useState({ open: false, ids: [] }); const [deleting, setDeleting] = useState(false); @@ -47,9 +59,9 @@ export default function AdminAddDashboard() { const fetchRequests = useCallback(async () => { if (!year || !semester) return; - const token = localStorage.getItem('authToken'); + const token = localStorage.getItem("authToken"); if (!token) { - setError('Authentication required'); + setError("Authentication required"); return; } @@ -64,8 +76,9 @@ export default function AdminAddDashboard() { }); setRequests(Array.isArray(data) ? data : []); } catch (err) { - const errorMsg = err.response?.data?.error || err.response?.data?.detail || err.message; - setError(errorMsg || 'Failed to load requests'); + const errorMsg = + err.response?.data?.error || err.response?.data?.detail || err.message; + setError(errorMsg || "Failed to load requests"); } finally { setLoading(false); } @@ -76,7 +89,7 @@ export default function AdminAddDashboard() { }, [fetchRequests]); const toggleSelection = useCallback((id) => { - setSelectedIds(prev => { + setSelectedIds((prev) => { const newSet = new Set(prev); newSet.has(id) ? newSet.delete(id) : newSet.add(id); return newSet; @@ -84,103 +97,127 @@ export default function AdminAddDashboard() { }, []); const pendingRequests = useMemo( - () => requests.filter(r => r.status === 'Pending'), - [requests] + () => requests.filter((r) => r.status === "Pending"), + [requests], ); const processedRequests = useMemo( - () => requests.filter(r => r.status !== 'Pending'), - [requests] + () => requests.filter((r) => r.status !== "Pending"), + [requests], ); const filteredProcessedRequests = useMemo( - () => statusFilter ? processedRequests.filter(r => r.status === statusFilter) : processedRequests, - [processedRequests, statusFilter] + () => + statusFilter + ? processedRequests.filter((r) => r.status === statusFilter) + : processedRequests, + [processedRequests, statusFilter], ); const toggleSelectAll = useCallback(() => { - if (selectedIds.size === pendingRequests.length && pendingRequests.length > 0) { + if ( + selectedIds.size === pendingRequests.length && + pendingRequests.length > 0 + ) { setSelectedIds(new Set()); } else { - setSelectedIds(new Set(pendingRequests.map(r => r.id))); + setSelectedIds(new Set(pendingRequests.map((r) => r.id))); } }, [selectedIds.size, pendingRequests]); - const handleAction = useCallback(async (action) => { - if (selectedIds.size === 0) { - showNotification({ - title: 'No Selection', - message: `Please select at least one request to ${action}.`, - color: 'yellow', - }); - return; - } - - const token = localStorage.getItem('authToken'); - if (!token) { - showNotification({ - title: 'Authentication Error', - message: 'Please login again', - color: 'red' - }); - return; - } - - setProcessing(true); - try { - const response = await axios.post( - approveAddRequestsRoute, - { - request_ids: Array.from(selectedIds), - action - }, - { headers: { Authorization: `Token ${token}` } } - ); - - const summary = response.data?.summary; - const results = response.data?.results || []; - - // Check for errors in results - const errors = results.filter(r => r.status === 'error' || r.status === 'already_processed'); - - if (summary && summary.success > 0) { + const handleAction = useCallback( + async (action) => { + if (selectedIds.size === 0) { showNotification({ - title: 'Success', - message: `Processed ${summary.success} request(s) successfully${errors.length > 0 ? `. ${errors.length} failed.` : ''}`, - color: 'green' + title: "No Selection", + message: `Please select at least one request to ${action}.`, + color: "yellow", }); - } else if (errors.length > 0) { - const errorMessages = errors.map(e => `ID ${e.id}: ${e.detail || e.current_status || e.status}`).join(', '); + return; + } + + const token = localStorage.getItem("authToken"); + if (!token) { showNotification({ - title: 'Processing Failed', - message: errorMessages.length > 100 ? 'Some requests failed. Check console for details.' : errorMessages, - color: 'red', - autoClose: 8000 + title: "Authentication Error", + message: "Please login again", + color: "red", }); - } else { + return; + } + + setProcessing(true); + try { + const response = await axios.post( + approveAddRequestsRoute, + { + request_ids: Array.from(selectedIds), + action, + }, + { headers: { Authorization: `Token ${token}` } }, + ); + + const summary = response.data?.summary; + const results = response.data?.results || []; + + // Check for errors in results + const errors = results.filter( + (r) => r.status === "error" || r.status === "already_processed", + ); + + if (summary && summary.success > 0) { + showNotification({ + title: "Success", + message: `Processed ${summary.success} request(s) successfully${errors.length > 0 ? `. ${errors.length} failed.` : ""}`, + color: "green", + }); + } else if (errors.length > 0) { + const errorMessages = errors + .map( + (e) => `ID ${e.id}: ${e.detail || e.current_status || e.status}`, + ) + .join(", "); + showNotification({ + title: "Processing Failed", + message: + errorMessages.length > 100 + ? "Some requests failed. Check console for details." + : errorMessages, + color: "red", + autoClose: 8000, + }); + } else { + showNotification({ + title: "Warning", + message: "No requests were processed successfully", + color: "yellow", + }); + } + + setSelectedIds(new Set()); + await fetchRequests(); + } catch (err) { + const errorMsg = err.response?.data?.error || err.message; showNotification({ - title: 'Warning', - message: 'No requests were processed successfully', - color: 'yellow' + title: `${action === "approve" ? "Approval" : "Rejection"} Error`, + message: errorMsg || `Failed to ${action} requests`, + color: "red", }); + } finally { + setProcessing(false); } + }, + [selectedIds, fetchRequests], + ); - setSelectedIds(new Set()); - await fetchRequests(); - } catch (err) { - const errorMsg = err.response?.data?.error || err.message; - showNotification({ - title: `${action === 'approve' ? 'Approval' : 'Rejection'} Error`, - message: errorMsg || `Failed to ${action} requests`, - color: 'red', - }); - } finally { - setProcessing(false); - } - }, [selectedIds, fetchRequests]); - - const handleApprove = useCallback(() => handleAction('approve'), [handleAction]); - const handleReject = useCallback(() => handleAction('reject'), [handleAction]); + const handleApprove = useCallback( + () => handleAction("approve"), + [handleAction], + ); + const handleReject = useCallback( + () => handleAction("reject"), + [handleAction], + ); const openDeleteModal = useCallback((ids) => { setDeleteModal({ open: true, ids: Array.isArray(ids) ? ids : [ids] }); @@ -194,12 +231,12 @@ export default function AdminAddDashboard() { const { ids } = deleteModal; if (ids.length === 0) return; - const token = localStorage.getItem('authToken'); + const token = localStorage.getItem("authToken"); if (!token) { showNotification({ - title: 'Authentication Error', - message: 'Please login again', - color: 'red' + title: "Authentication Error", + message: "Please login again", + color: "red", }); return; } @@ -209,28 +246,28 @@ export default function AdminAddDashboard() { const response = await axios.post( deleteAddRequestsRoute, { request_ids: ids }, - { headers: { Authorization: `Token ${token}` } } + { headers: { Authorization: `Token ${token}` } }, ); showNotification({ - title: 'Success', + title: "Success", message: `Deleted ${response.data.deleted} request(s)`, - color: 'green' + color: "green", }); - setSelectedIds(prev => { + setSelectedIds((prev) => { const newSet = new Set(prev); - ids.forEach(id => newSet.delete(id)); + ids.forEach((id) => newSet.delete(id)); return newSet; }); - + closeDeleteModal(); await fetchRequests(); } catch (err) { showNotification({ - title: 'Delete Failed', + title: "Delete Failed", message: err.response?.data?.error || err.message, - color: 'red', + color: "red", }); } finally { setDeleting(false); @@ -240,60 +277,72 @@ export default function AdminAddDashboard() { const handleBulkDelete = useCallback(() => { if (selectedIds.size === 0) { showNotification({ - title: 'No Selection', - message: 'Please select at least one request to delete', - color: 'yellow', + title: "No Selection", + message: "Please select at least one request to delete", + color: "yellow", }); return; } openDeleteModal(Array.from(selectedIds)); }, [selectedIds, openDeleteModal]); - const exportToExcel = useCallback((data, filename) => { - if (data.length === 0) { - showNotification({ - title: 'No Data', - message: 'No data available to export', - color: 'yellow', - }); - return; - } + const exportToExcel = useCallback( + (data, filename) => { + if (data.length === 0) { + showNotification({ + title: "No Data", + message: "No data available to export", + color: "yellow", + }); + return; + } - const headers = ['Student ID', 'Student Name', 'Slot', 'Course Code', 'Course Name', 'Status', 'Requested At', 'Processed At']; - const csvRows = [headers.join(',')]; - - data.forEach(r => { - const row = [ - r.student || '', - r.student_name || '', - r.slot || '', - r.course || '', - r.course_name || '', - r.status || '', - r.created_at ? new Date(r.created_at).toLocaleString() : '', - r.processed_at ? new Date(r.processed_at).toLocaleString() : '' + const headers = [ + "Student ID", + "Student Name", + "Slot", + "Course Code", + "Course Name", + "Status", + "Requested At", + "Processed At", ]; - csvRows.push(row.map(val => `"${val}"`).join(',')); - }); + const csvRows = [headers.join(",")]; + + data.forEach((r) => { + const row = [ + r.student || "", + r.student_name || "", + r.slot || "", + r.course || "", + r.course_name || "", + r.status || "", + r.created_at ? new Date(r.created_at).toLocaleString() : "", + r.processed_at ? new Date(r.processed_at).toLocaleString() : "", + ]; + csvRows.push(row.map((val) => `"${val}"`).join(",")); + }); - const csvContent = csvRows.join('\n'); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - - link.setAttribute('href', url); - link.setAttribute('download', `${filename}_${year}_${semester}.csv`); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - showNotification({ - title: 'Success', - message: 'Data exported successfully', - color: 'green', - }); - }, [year, semester]); + const csvContent = csvRows.join("\n"); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + + link.setAttribute("href", url); + link.setAttribute("download", `${filename}_${year}_${semester}.csv`); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + showNotification({ + title: "Success", + message: "Data exported successfully", + color: "green", + }); + }, + [year, semester], + ); return ( <> @@ -353,8 +402,10 @@ export default function AdminAddDashboard() { ) : error ? ( - {error} - ) : (!year || !semester) ? ( + + {error} + + ) : !year || !semester ? ( Select academic year and semester to view add course requests. @@ -387,7 +438,9 @@ export default function AdminAddDashboard() { @@ -398,9 +451,15 @@ export default function AdminAddDashboard() { 0} + checked={ + selectedIds.size === pendingRequests.length && + pendingRequests.length > 0 + } onChange={toggleSelectAll} - indeterminate={selectedIds.size > 0 && selectedIds.size < pendingRequests.length} + indeterminate={ + selectedIds.size > 0 && + selectedIds.size < pendingRequests.length + } /> Student @@ -412,7 +471,7 @@ export default function AdminAddDashboard() { - {pendingRequests.map(r => ( + {pendingRequests.map((r) => ( {r.student} {r.student_name && ( - {r.student_name} + + {r.student_name} + )} {r.slot} {r.course} {r.course_name && ( - {r.course_name} + + {r.course_name} + )} @@ -467,7 +530,12 @@ export default function AdminAddDashboard() { @@ -486,9 +554,9 @@ export default function AdminAddDashboard() { value={statusFilter} onChange={setStatusFilter} data={[ - { value: '', label: 'All' }, - { value: 'Approved', label: 'Approved' }, - { value: 'Rejected', label: 'Rejected' }, + { value: "", label: "All" }, + { value: "Approved", label: "Approved" }, + { value: "Rejected", label: "Rejected" }, ]} size="xs" style={{ width: 100 }} @@ -501,23 +569,29 @@ export default function AdminAddDashboard() { - {filteredProcessedRequests.map(r => ( + {filteredProcessedRequests.map((r) => ( {r.student} {r.student_name && ( - {r.student_name} + + {r.student_name} + )} {r.slot} {r.course} {r.course_name && ( - {r.course_name} + + {r.course_name} + )} - + {r.status} @@ -525,7 +599,7 @@ export default function AdminAddDashboard() { {r.processed_at ? new Date(r.processed_at).toLocaleString() - : '—'} + : "—"} ))} @@ -550,13 +624,18 @@ export default function AdminAddDashboard() { closeOnEscape={!deleting} > - Are you sure you want to permanently delete {deleteModal.ids.length} course add request{deleteModal.ids.length > 1 ? 's' : ''}? + Are you sure you want to permanently delete {deleteModal.ids.length}{" "} + course add request{deleteModal.ids.length > 1 ? "s" : ""}? This action cannot be undone. - + - {loadingStudents && ( -
- -
- )} + {loadingStudents && ( +
+ +
+ )} - {error && ( - - {error} - - )} + {error && ( + + {error} + + )} - {students.length > 0 && !loadingStudents && ( - <> - - - - - - - - - - - {students.map((st) => ( - - - - - - - ))} - -
UsernameCurrent BatchNew BatchNew Year
{st.username}{st.current_batch} - - - - handleYearChange(st.id, e.currentTarget.value) - } - /> -
- - - - - - )} - - setModalOpen(false)} - title="Confirm Batch Change" - > - You are changing the batch for these students: - + {students.length > 0 && !loadingStudents && ( + <> +
- - + + + - {toConfirm.map((st) => { - const change = changes[st.id]; - const newBObj = batches.find( - (b) => String(b.id) === String(change.new_batch_id) - ); - const newLabel = newBObj - ? `${newBObj.label} / ${change.new_batch_year}` - : "-"; - return ( - - - - - - ); - })} + {students.map((st) => ( + + + + + + + ))}
UsernameOld BatchNew Batch / YearCurrent BatchNew BatchNew Year
{st.username}{st.current_batch}{newLabel}
{st.username}{st.current_batch} + + + + handleYearChange(st.id, e.currentTarget.value) + } + /> +
+ - - -
- + + )} + + setModalOpen(false)} + title="Confirm Batch Change" + > + You are changing the batch for these students: + + + + + + + + + + {toConfirm.map((st) => { + const change = changes[st.id]; + const newBObj = batches.find( + (b) => String(b.id) === String(change.new_batch_id), + ); + const newLabel = newBObj + ? `${newBObj.label} / ${change.new_batch_year}` + : "-"; + return ( + + + + + + ); + })} + +
UsernameOld BatchNew Batch / Year
{st.username}{st.current_batch}{newLabel}
+ + + + +
+ ); } diff --git a/src/Modules/Academic/AdminDropDashboard.jsx b/src/Modules/Academic/AdminDropDashboard.jsx index f9e3882ff..5ac9850ae 100644 --- a/src/Modules/Academic/AdminDropDashboard.jsx +++ b/src/Modules/Academic/AdminDropDashboard.jsx @@ -1,23 +1,35 @@ -import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from "react"; import { - Title, Select, Group, Button, - Table, Text, Loader, Alert, - Card, Stack, Checkbox, Badge, Tabs, Modal, ActionIcon -} from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; -import { IconTrash } from '@tabler/icons-react'; -import axios from 'axios'; + Title, + Select, + Group, + Button, + Table, + Text, + Loader, + Alert, + Card, + Stack, + Checkbox, + Badge, + Tabs, + Modal, + ActionIcon, +} from "@mantine/core"; +import { showNotification } from "@mantine/notifications"; +import { IconTrash } from "@tabler/icons-react"; +import axios from "axios"; import { adminListDropRequestsRoute, approveDropRequestsRoute, deleteDropRequestsRoute, -} from '../../routes/academicRoutes'; +} from "../../routes/academicRoutes"; const SEMESTER_CHOICES = [ - { value: 'Odd Semester', label: 'Odd Semester' }, - { value: 'Even Semester', label: 'Even Semester' }, - { value: 'Summer Semester', label: 'Summer Semester' }, + { value: "Odd Semester", label: "Odd Semester" }, + { value: "Even Semester", label: "Even Semester" }, + { value: "Summer Semester", label: "Summer Semester" }, ]; const generateAcademicYears = () => { @@ -31,14 +43,14 @@ const generateAcademicYears = () => { }; export default function AdminDropDashboard() { - const [year, setYear] = useState(''); - const [semester, setSemester] = useState(''); + const [year, setYear] = useState(""); + const [semester, setSemester] = useState(""); const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedIds, setSelectedIds] = useState(new Set()); const [processing, setProcessing] = useState(false); - const [statusFilter, setStatusFilter] = useState(''); + const [statusFilter, setStatusFilter] = useState(""); const [deleteModal, setDeleteModal] = useState({ open: false, ids: [] }); const [deleting, setDeleting] = useState(false); @@ -47,9 +59,9 @@ export default function AdminDropDashboard() { const fetchRequests = useCallback(async () => { if (!year || !semester) return; - const token = localStorage.getItem('authToken'); + const token = localStorage.getItem("authToken"); if (!token) { - setError('Authentication required'); + setError("Authentication required"); return; } @@ -64,8 +76,9 @@ export default function AdminDropDashboard() { }); setRequests(Array.isArray(data) ? data : []); } catch (err) { - const errorMsg = err.response?.data?.error || err.response?.data?.detail || err.message; - setError(errorMsg || 'Failed to load requests'); + const errorMsg = + err.response?.data?.error || err.response?.data?.detail || err.message; + setError(errorMsg || "Failed to load requests"); } finally { setLoading(false); } @@ -76,7 +89,7 @@ export default function AdminDropDashboard() { }, [fetchRequests]); const toggleSelection = useCallback((id) => { - setSelectedIds(prev => { + setSelectedIds((prev) => { const newSet = new Set(prev); newSet.has(id) ? newSet.delete(id) : newSet.add(id); return newSet; @@ -84,84 +97,99 @@ export default function AdminDropDashboard() { }, []); const pendingRequests = useMemo( - () => requests.filter(r => r.status === 'Pending'), - [requests] + () => requests.filter((r) => r.status === "Pending"), + [requests], ); const processedRequests = useMemo( - () => requests.filter(r => r.status !== 'Pending'), - [requests] + () => requests.filter((r) => r.status !== "Pending"), + [requests], ); const filteredProcessedRequests = useMemo( - () => statusFilter ? processedRequests.filter(r => r.status === statusFilter) : processedRequests, - [processedRequests, statusFilter] + () => + statusFilter + ? processedRequests.filter((r) => r.status === statusFilter) + : processedRequests, + [processedRequests, statusFilter], ); const toggleSelectAll = useCallback(() => { - if (selectedIds.size === pendingRequests.length && pendingRequests.length > 0) { + if ( + selectedIds.size === pendingRequests.length && + pendingRequests.length > 0 + ) { setSelectedIds(new Set()); } else { - setSelectedIds(new Set(pendingRequests.map(r => r.id))); + setSelectedIds(new Set(pendingRequests.map((r) => r.id))); } }, [selectedIds.size, pendingRequests]); - const handleAction = useCallback(async (action) => { - if (selectedIds.size === 0) { - showNotification({ - title: 'No Selection', - message: `Please select at least one request to ${action}.`, - color: 'yellow', - }); - return; - } - - const token = localStorage.getItem('authToken'); - if (!token) { - showNotification({ - title: 'Authentication Error', - message: 'Please login again', - color: 'red' - }); - return; - } - - setProcessing(true); - try { - const response = await axios.post( - approveDropRequestsRoute, - { - request_ids: Array.from(selectedIds), - action - }, - { headers: { Authorization: `Token ${token}` } } - ); - - const summary = response.data?.summary; - showNotification({ - title: 'Success', - message: summary - ? `Processed ${summary.success} request(s) successfully` - : `${action === 'approve' ? 'Approved' : 'Rejected'} ${selectedIds.size} request(s)`, - color: 'green' - }); - - setSelectedIds(new Set()); - await fetchRequests(); - } catch (err) { - const errorMsg = err.response?.data?.error || err.message; - showNotification({ - title: `${action === 'approve' ? 'Approval' : 'Rejection'} Error`, - message: errorMsg || `Failed to ${action} requests`, - color: 'red', - }); - } finally { - setProcessing(false); - } - }, [selectedIds, fetchRequests]); + const handleAction = useCallback( + async (action) => { + if (selectedIds.size === 0) { + showNotification({ + title: "No Selection", + message: `Please select at least one request to ${action}.`, + color: "yellow", + }); + return; + } + + const token = localStorage.getItem("authToken"); + if (!token) { + showNotification({ + title: "Authentication Error", + message: "Please login again", + color: "red", + }); + return; + } + + setProcessing(true); + try { + const response = await axios.post( + approveDropRequestsRoute, + { + request_ids: Array.from(selectedIds), + action, + }, + { headers: { Authorization: `Token ${token}` } }, + ); + + const summary = response.data?.summary; + showNotification({ + title: "Success", + message: summary + ? `Processed ${summary.success} request(s) successfully` + : `${action === "approve" ? "Approved" : "Rejected"} ${selectedIds.size} request(s)`, + color: "green", + }); + + setSelectedIds(new Set()); + await fetchRequests(); + } catch (err) { + const errorMsg = err.response?.data?.error || err.message; + showNotification({ + title: `${action === "approve" ? "Approval" : "Rejection"} Error`, + message: errorMsg || `Failed to ${action} requests`, + color: "red", + }); + } finally { + setProcessing(false); + } + }, + [selectedIds, fetchRequests], + ); - const handleApprove = useCallback(() => handleAction('approve'), [handleAction]); - const handleReject = useCallback(() => handleAction('reject'), [handleAction]); + const handleApprove = useCallback( + () => handleAction("approve"), + [handleAction], + ); + const handleReject = useCallback( + () => handleAction("reject"), + [handleAction], + ); const openDeleteModal = useCallback((ids) => { setDeleteModal({ open: true, ids: Array.isArray(ids) ? ids : [ids] }); @@ -175,12 +203,12 @@ export default function AdminDropDashboard() { const { ids } = deleteModal; if (ids.length === 0) return; - const token = localStorage.getItem('authToken'); + const token = localStorage.getItem("authToken"); if (!token) { showNotification({ - title: 'Authentication Error', - message: 'Please login again', - color: 'red' + title: "Authentication Error", + message: "Please login again", + color: "red", }); return; } @@ -190,28 +218,28 @@ export default function AdminDropDashboard() { const response = await axios.post( deleteDropRequestsRoute, { request_ids: ids }, - { headers: { Authorization: `Token ${token}` } } + { headers: { Authorization: `Token ${token}` } }, ); showNotification({ - title: 'Success', + title: "Success", message: `Deleted ${response.data.deleted} request(s)`, - color: 'green' + color: "green", }); - setSelectedIds(prev => { + setSelectedIds((prev) => { const newSet = new Set(prev); - ids.forEach(id => newSet.delete(id)); + ids.forEach((id) => newSet.delete(id)); return newSet; }); - + closeDeleteModal(); await fetchRequests(); } catch (err) { showNotification({ - title: 'Delete Failed', + title: "Delete Failed", message: err.response?.data?.error || err.message, - color: 'red', + color: "red", }); } finally { setDeleting(false); @@ -221,60 +249,72 @@ export default function AdminDropDashboard() { const handleBulkDelete = useCallback(() => { if (selectedIds.size === 0) { showNotification({ - title: 'No Selection', - message: 'Please select at least one request to delete', - color: 'yellow', + title: "No Selection", + message: "Please select at least one request to delete", + color: "yellow", }); return; } openDeleteModal(Array.from(selectedIds)); }, [selectedIds, openDeleteModal]); - const exportToExcel = useCallback((data, filename) => { - if (data.length === 0) { - showNotification({ - title: 'No Data', - message: 'No data available to export', - color: 'yellow', + const exportToExcel = useCallback( + (data, filename) => { + if (data.length === 0) { + showNotification({ + title: "No Data", + message: "No data available to export", + color: "yellow", + }); + return; + } + + const headers = [ + "Student ID", + "Student Name", + "Slot", + "Course Code", + "Course Name", + "Status", + "Requested At", + "Processed At", + ]; + const csvRows = [headers.join(",")]; + + data.forEach((r) => { + const row = [ + r.student || "", + r.student_name || "", + r.slot || "", + r.course || "", + r.course_name || "", + r.status || "", + r.created_at ? new Date(r.created_at).toLocaleString() : "", + r.processed_at ? new Date(r.processed_at).toLocaleString() : "", + ]; + csvRows.push(row.map((val) => `"${val}"`).join(",")); }); - return; - } - const headers = ['Student ID', 'Student Name', 'Slot', 'Course Code', 'Course Name', 'Status', 'Requested At', 'Processed At']; - const csvRows = [headers.join(',')]; - - data.forEach(r => { - const row = [ - r.student || '', - r.student_name || '', - r.slot || '', - r.course || '', - r.course_name || '', - r.status || '', - r.created_at ? new Date(r.created_at).toLocaleString() : '', - r.processed_at ? new Date(r.processed_at).toLocaleString() : '' - ]; - csvRows.push(row.map(val => `"${val}"`).join(',')); - }); + const csvContent = csvRows.join("\n"); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); - const csvContent = csvRows.join('\n'); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - - link.setAttribute('href', url); - link.setAttribute('download', `${filename}_${year}_${semester}.csv`); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - showNotification({ - title: 'Success', - message: 'Data exported successfully', - color: 'green', - }); - }, [year, semester]); + link.setAttribute("href", url); + link.setAttribute("download", `${filename}_${year}_${semester}.csv`); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + showNotification({ + title: "Success", + message: "Data exported successfully", + color: "green", + }); + }, + [year, semester], + ); return ( <> @@ -334,8 +374,10 @@ export default function AdminDropDashboard() { ) : error ? ( - {error} - ) : (!year || !semester) ? ( + + {error} + + ) : !year || !semester ? ( Select academic year and semester to view drop requests. @@ -368,7 +410,9 @@ export default function AdminDropDashboard() { @@ -379,9 +423,15 @@ export default function AdminDropDashboard() { 0} + checked={ + selectedIds.size === pendingRequests.length && + pendingRequests.length > 0 + } onChange={toggleSelectAll} - indeterminate={selectedIds.size > 0 && selectedIds.size < pendingRequests.length} + indeterminate={ + selectedIds.size > 0 && + selectedIds.size < pendingRequests.length + } /> Student @@ -393,7 +443,7 @@ export default function AdminDropDashboard() { - {pendingRequests.map(r => ( + {pendingRequests.map((r) => ( {r.student} {r.student_name && ( - {r.student_name} + + {r.student_name} + )} {r.slot} {r.course} {r.course_name && ( - {r.course_name} + + {r.course_name} + )} @@ -448,7 +502,12 @@ export default function AdminDropDashboard() { @@ -467,9 +526,9 @@ export default function AdminDropDashboard() { value={statusFilter} onChange={setStatusFilter} data={[ - { value: '', label: 'All' }, - { value: 'Approved', label: 'Approved' }, - { value: 'Rejected', label: 'Rejected' }, + { value: "", label: "All" }, + { value: "Approved", label: "Approved" }, + { value: "Rejected", label: "Rejected" }, ]} size="xs" style={{ width: 100 }} @@ -482,23 +541,29 @@ export default function AdminDropDashboard() { - {filteredProcessedRequests.map(r => ( + {filteredProcessedRequests.map((r) => ( {r.student} {r.student_name && ( - {r.student_name} + + {r.student_name} + )} {r.slot} {r.course} {r.course_name && ( - {r.course_name} + + {r.course_name} + )} - + {r.status} @@ -506,7 +571,7 @@ export default function AdminDropDashboard() { {r.processed_at ? new Date(r.processed_at).toLocaleString() - : '—'} + : "—"} ))} @@ -531,13 +596,18 @@ export default function AdminDropDashboard() { closeOnEscape={!deleting} > - Are you sure you want to permanently delete {deleteModal.ids.length} course drop request{deleteModal.ids.length > 1 ? 's' : ''}? + Are you sure you want to permanently delete {deleteModal.ids.length}{" "} + course drop request{deleteModal.ids.length > 1 ? "s" : ""}? This action cannot be undone. -