feat: frontend alumnos sam-pruebas (RUT+clave, rendir y mis resultados)
This commit is contained in:
commit
e6d283e184
26 files changed
+2555
No files matched your search
@@ -0,0 +1,133 @@
|
||||
// Cliente del backend SAM: GET /api/sam-pruebas/* (repo ICHN/sam, prod desplegado).
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:5000";
|
||||
const B = `${API_BASE}/api/sam-pruebas`;
|
||||
|
||||
async function req(path: string, init?: RequestInit) {
|
||||
const r = await fetch(B + path, { cache: "no-store", ...init });
|
||||
if (!r.ok) {
|
||||
const t = await r.text().catch(() => "");
|
||||
throw new Error(t.slice(0, 300) || `HTTP ${r.status}`);
|
||||
}
|
||||
if (r.status === 204) return null;
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export const apiGet = (path: string) => req(path);
|
||||
export const apiPost = (path: string, body: unknown) =>
|
||||
req(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||||
export const apiPut = (path: string, body: unknown) =>
|
||||
req(path, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||||
|
||||
/** JSON.parse tolerante (columnas ::text que pueden venir null o malformadas). */
|
||||
export function parseJSON(s: string | null | undefined): any {
|
||||
if (!s) return null;
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Creador {
|
||||
idUsuario: string;
|
||||
nombres: string;
|
||||
apPaterno: string;
|
||||
apMaterno?: string;
|
||||
email: string;
|
||||
tipo?: string;
|
||||
}
|
||||
export interface Curso {
|
||||
idCursos: number;
|
||||
nombreCurso: string;
|
||||
nivelCurso?: number;
|
||||
duracion?: number;
|
||||
}
|
||||
export interface Alumno {
|
||||
idAlumno: string;
|
||||
nombres: string;
|
||||
apPaterno: string;
|
||||
apMaterno?: string;
|
||||
email: string;
|
||||
idCursoAbierto: number;
|
||||
idCursos: number;
|
||||
nombreCurso: string;
|
||||
}
|
||||
export interface Template {
|
||||
id: string;
|
||||
nombre: string;
|
||||
descripcion?: string;
|
||||
idCursos?: number;
|
||||
nivel?: string;
|
||||
tipoCurso: string;
|
||||
tipoPrueba: string;
|
||||
duracionMin: number;
|
||||
puntajeAprobacion: number;
|
||||
puntajeTotal: number;
|
||||
instrucciones?: string;
|
||||
usoCount: number;
|
||||
creadoPor?: string;
|
||||
}
|
||||
export interface Pregunta {
|
||||
id: string;
|
||||
templateId: string;
|
||||
sectionId?: string;
|
||||
orden: number;
|
||||
tipoPregunta: string;
|
||||
pregunta: string;
|
||||
opciones?: string;
|
||||
respuestaCorrecta?: string;
|
||||
explicacion?: string;
|
||||
puntaje: number;
|
||||
metadata?: string;
|
||||
}
|
||||
export interface BancoItem {
|
||||
id: string;
|
||||
pregunta: string;
|
||||
tipoPregunta: string;
|
||||
puntaje: number;
|
||||
dificultad: string;
|
||||
skill?: string;
|
||||
activo: boolean;
|
||||
usoCount: number;
|
||||
}
|
||||
export interface Asignacion {
|
||||
id: string;
|
||||
templateId: string;
|
||||
origen: string;
|
||||
idCursos?: number;
|
||||
idCursoAbierto?: number;
|
||||
fechaInicio?: string;
|
||||
fechaFin?: string;
|
||||
activo: boolean;
|
||||
asignadoPor?: string;
|
||||
}
|
||||
export interface Rendicion {
|
||||
id: string;
|
||||
asignacionId: string;
|
||||
idAlumno: string;
|
||||
intento: number;
|
||||
puntajeObtenido?: number;
|
||||
puntajeMaximo?: number;
|
||||
aprobado?: boolean;
|
||||
inicio: string;
|
||||
termino?: string;
|
||||
respuestas: string;
|
||||
}
|
||||
|
||||
export const TIPOS_PRUEBA = ["CA", "MWT", "MOT", "FOT", "FWT"];
|
||||
export const TIPOS_PREGUNTA = [
|
||||
"multiple-choice",
|
||||
"true-false",
|
||||
"short-answer",
|
||||
"essay",
|
||||
"matching",
|
||||
"ordering",
|
||||
"fill-in-the-blanks",
|
||||
"audio-response"
|
||||
];
|
||||
/** Tipos con corrección automática en el frontend. */
|
||||
export const AUTO_TYPES = ["multiple-choice", "true-false", "short-answer"];
|
||||
|
||||
export function nombreCompleto(a: { nombres: string; apPaterno: string; apMaterno?: string }) {
|
||||
return `${a.nombres} ${a.apPaterno}${a.apMaterno ? " " + a.apMaterno : ""}`;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet, type Alumno } from "./api";
|
||||
import { useSession } from "./session";
|
||||
|
||||
export * from "./api";
|
||||
|
||||
export interface MiCurso {
|
||||
idCursos: number;
|
||||
nombreCurso: string;
|
||||
}
|
||||
|
||||
/** Cursos con matrícula activa del alumno conectado. */
|
||||
export function useMisCursos() {
|
||||
const { user } = useSession();
|
||||
const [rows, setRows] = useState<Alumno[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
apiGet(`/alumnos?origen=${user.origen}&idAlumno=${encodeURIComponent(user.idAlumno)}`)
|
||||
.then((r: Alumno[]) => {
|
||||
setRows(r);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
setError(e.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
const cursos: MiCurso[] = [
|
||||
...new Map(rows.map((r) => [r.idCursos, { idCursos: r.idCursos, nombreCurso: r.nombreCurso }])).values()
|
||||
];
|
||||
return { cursos, rows, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
export interface AlumnoUser {
|
||||
idAlumno: string;
|
||||
nombres: string;
|
||||
apPaterno: string;
|
||||
apMaterno?: string;
|
||||
email: string;
|
||||
/** De qué tabla viene: SAM (sige_sam_v3.alumnos) o EMPRESA (sige_sam_empresa.alumnos). */
|
||||
origen: string;
|
||||
}
|
||||
|
||||
interface SessionCtx {
|
||||
user: AlumnoUser | null;
|
||||
loading: boolean;
|
||||
login: (idAlumno: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const Ctx = createContext<SessionCtx | null>(null);
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<AlumnoUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/auth/me")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((d) => setUser(d?.user ?? null))
|
||||
.catch(() => setUser(null))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function login(idAlumno: string, password: string) {
|
||||
const r = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ idAlumno, password })
|
||||
});
|
||||
const d = await r.json().catch(() => null);
|
||||
if (!r.ok || !d?.success) throw new Error(d?.message || "Credenciales inválidas");
|
||||
setUser(d.user);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
return <Ctx.Provider value={{ user, loading, login, logout }}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
const ctx = useContext(Ctx);
|
||||
if (!ctx) throw new Error("useSession fuera de SessionProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in new issue
Block a user