diff --git a/frontend/src/pages/Cobranza.jsx b/frontend/src/pages/Cobranza.jsx index a77422b..e28412f 100644 --- a/frontend/src/pages/Cobranza.jsx +++ b/frontend/src/pages/Cobranza.jsx @@ -458,6 +458,13 @@ function TarjetasResumen({ r }) { function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) { const [texto, setTexto] = useState(""); const [query, setQuery] = useState(""); + const [porRetirar, setPorRetirar] = useState(new Set()); + + useEffect(() => { + api.alumnosPorRetirar() + .then((r)=>setPorRetirar(new Set((r.matriculas||[]).map(String)))) + .catch(()=>{}); + }, []); const resultados = useMemo(() => { const q = query.trim().toLowerCase(); @@ -508,6 +515,7 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) { {resultados.map((a,i)=>{ const deuda = a.saldo > 0.01; + const retira = porRetirar.has(String(a.matricula)); const grp = agrupacion==="SEDE" ? a.sede : (agrupacion==="ASESOR" ? sectorista : a.programa); const celdas = [ {v:grp, cls:"col-name", al:"left"}, @@ -527,7 +535,9 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) { {v:fmtMoneda("S/ "+a.saldo), al:"right", saldo:true}, ]; return ( - + {celdas.map((c,j)=>( { let activo = true; @@ -561,10 +574,18 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) return () => { activo = false; }; }, [grupo, ano, mes, sectorista, agrupacion]); - const COLS = ["PROGRAMA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO"]; + // Cargar (y recargar) las matrículas marcadas POR RETIRAR para tachar sus filas + const cargarPorRetirar = () => { + api.alumnosPorRetirar() + .then((r) => setPorRetirar(new Set((r.matriculas || []).map(String)))) + .catch(() => {}); + }; + useEffect(() => { cargarPorRetirar(); }, []); + + const COLS = ["PROGRAMA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO","INFO"]; // Columnas redimensionables (igual que la tabla principal): PROGRAMA y ALUMNO más anchas - const anchosDet = COLS.map((h, i) => i === 0 ? 240 : (i === 1 ? 220 : (h.startsWith("%") ? 80 : 110))); + const anchosDet = COLS.map((h, i) => i === 0 ? 240 : (i === 1 ? 220 : (h==="INFO" ? 70 : h.startsWith("%") ? 80 : 110))); const cols = useColumnasAjustables(anchosDet); // Resumen para las 3 tarjetas (siempre visibles) — suma del detalle de alumnos @@ -590,6 +611,16 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) return { total, pagaron, deben }; }, [filas]); + // POR RETIRAR: cantidad de alumnos marcados y suma de su saldo + const retiro = useMemo(() => { + let cant=0, saldo=0; + filas.forEach((f) => { + if (String(f[0]).toUpperCase() === "TOTAL") return; + if (porRetirar.has(String(f[0]))) { cant++; saldo += toMonto(f[13]); } + }); + return { cant, saldo }; + }, [filas, porRetirar]); + return ( {loading ? : @@ -599,11 +630,11 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) {/* Etiquetas de conteo de alumnos */} -
+
- 👥 Total: {conteo.total} alumno{conteo.total!==1?"s":""} + 👥 Total: {conteo.total} + borderRadius:999,background:"#fffbeb",color:"#b45309",fontSize:13,fontWeight:700, + border:"1px solid #fde68a"}}> ⏳ Por pagar: {conteo.deben} + + + 🚫 Por retirar: {retiro.cant} + + + 💸 Saldo: S/ {retiro.saldo.toLocaleString("es-PE",{maximumFractionDigits:0})} + +
{COLS.map((c,i)=>)} - {filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL").map((f, i) => { + {filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL") + .filter((f)=> !soloRetirar || porRetirar.has(String(f[0]))) + .map((f, i) => { const saldo = toMonto(f[13]); const deuda = saldo > 0.01; // Regla "-": si %ANT vacío → CTA/COB ANT a "—" (igual MES y TOT) @@ -629,8 +681,11 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) const mesVacio = !String(f[9]||"").trim() || ["-","—"].includes(String(f[9]).trim()); const totVacio = !String(f[12]||"").trim() || ["-","—"].includes(String(f[12]).trim()); const programa = f[14] || "-"; // programa del alumno (extra del backend) + const retira = porRetirar.has(String(f[0])); // alumno POR RETIRAR return ( - + {f.slice(0, 14).map((v, j) => { // Columna 0: mostrar PROGRAMA en vez de la matrícula let val = (j===0) ? programa : v; @@ -638,7 +693,6 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) if ((j===7||j===8||j===9) && mesVacio) val = "—"; if ((j===10||j===11||j===12) && totVacio) val = "—"; const isPct = [6,9,12].includes(j); - const isMonto = String(val).includes("S/"); let cart = ""; if ([4,5,6].includes(j)) cart = "cart-ant"; else if ([7,8,9].includes(j)) cart = "cart-mes"; @@ -648,7 +702,7 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) return ( ); })} {(() => { - // TOTAL GENERAL del modal — solo alumnos visibles (respeta la sede filtrada) - const alumnos = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL"); + // TOTAL GENERAL del modal — solo alumnos visibles (respeta sede y filtro POR RETIRAR) + const alumnos = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL") + .filter((f)=> !soloRetirar || porRetirar.has(String(f[0]))); if (alumnos.length===0) return null; const sum = (j)=>alumnos.reduce((s,f)=>s+toMonto(f[j]),0); const t = {4:sum(4),5:sum(5),7:sum(7),8:sum(8),10:sum(10),11:sum(11),13:sum(13)}; @@ -684,11 +748,12 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose }) const val=celda(j); return ( ); })} + {/* columna INFO en la fila TOTAL */} ); })()} @@ -696,6 +761,136 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
{c}
); })} + + +
+ style={{textAlign:j===0?"left":"center",whiteSpace:"nowrap"}}> {isPct?:val}
} + {infoAlumno && ( + setInfoAlumno(null)} + onGuardado={cargarPorRetirar} /> + )} + + ); +} + +// Pop-up de INFO por alumno: cambiar estado (EN CURSO / POR RETIRAR) y observaciones. +// Carga y guarda en Supabase (tabla cobranza_estado_alumno). +function ModalInfoAlumno({ alumno, onClose, onGuardado }) { + const [estado, setEstado] = useState("EN CURSO"); + const [nuevaObs, setNuevaObs] = useState(""); + const [observaciones, setObservaciones] = useState([]); + const [cargando, setCargando] = useState(true); + const [guardando, setGuardando] = useState(false); + const [guardado, setGuardado] = useState(false); + + useEffect(() => { + let activo = true; + api.alumnoEstado(alumno.matricula) + .then((r)=>{ if(activo){ setEstado(r.estado||"EN CURSO"); setObservaciones(r.observaciones||[]); } }) + .catch(()=>{}) + .finally(()=>{ if(activo) setCargando(false); }); + return ()=>{ activo=false; }; + }, [alumno.matricula]); + + const agregarObs = () => { + const t = nuevaObs.trim(); + if (!t) return; + const fecha = new Date().toLocaleString("es-PE", { dateStyle:"short", timeStyle:"short" }); + setObservaciones((prev)=>[...prev, { texto:t, fecha }]); + setNuevaObs(""); + setGuardado(false); + }; + + const guardar = async () => { + setGuardando(true); setGuardado(false); + try { + await api.alumnoEstadoGuardar(alumno.matricula, estado, observaciones); + setGuardado(true); + if (onGuardado) onGuardado(); + } catch { + alert("No se pudo guardar. Intenta de nuevo."); + } finally { + setGuardando(false); + } + }; + + return ( + +
+
{alumno.alumno}
+
+ Matrícula: {alumno.matricula} · {alumno.programa} +
+
+ +
+ + +
+ +
+ +
+ setNuevaObs(e.target.value)} + onKeyDown={(e)=>{ if(e.key==="Enter") agregarObs(); }} + placeholder="Escribe una observación…" + style={{flex:1,padding:"8px 10px",fontSize:13,borderRadius:8,border:"1px solid #cbd5e1"}} /> + +
+
+ {observaciones.length===0 ? ( +
Sin observaciones.
+ ) : observaciones.map((o,i)=>( +
+
+
{o.texto}
+
{o.fecha}
+
+ +
+ ))} +
+
+ +
+ + +
+ + {guardado && ( +
+ Guardado correctamente. +
+ )}
); }