diff --git a/src/Modules/Academic/AdminAddDashboard.jsx b/src/Modules/Academic/AdminAddDashboard.jsx index dfb2ffbb0..22e157084 100644 --- a/src/Modules/Academic/AdminAddDashboard.jsx +++ b/src/Modules/Academic/AdminAddDashboard.jsx @@ -26,6 +26,7 @@ import { deleteAddRequestsRoute, } from "../../routes/academicRoutes"; import { buildSessionOptions } from "../../lib/academicYear"; +import { getApiErrorMessage } from "../../helper/apiError"; const SEMESTER_CHOICES = [ { value: "Odd Semester", label: "Odd Semester" }, @@ -71,7 +72,7 @@ export default function AdminAddDashboard() { setRequests(Array.isArray(data) ? data : []); } catch (err) { const errorMsg = - err.response?.data?.error || err.response?.data?.detail || err.message; + getApiErrorMessage(err); setError(errorMsg || "Failed to load requests"); } finally { setLoading(false); @@ -212,7 +213,7 @@ export default function AdminAddDashboard() { setSelectedIds(new Set()); await fetchRequests(); } catch (err) { - const errorMsg = err.response?.data?.error || err.message; + const errorMsg = getApiErrorMessage(err); showNotification({ title: `${action === "approve" ? "Approval" : "Rejection"} Error`, message: errorMsg || `Failed to ${action} requests`, @@ -281,7 +282,7 @@ export default function AdminAddDashboard() { } catch (err) { showNotification({ title: "Delete Failed", - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/AdminDropDashboard.jsx b/src/Modules/Academic/AdminDropDashboard.jsx index 446a1bd1a..bc3c716db 100644 --- a/src/Modules/Academic/AdminDropDashboard.jsx +++ b/src/Modules/Academic/AdminDropDashboard.jsx @@ -26,6 +26,7 @@ import { deleteDropRequestsRoute, } from "../../routes/academicRoutes"; import { buildSessionOptions } from "../../lib/academicYear"; +import { getApiErrorMessage } from "../../helper/apiError"; const SEMESTER_CHOICES = [ { value: "Odd Semester", label: "Odd Semester" }, @@ -71,7 +72,7 @@ export default function AdminDropDashboard() { setRequests(Array.isArray(data) ? data : []); } catch (err) { const errorMsg = - err.response?.data?.error || err.response?.data?.detail || err.message; + getApiErrorMessage(err); setError(errorMsg || "Failed to load requests"); } finally { setLoading(false); @@ -184,7 +185,7 @@ export default function AdminDropDashboard() { setSelectedIds(new Set()); await fetchRequests(); } catch (err) { - const errorMsg = err.response?.data?.error || err.message; + const errorMsg = getApiErrorMessage(err); showNotification({ title: `${action === "approve" ? "Approval" : "Rejection"} Error`, message: errorMsg || `Failed to ${action} requests`, @@ -253,7 +254,7 @@ export default function AdminDropDashboard() { } catch (err) { showNotification({ title: "Delete Failed", - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/AdminPhDCourseRequests.jsx b/src/Modules/Academic/AdminPhDCourseRequests.jsx index 21d152ebd..c2b4be3b5 100644 --- a/src/Modules/Academic/AdminPhDCourseRequests.jsx +++ b/src/Modules/Academic/AdminPhDCourseRequests.jsx @@ -34,6 +34,7 @@ import { adminVerifyTeachingCreditEnrollmentsRoute, adminRejectTeachingCreditEnrollmentsRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; const TYPE_LABEL = { course: "Course", @@ -179,7 +180,7 @@ export default function AdminPhDCourseRequests() { setRows(merged); } catch (err) { setError( - err.response?.data?.error || err.message || "Failed to load requests", + getApiErrorMessage(err, "Failed to load requests"), ); } finally { setLoading(false); @@ -319,7 +320,7 @@ export default function AdminPhDCourseRequests() { } catch (err) { showNotification({ title: `${action === "approve" ? "Approval" : "Rejection"} Error`, - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/AdminReplacementDashboard.jsx b/src/Modules/Academic/AdminReplacementDashboard.jsx index db86878a0..598a6bac8 100644 --- a/src/Modules/Academic/AdminReplacementDashboard.jsx +++ b/src/Modules/Academic/AdminReplacementDashboard.jsx @@ -16,6 +16,7 @@ import { deleteReplacementRequestsRoute, } from '../../routes/academicRoutes'; import { buildSessionLabels } from '../../lib/academicYear'; +import { getApiErrorMessage } from "../../helper/apiError"; const SEMESTER_CHOICES = [ { value: 'Odd Semester', label: 'Odd Semester' }, @@ -99,7 +100,7 @@ export default function AdminReplacementDashboard() { headers: { Authorization: `Token ${token}` }, }) .then(({ data }) => setRequests(Array.isArray(data) ? data : [])) - .catch(err => setError(err.response?.data?.detail || err.message)) + .catch(err => setError(getApiErrorMessage(err))) .finally(() => setLoading(false)); }, [year, semester]); @@ -150,7 +151,7 @@ export default function AdminReplacementDashboard() { .catch(err => { showNotification({ title: 'Allocation Error', - message: err.response?.data?.detail || err.message, + message: getApiErrorMessage(err), color: 'red', }); }) @@ -251,7 +252,7 @@ export default function AdminReplacementDashboard() { } catch (err) { showNotification({ title: 'Delete Failed', - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: 'red', }); } finally { @@ -308,7 +309,7 @@ export default function AdminReplacementDashboard() { } catch (err) { showNotification({ title: 'Revert Failed', - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: 'red', }); } finally { diff --git a/src/Modules/Academic/AllotCourses.jsx b/src/Modules/Academic/AllotCourses.jsx index 0afe102dc..6ece524f6 100644 --- a/src/Modules/Academic/AllotCourses.jsx +++ b/src/Modules/Academic/AllotCourses.jsx @@ -24,6 +24,7 @@ import { allotTeachingCreditRoute, listBatchesRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; // Course needs a per-row slot/course lookup + academic year + semester type. // Thesis/Seminar/Teaching Credit each have at most one slot per semester, so @@ -140,8 +141,7 @@ export default function AllotCourses() { console.error("API Error:", err); const errorMsg = err.response?.data?.message || - err.response?.data?.error || - err.message; + getApiErrorMessage(err); showNotification({ title: "Error fetching batches", message: errorMsg, diff --git a/src/Modules/Academic/GenerateStudentList.jsx b/src/Modules/Academic/GenerateStudentList.jsx index c86baff19..7b81cb7ae 100644 --- a/src/Modules/Academic/GenerateStudentList.jsx +++ b/src/Modules/Academic/GenerateStudentList.jsx @@ -33,6 +33,7 @@ import { listBatchesRoute, generateprereport, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; const generateAcademicYears = () => buildSessionLabels(new Date().getFullYear()); @@ -130,7 +131,7 @@ export default function GenerateStudentList({ view = "rolllist" }) { ); } catch (err) { console.error(err); - setError(err.response?.data?.detail || err.message); + setError(getApiErrorMessage(err)); } finally { setLoading(false); } @@ -194,7 +195,7 @@ export default function GenerateStudentList({ view = "rolllist" }) { showNotification({ title: "Error", message: `Failed to fetch preview data: ${ - err.response?.data?.detail || err.message + getApiErrorMessage(err) }`, color: "red", }); @@ -243,7 +244,7 @@ export default function GenerateStudentList({ view = "rolllist" }) { } catch (err) { showNotification({ title: "Export failed", - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { @@ -325,7 +326,7 @@ export default function GenerateStudentList({ view = "rolllist" }) { console.error("Generate List Error:", err); showNotification({ title: "Error", - message: err.response?.data?.detail || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { @@ -393,7 +394,7 @@ export default function GenerateStudentList({ view = "rolllist" }) { setPreRegData(res.data); } catch (err) { const msg = - err.response?.data?.detail || err.message || "Failed to fetch report"; + getApiErrorMessage(err, "Failed to fetch report"); setPreRegError(msg); showNotification({ title: "Error", message: msg, color: "red" }); } finally { @@ -450,7 +451,7 @@ export default function GenerateStudentList({ view = "rolllist" }) { } catch (err) { showNotification({ title: "Export failed", - message: err.response?.data?.detail || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/PhDCourseRegistration.jsx b/src/Modules/Academic/PhDCourseRegistration.jsx index 526be55ad..9e211c2fc 100644 --- a/src/Modules/Academic/PhDCourseRegistration.jsx +++ b/src/Modules/Academic/PhDCourseRegistration.jsx @@ -47,6 +47,7 @@ import { sourceOf, takenCourseIds, } from "./lib/blSlot"; +import { getApiErrorMessage } from "../../helper/apiError"; const SUMMARY_COLUMNS = ["Type", "Slot", "Detail", "Credits"]; const REQUEST_COLUMNS = [ @@ -176,7 +177,7 @@ function PhDCourseRegisterForm() { } catch (err) { if (isStale()) return; if (err.response?.status !== 403) { - setError(err.response?.data?.error || err.message); + setError(getApiErrorMessage(err)); } } finally { if (!isStale()) setLoading(false); @@ -379,7 +380,7 @@ function PhDCourseRegisterForm() { } catch (err) { showNotification({ title: "Submit failed", - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", autoClose: 8000, }); @@ -785,7 +786,7 @@ function PhDMyCourseRequests() { setSeminarInfo(seminarRes.data); setTeachingCreditInfo(teachingCreditRes.data); } catch (err) { - setError(err.response?.data?.error || err.message); + setError(getApiErrorMessage(err)); } finally { setLoading(false); } diff --git a/src/Modules/Academic/ReplacementRequestStudent.jsx b/src/Modules/Academic/ReplacementRequestStudent.jsx index 3564547ae..d75f47cd6 100644 --- a/src/Modules/Academic/ReplacementRequestStudent.jsx +++ b/src/Modules/Academic/ReplacementRequestStudent.jsx @@ -13,6 +13,7 @@ import { studentDropRequestsRoute, studentAddRequestsRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; const REPLACEMENT_COLUMNS = [ "Old course", @@ -79,7 +80,7 @@ export default function ReplacementRequestStudent() { setDropRequests(dropRes.data); setAddRequests(addRes.data); }) - .catch((err) => setError(err.response?.data?.detail || err.message)) + .catch((err) => setError(getApiErrorMessage(err))) .finally(() => setLoading(false)); }, []); diff --git a/src/Modules/Academic/SectionAssignment.jsx b/src/Modules/Academic/SectionAssignment.jsx index df30a7e6d..9376ecf2a 100644 --- a/src/Modules/Academic/SectionAssignment.jsx +++ b/src/Modules/Academic/SectionAssignment.jsx @@ -22,6 +22,7 @@ import { assignSectionRoute, } from "../../routes/academicRoutes"; import { useSectionsInUse } from "../../lib/sections"; +import { getApiErrorMessage } from "../../helper/apiError"; const OTHER = "__OTHER__"; @@ -60,7 +61,7 @@ function SectionAssignment() { } catch (err) { showNotification({ title: "Failed to load batches", - message: err.response?.data?.detail || err.message, + message: getApiErrorMessage(err), color: "red", }); } @@ -108,7 +109,7 @@ function SectionAssignment() { } catch (err) { showNotification({ title: "Failed to load students", - message: err.response?.data?.detail || err.message, + message: getApiErrorMessage(err), color: "red", }); setStudents([]); @@ -311,7 +312,7 @@ function SectionAssignment() { } catch (err) { showNotification({ title: "Assignment failed", - message: err.response?.data?.detail || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/StudentAddCourse.jsx b/src/Modules/Academic/StudentAddCourse.jsx index ad4361ecc..039dce1de 100644 --- a/src/Modules/Academic/StudentAddCourse.jsx +++ b/src/Modules/Academic/StudentAddCourse.jsx @@ -12,6 +12,7 @@ import { studentAvailableAddCoursesRoute, studentAddCourseRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; export default function StudentAddCourse() { const [slots, setSlots] = useState([]); @@ -55,7 +56,7 @@ export default function StudentAddCourse() { setSlots(enrichedSlots); }) - .catch((err) => setError(err.response?.data?.error || err.message)) + .catch((err) => setError(getApiErrorMessage(err))) .finally(() => setLoading(false)); }, []); @@ -161,7 +162,7 @@ export default function StudentAddCourse() { } catch (err) { showNotification({ title: "Submit failed", - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/StudentCourseReplacement.jsx b/src/Modules/Academic/StudentCourseReplacement.jsx index 844bd1f2c..3e0a881e3 100644 --- a/src/Modules/Academic/StudentCourseReplacement.jsx +++ b/src/Modules/Academic/StudentCourseReplacement.jsx @@ -21,6 +21,7 @@ import { studentBatchCreateRoute, studentListRequestsRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; export default function StudentCourseReplacement() { const [slots, setSlots] = useState([]); @@ -59,7 +60,7 @@ export default function StudentCourseReplacement() { } }) .catch((err) => { - setError(err.response?.data?.error || err.message); + setError(getApiErrorMessage(err)); }) .finally(() => setLoading(false)); }, []); @@ -120,7 +121,7 @@ export default function StudentCourseReplacement() { } catch (err) { showNotification({ title: "Submit failed", - message: err.response?.data?.error || err.message, + message: getApiErrorMessage(err), color: "red", }); } finally { diff --git a/src/Modules/Academic/StudentCourses.jsx b/src/Modules/Academic/StudentCourses.jsx index 947ef9e08..d2e4e5d49 100644 --- a/src/Modules/Academic/StudentCourses.jsx +++ b/src/Modules/Academic/StudentCourses.jsx @@ -36,6 +36,7 @@ import { adminProgressSeminarEnrollmentListRoute, adminTeachingCreditEnrollmentListRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; function authHeaders() { return { Authorization: `Token ${localStorage.getItem("authToken")}` }; @@ -225,7 +226,7 @@ export default function StudentCourses() { }); } } catch (err) { - setError(err.response?.data?.error || err.message || "Fetch failed"); + setError(getApiErrorMessage(err, "Fetch failed")); } finally { setLoading(false); } @@ -291,7 +292,7 @@ export default function StudentCourses() { }); await handleGetCourses(); } catch (err) { - setError(err.response?.data?.error || err.message || "Drop failed"); + setError(getApiErrorMessage(err, "Drop failed")); } finally { setLoading(false); setDropModalOpen(false); @@ -361,10 +362,7 @@ export default function StudentCourses() { } } catch (err) { setError( - err.response?.data?.error || - err.response?.data?.detail || - err.message || - "Add failed", + getApiErrorMessage(err, "Add failed"), ); } finally { setLoading(false); @@ -537,7 +535,7 @@ export default function StudentCourses() { }); await handleGetCourses(); } catch (err) { - setError(err.response?.data?.error || err.message || "Add failed"); + setError(getApiErrorMessage(err, "Add failed")); } finally { setLoading(false); } @@ -576,7 +574,7 @@ export default function StudentCourses() { }); await handleGetCourses(); } catch (err) { - setError(err.response?.data?.error || err.message || "Add failed"); + setError(getApiErrorMessage(err, "Add failed")); } finally { setLoading(false); } @@ -617,7 +615,7 @@ export default function StudentCourses() { }); await handleGetCourses(); } catch (err) { - setError(err.response?.data?.error || err.message || "Add failed"); + setError(getApiErrorMessage(err, "Add failed")); } finally { setLoading(false); } diff --git a/src/Modules/Academic/StudentDropCourse.jsx b/src/Modules/Academic/StudentDropCourse.jsx index f9b4b6d3f..90c872e2b 100644 --- a/src/Modules/Academic/StudentDropCourse.jsx +++ b/src/Modules/Academic/StudentDropCourse.jsx @@ -10,6 +10,7 @@ import { studentDropRegistrationsRoute, studentDropCourseRoute, } from "../../routes/academicRoutes"; +import { getApiErrorMessage } from "../../helper/apiError"; export default function StudentDropCourse() { const [regs, setRegs] = useState([]); @@ -35,7 +36,7 @@ export default function StudentDropCourse() { setError(null); } catch (err) { const errorMsg = - err.response?.data?.error || err.response?.data?.message || err.message; + getApiErrorMessage(err); setError(errorMsg || "Failed to load courses"); } finally { setLoading(false); @@ -83,7 +84,7 @@ export default function StudentDropCourse() { closeModal(); } catch (err) { const errorMsg = - err.response?.data?.error || err.response?.data?.message || err.message; + getApiErrorMessage(err); showNotification({ title: "Drop Request Failed", message: errorMsg || "Failed to submit drop request", diff --git a/src/Modules/Academic/courseRegistrationReceipt.js b/src/Modules/Academic/courseRegistrationReceipt.js index 498b1115e..418976c5a 100644 --- a/src/Modules/Academic/courseRegistrationReceipt.js +++ b/src/Modules/Academic/courseRegistrationReceipt.js @@ -1,4 +1,4 @@ -import jsPDF from "jspdf"; +import JsPDF from "jspdf"; import autoTable from "jspdf-autotable"; // Printed beside the notes line on both copies, so the office can tell at a @@ -34,7 +34,7 @@ export default function downloadCourseRegistrationReceipt({ } try { - const doc = new jsPDF({ + const doc = new JsPDF({ orientation: "portrait", unit: "mm", format: "legal", @@ -77,7 +77,6 @@ export default function downloadCourseRegistrationReceipt({ `Semester: ${studentInfo.semester || ""}`, ]; - const tableWidth = pageWidth - 40; const tableStartX = 20; const tableEndX = pageWidth - 20; const availableWidth = tableEndX - tableStartX; @@ -248,7 +247,6 @@ export default function downloadCourseRegistrationReceipt({ `Semester: ${studentInfo.semester || ""}`, ]; - const academicTableWidth = pageWidth - 40; const academicTableStartX = 20; const academicTableEndX = pageWidth - 20; const academicAvailableWidth = academicTableEndX - academicTableStartX; diff --git a/src/Modules/Examination/components/gradeSheetPdf.js b/src/Modules/Examination/components/gradeSheetPdf.js index 36a8fd048..b6421cc1a 100644 --- a/src/Modules/Examination/components/gradeSheetPdf.js +++ b/src/Modules/Examination/components/gradeSheetPdf.js @@ -1,4 +1,4 @@ -import jsPDF from "jspdf"; +import JsPDF from "jspdf"; import html2canvas from "html2canvas"; const PDF_MARGIN = { top: 75, left: 23, right: 23, bottom: 25 }; @@ -10,7 +10,7 @@ const CONTENT_W_PX = Math.round(CONTENT_W_MM * (96 / 25.4)); const CONTENT_H_PX = Math.round(CONTENT_H_MM * (96 / 25.4)); export async function saveGradeSheetPDF(html, filename) { - const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); + const pdf = new JsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); let firstPage = true; const cleanHTML = html @@ -40,10 +40,14 @@ export async function saveGradeSheetPDF(html, filename) { iDoc.write(cleanHTML); iDoc.close(); - await new Promise((r) => setTimeout(r, 250)); + await new Promise((r) => { + setTimeout(r, 250); + }); iDoc.body.style.cssText = `margin:0;padding:0;width:${CONTENT_W_PX}px;background:#fff;`; iframe.style.height = `${iDoc.body.scrollHeight}px`; - await new Promise((r) => setTimeout(r, 100)); + await new Promise((r) => { + setTimeout(r, 100); + }); const shedOverflowingPages = (pageDivs) => { for (let i = 0; i < pageDivs.length; i += 1) { @@ -145,7 +149,9 @@ export async function saveGradeSheetPDF(html, filename) { }); iframe.style.height = `${iDoc.body.scrollHeight}px`; - await new Promise((r) => setTimeout(r, 100)); + await new Promise((r) => { + setTimeout(r, 100); + }); const BORDER = "1px solid #000"; const NONE = "none"; @@ -209,7 +215,9 @@ export async function saveGradeSheetPDF(html, filename) { const pageNodes = Array.from(iDoc.querySelectorAll(".gs-page")); const nodesToRender = pageNodes.length > 0 ? pageNodes : [iDoc.body]; - for (const node of nodesToRender) { + for (let i = 0; i < nodesToRender.length; i += 1) { + const node = nodesToRender[i]; + // eslint-disable-next-line no-await-in-loop const canvas = await html2canvas(node, { scale: 2, useCORS: true, diff --git a/src/Modules/Examination/components/gradeSheetTemplate.js b/src/Modules/Examination/components/gradeSheetTemplate.js index e428bc544..97e136b99 100644 --- a/src/Modules/Examination/components/gradeSheetTemplate.js +++ b/src/Modules/Examination/components/gradeSheetTemplate.js @@ -25,7 +25,7 @@ function toRoman(n) { ]; let r = ""; let num = n; - for (let i = 0; i < vals.length; i++) { + for (let i = 0; i < vals.length; i += 1) { while (num >= vals[i]) { r += syms[i]; num -= vals[i]; diff --git a/src/Modules/Program_curriculum/Acad_admin/Admin_add_batch_form.jsx b/src/Modules/Program_curriculum/Acad_admin/Admin_add_batch_form.jsx index 4c1d0fb00..d55f00b04 100644 --- a/src/Modules/Program_curriculum/Acad_admin/Admin_add_batch_form.jsx +++ b/src/Modules/Program_curriculum/Acad_admin/Admin_add_batch_form.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { Select, - MultiSelect, NumberInput, Checkbox, Button, @@ -21,6 +20,7 @@ import { fetchGetUnlinkedCurriculum, } from "../api/api"; import { host } from "../../../routes/globalRoutes"; +import { getApiErrorMessage } from "../../../helper/apiError"; function Admin_add_batch_form() { const isMobile = useMediaQuery("(max-width: 768px)"); @@ -56,7 +56,8 @@ function Admin_add_batch_form() { validate: { batchName: (value) => (!value ? "Batch name is required" : null), discipline: (value) => (!value ? "Discipline is required" : null), - totalSeats: (value) => (value < 0 ? "Total seats cannot be negative" : null), + totalSeats: (value) => + value < 0 ? "Total seats cannot be negative" : null, disciplineBatch: (value) => (!value ? "Curriculum is required" : null), }, }); @@ -87,7 +88,7 @@ function Admin_add_batch_form() { const handleBatchNameChange = (value) => { form.setFieldValue("batchName", value); - + form.setFieldValue("disciplineBatch", ""); }; @@ -116,17 +117,17 @@ function Admin_add_batch_form() { } const curriculumData = form.values.disciplineBatch || ""; - + const payload = { batch_name: form.values.batchName, discipline: parseInt(form.values.discipline, 10), batchYear: parseInt(form.values.batchYear, 10), runningBatch: form.values.runningBatch, total_seats: parseInt(form.values.totalSeats, 10) || 0, - curriculum: curriculumData ? parseInt(curriculumData, 10) : null, - disciplineBatch: curriculumData ? parseInt(curriculumData, 10) : null + curriculum: curriculumData ? parseInt(curriculumData, 10) : null, + disciplineBatch: curriculumData ? parseInt(curriculumData, 10) : null, }; - + const response = await axios.post( `${host}/programme_curriculum/api/admin_add_batch/`, payload, @@ -136,7 +137,7 @@ function Admin_add_batch_form() { }, }, ); - + if (response.data.message) { notifications.show({ title: "✅ Batch Added Successfully!", @@ -144,19 +145,20 @@ function Admin_add_batch_form() { color: "green", autoClose: 5000, style: { - backgroundColor: '#d4edda', - borderColor: '#c3e6cb', - color: '#155724', + backgroundColor: "#d4edda", + borderColor: "#c3e6cb", + color: "#155724", }, }); - + form.reset(); setTimeout(() => { navigate("/programme_curriculum/admin_batches/"); }, 1500); } else { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to add batch"); + throw new Error( + response.data?.message || "Batch was not created. Please try again.", + ); } } catch (err) { notifications.show({ @@ -164,22 +166,23 @@ function Admin_add_batch_form() { message: (
- {err.message || "Unable to create batch. Please try again."} - - - Please check your inputs and try again. + + {getApiErrorMessage( + err, + "Unable to create batch. Please try again.", + )} +
), color: "red", autoClose: 7000, style: { - backgroundColor: '#f8d7da', - borderColor: '#f5c6cb', - color: '#721c24', + backgroundColor: "#f8d7da", + borderColor: "#f5c6cb", + color: "#721c24", }, }); - setError(err.message); } }; @@ -326,13 +329,13 @@ function Admin_add_batch_form() { - + diff --git a/src/helper/apiError.js b/src/helper/apiError.js new file mode 100644 index 000000000..e7d6d9534 --- /dev/null +++ b/src/helper/apiError.js @@ -0,0 +1,50 @@ +export function getApiErrorMessage( + err, + fallback = "Something went wrong. Please try again.", +) { + if (!err) return fallback; + + if (err.request && !err.response) { + return "Could not reach the server. Check your connection and try again."; + } + + const data = err.response?.data; + const status = err.response?.status; + + if (typeof data === "string") { + const text = data.trim(); + if (text && !text.startsWith("<")) return text; + } + + if (data && typeof data === "object") { + const direct = data.message || data.error || data.detail; + if (typeof direct === "string" && direct.trim()) return direct.trim(); + + if (Array.isArray(data.non_field_errors) && data.non_field_errors.length) { + return String(data.non_field_errors[0]); + } + + const fieldErrors = Object.entries(data) + .filter( + ([, value]) => + Array.isArray(value) && value.length && typeof value[0] === "string", + ) + .map(([field, messages]) => `${field}: ${messages[0]}`); + if (fieldErrors.length) return fieldErrors.join("; "); + } + + if (status === 401) return "Your session has expired. Please sign in again."; + if (status === 403) return "You do not have permission to do this."; + if (status === 404) return "The requested record was not found."; + if (status >= 500) { + return "The server ran into a problem. Please try again shortly."; + } + + if (!err.response && typeof err.message === "string" && err.message.trim()) { + return err.message; + } + + return fallback; +} + +export default getApiErrorMessage; diff --git a/src/helper/apiError.test.js b/src/helper/apiError.test.js new file mode 100644 index 000000000..4cb2b9e53 --- /dev/null +++ b/src/helper/apiError.test.js @@ -0,0 +1,97 @@ +import { describe, expect, it } from "vitest"; +import { getApiErrorMessage } from "./apiError"; + +const axiosError = (status, data) => ({ + message: `Request failed with status code ${status}`, + request: {}, + response: { status, data }, +}); + +describe("getApiErrorMessage", () => { + it("uses the message the Fusion API sends instead of the axios message", () => { + const err = axiosError(400, { + success: false, + message: "Missing required fields: programme, discipline", + }); + expect(getApiErrorMessage(err)).toBe( + "Missing required fields: programme, discipline", + ); + }); + + it("reads an error key", () => { + expect( + getApiErrorMessage(axiosError(400, { error: "Batch already exists" })), + ).toBe("Batch already exists"); + }); + + it("reads a DRF detail key", () => { + expect( + getApiErrorMessage(axiosError(400, { detail: "Invalid page." })), + ).toBe("Invalid page."); + }); + + it("flattens DRF field errors", () => { + const err = axiosError(400, { + discipline: ["This field is required."], + total_seats: ["Must be a positive integer."], + }); + expect(getApiErrorMessage(err)).toBe( + "discipline: This field is required.; total_seats: Must be a positive integer.", + ); + }); + + it("reads non_field_errors", () => { + const err = axiosError(400, { + non_field_errors: ["Batch with this year already exists."], + }); + expect(getApiErrorMessage(err)).toBe( + "Batch with this year already exists.", + ); + }); + + it("accepts a plain string body", () => { + expect( + getApiErrorMessage(axiosError(400, "Curriculum is not working")), + ).toBe("Curriculum is not working"); + }); + + it("ignores an HTML error page", () => { + const err = axiosError(500, "Server Error"); + expect(getApiErrorMessage(err)).toBe( + "The server ran into a problem. Please try again shortly.", + ); + }); + + it("explains the status when the body says nothing", () => { + expect(getApiErrorMessage(axiosError(401, {}))).toBe( + "Your session has expired. Please sign in again.", + ); + expect(getApiErrorMessage(axiosError(403, {}))).toBe( + "You do not have permission to do this.", + ); + expect(getApiErrorMessage(axiosError(404, {}))).toBe( + "The requested record was not found.", + ); + }); + + it("reports a request that never reached the server", () => { + expect(getApiErrorMessage({ message: "Network Error", request: {} })).toBe( + "Could not reach the server. Check your connection and try again.", + ); + }); + + it("passes through a locally thrown error", () => { + expect(getApiErrorMessage(new Error("Batch name is required"))).toBe( + "Batch name is required", + ); + }); + + it("falls back when there is nothing to read", () => { + expect( + getApiErrorMessage(axiosError(400, {}), "Unable to create batch."), + ).toBe("Unable to create batch."); + expect(getApiErrorMessage(null, "Unable to create batch.")).toBe( + "Unable to create batch.", + ); + }); +}); diff --git a/src/test/setup.js b/src/test/setup.js index 5af8f625a..09b0dc0e2 100644 --- a/src/test/setup.js +++ b/src/test/setup.js @@ -5,8 +5,11 @@ import { afterEach, beforeEach, vi } from "vitest"; const realError = console.error; const format = (args) => { - let i = 1; - return String(args[0]).replace(/%s/g, () => String(args[i++] ?? "")); + let i = 0; + return String(args[0]).replace(/%s/g, () => { + i += 1; + return String(args[i] ?? ""); + }); }; let propTypeFailures = [];