Files

198 lines
7.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import SamShell from "../components/SamShell";
import {
apiGet,
apiPost,
apiPut,
AUTO_TYPES,
parseJSON,
useMisCursos,
type Asignacion,
type Pregunta,
type Template
} from "../lib/mis";
import { useSession } from "../lib/session";
type Fase = "seleccion" | "rindiendo" | "resultado";
export default function Rendir() {
const { user } = useSession();
const { cursos } = useMisCursos();
const [templates, setTemplates] = useState<Template[]>([]);
const [asignaciones, setAsignaciones] = useState<(Asignacion & { cursoNombre: string })[]>([]);
const [asignacionId, setAsignacionId] = useState("");
const [preguntas, setPreguntas] = useState<Pregunta[]>([]);
const [respuestas, setRespuestas] = useState<Record<string, string>>({});
const [rendicionId, setRendicionId] = useState("");
const [fase, setFase] = useState<Fase>("seleccion");
const [nota, setNota] = useState<{ ob: number; mx: number; ap: boolean } | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
apiGet("/templates").then(setTemplates).catch(() => {});
}, []);
useEffect(() => {
if (!user || cursos.length === 0) return;
(async () => {
const todas: (Asignacion & { cursoNombre: string })[] = [];
for (const c of cursos) {
try {
const as: Asignacion[] = await apiGet(`/asignaciones?origen=${user.origen}&idCursos=${c.idCursos}`);
as.forEach((a) => todas.push({ ...a, cursoNombre: c.nombreCurso }));
} catch {
/* sin asignaciones */
}
}
setAsignaciones(todas);
})();
}, [user, cursos]);
const asig = asignaciones.find((a) => a.id === asignacionId);
const tpl = templates.find((t) => t.id === asig?.templateId);
async function iniciar(e: React.FormEvent) {
e.preventDefault();
setError("");
if (!user || !asig || !tpl) return;
setLoading(true);
try {
const r = await apiPost("/rendiciones/iniciar", { asignacionId, idAlumno: user.idAlumno, respuestas: "[]" });
setRendicionId(r.id);
setPreguntas(await apiGet(`/templates/${tpl.id}/preguntas`));
setRespuestas({});
setFase("rindiendo");
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}
function esCorrecta(p: Pregunta, val: string): boolean | null {
if (p.tipoPregunta === "multiple-choice") {
return val !== "" && Number(val) === Number(parseJSON(p.respuestaCorrecta));
}
if (p.tipoPregunta === "true-false") {
const exp = parseJSON(p.respuestaCorrecta);
return val !== "" && (val === "true") === (exp === true || exp === "true");
}
if (p.tipoPregunta === "short-answer") {
const exp = (parseJSON(p.respuestaCorrecta) ?? p.respuestaCorrecta ?? "").toString().trim().toLowerCase();
return val.trim() !== "" && val.trim().toLowerCase() === exp;
}
return null;
}
async function entregar() {
setError("");
const auto = preguntas.filter((p) => AUTO_TYPES.includes(p.tipoPregunta));
let ob = 0;
const mx = auto.reduce((s, p) => s + p.puntaje, 0);
const detalle = preguntas.map((p) => {
const v = respuestas[p.id] ?? "";
const c = esCorrecta(p, v);
if (c === true) ob += p.puntaje;
return { preguntaId: p.id, tipo: p.tipoPregunta, respuesta: v, correcta: c };
});
try {
await apiPut(`/rendiciones/${rendicionId}/cerrar`, {
puntajeObtenido: ob,
puntajeMaximo: mx,
respuestas: JSON.stringify(detalle)
});
const umbral = tpl?.puntajeAprobacion ?? 70;
setNota({ ob, mx, ap: mx > 0 ? ob >= (mx * umbral) / 100 : true });
setFase("resultado");
} catch (err: any) {
setError(err.message);
}
}
function renderPregunta(p: Pregunta) {
const v = respuestas[p.id] ?? "";
const set = (val: string) => setRespuestas({ ...respuestas, [p.id]: val });
if (p.tipoPregunta === "multiple-choice") {
const opts = (parseJSON(p.opciones) as string[]) ?? [];
return (
<div>
{opts.map((o, idx) => (
<div className="form-check" key={idx}>
<input className="form-check-input" type="radio" name={p.id} checked={v === String(idx)} onChange={() => set(String(idx))} />
<label className="form-check-label">{o}</label>
</div>
))}
</div>
);
}
if (p.tipoPregunta === "true-false") {
return (
<div>
{(["true", "false"] as const).map((b) => (
<div className="form-check form-check-inline" key={b}>
<input className="form-check-input" type="radio" name={p.id} checked={v === b} onChange={() => set(b)} />
<label className="form-check-label">{b === "true" ? "Verdadero" : "Falso"}</label>
</div>
))}
</div>
);
}
if (p.tipoPregunta === "short-answer") {
return <input className="form-control" value={v} onChange={(e) => set(e.target.value)} placeholder="Tu respuesta" />;
}
return <textarea className="form-control" rows={3} value={v} onChange={(e) => set(e.target.value)} placeholder="Tu respuesta (la revisa el profesor)" />;
}
return (
<SamShell title="Rendir" subtitle={tpl ? tpl.nombre : "Elige una de tus pruebas asignadas."} icon="bx-pencil" activeHref="/rendir">
{error && <div className="alert alert-danger">{error}</div>}
{loading && <div className="spinner-border" role="status" />}
{fase === "seleccion" && (
<form onSubmit={iniciar} className="row g-2">
<div className="col-md-8">
<label className="form-label">Mi prueba asignada</label>
<select className="form-select" value={asignacionId} onChange={(e) => setAsignacionId(e.target.value)} required>
<option value="">Seleccione…</option>
{asignaciones.map((a) => (
<option key={a.id} value={a.id}>
{templates.find((t) => t.id === a.templateId)?.nombre ?? "Prueba"} — {a.cursoNombre}
</option>
))}
</select>
</div>
<div className="col-md-4 d-flex align-items-end">
<button className="btn btn-success w-100" disabled={!asignacionId}>Iniciar prueba</button>
</div>
{asignaciones.length === 0 && <p className="text-muted">No tienes pruebas asignadas por ahora.</p>}
</form>
)}
{fase === "rindiendo" && (
<div>
<div className="alert alert-info">{tpl?.nombre} — {tpl?.duracionMin} min — responde y entrega.</div>
{preguntas.map((p, i) => (
<div className="card mb-2" key={p.id}>
<div className="card-body">
<p className="mb-2"><strong>{i + 1}.</strong> {p.pregunta} <span className="badge bg-secondary">{p.puntaje} pts</span></p>
{renderPregunta(p)}
</div>
</div>
))}
<button className="btn btn-primary" onClick={entregar} disabled={preguntas.length === 0}>Entregar prueba</button>
</div>
)}
{fase === "resultado" && nota && (
<div className={`alert ${nota.ap ? "alert-success" : "alert-warning"}`}>
<h4>{nota.ap ? "Aprobado" : "No aprobado"}</h4>
<p className="mb-0">Puntaje: {nota.ob}/{nota.mx} (las preguntas manuales las revisa el profesor).</p>
<a className="btn btn-outline-secondary mt-2" href="/resultados">Ver mis resultados</a>
</div>
)}
</SamShell>
);
}