@studious-lms/server 1.22.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.
Files changed (298) hide show
  1. package/.env.example +0 -1
  2. package/data/curriculum/nepal-cdc-g4-g5/README.md +90 -0
  3. package/data/curriculum/nepal-cdc-g4-g5/manifest.json +97 -0
  4. package/data/curriculum/nepal-cdc-g4-g5/proficiency-scale.json +76 -0
  5. package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +6936 -0
  6. package/data/curriculum/nepal-cdc-g4-g5/subjects/health-physical-creative-arts.json +3401 -0
  7. package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1511 -0
  8. package/data/curriculum/nepal-cdc-g4-g5/subjects/nepali.json +7928 -0
  9. package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +2420 -0
  10. package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +2177 -0
  11. package/dist/lib/assignmentTypeDefaults.d.ts +41 -0
  12. package/dist/lib/assignmentTypeDefaults.d.ts.map +1 -0
  13. package/dist/lib/assignmentTypeDefaults.js +167 -0
  14. package/dist/lib/assignmentTypeDefaults.js.map +1 -0
  15. package/dist/lib/config/env.d.ts +0 -8
  16. package/dist/lib/config/env.d.ts.map +1 -1
  17. package/dist/lib/config/env.js +2 -5
  18. package/dist/lib/config/env.js.map +1 -1
  19. package/dist/lib/feePdf.d.ts +19 -0
  20. package/dist/lib/feePdf.d.ts.map +1 -0
  21. package/dist/lib/feePdf.js +167 -0
  22. package/dist/lib/feePdf.js.map +1 -0
  23. package/dist/lib/feePdfFormat.d.ts +4 -0
  24. package/dist/lib/feePdfFormat.d.ts.map +1 -0
  25. package/dist/lib/feePdfFormat.js +16 -0
  26. package/dist/lib/feePdfFormat.js.map +1 -0
  27. package/dist/lib/idCardPdf.d.ts +26 -0
  28. package/dist/lib/idCardPdf.d.ts.map +1 -0
  29. package/dist/lib/idCardPdf.js +272 -0
  30. package/dist/lib/idCardPdf.js.map +1 -0
  31. package/dist/lib/pdfLetterhead.d.ts +41 -0
  32. package/dist/lib/pdfLetterhead.d.ts.map +1 -0
  33. package/dist/lib/pdfLetterhead.js +86 -0
  34. package/dist/lib/pdfLetterhead.js.map +1 -0
  35. package/dist/lib/transcriptPdf.d.ts +55 -0
  36. package/dist/lib/transcriptPdf.d.ts.map +1 -0
  37. package/dist/lib/transcriptPdf.js +303 -0
  38. package/dist/lib/transcriptPdf.js.map +1 -0
  39. package/dist/models/academicCalendar.d.ts +198 -0
  40. package/dist/models/academicCalendar.d.ts.map +1 -0
  41. package/dist/models/academicCalendar.js +106 -0
  42. package/dist/models/academicCalendar.js.map +1 -0
  43. package/dist/models/agenda.d.ts +8 -0
  44. package/dist/models/agenda.d.ts.map +1 -1
  45. package/dist/models/announcement.d.ts +2 -0
  46. package/dist/models/announcement.d.ts.map +1 -1
  47. package/dist/models/assignment.d.ts +66 -0
  48. package/dist/models/assignment.d.ts.map +1 -1
  49. package/dist/models/attendance.d.ts +659 -21
  50. package/dist/models/attendance.d.ts.map +1 -1
  51. package/dist/models/attendance.js +544 -75
  52. package/dist/models/attendance.js.map +1 -1
  53. package/dist/models/auth.d.ts +2 -0
  54. package/dist/models/auth.d.ts.map +1 -1
  55. package/dist/models/class.d.ts +12 -0
  56. package/dist/models/class.d.ts.map +1 -1
  57. package/dist/models/class.js +3 -2
  58. package/dist/models/class.js.map +1 -1
  59. package/dist/models/event.d.ts +16 -0
  60. package/dist/models/event.d.ts.map +1 -1
  61. package/dist/models/fees.d.ts +554 -0
  62. package/dist/models/fees.d.ts.map +1 -0
  63. package/dist/models/fees.js +278 -0
  64. package/dist/models/fees.js.map +1 -0
  65. package/dist/models/file.d.ts +29 -0
  66. package/dist/models/file.d.ts.map +1 -1
  67. package/dist/models/folder.d.ts +2 -0
  68. package/dist/models/folder.d.ts.map +1 -1
  69. package/dist/models/labChat.d.ts +2 -0
  70. package/dist/models/labChat.d.ts.map +1 -1
  71. package/dist/models/marketplace.d.ts +178 -0
  72. package/dist/models/marketplace.d.ts.map +1 -0
  73. package/dist/models/marketplace.js +115 -0
  74. package/dist/models/marketplace.js.map +1 -0
  75. package/dist/models/moduleItem.d.ts +304 -0
  76. package/dist/models/moduleItem.d.ts.map +1 -0
  77. package/dist/models/moduleItem.js +78 -0
  78. package/dist/models/moduleItem.js.map +1 -0
  79. package/dist/models/newtonChat.d.ts +1 -0
  80. package/dist/models/newtonChat.d.ts.map +1 -1
  81. package/dist/models/school.d.ts +193 -0
  82. package/dist/models/school.d.ts.map +1 -1
  83. package/dist/models/school.js +80 -2
  84. package/dist/models/school.js.map +1 -1
  85. package/dist/models/section.d.ts +4 -0
  86. package/dist/models/section.d.ts.map +1 -1
  87. package/dist/models/worksheet.d.ts +19 -0
  88. package/dist/models/worksheet.d.ts.map +1 -1
  89. package/dist/models/worksheet.js +2 -2
  90. package/dist/models/worksheet.js.map +1 -1
  91. package/dist/routers/_app.d.ts +11605 -7133
  92. package/dist/routers/_app.d.ts.map +1 -1
  93. package/dist/routers/_app.js +12 -2
  94. package/dist/routers/_app.js.map +1 -1
  95. package/dist/routers/academicCalendar.d.ts +229 -0
  96. package/dist/routers/academicCalendar.d.ts.map +1 -0
  97. package/dist/routers/academicCalendar.js +75 -0
  98. package/dist/routers/academicCalendar.js.map +1 -0
  99. package/dist/routers/admin.d.ts +2 -0
  100. package/dist/routers/admin.d.ts.map +1 -1
  101. package/dist/routers/agenda.d.ts +8 -0
  102. package/dist/routers/agenda.d.ts.map +1 -1
  103. package/dist/routers/assignment.d.ts +75 -0
  104. package/dist/routers/assignment.d.ts.map +1 -1
  105. package/dist/routers/assignment.js +10 -2
  106. package/dist/routers/assignment.js.map +1 -1
  107. package/dist/routers/attendance.d.ts +103 -0
  108. package/dist/routers/attendance.d.ts.map +1 -1
  109. package/dist/routers/attendance.js +8 -5
  110. package/dist/routers/attendance.js.map +1 -1
  111. package/dist/routers/class.d.ts +27 -0
  112. package/dist/routers/class.d.ts.map +1 -1
  113. package/dist/routers/class.js +6 -2
  114. package/dist/routers/class.js.map +1 -1
  115. package/dist/routers/event.d.ts +8 -0
  116. package/dist/routers/event.d.ts.map +1 -1
  117. package/dist/routers/fees.d.ts +513 -0
  118. package/dist/routers/fees.d.ts.map +1 -0
  119. package/dist/routers/fees.js +88 -0
  120. package/dist/routers/fees.js.map +1 -0
  121. package/dist/routers/file.d.ts +2 -0
  122. package/dist/routers/file.d.ts.map +1 -1
  123. package/dist/routers/gradebook.d.ts +4 -1
  124. package/dist/routers/gradebook.d.ts.map +1 -1
  125. package/dist/routers/marketplace.d.ts +371 -0
  126. package/dist/routers/marketplace.d.ts.map +1 -0
  127. package/dist/routers/marketplace.js +87 -0
  128. package/dist/routers/marketplace.js.map +1 -0
  129. package/dist/routers/moduleItem.d.ts +263 -0
  130. package/dist/routers/moduleItem.d.ts.map +1 -0
  131. package/dist/routers/moduleItem.js +60 -0
  132. package/dist/routers/moduleItem.js.map +1 -0
  133. package/dist/routers/parent.d.ts +169 -0
  134. package/dist/routers/parent.d.ts.map +1 -0
  135. package/dist/routers/parent.js +49 -0
  136. package/dist/routers/parent.js.map +1 -0
  137. package/dist/routers/school.d.ts +476 -6
  138. package/dist/routers/school.d.ts.map +1 -1
  139. package/dist/routers/school.js +130 -4
  140. package/dist/routers/school.js.map +1 -1
  141. package/dist/routers/worksheet.d.ts +27 -0
  142. package/dist/routers/worksheet.d.ts.map +1 -1
  143. package/dist/routers/worksheet.js +10 -3
  144. package/dist/routers/worksheet.js.map +1 -1
  145. package/dist/services/academicCalendar.d.ts +173 -0
  146. package/dist/services/academicCalendar.d.ts.map +1 -0
  147. package/dist/services/academicCalendar.js +214 -0
  148. package/dist/services/academicCalendar.js.map +1 -0
  149. package/dist/services/adminInstitution.d.ts +2 -0
  150. package/dist/services/adminInstitution.d.ts.map +1 -1
  151. package/dist/services/agenda.d.ts +8 -0
  152. package/dist/services/agenda.d.ts.map +1 -1
  153. package/dist/services/assignment.d.ts +75 -0
  154. package/dist/services/assignment.d.ts.map +1 -1
  155. package/dist/services/assignment.js +91 -12
  156. package/dist/services/assignment.js.map +1 -1
  157. package/dist/services/attendance.d.ts +323 -10
  158. package/dist/services/attendance.d.ts.map +1 -1
  159. package/dist/services/attendance.js +385 -25
  160. package/dist/services/attendance.js.map +1 -1
  161. package/dist/services/attendanceMerge.d.ts +53 -0
  162. package/dist/services/attendanceMerge.d.ts.map +1 -0
  163. package/dist/services/attendanceMerge.js +87 -0
  164. package/dist/services/attendanceMerge.js.map +1 -0
  165. package/dist/services/attendanceRate.d.ts +24 -0
  166. package/dist/services/attendanceRate.d.ts.map +1 -0
  167. package/dist/services/attendanceRate.js +31 -0
  168. package/dist/services/attendanceRate.js.map +1 -0
  169. package/dist/services/attendanceStats.d.ts +22 -0
  170. package/dist/services/attendanceStats.d.ts.map +1 -0
  171. package/dist/services/attendanceStats.js +45 -0
  172. package/dist/services/attendanceStats.js.map +1 -0
  173. package/dist/services/class.d.ts +27 -0
  174. package/dist/services/class.d.ts.map +1 -1
  175. package/dist/services/class.js +26 -3
  176. package/dist/services/class.js.map +1 -1
  177. package/dist/services/event.d.ts +8 -0
  178. package/dist/services/event.d.ts.map +1 -1
  179. package/dist/services/fees.d.ts +422 -0
  180. package/dist/services/fees.d.ts.map +1 -0
  181. package/dist/services/fees.js +310 -0
  182. package/dist/services/fees.js.map +1 -0
  183. package/dist/services/feesAuthz.d.ts +49 -0
  184. package/dist/services/feesAuthz.d.ts.map +1 -0
  185. package/dist/services/feesAuthz.js +52 -0
  186. package/dist/services/feesAuthz.js.map +1 -0
  187. package/dist/services/file.d.ts +4 -0
  188. package/dist/services/file.d.ts.map +1 -1
  189. package/dist/services/gradebook.d.ts +18 -1
  190. package/dist/services/gradebook.d.ts.map +1 -1
  191. package/dist/services/gradebook.js +57 -2
  192. package/dist/services/gradebook.js.map +1 -1
  193. package/dist/services/idCard.d.ts +11 -0
  194. package/dist/services/idCard.d.ts.map +1 -0
  195. package/dist/services/idCard.js +122 -0
  196. package/dist/services/idCard.js.map +1 -0
  197. package/dist/services/marketplace.d.ts +301 -0
  198. package/dist/services/marketplace.d.ts.map +1 -0
  199. package/dist/services/marketplace.js +628 -0
  200. package/dist/services/marketplace.js.map +1 -0
  201. package/dist/services/moduleItem.d.ts +196 -0
  202. package/dist/services/moduleItem.d.ts.map +1 -0
  203. package/dist/services/moduleItem.js +188 -0
  204. package/dist/services/moduleItem.js.map +1 -0
  205. package/dist/services/notification.d.ts +58 -0
  206. package/dist/services/notification.d.ts.map +1 -1
  207. package/dist/services/notification.js +30 -2
  208. package/dist/services/notification.js.map +1 -1
  209. package/dist/services/parentPortal.d.ts +133 -0
  210. package/dist/services/parentPortal.d.ts.map +1 -0
  211. package/dist/services/parentPortal.js +328 -0
  212. package/dist/services/parentPortal.js.map +1 -0
  213. package/dist/services/school.d.ts +122 -3
  214. package/dist/services/school.d.ts.map +1 -1
  215. package/dist/services/school.js +179 -47
  216. package/dist/services/school.js.map +1 -1
  217. package/dist/services/studentImport.d.ts +34 -0
  218. package/dist/services/studentImport.d.ts.map +1 -0
  219. package/dist/services/studentImport.js +236 -0
  220. package/dist/services/studentImport.js.map +1 -0
  221. package/dist/services/transcript.d.ts +61 -0
  222. package/dist/services/transcript.d.ts.map +1 -0
  223. package/dist/services/transcript.js +243 -0
  224. package/dist/services/transcript.js.map +1 -0
  225. package/dist/services/worksheet.d.ts +22 -0
  226. package/dist/services/worksheet.d.ts.map +1 -1
  227. package/dist/services/worksheet.js +94 -2
  228. package/dist/services/worksheet.js.map +1 -1
  229. package/dist/trpc.d.ts +1 -1
  230. package/dist/trpc.d.ts.map +1 -1
  231. package/dist/trpc.js +5 -5
  232. package/dist/trpc.js.map +1 -1
  233. package/dist/utils/adminPanelAccess.d.ts +4 -5
  234. package/dist/utils/adminPanelAccess.d.ts.map +1 -1
  235. package/dist/utils/adminPanelAccess.js +12 -34
  236. package/dist/utils/adminPanelAccess.js.map +1 -1
  237. package/package.json +2 -1
  238. package/prisma/migrations/20260804120000_add_attendance_records_and_excusals/migration.sql +114 -0
  239. package/prisma/migrations/20260804120100_add_fee_plans_invoices_payments/migration.sql +101 -0
  240. package/prisma/migrations/20260804120200_add_marketplace_listings/migration.sql +54 -0
  241. package/prisma/migrations/20260804120300_content_platform_b3_b9/migration.sql +63 -0
  242. package/prisma/migrations/20260804120400_c_academic_ops/migration.sql +77 -0
  243. package/prisma/schema.prisma +371 -11
  244. package/scripts/import-nepal-cdc-curriculum.ts +379 -0
  245. package/src/lib/assignmentTypeDefaults.ts +222 -0
  246. package/src/lib/config/env.ts +0 -3
  247. package/src/lib/feePdf.ts +199 -0
  248. package/src/lib/feePdfFormat.ts +14 -0
  249. package/src/lib/idCardPdf.ts +336 -0
  250. package/src/lib/pdfLetterhead.ts +124 -0
  251. package/src/lib/transcriptPdf.ts +502 -0
  252. package/src/models/academicCalendar.ts +139 -0
  253. package/src/models/attendance.ts +704 -73
  254. package/src/models/class.ts +1 -0
  255. package/src/models/fees.ts +349 -0
  256. package/src/models/marketplace.ts +158 -0
  257. package/src/models/moduleItem.ts +107 -0
  258. package/src/models/school.ts +95 -0
  259. package/src/models/worksheet.ts +2 -0
  260. package/src/routers/_app.ts +10 -0
  261. package/src/routers/academicCalendar.ts +103 -0
  262. package/src/routers/assignment.ts +8 -0
  263. package/src/routers/attendance.ts +7 -3
  264. package/src/routers/class.ts +4 -0
  265. package/src/routers/fees.ts +126 -0
  266. package/src/routers/marketplace.ts +134 -0
  267. package/src/routers/moduleItem.ts +89 -0
  268. package/src/routers/parent.ts +68 -0
  269. package/src/routers/school.ts +184 -1
  270. package/src/routers/worksheet.ts +11 -0
  271. package/src/services/academicCalendar.ts +299 -0
  272. package/src/services/assignment.ts +122 -12
  273. package/src/services/attendance.ts +548 -26
  274. package/src/services/attendanceMerge.ts +139 -0
  275. package/src/services/attendanceRate.ts +41 -0
  276. package/src/services/attendanceStats.ts +63 -0
  277. package/src/services/class.ts +25 -1
  278. package/src/services/fees.ts +424 -0
  279. package/src/services/feesAuthz.ts +77 -0
  280. package/src/services/gradebook.ts +70 -0
  281. package/src/services/idCard.ts +147 -0
  282. package/src/services/marketplace.ts +785 -0
  283. package/src/services/moduleItem.ts +274 -0
  284. package/src/services/notification.ts +43 -0
  285. package/src/services/parentPortal.ts +380 -0
  286. package/src/services/school.ts +243 -45
  287. package/src/services/studentImport.ts +273 -0
  288. package/src/services/transcript.ts +313 -0
  289. package/src/services/worksheet.ts +111 -0
  290. package/src/trpc.ts +3 -3
  291. package/src/utils/adminPanelAccess.ts +12 -35
  292. package/tests/unit/assignmentTypeDefaults.test.ts +52 -0
  293. package/tests/unit/lib/feePdf.test.ts +15 -0
  294. package/tests/unit/lib/idCardPdf.test.ts +40 -0
  295. package/tests/unit/lib/transcriptPdf.test.ts +111 -0
  296. package/tests/unit/services/attendanceMerge.test.ts +90 -0
  297. package/tests/unit/services/attendanceRate.test.ts +38 -0
  298. package/tests/unit/services/attendanceStats.test.ts +54 -0
@@ -1,10 +1,21 @@
1
- /**
2
- * Attendance model – attendance records, present/late/absent students per event.
3
- */
4
1
 
5
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a57d4718-2b2b-536c-90dc-37d792317353")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="04968f1d-49ec-52b6-ab2b-a86286e300ce")}catch(e){}}();
6
3
  import { prisma } from "../lib/prisma.js";
7
4
  import { classTeacherOrSchoolAdminWhere } from "./access.js";
5
+ const studentProfileSelect = {
6
+ id: true,
7
+ username: true,
8
+ profile: {
9
+ select: {
10
+ displayName: true,
11
+ profilePicture: true,
12
+ profilePictureThumbnail: true,
13
+ bio: true,
14
+ location: true,
15
+ website: true,
16
+ },
17
+ },
18
+ };
8
19
  const attendanceInclude = {
9
20
  event: {
10
21
  select: {
@@ -16,50 +27,27 @@ const attendanceInclude = {
16
27
  color: true,
17
28
  },
18
29
  },
19
- present: {
20
- select: {
21
- id: true,
22
- username: true,
23
- profile: {
24
- select: {
25
- displayName: true,
26
- profilePicture: true,
27
- profilePictureThumbnail: true,
28
- bio: true,
29
- location: true,
30
- website: true,
31
- },
32
- },
33
- },
34
- },
35
- late: {
30
+ present: { select: studentProfileSelect },
31
+ late: { select: studentProfileSelect },
32
+ absent: { select: studentProfileSelect },
33
+ records: {
36
34
  select: {
37
35
  id: true,
38
- username: true,
39
- profile: {
40
- select: {
41
- displayName: true,
42
- profilePicture: true,
43
- profilePictureThumbnail: true,
44
- bio: true,
45
- location: true,
46
- website: true,
47
- },
48
- },
49
- },
50
- },
51
- absent: {
52
- select: {
53
- id: true,
54
- username: true,
55
- profile: {
36
+ studentId: true,
37
+ status: true,
38
+ note: true,
39
+ excusedByExcusalId: true,
40
+ updatedById: true,
41
+ updatedAt: true,
42
+ student: { select: studentProfileSelect },
43
+ excusedByExcusal: {
56
44
  select: {
57
- displayName: true,
58
- profilePicture: true,
59
- profilePictureThumbnail: true,
60
- bio: true,
61
- location: true,
62
- website: true,
45
+ id: true,
46
+ reason: true,
47
+ note: true,
48
+ startDate: true,
49
+ endDate: true,
50
+ revokedAt: true,
63
51
  },
64
52
  },
65
53
  },
@@ -78,14 +66,27 @@ const attendanceUpdateInclude = {
78
66
  present: { select: { id: true, username: true } },
79
67
  late: { select: { id: true, username: true } },
80
68
  absent: { select: { id: true, username: true } },
81
- };
82
- /** @returns Class with students for attendance. */
83
- export function findClassWithStudents(classId) {
84
- return prisma.class.findUnique({
85
- where: { id: classId },
69
+ records: {
86
70
  select: {
87
71
  id: true,
88
- students: {
72
+ studentId: true,
73
+ status: true,
74
+ note: true,
75
+ excusedByExcusalId: true,
76
+ updatedById: true,
77
+ updatedAt: true,
78
+ student: { select: { id: true, username: true } },
79
+ excusedByExcusal: {
80
+ select: { id: true, reason: true, note: true },
81
+ },
82
+ },
83
+ },
84
+ };
85
+ const excusalInclude = {
86
+ students: {
87
+ select: {
88
+ studentId: true,
89
+ student: {
89
90
  select: {
90
91
  id: true,
91
92
  username: true,
@@ -94,14 +95,38 @@ export function findClassWithStudents(classId) {
94
95
  displayName: true,
95
96
  profilePicture: true,
96
97
  profilePictureThumbnail: true,
97
- bio: true,
98
- location: true,
99
- website: true,
100
98
  },
101
99
  },
102
100
  },
103
101
  },
104
102
  },
103
+ },
104
+ createdBy: {
105
+ select: {
106
+ id: true,
107
+ username: true,
108
+ profile: { select: { displayName: true } },
109
+ },
110
+ },
111
+ revokedBy: {
112
+ select: {
113
+ id: true,
114
+ username: true,
115
+ profile: { select: { displayName: true } },
116
+ },
117
+ },
118
+ };
119
+ /** @returns Class with students for attendance. */
120
+ export function findClassWithStudents(classId) {
121
+ return prisma.class.findUnique({
122
+ where: { id: classId },
123
+ select: {
124
+ id: true,
125
+ schoolId: true,
126
+ students: {
127
+ select: studentProfileSelect,
128
+ },
129
+ },
105
130
  });
106
131
  }
107
132
  /** @returns Class if user is a teacher or school admin. */
@@ -114,6 +139,7 @@ export function findClassWithTeacher(classId, teacherId) {
114
139
  select: {
115
140
  id: true,
116
141
  name: true,
142
+ schoolId: true,
117
143
  students: { select: { id: true } },
118
144
  },
119
145
  });
@@ -128,6 +154,15 @@ export function findEventsByClassId(classId) {
128
154
  export function findAttendanceByEventId(eventId) {
129
155
  return prisma.attendance.findFirst({
130
156
  where: { eventId },
157
+ include: {
158
+ records: {
159
+ select: {
160
+ studentId: true,
161
+ status: true,
162
+ excusedByExcusalId: true,
163
+ },
164
+ },
165
+ },
131
166
  });
132
167
  }
133
168
  /** @returns Attendance for class and event (eventId null = class-level). */
@@ -142,10 +177,19 @@ export function findAttendanceByClassAndEvent(classId, eventId) {
142
177
  select: {
143
178
  id: true,
144
179
  name: true,
180
+ startTime: true,
145
181
  },
146
182
  },
147
183
  late: { select: { id: true } },
148
184
  absent: { select: { id: true } },
185
+ records: {
186
+ select: {
187
+ studentId: true,
188
+ status: true,
189
+ excusedByExcusalId: true,
190
+ note: true,
191
+ },
192
+ },
149
193
  },
150
194
  });
151
195
  }
@@ -160,45 +204,470 @@ export function findManyAttendance(classId, eventId) {
160
204
  orderBy: { date: "desc" },
161
205
  });
162
206
  }
163
- /** Create an attendance record. */
207
+ /** Create an attendance session with legacy lists + per-student records. */
164
208
  export function createAttendance(data) {
209
+ const lateIds = data.lateIds ?? [];
210
+ const absentIds = data.absentIds ?? [];
211
+ const excusedIds = data.excusedIds ?? [];
212
+ const records = data.records ??
213
+ buildRecordsFromLists({
214
+ presentIds: data.presentIds,
215
+ lateIds,
216
+ absentIds,
217
+ excusedIds,
218
+ updatedById: data.updatedById,
219
+ });
165
220
  return prisma.attendance.create({
166
221
  data: {
167
222
  classId: data.classId,
168
223
  eventId: data.eventId ?? undefined,
169
- date: new Date(),
224
+ date: data.date ?? new Date(),
170
225
  present: { connect: data.presentIds.map((id) => ({ id })) },
171
- late: data.lateIds?.length
172
- ? { connect: data.lateIds.map((id) => ({ id })) }
173
- : undefined,
174
- absent: data.absentIds?.length
175
- ? { connect: data.absentIds.map((id) => ({ id })) }
176
- : undefined,
226
+ late: lateIds.length ? { connect: lateIds.map((id) => ({ id })) } : undefined,
227
+ absent: absentIds.length ? { connect: absentIds.map((id) => ({ id })) } : undefined,
228
+ records: {
229
+ create: records.map((record) => ({
230
+ studentId: record.studentId,
231
+ status: record.status,
232
+ note: record.note ?? undefined,
233
+ excusedByExcusalId: record.excusedByExcusalId ?? undefined,
234
+ updatedById: record.updatedById ?? data.updatedById ?? undefined,
235
+ })),
236
+ },
177
237
  },
178
238
  include: attendanceUpdateInclude,
179
239
  });
180
240
  }
181
241
  /** Create attendance for a specific event. */
182
242
  export function createAttendanceForEvent(data) {
183
- return prisma.attendance.create({
243
+ return createAttendance({
244
+ classId: data.classId,
245
+ eventId: data.eventId,
246
+ date: data.date,
247
+ presentIds: data.presentIds,
248
+ lateIds: data.lateIds,
249
+ absentIds: data.absentIds,
250
+ excusedIds: data.excusedIds,
251
+ records: data.records,
252
+ });
253
+ }
254
+ /** Update attendance present/late/absent lists and sync per-student records. */
255
+ export async function updateAttendance(id, data) {
256
+ const studentIds = data.records.map((r) => r.studentId);
257
+ return prisma.$transaction(async (tx) => {
258
+ // Replace per-student rows for students in this update set.
259
+ await tx.attendanceRecord.deleteMany({
260
+ where: {
261
+ attendanceId: id,
262
+ studentId: { in: studentIds },
263
+ },
264
+ });
265
+ if (data.records.length > 0) {
266
+ await tx.attendanceRecord.createMany({
267
+ data: data.records.map((record) => ({
268
+ attendanceId: id,
269
+ studentId: record.studentId,
270
+ status: record.status,
271
+ note: record.note ?? null,
272
+ excusedByExcusalId: record.excusedByExcusalId ?? null,
273
+ updatedById: record.updatedById ?? null,
274
+ })),
275
+ });
276
+ }
277
+ return tx.attendance.update({
278
+ where: { id },
279
+ data: {
280
+ present: { set: data.presentIds.map((id) => ({ id })) },
281
+ late: { set: data.lateIds.map((id) => ({ id })) },
282
+ absent: { set: data.absentIds.map((id) => ({ id })) },
283
+ },
284
+ include: attendanceUpdateInclude,
285
+ });
286
+ });
287
+ }
288
+ function buildRecordsFromLists(input) {
289
+ const records = [];
290
+ for (const studentId of input.presentIds) {
291
+ records.push({ studentId, status: "PRESENT", updatedById: input.updatedById });
292
+ }
293
+ for (const studentId of input.lateIds) {
294
+ records.push({ studentId, status: "LATE", updatedById: input.updatedById });
295
+ }
296
+ for (const studentId of input.absentIds) {
297
+ records.push({ studentId, status: "ABSENT", updatedById: input.updatedById });
298
+ }
299
+ for (const studentId of input.excusedIds) {
300
+ records.push({ studentId, status: "EXCUSED", updatedById: input.updatedById });
301
+ }
302
+ return records;
303
+ }
304
+ /** Active (non-revoked) excusals covering a calendar date for given students. */
305
+ export function findActiveExcusalsForStudents(input) {
306
+ if (input.studentIds.length === 0) {
307
+ return Promise.resolve([]);
308
+ }
309
+ const day = toDateOnly(input.date);
310
+ return prisma.studentExcusal.findMany({
311
+ where: {
312
+ schoolId: input.schoolId,
313
+ revokedAt: null,
314
+ startDate: { lte: day },
315
+ endDate: { gte: day },
316
+ students: { some: { studentId: { in: input.studentIds } } },
317
+ },
318
+ include: {
319
+ students: { select: { studentId: true } },
320
+ },
321
+ });
322
+ }
323
+ /** Active excusals overlapping a date range for school students. */
324
+ export function findActiveExcusalsInRange(input) {
325
+ const start = toDateOnly(input.startDate);
326
+ const end = toDateOnly(input.endDate);
327
+ return prisma.studentExcusal.findMany({
328
+ where: {
329
+ schoolId: input.schoolId,
330
+ revokedAt: null,
331
+ startDate: { lte: end },
332
+ endDate: { gte: start },
333
+ ...(input.studentIds?.length
334
+ ? { students: { some: { studentId: { in: input.studentIds } } } }
335
+ : {}),
336
+ },
337
+ include: {
338
+ students: { select: { studentId: true } },
339
+ },
340
+ });
341
+ }
342
+ export function listExcusalsForSchool(input) {
343
+ return prisma.studentExcusal.findMany({
344
+ where: {
345
+ schoolId: input.schoolId,
346
+ ...(input.includeRevoked ? {} : { revokedAt: null }),
347
+ ...(input.studentId
348
+ ? { students: { some: { studentId: input.studentId } } }
349
+ : {}),
350
+ },
351
+ include: excusalInclude,
352
+ orderBy: [{ startDate: "desc" }, { createdAt: "desc" }],
353
+ });
354
+ }
355
+ export function findExcusalById(excusalId) {
356
+ return prisma.studentExcusal.findUnique({
357
+ where: { id: excusalId },
358
+ include: {
359
+ ...excusalInclude,
360
+ students: { select: { studentId: true } },
361
+ },
362
+ });
363
+ }
364
+ export function createStudentExcusal(data) {
365
+ return prisma.studentExcusal.create({
184
366
  data: {
185
- class: { connect: { id: data.classId } },
186
- event: { connect: { id: data.eventId } },
187
- present: { connect: data.presentIds.map((id) => ({ id })) },
367
+ schoolId: data.schoolId,
368
+ startDate: toDateOnly(data.startDate),
369
+ endDate: toDateOnly(data.endDate),
370
+ reason: data.reason,
371
+ note: data.note ?? null,
372
+ createdById: data.createdById,
373
+ students: {
374
+ create: data.studentIds.map((studentId) => ({ studentId })),
375
+ },
188
376
  },
377
+ include: excusalInclude,
378
+ });
379
+ }
380
+ export function updateStudentExcusal(excusalId, data) {
381
+ return prisma.$transaction(async (tx) => {
382
+ if (data.studentIds) {
383
+ await tx.studentExcusalStudent.deleteMany({ where: { excusalId } });
384
+ await tx.studentExcusalStudent.createMany({
385
+ data: data.studentIds.map((studentId) => ({ excusalId, studentId })),
386
+ });
387
+ }
388
+ return tx.studentExcusal.update({
389
+ where: { id: excusalId },
390
+ data: {
391
+ ...(data.startDate !== undefined
392
+ ? { startDate: toDateOnly(data.startDate) }
393
+ : {}),
394
+ ...(data.endDate !== undefined ? { endDate: toDateOnly(data.endDate) } : {}),
395
+ ...(data.reason !== undefined ? { reason: data.reason } : {}),
396
+ ...(data.note !== undefined ? { note: data.note } : {}),
397
+ },
398
+ include: excusalInclude,
399
+ });
189
400
  });
190
401
  }
191
- /** Update attendance present/late/absent lists. */
192
- export function updateAttendance(id, data) {
193
- return prisma.attendance.update({
194
- where: { id },
402
+ export function revokeStudentExcusal(excusalId, revokedById) {
403
+ return prisma.studentExcusal.update({
404
+ where: { id: excusalId },
195
405
  data: {
196
- present: { set: data.presentIds.map((id) => ({ id })) },
197
- late: { set: data.lateIds.map((id) => ({ id })) },
198
- absent: { set: data.absentIds.map((id) => ({ id })) },
406
+ revokedAt: new Date(),
407
+ revokedById,
199
408
  },
200
- include: attendanceUpdateInclude,
409
+ include: excusalInclude,
201
410
  });
202
411
  }
412
+ /** Apply EXCUSED status for an excusal across matching class attendance sessions. */
413
+ export async function applyExcusalToAttendance(input) {
414
+ if (input.studentIds.length === 0) {
415
+ return { sessionsTouched: 0, recordsUpserted: 0 };
416
+ }
417
+ const start = startOfUtcDay(input.startDate);
418
+ const end = endOfUtcDay(input.endDate);
419
+ const events = await prisma.event.findMany({
420
+ where: {
421
+ class: {
422
+ schoolId: input.schoolId,
423
+ students: { some: { id: { in: input.studentIds } } },
424
+ },
425
+ startTime: { gte: start, lte: end },
426
+ },
427
+ select: {
428
+ id: true,
429
+ classId: true,
430
+ startTime: true,
431
+ class: {
432
+ select: {
433
+ id: true,
434
+ students: {
435
+ where: { id: { in: input.studentIds } },
436
+ select: { id: true },
437
+ },
438
+ },
439
+ },
440
+ },
441
+ });
442
+ let sessionsTouched = 0;
443
+ let recordsUpserted = 0;
444
+ for (const event of events) {
445
+ if (!event.classId || !event.class)
446
+ continue;
447
+ const classStudentIds = event.class.students.map((s) => s.id);
448
+ if (classStudentIds.length === 0)
449
+ continue;
450
+ let attendance = await prisma.attendance.findFirst({
451
+ where: { classId: event.classId, eventId: event.id },
452
+ include: {
453
+ present: { select: { id: true } },
454
+ late: { select: { id: true } },
455
+ absent: { select: { id: true } },
456
+ records: {
457
+ select: {
458
+ studentId: true,
459
+ status: true,
460
+ excusedByExcusalId: true,
461
+ note: true,
462
+ },
463
+ },
464
+ },
465
+ });
466
+ if (!attendance) {
467
+ // Default everyone present, then override excused students.
468
+ const allStudentIds = await prisma.class
469
+ .findUnique({
470
+ where: { id: event.classId },
471
+ select: { students: { select: { id: true } } },
472
+ })
473
+ .then((c) => c?.students.map((s) => s.id) ?? []);
474
+ const excusedSet = new Set(classStudentIds);
475
+ const presentIds = allStudentIds.filter((id) => !excusedSet.has(id));
476
+ attendance = await createAttendance({
477
+ classId: event.classId,
478
+ eventId: event.id,
479
+ date: event.startTime,
480
+ presentIds,
481
+ lateIds: [],
482
+ absentIds: [],
483
+ records: [
484
+ ...presentIds.map((studentId) => ({
485
+ studentId,
486
+ status: "PRESENT",
487
+ updatedById: input.updatedById,
488
+ })),
489
+ ...classStudentIds.map((studentId) => ({
490
+ studentId,
491
+ status: "EXCUSED",
492
+ note: null,
493
+ excusedByExcusalId: input.excusalId,
494
+ updatedById: input.updatedById,
495
+ })),
496
+ ],
497
+ });
498
+ sessionsTouched += 1;
499
+ recordsUpserted += classStudentIds.length;
500
+ continue;
501
+ }
502
+ const presentIds = attendance.present
503
+ .map((s) => s.id)
504
+ .filter((id) => !classStudentIds.includes(id));
505
+ const lateIds = attendance.late
506
+ .map((s) => s.id)
507
+ .filter((id) => !classStudentIds.includes(id));
508
+ const absentIds = attendance.absent
509
+ .map((s) => s.id)
510
+ .filter((id) => !classStudentIds.includes(id));
511
+ const recordByStudent = new Map(attendance.records.map((r) => [r.studentId, r]));
512
+ const nextRecords = [];
513
+ for (const [studentId, existing] of recordByStudent) {
514
+ if (classStudentIds.includes(studentId)) {
515
+ nextRecords.push({
516
+ studentId,
517
+ status: "EXCUSED",
518
+ note: existing.note,
519
+ excusedByExcusalId: input.excusalId,
520
+ updatedById: input.updatedById,
521
+ });
522
+ }
523
+ else {
524
+ nextRecords.push({
525
+ studentId,
526
+ status: existing.status,
527
+ note: existing.note,
528
+ excusedByExcusalId: existing.excusedByExcusalId,
529
+ });
530
+ }
531
+ }
532
+ for (const studentId of classStudentIds) {
533
+ if (!recordByStudent.has(studentId)) {
534
+ nextRecords.push({
535
+ studentId,
536
+ status: "EXCUSED",
537
+ excusedByExcusalId: input.excusalId,
538
+ updatedById: input.updatedById,
539
+ });
540
+ }
541
+ }
542
+ await updateAttendance(attendance.id, {
543
+ presentIds,
544
+ lateIds,
545
+ absentIds,
546
+ records: nextRecords,
547
+ });
548
+ sessionsTouched += 1;
549
+ recordsUpserted += classStudentIds.length;
550
+ }
551
+ return { sessionsTouched, recordsUpserted };
552
+ }
553
+ /** Clear EXCUSED locks tied to a revoked/updated excusal (leave status EXCUSED unless cleared). */
554
+ export async function clearExcusalLocks(excusalId) {
555
+ return prisma.attendanceRecord.updateMany({
556
+ where: { excusedByExcusalId: excusalId },
557
+ data: { excusedByExcusalId: null },
558
+ });
559
+ }
560
+ export function listSchoolStudentMemberships(schoolId) {
561
+ return prisma.schoolMembership.findMany({
562
+ where: {
563
+ schoolId,
564
+ role: "STUDENT",
565
+ status: "ACTIVE",
566
+ },
567
+ select: {
568
+ userId: true,
569
+ user: {
570
+ select: {
571
+ id: true,
572
+ username: true,
573
+ profile: {
574
+ select: {
575
+ displayName: true,
576
+ profilePicture: true,
577
+ profilePictureThumbnail: true,
578
+ },
579
+ },
580
+ studentIn: {
581
+ where: { schoolId },
582
+ select: { id: true, name: true, section: true, subject: true },
583
+ },
584
+ },
585
+ },
586
+ },
587
+ orderBy: { createdAt: "asc" },
588
+ });
589
+ }
590
+ export function listAttendanceRecordsForSchoolStudents(input) {
591
+ if (input.studentIds.length === 0) {
592
+ return Promise.resolve([]);
593
+ }
594
+ return prisma.attendanceRecord.findMany({
595
+ where: {
596
+ studentId: { in: input.studentIds },
597
+ attendance: {
598
+ class: { schoolId: input.schoolId },
599
+ ...(input.from || input.to
600
+ ? {
601
+ date: {
602
+ ...(input.from ? { gte: input.from } : {}),
603
+ ...(input.to ? { lte: input.to } : {}),
604
+ },
605
+ }
606
+ : {}),
607
+ },
608
+ },
609
+ select: {
610
+ studentId: true,
611
+ status: true,
612
+ attendance: {
613
+ select: {
614
+ id: true,
615
+ date: true,
616
+ classId: true,
617
+ eventId: true,
618
+ class: { select: { id: true, name: true } },
619
+ event: { select: { id: true, name: true, startTime: true } },
620
+ },
621
+ },
622
+ },
623
+ });
624
+ }
625
+ export function listTodaySchoolAttendanceSessions(schoolId, day) {
626
+ const start = startOfUtcDay(day);
627
+ const end = endOfUtcDay(day);
628
+ return prisma.attendance.findMany({
629
+ where: {
630
+ class: { schoolId },
631
+ OR: [
632
+ { date: { gte: start, lte: end } },
633
+ { event: { startTime: { gte: start, lte: end } } },
634
+ ],
635
+ },
636
+ select: {
637
+ id: true,
638
+ date: true,
639
+ classId: true,
640
+ eventId: true,
641
+ class: {
642
+ select: {
643
+ id: true,
644
+ name: true,
645
+ section: true,
646
+ subject: true,
647
+ _count: { select: { students: true } },
648
+ },
649
+ },
650
+ event: {
651
+ select: { id: true, name: true, startTime: true, endTime: true },
652
+ },
653
+ records: {
654
+ select: {
655
+ studentId: true,
656
+ status: true,
657
+ },
658
+ },
659
+ },
660
+ orderBy: { date: "asc" },
661
+ });
662
+ }
663
+ export function toDateOnly(date) {
664
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
665
+ }
666
+ export function startOfUtcDay(date) {
667
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0));
668
+ }
669
+ export function endOfUtcDay(date) {
670
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 23, 59, 59, 999));
671
+ }
203
672
  //# sourceMappingURL=attendance.js.map
204
- //# debugId=a57d4718-2b2b-536c-90dc-37d792317353
673
+ //# debugId=04968f1d-49ec-52b6-ab2b-a86286e300ce