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:
92
apps/backend/src/modules/classes/lib/helpers.ts
Normal file
92
apps/backend/src/modules/classes/lib/helpers.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { PrismaClient } from '@generated/prisma/client';
|
||||
import type {
|
||||
AttendeePaymentStatus,
|
||||
ClassToday,
|
||||
ProblemDetails,
|
||||
Result,
|
||||
} from '@gruperly/shared';
|
||||
import { err } from '@gruperly/shared';
|
||||
import { noGroupAccessProblem, notFoundResourceProblem } from '@/http/problem-builders';
|
||||
|
||||
export type ClassesDb = Pick<
|
||||
PrismaClient,
|
||||
'group' | 'classSession' | 'attendee' | 'payment' | 'attendance' | 'slotRelease'
|
||||
>;
|
||||
|
||||
type PaymentLike = {
|
||||
status: 'PENDING' | 'PAID' | 'OVERDUE' | 'CANCELLED';
|
||||
dueDate: Date;
|
||||
};
|
||||
|
||||
// Estado de cobro calculado en tiempo real (sin columna denormalizada):
|
||||
// PENDIENTE si hay un pago vencido (OVERDUE) o un PENDING cuyo vencimiento ya pasó.
|
||||
export function resolvePaymentStatus(
|
||||
payments: PaymentLike[],
|
||||
startOfToday: Date,
|
||||
): AttendeePaymentStatus {
|
||||
const hasUnpaid = payments.some(
|
||||
(payment) =>
|
||||
payment.status === 'OVERDUE'
|
||||
|| (payment.status === 'PENDING' && payment.dueDate.getTime() < startOfToday.getTime()),
|
||||
);
|
||||
return hasUnpaid ? 'PENDING' : 'UP_TO_DATE';
|
||||
}
|
||||
|
||||
export function absenceReleaseHours(): number {
|
||||
const raw = process.env.ABSENCE_RELEASE_HOURS;
|
||||
if (raw == null || raw.trim() === '') {
|
||||
return 12;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 12;
|
||||
}
|
||||
|
||||
export type ClassSessionRecord = {
|
||||
id: string;
|
||||
groupId: string;
|
||||
startsAt: Date;
|
||||
};
|
||||
|
||||
type GroupWithAccess = {
|
||||
createdById: string;
|
||||
members: { userId: string }[];
|
||||
};
|
||||
|
||||
// Verifica que el usuario sea owner o miembro del grupo dueño de la sesión.
|
||||
export function ensureSessionAccess(
|
||||
session: ClassSessionRecord & { group: GroupWithAccess },
|
||||
userId: string,
|
||||
): Result<ClassSessionRecord, ProblemDetails> {
|
||||
const isOwner = session.group.createdById === userId;
|
||||
const isMember = session.group.members.length > 0;
|
||||
if (!isOwner && !isMember) {
|
||||
return err(noGroupAccessProblem());
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
value: { id: session.id, groupId: session.groupId, startsAt: session.startsAt },
|
||||
};
|
||||
}
|
||||
|
||||
export function sessionNotFound(sessionId: string): ProblemDetails {
|
||||
return notFoundResourceProblem('Class session', sessionId);
|
||||
}
|
||||
|
||||
export function toClassToday(
|
||||
session: { id: string; startsAt: Date },
|
||||
group: { id: string; name: string; capacity: number | null },
|
||||
enrolledCount: number,
|
||||
hasAttendance: boolean,
|
||||
): ClassToday {
|
||||
return {
|
||||
sessionId: session.id,
|
||||
groupId: group.id,
|
||||
groupName: group.name,
|
||||
startsAt: session.startsAt.toISOString(),
|
||||
enrolledCount,
|
||||
capacity: group.capacity,
|
||||
availableSlots:
|
||||
group.capacity == null ? null : Math.max(0, group.capacity - enrolledCount),
|
||||
hasAttendance,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user