447 lines
21 KiB
JavaScript
447 lines
21 KiB
JavaScript
// src/pages/Ocupabilidad.jsx
|
||
import { useState, useEffect, useMemo, useRef } from "react";
|
||
import { api } from "../lib/api";
|
||
import { Loader, ErrorBox, ProgressBar, Filters } from "../components/UI";
|
||
import { useColumnasAjustables } from "../lib/useColumnasAjustables";
|
||
import Modal from "../components/Modal";
|
||
import { setVistaActual, onRefrescar } from "../lib/vistaActual";
|
||
|
||
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
|
||
const ANOS = [2024, 2025, 2026];
|
||
const SEDES = ["TODOS", "LIMA", "AREQUIPA", "PIURA", "TRUJILLO"];
|
||
const PROGRAMAS = ["TODOS", "SEMINARIOS", "OTROS", "TEAC", "TERC"];
|
||
|
||
const COLS = ["PROGRAMA","FECHA INICIO","DÍAS PARA INICIO","TOTAL INSCRITOS","RETIRADOS","INSCRITOS EN CURSO","META INSCRITOS","AVANCE INSCRITOS","INSCRITOS MES","INSCRITOS P.C","INSCRITOS REFRIPERU","INSCRITOS CONTINUIDAD","OPC."];
|
||
|
||
function toNum(v) { const n = parseFloat(String(v).replace("%","").trim()); return isNaN(n) ? 0 : n; }
|
||
|
||
export default function Ocupabilidad() {
|
||
const hoy = new Date();
|
||
const [ano, setAno] = useState(hoy.getFullYear());
|
||
// meses = array de meses seleccionados (1-12). Permite elegir varios.
|
||
const [meses, setMeses] = useState([hoy.getMonth() + 1]);
|
||
const [sede, setSede] = useState("TODOS");
|
||
const [programa, setPrograma] = useState("TODOS");
|
||
const [mostrarRepro, setMostrarRepro] = useState(true);
|
||
const [datos, setDatos] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState(null);
|
||
const [modalCurso, setModalCurso] = useState(null); // curso seleccionado para "Ver alumnos"
|
||
const cols = useColumnasAjustables([280, 110, 120, 110, 100, 120, 110, 120, 100, 100, 130, 140, 80]);
|
||
|
||
// Clave estable de meses (ordenados) para las dependencias del effect.
|
||
const mesesKey = useMemo(() => [...meses].sort((a, b) => a - b).join(","), [meses]);
|
||
|
||
useEffect(() => {
|
||
let activo = true;
|
||
const lista = mesesKey ? mesesKey.split(",").map(Number) : [];
|
||
setVistaActual(`ocupabilidad:${ano}|${mesesKey}|${sede}|${programa}`);
|
||
const cargar = () => {
|
||
setLoading(true); setError(null);
|
||
if (!lista.length) { setDatos([]); setLoading(false); return; }
|
||
// Una llamada por cada mes seleccionado; se juntan todas las filas.
|
||
Promise.all(lista.map((m) => api.ocupabilidad(ano, m, sede, programa)))
|
||
.then((resArr) => {
|
||
if (!activo) return;
|
||
const juntos = [];
|
||
resArr.forEach((res) => (res.datos || []).forEach((d) => juntos.push(d)));
|
||
setDatos(juntos);
|
||
setLoading(false);
|
||
})
|
||
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
|
||
};
|
||
cargar();
|
||
const off = onRefrescar(cargar);
|
||
return () => { activo = false; off(); };
|
||
}, [ano, mesesKey, sede, programa]);
|
||
|
||
// Toggle "Mostrar Reprogramados": si está apagado, ocultar filas de inicio
|
||
const datosVisibles = useMemo(() => {
|
||
if (mostrarRepro) return datos;
|
||
return datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO");
|
||
}, [datos, mostrarRepro]);
|
||
|
||
const kpis = useMemo(() => {
|
||
if (!datosVisibles.length) return null;
|
||
const esRepro = (d) => String(d.dias_para_inicio).trim().toUpperCase() === "REPROGRAMADO";
|
||
// Las filas REPROGRAMADO (inicios) no entran en las métricas (igual que escritorio)
|
||
const validos = datosVisibles.filter((d) => !esRepro(d));
|
||
const insc = validos.reduce((s, d) => s + (parseInt(d.Inscritos_Activos) || 0), 0);
|
||
const totalInsc = validos.reduce((s, d) => s + (parseInt(d.Inscritos_Totales) || 0), 0);
|
||
const retirados = validos.reduce((s, d) => s + (parseInt(d.Retirados ?? d.Inscritos_Retirados) || 0), 0);
|
||
const meta = validos.reduce((s, d) => s + (parseInt(d.Meta_Curso) || 0), 0);
|
||
return {
|
||
cursos: validos.length, // solo programados (sin reprogramados)
|
||
reprogramados: datosVisibles.filter(esRepro).length, // inicios rojos (REPROGRAMADO)
|
||
inscritos: totalInsc, // columna TOTAL INSCRITOS
|
||
meta,
|
||
avance: meta > 0 ? (totalInsc / meta * 100) : 0,
|
||
retirados,
|
||
desercion: totalInsc > 0 ? (retirados / totalInsc * 100) : 0,
|
||
sobreMeta: validos.filter((d) => (parseInt(d.Inscritos_Totales)||0) >= (parseInt(d.Meta_Curso)||1)).length,
|
||
pc: validos.reduce((s, d) => s + (parseInt(d.Inscritos_PC) || 0), 0),
|
||
};
|
||
}, [datosVisibles]);
|
||
|
||
const totales = useMemo(() => {
|
||
const t = { mes:0, total:0, pc:0, retirados:0, activos:0, refri:0, cont:0, meta:0 };
|
||
datosVisibles.forEach((d) => {
|
||
t.mes += parseInt(d.Inscritos_Mes)||0;
|
||
t.total += parseInt(d.Inscritos_Totales)||0;
|
||
t.pc += parseInt(d.Inscritos_PC)||0;
|
||
t.retirados += parseInt(d.Retirados ?? d.Inscritos_Retirados)||0;
|
||
t.activos += parseInt(d.Inscritos_Activos)||0;
|
||
t.refri += parseInt(d.Descuento)||0;
|
||
t.cont += parseInt(d.Inscritos_Continuidad)||0;
|
||
t.meta += parseInt(d.Meta_Curso)||0;
|
||
});
|
||
return t;
|
||
}, [datosVisibles]);
|
||
|
||
const KPI_CARDS = kpis ? [
|
||
["📚","Cursos Activos", kpis.cursos, "programas este mes"],
|
||
["🔁","Cursos Reprogramados", kpis.reprogramados, "inicios reprogramados"],
|
||
["👥","Total Inscritos", kpis.inscritos, "alumnos en curso"],
|
||
["🎯","Meta Total", kpis.meta, "inscritos objetivo"],
|
||
["📈","Avance Inscritos", `${kpis.avance.toFixed(1)}%`, "total inscritos / meta"],
|
||
["📉","Deserción", `${kpis.desercion.toFixed(1)}%`, `${kpis.retirados} retirados / ${kpis.inscritos} inscritos`],
|
||
] : [];
|
||
|
||
// Estilo para inmovilizar la fila TOTAL GENERAL abajo (sticky), igual que Cobranza.
|
||
const ST = { position: "sticky", bottom: 0, zIndex: 9, backgroundColor: "#eff6ff",
|
||
boxShadow: "inset 0 1px 0 0 #cbd5e1" };
|
||
|
||
async function exportarExcel() {
|
||
if (!datosVisibles.length) return;
|
||
// Cargar SheetJS desde CDN solo cuando se necesita
|
||
const XLSX = await import("xlsx");
|
||
const filas = datosVisibles.map((d) => ({
|
||
"PROGRAMA": d.programa_frecuencia || d.dsc_programa || "",
|
||
"FECHA INICIO": d.fch_inicio || "",
|
||
"DÍAS PARA INICIO": d.dias_para_inicio ?? "",
|
||
"TOTAL INSCRITOS": parseInt(d.Inscritos_Totales)||0,
|
||
"RETIRADOS": parseInt(d.Retirados ?? d.Inscritos_Retirados)||0,
|
||
"INSCRITOS EN CURSO": parseInt(d.Inscritos_Activos)||0,
|
||
"META INSCRITOS": parseInt(d.Meta_Curso)||0,
|
||
"AVANCE INSCRITOS": d.Avance_Inscritos || "",
|
||
"INSCRITOS MES": parseInt(d.Inscritos_Mes)||0,
|
||
"INSCRITOS P.C": parseInt(d.Inscritos_PC)||0,
|
||
"INSCRITOS REFRIPERU": parseInt(d.Descuento)||0,
|
||
"INSCRITOS CONTINUIDAD": parseInt(d.Inscritos_Continuidad)||0,
|
||
}));
|
||
const ws = XLSX.utils.json_to_sheet(filas);
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, "Ocupabilidad");
|
||
XLSX.writeFile(wb, `cursos_${ano}_${mesesKey.replace(/,/g, "-")}.xlsx`);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<h1 className="page-title">📊 Ocupabilidad</h1>
|
||
|
||
<Filters>
|
||
<SelectBonito label="Año" value={ano} options={ANOS} onChange={(v) => setAno(+v)} minWidth={110} />
|
||
<MesesMulti label="Mes" meses={meses} onChange={setMeses} />
|
||
<SelectBonito label="Sede" value={sede} options={SEDES} onChange={setSede} minWidth={140} />
|
||
<SelectBonito label="Programa" value={programa} options={PROGRAMAS} onChange={setPrograma} minWidth={150} />
|
||
<div className="filter-group">
|
||
<label>Reprogramados</label>
|
||
<label style={{display:"flex",alignItems:"center",gap:6,height:38,cursor:"pointer",fontSize:13}}>
|
||
<input type="checkbox" checked={mostrarRepro} onChange={(e)=>setMostrarRepro(e.target.checked)} />
|
||
Mostrar
|
||
</label>
|
||
</div>
|
||
<button className="btn btn-primary" style={{height:38,marginLeft:"auto"}} onClick={exportarExcel} disabled={!datosVisibles.length}>
|
||
⬇️ Exportar Excel
|
||
</button>
|
||
</Filters>
|
||
|
||
{loading ? <Loader text="Calculando ocupabilidad..." /> :
|
||
error ? <ErrorBox msg={error} /> :
|
||
<>
|
||
<div className="kpis">
|
||
{KPI_CARDS.map(([ico,label,val,sub]) => (
|
||
<div className="kpi" key={label}>
|
||
<div className="ico">{ico}</div>
|
||
<div className="label" style={{fontSize:15}}>{label}</div>
|
||
<div className="value">{val}</div>
|
||
<div className="sub">{sub}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}>
|
||
<table {...cols.tableProps}>
|
||
<cols.ColGroup />
|
||
<thead><tr>{COLS.map((c, i) => <th key={c} style={{position:"sticky",top:0,zIndex:2}}>{c}<cols.Resizer index={i} /></th>)}</tr></thead>
|
||
<tbody>
|
||
{datosVisibles.map((d, i) => {
|
||
const prog = d.programa_frecuencia || d.dsc_programa || "";
|
||
const dias = parseInt(d.dias_para_inicio);
|
||
const pct = toNum(d.Avance_Inscritos);
|
||
// Inicio reprogramado (existe en Supabase pero no en SQL este mes) → fila roja
|
||
const esInicio = String(d.dias_para_inicio).trim().toUpperCase() === "REPROGRAMADO";
|
||
const cel = (v) => esInicio ? "-" : v;
|
||
return (
|
||
<tr key={i} className={esInicio ? "row-inicio" : ""}>
|
||
<td className="col-name" style={{minWidth:300,maxWidth:480,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{prog}</td>
|
||
<td>{d.fch_inicio || ""}</td>
|
||
<td style={!esInicio && dias < 0 ? {color:"#dc2626",fontWeight:700} : {}}>{d.dias_para_inicio ?? ""}</td>
|
||
<td>{cel(parseInt(d.Inscritos_Totales)||0)}</td>
|
||
<td>{cel(parseInt(d.Retirados ?? d.Inscritos_Retirados)||0)}</td>
|
||
<td>{cel(parseInt(d.Inscritos_Activos)||0)}</td>
|
||
<td>{cel(parseInt(d.Meta_Curso)||0)}</td>
|
||
<td>{esInicio ? "-" : <ProgressBar pct={pct} />}</td>
|
||
<td>{cel(parseInt(d.Inscritos_Mes)||0)}</td>
|
||
<td>{cel(parseInt(d.Inscritos_PC)||0)}</td>
|
||
<td>{cel(parseInt(d.Descuento)||0)}</td>
|
||
<td>{cel(parseInt(d.Inscritos_Continuidad)||0)}</td>
|
||
<td>
|
||
{esInicio ? "-" : (
|
||
<button className="btn btn-ghost" style={{padding:"4px 10px",fontSize:11}}
|
||
onClick={() => setModalCurso({ num_indice: d.num_indice, programa: prog })}>
|
||
👁️ Ver
|
||
</button>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
{datosVisibles.length > 0 && (
|
||
<tr className="total-row">
|
||
<td className="col-name" style={ST}>TOTAL GENERAL</td>
|
||
<td style={ST}>—</td><td style={ST}>—</td>
|
||
<td style={ST}>{totales.total}</td>
|
||
<td style={ST}>{totales.retirados}</td>
|
||
<td style={ST}>{totales.activos}</td>
|
||
<td style={ST}>{totales.meta}</td>
|
||
<td style={ST}><ProgressBar pct={totales.meta>0 ? totales.total/totales.meta*100 : 0} /></td>
|
||
<td style={ST}>{totales.mes}</td><td style={ST}>{totales.pc}</td><td style={ST}>{totales.refri}</td><td style={ST}>{totales.cont}</td>
|
||
<td style={ST}>—</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>}
|
||
|
||
{modalCurso && (
|
||
<ModalAlumnosCurso
|
||
numIndice={modalCurso.num_indice}
|
||
programa={modalCurso.programa}
|
||
onClose={() => setModalCurso(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Select de una opcion, desplegable custom (mismo estilo que el filtro de Mes).
|
||
// Opciones: array de strings o de {value, label}.
|
||
function SelectBonito({ label, value, options, onChange, minWidth = 150 }) {
|
||
const [abierto, setAbierto] = useState(false);
|
||
const ref = useRef(null);
|
||
const opts = options.map((o) => (typeof o === "object" ? o : { value: o, label: String(o) }));
|
||
const actual = opts.find((o) => String(o.value) === String(value));
|
||
|
||
useEffect(() => {
|
||
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
|
||
document.addEventListener("mousedown", fuera);
|
||
return () => document.removeEventListener("mousedown", fuera);
|
||
}, []);
|
||
|
||
return (
|
||
<div className="filter-group" ref={ref} style={{ position: "relative", minWidth }}>
|
||
<label>{label}</label>
|
||
<button type="button" onClick={() => setAbierto((v) => !v)}
|
||
style={{ height: 38, width: "100%", textAlign: "left", padding: "0 12px",
|
||
border: "1px solid #cbd5e1", borderRadius: 8, background: "#fff",
|
||
cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center",
|
||
justifyContent: "space-between", gap: 8 }}>
|
||
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||
{actual ? actual.label : "—"}
|
||
</span>
|
||
<span style={{ color: "#94a3b8", fontSize: 11, transition: "transform .15s",
|
||
transform: abierto ? "rotate(180deg)" : "none" }}>▼</span>
|
||
</button>
|
||
{abierto && (
|
||
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
|
||
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
|
||
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6,
|
||
maxHeight: 300, overflowY: "auto" }}>
|
||
{opts.map((o) => {
|
||
const on = String(o.value) === String(value);
|
||
return (
|
||
<div key={o.value} onClick={() => { onChange(o.value); setAbierto(false); }}
|
||
style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", fontSize: 13,
|
||
fontWeight: on ? 700 : 500,
|
||
color: on ? "#1e3a5f" : "#334155",
|
||
background: on ? "#eff6ff" : "transparent" }}
|
||
onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "#f8fafc"; }}
|
||
onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}>
|
||
{o.label}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Filtro de MES con casillas (multi-seleccion). meses = array de 1..12.
|
||
function MesesMulti({ label, meses, onChange }) {
|
||
const [abierto, setAbierto] = useState(false);
|
||
const ref = useRef(null);
|
||
|
||
useEffect(() => {
|
||
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
|
||
document.addEventListener("mousedown", fuera);
|
||
return () => document.removeEventListener("mousedown", fuera);
|
||
}, []);
|
||
|
||
const toggle = (m) => {
|
||
const set = new Set(meses);
|
||
if (set.has(m)) set.delete(m); else set.add(m);
|
||
onChange([...set].sort((a, b) => a - b));
|
||
};
|
||
|
||
const texto = meses.length === 0 ? "— Elegir —"
|
||
: meses.length === 1 ? MESES[meses[0] - 1]
|
||
: meses.length === 12 ? "TODOS"
|
||
: `${meses.length} meses`;
|
||
|
||
return (
|
||
<div className="filter-group" ref={ref} style={{ position: "relative", minWidth: 200 }}>
|
||
<label>{label}</label>
|
||
<button type="button" onClick={() => setAbierto((v) => !v)}
|
||
style={{ height: 38, width: "100%", textAlign: "left", padding: "0 12px",
|
||
border: "1px solid #cbd5e1", borderRadius: 8, background: "#fff",
|
||
cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center",
|
||
justifyContent: "space-between", gap: 8 }}>
|
||
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{texto}</span>
|
||
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
|
||
</button>
|
||
{abierto && (
|
||
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
|
||
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
|
||
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, minWidth: 180,
|
||
maxHeight: 300, overflowY: "auto" }}>
|
||
<div style={{ display: "flex", gap: 6, padding: "4px 6px 8px", borderBottom: "1px solid #f1f5f9", marginBottom: 4 }}>
|
||
<button type="button" onClick={() => onChange(MESES.map((_, i) => i + 1))}
|
||
style={{ flex: 1, fontSize: 11, padding: "4px 6px", border: "1px solid #e2e8f0",
|
||
borderRadius: 6, background: "#f8fafc", cursor: "pointer" }}>Todos</button>
|
||
<button type="button" onClick={() => onChange([])}
|
||
style={{ flex: 1, fontSize: 11, padding: "4px 6px", border: "1px solid #e2e8f0",
|
||
borderRadius: 6, background: "#f8fafc", cursor: "pointer" }}>Ninguno</button>
|
||
</div>
|
||
{MESES.map((m, i) => {
|
||
const val = i + 1;
|
||
const on = meses.includes(val);
|
||
return (
|
||
<label key={m} style={{ display: "flex", alignItems: "center", gap: 8,
|
||
padding: "6px 8px", borderRadius: 6, cursor: "pointer", fontSize: 13,
|
||
background: on ? "#eff6ff" : "transparent" }}>
|
||
<input type="checkbox" checked={on} onChange={() => toggle(val)} />
|
||
{m}
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ModalAlumnosCurso({ numIndice, programa, onClose }) {
|
||
const [alumnos, setAlumnos] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
let activo = true;
|
||
setLoading(true);
|
||
api.ocupabilidadAlumnos(numIndice)
|
||
.then((res) => { if (activo) { setAlumnos(res.alumnos || []); setLoading(false); } })
|
||
.catch(() => { if (activo) { setAlumnos([]); setLoading(false); } });
|
||
return () => { activo = false; };
|
||
}, [numIndice]);
|
||
|
||
// Colores por estado
|
||
const colorEstado = (e) => {
|
||
if (e === "ALU") return { bg:"#ecfdf5", fg:"#047857", bd:"#a7f3d0" };
|
||
if (e === "PRE") return { bg:"#eff6ff", fg:"#1e40af", bd:"#bfdbfe" };
|
||
if (e === "RET") return { bg:"#fef2f2", fg:"#b91c1c", bd:"#fecaca" };
|
||
return { bg:"#f1f5f9", fg:"#475569", bd:"#e2e8f0" };
|
||
};
|
||
|
||
// Conteo por estado
|
||
const conteo = useMemo(() => {
|
||
const c = {};
|
||
alumnos.forEach((a) => { c[a.estado] = (c[a.estado] || 0) + 1; });
|
||
return c;
|
||
}, [alumnos]);
|
||
|
||
return (
|
||
<Modal title={`👥 Alumnos Inscritos — ${programa}`} onClose={onClose} width={1050}>
|
||
{loading ? <Loader text="Cargando alumnos..." /> :
|
||
alumnos.length === 0 ? <div style={{color:"#94a3b8",padding:20,textAlign:"center"}}>Sin alumnos.</div> :
|
||
<>
|
||
{/* Etiquetas de conteo */}
|
||
<div style={{display:"flex",gap:10,marginBottom:14,flexWrap:"wrap"}}>
|
||
<span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
|
||
borderRadius:999,background:"#eff6ff",color:"#1e40af",fontSize:13,fontWeight:700,
|
||
border:"1px solid #bfdbfe"}}>
|
||
👥 Total: {alumnos.length} alumno{alumnos.length!==1?"s":""}
|
||
</span>
|
||
{["ALU","PRE","RET"].map((e)=> conteo[e] ? (
|
||
<span key={e} style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
|
||
borderRadius:999,background:colorEstado(e).bg,color:colorEstado(e).fg,fontSize:13,fontWeight:700,
|
||
border:`1px solid ${colorEstado(e).bd}`}}>
|
||
{e}: {conteo[e]}
|
||
</span>
|
||
) : null)}
|
||
</div>
|
||
<div className="table-wrap" style={{overflowX:"auto"}}>
|
||
<table className="cob-table" style={{minWidth:900}}>
|
||
<thead><tr>
|
||
<th style={{textAlign:"left"}}>ALUMNO</th>
|
||
<th>ESTADO</th>
|
||
<th style={{textAlign:"left"}}>CURSO ANTERIOR (TEAC/TERC)</th>
|
||
<th>ESTADO ANTERIOR</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{alumnos.map((a, i) => {
|
||
const c = colorEstado(a.estado);
|
||
const tieneAnt = a.curso_anterior && a.curso_anterior !== "-";
|
||
const cAnt = colorEstado(a.estado_anterior);
|
||
return (
|
||
<tr key={i}>
|
||
<td className="col-name" style={{textAlign:"left"}}>{a.alumno}</td>
|
||
<td style={{textAlign:"center"}}>
|
||
<span style={{display:"inline-block",padding:"2px 10px",borderRadius:999,
|
||
background:c.bg,color:c.fg,fontWeight:700,fontSize:12,border:`1px solid ${c.bd}`}}>
|
||
{a.estado}
|
||
</span>
|
||
</td>
|
||
<td className="col-name" style={{textAlign:"left",color:tieneAnt?undefined:"#cbd5e1"}}>
|
||
{a.curso_anterior || "-"}
|
||
</td>
|
||
<td style={{textAlign:"center"}}>
|
||
{tieneAnt ? (
|
||
<span style={{display:"inline-block",padding:"2px 10px",borderRadius:999,
|
||
background:cAnt.bg,color:cAnt.fg,fontWeight:700,fontSize:12,border:`1px solid ${cAnt.bd}`}}>
|
||
{a.estado_anterior}
|
||
</span>
|
||
) : <span style={{color:"#cbd5e1"}}>-</span>}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>}
|
||
</Modal>
|
||
);
|
||
}
|