User (Trabajadores del instituto)
us_id (PK)us_full_nameus_emailus_password_hashus_role (Enum: ADMIN, TEACHER)Student (El alumno y su contrato global)
st_id (PK)st_identification_card (Cédula/DNI)st_full_namest_phone_numberst_start_datest_is_graduated (Booleano)st_contract_status (Enum: ACTIVE, FROZEN, INACTIVE) ⬅️ ¡Nuevo campo agregado!
st_progress_category (Enum: FAST, MODERATE, SLOW, NOT_ENOUGH_DATA) ⬅️Module (Catálogo de cursos: A1, A2, etc.)
mo_id (PK)mo_namemo_descriptionStudentModule (Contrato / Compra de módulos)
st_mod_id (PK)st_mod_student_id (FK -> Student.st_id)st_mod_module_id (FK -> Module.mo_id)st_mod_seller_id (FK -> User.us_id) (El Asesor de Ventas que realizó el registro)
st_mod_status (Enum: ACTIVE, APPROVED, LOCKED)st_mod_purchase_dateAttendanceSession (Jornada de estudio diaria)
at_se_id (PK)at_se_student_id (FK -> Student.st_id)at_se_teacher_id (FK -> User.us_id, acepta nulos si la salida fue automática)at_se_session_dateat_se_entry_timeat_se_exit_timeat_se_total_minutesat_se_status (Enum: IN_PROGRESS, PENDING_APPROVAL, APPROVED)LessonLog (Registro de lecciones estudiadas por jornada)
le_lo_id (PK)le_lo_attendance_session_id (FK -> AttendanceSession.at_se_id)le_lo_lesson_numberle_lo_notesRetentionAlert (Historial de alertas por inasistencia)
re_al_id (PK)re_al_student_id (FK -> Student.st_id)re_al_user_id (FK -> User.us_id) (Admin que realizó la notificación)re_al_contact_datere_al_has_responded (Booleano)re_al_days_absent (Entero) ⬅️ ¡Nuevo! (Días de ausencia al momento de crear la alerta)re_al_is_justified (Booleano) ⬅️ ¡Nuevo! (Campo Sí/No de justificación)re_al_justification_reason (Texto) ⬅️ ¡Nuevo! (El motivo de la inasistencia, permite nulos)re_al_return_deadline (Fecha) ⬅️ ¡Nuevo! (Fecha límite acordada para su regreso, permite nulos)re_al_observations (Texto)re_al_status (Enum: PENDING, RESOLVED, CLOSED_FROZEN)re_al_resolution_date (Fecha, permite nulos)erDiagram
%% Relationships
User ||--o{ AttendanceSession : "validates (Teacher)"
User ||--o{ RetentionAlert : "registers (Admin)"
User ||--o{ StudentModule : "sells (Seller)"
Student ||--o{ StudentModule : "contracts"
Student ||--o{ AttendanceSession : "attends"
Student ||--o{ RetentionAlert : "receives"
Module ||--o{ StudentModule : "is_included_in"
AttendanceSession ||--o{ LessonLog : "contains_lessons"
%% Entities
User {
int us_id PK
string us_full_name
string us_email
string us_password_hash
string us_role "ADMIN, TEACHER"
}
Student {
int st_id PK
string st_identification_card
string st_full_name
string st_phone_number
date st_start_date
boolean st_is_graduated
string st_contract_status "ACTIVE, FROZEN, INACTIVE"
}
Module {
int mo_id PK
string mo_name
string mo_description
}
StudentModule {
int st_mod_id PK
int st_mod_student_id FK
int st_mod_module_id FK
int st_mod_seller_id FK
string st_mod_status "ACTIVE, CLOSED, LOCKED"
date st_mod_purchase_date
}
AttendanceSession {
int at_se_id PK
int at_se_student_id FK
int at_se_teacher_id FK "Nullable"
date at_se_session_date
time at_se_entry_time
time at_se_exit_time
int at_se_total_minutes
string at_se_status "IN_PROGRESS, PENDING_APPROVAL, APPROVED"
}
LessonLog {
int le_lo_id PK
int le_lo_attendance_session_id FK
int le_lo_lesson_number
string le_lo_notes
}
RetentionAlert {
int re_al_id PK
int re_al_student_id FK
int re_al_user_id FK
date re_al_contact_date
boolean re_al_has_responded
int re_al_days_absent
boolean re_al_is_justified
string re_al_justification_reason "Nullable"
date re_al_return_deadline "Nullable"
string re_al_observations "Nullable"
string re_al_status "PENDING, RESOLVED, CLOSED_FROZEN"
date re_al_resolution_date "Nullable"
}
server.ts o main.ts de tu API), justo después de establecer la conexión con PostgreSQL, llamas a una función asíncrona encargada de hacer toda la matemática. Gracias a cómo funciona el Event Loop (bucle de eventos) en JavaScript/TypeScript, esta función se ejecutará en segundo plano. Tu servidor estará listo para recibir peticiones web inmediatamente, por lo que si un docente o estudiante madrugador entra al sistema en ese mismo segundo, no sentirá ninguna lentitud.SystemConfig) para preguntar: "¿Cuál fue la fecha del último recálculo exitoso?". Si la fecha es igual a la de hoy, la función simplemente se detiene sin hacer nada.