feat: add absence notification and analytics features

- Implemented AbsenceNotifyView for notifying absence with session details.
- Created AnalyticsView for displaying group analytics and managing students at risk.
- Added ClassAttendanceView for marking attendance in classes.
- Defined new schemas for analytics and attendance in shared package.
This commit is contained in:
Jose Selesan
2026-09-23 16:24:45 -03:00
parent a363e9e8fc
commit 2e184845e1
59 changed files with 3854 additions and 23 deletions

View File

@@ -1,5 +1,7 @@
export * from './lib/problem-details.js';
export * from './lib/result.js';
export * from './schemas/analytics.js';
export * from './schemas/attendance.js';
export * from './schemas/attendees.js';
export * from './schemas/enums.js';
export * from './schemas/groups.js';

View File

@@ -0,0 +1,67 @@
import { z } from 'zod';
import { attendanceStatusSchema } from './attendance.js';
const isoDateTimeSchema = z.string().datetime();
export const attendeeStatusSchema = z.enum(['ACTIVE', 'PAUSED', 'DROPPED']);
export type AttendeeStatus = z.output<typeof attendeeStatusSchema>;
export const riskLevelSchema = z.enum(['HIGH', 'MEDIUM']);
export type RiskLevel = z.output<typeof riskLevelSchema>;
export const GroupAnalyticsSchema = z.object({
attendanceRate: z.number().min(0).max(100),
totalPresent: z.number().int().min(0),
totalClasses: z.number().int().min(0),
recoveredSlots: z.number().int().min(0),
});
export type GroupAnalytics = z.output<typeof GroupAnalyticsSchema>;
export type GroupAnalyticsDto = GroupAnalytics;
export const StudentAtRiskSchema = z.object({
attendeeId: z.string(),
fullName: z.string(),
phone: z.string().nullable(),
riskLevel: riskLevelSchema,
consecutiveAbsences: z.number().int().min(0),
lastAttendedAt: isoDateTimeSchema.nullable(),
monthlyAttendanceRate: z.number().min(0).max(100),
});
export type StudentAtRisk = z.output<typeof StudentAtRiskSchema>;
export type StudentAtRiskDto = StudentAtRisk;
export const StudentsAtRiskSchema = z.object({
groupName: z.string(),
data: z.array(StudentAtRiskSchema),
});
export type StudentsAtRisk = z.output<typeof StudentsAtRiskSchema>;
export type StudentsAtRiskDto = StudentsAtRisk;
export const AttendanceHistoryItemSchema = z.object({
classSessionId: z.string(),
startsAt: isoDateTimeSchema,
status: attendanceStatusSchema.nullable(),
});
export type AttendanceHistoryItem = z.output<typeof AttendanceHistoryItemSchema>;
export const AttendeeHistorySchema = z.object({
attendeeId: z.string(),
fullName: z.string(),
groupId: z.string(),
groupName: z.string(),
attendanceRate: z.number().min(0).max(100),
sessions: z.array(AttendanceHistoryItemSchema),
});
export type AttendeeHistory = z.output<typeof AttendeeHistorySchema>;
export type AttendeeHistoryDto = AttendeeHistory;
export const UpdateStudentStatusSchema = z.object({
status: attendeeStatusSchema,
});
export type UpdateStudentStatus = z.output<typeof UpdateStudentStatusSchema>;
export const UpdateStudentStatusResultSchema = z.object({
attendeeId: z.string(),
status: attendeeStatusSchema,
});
export type UpdateStudentStatusResult = z.output<typeof UpdateStudentStatusResultSchema>;

View File

@@ -0,0 +1,111 @@
import { z } from 'zod';
import { timeZoneSchema } from './home.js';
const isoDateTimeSchema = z.string().datetime();
export const attendanceStatusSchema = z.enum(['PRESENT', 'ABSENT', 'EXCUSED']);
export type AttendanceStatus = z.output<typeof attendanceStatusSchema>;
export const attendeePaymentStatusSchema = z.enum(['UP_TO_DATE', 'PENDING']);
export type AttendeePaymentStatus = z.output<typeof attendeePaymentStatusSchema>;
export const MarkAttendanceRecordSchema = z.object({
attendeeId: z.string().min(1),
status: z.enum(['PRESENT', 'ABSENT']),
});
export type MarkAttendanceRecord = z.output<typeof MarkAttendanceRecordSchema>;
export const MarkAttendanceSchema = z.object({
classSessionId: z.string().min(1),
records: z.array(MarkAttendanceRecordSchema).min(1, 'Debes enviar al menos un registro de asistencia'),
});
export type MarkAttendance = z.output<typeof MarkAttendanceSchema>;
export const MarkAttendanceResultSchema = z.object({
classSessionId: z.string(),
marked: z.number().int().min(0),
});
export type MarkAttendanceResult = z.output<typeof MarkAttendanceResultSchema>;
export const NotifyAbsenceSchema = z.object({
attendeeId: z.string().min(1),
classSessionId: z.string().min(1),
});
export type NotifyAbsence = z.output<typeof NotifyAbsenceSchema>;
export const PublicNotifyAbsenceSchema = z.object({
token: z.string().min(1),
classSessionId: z.string().min(1),
});
export type PublicNotifyAbsence = z.output<typeof PublicNotifyAbsenceSchema>;
export const NotifyAbsenceResultSchema = z.object({
classSessionId: z.string(),
notified: z.boolean(),
slotReleased: z.boolean(),
});
export type NotifyAbsenceResult = z.output<typeof NotifyAbsenceResultSchema>;
export const AbsenceQuerySchema = z.object({
token: z.string().min(1),
});
export type AbsenceQuery = z.output<typeof AbsenceQuerySchema>;
export const ClassTodaySchema = z.object({
sessionId: z.string(),
groupId: z.string(),
groupName: z.string(),
startsAt: isoDateTimeSchema,
enrolledCount: z.number().int().min(0),
capacity: z.number().int().min(1).nullable(),
availableSlots: z.number().int().min(0).nullable(),
hasAttendance: z.boolean(),
});
export type ClassToday = z.output<typeof ClassTodaySchema>;
export type ClassTodayDto = ClassToday;
export const ClassTodayListSchema = z.object({
data: z.array(ClassTodaySchema),
});
export type ClassTodayList = z.output<typeof ClassTodayListSchema>;
export const TodayClassesQuerySchema = z.object({
timeZone: timeZoneSchema.optional(),
});
export type TodayClassesQuery = z.output<typeof TodayClassesQuerySchema>;
export const SessionStudentSchema = z.object({
attendeeId: z.string(),
fullName: z.string(),
paymentStatus: attendeePaymentStatusSchema,
attendanceStatus: attendanceStatusSchema.nullable(),
notifiedAbsence: z.boolean(),
});
export type SessionStudent = z.output<typeof SessionStudentSchema>;
export type SessionStudentDto = SessionStudent;
export const SessionStudentsSchema = z.object({
sessionId: z.string(),
groupId: z.string(),
groupName: z.string(),
startsAt: isoDateTimeSchema,
students: z.array(SessionStudentSchema),
});
export type SessionStudents = z.output<typeof SessionStudentsSchema>;
export const AbsenceSessionSchema = z.object({
sessionId: z.string(),
groupName: z.string(),
startsAt: isoDateTimeSchema,
notified: z.boolean(),
attendanceStatus: attendanceStatusSchema.nullable(),
});
export type AbsenceSession = z.output<typeof AbsenceSessionSchema>;
export const AbsenceDetailsSchema = z.object({
attendeeId: z.string(),
fullName: z.string(),
groupName: z.string(),
sessions: z.array(AbsenceSessionSchema),
});
export type AbsenceDetails = z.output<typeof AbsenceDetailsSchema>;

View File

@@ -16,6 +16,7 @@ export const AttendeeDtoSchema = z.object({
guardianName: z.string().nullable(),
guardianPhone: z.string().nullable(),
notes: z.string().nullable(),
notifyToken: z.string().nullable().optional(),
createdAt: isoDateTimeSchema,
updatedAt: isoDateTimeSchema,
});

View File

@@ -15,6 +15,8 @@ const timeZoneSchema = z
}
}, 'Zona horaria IANA inválida');
export { timeZoneSchema };
export const HomeStatsSchema = z.object({
groups: z.number().int().nonnegative(),
attendees: z.number().int().nonnegative(),