@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.
Files changed (117) hide show
  1. package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +4 -4
  2. package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1 -1
  3. package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +4 -4
  4. package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +4 -4
  5. package/dist/models/attendance.d.ts.map +1 -1
  6. package/dist/models/attendance.js +17 -14
  7. package/dist/models/attendance.js.map +1 -1
  8. package/dist/models/marketplace.d.ts +37 -0
  9. package/dist/models/marketplace.d.ts.map +1 -1
  10. package/dist/models/marketplace.js +10 -2
  11. package/dist/models/marketplace.js.map +1 -1
  12. package/dist/models/module.d.ts +228 -0
  13. package/dist/models/module.d.ts.map +1 -0
  14. package/dist/models/module.js +69 -0
  15. package/dist/models/module.js.map +1 -0
  16. package/dist/models/moduleItem.d.ts +5 -1
  17. package/dist/models/moduleItem.d.ts.map +1 -1
  18. package/dist/models/moduleItem.js +19 -4
  19. package/dist/models/moduleItem.js.map +1 -1
  20. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  21. package/dist/pipelines/gradeWorksheet.js +4 -2
  22. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  23. package/dist/routers/_app.d.ts +768 -6
  24. package/dist/routers/_app.d.ts.map +1 -1
  25. package/dist/routers/_app.js +4 -2
  26. package/dist/routers/_app.js.map +1 -1
  27. package/dist/routers/fees.d.ts +1 -1
  28. package/dist/routers/marketplace.d.ts +41 -0
  29. package/dist/routers/marketplace.d.ts.map +1 -1
  30. package/dist/routers/marketplace.js +13 -3
  31. package/dist/routers/marketplace.js.map +1 -1
  32. package/dist/routers/module.d.ts +345 -0
  33. package/dist/routers/module.d.ts.map +1 -0
  34. package/dist/routers/module.js +82 -0
  35. package/dist/routers/module.js.map +1 -0
  36. package/dist/routers/moduleItem.d.ts.map +1 -1
  37. package/dist/routers/moduleItem.js +7 -6
  38. package/dist/routers/moduleItem.js.map +1 -1
  39. package/dist/routers/parent.d.ts +2 -1
  40. package/dist/routers/parent.d.ts.map +1 -1
  41. package/dist/routers/school.d.ts +1 -1
  42. package/dist/routers/school.d.ts.map +1 -1
  43. package/dist/routers/school.js +14 -4
  44. package/dist/routers/school.js.map +1 -1
  45. package/dist/services/academicCalendar.d.ts.map +1 -1
  46. package/dist/services/academicCalendar.js +30 -2
  47. package/dist/services/academicCalendar.js.map +1 -1
  48. package/dist/services/assignment.d.ts.map +1 -1
  49. package/dist/services/assignment.js +8 -3
  50. package/dist/services/assignment.js.map +1 -1
  51. package/dist/services/attendance.d.ts +6 -2
  52. package/dist/services/attendance.d.ts.map +1 -1
  53. package/dist/services/attendance.js +8 -4
  54. package/dist/services/attendance.js.map +1 -1
  55. package/dist/services/class.d.ts +14 -9
  56. package/dist/services/class.d.ts.map +1 -1
  57. package/dist/services/class.js +4 -4
  58. package/dist/services/class.js.map +1 -1
  59. package/dist/services/fees.d.ts.map +1 -1
  60. package/dist/services/fees.js +24 -4
  61. package/dist/services/fees.js.map +1 -1
  62. package/dist/services/feesAuthz.d.ts +2 -5
  63. package/dist/services/feesAuthz.d.ts.map +1 -1
  64. package/dist/services/feesAuthz.js +5 -8
  65. package/dist/services/feesAuthz.js.map +1 -1
  66. package/dist/services/marketplace.d.ts +36 -0
  67. package/dist/services/marketplace.d.ts.map +1 -1
  68. package/dist/services/marketplace.js +36 -6
  69. package/dist/services/marketplace.js.map +1 -1
  70. package/dist/services/module.d.ts +288 -0
  71. package/dist/services/module.d.ts.map +1 -0
  72. package/dist/services/module.js +518 -0
  73. package/dist/services/module.js.map +1 -0
  74. package/dist/services/moduleItem.d.ts.map +1 -1
  75. package/dist/services/moduleItem.js +23 -7
  76. package/dist/services/moduleItem.js.map +1 -1
  77. package/dist/services/parentPortal.d.ts +3 -5
  78. package/dist/services/parentPortal.d.ts.map +1 -1
  79. package/dist/services/parentPortal.js +29 -43
  80. package/dist/services/parentPortal.js.map +1 -1
  81. package/dist/services/school.d.ts.map +1 -1
  82. package/dist/services/school.js +52 -16
  83. package/dist/services/school.js.map +1 -1
  84. package/dist/services/studentImport.d.ts.map +1 -1
  85. package/dist/services/studentImport.js +38 -25
  86. package/dist/services/studentImport.js.map +1 -1
  87. package/dist/services/transcript.d.ts +2 -3
  88. package/dist/services/transcript.d.ts.map +1 -1
  89. package/dist/services/transcript.js +13 -25
  90. package/dist/services/transcript.js.map +1 -1
  91. package/package.json +1 -1
  92. package/prisma/migrations/20260708013000_add_modules/migration.sql +100 -0
  93. package/prisma/schema.prisma +93 -0
  94. package/scripts/import-nepal-cdc-curriculum.ts +29 -6
  95. package/src/models/attendance.ts +10 -12
  96. package/src/models/marketplace.ts +19 -0
  97. package/src/models/module.ts +77 -0
  98. package/src/models/moduleItem.ts +20 -2
  99. package/src/pipelines/gradeWorksheet.ts +6 -0
  100. package/src/routers/_app.ts +2 -0
  101. package/src/routers/marketplace.ts +16 -0
  102. package/src/routers/module.ts +118 -0
  103. package/src/routers/moduleItem.ts +6 -4
  104. package/src/routers/school.ts +16 -6
  105. package/src/services/academicCalendar.ts +28 -0
  106. package/src/services/assignment.ts +12 -1
  107. package/src/services/attendance.ts +7 -3
  108. package/src/services/class.ts +2 -2
  109. package/src/services/fees.ts +25 -2
  110. package/src/services/feesAuthz.ts +3 -7
  111. package/src/services/marketplace.ts +47 -3
  112. package/src/services/module.ts +683 -0
  113. package/src/services/moduleItem.ts +28 -4
  114. package/src/services/parentPortal.ts +26 -39
  115. package/src/services/school.ts +52 -15
  116. package/src/services/studentImport.ts +37 -25
  117. 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 attendance = await prisma.attendance.findMany({
234
+ const attendanceRecords = await prisma.attendanceRecord.findMany({
238
235
  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
- ],
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
- 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 } },
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 = 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
- });
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 };
@@ -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
- ...(input.from ? { gte: input.from } : {}),
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
- ...(hasRange ? { startTime: range } : {}),
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
- ...(hasRange ? { dueDate: range } : {}),
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
- ...(hasRange ? { date: range } : {}),
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
- ...(input.from ? { gte: input.from } : {}),
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
- ...(hasRange ? { startTime: range } : {}),
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: { students: { some: { id: input.memberId } } },
861
+ class: {
862
+ schoolId: input.schoolId,
863
+ students: { some: { id: input.memberId } },
864
+ },
834
865
  template: false,
835
- ...(hasRange ? { dueDate: range } : {}),
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
- ...(hasRange ? { date: range } : {}),
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
- { class: { teachers: { some: { id: input.memberId } } } },
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
- ...(hasRange ? { startTime: range } : {}),
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 { createUser, findUserByEmail, findUserByUsername } from "../models/auth.js";
15
- import { findActiveSchoolAdminMembership, upsertSchoolMembership } from "../models/school.js";
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 createUser({
219
- username,
220
- email,
221
- password: hashed,
222
- verified: true,
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
- if (displayName) {
226
- await upsertUserProfile(user.id, { displayName });
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
- await upsertSchoolMembership({
230
- schoolId: input.schoolId,
231
- userId: user.id,
232
- role: "STUDENT",
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
- if (classId) {
236
- await prisma.class.update({
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 via the existing present/late/absent M2M lists only
12
- * (same shaping as getStudentTimeline). Do not conflict with A1 Attendance
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
- // Same M2M present/late/absent read path as getStudentTimeline (A1-safe).
97
- const attendance = await prisma.attendance.findMany({
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
- OR: [
100
- { present: { some: { id: studentId } } },
101
- { late: { some: { id: studentId } } },
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 attendance.map((record) => ({
115
- status: record.present.length
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