@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.
- 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 +193 -0
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +80 -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 +11605 -7133
- 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 +476 -6
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +130 -4
- 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 +122 -3
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +179 -47
- 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 +95 -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 +184 -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 +243 -45
- package/src/services/studentImport.ts +273 -0
- package/src/services/transcript.ts +313 -0
- package/src/services/worksheet.ts +111 -0
- package/src/trpc.ts +3 -3
- package/src/utils/adminPanelAccess.ts +12 -35
- package/tests/unit/assignmentTypeDefaults.test.ts +52 -0
- package/tests/unit/lib/feePdf.test.ts +15 -0
- package/tests/unit/lib/idCardPdf.test.ts +40 -0
- package/tests/unit/lib/transcriptPdf.test.ts +111 -0
- package/tests/unit/services/attendanceMerge.test.ts +90 -0
- package/tests/unit/services/attendanceRate.test.ts +38 -0
- package/tests/unit/services/attendanceStats.test.ts +54 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
3
|
+
import {
|
|
4
|
+
getChildAttendance,
|
|
5
|
+
getChildGrades,
|
|
6
|
+
linkParentToStudent,
|
|
7
|
+
listMyChildren,
|
|
8
|
+
listParentLinks,
|
|
9
|
+
unlinkParentFromStudent,
|
|
10
|
+
} from "../services/parentPortal.js";
|
|
11
|
+
|
|
12
|
+
export const parentRouter = createTRPCRouter({
|
|
13
|
+
linkStudent: protectedProcedure
|
|
14
|
+
.input(
|
|
15
|
+
z.object({
|
|
16
|
+
schoolId: z.string(),
|
|
17
|
+
parentUserId: z.string(),
|
|
18
|
+
studentUserId: z.string(),
|
|
19
|
+
})
|
|
20
|
+
)
|
|
21
|
+
.mutation(({ ctx, input }) => linkParentToStudent(ctx.user!.id, input)),
|
|
22
|
+
|
|
23
|
+
unlinkStudent: protectedProcedure
|
|
24
|
+
.input(
|
|
25
|
+
z.object({
|
|
26
|
+
schoolId: z.string(),
|
|
27
|
+
parentUserId: z.string(),
|
|
28
|
+
studentUserId: z.string(),
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
.mutation(({ ctx, input }) => unlinkParentFromStudent(ctx.user!.id, input)),
|
|
32
|
+
|
|
33
|
+
listLinks: protectedProcedure
|
|
34
|
+
.input(
|
|
35
|
+
z.object({
|
|
36
|
+
schoolId: z.string(),
|
|
37
|
+
parentUserId: z.string().optional(),
|
|
38
|
+
studentUserId: z.string().optional(),
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
.query(({ ctx, input }) => listParentLinks(ctx.user!.id, input)),
|
|
42
|
+
|
|
43
|
+
listMyChildren: protectedProcedure
|
|
44
|
+
.input(z.object({ schoolId: z.string() }))
|
|
45
|
+
.query(({ ctx, input }) => listMyChildren(ctx.user!.id, input.schoolId)),
|
|
46
|
+
|
|
47
|
+
getChildAttendance: protectedProcedure
|
|
48
|
+
.input(
|
|
49
|
+
z.object({
|
|
50
|
+
schoolId: z.string(),
|
|
51
|
+
studentUserId: z.string(),
|
|
52
|
+
classId: z.string().optional(),
|
|
53
|
+
from: z.date().optional(),
|
|
54
|
+
to: z.date().optional(),
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
.query(({ ctx, input }) => getChildAttendance(ctx.user!.id, input)),
|
|
58
|
+
|
|
59
|
+
getChildGrades: protectedProcedure
|
|
60
|
+
.input(
|
|
61
|
+
z.object({
|
|
62
|
+
schoolId: z.string(),
|
|
63
|
+
studentUserId: z.string(),
|
|
64
|
+
classId: z.string().optional(),
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
.query(({ ctx, input }) => getChildGrades(ctx.user!.id, input)),
|
|
68
|
+
});
|
package/src/routers/school.ts
CHANGED
|
@@ -7,13 +7,17 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
acceptInvite,
|
|
9
9
|
addMember,
|
|
10
|
+
bulkRemoveMembers,
|
|
11
|
+
bulkUpdateMemberRoles,
|
|
10
12
|
confirmSchoolAssetUpload,
|
|
11
13
|
createInvite,
|
|
12
14
|
createSchoolClass,
|
|
13
15
|
createSchoolRecord,
|
|
16
|
+
deleteSchoolClass,
|
|
14
17
|
getAdminOverview,
|
|
15
18
|
getCurrentSchool,
|
|
16
19
|
getInvite,
|
|
20
|
+
getMemberDetail,
|
|
17
21
|
getMemberTimeline,
|
|
18
22
|
getSchoolAssetUploadUrl,
|
|
19
23
|
getStudentTimeline,
|
|
@@ -24,17 +28,47 @@ import {
|
|
|
24
28
|
removeMember,
|
|
25
29
|
revokeInvite,
|
|
26
30
|
searchMembers,
|
|
31
|
+
updateClassMembers,
|
|
27
32
|
updateMemberRole,
|
|
28
33
|
updateSchoolDetails,
|
|
29
34
|
} from "../services/school.js";
|
|
30
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";
|
|
31
50
|
|
|
32
|
-
const schoolRoleSchema = z.enum([
|
|
51
|
+
const schoolRoleSchema = z.enum([
|
|
52
|
+
"ADMIN",
|
|
53
|
+
"TEACHER",
|
|
54
|
+
"STUDENT",
|
|
55
|
+
"ACCOUNTANT",
|
|
56
|
+
"PARENT",
|
|
57
|
+
]);
|
|
33
58
|
|
|
34
59
|
const schoolIdSchema = z.object({
|
|
35
60
|
schoolId: z.string(),
|
|
36
61
|
});
|
|
37
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
|
+
|
|
38
72
|
export const schoolRouter = createTRPCRouter({
|
|
39
73
|
getCurrent: protectedProcedure
|
|
40
74
|
.input(z.object({ schoolId: z.string().optional() }).optional())
|
|
@@ -61,6 +95,7 @@ export const schoolRouter = createTRPCRouter({
|
|
|
61
95
|
description: z.string().nullable().optional(),
|
|
62
96
|
subdomain: z.string().nullable().optional(),
|
|
63
97
|
logoId: z.string().nullable().optional(),
|
|
98
|
+
letterheadFileId: z.string().nullable().optional(),
|
|
64
99
|
})
|
|
65
100
|
)
|
|
66
101
|
.mutation(({ ctx, input }) => updateSchoolDetails(ctx.user!.id, input)),
|
|
@@ -143,6 +178,66 @@ export const schoolRouter = createTRPCRouter({
|
|
|
143
178
|
.input(schoolIdSchema)
|
|
144
179
|
.query(({ ctx, input }) => getAdminOverview(ctx.user!.id, input.schoolId)),
|
|
145
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
|
+
|
|
146
241
|
getAiUsage: protectedProcedure
|
|
147
242
|
.input(
|
|
148
243
|
z.object({
|
|
@@ -174,6 +269,23 @@ export const schoolRouter = createTRPCRouter({
|
|
|
174
269
|
)
|
|
175
270
|
.mutation(({ ctx, input }) => createSchoolClass(ctx.user!.id, input)),
|
|
176
271
|
|
|
272
|
+
deleteClass: protectedProcedure
|
|
273
|
+
.input(z.object({ schoolId: z.string(), classId: z.string() }))
|
|
274
|
+
.mutation(({ ctx, input }) => deleteSchoolClass(ctx.user!.id, input)),
|
|
275
|
+
|
|
276
|
+
updateClassMembers: protectedProcedure
|
|
277
|
+
.input(
|
|
278
|
+
z.object({
|
|
279
|
+
schoolId: z.string(),
|
|
280
|
+
classId: z.string(),
|
|
281
|
+
addTeacherIds: z.array(z.string()).optional(),
|
|
282
|
+
removeTeacherIds: z.array(z.string()).optional(),
|
|
283
|
+
addStudentIds: z.array(z.string()).optional(),
|
|
284
|
+
removeStudentIds: z.array(z.string()).optional(),
|
|
285
|
+
})
|
|
286
|
+
)
|
|
287
|
+
.mutation(({ ctx, input }) => updateClassMembers(ctx.user!.id, input)),
|
|
288
|
+
|
|
177
289
|
listMembers: protectedProcedure
|
|
178
290
|
.input(schoolIdSchema)
|
|
179
291
|
.query(({ ctx, input }) => listMembers(ctx.user!.id, input.schoolId)),
|
|
@@ -202,6 +314,66 @@ export const schoolRouter = createTRPCRouter({
|
|
|
202
314
|
.input(z.object({ schoolId: z.string(), memberUserId: z.string() }))
|
|
203
315
|
.mutation(({ ctx, input }) => removeMember(ctx.user!.id, input)),
|
|
204
316
|
|
|
317
|
+
getMemberDetail: protectedProcedure
|
|
318
|
+
.input(z.object({ schoolId: z.string(), memberUserId: z.string() }))
|
|
319
|
+
.query(({ ctx, input }) => getMemberDetail(ctx.user!.id, input)),
|
|
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
|
+
|
|
346
|
+
bulkUpdateMemberRoles: protectedProcedure
|
|
347
|
+
.input(
|
|
348
|
+
z.object({
|
|
349
|
+
schoolId: z.string(),
|
|
350
|
+
memberUserIds: z.array(z.string()).min(1).max(200),
|
|
351
|
+
role: schoolRoleSchema,
|
|
352
|
+
})
|
|
353
|
+
)
|
|
354
|
+
.mutation(({ ctx, input }) => bulkUpdateMemberRoles(ctx.user!.id, input)),
|
|
355
|
+
|
|
356
|
+
bulkRemoveMembers: protectedProcedure
|
|
357
|
+
.input(
|
|
358
|
+
z.object({
|
|
359
|
+
schoolId: z.string(),
|
|
360
|
+
memberUserIds: z.array(z.string()).min(1).max(200),
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
.mutation(({ ctx, input }) => bulkRemoveMembers(ctx.user!.id, input)),
|
|
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
|
+
|
|
205
377
|
listInvites: protectedProcedure
|
|
206
378
|
.input(schoolIdSchema)
|
|
207
379
|
.query(({ ctx, input }) => listInvites(ctx.user!.id, input.schoolId)),
|
|
@@ -232,4 +404,15 @@ export const schoolRouter = createTRPCRouter({
|
|
|
232
404
|
acceptInvite: protectedProcedure
|
|
233
405
|
.input(z.object({ token: z.string() }))
|
|
234
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)),
|
|
235
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
|
+
}
|