@studious-lms/server 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -1
- package/data/curriculum/nepal-cdc-g4-g5/README.md +90 -0
- package/data/curriculum/nepal-cdc-g4-g5/manifest.json +97 -0
- package/data/curriculum/nepal-cdc-g4-g5/proficiency-scale.json +76 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +6936 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/health-physical-creative-arts.json +3401 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1511 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/nepali.json +7928 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +2420 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +2177 -0
- package/dist/lib/assignmentTypeDefaults.d.ts +41 -0
- package/dist/lib/assignmentTypeDefaults.d.ts.map +1 -0
- package/dist/lib/assignmentTypeDefaults.js +167 -0
- package/dist/lib/assignmentTypeDefaults.js.map +1 -0
- package/dist/lib/config/env.d.ts +0 -8
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +2 -5
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/feePdf.d.ts +19 -0
- package/dist/lib/feePdf.d.ts.map +1 -0
- package/dist/lib/feePdf.js +167 -0
- package/dist/lib/feePdf.js.map +1 -0
- package/dist/lib/feePdfFormat.d.ts +4 -0
- package/dist/lib/feePdfFormat.d.ts.map +1 -0
- package/dist/lib/feePdfFormat.js +16 -0
- package/dist/lib/feePdfFormat.js.map +1 -0
- package/dist/lib/idCardPdf.d.ts +26 -0
- package/dist/lib/idCardPdf.d.ts.map +1 -0
- package/dist/lib/idCardPdf.js +272 -0
- package/dist/lib/idCardPdf.js.map +1 -0
- package/dist/lib/pdfLetterhead.d.ts +41 -0
- package/dist/lib/pdfLetterhead.d.ts.map +1 -0
- package/dist/lib/pdfLetterhead.js +86 -0
- package/dist/lib/pdfLetterhead.js.map +1 -0
- package/dist/lib/transcriptPdf.d.ts +55 -0
- package/dist/lib/transcriptPdf.d.ts.map +1 -0
- package/dist/lib/transcriptPdf.js +303 -0
- package/dist/lib/transcriptPdf.js.map +1 -0
- package/dist/models/academicCalendar.d.ts +198 -0
- package/dist/models/academicCalendar.d.ts.map +1 -0
- package/dist/models/academicCalendar.js +106 -0
- package/dist/models/academicCalendar.js.map +1 -0
- package/dist/models/agenda.d.ts +8 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +2 -0
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +66 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +659 -21
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +544 -75
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +2 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +12 -0
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +3 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/event.d.ts +16 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/fees.d.ts +554 -0
- package/dist/models/fees.d.ts.map +1 -0
- package/dist/models/fees.js +278 -0
- package/dist/models/fees.js.map +1 -0
- package/dist/models/file.d.ts +29 -0
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +2 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +2 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketplace.d.ts +178 -0
- package/dist/models/marketplace.d.ts.map +1 -0
- package/dist/models/marketplace.js +115 -0
- package/dist/models/marketplace.js.map +1 -0
- package/dist/models/moduleItem.d.ts +304 -0
- package/dist/models/moduleItem.d.ts.map +1 -0
- package/dist/models/moduleItem.js +78 -0
- package/dist/models/moduleItem.js.map +1 -0
- package/dist/models/newtonChat.d.ts +1 -0
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +18 -0
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +7 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +4 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +19 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +11341 -7097
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +12 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/academicCalendar.d.ts +229 -0
- package/dist/routers/academicCalendar.d.ts.map +1 -0
- package/dist/routers/academicCalendar.js +75 -0
- package/dist/routers/academicCalendar.js.map +1 -0
- package/dist/routers/admin.d.ts +2 -0
- package/dist/routers/admin.d.ts.map +1 -1
- package/dist/routers/agenda.d.ts +8 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +75 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +10 -2
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +103 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +8 -5
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/class.d.ts +27 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +6 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/event.d.ts +8 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/fees.d.ts +513 -0
- package/dist/routers/fees.d.ts.map +1 -0
- package/dist/routers/fees.js +88 -0
- package/dist/routers/fees.js.map +1 -0
- package/dist/routers/file.d.ts +2 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/gradebook.d.ts +4 -1
- package/dist/routers/gradebook.d.ts.map +1 -1
- package/dist/routers/marketplace.d.ts +371 -0
- package/dist/routers/marketplace.d.ts.map +1 -0
- package/dist/routers/marketplace.js +87 -0
- package/dist/routers/marketplace.js.map +1 -0
- package/dist/routers/moduleItem.d.ts +263 -0
- package/dist/routers/moduleItem.d.ts.map +1 -0
- package/dist/routers/moduleItem.js +60 -0
- package/dist/routers/moduleItem.js.map +1 -0
- package/dist/routers/parent.d.ts +169 -0
- package/dist/routers/parent.d.ts.map +1 -0
- package/dist/routers/parent.js +49 -0
- package/dist/routers/parent.js.map +1 -0
- package/dist/routers/school.d.ts +363 -7
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +100 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/worksheet.d.ts +27 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +10 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/academicCalendar.d.ts +173 -0
- package/dist/services/academicCalendar.d.ts.map +1 -0
- package/dist/services/academicCalendar.js +214 -0
- package/dist/services/academicCalendar.js.map +1 -0
- package/dist/services/adminInstitution.d.ts +2 -0
- package/dist/services/adminInstitution.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +8 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +75 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +91 -12
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +323 -10
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +385 -25
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/attendanceMerge.d.ts +53 -0
- package/dist/services/attendanceMerge.d.ts.map +1 -0
- package/dist/services/attendanceMerge.js +87 -0
- package/dist/services/attendanceMerge.js.map +1 -0
- package/dist/services/attendanceRate.d.ts +24 -0
- package/dist/services/attendanceRate.d.ts.map +1 -0
- package/dist/services/attendanceRate.js +31 -0
- package/dist/services/attendanceRate.js.map +1 -0
- package/dist/services/attendanceStats.d.ts +22 -0
- package/dist/services/attendanceStats.d.ts.map +1 -0
- package/dist/services/attendanceStats.js +45 -0
- package/dist/services/attendanceStats.js.map +1 -0
- package/dist/services/class.d.ts +27 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +26 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/event.d.ts +8 -0
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/fees.d.ts +422 -0
- package/dist/services/fees.d.ts.map +1 -0
- package/dist/services/fees.js +310 -0
- package/dist/services/fees.js.map +1 -0
- package/dist/services/feesAuthz.d.ts +49 -0
- package/dist/services/feesAuthz.d.ts.map +1 -0
- package/dist/services/feesAuthz.js +52 -0
- package/dist/services/feesAuthz.js.map +1 -0
- package/dist/services/file.d.ts +4 -0
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/gradebook.d.ts +18 -1
- package/dist/services/gradebook.d.ts.map +1 -1
- package/dist/services/gradebook.js +57 -2
- package/dist/services/gradebook.js.map +1 -1
- package/dist/services/idCard.d.ts +11 -0
- package/dist/services/idCard.d.ts.map +1 -0
- package/dist/services/idCard.js +122 -0
- package/dist/services/idCard.js.map +1 -0
- package/dist/services/marketplace.d.ts +301 -0
- package/dist/services/marketplace.d.ts.map +1 -0
- package/dist/services/marketplace.js +628 -0
- package/dist/services/marketplace.js.map +1 -0
- package/dist/services/moduleItem.d.ts +196 -0
- package/dist/services/moduleItem.d.ts.map +1 -0
- package/dist/services/moduleItem.js +188 -0
- package/dist/services/moduleItem.js.map +1 -0
- package/dist/services/notification.d.ts +58 -0
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/notification.js +30 -2
- package/dist/services/notification.js.map +1 -1
- package/dist/services/parentPortal.d.ts +133 -0
- package/dist/services/parentPortal.d.ts.map +1 -0
- package/dist/services/parentPortal.js +328 -0
- package/dist/services/parentPortal.js.map +1 -0
- package/dist/services/school.d.ts +28 -3
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +65 -45
- package/dist/services/school.js.map +1 -1
- package/dist/services/studentImport.d.ts +34 -0
- package/dist/services/studentImport.d.ts.map +1 -0
- package/dist/services/studentImport.js +236 -0
- package/dist/services/studentImport.js.map +1 -0
- package/dist/services/transcript.d.ts +61 -0
- package/dist/services/transcript.d.ts.map +1 -0
- package/dist/services/transcript.js +243 -0
- package/dist/services/transcript.js.map +1 -0
- package/dist/services/worksheet.d.ts +22 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +94 -2
- package/dist/services/worksheet.js.map +1 -1
- package/dist/trpc.d.ts +1 -1
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +5 -5
- package/dist/trpc.js.map +1 -1
- package/dist/utils/adminPanelAccess.d.ts +4 -5
- package/dist/utils/adminPanelAccess.d.ts.map +1 -1
- package/dist/utils/adminPanelAccess.js +12 -34
- package/dist/utils/adminPanelAccess.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260804120000_add_attendance_records_and_excusals/migration.sql +114 -0
- package/prisma/migrations/20260804120100_add_fee_plans_invoices_payments/migration.sql +101 -0
- package/prisma/migrations/20260804120200_add_marketplace_listings/migration.sql +54 -0
- package/prisma/migrations/20260804120300_content_platform_b3_b9/migration.sql +63 -0
- package/prisma/migrations/20260804120400_c_academic_ops/migration.sql +77 -0
- package/prisma/schema.prisma +371 -11
- package/scripts/import-nepal-cdc-curriculum.ts +379 -0
- package/src/lib/assignmentTypeDefaults.ts +222 -0
- package/src/lib/config/env.ts +0 -3
- package/src/lib/feePdf.ts +199 -0
- package/src/lib/feePdfFormat.ts +14 -0
- package/src/lib/idCardPdf.ts +336 -0
- package/src/lib/pdfLetterhead.ts +124 -0
- package/src/lib/transcriptPdf.ts +502 -0
- package/src/models/academicCalendar.ts +139 -0
- package/src/models/attendance.ts +704 -73
- package/src/models/class.ts +1 -0
- package/src/models/fees.ts +349 -0
- package/src/models/marketplace.ts +158 -0
- package/src/models/moduleItem.ts +107 -0
- package/src/models/school.ts +6 -0
- package/src/models/worksheet.ts +2 -0
- package/src/routers/_app.ts +10 -0
- package/src/routers/academicCalendar.ts +103 -0
- package/src/routers/assignment.ts +8 -0
- package/src/routers/attendance.ts +7 -3
- package/src/routers/class.ts +4 -0
- package/src/routers/fees.ts +126 -0
- package/src/routers/marketplace.ts +134 -0
- package/src/routers/moduleItem.ts +89 -0
- package/src/routers/parent.ts +68 -0
- package/src/routers/school.ts +139 -1
- package/src/routers/worksheet.ts +11 -0
- package/src/services/academicCalendar.ts +299 -0
- package/src/services/assignment.ts +122 -12
- package/src/services/attendance.ts +548 -26
- package/src/services/attendanceMerge.ts +139 -0
- package/src/services/attendanceRate.ts +41 -0
- package/src/services/attendanceStats.ts +63 -0
- package/src/services/class.ts +25 -1
- package/src/services/fees.ts +424 -0
- package/src/services/feesAuthz.ts +77 -0
- package/src/services/gradebook.ts +70 -0
- package/src/services/idCard.ts +147 -0
- package/src/services/marketplace.ts +785 -0
- package/src/services/moduleItem.ts +274 -0
- package/src/services/notification.ts +43 -0
- package/src/services/parentPortal.ts +380 -0
- package/src/services/school.ts +65 -44
- package/src/services/studentImport.ts +273 -0
- package/src/services/transcript.ts +313 -0
- package/src/services/worksheet.ts +111 -0
- package/src/trpc.ts +3 -3
- package/src/utils/adminPanelAccess.ts +12 -35
- package/tests/unit/assignmentTypeDefaults.test.ts +52 -0
- package/tests/unit/lib/feePdf.test.ts +15 -0
- package/tests/unit/lib/idCardPdf.test.ts +40 -0
- package/tests/unit/lib/transcriptPdf.test.ts +111 -0
- package/tests/unit/services/attendanceMerge.test.ts +90 -0
- package/tests/unit/services/attendanceRate.test.ts +38 -0
- package/tests/unit/services/attendanceStats.test.ts +54 -0
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Attendance service –
|
|
3
|
-
*
|
|
2
|
+
* Attendance service – class attendance + school-admin excusals / stats.
|
|
3
|
+
* Dual-writes legacy present/late/absent lists and per-student AttendanceRecord.
|
|
4
|
+
* Active StudentExcusal rows force EXCUSED (locked for teachers).
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="61646503-b6ff-5d4c-8251-5ecf346dad65")}catch(e){}}();
|
|
7
8
|
import { TRPCError } from "@trpc/server";
|
|
8
|
-
import { findClassWithStudents, findClassWithTeacher, findEventsByClassId, findAttendanceByEventId, findAttendanceByClassAndEvent, findManyAttendance, createAttendance, createAttendanceForEvent, updateAttendance, } from "../models/attendance.js";
|
|
9
|
+
import { findClassWithStudents, findClassWithTeacher, findEventsByClassId, findAttendanceByEventId, findAttendanceByClassAndEvent, findManyAttendance, createAttendance, createAttendanceForEvent, updateAttendance, findActiveExcusalsForStudents, listExcusalsForSchool, findExcusalById, createStudentExcusal, updateStudentExcusal, revokeStudentExcusal, applyExcusalToAttendance, clearExcusalLocks, listSchoolStudentMemberships, listAttendanceRecordsForSchoolStudents, listTodaySchoolAttendanceSessions, toDateOnly, startOfUtcDay, endOfUtcDay, } from "../models/attendance.js";
|
|
10
|
+
import { findActiveSchoolAdminMembership } from "../models/school.js";
|
|
9
11
|
import { sendToMultiple } from "./notification.js";
|
|
10
12
|
import { logger } from "../utils/logger.js";
|
|
13
|
+
import { buildExcusalLocks, mergeAttendanceUpdateWithLocks, } from "./attendanceMerge.js";
|
|
14
|
+
import { computeAttendancePercentage, emptyAttendanceCounts, tallyAttendanceStatuses, } from "./attendanceStats.js";
|
|
15
|
+
async function requireSchoolAdmin(schoolId, userId) {
|
|
16
|
+
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
17
|
+
if (!membership) {
|
|
18
|
+
throw new TRPCError({
|
|
19
|
+
code: "FORBIDDEN",
|
|
20
|
+
message: "Only school admins can perform this action",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return membership;
|
|
24
|
+
}
|
|
25
|
+
function sessionDate(attendance) {
|
|
26
|
+
return attendance.event?.startTime ?? attendance.date;
|
|
27
|
+
}
|
|
11
28
|
/** Get attendance for a class (optionally filtered by event). Creates missing records. */
|
|
12
29
|
export async function getAttendance(classId, eventId) {
|
|
13
30
|
const classData = await findClassWithStudents(classId);
|
|
@@ -22,16 +39,83 @@ export async function getAttendance(classId, eventId) {
|
|
|
22
39
|
for (const event of events) {
|
|
23
40
|
const existing = await findAttendanceByEventId(event.id);
|
|
24
41
|
if (!existing) {
|
|
42
|
+
const locks = classData.schoolId && studentIds.length > 0
|
|
43
|
+
? buildExcusalLocks(await findActiveExcusalsForStudents({
|
|
44
|
+
schoolId: classData.schoolId,
|
|
45
|
+
studentIds,
|
|
46
|
+
date: event.startTime,
|
|
47
|
+
}), studentIds)
|
|
48
|
+
: [];
|
|
49
|
+
const merged = mergeAttendanceUpdateWithLocks({
|
|
50
|
+
present: classData.students.map((s) => ({
|
|
51
|
+
id: s.id,
|
|
52
|
+
username: s.username,
|
|
53
|
+
})),
|
|
54
|
+
late: [],
|
|
55
|
+
absent: [],
|
|
56
|
+
locks,
|
|
57
|
+
});
|
|
25
58
|
await createAttendanceForEvent({
|
|
26
59
|
classId,
|
|
27
60
|
eventId: event.id,
|
|
28
|
-
|
|
61
|
+
date: event.startTime,
|
|
62
|
+
presentIds: merged.presentIds,
|
|
63
|
+
lateIds: merged.lateIds,
|
|
64
|
+
absentIds: merged.absentIds,
|
|
65
|
+
excusedIds: merged.excusedIds,
|
|
66
|
+
records: merged.records,
|
|
29
67
|
});
|
|
30
68
|
}
|
|
69
|
+
else if (classData.schoolId && studentIds.length > 0) {
|
|
70
|
+
// Ensure active excusals are reflected on existing sessions.
|
|
71
|
+
const locks = buildExcusalLocks(await findActiveExcusalsForStudents({
|
|
72
|
+
schoolId: classData.schoolId,
|
|
73
|
+
studentIds,
|
|
74
|
+
date: event.startTime,
|
|
75
|
+
}), studentIds);
|
|
76
|
+
const needsLock = locks.filter((lock) => {
|
|
77
|
+
const current = existing.records.find((r) => r.studentId === lock.studentId);
|
|
78
|
+
return !(current?.status === "EXCUSED" &&
|
|
79
|
+
current.excusedByExcusalId === lock.excusalId);
|
|
80
|
+
});
|
|
81
|
+
const byExcusal = new Map();
|
|
82
|
+
for (const lock of needsLock) {
|
|
83
|
+
const list = byExcusal.get(lock.excusalId) ?? [];
|
|
84
|
+
list.push(lock.studentId);
|
|
85
|
+
byExcusal.set(lock.excusalId, list);
|
|
86
|
+
}
|
|
87
|
+
for (const [excusalId, lockedStudentIds] of byExcusal) {
|
|
88
|
+
await applyExcusalToAttendance({
|
|
89
|
+
schoolId: classData.schoolId,
|
|
90
|
+
excusalId,
|
|
91
|
+
studentIds: lockedStudentIds,
|
|
92
|
+
startDate: event.startTime,
|
|
93
|
+
endDate: event.startTime,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
31
97
|
}
|
|
32
|
-
|
|
98
|
+
const sessions = await findManyAttendance(classId, eventId);
|
|
99
|
+
return sessions.map(shapeAttendanceSession);
|
|
100
|
+
}
|
|
101
|
+
/** Additive shape: keep present/late/absent; expose excused + lock metadata for UI. */
|
|
102
|
+
function shapeAttendanceSession(session) {
|
|
103
|
+
const records = session.records.map((r) => ({
|
|
104
|
+
...r,
|
|
105
|
+
lockedFromExcusal: Boolean(r.excusedByExcusalId),
|
|
106
|
+
reason: r.excusedByExcusal?.reason ?? null,
|
|
107
|
+
excusalNote: r.excusedByExcusal?.note ?? null,
|
|
108
|
+
}));
|
|
109
|
+
const excused = records
|
|
110
|
+
.filter((r) => r.status === "EXCUSED")
|
|
111
|
+
.map((r) => r.student);
|
|
112
|
+
return {
|
|
113
|
+
...session,
|
|
114
|
+
excused,
|
|
115
|
+
records,
|
|
116
|
+
};
|
|
33
117
|
}
|
|
34
|
-
/** Update attendance record (present/late/absent). Teacher-only. */
|
|
118
|
+
/** Update attendance record (present/late/absent/excused). Teacher-only. */
|
|
35
119
|
export async function updateAttendanceRecord(userId, classId, eventId, attendance) {
|
|
36
120
|
const classData = await findClassWithTeacher(classId, userId);
|
|
37
121
|
if (!classData) {
|
|
@@ -41,51 +125,325 @@ export async function updateAttendanceRecord(userId, classId, eventId, attendanc
|
|
|
41
125
|
});
|
|
42
126
|
}
|
|
43
127
|
const existing = await findAttendanceByClassAndEvent(classId, eventId ?? null);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
128
|
+
const dateForExcusal = existing
|
|
129
|
+
? sessionDate(existing)
|
|
130
|
+
: new Date();
|
|
131
|
+
const classStudentIds = classData.students.map((s) => s.id);
|
|
132
|
+
const locks = classData.schoolId && classStudentIds.length > 0
|
|
133
|
+
? buildExcusalLocks(await findActiveExcusalsForStudents({
|
|
134
|
+
schoolId: classData.schoolId,
|
|
135
|
+
studentIds: classStudentIds,
|
|
136
|
+
date: dateForExcusal,
|
|
137
|
+
}), classStudentIds)
|
|
138
|
+
: [];
|
|
139
|
+
const existingNotes = new Map((existing?.records ?? []).map((r) => [r.studentId, r.note]));
|
|
140
|
+
const merged = mergeAttendanceUpdateWithLocks({
|
|
141
|
+
present: attendance.present,
|
|
142
|
+
late: attendance.late,
|
|
143
|
+
absent: attendance.absent,
|
|
144
|
+
excused: attendance.excused,
|
|
145
|
+
locks,
|
|
146
|
+
updatedById: userId,
|
|
147
|
+
existingNotesByStudentId: existingNotes,
|
|
148
|
+
});
|
|
47
149
|
const previousLateIds = new Set(existing?.late.map((student) => student.id) ?? []);
|
|
48
150
|
const previousAbsentIds = new Set(existing?.absent.map((student) => student.id) ?? []);
|
|
49
|
-
const
|
|
151
|
+
const classStudentIdSet = new Set(classStudentIds);
|
|
50
152
|
if (!existing) {
|
|
51
153
|
const created = await createAttendance({
|
|
52
154
|
classId,
|
|
53
155
|
eventId: eventId ?? null,
|
|
54
|
-
presentIds,
|
|
55
|
-
lateIds,
|
|
56
|
-
absentIds,
|
|
156
|
+
presentIds: merged.presentIds,
|
|
157
|
+
lateIds: merged.lateIds,
|
|
158
|
+
absentIds: merged.absentIds,
|
|
159
|
+
excusedIds: merged.excusedIds,
|
|
160
|
+
records: merged.records,
|
|
161
|
+
updatedById: userId,
|
|
57
162
|
});
|
|
58
163
|
await notifyMarkedLateOrAbsent({
|
|
59
164
|
classId,
|
|
60
165
|
className: classData.name,
|
|
61
166
|
eventId: eventId ?? null,
|
|
62
167
|
eventName: created.event?.name ?? null,
|
|
63
|
-
classStudentIds,
|
|
64
|
-
lateIds,
|
|
65
|
-
absentIds,
|
|
168
|
+
classStudentIds: classStudentIdSet,
|
|
169
|
+
lateIds: merged.lateIds,
|
|
170
|
+
absentIds: merged.absentIds,
|
|
66
171
|
previousLateIds,
|
|
67
172
|
previousAbsentIds,
|
|
68
173
|
});
|
|
69
|
-
return created;
|
|
174
|
+
return shapeAttendanceSession(created);
|
|
70
175
|
}
|
|
71
176
|
const updated = await updateAttendance(existing.id, {
|
|
72
|
-
presentIds,
|
|
73
|
-
lateIds,
|
|
74
|
-
absentIds,
|
|
177
|
+
presentIds: merged.presentIds,
|
|
178
|
+
lateIds: merged.lateIds,
|
|
179
|
+
absentIds: merged.absentIds,
|
|
180
|
+
records: merged.records,
|
|
75
181
|
});
|
|
76
182
|
await notifyMarkedLateOrAbsent({
|
|
77
183
|
classId,
|
|
78
184
|
className: classData.name,
|
|
79
185
|
eventId: eventId ?? null,
|
|
80
186
|
eventName: existing.event?.name ?? null,
|
|
81
|
-
classStudentIds,
|
|
82
|
-
lateIds,
|
|
83
|
-
absentIds,
|
|
187
|
+
classStudentIds: classStudentIdSet,
|
|
188
|
+
lateIds: merged.lateIds,
|
|
189
|
+
absentIds: merged.absentIds,
|
|
84
190
|
previousLateIds,
|
|
85
191
|
previousAbsentIds,
|
|
86
192
|
});
|
|
193
|
+
return shapeAttendanceSession(updated);
|
|
194
|
+
}
|
|
195
|
+
/** School-admin: today's attendance rollup across classes. */
|
|
196
|
+
export async function listTodayAttendanceSummary(userId, schoolId) {
|
|
197
|
+
await requireSchoolAdmin(schoolId, userId);
|
|
198
|
+
const today = new Date();
|
|
199
|
+
const sessions = await listTodaySchoolAttendanceSessions(schoolId, today);
|
|
200
|
+
const memberships = await listSchoolStudentMemberships(schoolId);
|
|
201
|
+
const totalStudents = memberships.length;
|
|
202
|
+
const counts = emptyAttendanceCounts();
|
|
203
|
+
const rows = [];
|
|
204
|
+
for (const session of sessions) {
|
|
205
|
+
const tallied = tallyAttendanceStatuses(session.records.map((r) => r.status));
|
|
206
|
+
counts.present += tallied.present;
|
|
207
|
+
counts.late += tallied.late;
|
|
208
|
+
counts.absent += tallied.absent;
|
|
209
|
+
counts.excused += tallied.excused;
|
|
210
|
+
const classStudentCount = session.class._count.students;
|
|
211
|
+
const marked = tallied.present + tallied.late + tallied.absent + tallied.excused;
|
|
212
|
+
rows.push({
|
|
213
|
+
attendanceId: session.id,
|
|
214
|
+
classId: session.classId,
|
|
215
|
+
className: session.class.name,
|
|
216
|
+
section: session.class.section,
|
|
217
|
+
subject: session.class.subject,
|
|
218
|
+
eventId: session.eventId,
|
|
219
|
+
eventName: session.event?.name ?? null,
|
|
220
|
+
date: session.event?.startTime ?? session.date,
|
|
221
|
+
present: tallied.present,
|
|
222
|
+
late: tallied.late,
|
|
223
|
+
absent: tallied.absent,
|
|
224
|
+
excused: tallied.excused,
|
|
225
|
+
unmarked: Math.max(0, classStudentCount - marked),
|
|
226
|
+
classStudentCount,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
date: toDateOnly(today),
|
|
231
|
+
totalStudents,
|
|
232
|
+
totals: counts,
|
|
233
|
+
percentage: computeAttendancePercentage(counts),
|
|
234
|
+
sessions: rows,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** School-admin: per-student attendance counts + % (excused excluded from %). */
|
|
238
|
+
export async function listStudentAttendanceStats(userId, input) {
|
|
239
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
240
|
+
const memberships = await listSchoolStudentMemberships(input.schoolId);
|
|
241
|
+
const filtered = input.studentId
|
|
242
|
+
? memberships.filter((m) => m.userId === input.studentId)
|
|
243
|
+
: memberships;
|
|
244
|
+
const studentIds = filtered.map((m) => m.userId);
|
|
245
|
+
const records = await listAttendanceRecordsForSchoolStudents({
|
|
246
|
+
schoolId: input.schoolId,
|
|
247
|
+
studentIds,
|
|
248
|
+
from: input.from,
|
|
249
|
+
to: input.to,
|
|
250
|
+
});
|
|
251
|
+
const byStudent = new Map();
|
|
252
|
+
for (const id of studentIds) {
|
|
253
|
+
byStudent.set(id, emptyAttendanceCounts());
|
|
254
|
+
}
|
|
255
|
+
for (const record of records) {
|
|
256
|
+
const counts = byStudent.get(record.studentId);
|
|
257
|
+
if (!counts)
|
|
258
|
+
continue;
|
|
259
|
+
switch (record.status) {
|
|
260
|
+
case "PRESENT":
|
|
261
|
+
counts.present += 1;
|
|
262
|
+
break;
|
|
263
|
+
case "LATE":
|
|
264
|
+
counts.late += 1;
|
|
265
|
+
break;
|
|
266
|
+
case "ABSENT":
|
|
267
|
+
counts.absent += 1;
|
|
268
|
+
break;
|
|
269
|
+
case "EXCUSED":
|
|
270
|
+
counts.excused += 1;
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return filtered.map((membership) => {
|
|
277
|
+
const counts = byStudent.get(membership.userId) ?? emptyAttendanceCounts();
|
|
278
|
+
return {
|
|
279
|
+
studentId: membership.userId,
|
|
280
|
+
username: membership.user.username,
|
|
281
|
+
displayName: membership.user.profile?.displayName ?? null,
|
|
282
|
+
profilePicture: membership.user.profile?.profilePicture ?? null,
|
|
283
|
+
profilePictureThumbnail: membership.user.profile?.profilePictureThumbnail ?? null,
|
|
284
|
+
classes: membership.user.studentIn,
|
|
285
|
+
present: counts.present,
|
|
286
|
+
late: counts.late,
|
|
287
|
+
absent: counts.absent,
|
|
288
|
+
excused: counts.excused,
|
|
289
|
+
attendancePercentage: computeAttendancePercentage(counts),
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
export async function listExcusals(userId, input) {
|
|
294
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
295
|
+
const excusals = await listExcusalsForSchool(input);
|
|
296
|
+
const now = toDateOnly(new Date());
|
|
297
|
+
return excusals.map((excusal) => {
|
|
298
|
+
let status = "ACTIVE";
|
|
299
|
+
if (excusal.revokedAt) {
|
|
300
|
+
status = "REVOKED";
|
|
301
|
+
}
|
|
302
|
+
else if (toDateOnly(excusal.startDate) > now) {
|
|
303
|
+
status = "UPCOMING";
|
|
304
|
+
}
|
|
305
|
+
else if (toDateOnly(excusal.endDate) < now) {
|
|
306
|
+
status = "ACTIVE"; // past but not revoked — still listed as ACTIVE historically
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
id: excusal.id,
|
|
310
|
+
schoolId: excusal.schoolId,
|
|
311
|
+
startDate: excusal.startDate,
|
|
312
|
+
endDate: excusal.endDate,
|
|
313
|
+
reason: excusal.reason,
|
|
314
|
+
note: excusal.note,
|
|
315
|
+
revokedAt: excusal.revokedAt,
|
|
316
|
+
createdAt: excusal.createdAt,
|
|
317
|
+
updatedAt: excusal.updatedAt,
|
|
318
|
+
status,
|
|
319
|
+
studentIds: excusal.students.map((s) => s.studentId),
|
|
320
|
+
students: excusal.students.map((s) => ({
|
|
321
|
+
id: s.student.id,
|
|
322
|
+
username: s.student.username,
|
|
323
|
+
displayName: s.student.profile?.displayName ?? null,
|
|
324
|
+
profilePicture: s.student.profile?.profilePicture ?? null,
|
|
325
|
+
profilePictureThumbnail: s.student.profile?.profilePictureThumbnail ?? null,
|
|
326
|
+
})),
|
|
327
|
+
createdBy: {
|
|
328
|
+
id: excusal.createdBy.id,
|
|
329
|
+
username: excusal.createdBy.username,
|
|
330
|
+
displayName: excusal.createdBy.profile?.displayName ?? null,
|
|
331
|
+
},
|
|
332
|
+
revokedBy: excusal.revokedBy
|
|
333
|
+
? {
|
|
334
|
+
id: excusal.revokedBy.id,
|
|
335
|
+
username: excusal.revokedBy.username,
|
|
336
|
+
displayName: excusal.revokedBy.profile?.displayName ?? null,
|
|
337
|
+
}
|
|
338
|
+
: null,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
export async function createExcusal(userId, input) {
|
|
343
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
344
|
+
if (toDateOnly(input.endDate) < toDateOnly(input.startDate)) {
|
|
345
|
+
throw new TRPCError({
|
|
346
|
+
code: "BAD_REQUEST",
|
|
347
|
+
message: "endDate must be on or after startDate",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const uniqueStudentIds = [...new Set(input.studentIds)];
|
|
351
|
+
const memberships = await listSchoolStudentMemberships(input.schoolId);
|
|
352
|
+
const schoolStudentIds = new Set(memberships.map((m) => m.userId));
|
|
353
|
+
const invalid = uniqueStudentIds.filter((id) => !schoolStudentIds.has(id));
|
|
354
|
+
if (invalid.length > 0) {
|
|
355
|
+
throw new TRPCError({
|
|
356
|
+
code: "BAD_REQUEST",
|
|
357
|
+
message: "One or more students are not active members of this school",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const excusal = await createStudentExcusal({
|
|
361
|
+
schoolId: input.schoolId,
|
|
362
|
+
studentIds: uniqueStudentIds,
|
|
363
|
+
startDate: input.startDate,
|
|
364
|
+
endDate: input.endDate,
|
|
365
|
+
reason: input.reason,
|
|
366
|
+
note: input.note,
|
|
367
|
+
createdById: userId,
|
|
368
|
+
});
|
|
369
|
+
await applyExcusalToAttendance({
|
|
370
|
+
schoolId: input.schoolId,
|
|
371
|
+
excusalId: excusal.id,
|
|
372
|
+
studentIds: uniqueStudentIds,
|
|
373
|
+
startDate: input.startDate,
|
|
374
|
+
endDate: input.endDate,
|
|
375
|
+
updatedById: userId,
|
|
376
|
+
});
|
|
377
|
+
return excusal;
|
|
378
|
+
}
|
|
379
|
+
export async function updateExcusal(userId, input) {
|
|
380
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
381
|
+
const existing = await findExcusalById(input.excusalId);
|
|
382
|
+
if (!existing || existing.schoolId !== input.schoolId) {
|
|
383
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Excusal not found" });
|
|
384
|
+
}
|
|
385
|
+
if (existing.revokedAt) {
|
|
386
|
+
throw new TRPCError({
|
|
387
|
+
code: "BAD_REQUEST",
|
|
388
|
+
message: "Cannot update a revoked excusal",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const startDate = input.startDate ?? existing.startDate;
|
|
392
|
+
const endDate = input.endDate ?? existing.endDate;
|
|
393
|
+
if (toDateOnly(endDate) < toDateOnly(startDate)) {
|
|
394
|
+
throw new TRPCError({
|
|
395
|
+
code: "BAD_REQUEST",
|
|
396
|
+
message: "endDate must be on or after startDate",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
let studentIds = input.studentIds
|
|
400
|
+
? [...new Set(input.studentIds)]
|
|
401
|
+
: existing.students.map((s) => s.studentId);
|
|
402
|
+
if (input.studentIds) {
|
|
403
|
+
const memberships = await listSchoolStudentMemberships(input.schoolId);
|
|
404
|
+
const schoolStudentIds = new Set(memberships.map((m) => m.userId));
|
|
405
|
+
const invalid = studentIds.filter((id) => !schoolStudentIds.has(id));
|
|
406
|
+
if (invalid.length > 0) {
|
|
407
|
+
throw new TRPCError({
|
|
408
|
+
code: "BAD_REQUEST",
|
|
409
|
+
message: "One or more students are not active members of this school",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Clear previous locks, then re-apply for the new window/students.
|
|
414
|
+
await clearExcusalLocks(input.excusalId);
|
|
415
|
+
const updated = await updateStudentExcusal(input.excusalId, {
|
|
416
|
+
studentIds: input.studentIds ? studentIds : undefined,
|
|
417
|
+
startDate: input.startDate,
|
|
418
|
+
endDate: input.endDate,
|
|
419
|
+
reason: input.reason,
|
|
420
|
+
note: input.note,
|
|
421
|
+
});
|
|
422
|
+
await applyExcusalToAttendance({
|
|
423
|
+
schoolId: input.schoolId,
|
|
424
|
+
excusalId: updated.id,
|
|
425
|
+
studentIds,
|
|
426
|
+
startDate,
|
|
427
|
+
endDate,
|
|
428
|
+
updatedById: userId,
|
|
429
|
+
});
|
|
87
430
|
return updated;
|
|
88
431
|
}
|
|
432
|
+
export async function revokeExcusal(userId, input) {
|
|
433
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
434
|
+
const existing = await findExcusalById(input.excusalId);
|
|
435
|
+
if (!existing || existing.schoolId !== input.schoolId) {
|
|
436
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Excusal not found" });
|
|
437
|
+
}
|
|
438
|
+
if (existing.revokedAt) {
|
|
439
|
+
throw new TRPCError({
|
|
440
|
+
code: "BAD_REQUEST",
|
|
441
|
+
message: "Excusal is already revoked",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
await clearExcusalLocks(input.excusalId);
|
|
445
|
+
return revokeStudentExcusal(input.excusalId, userId);
|
|
446
|
+
}
|
|
89
447
|
/** Notify only students whose late or absent status is newly added. */
|
|
90
448
|
async function notifyMarkedLateOrAbsent(input) {
|
|
91
449
|
const lateReceiverIds = [...new Set(input.lateIds)].filter((id) => input.classStudentIds.has(id) && !input.previousLateIds.has(id));
|
|
@@ -129,5 +487,7 @@ async function sendAttendanceNotification(input) {
|
|
|
129
487
|
logger.error("Failed to send attendance notifications:", { error });
|
|
130
488
|
}
|
|
131
489
|
}
|
|
490
|
+
// Re-export date helpers used by callers/tests.
|
|
491
|
+
export { toDateOnly, startOfUtcDay, endOfUtcDay };
|
|
132
492
|
//# sourceMappingURL=attendance.js.map
|
|
133
|
-
//# debugId=
|
|
493
|
+
//# debugId=61646503-b6ff-5d4c-8251-5ecf346dad65
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendance.js","sources":["services/attendance.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Attendance service – get and update class attendance records.\n * Auto-creates attendance records for events that don't have them yet.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findClassWithStudents,\n findClassWithTeacher,\n findEventsByClassId,\n findAttendanceByEventId,\n findAttendanceByClassAndEvent,\n findManyAttendance,\n createAttendance,\n createAttendanceForEvent,\n updateAttendance,\n} from \"../models/attendance.js\";\nimport { sendToMultiple } from \"./notification.js\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Get attendance for a class (optionally filtered by event). Creates missing records. */\nexport async function getAttendance(classId: string, eventId?: string) {\n const classData = await findClassWithStudents(classId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to view this class's attendance\",\n });\n }\n\n const events = await findEventsByClassId(classId);\n const studentIds = classData.students.map((s) => s.id);\n\n for (const event of events) {\n const existing = await findAttendanceByEventId(event.id);\n if (!existing) {\n await createAttendanceForEvent({\n classId,\n eventId: event.id,\n presentIds: studentIds,\n });\n }\n }\n\n return findManyAttendance(classId, eventId);\n}\n\n/** Update attendance record (present/late/absent). Teacher-only. */\nexport async function updateAttendanceRecord(\n userId: string,\n classId: string,\n eventId: string | null | undefined,\n attendance: {\n present: { id: string; username: string }[];\n late: { id: string; username: string }[];\n absent: { id: string; username: string }[];\n }\n) {\n const classData = await findClassWithTeacher(classId, userId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to update this class's attendance\",\n });\n }\n\n const existing = await findAttendanceByClassAndEvent(classId, eventId ?? null);\n\n const presentIds = attendance.present.map((s) => s.id);\n const lateIds = attendance.late.map((s) => s.id);\n const absentIds = attendance.absent.map((s) => s.id);\n const previousLateIds = new Set(existing?.late.map((student) => student.id) ?? []);\n const previousAbsentIds = new Set(existing?.absent.map((student) => student.id) ?? []);\n const classStudentIds = new Set(classData.students.map((student) => student.id));\n\n if (!existing) {\n const created = await createAttendance({\n classId,\n eventId: eventId ?? null,\n presentIds,\n lateIds,\n absentIds,\n });\n await notifyMarkedLateOrAbsent({\n classId,\n className: classData.name,\n eventId: eventId ?? null,\n eventName: created.event?.name ?? null,\n classStudentIds,\n lateIds,\n absentIds,\n previousLateIds,\n previousAbsentIds,\n });\n return created;\n }\n\n const updated = await updateAttendance(existing.id, {\n presentIds,\n lateIds,\n absentIds,\n });\n await notifyMarkedLateOrAbsent({\n classId,\n className: classData.name,\n eventId: eventId ?? null,\n eventName: existing.event?.name ?? null,\n classStudentIds,\n lateIds,\n absentIds,\n previousLateIds,\n previousAbsentIds,\n });\n return updated;\n}\n\n/** Notify only students whose late or absent status is newly added. */\nasync function notifyMarkedLateOrAbsent(input: {\n classId: string;\n className: string;\n eventId: string | null;\n eventName: string | null;\n classStudentIds: Set<string>;\n lateIds: string[];\n absentIds: string[];\n previousLateIds: Set<string>;\n previousAbsentIds: Set<string>;\n}) {\n const lateReceiverIds = [...new Set(input.lateIds)].filter(\n (id) => input.classStudentIds.has(id) && !input.previousLateIds.has(id)\n );\n const absentReceiverIds = [...new Set(input.absentIds)].filter(\n (id) => input.classStudentIds.has(id) && !input.previousAbsentIds.has(id)\n );\n const link = input.eventId\n ? `/class/${input.classId}/attendance/${input.eventId}`\n : `/class/${input.classId}/attendance`;\n const attendanceLabel = input.eventName\n ? `\"${input.eventName}\" in ${input.className}`\n : input.className;\n\n await Promise.all([\n sendAttendanceNotification({\n receiverIds: lateReceiverIds,\n title: `Marked late in ${input.className}`,\n content: `You were marked late for ${attendanceLabel}.\\n[View attendance](${link})`,\n type: \"ATTENDANCE\",\n entityType: \"ATTENDANCE\",\n entityId: input.eventId,\n classId: input.classId,\n }),\n sendAttendanceNotification({\n receiverIds: absentReceiverIds,\n title: `Marked absent in ${input.className}`,\n content: `You were marked absent for ${attendanceLabel}.\\n[View attendance](${link})`,\n type: \"ATTENDANCE\",\n entityType: \"ATTENDANCE\",\n entityId: input.eventId,\n classId: input.classId,\n }),\n ]);\n}\n\n/** Send attendance notifications without failing the attendance update itself. */\nasync function sendAttendanceNotification(input: {\n receiverIds: string[];\n title: string;\n content: string;\n type: \"ATTENDANCE\";\n entityType: string;\n entityId: string | null;\n classId: string;\n}) {\n if (input.receiverIds.length === 0) {\n return;\n }\n\n try {\n await sendToMultiple(input);\n } catch (error) {\n logger.error(\"Failed to send attendance notifications:\", { error });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"703d27e8-ea48-5b60-9620-38757b18337b"}
|
|
1
|
+
{"version":3,"file":"attendance.js","sources":["services/attendance.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Attendance service – class attendance + school-admin excusals / stats.\n * Dual-writes legacy present/late/absent lists and per-student AttendanceRecord.\n * Active StudentExcusal rows force EXCUSED (locked for teachers).\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findClassWithStudents,\n findClassWithTeacher,\n findEventsByClassId,\n findAttendanceByEventId,\n findAttendanceByClassAndEvent,\n findManyAttendance,\n createAttendance,\n createAttendanceForEvent,\n updateAttendance,\n findActiveExcusalsForStudents,\n listExcusalsForSchool,\n findExcusalById,\n createStudentExcusal,\n updateStudentExcusal,\n revokeStudentExcusal,\n applyExcusalToAttendance,\n clearExcusalLocks,\n listSchoolStudentMemberships,\n listAttendanceRecordsForSchoolStudents,\n listTodaySchoolAttendanceSessions,\n toDateOnly,\n startOfUtcDay,\n endOfUtcDay,\n} from \"../models/attendance.js\";\nimport { findActiveSchoolAdminMembership } from \"../models/school.js\";\nimport { sendToMultiple } from \"./notification.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n buildExcusalLocks,\n mergeAttendanceUpdateWithLocks,\n type StudentRef,\n} from \"./attendanceMerge.js\";\nimport {\n computeAttendancePercentage,\n emptyAttendanceCounts,\n tallyAttendanceStatuses,\n} from \"./attendanceStats.js\";\n\nasync function requireSchoolAdmin(schoolId: string, userId: string) {\n const membership = await findActiveSchoolAdminMembership(schoolId, userId);\n if (!membership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only school admins can perform this action\",\n });\n }\n return membership;\n}\n\nfunction sessionDate(attendance: {\n date: Date;\n event?: { startTime: Date } | null;\n}): Date {\n return attendance.event?.startTime ?? attendance.date;\n}\n\n/** Get attendance for a class (optionally filtered by event). Creates missing records. */\nexport async function getAttendance(classId: string, eventId?: string) {\n const classData = await findClassWithStudents(classId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to view this class's attendance\",\n });\n }\n\n const events = await findEventsByClassId(classId);\n const studentIds = classData.students.map((s) => s.id);\n\n for (const event of events) {\n const existing = await findAttendanceByEventId(event.id);\n if (!existing) {\n const locks =\n classData.schoolId && studentIds.length > 0\n ? buildExcusalLocks(\n await findActiveExcusalsForStudents({\n schoolId: classData.schoolId,\n studentIds,\n date: event.startTime,\n }),\n studentIds\n )\n : [];\n\n const merged = mergeAttendanceUpdateWithLocks({\n present: classData.students.map((s) => ({\n id: s.id,\n username: s.username,\n })),\n late: [],\n absent: [],\n locks,\n });\n\n await createAttendanceForEvent({\n classId,\n eventId: event.id,\n date: event.startTime,\n presentIds: merged.presentIds,\n lateIds: merged.lateIds,\n absentIds: merged.absentIds,\n excusedIds: merged.excusedIds,\n records: merged.records,\n });\n } else if (classData.schoolId && studentIds.length > 0) {\n // Ensure active excusals are reflected on existing sessions.\n const locks = buildExcusalLocks(\n await findActiveExcusalsForStudents({\n schoolId: classData.schoolId,\n studentIds,\n date: event.startTime,\n }),\n studentIds\n );\n const needsLock = locks.filter((lock) => {\n const current = existing.records.find((r) => r.studentId === lock.studentId);\n return !(\n current?.status === \"EXCUSED\" &&\n current.excusedByExcusalId === lock.excusalId\n );\n });\n const byExcusal = new Map<string, string[]>();\n for (const lock of needsLock) {\n const list = byExcusal.get(lock.excusalId) ?? [];\n list.push(lock.studentId);\n byExcusal.set(lock.excusalId, list);\n }\n for (const [excusalId, lockedStudentIds] of byExcusal) {\n await applyExcusalToAttendance({\n schoolId: classData.schoolId,\n excusalId,\n studentIds: lockedStudentIds,\n startDate: event.startTime,\n endDate: event.startTime,\n });\n }\n }\n }\n\n const sessions = await findManyAttendance(classId, eventId);\n return sessions.map(shapeAttendanceSession);\n}\n\n/** Additive shape: keep present/late/absent; expose excused + lock metadata for UI. */\nfunction shapeAttendanceSession<\n T extends {\n records: Array<{\n id: string;\n studentId: string;\n status: string;\n note: string | null;\n excusedByExcusalId: string | null;\n updatedById: string | null;\n updatedAt: Date;\n student: { id: string; username: string };\n excusedByExcusal: {\n id: string;\n reason: string;\n note: string | null;\n } | null;\n }>;\n present: unknown[];\n late: unknown[];\n absent: unknown[];\n },\n>(session: T) {\n const records = session.records.map((r) => ({\n ...r,\n lockedFromExcusal: Boolean(r.excusedByExcusalId),\n reason: r.excusedByExcusal?.reason ?? null,\n excusalNote: r.excusedByExcusal?.note ?? null,\n }));\n const excused = records\n .filter((r) => r.status === \"EXCUSED\")\n .map((r) => r.student);\n\n return {\n ...session,\n excused,\n records,\n };\n}\n\n/** Update attendance record (present/late/absent/excused). Teacher-only. */\nexport async function updateAttendanceRecord(\n userId: string,\n classId: string,\n eventId: string | null | undefined,\n attendance: {\n present: StudentRef[];\n late: StudentRef[];\n absent: StudentRef[];\n excused?: StudentRef[];\n }\n) {\n const classData = await findClassWithTeacher(classId, userId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to update this class's attendance\",\n });\n }\n\n const existing = await findAttendanceByClassAndEvent(classId, eventId ?? null);\n const dateForExcusal = existing\n ? sessionDate(existing)\n : new Date();\n\n const classStudentIds = classData.students.map((s) => s.id);\n const locks =\n classData.schoolId && classStudentIds.length > 0\n ? buildExcusalLocks(\n await findActiveExcusalsForStudents({\n schoolId: classData.schoolId,\n studentIds: classStudentIds,\n date: dateForExcusal,\n }),\n classStudentIds\n )\n : [];\n\n const existingNotes = new Map(\n (existing?.records ?? []).map((r) => [r.studentId, r.note])\n );\n\n const merged = mergeAttendanceUpdateWithLocks({\n present: attendance.present,\n late: attendance.late,\n absent: attendance.absent,\n excused: attendance.excused,\n locks,\n updatedById: userId,\n existingNotesByStudentId: existingNotes,\n });\n\n const previousLateIds = new Set(existing?.late.map((student) => student.id) ?? []);\n const previousAbsentIds = new Set(\n existing?.absent.map((student) => student.id) ?? []\n );\n const classStudentIdSet = new Set(classStudentIds);\n\n if (!existing) {\n const created = await createAttendance({\n classId,\n eventId: eventId ?? null,\n presentIds: merged.presentIds,\n lateIds: merged.lateIds,\n absentIds: merged.absentIds,\n excusedIds: merged.excusedIds,\n records: merged.records,\n updatedById: userId,\n });\n await notifyMarkedLateOrAbsent({\n classId,\n className: classData.name,\n eventId: eventId ?? null,\n eventName: created.event?.name ?? null,\n classStudentIds: classStudentIdSet,\n lateIds: merged.lateIds,\n absentIds: merged.absentIds,\n previousLateIds,\n previousAbsentIds,\n });\n return shapeAttendanceSession(created);\n }\n\n const updated = await updateAttendance(existing.id, {\n presentIds: merged.presentIds,\n lateIds: merged.lateIds,\n absentIds: merged.absentIds,\n records: merged.records,\n });\n await notifyMarkedLateOrAbsent({\n classId,\n className: classData.name,\n eventId: eventId ?? null,\n eventName: existing.event?.name ?? null,\n classStudentIds: classStudentIdSet,\n lateIds: merged.lateIds,\n absentIds: merged.absentIds,\n previousLateIds,\n previousAbsentIds,\n });\n return shapeAttendanceSession(updated);\n}\n\n/** School-admin: today's attendance rollup across classes. */\nexport async function listTodayAttendanceSummary(\n userId: string,\n schoolId: string\n) {\n await requireSchoolAdmin(schoolId, userId);\n\n const today = new Date();\n const sessions = await listTodaySchoolAttendanceSessions(schoolId, today);\n const memberships = await listSchoolStudentMemberships(schoolId);\n const totalStudents = memberships.length;\n\n const counts = emptyAttendanceCounts();\n const rows: Array<{\n attendanceId: string;\n classId: string;\n className: string;\n section: string;\n subject: string;\n eventId: string | null;\n eventName: string | null;\n date: Date;\n present: number;\n late: number;\n absent: number;\n excused: number;\n unmarked: number;\n classStudentCount: number;\n }> = [];\n\n for (const session of sessions) {\n const tallied = tallyAttendanceStatuses(session.records.map((r) => r.status));\n counts.present += tallied.present;\n counts.late += tallied.late;\n counts.absent += tallied.absent;\n counts.excused += tallied.excused;\n const classStudentCount = session.class._count.students;\n const marked =\n tallied.present + tallied.late + tallied.absent + tallied.excused;\n rows.push({\n attendanceId: session.id,\n classId: session.classId,\n className: session.class.name,\n section: session.class.section,\n subject: session.class.subject,\n eventId: session.eventId,\n eventName: session.event?.name ?? null,\n date: session.event?.startTime ?? session.date,\n present: tallied.present,\n late: tallied.late,\n absent: tallied.absent,\n excused: tallied.excused,\n unmarked: Math.max(0, classStudentCount - marked),\n classStudentCount,\n });\n }\n\n return {\n date: toDateOnly(today),\n totalStudents,\n totals: counts,\n percentage: computeAttendancePercentage(counts),\n sessions: rows,\n };\n}\n\n/** School-admin: per-student attendance counts + % (excused excluded from %). */\nexport async function listStudentAttendanceStats(\n userId: string,\n input: {\n schoolId: string;\n from?: Date;\n to?: Date;\n studentId?: string;\n }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const memberships = await listSchoolStudentMemberships(input.schoolId);\n const filtered = input.studentId\n ? memberships.filter((m) => m.userId === input.studentId)\n : memberships;\n const studentIds = filtered.map((m) => m.userId);\n\n const records = await listAttendanceRecordsForSchoolStudents({\n schoolId: input.schoolId,\n studentIds,\n from: input.from,\n to: input.to,\n });\n\n const byStudent = new Map<string, ReturnType<typeof emptyAttendanceCounts>>();\n for (const id of studentIds) {\n byStudent.set(id, emptyAttendanceCounts());\n }\n for (const record of records) {\n const counts = byStudent.get(record.studentId);\n if (!counts) continue;\n switch (record.status) {\n case \"PRESENT\":\n counts.present += 1;\n break;\n case \"LATE\":\n counts.late += 1;\n break;\n case \"ABSENT\":\n counts.absent += 1;\n break;\n case \"EXCUSED\":\n counts.excused += 1;\n break;\n default:\n break;\n }\n }\n\n return filtered.map((membership) => {\n const counts = byStudent.get(membership.userId) ?? emptyAttendanceCounts();\n return {\n studentId: membership.userId,\n username: membership.user.username,\n displayName: membership.user.profile?.displayName ?? null,\n profilePicture: membership.user.profile?.profilePicture ?? null,\n profilePictureThumbnail:\n membership.user.profile?.profilePictureThumbnail ?? null,\n classes: membership.user.studentIn,\n present: counts.present,\n late: counts.late,\n absent: counts.absent,\n excused: counts.excused,\n attendancePercentage: computeAttendancePercentage(counts),\n };\n });\n}\n\nexport async function listExcusals(\n userId: string,\n input: {\n schoolId: string;\n includeRevoked?: boolean;\n studentId?: string;\n }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const excusals = await listExcusalsForSchool(input);\n const now = toDateOnly(new Date());\n\n return excusals.map((excusal) => {\n let status: \"ACTIVE\" | \"UPCOMING\" | \"REVOKED\" = \"ACTIVE\";\n if (excusal.revokedAt) {\n status = \"REVOKED\";\n } else if (toDateOnly(excusal.startDate) > now) {\n status = \"UPCOMING\";\n } else if (toDateOnly(excusal.endDate) < now) {\n status = \"ACTIVE\"; // past but not revoked — still listed as ACTIVE historically\n }\n\n return {\n id: excusal.id,\n schoolId: excusal.schoolId,\n startDate: excusal.startDate,\n endDate: excusal.endDate,\n reason: excusal.reason,\n note: excusal.note,\n revokedAt: excusal.revokedAt,\n createdAt: excusal.createdAt,\n updatedAt: excusal.updatedAt,\n status,\n studentIds: excusal.students.map((s) => s.studentId),\n students: excusal.students.map((s) => ({\n id: s.student.id,\n username: s.student.username,\n displayName: s.student.profile?.displayName ?? null,\n profilePicture: s.student.profile?.profilePicture ?? null,\n profilePictureThumbnail:\n s.student.profile?.profilePictureThumbnail ?? null,\n })),\n createdBy: {\n id: excusal.createdBy.id,\n username: excusal.createdBy.username,\n displayName: excusal.createdBy.profile?.displayName ?? null,\n },\n revokedBy: excusal.revokedBy\n ? {\n id: excusal.revokedBy.id,\n username: excusal.revokedBy.username,\n displayName: excusal.revokedBy.profile?.displayName ?? null,\n }\n : null,\n };\n });\n}\n\nexport async function createExcusal(\n userId: string,\n input: {\n schoolId: string;\n studentIds: string[];\n startDate: Date;\n endDate: Date;\n reason: string;\n note?: string | null;\n }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n if (toDateOnly(input.endDate) < toDateOnly(input.startDate)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"endDate must be on or after startDate\",\n });\n }\n\n const uniqueStudentIds = [...new Set(input.studentIds)];\n const memberships = await listSchoolStudentMemberships(input.schoolId);\n const schoolStudentIds = new Set(memberships.map((m) => m.userId));\n const invalid = uniqueStudentIds.filter((id) => !schoolStudentIds.has(id));\n if (invalid.length > 0) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"One or more students are not active members of this school\",\n });\n }\n\n const excusal = await createStudentExcusal({\n schoolId: input.schoolId,\n studentIds: uniqueStudentIds,\n startDate: input.startDate,\n endDate: input.endDate,\n reason: input.reason,\n note: input.note,\n createdById: userId,\n });\n\n await applyExcusalToAttendance({\n schoolId: input.schoolId,\n excusalId: excusal.id,\n studentIds: uniqueStudentIds,\n startDate: input.startDate,\n endDate: input.endDate,\n updatedById: userId,\n });\n\n return excusal;\n}\n\nexport async function updateExcusal(\n userId: string,\n input: {\n schoolId: string;\n excusalId: string;\n studentIds?: string[];\n startDate?: Date;\n endDate?: Date;\n reason?: string;\n note?: string | null;\n }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const existing = await findExcusalById(input.excusalId);\n if (!existing || existing.schoolId !== input.schoolId) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Excusal not found\" });\n }\n if (existing.revokedAt) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot update a revoked excusal\",\n });\n }\n\n const startDate = input.startDate ?? existing.startDate;\n const endDate = input.endDate ?? existing.endDate;\n if (toDateOnly(endDate) < toDateOnly(startDate)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"endDate must be on or after startDate\",\n });\n }\n\n let studentIds = input.studentIds\n ? [...new Set(input.studentIds)]\n : existing.students.map((s) => s.studentId);\n\n if (input.studentIds) {\n const memberships = await listSchoolStudentMemberships(input.schoolId);\n const schoolStudentIds = new Set(memberships.map((m) => m.userId));\n const invalid = studentIds.filter((id) => !schoolStudentIds.has(id));\n if (invalid.length > 0) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"One or more students are not active members of this school\",\n });\n }\n }\n\n // Clear previous locks, then re-apply for the new window/students.\n await clearExcusalLocks(input.excusalId);\n\n const updated = await updateStudentExcusal(input.excusalId, {\n studentIds: input.studentIds ? studentIds : undefined,\n startDate: input.startDate,\n endDate: input.endDate,\n reason: input.reason,\n note: input.note,\n });\n\n await applyExcusalToAttendance({\n schoolId: input.schoolId,\n excusalId: updated.id,\n studentIds,\n startDate,\n endDate,\n updatedById: userId,\n });\n\n return updated;\n}\n\nexport async function revokeExcusal(\n userId: string,\n input: { schoolId: string; excusalId: string }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const existing = await findExcusalById(input.excusalId);\n if (!existing || existing.schoolId !== input.schoolId) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Excusal not found\" });\n }\n if (existing.revokedAt) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Excusal is already revoked\",\n });\n }\n\n await clearExcusalLocks(input.excusalId);\n return revokeStudentExcusal(input.excusalId, userId);\n}\n\n/** Notify only students whose late or absent status is newly added. */\nasync function notifyMarkedLateOrAbsent(input: {\n classId: string;\n className: string;\n eventId: string | null;\n eventName: string | null;\n classStudentIds: Set<string>;\n lateIds: string[];\n absentIds: string[];\n previousLateIds: Set<string>;\n previousAbsentIds: Set<string>;\n}) {\n const lateReceiverIds = [...new Set(input.lateIds)].filter(\n (id) => input.classStudentIds.has(id) && !input.previousLateIds.has(id)\n );\n const absentReceiverIds = [...new Set(input.absentIds)].filter(\n (id) => input.classStudentIds.has(id) && !input.previousAbsentIds.has(id)\n );\n const link = input.eventId\n ? `/class/${input.classId}/attendance/${input.eventId}`\n : `/class/${input.classId}/attendance`;\n const attendanceLabel = input.eventName\n ? `\"${input.eventName}\" in ${input.className}`\n : input.className;\n\n await Promise.all([\n sendAttendanceNotification({\n receiverIds: lateReceiverIds,\n title: `Marked late in ${input.className}`,\n content: `You were marked late for ${attendanceLabel}.\\n[View attendance](${link})`,\n type: \"ATTENDANCE\",\n entityType: \"ATTENDANCE\",\n entityId: input.eventId,\n classId: input.classId,\n }),\n sendAttendanceNotification({\n receiverIds: absentReceiverIds,\n title: `Marked absent in ${input.className}`,\n content: `You were marked absent for ${attendanceLabel}.\\n[View attendance](${link})`,\n type: \"ATTENDANCE\",\n entityType: \"ATTENDANCE\",\n entityId: input.eventId,\n classId: input.classId,\n }),\n ]);\n}\n\n/** Send attendance notifications without failing the attendance update itself. */\nasync function sendAttendanceNotification(input: {\n receiverIds: string[];\n title: string;\n content: string;\n type: \"ATTENDANCE\";\n entityType: string;\n entityId: string | null;\n classId: string;\n}) {\n if (input.receiverIds.length === 0) {\n return;\n }\n\n try {\n await sendToMultiple(input);\n } catch (error) {\n logger.error(\"Failed to send attendance notifications:\", { error });\n }\n}\n\n// Re-export date helpers used by callers/tests.\nexport { toDateOnly, startOfUtcDay, endOfUtcDay };\n"],"names":[],"mappings":"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","debug_id":"61646503-b6ff-5d4c-8251-5ecf346dad65"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for merging teacher attendance updates with excusal locks.
|
|
3
|
+
*/
|
|
4
|
+
import type { AttendanceStatus } from "@prisma/client";
|
|
5
|
+
export type StudentRef = {
|
|
6
|
+
id: string;
|
|
7
|
+
username: string;
|
|
8
|
+
};
|
|
9
|
+
export type AttendanceLock = {
|
|
10
|
+
studentId: string;
|
|
11
|
+
excusalId: string;
|
|
12
|
+
reason?: string | null;
|
|
13
|
+
note?: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type MergedAttendanceUpdate = {
|
|
16
|
+
presentIds: string[];
|
|
17
|
+
lateIds: string[];
|
|
18
|
+
absentIds: string[];
|
|
19
|
+
excusedIds: string[];
|
|
20
|
+
records: Array<{
|
|
21
|
+
studentId: string;
|
|
22
|
+
status: AttendanceStatus;
|
|
23
|
+
note?: string | null;
|
|
24
|
+
excusedByExcusalId?: string | null;
|
|
25
|
+
updatedById?: string | null;
|
|
26
|
+
}>;
|
|
27
|
+
/** Students whose status was forced to EXCUSED by an active excusal. */
|
|
28
|
+
lockedStudentIds: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Build present/late/absent/excused lists + per-student records.
|
|
32
|
+
* Active excusal locks always win (status EXCUSED + excusedByExcusalId set).
|
|
33
|
+
*/
|
|
34
|
+
export declare function mergeAttendanceUpdateWithLocks(input: {
|
|
35
|
+
present: StudentRef[];
|
|
36
|
+
late: StudentRef[];
|
|
37
|
+
absent: StudentRef[];
|
|
38
|
+
excused?: StudentRef[];
|
|
39
|
+
locks: AttendanceLock[];
|
|
40
|
+
updatedById?: string | null;
|
|
41
|
+
/** Preserve notes for locked students when available. */
|
|
42
|
+
existingNotesByStudentId?: Map<string, string | null | undefined>;
|
|
43
|
+
}): MergedAttendanceUpdate;
|
|
44
|
+
/** Map active excusals (with student join rows) to per-student locks. */
|
|
45
|
+
export declare function buildExcusalLocks(excusals: Array<{
|
|
46
|
+
id: string;
|
|
47
|
+
reason?: string | null;
|
|
48
|
+
note?: string | null;
|
|
49
|
+
students: Array<{
|
|
50
|
+
studentId: string;
|
|
51
|
+
}>;
|
|
52
|
+
}>, studentIds?: Iterable<string>): AttendanceLock[];
|
|
53
|
+
//# sourceMappingURL=attendanceMerge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attendanceMerge.d.ts","sourceRoot":"/","sources":["services/attendanceMerge.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,KAAK,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,gBAAgB,CAAC;QACzB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC7B,CAAC,CAAC;IACH,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE;IACpD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yDAAyD;IACzD,wBAAwB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnE,GAAG,sBAAsB,CA+DzB;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,KAAK,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxC,CAAC,EACF,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC5B,cAAc,EAAE,CAqBlB"}
|