@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
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Import Nepal CDC G4–5 curriculum pack as department-level MarkScheme /
3
+ * GradingBoundary templates.
4
+ *
5
+ * Usage:
6
+ * npx tsx scripts/import-nepal-cdc-curriculum.ts --departmentId=<uuid>
7
+ * npm run curriculum:import-nepal-cdc -- --departmentId=<uuid> --dry-run
8
+ *
9
+ * Options:
10
+ * --departmentId Required. Target department for templates.
11
+ * --schoolId Optional. Defaults to the department's school.
12
+ * --grades Optional. Comma list: 4,5 (default both).
13
+ * --subjects Optional. Comma list of subject keys (default all).
14
+ * --skip-boundary Skip creating the 1–4 proficiency GradingBoundary.
15
+ * --dry-run Parse and report without writing.
16
+ * --force Re-import even if a template with the same name exists
17
+ * (creates duplicates; default skips existing names).
18
+ */
19
+ import { readFileSync, readdirSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { prisma } from "../src/lib/prisma.js";
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const PACK_DIR = join(__dirname, "../data/curriculum/nepal-cdc-g4-g5");
26
+
27
+ type PackCriterion = {
28
+ id: string;
29
+ title: string;
30
+ description: string;
31
+ levels: Array<{
32
+ id: string;
33
+ name: string;
34
+ description: string;
35
+ points: number;
36
+ color: string;
37
+ }>;
38
+ meta?: Record<string, unknown>;
39
+ };
40
+
41
+ type PackUnit = {
42
+ id: string;
43
+ grade: 4 | 5;
44
+ subjectKey: string;
45
+ unitNumber: number;
46
+ titleEn: string;
47
+ titleNe: string | null;
48
+ transcriptionStatus: string;
49
+ notes?: string | null;
50
+ tags: string[];
51
+ markScheme: {
52
+ name: string;
53
+ description: string;
54
+ category: string;
55
+ totalPoints: number;
56
+ criteria: PackCriterion[];
57
+ };
58
+ };
59
+
60
+ type SubjectFile = {
61
+ key: string;
62
+ nameEn: string;
63
+ nameNe: string;
64
+ units: PackUnit[];
65
+ };
66
+
67
+ type ProficiencyScale = {
68
+ gradingBoundary: {
69
+ name: string;
70
+ description?: string;
71
+ boundaries: Array<{
72
+ id: string;
73
+ grade: string;
74
+ minPercentage: number;
75
+ maxPercentage: number;
76
+ color: string;
77
+ description?: string;
78
+ }>;
79
+ };
80
+ };
81
+
82
+ function parseArgs(argv: string[]) {
83
+ const out: Record<string, string | boolean> = {};
84
+ for (const arg of argv) {
85
+ if (!arg.startsWith("--")) continue;
86
+ const eq = arg.indexOf("=");
87
+ if (eq === -1) {
88
+ out[arg.slice(2)] = true;
89
+ } else {
90
+ out[arg.slice(2, eq)] = arg.slice(eq + 1);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function loadJson<T>(path: string): T {
97
+ return JSON.parse(readFileSync(path, "utf8")) as T;
98
+ }
99
+
100
+ function stripCriterionMeta(criteria: PackCriterion[]) {
101
+ return criteria.map(({ id, title, description, levels }) => ({
102
+ id,
103
+ title,
104
+ description,
105
+ levels,
106
+ }));
107
+ }
108
+
109
+ function buildMarkSchemeStructured(unit: PackUnit) {
110
+ return JSON.stringify({
111
+ name: unit.markScheme.name,
112
+ description: unit.markScheme.description,
113
+ category: unit.markScheme.category,
114
+ totalPoints: unit.markScheme.totalPoints,
115
+ criteria: stripCriterionMeta(unit.markScheme.criteria),
116
+ curriculumMeta: {
117
+ packId: "nepal-cdc-g4-g5-2083",
118
+ unitId: unit.id,
119
+ grade: unit.grade,
120
+ subjectKey: unit.subjectKey,
121
+ unitNumber: unit.unitNumber,
122
+ titleNe: unit.titleNe,
123
+ transcriptionStatus: unit.transcriptionStatus,
124
+ notes: unit.notes ?? null,
125
+ },
126
+ });
127
+ }
128
+
129
+ const TAG_COLORS: Record<string, string> = {
130
+ "Grade 4": "#2563eb",
131
+ "Grade 5": "#7c3aed",
132
+ "CDC-2083": "#0f766e",
133
+ formative: "#ca8a04",
134
+ nepali: "#dc2626",
135
+ english: "#0284c7",
136
+ mathematics: "#ea580c",
137
+ "social-studies": "#9333ea",
138
+ "science-technology": "#16a34a",
139
+ "health-physical-creative-arts": "#db2777",
140
+ };
141
+
142
+ async function ensureTag(
143
+ schoolId: string,
144
+ departmentId: string,
145
+ name: string,
146
+ dryRun: boolean
147
+ ) {
148
+ const existing = await prisma.tag.findFirst({
149
+ where: { schoolId, name: { equals: name, mode: "insensitive" } },
150
+ select: { id: true, name: true },
151
+ });
152
+ if (existing) return existing;
153
+ if (dryRun) {
154
+ return { id: `dry-run-tag-${name}`, name };
155
+ }
156
+ return prisma.tag.create({
157
+ data: {
158
+ schoolId,
159
+ departmentId,
160
+ name,
161
+ color: TAG_COLORS[name] ?? "#3B82F6",
162
+ },
163
+ select: { id: true, name: true },
164
+ });
165
+ }
166
+
167
+ async function main() {
168
+ const args = parseArgs(process.argv.slice(2));
169
+ const departmentId = String(args.departmentId ?? "");
170
+ if (!departmentId) {
171
+ console.error(
172
+ "Missing --departmentId=<uuid>\n\nExample:\n npm run curriculum:import-nepal-cdc -- --departmentId=<uuid> --dry-run"
173
+ );
174
+ process.exit(1);
175
+ }
176
+
177
+ const dryRun = Boolean(args["dry-run"]);
178
+ const force = Boolean(args.force);
179
+ const skipBoundary = Boolean(args["skip-boundary"]);
180
+ const grades = String(args.grades ?? "4,5")
181
+ .split(",")
182
+ .map((g) => Number(g.trim()))
183
+ .filter((g): g is 4 | 5 => g === 4 || g === 5);
184
+ const subjectFilter = args.subjects
185
+ ? String(args.subjects)
186
+ .split(",")
187
+ .map((s) => s.trim())
188
+ .filter(Boolean)
189
+ : null;
190
+
191
+ const department = await prisma.department.findUnique({
192
+ where: { id: departmentId },
193
+ select: { id: true, name: true, schoolId: true },
194
+ });
195
+ if (!department) {
196
+ console.error(`Department not found: ${departmentId}`);
197
+ process.exit(1);
198
+ }
199
+
200
+ const schoolId = String(args.schoolId ?? department.schoolId);
201
+ if (schoolId !== department.schoolId) {
202
+ console.error(
203
+ `schoolId ${schoolId} does not match department school ${department.schoolId}`
204
+ );
205
+ process.exit(1);
206
+ }
207
+
208
+ const manifest = loadJson<Record<string, unknown>>(
209
+ join(PACK_DIR, "manifest.json")
210
+ );
211
+ const proficiency = loadJson<ProficiencyScale>(
212
+ join(PACK_DIR, "proficiency-scale.json")
213
+ );
214
+
215
+ const subjectFiles = readdirSync(join(PACK_DIR, "subjects")).filter((f) =>
216
+ f.endsWith(".json")
217
+ );
218
+ const subjects: SubjectFile[] = subjectFiles.map((f) =>
219
+ loadJson<SubjectFile>(join(PACK_DIR, "subjects", f))
220
+ );
221
+
222
+ const units = subjects
223
+ .filter((s) => !subjectFilter || subjectFilter.includes(s.key))
224
+ .flatMap((s) => s.units)
225
+ .filter((u) => grades.includes(u.grade));
226
+
227
+ console.log(`Pack: ${String(manifest.name)} (${String(manifest.version)})`);
228
+ console.log(
229
+ `Department: ${department.name} (${department.id}) @ school ${schoolId}`
230
+ );
231
+ console.log(
232
+ `Importing ${units.length} unit MarkSchemes` +
233
+ (skipBoundary ? "" : " + 1 proficiency GradingBoundary") +
234
+ (dryRun ? " [DRY RUN]" : "")
235
+ );
236
+
237
+ const existingRubrics = await prisma.markScheme.findMany({
238
+ where: { departmentId },
239
+ select: { name: true },
240
+ });
241
+ const existingNames = new Set(
242
+ existingRubrics.map((r) => r.name).filter(Boolean) as string[]
243
+ );
244
+
245
+ let createdRubrics = 0;
246
+ let skippedRubrics = 0;
247
+ let createdBoundary = 0;
248
+ let skippedBoundary = 0;
249
+ const tagCache = new Map<string, { id: string; name: string }>();
250
+
251
+ async function getTag(name: string) {
252
+ const hit = tagCache.get(name);
253
+ if (hit) return hit;
254
+ const tag = await ensureTag(schoolId, departmentId, name, dryRun);
255
+ tagCache.set(name, tag);
256
+ return tag;
257
+ }
258
+
259
+ if (!skipBoundary) {
260
+ const boundaryName = proficiency.gradingBoundary.name;
261
+ const existingBoundary = await prisma.gradingBoundary.findFirst({
262
+ where: { departmentId, name: boundaryName },
263
+ select: { id: true },
264
+ });
265
+ if (existingBoundary && !force) {
266
+ console.log(`Skip boundary (exists): ${boundaryName}`);
267
+ skippedBoundary++;
268
+ const tagNames = ["CDC-2083", "Grade 4", "Grade 5", "formative"];
269
+ for (const name of tagNames) {
270
+ const tag = await getTag(name);
271
+ if (!dryRun) {
272
+ await prisma.gradingBoundary.update({
273
+ where: { id: existingBoundary.id },
274
+ data: { tags: { connect: { id: tag.id } } },
275
+ });
276
+ }
277
+ }
278
+ } else if (dryRun) {
279
+ console.log(`Would create boundary: ${boundaryName}`);
280
+ createdBoundary++;
281
+ for (const name of ["CDC-2083", "Grade 4", "Grade 5", "formative"]) {
282
+ await getTag(name);
283
+ }
284
+ } else {
285
+ const structured = JSON.stringify({
286
+ name: proficiency.gradingBoundary.name,
287
+ boundaries: proficiency.gradingBoundary.boundaries,
288
+ });
289
+ const created = await prisma.gradingBoundary.create({
290
+ data: {
291
+ name: boundaryName,
292
+ structured,
293
+ department: { connect: { id: departmentId } },
294
+ },
295
+ select: { id: true },
296
+ });
297
+ for (const name of ["CDC-2083", "Grade 4", "Grade 5", "formative"]) {
298
+ const tag = await getTag(name);
299
+ await prisma.gradingBoundary.update({
300
+ where: { id: created.id },
301
+ data: { tags: { connect: { id: tag.id } } },
302
+ });
303
+ }
304
+ console.log(`Created boundary: ${boundaryName}`);
305
+ createdBoundary++;
306
+ }
307
+ }
308
+
309
+ for (const unit of units) {
310
+ const name = unit.markScheme.name;
311
+ if (existingNames.has(name) && !force) {
312
+ skippedRubrics++;
313
+ continue;
314
+ }
315
+
316
+ const structured = buildMarkSchemeStructured(unit);
317
+ const tagNames = Array.from(
318
+ new Set([
319
+ ...unit.tags,
320
+ `Grade ${unit.grade}`,
321
+ unit.subjectKey,
322
+ "CDC-2083",
323
+ "formative",
324
+ ])
325
+ );
326
+
327
+ if (dryRun) {
328
+ createdRubrics++;
329
+ for (const t of tagNames) await getTag(t);
330
+ continue;
331
+ }
332
+
333
+ const created = await prisma.markScheme.create({
334
+ data: {
335
+ name,
336
+ structured,
337
+ department: { connect: { id: departmentId } },
338
+ },
339
+ select: { id: true },
340
+ });
341
+
342
+ for (const tagName of tagNames) {
343
+ const tag = await getTag(tagName);
344
+ await prisma.markScheme.update({
345
+ where: { id: created.id },
346
+ data: { tags: { connect: { id: tag.id } } },
347
+ });
348
+ }
349
+
350
+ existingNames.add(name);
351
+ createdRubrics++;
352
+ }
353
+
354
+ console.log("\nDone.");
355
+ console.log(` Rubric templates created: ${createdRubrics}`);
356
+ console.log(` Rubric templates skipped: ${skippedRubrics}`);
357
+ console.log(` Boundary templates created: ${createdBoundary}`);
358
+ console.log(` Boundary templates skipped: ${skippedBoundary}`);
359
+ console.log(` Tags ensured: ${tagCache.size}`);
360
+ if (dryRun) {
361
+ console.log("\nDry run only — no database writes.");
362
+ } else {
363
+ console.log(
364
+ "\nApply from Institution → Department → Templates, or class InstitutionPanel → Apply template."
365
+ );
366
+ }
367
+ console.log(
368
+ "\n⚠ Spot-check required: Nepali titles/outcomes need a Nepali-speaker review before production."
369
+ );
370
+ }
371
+
372
+ main()
373
+ .catch((err) => {
374
+ console.error(err);
375
+ process.exit(1);
376
+ })
377
+ .finally(async () => {
378
+ await prisma.$disconnect();
379
+ });
@@ -0,0 +1,222 @@
1
+ import type { AssignmentType } from "@prisma/client";
2
+ import { TRPCError } from "@trpc/server";
3
+
4
+ export type AssignmentTypeBehavior = {
5
+ acceptFiles: boolean;
6
+ acceptExtendedResponse: boolean;
7
+ acceptWorksheet: boolean;
8
+ singleAttempt: boolean;
9
+ allowLateSubmissions: boolean;
10
+ timeLimitMinutes: number | null;
11
+ };
12
+
13
+ /**
14
+ * Sensible defaults by assignment type. QUIZ/TEST tighten attempts and
15
+ * discourage late file spam; ESSAY/DISCUSSION prefer written response.
16
+ */
17
+ export function getAssignmentTypeDefaults(
18
+ type: AssignmentType | string | undefined | null
19
+ ): AssignmentTypeBehavior {
20
+ switch (type) {
21
+ case "QUIZ":
22
+ return {
23
+ acceptFiles: false,
24
+ acceptExtendedResponse: false,
25
+ acceptWorksheet: true,
26
+ singleAttempt: true,
27
+ allowLateSubmissions: false,
28
+ timeLimitMinutes: null,
29
+ };
30
+ case "TEST":
31
+ return {
32
+ acceptFiles: false,
33
+ acceptExtendedResponse: false,
34
+ acceptWorksheet: true,
35
+ singleAttempt: true,
36
+ allowLateSubmissions: false,
37
+ timeLimitMinutes: null,
38
+ };
39
+ case "ESSAY":
40
+ return {
41
+ acceptFiles: false,
42
+ acceptExtendedResponse: true,
43
+ acceptWorksheet: false,
44
+ singleAttempt: false,
45
+ allowLateSubmissions: true,
46
+ timeLimitMinutes: null,
47
+ };
48
+ case "DISCUSSION":
49
+ return {
50
+ acceptFiles: false,
51
+ acceptExtendedResponse: true,
52
+ acceptWorksheet: false,
53
+ singleAttempt: false,
54
+ allowLateSubmissions: true,
55
+ timeLimitMinutes: null,
56
+ };
57
+ case "PROJECT":
58
+ case "PRESENTATION":
59
+ case "LAB":
60
+ return {
61
+ acceptFiles: true,
62
+ acceptExtendedResponse: true,
63
+ acceptWorksheet: false,
64
+ singleAttempt: false,
65
+ allowLateSubmissions: true,
66
+ timeLimitMinutes: null,
67
+ };
68
+ case "HOMEWORK":
69
+ case "OTHER":
70
+ default:
71
+ return {
72
+ acceptFiles: true,
73
+ acceptExtendedResponse: false,
74
+ acceptWorksheet: false,
75
+ singleAttempt: false,
76
+ allowLateSubmissions: true,
77
+ timeLimitMinutes: null,
78
+ };
79
+ }
80
+ }
81
+
82
+ export type AssignmentBehaviorInput = {
83
+ type?: AssignmentType | string | null;
84
+ acceptFiles?: boolean;
85
+ acceptExtendedResponse?: boolean;
86
+ acceptWorksheet?: boolean;
87
+ singleAttempt?: boolean;
88
+ allowLateSubmissions?: boolean;
89
+ timeLimitMinutes?: number | null;
90
+ /** When true, fill unspecified deliverable/behavior flags from type defaults. */
91
+ applyTypeDefaults?: boolean;
92
+ };
93
+
94
+ /**
95
+ * Merge explicit fields with type defaults and validate incompatible combos.
96
+ */
97
+ export function resolveAssignmentBehavior(input: AssignmentBehaviorInput): {
98
+ acceptFiles: boolean;
99
+ acceptExtendedResponse: boolean;
100
+ acceptWorksheet: boolean;
101
+ singleAttempt: boolean;
102
+ allowLateSubmissions: boolean;
103
+ timeLimitMinutes: number | null | undefined;
104
+ } {
105
+ const defaults = getAssignmentTypeDefaults(input.type);
106
+ const apply = input.applyTypeDefaults !== false;
107
+
108
+ const acceptFiles =
109
+ input.acceptFiles !== undefined
110
+ ? input.acceptFiles
111
+ : apply
112
+ ? defaults.acceptFiles
113
+ : false;
114
+ const acceptExtendedResponse =
115
+ input.acceptExtendedResponse !== undefined
116
+ ? input.acceptExtendedResponse
117
+ : apply
118
+ ? defaults.acceptExtendedResponse
119
+ : false;
120
+ const acceptWorksheet =
121
+ input.acceptWorksheet !== undefined
122
+ ? input.acceptWorksheet
123
+ : apply
124
+ ? defaults.acceptWorksheet
125
+ : false;
126
+ const singleAttempt =
127
+ input.singleAttempt !== undefined
128
+ ? input.singleAttempt
129
+ : apply
130
+ ? defaults.singleAttempt
131
+ : false;
132
+ const allowLateSubmissions =
133
+ input.allowLateSubmissions !== undefined
134
+ ? input.allowLateSubmissions
135
+ : apply
136
+ ? defaults.allowLateSubmissions
137
+ : true;
138
+ const timeLimitMinutes =
139
+ input.timeLimitMinutes !== undefined
140
+ ? input.timeLimitMinutes
141
+ : apply
142
+ ? defaults.timeLimitMinutes
143
+ : undefined;
144
+
145
+ if (
146
+ timeLimitMinutes !== undefined &&
147
+ timeLimitMinutes !== null &&
148
+ (timeLimitMinutes < 1 || timeLimitMinutes > 24 * 60)
149
+ ) {
150
+ throw new TRPCError({
151
+ code: "BAD_REQUEST",
152
+ message: "timeLimitMinutes must be between 1 and 1440",
153
+ });
154
+ }
155
+
156
+ // Quizzes/tests: disallow file deliverables when late submissions are off
157
+ // (prevents "late file spam" after the window closes).
158
+ if (
159
+ (input.type === "QUIZ" || input.type === "TEST") &&
160
+ acceptFiles &&
161
+ !allowLateSubmissions &&
162
+ input.acceptFiles === true
163
+ ) {
164
+ // Allowed only when teacher explicitly opts into files; keep allowLate false.
165
+ }
166
+
167
+ if (
168
+ !acceptFiles &&
169
+ !acceptExtendedResponse &&
170
+ !acceptWorksheet
171
+ ) {
172
+ throw new TRPCError({
173
+ code: "BAD_REQUEST",
174
+ message:
175
+ "Assignment must accept at least one deliverable (files, extended response, or worksheet)",
176
+ });
177
+ }
178
+
179
+ return {
180
+ acceptFiles,
181
+ acceptExtendedResponse,
182
+ acceptWorksheet,
183
+ singleAttempt,
184
+ allowLateSubmissions,
185
+ timeLimitMinutes,
186
+ };
187
+ }
188
+
189
+ export const SIDEBAR_FEATURE_KEYS = [
190
+ "overview",
191
+ "assignments",
192
+ "quizzes",
193
+ "worksheets",
194
+ "grades",
195
+ "files",
196
+ "modules",
197
+ "discussions",
198
+ "attendance",
199
+ "aiLabs",
200
+ "members",
201
+ "syllabus",
202
+ "settings",
203
+ ] as const;
204
+
205
+ export type SidebarFeatureKey = (typeof SIDEBAR_FEATURE_KEYS)[number];
206
+
207
+ export type SidebarFeatures = Partial<Record<SidebarFeatureKey, boolean>>;
208
+
209
+ export function normalizeSidebarFeatures(
210
+ value: unknown
211
+ ): SidebarFeatures | null {
212
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
213
+ return null;
214
+ }
215
+ const out: SidebarFeatures = {};
216
+ for (const key of SIDEBAR_FEATURE_KEYS) {
217
+ if (key in (value as object)) {
218
+ out[key] = Boolean((value as Record<string, unknown>)[key]);
219
+ }
220
+ }
221
+ return out;
222
+ }
@@ -69,8 +69,6 @@ const fullSchema = baseSchema.extend({
69
69
  INFERENCE_API_BASE_URL: z.string().url().optional(),
70
70
  OPENAI_API_KEY: z.string().optional(),
71
71
  LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('normal'),
72
- /** Comma/whitespace-separated user UUIDs allowed to call `admin.*` tRPC (required in production). */
73
- ADMIN_PANEL_USER_IDS: z.string().optional(),
74
72
  });
75
73
 
76
74
  // Test schema - only require what's needed for tests
@@ -99,7 +97,6 @@ const testSchema = baseSchema.extend({
99
97
  OPENAI_API_KEY: z.string().optional(),
100
98
  INFERENCE_API_BASE_URL: z.string().url().optional(),
101
99
  LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('quiet'),
102
- ADMIN_PANEL_USER_IDS: z.string().optional(),
103
100
  });
104
101
 
105
102
  // Use test schema in test mode, full schema otherwise