Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/Modules/Academic/RegisteredCourses.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export default function RegisteredCourses() {
paddingBottom: "5px",
}}
>
STUDENT COPY
Course Confirmation Form (Student Copy)
</div>

<div
Expand Down Expand Up @@ -473,7 +473,8 @@ export default function RegisteredCourses() {
2. The above courses do not clash with any other courses.
<br />
<br />
<strong>Additional notes:</strong>
<strong>Additional notes:</strong>{" "}
<strong>For Backlog/Improvement course</strong>
<br />
<div
style={{
Expand Down Expand Up @@ -509,6 +510,16 @@ export default function RegisteredCourses() {
<div style={{ fontSize: "9px", fontWeight: "bold" }}>
Student's Signature
</div>
<div
style={{
fontSize: "9px",
fontWeight: "bold",
textAlign: "left",
marginTop: "6px",
}}
>
Mobile Number:
</div>
</div>
<div style={{ textAlign: "center", width: "180px" }}>
<div
Expand Down Expand Up @@ -553,7 +564,7 @@ export default function RegisteredCourses() {
paddingBottom: "5px",
}}
>
ACADEMIC COPY
Course Confirmation Form (Academic Copy)
</div>

<div
Expand Down Expand Up @@ -610,7 +621,8 @@ export default function RegisteredCourses() {
2. The above courses do not clash with any other courses.
<br />
<br />
<strong>Additional notes:</strong>
<strong>Additional notes:</strong>{" "}
<strong>For Backlog/Improvement course</strong>
<br />
<div
style={{
Expand Down Expand Up @@ -646,6 +658,16 @@ export default function RegisteredCourses() {
<div style={{ fontSize: "9px", fontWeight: "bold" }}>
Student's Signature
</div>
<div
style={{
fontSize: "9px",
fontWeight: "bold",
textAlign: "left",
marginTop: "6px",
}}
>
Mobile Number:
</div>
</div>
<div style={{ textAlign: "center", width: "180px" }}>
<div
Expand Down
58 changes: 50 additions & 8 deletions src/Modules/Academic/courseRegistrationReceipt.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";

// Printed beside the notes line on both copies, so the office can tell at a
// glance which registrations are backlog or improvement attempts.
const NOTES_HINT = "For Backlog/Improvement course";

// The course registration receipt: student copy and academic copy on one page.
// Shared so the student's own download and the academic office's download of
// any student's receipt cannot drift apart.
Expand All @@ -19,9 +23,15 @@ export default function downloadCourseRegistrationReceipt({
const printedOn = `${String(now.getDate()).padStart(2, "0")}.${String(
now.getMonth() + 1,
).padStart(2, "0")}.${now.getFullYear()}`;
const prevCpiLabel = studentInfo.prevSemCpi
? `Prev. Sem. CPI: ${studentInfo.prevSemCpi}`
: "Prev. Sem. CPI: -";
// A first-semester student has no previous semester, so the label is omitted
// rather than printed with a placeholder.
const isFirstSemester = Number(studentInfo.semester) === 1;
let prevCpiLabel = "";
if (!isFirstSemester) {
prevCpiLabel = studentInfo.prevSemCpi
? `Prev. Sem. CPI: ${studentInfo.prevSemCpi}`
: "Prev. Sem. CPI: -";
}

try {
const doc = new jsPDF({
Expand All @@ -37,7 +47,14 @@ export default function downloadCourseRegistrationReceipt({

doc.setFontSize(14);
doc.setFont("helvetica", "bold");
doc.text("STUDENT COPY", pageWidth / 2, yPosition, { align: "center" });
doc.text(
"Course Confirmation Form (Student Copy)",
pageWidth / 2,
yPosition,
{
align: "center",
},
);
doc.setFontSize(9);
doc.setFont("helvetica", "normal");
doc.text(`Date: ${printedOn}`, pageWidth - 20, yPosition, {
Expand Down Expand Up @@ -142,7 +159,9 @@ export default function downloadCourseRegistrationReceipt({
doc.setFont("helvetica", "bold");
doc.setFontSize(10);
doc.text(`Total Credits: ${totalCredits}`, 20, yPosition);
doc.text(prevCpiLabel, pageWidth - 20, yPosition, { align: "right" });
if (prevCpiLabel) {
doc.text(prevCpiLabel, pageWidth - 20, yPosition, { align: "right" });
}
yPosition += 6;

doc.setFont("helvetica", "normal");
Expand All @@ -161,6 +180,11 @@ export default function downloadCourseRegistrationReceipt({

doc.setFont("helvetica", "bold");
doc.text("Additional notes:", 20, yPosition);
doc.text(
NOTES_HINT,
20 + doc.getTextWidth("Additional notes: "),
yPosition,
);
yPosition += 5;

doc.setLineWidth(0.3);
Expand All @@ -180,7 +204,9 @@ export default function downloadCourseRegistrationReceipt({
doc.line(20, yPosition + 5, leftSigX + 50, yPosition + 5);
doc.line(rightSigX - 20, yPosition + 5, pageWidth - 20, yPosition + 5);

yPosition += 15;
doc.text("Mobile Number:", 20, yPosition + 14);

yPosition += 20;

doc.setLineWidth(0.2);
doc.setLineDashPattern([1, 1], 0);
Expand All @@ -192,7 +218,14 @@ export default function downloadCourseRegistrationReceipt({

doc.setFontSize(14);
doc.setFont("helvetica", "bold");
doc.text("ACADEMIC COPY", pageWidth / 2, yPosition, { align: "center" });
doc.text(
"Course Confirmation Form (Academic Copy)",
pageWidth / 2,
yPosition,
{
align: "center",
},
);
doc.setFontSize(9);
doc.setFont("helvetica", "normal");
doc.text(`Date: ${printedOn}`, pageWidth - 20, yPosition, {
Expand Down Expand Up @@ -293,7 +326,9 @@ export default function downloadCourseRegistrationReceipt({
doc.setFont("helvetica", "bold");
doc.setFontSize(10);
doc.text(`Total Credits: ${totalCredits}`, 20, yPosition);
doc.text(prevCpiLabel, pageWidth - 20, yPosition, { align: "right" });
if (prevCpiLabel) {
doc.text(prevCpiLabel, pageWidth - 20, yPosition, { align: "right" });
}
yPosition += 6;

doc.setFont("helvetica", "normal");
Expand All @@ -312,6 +347,11 @@ export default function downloadCourseRegistrationReceipt({

doc.setFont("helvetica", "bold");
doc.text("Additional notes:", 20, yPosition);
doc.text(
NOTES_HINT,
20 + doc.getTextWidth("Additional notes: "),
yPosition,
);
yPosition += 5;

doc.setLineWidth(0.3);
Expand All @@ -336,6 +376,8 @@ export default function downloadCourseRegistrationReceipt({
yPosition + 5,
);

doc.text("Mobile Number:", 20, yPosition + 14);

doc.save(filename);
} catch (error) {
alert(`Error generating PDF: ${error.message}. Please try again.`);
Expand Down
23 changes: 22 additions & 1 deletion src/Modules/Dashboard/StudentCreditSummary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SWAYAM_CREDIT_CAP,
computeCreditSummary,
fmtCredits,
hasCompletedDegree,
remainingCreditRequirement,
swayamAboveCap,
} from "../../lib/credits";
Expand Down Expand Up @@ -274,7 +275,27 @@ export default function StudentCreditSummary() {
<SemesterCard label="Total" row={totals} emphasis />
</Stack>

{isUg && (
{isUg && hasCompletedDegree(totals) && (
<Group
justify="space-between"
align="center"
wrap="wrap"
gap="sm"
className={classes.formula}
>
<Stack gap={2} className={classes.formulaText}>
<Text className={classes.formulaLabel}>
Congratulations, you have completed your degree successfully
</Text>
<Text className={classes.formulaNote}>
All {DEGREE_CREDIT_REQUIREMENT} credits required for the degree
have been earned.
</Text>
</Stack>
</Group>
)}

{isUg && !hasCompletedDegree(totals) && (
<Group
justify="space-between"
align="center"
Expand Down
50 changes: 50 additions & 0 deletions src/Modules/Dashboard/StudentCreditSummary.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,56 @@ describe("StudentCreditSummary", () => {
);
});

it("congratulates a UG student who has met the requirement, and drops the sum", async () => {
axios.get.mockResolvedValue({
data: {
semesters: [
{
label: "Semester 1",
courses: [
{ code: "CS1010", credits: 148, grade: "A", remark: "Regular" },
],
},
],
programme_category: "UG",
},
});
renderPanel();

await waitFor(() =>
expect(screen.getByText("Credits Details")).toBeInTheDocument(),
);

expect(
screen.getByText(
"Congratulations, you have completed your degree successfully",
),
).toBeInTheDocument();
expect(
screen.queryByText("Remaining Credits requirement for degree"),
).not.toBeInTheDocument();
});

it("still shows the remaining requirement while credits are outstanding", async () => {
axios.get.mockResolvedValue({
data: { semesters: SEMESTERS, programme_category: "UG" },
});
renderPanel();

await waitFor(() =>
expect(screen.getByText("Credits Details")).toBeInTheDocument(),
);

expect(
screen.getByText("Remaining Credits requirement for degree"),
).toBeInTheDocument();
expect(
screen.queryByText(
"Congratulations, you have completed your degree successfully",
),
).not.toBeInTheDocument();
});

it("totals the credits and states the degree requirement", async () => {
axios.get.mockResolvedValue({
data: { semesters: SEMESTERS, programme_category: "UG" },
Expand Down
36 changes: 34 additions & 2 deletions src/Modules/Examination/GradeValidation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ function buildCreditsDetailsHTML(semesters) {
}

function buildValidationHTML(studentInfo, semesters) {
// Decided by the server, which owns what this record says; blank for the
// programmes the 148-credit rule does not describe.
const standingText = studentInfo.standing_text || "";
const completionRow = standingText
? `
<tr>
<td class="standing" colspan="4">${esc(standingText)}</td>
</tr>`
: "";

const lastGraded = studentInfo.last_graded_semester || "";
const lastGradedRow = lastGraded
? `
<tr>
<td class="lbl">Last Graded Semester</td>
<td class="val" colspan="3">${esc(lastGraded)}</td>
</tr>`
: "";

const semesterBlocks = semesters
.map((sem) => {
const isReg = sem.is_registered_only === true;
Expand Down Expand Up @@ -110,7 +129,10 @@ function buildValidationHTML(studentInfo, semesters) {

return `
<div class="sem-block">
<div class="sem-heading" style="background:${headingBg}">${esc(sem.label)}</div>
<div class="sem-heading" style="background:${headingBg}">
<span>${esc(sem.label)}</span>
<span class="sem-year">${esc(sem.academic_year || "")}</span>
</div>
<table class="course-table">
<thead>
<tr>
Expand Down Expand Up @@ -166,6 +188,16 @@ function buildValidationHTML(studentInfo, semesters) {
background: #dce7f3;
border: 1pt solid #000;
border-bottom: none;
display: flex;
justify-content: space-between;
align-items: baseline;
}
.sem-year { font-weight: bold; }

.standing {
text-align: center;
font-weight: bold;
color: #c00000;
}

.course-table {
Expand Down Expand Up @@ -264,7 +296,7 @@ function buildValidationHTML(studentInfo, semesters) {
<td class="val">${esc(studentInfo.name)}</td>
<td class="lbl">Discipline</td>
<td class="val">${esc(studentInfo.discipline)}</td>
</tr>
</tr>${lastGradedRow}${completionRow}
</table>

${semesterBlocks}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/credits.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ export const creditsTowardsDegree = (totals) =>
export function remainingCreditRequirement(totals) {
return DEGREE_CREDIT_REQUIREMENT - creditsTowardsDegree(totals);
}

// Nothing left to earn, so a student carrying surplus credits counts as done.
export const hasCompletedDegree = (totals) =>
remainingCreditRequirement(totals) <= 0;
18 changes: 18 additions & 0 deletions src/lib/credits.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
computeCreditSummary,
fmtCredits,
hasCompletedDegree,
remainingCreditRequirement,
} from "./credits";

Expand Down Expand Up @@ -251,3 +252,20 @@ describe("remainingCreditRequirement", () => {
expect(remainingCreditRequirement({ earned: 151, swayam: 5 })).toBe(-3);
});
});

describe("hasCompletedDegree", () => {
it("is false while credits are still outstanding", () => {
expect(hasCompletedDegree({ earned: 122, swayam: 6 })).toBe(false);
expect(hasCompletedDegree({ earned: 147, swayam: 0 })).toBe(false);
});

it("is true on exactly the requirement, and on a surplus", () => {
expect(hasCompletedDegree({ earned: 148, swayam: 0 })).toBe(true);
expect(hasCompletedDegree({ earned: 151, swayam: 5 })).toBe(true);
});

it("does not count swayam credits above the cap towards completion", () => {
// 150 earned, but 4 of the 10 swayam credits do not count: 146 < 148.
expect(hasCompletedDegree({ earned: 150, swayam: 10 })).toBe(false);
});
});
Loading