From 70faee310ad5174cce104aaa75dc00406270369d Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 7 Oct 2026 07:52:32 +0530 Subject: [PATCH 1/3] Show the server's own reason when adding a batch fails The Add Batch form reported "Request failed with status code 400", which is axios's own message and says nothing about what went wrong. The backend had already sent the reason: create_batch answers with {success: false, message: "Missing required fields: ..."} or "Invalid curriculum ID: N. Curriculum not found or not working." That text arrives in error.response.data and was being discarded. getApiErrorMessage reads it, covering the shapes these APIs actually return: message, error, detail, non_field_errors, DRF per-field arrays and a bare string, ignoring an HTML error page. When the body says nothing it falls back to the meaning of the status, and a request that never reached the server is reported as such rather than as a failed one. Two other faults in the same handler: setError on a failed submit replaced the whole page with a red line, because that state is the load-failure screen. A rejected submit destroyed the form and everything typed into it. The toast is enough. The non-message branch called response.json() on an axios response, which has no such method and would have thrown. Also drops an unused MultiSelect import the lint hook rejects. 11 unit tests on the reader. --- .../Acad_admin/Admin_add_batch_form.jsx | 61 ++++++------ src/helper/apiError.js | 50 ++++++++++ src/helper/apiError.test.js | 97 +++++++++++++++++++ 3 files changed, 179 insertions(+), 29 deletions(-) create mode 100644 src/helper/apiError.js create mode 100644 src/helper/apiError.test.js 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.", + ); + }); +}); From a556926093d6765d59a6a87135d066eae9134826 Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 7 Oct 2026 07:52:39 +0530 Subject: [PATCH 2/3] Read Academic error messages through the shared reader Every call site here already tried to pull the server's explanation out of a failed request, but each one checked a different subset of keys: some read error, some detail, some both, some added message. An endpoint answering with a key its caller does not check falls through to axios's own "Request failed with status code 400", which is what the user then sees. The dev backend returns {"error": ...} for a permission failure, so the files checking only detail showed the useless line. 38 call sites across 13 files now go through getApiErrorMessage, which checks every shape the APIs return. The existing fallback strings are preserved as its second argument, so a message like "Drop failed" still appears when the server offers nothing. No behaviour change beyond the messages themselves. --- src/Modules/Academic/AdminAddDashboard.jsx | 7 ++++--- src/Modules/Academic/AdminDropDashboard.jsx | 7 ++++--- src/Modules/Academic/AdminPhDCourseRequests.jsx | 5 +++-- .../Academic/AdminReplacementDashboard.jsx | 9 +++++---- src/Modules/Academic/AllotCourses.jsx | 4 ++-- src/Modules/Academic/GenerateStudentList.jsx | 13 +++++++------ src/Modules/Academic/PhDCourseRegistration.jsx | 7 ++++--- .../Academic/ReplacementRequestStudent.jsx | 3 ++- src/Modules/Academic/SectionAssignment.jsx | 7 ++++--- src/Modules/Academic/StudentAddCourse.jsx | 5 +++-- .../Academic/StudentCourseReplacement.jsx | 5 +++-- src/Modules/Academic/StudentCourses.jsx | 16 +++++++--------- src/Modules/Academic/StudentDropCourse.jsx | 5 +++-- 13 files changed, 51 insertions(+), 42 deletions(-) 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", From b032cfa4292d19a10ceba311c34e584284bdfb76 Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 7 Oct 2026 07:54:33 +0530 Subject: [PATCH 3/3] Clear the lint errors that blocked the pre-commit hook eslint . failed on acad-main with 11 errors, so the pre-commit hook rejected every commit regardless of what it contained. None of the fixes change behaviour: jsPDF is imported as JsPDF in the two PDF builders, since new-cap wants a constructor to start with a capital. tableWidth and academicTableWidth in the receipt were computed and never read. The three setTimeout promises in the grade sheet builder returned the timer id out of the executor, which no-promise-executor-return flags; the executor body is braced now and resolves the same way. The page render loop is indexed rather than for..of. Its await is sequential on purpose, because each page is drawn onto the document in order, so that one rule is disabled on the line rather than restructured. Two post-increments became += 1. The one in the test console formatter read and incremented in the same expression, so the counter starts at 0 and increments before the read to keep the same argument order. eslint . is clean, 478 tests pass, build succeeds. --- .../Academic/courseRegistrationReceipt.js | 6 ++---- .../Examination/components/gradeSheetPdf.js | 20 +++++++++++++------ .../components/gradeSheetTemplate.js | 2 +- src/test/setup.js | 7 +++++-- 4 files changed, 22 insertions(+), 13 deletions(-) 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/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 = [];