@studious-lms/server 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -1
- package/data/curriculum/nepal-cdc-g4-g5/README.md +90 -0
- package/data/curriculum/nepal-cdc-g4-g5/manifest.json +97 -0
- package/data/curriculum/nepal-cdc-g4-g5/proficiency-scale.json +76 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +6936 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/health-physical-creative-arts.json +3401 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1511 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/nepali.json +7928 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +2420 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +2177 -0
- package/dist/lib/assignmentTypeDefaults.d.ts +41 -0
- package/dist/lib/assignmentTypeDefaults.d.ts.map +1 -0
- package/dist/lib/assignmentTypeDefaults.js +167 -0
- package/dist/lib/assignmentTypeDefaults.js.map +1 -0
- package/dist/lib/config/env.d.ts +0 -8
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +2 -5
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/feePdf.d.ts +19 -0
- package/dist/lib/feePdf.d.ts.map +1 -0
- package/dist/lib/feePdf.js +167 -0
- package/dist/lib/feePdf.js.map +1 -0
- package/dist/lib/feePdfFormat.d.ts +4 -0
- package/dist/lib/feePdfFormat.d.ts.map +1 -0
- package/dist/lib/feePdfFormat.js +16 -0
- package/dist/lib/feePdfFormat.js.map +1 -0
- package/dist/lib/idCardPdf.d.ts +26 -0
- package/dist/lib/idCardPdf.d.ts.map +1 -0
- package/dist/lib/idCardPdf.js +272 -0
- package/dist/lib/idCardPdf.js.map +1 -0
- package/dist/lib/pdfLetterhead.d.ts +41 -0
- package/dist/lib/pdfLetterhead.d.ts.map +1 -0
- package/dist/lib/pdfLetterhead.js +86 -0
- package/dist/lib/pdfLetterhead.js.map +1 -0
- package/dist/lib/transcriptPdf.d.ts +55 -0
- package/dist/lib/transcriptPdf.d.ts.map +1 -0
- package/dist/lib/transcriptPdf.js +303 -0
- package/dist/lib/transcriptPdf.js.map +1 -0
- package/dist/models/academicCalendar.d.ts +198 -0
- package/dist/models/academicCalendar.d.ts.map +1 -0
- package/dist/models/academicCalendar.js +106 -0
- package/dist/models/academicCalendar.js.map +1 -0
- package/dist/models/agenda.d.ts +8 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +2 -0
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +66 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +659 -21
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +544 -75
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +2 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +12 -0
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +3 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/event.d.ts +16 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/fees.d.ts +554 -0
- package/dist/models/fees.d.ts.map +1 -0
- package/dist/models/fees.js +278 -0
- package/dist/models/fees.js.map +1 -0
- package/dist/models/file.d.ts +29 -0
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +2 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +2 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketplace.d.ts +178 -0
- package/dist/models/marketplace.d.ts.map +1 -0
- package/dist/models/marketplace.js +115 -0
- package/dist/models/marketplace.js.map +1 -0
- package/dist/models/moduleItem.d.ts +304 -0
- package/dist/models/moduleItem.d.ts.map +1 -0
- package/dist/models/moduleItem.js +78 -0
- package/dist/models/moduleItem.js.map +1 -0
- package/dist/models/newtonChat.d.ts +1 -0
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +18 -0
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +7 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +4 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +19 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +11341 -7097
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +12 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/academicCalendar.d.ts +229 -0
- package/dist/routers/academicCalendar.d.ts.map +1 -0
- package/dist/routers/academicCalendar.js +75 -0
- package/dist/routers/academicCalendar.js.map +1 -0
- package/dist/routers/admin.d.ts +2 -0
- package/dist/routers/admin.d.ts.map +1 -1
- package/dist/routers/agenda.d.ts +8 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +75 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +10 -2
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +103 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +8 -5
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/class.d.ts +27 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +6 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/event.d.ts +8 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/fees.d.ts +513 -0
- package/dist/routers/fees.d.ts.map +1 -0
- package/dist/routers/fees.js +88 -0
- package/dist/routers/fees.js.map +1 -0
- package/dist/routers/file.d.ts +2 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/gradebook.d.ts +4 -1
- package/dist/routers/gradebook.d.ts.map +1 -1
- package/dist/routers/marketplace.d.ts +371 -0
- package/dist/routers/marketplace.d.ts.map +1 -0
- package/dist/routers/marketplace.js +87 -0
- package/dist/routers/marketplace.js.map +1 -0
- package/dist/routers/moduleItem.d.ts +263 -0
- package/dist/routers/moduleItem.d.ts.map +1 -0
- package/dist/routers/moduleItem.js +60 -0
- package/dist/routers/moduleItem.js.map +1 -0
- package/dist/routers/parent.d.ts +169 -0
- package/dist/routers/parent.d.ts.map +1 -0
- package/dist/routers/parent.js +49 -0
- package/dist/routers/parent.js.map +1 -0
- package/dist/routers/school.d.ts +363 -7
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +100 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/worksheet.d.ts +27 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +10 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/academicCalendar.d.ts +173 -0
- package/dist/services/academicCalendar.d.ts.map +1 -0
- package/dist/services/academicCalendar.js +214 -0
- package/dist/services/academicCalendar.js.map +1 -0
- package/dist/services/adminInstitution.d.ts +2 -0
- package/dist/services/adminInstitution.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +8 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +75 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +91 -12
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +323 -10
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +385 -25
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/attendanceMerge.d.ts +53 -0
- package/dist/services/attendanceMerge.d.ts.map +1 -0
- package/dist/services/attendanceMerge.js +87 -0
- package/dist/services/attendanceMerge.js.map +1 -0
- package/dist/services/attendanceRate.d.ts +24 -0
- package/dist/services/attendanceRate.d.ts.map +1 -0
- package/dist/services/attendanceRate.js +31 -0
- package/dist/services/attendanceRate.js.map +1 -0
- package/dist/services/attendanceStats.d.ts +22 -0
- package/dist/services/attendanceStats.d.ts.map +1 -0
- package/dist/services/attendanceStats.js +45 -0
- package/dist/services/attendanceStats.js.map +1 -0
- package/dist/services/class.d.ts +27 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +26 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/event.d.ts +8 -0
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/fees.d.ts +422 -0
- package/dist/services/fees.d.ts.map +1 -0
- package/dist/services/fees.js +310 -0
- package/dist/services/fees.js.map +1 -0
- package/dist/services/feesAuthz.d.ts +49 -0
- package/dist/services/feesAuthz.d.ts.map +1 -0
- package/dist/services/feesAuthz.js +52 -0
- package/dist/services/feesAuthz.js.map +1 -0
- package/dist/services/file.d.ts +4 -0
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/gradebook.d.ts +18 -1
- package/dist/services/gradebook.d.ts.map +1 -1
- package/dist/services/gradebook.js +57 -2
- package/dist/services/gradebook.js.map +1 -1
- package/dist/services/idCard.d.ts +11 -0
- package/dist/services/idCard.d.ts.map +1 -0
- package/dist/services/idCard.js +122 -0
- package/dist/services/idCard.js.map +1 -0
- package/dist/services/marketplace.d.ts +301 -0
- package/dist/services/marketplace.d.ts.map +1 -0
- package/dist/services/marketplace.js +628 -0
- package/dist/services/marketplace.js.map +1 -0
- package/dist/services/moduleItem.d.ts +196 -0
- package/dist/services/moduleItem.d.ts.map +1 -0
- package/dist/services/moduleItem.js +188 -0
- package/dist/services/moduleItem.js.map +1 -0
- package/dist/services/notification.d.ts +58 -0
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/notification.js +30 -2
- package/dist/services/notification.js.map +1 -1
- package/dist/services/parentPortal.d.ts +133 -0
- package/dist/services/parentPortal.d.ts.map +1 -0
- package/dist/services/parentPortal.js +328 -0
- package/dist/services/parentPortal.js.map +1 -0
- package/dist/services/school.d.ts +28 -3
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +65 -45
- package/dist/services/school.js.map +1 -1
- package/dist/services/studentImport.d.ts +34 -0
- package/dist/services/studentImport.d.ts.map +1 -0
- package/dist/services/studentImport.js +236 -0
- package/dist/services/studentImport.js.map +1 -0
- package/dist/services/transcript.d.ts +61 -0
- package/dist/services/transcript.d.ts.map +1 -0
- package/dist/services/transcript.js +243 -0
- package/dist/services/transcript.js.map +1 -0
- package/dist/services/worksheet.d.ts +22 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +94 -2
- package/dist/services/worksheet.js.map +1 -1
- package/dist/trpc.d.ts +1 -1
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +5 -5
- package/dist/trpc.js.map +1 -1
- package/dist/utils/adminPanelAccess.d.ts +4 -5
- package/dist/utils/adminPanelAccess.d.ts.map +1 -1
- package/dist/utils/adminPanelAccess.js +12 -34
- package/dist/utils/adminPanelAccess.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260804120000_add_attendance_records_and_excusals/migration.sql +114 -0
- package/prisma/migrations/20260804120100_add_fee_plans_invoices_payments/migration.sql +101 -0
- package/prisma/migrations/20260804120200_add_marketplace_listings/migration.sql +54 -0
- package/prisma/migrations/20260804120300_content_platform_b3_b9/migration.sql +63 -0
- package/prisma/migrations/20260804120400_c_academic_ops/migration.sql +77 -0
- package/prisma/schema.prisma +371 -11
- package/scripts/import-nepal-cdc-curriculum.ts +379 -0
- package/src/lib/assignmentTypeDefaults.ts +222 -0
- package/src/lib/config/env.ts +0 -3
- package/src/lib/feePdf.ts +199 -0
- package/src/lib/feePdfFormat.ts +14 -0
- package/src/lib/idCardPdf.ts +336 -0
- package/src/lib/pdfLetterhead.ts +124 -0
- package/src/lib/transcriptPdf.ts +502 -0
- package/src/models/academicCalendar.ts +139 -0
- package/src/models/attendance.ts +704 -73
- package/src/models/class.ts +1 -0
- package/src/models/fees.ts +349 -0
- package/src/models/marketplace.ts +158 -0
- package/src/models/moduleItem.ts +107 -0
- package/src/models/school.ts +6 -0
- package/src/models/worksheet.ts +2 -0
- package/src/routers/_app.ts +10 -0
- package/src/routers/academicCalendar.ts +103 -0
- package/src/routers/assignment.ts +8 -0
- package/src/routers/attendance.ts +7 -3
- package/src/routers/class.ts +4 -0
- package/src/routers/fees.ts +126 -0
- package/src/routers/marketplace.ts +134 -0
- package/src/routers/moduleItem.ts +89 -0
- package/src/routers/parent.ts +68 -0
- package/src/routers/school.ts +139 -1
- package/src/routers/worksheet.ts +11 -0
- package/src/services/academicCalendar.ts +299 -0
- package/src/services/assignment.ts +122 -12
- package/src/services/attendance.ts +548 -26
- package/src/services/attendanceMerge.ts +139 -0
- package/src/services/attendanceRate.ts +41 -0
- package/src/services/attendanceStats.ts +63 -0
- package/src/services/class.ts +25 -1
- package/src/services/fees.ts +424 -0
- package/src/services/feesAuthz.ts +77 -0
- package/src/services/gradebook.ts +70 -0
- package/src/services/idCard.ts +147 -0
- package/src/services/marketplace.ts +785 -0
- package/src/services/moduleItem.ts +274 -0
- package/src/services/notification.ts +43 -0
- package/src/services/parentPortal.ts +380 -0
- package/src/services/school.ts +65 -44
- package/src/services/studentImport.ts +273 -0
- package/src/services/transcript.ts +313 -0
- package/src/services/worksheet.ts +111 -0
- package/src/trpc.ts +3 -3
- package/src/utils/adminPanelAccess.ts +12 -35
- package/tests/unit/assignmentTypeDefaults.test.ts +52 -0
- package/tests/unit/lib/feePdf.test.ts +15 -0
- package/tests/unit/lib/idCardPdf.test.ts +40 -0
- package/tests/unit/lib/transcriptPdf.test.ts +111 -0
- package/tests/unit/services/attendanceMerge.test.ts +90 -0
- package/tests/unit/services/attendanceRate.test.ts +38 -0
- package/tests/unit/services/attendanceStats.test.ts +54 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildTranscriptPdf,
|
|
4
|
+
isLevelBasedBoundary,
|
|
5
|
+
} from "../../../src/lib/transcriptPdf.js";
|
|
6
|
+
|
|
7
|
+
describe("isLevelBasedBoundary", () => {
|
|
8
|
+
it("returns false for empty / invalid payloads", () => {
|
|
9
|
+
expect(isLevelBasedBoundary(null)).toBe(false);
|
|
10
|
+
expect(isLevelBasedBoundary("")).toBe(false);
|
|
11
|
+
expect(isLevelBasedBoundary("not-json")).toBe(false);
|
|
12
|
+
expect(isLevelBasedBoundary(JSON.stringify({ boundaries: [] }))).toBe(
|
|
13
|
+
false
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("detects CDC Annex-4 level labels", () => {
|
|
18
|
+
const structured = JSON.stringify({
|
|
19
|
+
name: "CDC Levels",
|
|
20
|
+
boundaries: [
|
|
21
|
+
{ grade: "Advanced", minPercentage: 75, maxPercentage: 100 },
|
|
22
|
+
{ grade: "Proficient", minPercentage: 50, maxPercentage: 74 },
|
|
23
|
+
{ grade: "Basic", minPercentage: 25, maxPercentage: 49 },
|
|
24
|
+
{ grade: "Below Basic", minPercentage: 0, maxPercentage: 24 },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
expect(isLevelBasedBoundary(structured)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns false for letter-grade scales", () => {
|
|
31
|
+
const structured = JSON.stringify({
|
|
32
|
+
name: "AP",
|
|
33
|
+
boundaries: [
|
|
34
|
+
{ grade: "A", minPercentage: 90, maxPercentage: 100 },
|
|
35
|
+
{ grade: "B", minPercentage: 80, maxPercentage: 89 },
|
|
36
|
+
{ grade: "C", minPercentage: 70, maxPercentage: 79 },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
expect(isLevelBasedBoundary(structured)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("buildTranscriptPdf", () => {
|
|
44
|
+
it("builds a non-empty PDF for letter variant", async () => {
|
|
45
|
+
const bytes = await buildTranscriptPdf({
|
|
46
|
+
schoolName: "Kathmandu Pilot School",
|
|
47
|
+
studentName: "Asha Thapa",
|
|
48
|
+
studentUsername: "asha.thapa",
|
|
49
|
+
schoolYear: "2082 BS",
|
|
50
|
+
variant: "letter",
|
|
51
|
+
classes: [
|
|
52
|
+
{
|
|
53
|
+
className: "Mathematics 5",
|
|
54
|
+
subject: "Mathematics",
|
|
55
|
+
section: "A",
|
|
56
|
+
percentage: 88,
|
|
57
|
+
gradeLabel: "A",
|
|
58
|
+
categorySummaries: [
|
|
59
|
+
{ name: "Homework", average: 90 },
|
|
60
|
+
{ name: "Exams", average: 86 },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
attendance: {
|
|
65
|
+
present: 18,
|
|
66
|
+
late: 1,
|
|
67
|
+
absent: 1,
|
|
68
|
+
total: 20,
|
|
69
|
+
percent: 95,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(bytes.byteLength).toBeGreaterThan(500);
|
|
73
|
+
expect(bytes[0]).toBe(0x25); // %PDF
|
|
74
|
+
expect(String.fromCharCode(bytes[1], bytes[2], bytes[3])).toBe("PDF");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("builds a non-empty PDF for level (Annex-4) variant", async () => {
|
|
78
|
+
const bytes = await buildTranscriptPdf({
|
|
79
|
+
schoolName: "Kathmandu Pilot School",
|
|
80
|
+
studentName: "Bikash Rai",
|
|
81
|
+
schoolYear: "2082 BS",
|
|
82
|
+
variant: "level",
|
|
83
|
+
classes: [
|
|
84
|
+
{
|
|
85
|
+
className: "English 3",
|
|
86
|
+
subject: "English",
|
|
87
|
+
section: "B",
|
|
88
|
+
percentage: 72,
|
|
89
|
+
gradeLabel: "Proficient",
|
|
90
|
+
categorySummaries: [],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
className: "Science 3",
|
|
94
|
+
subject: "Science",
|
|
95
|
+
section: "B",
|
|
96
|
+
percentage: 40,
|
|
97
|
+
gradeLabel: "Basic",
|
|
98
|
+
categorySummaries: [],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
attendance: {
|
|
102
|
+
present: 10,
|
|
103
|
+
late: 0,
|
|
104
|
+
absent: 2,
|
|
105
|
+
total: 12,
|
|
106
|
+
percent: (10 / 12) * 100,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(bytes.byteLength).toBeGreaterThan(500);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildExcusalLocks,
|
|
4
|
+
mergeAttendanceUpdateWithLocks,
|
|
5
|
+
} from "../../../src/services/attendanceMerge.js";
|
|
6
|
+
|
|
7
|
+
describe("buildExcusalLocks", () => {
|
|
8
|
+
it("maps excusal students to locks and prefers first excusal per student", () => {
|
|
9
|
+
const locks = buildExcusalLocks(
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
id: "ex-1",
|
|
13
|
+
reason: "ILLNESS",
|
|
14
|
+
students: [{ studentId: "s1" }, { studentId: "s2" }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "ex-2",
|
|
18
|
+
reason: "FAMILY",
|
|
19
|
+
students: [{ studentId: "s1" }],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
["s1", "s2", "s3"]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(locks).toEqual([
|
|
26
|
+
{ studentId: "s1", excusalId: "ex-1", reason: "ILLNESS", note: undefined },
|
|
27
|
+
{ studentId: "s2", excusalId: "ex-1", reason: "ILLNESS", note: undefined },
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("mergeAttendanceUpdateWithLocks", () => {
|
|
33
|
+
it("forces locked students to EXCUSED and strips them from other lists", () => {
|
|
34
|
+
const merged = mergeAttendanceUpdateWithLocks({
|
|
35
|
+
present: [
|
|
36
|
+
{ id: "s1", username: "a" },
|
|
37
|
+
{ id: "s2", username: "b" },
|
|
38
|
+
],
|
|
39
|
+
late: [{ id: "s3", username: "c" }],
|
|
40
|
+
absent: [{ id: "s4", username: "d" }],
|
|
41
|
+
excused: [{ id: "s5", username: "e" }],
|
|
42
|
+
locks: [
|
|
43
|
+
{ studentId: "s2", excusalId: "ex-1", reason: "ILLNESS" },
|
|
44
|
+
{ studentId: "s4", excusalId: "ex-2", reason: "FAMILY" },
|
|
45
|
+
],
|
|
46
|
+
updatedById: "teacher-1",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(merged.presentIds).toEqual(["s1"]);
|
|
50
|
+
expect(merged.lateIds).toEqual(["s3"]);
|
|
51
|
+
expect(merged.absentIds).toEqual([]);
|
|
52
|
+
expect(merged.excusedIds.sort()).toEqual(["s2", "s4", "s5"].sort());
|
|
53
|
+
expect(merged.lockedStudentIds.sort()).toEqual(["s2", "s4"].sort());
|
|
54
|
+
|
|
55
|
+
const byStudent = new Map(merged.records.map((r) => [r.studentId, r]));
|
|
56
|
+
expect(byStudent.get("s2")).toMatchObject({
|
|
57
|
+
status: "EXCUSED",
|
|
58
|
+
excusedByExcusalId: "ex-1",
|
|
59
|
+
updatedById: "teacher-1",
|
|
60
|
+
});
|
|
61
|
+
expect(byStudent.get("s4")).toMatchObject({
|
|
62
|
+
status: "EXCUSED",
|
|
63
|
+
excusedByExcusalId: "ex-2",
|
|
64
|
+
});
|
|
65
|
+
expect(byStudent.get("s5")).toMatchObject({
|
|
66
|
+
status: "EXCUSED",
|
|
67
|
+
excusedByExcusalId: null,
|
|
68
|
+
});
|
|
69
|
+
expect(byStudent.get("s1")?.status).toBe("PRESENT");
|
|
70
|
+
expect(byStudent.get("s3")?.status).toBe("LATE");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("applies excusal locks even when teacher marks the student present", () => {
|
|
74
|
+
const merged = mergeAttendanceUpdateWithLocks({
|
|
75
|
+
present: [{ id: "s1", username: "a" }],
|
|
76
|
+
late: [],
|
|
77
|
+
absent: [],
|
|
78
|
+
locks: [{ studentId: "s1", excusalId: "ex-9" }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(merged.presentIds).toEqual([]);
|
|
82
|
+
expect(merged.records).toEqual([
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
studentId: "s1",
|
|
85
|
+
status: "EXCUSED",
|
|
86
|
+
excusedByExcusalId: "ex-9",
|
|
87
|
+
}),
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { computeAttendanceRate } from "../../../src/services/attendanceRate.js";
|
|
3
|
+
|
|
4
|
+
describe("computeAttendanceRate", () => {
|
|
5
|
+
it("returns null percent with no records", () => {
|
|
6
|
+
expect(computeAttendanceRate([])).toEqual({
|
|
7
|
+
present: 0,
|
|
8
|
+
late: 0,
|
|
9
|
+
absent: 0,
|
|
10
|
+
total: 0,
|
|
11
|
+
percent: null,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("counts late toward attendance rate", () => {
|
|
16
|
+
const result = computeAttendanceRate([
|
|
17
|
+
{ status: "PRESENT" },
|
|
18
|
+
{ status: "PRESENT" },
|
|
19
|
+
{ status: "LATE" },
|
|
20
|
+
{ status: "ABSENT" },
|
|
21
|
+
]);
|
|
22
|
+
expect(result).toEqual({
|
|
23
|
+
present: 2,
|
|
24
|
+
late: 1,
|
|
25
|
+
absent: 1,
|
|
26
|
+
total: 4,
|
|
27
|
+
percent: 75,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("is 100% when never absent", () => {
|
|
32
|
+
const result = computeAttendanceRate([
|
|
33
|
+
{ status: "PRESENT" },
|
|
34
|
+
{ status: "LATE" },
|
|
35
|
+
]);
|
|
36
|
+
expect(result.percent).toBe(100);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
computeAttendancePercentage,
|
|
4
|
+
computeAttendancePercentageFromStatuses,
|
|
5
|
+
tallyAttendanceStatuses,
|
|
6
|
+
} from "../../../src/services/attendanceStats.js";
|
|
7
|
+
|
|
8
|
+
describe("attendanceStats", () => {
|
|
9
|
+
it("tallies each attendance status", () => {
|
|
10
|
+
expect(
|
|
11
|
+
tallyAttendanceStatuses([
|
|
12
|
+
"PRESENT",
|
|
13
|
+
"LATE",
|
|
14
|
+
"ABSENT",
|
|
15
|
+
"EXCUSED",
|
|
16
|
+
"PRESENT",
|
|
17
|
+
])
|
|
18
|
+
).toEqual({
|
|
19
|
+
present: 2,
|
|
20
|
+
late: 1,
|
|
21
|
+
absent: 1,
|
|
22
|
+
excused: 1,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("excludes excused from attendance percentage denominator and numerator", () => {
|
|
27
|
+
// 2 present + 1 late + 1 absent + 3 excused => (2+1)/(2+1+1) = 75%
|
|
28
|
+
const percentage = computeAttendancePercentageFromStatuses([
|
|
29
|
+
"PRESENT",
|
|
30
|
+
"PRESENT",
|
|
31
|
+
"LATE",
|
|
32
|
+
"ABSENT",
|
|
33
|
+
"EXCUSED",
|
|
34
|
+
"EXCUSED",
|
|
35
|
+
"EXCUSED",
|
|
36
|
+
]);
|
|
37
|
+
expect(percentage).toBeCloseTo(75);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns null when only excused records exist", () => {
|
|
41
|
+
expect(
|
|
42
|
+
computeAttendancePercentageFromStatuses(["EXCUSED", "EXCUSED"])
|
|
43
|
+
).toBeNull();
|
|
44
|
+
expect(
|
|
45
|
+
computeAttendancePercentage({ present: 0, late: 0, absent: 0 })
|
|
46
|
+
).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("treats late as present for attendance rate", () => {
|
|
50
|
+
expect(
|
|
51
|
+
computeAttendancePercentage({ present: 1, late: 1, absent: 2 })
|
|
52
|
+
).toBeCloseTo(50);
|
|
53
|
+
});
|
|
54
|
+
});
|