- 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.
93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
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,
|
|
};
|
|
}
|