diff --git a/src/Modules/Certificates/GeneratedCertificatesModal.jsx b/src/Modules/Certificates/GeneratedCertificatesModal.jsx index 66a9c0556..e6489bfcd 100644 --- a/src/Modules/Certificates/GeneratedCertificatesModal.jsx +++ b/src/Modules/Certificates/GeneratedCertificatesModal.jsx @@ -28,6 +28,8 @@ import { feeCertificatesRoute, feeStructureCertificateHistoryPdfRoute, feeStructureCertificatesRoute, + pointerConversionCertificatePdfRoute, + pointerConversionCertificatesRoute, } from "../../routes/academicRoutes"; import classes from "./GeneratedCertificatesModal.module.css"; @@ -70,6 +72,10 @@ const ROUTES_BY_VARIANT = { list: feeStructureCertificatesRoute, pdf: feeStructureCertificateHistoryPdfRoute, }, + pointerConversion: { + list: pointerConversionCertificatesRoute, + pdf: pointerConversionCertificatePdfRoute, + }, }; export default function GeneratedCertificatesModal({ @@ -325,7 +331,13 @@ export default function GeneratedCertificatesModal({ GeneratedCertificatesModal.propTypes = { opened: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, - variant: PropTypes.oneOf(["bonafide", "fee", "demand", "feeStructure"]), + variant: PropTypes.oneOf([ + "bonafide", + "fee", + "demand", + "feeStructure", + "pointerConversion", + ]), }; GeneratedCertificatesModal.defaultProps = { diff --git a/src/Modules/Certificates/PointerConversionCertificate.jsx b/src/Modules/Certificates/PointerConversionCertificate.jsx new file mode 100644 index 000000000..95fda5d48 --- /dev/null +++ b/src/Modules/Certificates/PointerConversionCertificate.jsx @@ -0,0 +1,449 @@ +import { useState } from "react"; +import axios from "axios"; +import { saveAs } from "file-saver"; +import { + Alert, + Badge, + Button, + Divider, + Grid, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title, +} from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { + ClockCounterClockwise, + DownloadSimple, + Eye, + Info, + MagnifyingGlass, + Warning, +} from "@phosphor-icons/react"; + +import { + pointerConversionPdfRoute, + pointerConversionStudentRoute, +} from "../../routes/academicRoutes"; +import classes from "./BonafideCertificate.module.css"; +import GeneratedCertificatesModal from "./GeneratedCertificatesModal"; + +const authConfig = () => ({ + headers: { + Authorization: `Token ${localStorage.getItem("authToken")}`, + }, +}); + +const responseMessage = async (error) => { + const payload = error?.response?.data; + if (payload instanceof Blob) { + try { + return JSON.parse(await payload.text()).error; + } catch { + return "Certificate generation failed."; + } + } + return ( + payload?.error || + payload?.message || + payload?.detail || + "Unable to complete the request." + ); +}; + +const filenameFrom = (header, fallback) => { + const match = header?.match(/filename="?([^";]+)"?/i); + return match?.[1] || fallback; +}; + +export default function PointerConversionCertificate() { + const [rollNumber, setRollNumber] = useState(""); + const [selectedStudent, setSelectedStudent] = useState(null); + const [certificateMeta, setCertificateMeta] = useState(null); + const [cpiText, setCpiText] = useState(""); + const [minCpi, setMinCpi] = useState("5.0"); + const [preview, setPreview] = useState(null); + const [fetchingStudent, setFetchingStudent] = useState(false); + const [downloading, setDownloading] = useState(false); + const [historyOpened, setHistoryOpened] = useState(false); + + const clearStudent = () => { + setSelectedStudent(null); + setCertificateMeta(null); + setCpiText(""); + setMinCpi("5.0"); + setPreview(null); + }; + + const fetchStudent = async () => { + const normalizedRollNumber = rollNumber.trim().toUpperCase(); + if (!normalizedRollNumber || fetchingStudent) return; + + clearStudent(); + setRollNumber(normalizedRollNumber); + setFetchingStudent(true); + try { + const { data } = await axios.get(pointerConversionStudentRoute, { + ...authConfig(), + params: { roll_number: normalizedRollNumber }, + }); + setSelectedStudent(data.student || null); + setCertificateMeta(data.certificate || null); + setCpiText(data.student?.cpi_display || ""); + setMinCpi(data.student?.min_cpi || "5.0"); + } catch (error) { + notifications.show({ + color: "red", + title: "Student unavailable", + message: await responseMessage(error), + }); + } finally { + setFetchingStudent(false); + } + }; + + const showPreview = () => { + if (!selectedStudent || !certificateMeta) return; + setPreview({ + student: selectedStudent, + meta: certificateMeta, + cpiText: cpiText.trim() || selectedStudent.cpi_display || "……………", + minCpi: minCpi.trim() || "5.0", + }); + }; + + const download = async () => { + if (!selectedStudent || downloading) return; + setDownloading(true); + try { + const response = await axios.post( + pointerConversionPdfRoute, + { + student_id: selectedStudent.student_id, + cpi_text: cpiText.trim() || selectedStudent.cpi_display || "……………", + min_cpi: minCpi.trim() || "5.0", + }, + { ...authConfig(), responseType: "blob" }, + ); + const filename = filenameFrom( + response.headers["content-disposition"], + `${selectedStudent.roll_number}_Pointer_Conversion_Certificate.pdf`, + ); + saveAs(response.data, filename); + notifications.show({ + color: "green", + title: "Certificate downloaded", + message: response.headers["x-certificate-reference"] || filename, + }); + } catch (error) { + notifications.show({ + color: "red", + title: "Download failed", + message: await responseMessage(error), + }); + } finally { + setDownloading(false); + } + }; + + const ready = Boolean(selectedStudent?.is_ready); + + return ( + <> + + + + +
+ Generate certificate + + Enter a roll number to fetch the student details. + +
+ + { + setRollNumber(event.currentTarget.value.toUpperCase()); + clearStudent(); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + fetchStudent(); + } + }} + rightSection={ + fetchingStudent ? : undefined + } + /> + + {selectedStudent && ( + + + {selectedStudent.name} + + {selectedStudent.is_ready ? "Ready" : "Incomplete"} + + + Roll No.: {selectedStudent.roll_number} + + {selectedStudent.programme} in {selectedStudent.discipline} + + {selectedStudent.semester_ordinal && ( + + {selectedStudent.semester_ordinal} Semester + + )} + + )} + + {selectedStudent && !selectedStudent.is_ready && ( + } + title="Student data incomplete" + > + {selectedStudent.validation_errors.join(" ")} + + )} + + {selectedStudent && ( + <> + 0 + ? "Automatically calculated from course grades." + : "No grades found in records. Enter CPI manually, or leave blank to display dots." + } + value={cpiText} + onChange={(event) => { + setCpiText(event.currentTarget.value); + setPreview(null); + }} + /> + { + setMinCpi(event.currentTarget.value); + setPreview(null); + }} + /> + + )} + + + + + + +
+
+
+ + + {preview ? ( +
+
+
+
+
{preview.meta.signatory_name}
+
{preview.meta.signatory_title}
+
+
+
{preview.meta.reference_preview}
+
Date – {preview.meta.issued_on}
+
+
+

+ TO WHOM SO EVER IT MAY CONCERN +

+

+      This is to certify that: +

+

+ 1. The graduation requirement for {preview.student.programme}{" "} + ({preview.student.programme_abbr}) programme at PDPM-IIITDM + Jabalpur is a minimum CPI of {preview.minCpi} out of the + maximum of 10.0. +

+

+ 2. Notionally, the CPI may be multiplied by a factor of 10 to + obtain the numerical percentage. +

+

+ Issued to : +

+ + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + : + + {preview.student.salutation} {preview.student.name} +
+ Roll No. + + : + + {preview.student.roll_number} +
+ Branch + + : + + {preview.student.discipline} +
+ CPI + + : + + {preview.cpiText} +
+
+
{preview.meta.signatory_name}
+
{preview.meta.signatory_title}
+
+
+
+ ) : ( + } + title="Certificate preview" + > + Enter a roll number, fetch the student, adjust CPI if needed, and + choose Preview. The certificate can then be printed or downloaded + as PDF. + + )} +
+
+ setHistoryOpened(false)} + variant="pointerConversion" + /> + + ); +} diff --git a/src/Modules/Certificates/index.jsx b/src/Modules/Certificates/index.jsx index 6771bd8c5..075d1f964 100644 --- a/src/Modules/Certificates/index.jsx +++ b/src/Modules/Certificates/index.jsx @@ -10,6 +10,9 @@ const COMPONENTS = { feeCertificate: lazy(() => import("./FeeCertificate")), demandLetter: lazy(() => import("./DemandLetter")), feeStructureCertificate: lazy(() => import("./FeeStructureCertificate")), + pointerConversionCertificate: lazy( + () => import("./PointerConversionCertificate"), + ), }; export default function Certificates() { diff --git a/src/Modules/Certificates/pages.js b/src/Modules/Certificates/pages.js index 3e5e44c1d..9571cd092 100644 --- a/src/Modules/Certificates/pages.js +++ b/src/Modules/Certificates/pages.js @@ -37,4 +37,13 @@ export const CERTIFICATE_PAGES = [ roles: ["acadadmin"], desktopOnly: true, }, + { + key: "pointerConversionCertificate", + slug: "pointer-conversion-certificate", + title: "Pointer Conversion Certificate", + icon: "Certificate", + group: "Certificates", + roles: ["acadadmin"], + desktopOnly: true, + }, ]; diff --git a/src/routes/academicRoutes/index.jsx b/src/routes/academicRoutes/index.jsx index cb73c8c86..b1b5e742a 100644 --- a/src/routes/academicRoutes/index.jsx +++ b/src/routes/academicRoutes/index.jsx @@ -58,6 +58,12 @@ export const feeStructureCertificatesRoute = `${host}/academic-procedures/api/ac export const feeStructureCertificateHistoryPdfRoute = (certificateId) => `${feeStructureCertificatesRoute}${certificateId}/pdf/`; +export const pointerConversionStudentRoute = `${host}/academic-procedures/api/acad/pointer-conversion/student/`; +export const pointerConversionPdfRoute = `${host}/academic-procedures/api/acad/pointer-conversion/pdf/`; +export const pointerConversionCertificatesRoute = `${host}/academic-procedures/api/acad/pointer-conversion/certificates/`; +export const pointerConversionCertificatePdfRoute = (certificateId) => + `${pointerConversionCertificatesRoute}${certificateId}/pdf/`; + export const feeStructuresRoute = `${host}/academic-procedures/api/acad/fee-structures/`; export const feeStructureTemplateRoute = `${feeStructuresRoute}template/`; export const feeStructureDetailRoute = (structureId) =>