@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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModuleItem service – Canvas-style class content (pages, file packs, videos).
|
|
3
|
+
*/
|
|
4
|
+
import { TRPCError } from "@trpc/server";
|
|
5
|
+
import type { ModuleItemType } from "@prisma/client";
|
|
6
|
+
import { prisma } from "../lib/prisma.js";
|
|
7
|
+
import {
|
|
8
|
+
attachFilesToModuleItem,
|
|
9
|
+
createModuleItem,
|
|
10
|
+
deleteModuleItem,
|
|
11
|
+
findModuleItemByIdForMember,
|
|
12
|
+
findModuleItemByIdForTeacher,
|
|
13
|
+
findModuleItemsByClassId,
|
|
14
|
+
findModuleItemsBySectionId,
|
|
15
|
+
updateModuleItem,
|
|
16
|
+
} from "../models/moduleItem.js";
|
|
17
|
+
import { classTeacherOrSchoolAdminWhere } from "../models/access.js";
|
|
18
|
+
import { createDirectUploadFiles } from "../lib/fileUpload.js";
|
|
19
|
+
|
|
20
|
+
const VIDEO_MIME_PREFIX = "video/";
|
|
21
|
+
|
|
22
|
+
function assertVideoFiles(
|
|
23
|
+
type: ModuleItemType,
|
|
24
|
+
files?: { type: string }[]
|
|
25
|
+
) {
|
|
26
|
+
if (type !== "VIDEO" || !files?.length) return;
|
|
27
|
+
const invalid = files.filter((f) => !f.type.startsWith(VIDEO_MIME_PREFIX));
|
|
28
|
+
if (invalid.length > 0) {
|
|
29
|
+
throw new TRPCError({
|
|
30
|
+
code: "BAD_REQUEST",
|
|
31
|
+
message: "VIDEO module items only accept video MIME types",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function assertTeacherOwnsSection(sectionId: string, userId: string) {
|
|
37
|
+
const section = await prisma.section.findFirst({
|
|
38
|
+
where: {
|
|
39
|
+
id: sectionId,
|
|
40
|
+
class: classTeacherOrSchoolAdminWhere(userId),
|
|
41
|
+
},
|
|
42
|
+
select: { id: true, classId: true },
|
|
43
|
+
});
|
|
44
|
+
if (!section) {
|
|
45
|
+
throw new TRPCError({
|
|
46
|
+
code: "FORBIDDEN",
|
|
47
|
+
message: "Only class teachers can manage module items",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return section;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function listModuleItemsForClass(classId: string, userId: string) {
|
|
54
|
+
const membership = await prisma.class.findFirst({
|
|
55
|
+
where: {
|
|
56
|
+
id: classId,
|
|
57
|
+
OR: [
|
|
58
|
+
{ teachers: { some: { id: userId } } },
|
|
59
|
+
{ students: { some: { id: userId } } },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
select: { id: true },
|
|
63
|
+
});
|
|
64
|
+
if (!membership) {
|
|
65
|
+
throw new TRPCError({ code: "FORBIDDEN", message: "Not a class member" });
|
|
66
|
+
}
|
|
67
|
+
const items = await findModuleItemsByClassId(classId);
|
|
68
|
+
const isTeacher = await prisma.class.findFirst({
|
|
69
|
+
where: { id: classId, ...classTeacherOrSchoolAdminWhere(userId) },
|
|
70
|
+
select: { id: true },
|
|
71
|
+
});
|
|
72
|
+
if (isTeacher) return items;
|
|
73
|
+
return items.filter((item) => item.published);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function listModuleItemsForSection(
|
|
77
|
+
sectionId: string,
|
|
78
|
+
userId: string
|
|
79
|
+
) {
|
|
80
|
+
const section = await prisma.section.findFirst({
|
|
81
|
+
where: {
|
|
82
|
+
id: sectionId,
|
|
83
|
+
class: {
|
|
84
|
+
OR: [
|
|
85
|
+
{ teachers: { some: { id: userId } } },
|
|
86
|
+
{ students: { some: { id: userId } } },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
select: { id: true, classId: true },
|
|
91
|
+
});
|
|
92
|
+
if (!section) {
|
|
93
|
+
throw new TRPCError({ code: "FORBIDDEN", message: "Not a class member" });
|
|
94
|
+
}
|
|
95
|
+
const items = await findModuleItemsBySectionId(sectionId);
|
|
96
|
+
const isTeacher = await prisma.class.findFirst({
|
|
97
|
+
where: {
|
|
98
|
+
id: section.classId,
|
|
99
|
+
...classTeacherOrSchoolAdminWhere(userId),
|
|
100
|
+
},
|
|
101
|
+
select: { id: true },
|
|
102
|
+
});
|
|
103
|
+
if (isTeacher) return items;
|
|
104
|
+
return items.filter((item) => item.published);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getModuleItem(id: string, userId: string) {
|
|
108
|
+
const item = await findModuleItemByIdForMember(id, userId);
|
|
109
|
+
if (!item) {
|
|
110
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Module item not found" });
|
|
111
|
+
}
|
|
112
|
+
const isTeacher = await findModuleItemByIdForTeacher(id, userId);
|
|
113
|
+
if (!item.published && !isTeacher) {
|
|
114
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Module item not found" });
|
|
115
|
+
}
|
|
116
|
+
return item;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function createModuleItemRecord(
|
|
120
|
+
userId: string,
|
|
121
|
+
input: {
|
|
122
|
+
sectionId: string;
|
|
123
|
+
title: string;
|
|
124
|
+
type: ModuleItemType;
|
|
125
|
+
body?: string | null;
|
|
126
|
+
published?: boolean;
|
|
127
|
+
files?: { name: string; type: string; size: number }[];
|
|
128
|
+
existingFileIds?: string[];
|
|
129
|
+
}
|
|
130
|
+
) {
|
|
131
|
+
await assertTeacherOwnsSection(input.sectionId, userId);
|
|
132
|
+
|
|
133
|
+
if (input.type === "PAGE" && !input.body && !input.files?.length) {
|
|
134
|
+
// Allow empty page draft; body can be filled later.
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
(input.type === "FILES" || input.type === "VIDEO") &&
|
|
138
|
+
!input.files?.length &&
|
|
139
|
+
!input.existingFileIds?.length
|
|
140
|
+
) {
|
|
141
|
+
// Allow empty collection; files attach later via upload URLs.
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
assertVideoFiles(input.type, input.files);
|
|
145
|
+
|
|
146
|
+
const maxOrder = await prisma.moduleItem.aggregate({
|
|
147
|
+
where: { sectionId: input.sectionId },
|
|
148
|
+
_max: { order: true },
|
|
149
|
+
});
|
|
150
|
+
const order = (maxOrder._max.order ?? 0) + 1;
|
|
151
|
+
|
|
152
|
+
const created = await createModuleItem({
|
|
153
|
+
sectionId: input.sectionId,
|
|
154
|
+
title: input.title,
|
|
155
|
+
type: input.type,
|
|
156
|
+
body: input.type === "PAGE" ? input.body ?? "" : input.body,
|
|
157
|
+
order,
|
|
158
|
+
published: input.published ?? true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (input.existingFileIds?.length) {
|
|
162
|
+
await attachFilesToModuleItem(created.id, input.existingFileIds);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let uploadFiles: Awaited<ReturnType<typeof createDirectUploadFiles>> = [];
|
|
166
|
+
if (input.files?.length) {
|
|
167
|
+
uploadFiles = await createDirectUploadFiles(
|
|
168
|
+
input.files,
|
|
169
|
+
userId,
|
|
170
|
+
undefined,
|
|
171
|
+
undefined,
|
|
172
|
+
undefined
|
|
173
|
+
);
|
|
174
|
+
await attachFilesToModuleItem(
|
|
175
|
+
created.id,
|
|
176
|
+
uploadFiles.map((f) => f.id)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const refreshed = await findModuleItemByIdForTeacher(created.id, userId);
|
|
181
|
+
return { item: refreshed ?? created, uploadFiles };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function updateModuleItemRecord(
|
|
185
|
+
userId: string,
|
|
186
|
+
input: {
|
|
187
|
+
id: string;
|
|
188
|
+
title?: string;
|
|
189
|
+
body?: string | null;
|
|
190
|
+
published?: boolean;
|
|
191
|
+
type?: ModuleItemType;
|
|
192
|
+
sectionId?: string;
|
|
193
|
+
order?: number | null;
|
|
194
|
+
existingFileIds?: string[];
|
|
195
|
+
files?: { name: string; type: string; size: number }[];
|
|
196
|
+
}
|
|
197
|
+
) {
|
|
198
|
+
const existing = await findModuleItemByIdForTeacher(input.id, userId);
|
|
199
|
+
if (!existing) {
|
|
200
|
+
throw new TRPCError({
|
|
201
|
+
code: "FORBIDDEN",
|
|
202
|
+
message: "Only class teachers can update module items",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const nextType = input.type ?? existing.type;
|
|
207
|
+
assertVideoFiles(nextType, input.files);
|
|
208
|
+
|
|
209
|
+
if (input.sectionId && input.sectionId !== existing.sectionId) {
|
|
210
|
+
await assertTeacherOwnsSection(input.sectionId, userId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const updated = await updateModuleItem(input.id, {
|
|
214
|
+
title: input.title,
|
|
215
|
+
body: input.body,
|
|
216
|
+
published: input.published,
|
|
217
|
+
type: input.type,
|
|
218
|
+
sectionId: input.sectionId,
|
|
219
|
+
order: input.order,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (input.existingFileIds?.length) {
|
|
223
|
+
await attachFilesToModuleItem(input.id, input.existingFileIds);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let uploadFiles: Awaited<ReturnType<typeof createDirectUploadFiles>> = [];
|
|
227
|
+
if (input.files?.length) {
|
|
228
|
+
uploadFiles = await createDirectUploadFiles(
|
|
229
|
+
input.files,
|
|
230
|
+
userId,
|
|
231
|
+
undefined,
|
|
232
|
+
undefined,
|
|
233
|
+
undefined
|
|
234
|
+
);
|
|
235
|
+
await attachFilesToModuleItem(
|
|
236
|
+
input.id,
|
|
237
|
+
uploadFiles.map((f) => f.id)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const refreshed = await findModuleItemByIdForTeacher(input.id, userId);
|
|
242
|
+
return { item: refreshed ?? updated, uploadFiles };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function deleteModuleItemRecord(userId: string, id: string) {
|
|
246
|
+
const existing = await findModuleItemByIdForTeacher(id, userId);
|
|
247
|
+
if (!existing) {
|
|
248
|
+
throw new TRPCError({
|
|
249
|
+
code: "FORBIDDEN",
|
|
250
|
+
message: "Only class teachers can delete module items",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
await deleteModuleItem(id);
|
|
254
|
+
return { success: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function reorderModuleItems(
|
|
258
|
+
userId: string,
|
|
259
|
+
input: {
|
|
260
|
+
sectionId: string;
|
|
261
|
+
orderedIds: string[];
|
|
262
|
+
}
|
|
263
|
+
) {
|
|
264
|
+
await assertTeacherOwnsSection(input.sectionId, userId);
|
|
265
|
+
await prisma.$transaction(
|
|
266
|
+
input.orderedIds.map((id, index) =>
|
|
267
|
+
prisma.moduleItem.updateMany({
|
|
268
|
+
where: { id, sectionId: input.sectionId },
|
|
269
|
+
data: { order: index + 1 },
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
return findModuleItemsBySectionId(input.sectionId);
|
|
274
|
+
}
|
|
@@ -130,3 +130,46 @@ export async function markAllAsRead(userId: string) {
|
|
|
130
130
|
const result = await markAllNotificationsAsReadForReceiver(userId);
|
|
131
131
|
return { count: result.count };
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
/** C6: Emit a FEES notification (due / paid / reminder). Used by A2 fee flows. */
|
|
135
|
+
export async function notifyFeeEvent(input: {
|
|
136
|
+
receiverIds: string[];
|
|
137
|
+
title: string;
|
|
138
|
+
content: string;
|
|
139
|
+
entityId?: string | null;
|
|
140
|
+
senderId?: string | null;
|
|
141
|
+
}) {
|
|
142
|
+
return sendToMultiple({
|
|
143
|
+
receiverIds: input.receiverIds,
|
|
144
|
+
title: input.title,
|
|
145
|
+
content: input.content,
|
|
146
|
+
senderId: input.senderId,
|
|
147
|
+
type: "FEES",
|
|
148
|
+
entityType: "FEE",
|
|
149
|
+
entityId: input.entityId ?? null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* C6: Excusal notification hook (trivial).
|
|
155
|
+
* A1 owns StudentExcusal; call this when an excusal is created/decided.
|
|
156
|
+
*/
|
|
157
|
+
export async function notifyExcusalEvent(input: {
|
|
158
|
+
receiverIds: string[];
|
|
159
|
+
title: string;
|
|
160
|
+
content: string;
|
|
161
|
+
entityId?: string | null;
|
|
162
|
+
classId?: string | null;
|
|
163
|
+
senderId?: string | null;
|
|
164
|
+
}) {
|
|
165
|
+
return sendToMultiple({
|
|
166
|
+
receiverIds: input.receiverIds,
|
|
167
|
+
title: input.title,
|
|
168
|
+
content: input.content,
|
|
169
|
+
senderId: input.senderId,
|
|
170
|
+
type: "EXCUSAL",
|
|
171
|
+
entityType: "EXCUSAL",
|
|
172
|
+
entityId: input.entityId ?? null,
|
|
173
|
+
classId: input.classId ?? null,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4: Minimal parent portal — link parents to students and expose
|
|
3
|
+
* read-only attendance / grades stubs for linked children.
|
|
4
|
+
*/
|
|
5
|
+
import { TRPCError } from "@trpc/server";
|
|
6
|
+
import { prisma } from "../lib/prisma.js";
|
|
7
|
+
import {
|
|
8
|
+
findActiveSchoolAdminMembership,
|
|
9
|
+
findActiveSchoolMembership,
|
|
10
|
+
findSchoolMembership,
|
|
11
|
+
} from "../models/school.js";
|
|
12
|
+
|
|
13
|
+
async function requireSchoolAdmin(schoolId: string, userId: string) {
|
|
14
|
+
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
15
|
+
if (!membership) {
|
|
16
|
+
throw new TRPCError({
|
|
17
|
+
code: "FORBIDDEN",
|
|
18
|
+
message: "Only school admins can perform this action",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return membership;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function requireParentMembership(schoolId: string, userId: string) {
|
|
25
|
+
const membership = await findActiveSchoolMembership(schoolId, userId);
|
|
26
|
+
if (!membership || membership.role !== "PARENT") {
|
|
27
|
+
throw new TRPCError({
|
|
28
|
+
code: "FORBIDDEN",
|
|
29
|
+
message: "Only parents can access this resource",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return membership;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function linkParentToStudent(
|
|
36
|
+
userId: string,
|
|
37
|
+
input: { schoolId: string; parentUserId: string; studentUserId: string }
|
|
38
|
+
) {
|
|
39
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
40
|
+
|
|
41
|
+
const [parentMembership, studentMembership] = await Promise.all([
|
|
42
|
+
findSchoolMembership(input.schoolId, input.parentUserId),
|
|
43
|
+
findSchoolMembership(input.schoolId, input.studentUserId),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
if (!parentMembership || parentMembership.status !== "ACTIVE") {
|
|
47
|
+
throw new TRPCError({
|
|
48
|
+
code: "BAD_REQUEST",
|
|
49
|
+
message: "Parent must be an active school member",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (parentMembership.role !== "PARENT") {
|
|
53
|
+
throw new TRPCError({
|
|
54
|
+
code: "BAD_REQUEST",
|
|
55
|
+
message: "parentUserId must have the PARENT role",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
!studentMembership ||
|
|
60
|
+
studentMembership.status !== "ACTIVE" ||
|
|
61
|
+
studentMembership.role !== "STUDENT"
|
|
62
|
+
) {
|
|
63
|
+
throw new TRPCError({
|
|
64
|
+
code: "BAD_REQUEST",
|
|
65
|
+
message: "studentUserId must be an active STUDENT member",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const link = await prisma.parentStudentLink.upsert({
|
|
70
|
+
where: {
|
|
71
|
+
schoolId_parentUserId_studentUserId: {
|
|
72
|
+
schoolId: input.schoolId,
|
|
73
|
+
parentUserId: input.parentUserId,
|
|
74
|
+
studentUserId: input.studentUserId,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
update: {},
|
|
78
|
+
create: {
|
|
79
|
+
schoolId: input.schoolId,
|
|
80
|
+
parentUserId: input.parentUserId,
|
|
81
|
+
studentUserId: input.studentUserId,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { link };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function unlinkParentFromStudent(
|
|
89
|
+
userId: string,
|
|
90
|
+
input: { schoolId: string; parentUserId: string; studentUserId: string }
|
|
91
|
+
) {
|
|
92
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
93
|
+
await prisma.parentStudentLink.deleteMany({
|
|
94
|
+
where: {
|
|
95
|
+
schoolId: input.schoolId,
|
|
96
|
+
parentUserId: input.parentUserId,
|
|
97
|
+
studentUserId: input.studentUserId,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return { ok: true as const };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function listParentLinks(
|
|
104
|
+
userId: string,
|
|
105
|
+
input: { schoolId: string; parentUserId?: string; studentUserId?: string }
|
|
106
|
+
) {
|
|
107
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
108
|
+
const links = await prisma.parentStudentLink.findMany({
|
|
109
|
+
where: {
|
|
110
|
+
schoolId: input.schoolId,
|
|
111
|
+
...(input.parentUserId ? { parentUserId: input.parentUserId } : {}),
|
|
112
|
+
...(input.studentUserId ? { studentUserId: input.studentUserId } : {}),
|
|
113
|
+
},
|
|
114
|
+
include: {
|
|
115
|
+
parent: {
|
|
116
|
+
select: {
|
|
117
|
+
id: true,
|
|
118
|
+
username: true,
|
|
119
|
+
email: true,
|
|
120
|
+
profile: { select: { displayName: true } },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
student: {
|
|
124
|
+
select: {
|
|
125
|
+
id: true,
|
|
126
|
+
username: true,
|
|
127
|
+
email: true,
|
|
128
|
+
profile: { select: { displayName: true } },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
orderBy: { createdAt: "desc" },
|
|
133
|
+
});
|
|
134
|
+
return { links };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function listMyChildren(userId: string, schoolId: string) {
|
|
138
|
+
await requireParentMembership(schoolId, userId);
|
|
139
|
+
const links = await prisma.parentStudentLink.findMany({
|
|
140
|
+
where: { schoolId, parentUserId: userId },
|
|
141
|
+
include: {
|
|
142
|
+
student: {
|
|
143
|
+
select: {
|
|
144
|
+
id: true,
|
|
145
|
+
username: true,
|
|
146
|
+
email: true,
|
|
147
|
+
profile: {
|
|
148
|
+
select: {
|
|
149
|
+
displayName: true,
|
|
150
|
+
profilePicture: true,
|
|
151
|
+
profilePictureThumbnail: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
studentIn: {
|
|
155
|
+
where: { schoolId },
|
|
156
|
+
select: {
|
|
157
|
+
id: true,
|
|
158
|
+
name: true,
|
|
159
|
+
subject: true,
|
|
160
|
+
section: true,
|
|
161
|
+
color: true,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
orderBy: { createdAt: "asc" },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
children: links.map((link) => ({
|
|
172
|
+
linkId: link.id,
|
|
173
|
+
student: {
|
|
174
|
+
id: link.student.id,
|
|
175
|
+
username: link.student.username,
|
|
176
|
+
email: link.student.email,
|
|
177
|
+
displayName: link.student.profile?.displayName ?? null,
|
|
178
|
+
profilePicture: link.student.profile?.profilePicture ?? null,
|
|
179
|
+
profilePictureThumbnail:
|
|
180
|
+
link.student.profile?.profilePictureThumbnail ?? null,
|
|
181
|
+
classes: link.student.studentIn,
|
|
182
|
+
},
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function assertParentOfStudent(
|
|
188
|
+
schoolId: string,
|
|
189
|
+
parentUserId: string,
|
|
190
|
+
studentUserId: string
|
|
191
|
+
) {
|
|
192
|
+
await requireParentMembership(schoolId, parentUserId);
|
|
193
|
+
const link = await prisma.parentStudentLink.findUnique({
|
|
194
|
+
where: {
|
|
195
|
+
schoolId_parentUserId_studentUserId: {
|
|
196
|
+
schoolId,
|
|
197
|
+
parentUserId,
|
|
198
|
+
studentUserId,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (!link) {
|
|
203
|
+
throw new TRPCError({
|
|
204
|
+
code: "FORBIDDEN",
|
|
205
|
+
message: "You are not linked to this student",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return link;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Read-only attendance stub for a linked child (aggregates class attendance M2M).
|
|
213
|
+
* Does not touch A1 AttendanceRecord refactor.
|
|
214
|
+
*/
|
|
215
|
+
export async function getChildAttendance(
|
|
216
|
+
userId: string,
|
|
217
|
+
input: {
|
|
218
|
+
schoolId: string;
|
|
219
|
+
studentUserId: string;
|
|
220
|
+
classId?: string;
|
|
221
|
+
from?: Date;
|
|
222
|
+
to?: Date;
|
|
223
|
+
}
|
|
224
|
+
) {
|
|
225
|
+
await assertParentOfStudent(input.schoolId, userId, input.studentUserId);
|
|
226
|
+
|
|
227
|
+
const classes = await prisma.class.findMany({
|
|
228
|
+
where: {
|
|
229
|
+
schoolId: input.schoolId,
|
|
230
|
+
students: { some: { id: input.studentUserId } },
|
|
231
|
+
...(input.classId ? { id: input.classId } : {}),
|
|
232
|
+
},
|
|
233
|
+
select: { id: true, name: true, subject: true, section: true },
|
|
234
|
+
});
|
|
235
|
+
const classIds = classes.map((c) => c.id);
|
|
236
|
+
|
|
237
|
+
const attendance = await prisma.attendance.findMany({
|
|
238
|
+
where: {
|
|
239
|
+
classId: { in: classIds },
|
|
240
|
+
...(input.from || input.to
|
|
241
|
+
? {
|
|
242
|
+
date: {
|
|
243
|
+
...(input.from ? { gte: input.from } : {}),
|
|
244
|
+
...(input.to ? { lte: input.to } : {}),
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
: {}),
|
|
248
|
+
OR: [
|
|
249
|
+
{ present: { some: { id: input.studentUserId } } },
|
|
250
|
+
{ late: { some: { id: input.studentUserId } } },
|
|
251
|
+
{ absent: { some: { id: input.studentUserId } } },
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
select: {
|
|
255
|
+
id: true,
|
|
256
|
+
date: true,
|
|
257
|
+
classId: true,
|
|
258
|
+
present: { where: { id: input.studentUserId }, select: { id: true } },
|
|
259
|
+
late: { where: { id: input.studentUserId }, select: { id: true } },
|
|
260
|
+
absent: { where: { id: input.studentUserId }, select: { id: true } },
|
|
261
|
+
},
|
|
262
|
+
orderBy: { date: "desc" },
|
|
263
|
+
take: 200,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const classMap = new Map(classes.map((c) => [c.id, c]));
|
|
267
|
+
const records = attendance.map((row) => {
|
|
268
|
+
let status: "PRESENT" | "LATE" | "ABSENT" = "PRESENT";
|
|
269
|
+
if (row.absent.length > 0) status = "ABSENT";
|
|
270
|
+
else if (row.late.length > 0) status = "LATE";
|
|
271
|
+
return {
|
|
272
|
+
attendanceId: row.id,
|
|
273
|
+
date: row.date,
|
|
274
|
+
classId: row.classId,
|
|
275
|
+
class: classMap.get(row.classId) ?? null,
|
|
276
|
+
status,
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const summary = records.reduce(
|
|
281
|
+
(acc, r) => {
|
|
282
|
+
acc.total += 1;
|
|
283
|
+
if (r.status === "PRESENT") acc.present += 1;
|
|
284
|
+
else if (r.status === "LATE") acc.late += 1;
|
|
285
|
+
else acc.absent += 1;
|
|
286
|
+
return acc;
|
|
287
|
+
},
|
|
288
|
+
{ present: 0, late: 0, absent: 0, total: 0 }
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return { summary, records };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Read-only grades stub — returns assignment score rows for the linked child.
|
|
296
|
+
*/
|
|
297
|
+
export async function getChildGrades(
|
|
298
|
+
userId: string,
|
|
299
|
+
input: { schoolId: string; studentUserId: string; classId?: string }
|
|
300
|
+
) {
|
|
301
|
+
await assertParentOfStudent(input.schoolId, userId, input.studentUserId);
|
|
302
|
+
|
|
303
|
+
const classes = await prisma.class.findMany({
|
|
304
|
+
where: {
|
|
305
|
+
schoolId: input.schoolId,
|
|
306
|
+
students: { some: { id: input.studentUserId } },
|
|
307
|
+
...(input.classId ? { id: input.classId } : {}),
|
|
308
|
+
},
|
|
309
|
+
select: {
|
|
310
|
+
id: true,
|
|
311
|
+
name: true,
|
|
312
|
+
subject: true,
|
|
313
|
+
section: true,
|
|
314
|
+
assignments: {
|
|
315
|
+
select: {
|
|
316
|
+
id: true,
|
|
317
|
+
title: true,
|
|
318
|
+
type: true,
|
|
319
|
+
dueDate: true,
|
|
320
|
+
maxGrade: true,
|
|
321
|
+
submissions: {
|
|
322
|
+
where: { studentId: input.studentUserId },
|
|
323
|
+
select: {
|
|
324
|
+
id: true,
|
|
325
|
+
gradeReceived: true,
|
|
326
|
+
returned: true,
|
|
327
|
+
submittedAt: true,
|
|
328
|
+
},
|
|
329
|
+
take: 1,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
orderBy: { dueDate: "desc" },
|
|
333
|
+
take: 100,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
classes: classes.map((cls) => ({
|
|
340
|
+
id: cls.id,
|
|
341
|
+
name: cls.name,
|
|
342
|
+
subject: cls.subject,
|
|
343
|
+
section: cls.section,
|
|
344
|
+
assignments: cls.assignments.map((a) => ({
|
|
345
|
+
id: a.id,
|
|
346
|
+
title: a.title,
|
|
347
|
+
type: a.type,
|
|
348
|
+
dueDate: a.dueDate,
|
|
349
|
+
maxGrade: a.maxGrade,
|
|
350
|
+
submission: a.submissions[0]
|
|
351
|
+
? {
|
|
352
|
+
id: a.submissions[0].id,
|
|
353
|
+
gradeReceived: a.submissions[0].gradeReceived,
|
|
354
|
+
returned: a.submissions[0].returned,
|
|
355
|
+
submittedAt: a.submissions[0].submittedAt,
|
|
356
|
+
}
|
|
357
|
+
: null,
|
|
358
|
+
})),
|
|
359
|
+
})),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Helper for other services: is this user a parent of the student? */
|
|
364
|
+
export async function isParentOfStudent(
|
|
365
|
+
schoolId: string,
|
|
366
|
+
parentUserId: string,
|
|
367
|
+
studentUserId: string
|
|
368
|
+
) {
|
|
369
|
+
const link = await prisma.parentStudentLink.findUnique({
|
|
370
|
+
where: {
|
|
371
|
+
schoolId_parentUserId_studentUserId: {
|
|
372
|
+
schoolId,
|
|
373
|
+
parentUserId,
|
|
374
|
+
studentUserId,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
select: { id: true },
|
|
378
|
+
});
|
|
379
|
+
return Boolean(link);
|
|
380
|
+
}
|