@studious-lms/server 1.24.0 → 1.26.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/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +4 -4
- package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1 -1
- package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +4 -4
- package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +4 -4
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +17 -14
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/marketplace.d.ts +37 -0
- package/dist/models/marketplace.d.ts.map +1 -1
- package/dist/models/marketplace.js +10 -2
- package/dist/models/marketplace.js.map +1 -1
- package/dist/models/module.d.ts +228 -0
- package/dist/models/module.d.ts.map +1 -0
- package/dist/models/module.js +69 -0
- package/dist/models/module.js.map +1 -0
- package/dist/models/moduleItem.d.ts +5 -1
- package/dist/models/moduleItem.d.ts.map +1 -1
- package/dist/models/moduleItem.js +19 -4
- package/dist/models/moduleItem.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +4 -2
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +768 -6
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/fees.d.ts +1 -1
- package/dist/routers/marketplace.d.ts +41 -0
- package/dist/routers/marketplace.d.ts.map +1 -1
- package/dist/routers/marketplace.js +13 -3
- package/dist/routers/marketplace.js.map +1 -1
- package/dist/routers/module.d.ts +345 -0
- package/dist/routers/module.d.ts.map +1 -0
- package/dist/routers/module.js +82 -0
- package/dist/routers/module.js.map +1 -0
- package/dist/routers/moduleItem.d.ts.map +1 -1
- package/dist/routers/moduleItem.js +7 -6
- package/dist/routers/moduleItem.js.map +1 -1
- package/dist/routers/parent.d.ts +2 -1
- package/dist/routers/parent.d.ts.map +1 -1
- package/dist/routers/school.d.ts +1 -1
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +14 -4
- package/dist/routers/school.js.map +1 -1
- package/dist/services/academicCalendar.d.ts.map +1 -1
- package/dist/services/academicCalendar.js +30 -2
- package/dist/services/academicCalendar.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +8 -3
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +6 -2
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +8 -4
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/class.d.ts +14 -9
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +4 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/fees.d.ts.map +1 -1
- package/dist/services/fees.js +24 -4
- package/dist/services/fees.js.map +1 -1
- package/dist/services/feesAuthz.d.ts +2 -5
- package/dist/services/feesAuthz.d.ts.map +1 -1
- package/dist/services/feesAuthz.js +5 -8
- package/dist/services/feesAuthz.js.map +1 -1
- package/dist/services/marketplace.d.ts +36 -0
- package/dist/services/marketplace.d.ts.map +1 -1
- package/dist/services/marketplace.js +36 -6
- package/dist/services/marketplace.js.map +1 -1
- package/dist/services/module.d.ts +288 -0
- package/dist/services/module.d.ts.map +1 -0
- package/dist/services/module.js +518 -0
- package/dist/services/module.js.map +1 -0
- package/dist/services/moduleItem.d.ts.map +1 -1
- package/dist/services/moduleItem.js +23 -7
- package/dist/services/moduleItem.js.map +1 -1
- package/dist/services/parentPortal.d.ts +3 -5
- package/dist/services/parentPortal.d.ts.map +1 -1
- package/dist/services/parentPortal.js +29 -43
- package/dist/services/parentPortal.js.map +1 -1
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +52 -16
- package/dist/services/school.js.map +1 -1
- package/dist/services/studentImport.d.ts.map +1 -1
- package/dist/services/studentImport.js +38 -25
- package/dist/services/studentImport.js.map +1 -1
- package/dist/services/transcript.d.ts +2 -3
- package/dist/services/transcript.d.ts.map +1 -1
- package/dist/services/transcript.js +13 -25
- package/dist/services/transcript.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260708013000_add_modules/migration.sql +100 -0
- package/prisma/schema.prisma +93 -0
- package/scripts/import-nepal-cdc-curriculum.ts +29 -6
- package/src/models/attendance.ts +10 -12
- package/src/models/marketplace.ts +19 -0
- package/src/models/module.ts +77 -0
- package/src/models/moduleItem.ts +20 -2
- package/src/pipelines/gradeWorksheet.ts +6 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/marketplace.ts +16 -0
- package/src/routers/module.ts +118 -0
- package/src/routers/moduleItem.ts +6 -4
- package/src/routers/school.ts +16 -6
- package/src/services/academicCalendar.ts +28 -0
- package/src/services/assignment.ts +12 -1
- package/src/services/attendance.ts +7 -3
- package/src/services/class.ts +2 -2
- package/src/services/fees.ts +25 -2
- package/src/services/feesAuthz.ts +3 -7
- package/src/services/marketplace.ts +47 -3
- package/src/services/module.ts +683 -0
- package/src/services/moduleItem.ts +28 -4
- package/src/services/parentPortal.ts +26 -39
- package/src/services/school.ts +52 -15
- package/src/services/studentImport.ts +37 -25
- package/src/services/transcript.ts +11 -23
|
@@ -7,6 +7,7 @@ import { prisma } from "../lib/prisma.js";
|
|
|
7
7
|
import {
|
|
8
8
|
attachFilesToModuleItem,
|
|
9
9
|
createModuleItem,
|
|
10
|
+
findAccessibleFilesByIds,
|
|
10
11
|
deleteModuleItem,
|
|
11
12
|
findModuleItemByIdForMember,
|
|
12
13
|
findModuleItemByIdForTeacher,
|
|
@@ -33,6 +34,21 @@ function assertVideoFiles(
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
async function resolveExistingFiles(
|
|
38
|
+
type: ModuleItemType,
|
|
39
|
+
existingFileIds: string[],
|
|
40
|
+
userId: string
|
|
41
|
+
) {
|
|
42
|
+
const accessible = await findAccessibleFilesByIds(existingFileIds, userId);
|
|
43
|
+
if (accessible.length !== new Set(existingFileIds).size) {
|
|
44
|
+
throw new TRPCError({
|
|
45
|
+
code: "FORBIDDEN",
|
|
46
|
+
message: "One or more files are not accessible",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
assertVideoFiles(type, accessible);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
async function assertTeacherOwnsSection(sectionId: string, userId: string) {
|
|
37
53
|
const section = await prisma.section.findFirst({
|
|
38
54
|
where: {
|
|
@@ -142,6 +158,9 @@ export async function createModuleItemRecord(
|
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
assertVideoFiles(input.type, input.files);
|
|
161
|
+
if (input.existingFileIds?.length) {
|
|
162
|
+
await resolveExistingFiles(input.type, input.existingFileIds, userId);
|
|
163
|
+
}
|
|
145
164
|
|
|
146
165
|
const maxOrder = await prisma.moduleItem.aggregate({
|
|
147
166
|
where: { sectionId: input.sectionId },
|
|
@@ -159,7 +178,7 @@ export async function createModuleItemRecord(
|
|
|
159
178
|
});
|
|
160
179
|
|
|
161
180
|
if (input.existingFileIds?.length) {
|
|
162
|
-
await attachFilesToModuleItem(created.id, input.existingFileIds);
|
|
181
|
+
await attachFilesToModuleItem(created.id, input.existingFileIds, userId);
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
let uploadFiles: Awaited<ReturnType<typeof createDirectUploadFiles>> = [];
|
|
@@ -173,7 +192,8 @@ export async function createModuleItemRecord(
|
|
|
173
192
|
);
|
|
174
193
|
await attachFilesToModuleItem(
|
|
175
194
|
created.id,
|
|
176
|
-
uploadFiles.map((f) => f.id)
|
|
195
|
+
uploadFiles.map((f) => f.id),
|
|
196
|
+
userId
|
|
177
197
|
);
|
|
178
198
|
}
|
|
179
199
|
|
|
@@ -205,6 +225,9 @@ export async function updateModuleItemRecord(
|
|
|
205
225
|
|
|
206
226
|
const nextType = input.type ?? existing.type;
|
|
207
227
|
assertVideoFiles(nextType, input.files);
|
|
228
|
+
if (input.existingFileIds?.length) {
|
|
229
|
+
await resolveExistingFiles(nextType, input.existingFileIds, userId);
|
|
230
|
+
}
|
|
208
231
|
|
|
209
232
|
if (input.sectionId && input.sectionId !== existing.sectionId) {
|
|
210
233
|
await assertTeacherOwnsSection(input.sectionId, userId);
|
|
@@ -220,7 +243,7 @@ export async function updateModuleItemRecord(
|
|
|
220
243
|
});
|
|
221
244
|
|
|
222
245
|
if (input.existingFileIds?.length) {
|
|
223
|
-
await attachFilesToModuleItem(input.id, input.existingFileIds);
|
|
246
|
+
await attachFilesToModuleItem(input.id, input.existingFileIds, userId);
|
|
224
247
|
}
|
|
225
248
|
|
|
226
249
|
let uploadFiles: Awaited<ReturnType<typeof createDirectUploadFiles>> = [];
|
|
@@ -234,7 +257,8 @@ export async function updateModuleItemRecord(
|
|
|
234
257
|
);
|
|
235
258
|
await attachFilesToModuleItem(
|
|
236
259
|
input.id,
|
|
237
|
-
uploadFiles.map((f) => f.id)
|
|
260
|
+
uploadFiles.map((f) => f.id),
|
|
261
|
+
userId
|
|
238
262
|
);
|
|
239
263
|
}
|
|
240
264
|
|
|
@@ -208,10 +208,7 @@ async function assertParentOfStudent(
|
|
|
208
208
|
return link;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
/**
|
|
212
|
-
* Read-only attendance stub for a linked child (aggregates class attendance M2M).
|
|
213
|
-
* Does not touch A1 AttendanceRecord refactor.
|
|
214
|
-
*/
|
|
211
|
+
/** Read-only attendance view for a linked child (reads AttendanceRecord). */
|
|
215
212
|
export async function getChildAttendance(
|
|
216
213
|
userId: string,
|
|
217
214
|
input: {
|
|
@@ -234,58 +231,48 @@ export async function getChildAttendance(
|
|
|
234
231
|
});
|
|
235
232
|
const classIds = classes.map((c) => c.id);
|
|
236
233
|
|
|
237
|
-
const
|
|
234
|
+
const attendanceRecords = await prisma.attendanceRecord.findMany({
|
|
238
235
|
where: {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
{ absent: { some: { id: input.studentUserId } } },
|
|
252
|
-
],
|
|
236
|
+
studentId: input.studentUserId,
|
|
237
|
+
attendance: {
|
|
238
|
+
classId: { in: classIds },
|
|
239
|
+
...(input.from || input.to
|
|
240
|
+
? {
|
|
241
|
+
date: {
|
|
242
|
+
...(input.from ? { gte: input.from } : {}),
|
|
243
|
+
...(input.to ? { lte: input.to } : {}),
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
},
|
|
253
248
|
},
|
|
254
249
|
select: {
|
|
255
|
-
|
|
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 } },
|
|
250
|
+
status: true,
|
|
251
|
+
attendance: { select: { id: true, date: true, classId: true } },
|
|
261
252
|
},
|
|
262
|
-
orderBy: { date: "desc" },
|
|
253
|
+
orderBy: { attendance: { date: "desc" } },
|
|
263
254
|
take: 200,
|
|
264
255
|
});
|
|
265
256
|
|
|
266
257
|
const classMap = new Map(classes.map((c) => [c.id, c]));
|
|
267
|
-
const records =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
classId: row.classId,
|
|
275
|
-
class: classMap.get(row.classId) ?? null,
|
|
276
|
-
status,
|
|
277
|
-
};
|
|
278
|
-
});
|
|
258
|
+
const records = attendanceRecords.map((row) => ({
|
|
259
|
+
attendanceId: row.attendance.id,
|
|
260
|
+
date: row.attendance.date,
|
|
261
|
+
classId: row.attendance.classId,
|
|
262
|
+
class: classMap.get(row.attendance.classId) ?? null,
|
|
263
|
+
status: row.status,
|
|
264
|
+
}));
|
|
279
265
|
|
|
280
266
|
const summary = records.reduce(
|
|
281
267
|
(acc, r) => {
|
|
282
268
|
acc.total += 1;
|
|
283
269
|
if (r.status === "PRESENT") acc.present += 1;
|
|
284
270
|
else if (r.status === "LATE") acc.late += 1;
|
|
271
|
+
else if (r.status === "EXCUSED") acc.excused += 1;
|
|
285
272
|
else acc.absent += 1;
|
|
286
273
|
return acc;
|
|
287
274
|
},
|
|
288
|
-
{ present: 0, late: 0, absent: 0, total: 0 }
|
|
275
|
+
{ present: 0, late: 0, absent: 0, excused: 0, total: 0 }
|
|
289
276
|
);
|
|
290
277
|
|
|
291
278
|
return { summary, records };
|
package/src/services/school.ts
CHANGED
|
@@ -133,6 +133,28 @@ export async function updateSchoolDetails(
|
|
|
133
133
|
}
|
|
134
134
|
) {
|
|
135
135
|
await requireSchoolAdmin(input.schoolId, userId);
|
|
136
|
+
const brandingFileIds = [input.logoId, input.letterheadFileId].filter(
|
|
137
|
+
(id): id is string => typeof id === "string"
|
|
138
|
+
);
|
|
139
|
+
if (brandingFileIds.length) {
|
|
140
|
+
const accessible = await prisma.file.findMany({
|
|
141
|
+
where: {
|
|
142
|
+
id: { in: brandingFileIds },
|
|
143
|
+
OR: [
|
|
144
|
+
{ userId },
|
|
145
|
+
{ folder: { schoolStorageId: input.schoolId } },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
select: { id: true },
|
|
149
|
+
});
|
|
150
|
+
const accessibleIds = new Set(accessible.map((f) => f.id));
|
|
151
|
+
if (brandingFileIds.some((id) => !accessibleIds.has(id))) {
|
|
152
|
+
throw new TRPCError({
|
|
153
|
+
code: "FORBIDDEN",
|
|
154
|
+
message: "File does not belong to this school",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
136
158
|
const updated = await updateSchoolRecord(input.schoolId, {
|
|
137
159
|
name: input.name,
|
|
138
160
|
description: input.description,
|
|
@@ -628,17 +650,19 @@ export async function getStudentTimeline(
|
|
|
628
650
|
});
|
|
629
651
|
}
|
|
630
652
|
|
|
653
|
+
// Default to the last 12 months when no range is given, to bound reads.
|
|
654
|
+
const defaultFrom = new Date();
|
|
655
|
+
defaultFrom.setFullYear(defaultFrom.getFullYear() - 1);
|
|
631
656
|
const range = {
|
|
632
|
-
|
|
657
|
+
gte: input.from ?? defaultFrom,
|
|
633
658
|
...(input.to ? { lte: input.to } : {}),
|
|
634
659
|
};
|
|
635
|
-
const hasRange = Object.keys(range).length > 0;
|
|
636
|
-
|
|
637
660
|
const [events, assignments, attendance] = await Promise.all([
|
|
638
661
|
prisma.event.findMany({
|
|
639
662
|
where: {
|
|
640
663
|
userId: input.studentId,
|
|
641
|
-
|
|
664
|
+
startTime: range,
|
|
665
|
+
OR: [{ classId: null }, { class: { schoolId: input.schoolId } }],
|
|
642
666
|
},
|
|
643
667
|
select: {
|
|
644
668
|
id: true,
|
|
@@ -655,10 +679,11 @@ export async function getStudentTimeline(
|
|
|
655
679
|
prisma.assignment.findMany({
|
|
656
680
|
where: {
|
|
657
681
|
class: {
|
|
682
|
+
schoolId: input.schoolId,
|
|
658
683
|
students: { some: { id: input.studentId } },
|
|
659
684
|
},
|
|
660
685
|
template: false,
|
|
661
|
-
|
|
686
|
+
dueDate: range,
|
|
662
687
|
},
|
|
663
688
|
select: {
|
|
664
689
|
id: true,
|
|
@@ -695,7 +720,7 @@ export async function getStudentTimeline(
|
|
|
695
720
|
studentId: input.studentId,
|
|
696
721
|
attendance: {
|
|
697
722
|
class: { schoolId: input.schoolId },
|
|
698
|
-
|
|
723
|
+
date: range,
|
|
699
724
|
},
|
|
700
725
|
},
|
|
701
726
|
select: {
|
|
@@ -786,11 +811,13 @@ export async function getMemberTimeline(
|
|
|
786
811
|
});
|
|
787
812
|
}
|
|
788
813
|
|
|
814
|
+
// Default to the last 12 months when no range is given, to bound reads.
|
|
815
|
+
const defaultFrom = new Date();
|
|
816
|
+
defaultFrom.setFullYear(defaultFrom.getFullYear() - 1);
|
|
789
817
|
const range = {
|
|
790
|
-
|
|
818
|
+
gte: input.from ?? defaultFrom,
|
|
791
819
|
...(input.to ? { lte: input.to } : {}),
|
|
792
820
|
};
|
|
793
|
-
const hasRange = Object.keys(range).length > 0;
|
|
794
821
|
|
|
795
822
|
if (membership.role === "STUDENT") {
|
|
796
823
|
if (!callerIsAdmin) {
|
|
@@ -814,7 +841,8 @@ export async function getMemberTimeline(
|
|
|
814
841
|
prisma.event.findMany({
|
|
815
842
|
where: {
|
|
816
843
|
userId: input.memberId,
|
|
817
|
-
|
|
844
|
+
startTime: range,
|
|
845
|
+
OR: [{ classId: null }, { class: { schoolId: input.schoolId } }],
|
|
818
846
|
},
|
|
819
847
|
select: {
|
|
820
848
|
id: true,
|
|
@@ -830,9 +858,12 @@ export async function getMemberTimeline(
|
|
|
830
858
|
}),
|
|
831
859
|
prisma.assignment.findMany({
|
|
832
860
|
where: {
|
|
833
|
-
class: {
|
|
861
|
+
class: {
|
|
862
|
+
schoolId: input.schoolId,
|
|
863
|
+
students: { some: { id: input.memberId } },
|
|
864
|
+
},
|
|
834
865
|
template: false,
|
|
835
|
-
|
|
866
|
+
dueDate: range,
|
|
836
867
|
},
|
|
837
868
|
select: {
|
|
838
869
|
id: true,
|
|
@@ -869,7 +900,7 @@ export async function getMemberTimeline(
|
|
|
869
900
|
studentId: input.memberId,
|
|
870
901
|
attendance: {
|
|
871
902
|
class: { schoolId: input.schoolId },
|
|
872
|
-
|
|
903
|
+
date: range,
|
|
873
904
|
},
|
|
874
905
|
},
|
|
875
906
|
select: {
|
|
@@ -922,10 +953,16 @@ export async function getMemberTimeline(
|
|
|
922
953
|
prisma.event.findMany({
|
|
923
954
|
where: {
|
|
924
955
|
OR: [
|
|
925
|
-
{ userId: input.memberId },
|
|
926
|
-
{
|
|
956
|
+
{ userId: input.memberId, classId: null },
|
|
957
|
+
{ userId: input.memberId, class: { schoolId: input.schoolId } },
|
|
958
|
+
{
|
|
959
|
+
class: {
|
|
960
|
+
schoolId: input.schoolId,
|
|
961
|
+
teachers: { some: { id: input.memberId } },
|
|
962
|
+
},
|
|
963
|
+
},
|
|
927
964
|
],
|
|
928
|
-
|
|
965
|
+
startTime: range,
|
|
929
966
|
},
|
|
930
967
|
select: {
|
|
931
968
|
id: true,
|
|
@@ -11,9 +11,8 @@ import { randomBytes } from "crypto";
|
|
|
11
11
|
import { hash } from "bcryptjs";
|
|
12
12
|
import { TRPCError } from "@trpc/server";
|
|
13
13
|
import { prisma } from "../lib/prisma.js";
|
|
14
|
-
import {
|
|
15
|
-
import { findActiveSchoolAdminMembership
|
|
16
|
-
import { upsertUserProfile } from "../models/user.js";
|
|
14
|
+
import { findUserByEmail, findUserByUsername } from "../models/auth.js";
|
|
15
|
+
import { findActiveSchoolAdminMembership } from "../models/school.js";
|
|
17
16
|
import { generateUniqueUsername } from "../utils/generateUsername.js";
|
|
18
17
|
|
|
19
18
|
export type StudentCsvRowResult =
|
|
@@ -140,8 +139,7 @@ export async function importStudentsCsv(
|
|
|
140
139
|
for (let i = 0; i < rows.length; i += 1) {
|
|
141
140
|
const rowNum = i + 2; // 1-indexed + header
|
|
142
141
|
const row = rows[i]!;
|
|
143
|
-
const displayName =
|
|
144
|
-
row.displayname || row.name || row.fullname || row.fullname || "";
|
|
142
|
+
const displayName = row.displayname || row.name || row.fullname || "";
|
|
145
143
|
const requestedUsername = row.username || "";
|
|
146
144
|
const requestedEmail = (row.email || "").toLowerCase();
|
|
147
145
|
const requestedPassword = row.password || "";
|
|
@@ -215,29 +213,43 @@ export async function importStudentsCsv(
|
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
const hashed = await hash(password, 10);
|
|
218
|
-
const user = await
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
const user = await prisma.$transaction(async (tx) => {
|
|
217
|
+
const createdUser = await tx.user.create({
|
|
218
|
+
data: {
|
|
219
|
+
username,
|
|
220
|
+
email,
|
|
221
|
+
password: hashed,
|
|
222
|
+
verified: true,
|
|
223
|
+
profile: { create: displayName ? { displayName } : {} },
|
|
224
|
+
},
|
|
225
|
+
select: { id: true, username: true, email: true },
|
|
226
|
+
});
|
|
224
227
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
await tx.schoolMembership.upsert({
|
|
229
|
+
where: {
|
|
230
|
+
schoolId_userId: {
|
|
231
|
+
schoolId: input.schoolId,
|
|
232
|
+
userId: createdUser.id,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
update: { role: "STUDENT", status: "ACTIVE" },
|
|
236
|
+
create: {
|
|
237
|
+
schoolId: input.schoolId,
|
|
238
|
+
userId: createdUser.id,
|
|
239
|
+
role: "STUDENT",
|
|
240
|
+
status: "ACTIVE",
|
|
241
|
+
},
|
|
242
|
+
});
|
|
228
243
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
if (classId) {
|
|
245
|
+
await tx.class.update({
|
|
246
|
+
where: { id: classId },
|
|
247
|
+
data: { students: { connect: { id: createdUser.id } } },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
234
250
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
where: { id: classId },
|
|
238
|
-
data: { students: { connect: { id: user.id } } },
|
|
239
|
-
});
|
|
240
|
-
}
|
|
251
|
+
return createdUser;
|
|
252
|
+
});
|
|
241
253
|
|
|
242
254
|
results.push({
|
|
243
255
|
ok: true,
|
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
* AcademicTerm (C1) is not modelled yet — accept `from`/`to` date range and/or
|
|
9
9
|
* a free-text `schoolYear` string for the report header.
|
|
10
10
|
*
|
|
11
|
-
* Attendance is read
|
|
12
|
-
*
|
|
13
|
-
* refactor — rate math lives in attendanceRate.ts.
|
|
11
|
+
* Attendance is read from AttendanceRecord (EXCUSED excluded from the rate
|
|
12
|
+
* denominator) — rate math lives in attendanceRate.ts.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { TRPCError } from "@trpc/server";
|
|
@@ -93,30 +92,19 @@ async function fetchStudentAttendance(
|
|
|
93
92
|
};
|
|
94
93
|
const hasRange = Object.keys(range).length > 0;
|
|
95
94
|
|
|
96
|
-
//
|
|
97
|
-
|
|
95
|
+
// Read AttendanceRecord so EXCUSED records are excluded from the rate
|
|
96
|
+
// denominator (computeAttendanceRate ignores non-PRESENT/LATE/ABSENT).
|
|
97
|
+
const records = await prisma.attendanceRecord.findMany({
|
|
98
98
|
where: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{ absent: { some: { id: studentId } } },
|
|
103
|
-
],
|
|
104
|
-
...(hasRange ? { date: range } : {}),
|
|
105
|
-
},
|
|
106
|
-
select: {
|
|
107
|
-
id: true,
|
|
108
|
-
present: { where: { id: studentId }, select: { id: true } },
|
|
109
|
-
late: { where: { id: studentId }, select: { id: true } },
|
|
110
|
-
absent: { where: { id: studentId }, select: { id: true } },
|
|
99
|
+
studentId,
|
|
100
|
+
status: { in: ["PRESENT", "LATE", "ABSENT"] },
|
|
101
|
+
...(hasRange ? { attendance: { date: range } } : {}),
|
|
111
102
|
},
|
|
103
|
+
select: { status: true },
|
|
112
104
|
});
|
|
113
105
|
|
|
114
|
-
return
|
|
115
|
-
status: record.
|
|
116
|
-
? ("PRESENT" as const)
|
|
117
|
-
: record.late.length
|
|
118
|
-
? ("LATE" as const)
|
|
119
|
-
: ("ABSENT" as const),
|
|
106
|
+
return records.map((record) => ({
|
|
107
|
+
status: record.status as "PRESENT" | "LATE" | "ABSENT",
|
|
120
108
|
}));
|
|
121
109
|
}
|
|
122
110
|
|