@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
package/src/routers/school.ts
CHANGED
|
@@ -33,13 +33,42 @@ import {
|
|
|
33
33
|
updateSchoolDetails,
|
|
34
34
|
} from "../services/school.js";
|
|
35
35
|
import { getSchoolAiUsage } from "../services/aiUsage.js";
|
|
36
|
+
import {
|
|
37
|
+
listTodayAttendanceSummary,
|
|
38
|
+
listStudentAttendanceStats,
|
|
39
|
+
listExcusals,
|
|
40
|
+
createExcusal,
|
|
41
|
+
updateExcusal,
|
|
42
|
+
revokeExcusal,
|
|
43
|
+
} from "../services/attendance.js";
|
|
44
|
+
import { generateStudentIdCards } from "../services/idCard.js";
|
|
45
|
+
import {
|
|
46
|
+
generateTranscript,
|
|
47
|
+
generateTranscripts,
|
|
48
|
+
} from "../services/transcript.js";
|
|
49
|
+
import { importStudentsCsv } from "../services/studentImport.js";
|
|
36
50
|
|
|
37
|
-
const schoolRoleSchema = z.enum([
|
|
51
|
+
const schoolRoleSchema = z.enum([
|
|
52
|
+
"ADMIN",
|
|
53
|
+
"TEACHER",
|
|
54
|
+
"STUDENT",
|
|
55
|
+
"ACCOUNTANT",
|
|
56
|
+
"PARENT",
|
|
57
|
+
]);
|
|
38
58
|
|
|
39
59
|
const schoolIdSchema = z.object({
|
|
40
60
|
schoolId: z.string(),
|
|
41
61
|
});
|
|
42
62
|
|
|
63
|
+
/** Interim period params until AcademicTerm (C1) exists. */
|
|
64
|
+
const transcriptPeriodSchema = z.object({
|
|
65
|
+
schoolId: z.string(),
|
|
66
|
+
from: z.date().optional(),
|
|
67
|
+
to: z.date().optional(),
|
|
68
|
+
/** Free-text school year label (e.g. "2082 BS"). Prefer AcademicTerm (C1) later. */
|
|
69
|
+
schoolYear: z.string().max(64).optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
43
72
|
export const schoolRouter = createTRPCRouter({
|
|
44
73
|
getCurrent: protectedProcedure
|
|
45
74
|
.input(z.object({ schoolId: z.string().optional() }).optional())
|
|
@@ -66,6 +95,7 @@ export const schoolRouter = createTRPCRouter({
|
|
|
66
95
|
description: z.string().nullable().optional(),
|
|
67
96
|
subdomain: z.string().nullable().optional(),
|
|
68
97
|
logoId: z.string().nullable().optional(),
|
|
98
|
+
letterheadFileId: z.string().nullable().optional(),
|
|
69
99
|
})
|
|
70
100
|
)
|
|
71
101
|
.mutation(({ ctx, input }) => updateSchoolDetails(ctx.user!.id, input)),
|
|
@@ -148,6 +178,66 @@ export const schoolRouter = createTRPCRouter({
|
|
|
148
178
|
.input(schoolIdSchema)
|
|
149
179
|
.query(({ ctx, input }) => getAdminOverview(ctx.user!.id, input.schoolId)),
|
|
150
180
|
|
|
181
|
+
/** School-admin: today's attendance rollup across classes. */
|
|
182
|
+
listTodayAttendanceSummary: protectedProcedure
|
|
183
|
+
.input(schoolIdSchema)
|
|
184
|
+
.query(({ ctx, input }) =>
|
|
185
|
+
listTodayAttendanceSummary(ctx.user!.id, input.schoolId)
|
|
186
|
+
),
|
|
187
|
+
|
|
188
|
+
/** School-admin: per-student attendance counts + % (excused excluded from %). */
|
|
189
|
+
listStudentAttendanceStats: protectedProcedure
|
|
190
|
+
.input(
|
|
191
|
+
z.object({
|
|
192
|
+
schoolId: z.string(),
|
|
193
|
+
from: z.date().optional(),
|
|
194
|
+
to: z.date().optional(),
|
|
195
|
+
studentId: z.string().optional(),
|
|
196
|
+
})
|
|
197
|
+
)
|
|
198
|
+
.query(({ ctx, input }) => listStudentAttendanceStats(ctx.user!.id, input)),
|
|
199
|
+
|
|
200
|
+
listExcusals: protectedProcedure
|
|
201
|
+
.input(
|
|
202
|
+
z.object({
|
|
203
|
+
schoolId: z.string(),
|
|
204
|
+
includeRevoked: z.boolean().optional(),
|
|
205
|
+
studentId: z.string().optional(),
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
.query(({ ctx, input }) => listExcusals(ctx.user!.id, input)),
|
|
209
|
+
|
|
210
|
+
createExcusal: protectedProcedure
|
|
211
|
+
.input(
|
|
212
|
+
z.object({
|
|
213
|
+
schoolId: z.string(),
|
|
214
|
+
studentIds: z.array(z.string()).min(1),
|
|
215
|
+
startDate: z.date(),
|
|
216
|
+
endDate: z.date(),
|
|
217
|
+
reason: z.string().min(1),
|
|
218
|
+
note: z.string().nullable().optional(),
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
.mutation(({ ctx, input }) => createExcusal(ctx.user!.id, input)),
|
|
222
|
+
|
|
223
|
+
updateExcusal: protectedProcedure
|
|
224
|
+
.input(
|
|
225
|
+
z.object({
|
|
226
|
+
schoolId: z.string(),
|
|
227
|
+
excusalId: z.string(),
|
|
228
|
+
studentIds: z.array(z.string()).min(1).optional(),
|
|
229
|
+
startDate: z.date().optional(),
|
|
230
|
+
endDate: z.date().optional(),
|
|
231
|
+
reason: z.string().min(1).optional(),
|
|
232
|
+
note: z.string().nullable().optional(),
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
.mutation(({ ctx, input }) => updateExcusal(ctx.user!.id, input)),
|
|
236
|
+
|
|
237
|
+
revokeExcusal: protectedProcedure
|
|
238
|
+
.input(z.object({ schoolId: z.string(), excusalId: z.string() }))
|
|
239
|
+
.mutation(({ ctx, input }) => revokeExcusal(ctx.user!.id, input)),
|
|
240
|
+
|
|
151
241
|
getAiUsage: protectedProcedure
|
|
152
242
|
.input(
|
|
153
243
|
z.object({
|
|
@@ -228,6 +318,31 @@ export const schoolRouter = createTRPCRouter({
|
|
|
228
318
|
.input(z.object({ schoolId: z.string(), memberUserId: z.string() }))
|
|
229
319
|
.query(({ ctx, input }) => getMemberDetail(ctx.user!.id, input)),
|
|
230
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Generate a branded transcript / progress-report PDF for one student.
|
|
323
|
+
* School admin only. Uses gradebook + attendance (present/late/absent).
|
|
324
|
+
* No AcademicTerm yet — pass schoolYear and/or from/to (C1 dependency).
|
|
325
|
+
*/
|
|
326
|
+
generateTranscript: protectedProcedure
|
|
327
|
+
.input(
|
|
328
|
+
transcriptPeriodSchema.extend({
|
|
329
|
+
studentId: z.string(),
|
|
330
|
+
})
|
|
331
|
+
)
|
|
332
|
+
.mutation(({ ctx, input }) => generateTranscript(ctx.user!.id, input)),
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Bulk generate transcripts for multiple students (max 50).
|
|
336
|
+
* Returns per-student PDFs plus any per-student errors.
|
|
337
|
+
*/
|
|
338
|
+
generateTranscripts: protectedProcedure
|
|
339
|
+
.input(
|
|
340
|
+
transcriptPeriodSchema.extend({
|
|
341
|
+
studentIds: z.array(z.string()).min(1).max(50),
|
|
342
|
+
})
|
|
343
|
+
)
|
|
344
|
+
.mutation(({ ctx, input }) => generateTranscripts(ctx.user!.id, input)),
|
|
345
|
+
|
|
231
346
|
bulkUpdateMemberRoles: protectedProcedure
|
|
232
347
|
.input(
|
|
233
348
|
z.object({
|
|
@@ -247,6 +362,18 @@ export const schoolRouter = createTRPCRouter({
|
|
|
247
362
|
)
|
|
248
363
|
.mutation(({ ctx, input }) => bulkRemoveMembers(ctx.user!.id, input)),
|
|
249
364
|
|
|
365
|
+
/** Generate a bulk PDF of front-side student ID cards for selected members. */
|
|
366
|
+
generateIdCards: protectedProcedure
|
|
367
|
+
.input(
|
|
368
|
+
z.object({
|
|
369
|
+
schoolId: z.string(),
|
|
370
|
+
studentUserIds: z.array(z.string()).min(1).max(100),
|
|
371
|
+
})
|
|
372
|
+
)
|
|
373
|
+
.mutation(({ ctx, input }) =>
|
|
374
|
+
generateStudentIdCards(ctx.user!.id, input)
|
|
375
|
+
),
|
|
376
|
+
|
|
250
377
|
listInvites: protectedProcedure
|
|
251
378
|
.input(schoolIdSchema)
|
|
252
379
|
.query(({ ctx, input }) => listInvites(ctx.user!.id, input.schoolId)),
|
|
@@ -277,4 +404,15 @@ export const schoolRouter = createTRPCRouter({
|
|
|
277
404
|
acceptInvite: protectedProcedure
|
|
278
405
|
.input(z.object({ token: z.string() }))
|
|
279
406
|
.mutation(({ ctx, input }) => acceptInvite(ctx.user!.id, input.token)),
|
|
407
|
+
|
|
408
|
+
/** C3: Bulk student CSV onboarding (username/password generated when omitted). */
|
|
409
|
+
importStudentsCsv: protectedProcedure
|
|
410
|
+
.input(
|
|
411
|
+
z.object({
|
|
412
|
+
schoolId: z.string(),
|
|
413
|
+
csvText: z.string().min(1).max(2_000_000),
|
|
414
|
+
dryRun: z.boolean().optional(),
|
|
415
|
+
})
|
|
416
|
+
)
|
|
417
|
+
.mutation(({ ctx, input }) => importStudentsCsv(ctx.user!.id, input)),
|
|
280
418
|
});
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
regradeAll,
|
|
34
34
|
gradeAnswer,
|
|
35
35
|
addCommentToResponse,
|
|
36
|
+
getWorksheetQuestionStats,
|
|
36
37
|
} from "../services/worksheet.js";
|
|
37
38
|
|
|
38
39
|
const directFileSchema = z.object({
|
|
@@ -235,6 +236,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
235
236
|
"ESSAY",
|
|
236
237
|
]),
|
|
237
238
|
order: z.number().int().min(0).optional(),
|
|
239
|
+
parentQuestionId: z.string().nullable().optional(),
|
|
238
240
|
})
|
|
239
241
|
)
|
|
240
242
|
.mutation(({ ctx, input }) =>
|
|
@@ -246,9 +248,16 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
246
248
|
markScheme: input.markScheme,
|
|
247
249
|
type: input.type,
|
|
248
250
|
order: input.order,
|
|
251
|
+
parentQuestionId: input.parentQuestionId,
|
|
249
252
|
})
|
|
250
253
|
),
|
|
251
254
|
|
|
255
|
+
getQuestionStats: protectedProcedure
|
|
256
|
+
.input(z.object({ worksheetId: z.string() }))
|
|
257
|
+
.query(({ ctx, input }) =>
|
|
258
|
+
getWorksheetQuestionStats(input.worksheetId, ctx.user!.id)
|
|
259
|
+
),
|
|
260
|
+
|
|
252
261
|
reorderQuestions: protectedProcedure
|
|
253
262
|
.input(
|
|
254
263
|
z.object({
|
|
@@ -288,6 +297,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
288
297
|
"ESSAY",
|
|
289
298
|
])
|
|
290
299
|
.optional(),
|
|
300
|
+
parentQuestionId: z.string().nullable().optional(),
|
|
291
301
|
})
|
|
292
302
|
)
|
|
293
303
|
.mutation(({ ctx, input }) =>
|
|
@@ -302,6 +312,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
302
312
|
options: input.options,
|
|
303
313
|
markScheme: input.markScheme,
|
|
304
314
|
type: input.type,
|
|
315
|
+
parentQuestionId: input.parentQuestionId,
|
|
305
316
|
}
|
|
306
317
|
)
|
|
307
318
|
),
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Academic year / term service (C1) + student promotion (C2).
|
|
3
|
+
*/
|
|
4
|
+
import { TRPCError } from "@trpc/server";
|
|
5
|
+
import { prisma } from "../lib/prisma.js";
|
|
6
|
+
import {
|
|
7
|
+
createAcademicTerm,
|
|
8
|
+
createAcademicYear,
|
|
9
|
+
deleteAcademicTerm,
|
|
10
|
+
deleteAcademicYear,
|
|
11
|
+
getAcademicYearById,
|
|
12
|
+
getCurrentAcademicYear,
|
|
13
|
+
listAcademicYears,
|
|
14
|
+
updateAcademicTerm,
|
|
15
|
+
updateAcademicYear,
|
|
16
|
+
} from "../models/academicCalendar.js";
|
|
17
|
+
import {
|
|
18
|
+
findActiveSchoolAdminMembership,
|
|
19
|
+
findSchoolClass,
|
|
20
|
+
listActiveSchoolMemberships,
|
|
21
|
+
} from "../models/school.js";
|
|
22
|
+
|
|
23
|
+
async function requireSchoolAdmin(schoolId: string, userId: string) {
|
|
24
|
+
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
25
|
+
if (!membership) {
|
|
26
|
+
throw new TRPCError({
|
|
27
|
+
code: "FORBIDDEN",
|
|
28
|
+
message: "Only school admins can perform this action",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return membership;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function listYears(userId: string, schoolId: string) {
|
|
35
|
+
await requireSchoolAdmin(schoolId, userId);
|
|
36
|
+
return { years: await listAcademicYears(schoolId) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getCurrentYear(userId: string, schoolId: string) {
|
|
40
|
+
await requireSchoolAdmin(schoolId, userId);
|
|
41
|
+
return { year: await getCurrentAcademicYear(schoolId) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function createYear(
|
|
45
|
+
userId: string,
|
|
46
|
+
input: {
|
|
47
|
+
schoolId: string;
|
|
48
|
+
name: string;
|
|
49
|
+
startDate: Date;
|
|
50
|
+
endDate: Date;
|
|
51
|
+
isCurrent?: boolean;
|
|
52
|
+
}
|
|
53
|
+
) {
|
|
54
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
55
|
+
if (input.endDate < input.startDate) {
|
|
56
|
+
throw new TRPCError({
|
|
57
|
+
code: "BAD_REQUEST",
|
|
58
|
+
message: "endDate must be on or after startDate",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const year = await createAcademicYear(input);
|
|
63
|
+
return { year };
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
if (
|
|
66
|
+
err &&
|
|
67
|
+
typeof err === "object" &&
|
|
68
|
+
"code" in err &&
|
|
69
|
+
(err as { code: string }).code === "P2002"
|
|
70
|
+
) {
|
|
71
|
+
throw new TRPCError({
|
|
72
|
+
code: "CONFLICT",
|
|
73
|
+
message: "An academic year with this name already exists",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function updateYear(
|
|
81
|
+
userId: string,
|
|
82
|
+
input: {
|
|
83
|
+
schoolId: string;
|
|
84
|
+
yearId: string;
|
|
85
|
+
name?: string;
|
|
86
|
+
startDate?: Date;
|
|
87
|
+
endDate?: Date;
|
|
88
|
+
isCurrent?: boolean;
|
|
89
|
+
}
|
|
90
|
+
) {
|
|
91
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
92
|
+
const year = await updateAcademicYear(input.yearId, input.schoolId, {
|
|
93
|
+
name: input.name,
|
|
94
|
+
startDate: input.startDate,
|
|
95
|
+
endDate: input.endDate,
|
|
96
|
+
isCurrent: input.isCurrent,
|
|
97
|
+
});
|
|
98
|
+
if (!year) {
|
|
99
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic year not found" });
|
|
100
|
+
}
|
|
101
|
+
return { year };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function removeYear(
|
|
105
|
+
userId: string,
|
|
106
|
+
input: { schoolId: string; yearId: string }
|
|
107
|
+
) {
|
|
108
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
109
|
+
const result = await deleteAcademicYear(input.yearId, input.schoolId);
|
|
110
|
+
if (result.count === 0) {
|
|
111
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic year not found" });
|
|
112
|
+
}
|
|
113
|
+
return { ok: true as const };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function createTerm(
|
|
117
|
+
userId: string,
|
|
118
|
+
input: {
|
|
119
|
+
schoolId: string;
|
|
120
|
+
yearId: string;
|
|
121
|
+
name: string;
|
|
122
|
+
startDate: Date;
|
|
123
|
+
endDate: Date;
|
|
124
|
+
isCurrent?: boolean;
|
|
125
|
+
}
|
|
126
|
+
) {
|
|
127
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
128
|
+
const year = await getAcademicYearById(input.yearId, input.schoolId);
|
|
129
|
+
if (!year) {
|
|
130
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic year not found" });
|
|
131
|
+
}
|
|
132
|
+
if (input.endDate < input.startDate) {
|
|
133
|
+
throw new TRPCError({
|
|
134
|
+
code: "BAD_REQUEST",
|
|
135
|
+
message: "endDate must be on or after startDate",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const term = await createAcademicTerm({
|
|
140
|
+
academicYearId: input.yearId,
|
|
141
|
+
name: input.name,
|
|
142
|
+
startDate: input.startDate,
|
|
143
|
+
endDate: input.endDate,
|
|
144
|
+
isCurrent: input.isCurrent,
|
|
145
|
+
});
|
|
146
|
+
return { term };
|
|
147
|
+
} catch (err: unknown) {
|
|
148
|
+
if (
|
|
149
|
+
err &&
|
|
150
|
+
typeof err === "object" &&
|
|
151
|
+
"code" in err &&
|
|
152
|
+
(err as { code: string }).code === "P2002"
|
|
153
|
+
) {
|
|
154
|
+
throw new TRPCError({
|
|
155
|
+
code: "CONFLICT",
|
|
156
|
+
message: "A term with this name already exists in the year",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function updateTerm(
|
|
164
|
+
userId: string,
|
|
165
|
+
input: {
|
|
166
|
+
schoolId: string;
|
|
167
|
+
yearId: string;
|
|
168
|
+
termId: string;
|
|
169
|
+
name?: string;
|
|
170
|
+
startDate?: Date;
|
|
171
|
+
endDate?: Date;
|
|
172
|
+
isCurrent?: boolean;
|
|
173
|
+
}
|
|
174
|
+
) {
|
|
175
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
176
|
+
const year = await getAcademicYearById(input.yearId, input.schoolId);
|
|
177
|
+
if (!year) {
|
|
178
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic year not found" });
|
|
179
|
+
}
|
|
180
|
+
const term = await updateAcademicTerm(input.termId, input.yearId, {
|
|
181
|
+
name: input.name,
|
|
182
|
+
startDate: input.startDate,
|
|
183
|
+
endDate: input.endDate,
|
|
184
|
+
isCurrent: input.isCurrent,
|
|
185
|
+
});
|
|
186
|
+
if (!term) {
|
|
187
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic term not found" });
|
|
188
|
+
}
|
|
189
|
+
return { term };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function removeTerm(
|
|
193
|
+
userId: string,
|
|
194
|
+
input: { schoolId: string; yearId: string; termId: string }
|
|
195
|
+
) {
|
|
196
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
197
|
+
const year = await getAcademicYearById(input.yearId, input.schoolId);
|
|
198
|
+
if (!year) {
|
|
199
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic year not found" });
|
|
200
|
+
}
|
|
201
|
+
const result = await deleteAcademicTerm(input.termId, input.yearId);
|
|
202
|
+
if (result.count === 0) {
|
|
203
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Academic term not found" });
|
|
204
|
+
}
|
|
205
|
+
return { ok: true as const };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* C2: Promote / move students from one class to another (basic rollover).
|
|
210
|
+
* Disconnects from `fromClassId` and connects to `toClassId`.
|
|
211
|
+
*/
|
|
212
|
+
export async function promoteStudents(
|
|
213
|
+
userId: string,
|
|
214
|
+
input: {
|
|
215
|
+
schoolId: string;
|
|
216
|
+
fromClassId: string;
|
|
217
|
+
toClassId: string;
|
|
218
|
+
studentUserIds: string[];
|
|
219
|
+
}
|
|
220
|
+
) {
|
|
221
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
222
|
+
|
|
223
|
+
if (input.fromClassId === input.toClassId) {
|
|
224
|
+
throw new TRPCError({
|
|
225
|
+
code: "BAD_REQUEST",
|
|
226
|
+
message: "fromClassId and toClassId must differ",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (input.studentUserIds.length === 0) {
|
|
230
|
+
throw new TRPCError({
|
|
231
|
+
code: "BAD_REQUEST",
|
|
232
|
+
message: "Select at least one student",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const [fromClass, toClass] = await Promise.all([
|
|
237
|
+
findSchoolClass(input.schoolId, input.fromClassId),
|
|
238
|
+
findSchoolClass(input.schoolId, input.toClassId),
|
|
239
|
+
]);
|
|
240
|
+
if (!fromClass || !toClass) {
|
|
241
|
+
throw new TRPCError({
|
|
242
|
+
code: "NOT_FOUND",
|
|
243
|
+
message: "Source or destination class not found in this school",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const memberships = await listActiveSchoolMemberships(
|
|
248
|
+
input.schoolId,
|
|
249
|
+
input.studentUserIds
|
|
250
|
+
);
|
|
251
|
+
const studentIds = memberships
|
|
252
|
+
.filter((m) => m.role === "STUDENT")
|
|
253
|
+
.map((m) => m.userId);
|
|
254
|
+
if (studentIds.length === 0) {
|
|
255
|
+
throw new TRPCError({
|
|
256
|
+
code: "BAD_REQUEST",
|
|
257
|
+
message: "No active student memberships found for the given users",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Only move students currently enrolled in the source class.
|
|
262
|
+
const enrolled = await prisma.class.findFirst({
|
|
263
|
+
where: {
|
|
264
|
+
id: input.fromClassId,
|
|
265
|
+
schoolId: input.schoolId,
|
|
266
|
+
},
|
|
267
|
+
select: {
|
|
268
|
+
students: {
|
|
269
|
+
where: { id: { in: studentIds } },
|
|
270
|
+
select: { id: true },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
const moveIds = (enrolled?.students ?? []).map((s) => s.id);
|
|
275
|
+
if (moveIds.length === 0) {
|
|
276
|
+
throw new TRPCError({
|
|
277
|
+
code: "BAD_REQUEST",
|
|
278
|
+
message: "None of the selected students are in the source class",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await prisma.$transaction([
|
|
283
|
+
prisma.class.update({
|
|
284
|
+
where: { id: input.fromClassId },
|
|
285
|
+
data: { students: { disconnect: moveIds.map((id) => ({ id })) } },
|
|
286
|
+
}),
|
|
287
|
+
prisma.class.update({
|
|
288
|
+
where: { id: input.toClassId },
|
|
289
|
+
data: { students: { connect: moveIds.map((id) => ({ id })) } },
|
|
290
|
+
}),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
promotedCount: moveIds.length,
|
|
295
|
+
studentUserIds: moveIds,
|
|
296
|
+
fromClassId: input.fromClassId,
|
|
297
|
+
toClassId: input.toClassId,
|
|
298
|
+
};
|
|
299
|
+
}
|