reporte por mes y año

This commit is contained in:
2026-03-26 18:43:54 -06:00
parent 4ab004c5ea
commit bb90e40906
2 changed files with 181 additions and 105 deletions
+163 -105
View File
@@ -10,6 +10,7 @@ import { saveAs } from "file-saver";
interface Periodo { interface Periodo {
id_periodo: number; id_periodo: number;
semestre: string; semestre: string;
fecha_inicio_servicio: string;
} }
interface ApiResponse { interface ApiResponse {
@@ -21,6 +22,8 @@ interface ApiResponse {
total: string; total: string;
} }
type Tipo = "periodo" | "mes" | "anio";
type TipoConteo = "WINDOWS" | "MACINTOSH" | "LINUX" | "PROFESORES"; type TipoConteo = "WINDOWS" | "MACINTOSH" | "LINUX" | "PROFESORES";
interface TablaRow { interface TablaRow {
@@ -37,13 +40,20 @@ interface TablaRow {
export default function Inscripciones() { export default function Inscripciones() {
const [periodo, setPeriodo] = useState<Periodo[]>([]); const [periodo, setPeriodo] = useState<Periodo[]>([]);
const [periodoInicio, setPeriodoInicio] = useState<number | null>(null); const [tipo, setTipo] = useState<Tipo>("periodo");
const [periodoFin, setPeriodoFin] = useState<number | null>(null);
const [periodoInicio, setPeriodoInicio] = useState<any>(null);
const [periodoFin, setPeriodoFin] = useState<any>(null);
const [anio, setAnio] = useState<number | null>(null);
const [tablaData, setTablaData] = useState<TablaRow[]>([]); const [tablaData, setTablaData] = useState<TablaRow[]>([]);
const [rawData, setRawData] = useState<ApiResponse[]>([]); const [rawData, setRawData] = useState<ApiResponse[]>([]);
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
// Obtener periodos
useEffect(() => { useEffect(() => {
const getPeriodo = async () => { const getPeriodo = async () => {
const response = await axios.get(`${envConfig.apiUrl}/periodo`, { headers }); const response = await axios.get(`${envConfig.apiUrl}/periodo`, { headers });
@@ -52,77 +62,101 @@ export default function Inscripciones() {
getPeriodo(); getPeriodo();
}, []); }, []);
const handleBuscar = async (e: React.FormEvent) => { // Obtener años desde fechas
e.preventDefault(); const aniosDisponibles = Array.from(
new Set(
periodo.map((p) =>
new Date(p.fecha_inicio_servicio).getFullYear()
)
)
).sort();
if (!periodoInicio || !periodoFin) { // ============================
alert("Selecciona ambos periodos"); // 🔍 BUSCAR
return; // ============================
} const handleBuscar = async (e: React.FormEvent) => {
e.preventDefault();
if (periodoInicio > periodoFin) { let url = "";
alert("El periodo inicio no puede ser mayor al final");
return;
}
const esMismoPeriodo = periodoInicio === periodoFin; if (tipo === "periodo") {
if (!periodoInicio || !periodoFin) {
try { alert("Selecciona ambos periodos");
const url = esMismoPeriodo return;
? `${envConfig.apiUrl}/alumno-inscrito/inscritos/${periodoInicio}` // 👈 uno solo
: `${envConfig.apiUrl}/alumno-inscrito/inscritos/${periodoInicio}/${periodoFin}`; // 👈 rango
const response = await axios.get(url, { headers });
const data: ApiResponse[] = response.data;
setRawData(data);
const agrupado: Record<string, TablaRow> = {};
data.forEach((item) => {
const key = `${item.carrera}-${item.semestre}`;
if (!agrupado[key]) {
agrupado[key] = {
carrera: item.carrera,
semestre: item.semestre,
WINDOWS: 0,
MACINTOSH: 0,
LINUX: 0,
PROFESORES: 0,
TOTAL: 0,
femenino: 0,
masculino: 0,
};
} }
const fila = agrupado[key]; url = `${envConfig.apiUrl}/alumno-inscrito/inscritos/periodo/${periodoInicio}/${periodoFin}`;
const tipoProfesor = item.profesor as TipoConteo; }
const total = Number(item.total); if (tipo === "anio") {
const fem = Number(item.femenino); if (!periodoInicio || !periodoFin) {
const masc = Number(item.masculino); alert("Selecciona ambos años");
return;
}
fila[tipoProfesor] += total; url = `${envConfig.apiUrl}/alumno-inscrito/inscritos/anio/${periodoInicio}/${periodoFin}`;
fila.TOTAL += total; }
fila.femenino += fem;
fila.masculino += masc;
});
setTablaData(Object.values(agrupado)); if (tipo === "mes") {
} catch (error) { if (!anio) {
console.error("Error al obtener inscritos", error); alert("Selecciona un año");
return;
} }
};
// Export Excel url = `${envConfig.apiUrl}/alumno-inscrito/inscritos/mes/${anio}`;
}
try {
console.log("URL:", url);
const response = await axios.get(url, { headers });
const data: ApiResponse[] = response.data;
setRawData(data);
const agrupado: Record<string, TablaRow> = {};
data.forEach((item) => {
const key = `${item.carrera}-${item.semestre}`;
if (!agrupado[key]) {
agrupado[key] = {
carrera: item.carrera,
semestre: item.semestre,
WINDOWS: 0,
MACINTOSH: 0,
LINUX: 0,
PROFESORES: 0,
TOTAL: 0,
femenino: 0,
masculino: 0,
};
}
const fila = agrupado[key];
const tipoProfesor = item.profesor as TipoConteo;
const total = Number(item.total);
const fem = Number(item.femenino);
const masc = Number(item.masculino);
fila[tipoProfesor] += total;
fila.TOTAL += total;
fila.femenino += fem;
fila.masculino += masc;
});
setTablaData(Object.values(agrupado));
} catch (error) {
console.error("Error al obtener inscritos", error);
}
};
const exportarExcel = () => { const exportarExcel = () => {
if (!rawData.length) return; if (!rawData.length) return;
const periodosUnicos = Array.from( const periodosUnicos = Array.from(
new Set(rawData.map((t) => t.semestre)) new Set(rawData.map((t) => t.semestre))
).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); );
const carrerasUnicas = Array.from( const carrerasUnicas = Array.from(
new Set(rawData.map((t) => t.carrera)) new Set(rawData.map((t) => t.carrera))
@@ -132,8 +166,6 @@ export default function Inscripciones() {
const fila: any = { Carrera: carrera }; const fila: any = { Carrera: carrera };
let totalFila = 0; let totalFila = 0;
let totalFemenino = 0;
let totalMasculino = 0;
periodosUnicos.forEach((periodo) => { periodosUnicos.forEach((periodo) => {
const items = rawData.filter( const items = rawData.filter(
@@ -147,36 +179,16 @@ export default function Inscripciones() {
fila[periodo] = totalPeriodo; fila[periodo] = totalPeriodo;
totalFila += totalPeriodo; totalFila += totalPeriodo;
totalFemenino += items.reduce(
(acc, item) => acc + Number(item.femenino),
0
);
totalMasculino += items.reduce(
(acc, item) => acc + Number(item.masculino),
0
);
}); });
fila["Femenino"] = totalFemenino;
fila["Masculino"] = totalMasculino;
fila["Total"] = totalFila; fila["Total"] = totalFila;
return fila; return fila;
}); });
const worksheet = XLSX.utils.json_to_sheet(dataExcel); const worksheet = XLSX.utils.json_to_sheet(dataExcel);
worksheet["!cols"] = [
{ wch: 25 },
...periodosUnicos.map(() => ({ wch: 15 })),
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
];
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Inscritos"); XLSX.utils.book_append_sheet(workbook, worksheet, "Inscritos");
const excelBuffer = XLSX.write(workbook, { const excelBuffer = XLSX.write(workbook, {
@@ -196,41 +208,87 @@ export default function Inscripciones() {
<h2 className="title">INSCRITOS</h2> <h2 className="title">INSCRITOS</h2>
<form className="containerForm" onSubmit={handleBuscar}> <form className="containerForm" onSubmit={handleBuscar}>
<label>Seleccione rango de periodos</label> <label>Filtro</label>
{/* Selector tipo */}
<select value={tipo} onChange={(e) => setTipo(e.target.value as Tipo)}>
<option value="periodo">Periodo</option>
<option value="anio">Año</option>
<option value="mes">Mes</option>
</select>
<div className="groupInput"> <div className="groupInput">
<select onChange={(e) => setPeriodoInicio(Number(e.target.value))} defaultValue=""> {/* PERIODO */}
<option value="" disabled>Inicio</option> {tipo === "periodo" && (
{periodo.map((p) => ( <>
<option key={p.id_periodo} value={p.id_periodo}> <select onChange={(e) => setPeriodoInicio(Number(e.target.value))}>
{p.semestre} <option value="">Inicio</option>
</option> {periodo.map((p) => (
))} <option key={p.id_periodo} value={p.id_periodo}>
</select> {p.semestre}
</option>
))}
</select>
<select onChange={(e) => setPeriodoFin(Number(e.target.value))} defaultValue=""> <select onChange={(e) => setPeriodoFin(Number(e.target.value))}>
<option value="" disabled>Fin</option> <option value="">Fin</option>
{periodo.map((p) => ( {periodo.map((p) => (
<option key={p.id_periodo} value={p.id_periodo}> <option key={p.id_periodo} value={p.id_periodo}>
{p.semestre} {p.semestre}
</option> </option>
))} ))}
</select> </select>
</>
)}
{/* AÑO */}
{tipo === "anio" && (
<>
<select onChange={(e) => setPeriodoInicio(Number(e.target.value))}>
<option value="">Año inicio</option>
{aniosDisponibles.map((a) => (
<option key={a} value={a}>
{a}
</option>
))}
</select>
<select onChange={(e) => setPeriodoFin(Number(e.target.value))}>
<option value="">Año fin</option>
{aniosDisponibles.map((a) => (
<option key={a} value={a}>
{a}
</option>
))}
</select>
</>
)}
{/* MES */}
{tipo === "mes" && (
<>
<select onChange={(e) => setAnio(Number(e.target.value))}>
<option value="">Año</option>
{aniosDisponibles.map((a) => (
<option key={a} value={a}>
{a}
</option>
))}
</select>
</>
)}
<button className="button buttonSearch" type="submit"> <button className="button buttonSearch" type="submit">
Buscar Buscar
</button> </button>
<button <button type="button" className="button buttonSearch" onClick={exportarExcel}>
type="button"
className="button buttonSearch"
onClick={exportarExcel}
>
Exportar Excel Exportar Excel
</button> </button>
</div> </div>
</form> </form>
{/* TABLA */}
<div className="containerTable"> <div className="containerTable">
<table> <table>
<thead> <thead>
@@ -250,7 +308,7 @@ export default function Inscripciones() {
<tbody> <tbody>
{tablaData.map((row, index) => ( {tablaData.map((row, index) => (
<tr key={index}> <tr key={index}>
<td className="textLeft">{row.carrera}</td> <td>{row.carrera}</td>
<td>{row.semestre}</td> <td>{row.semestre}</td>
<td>{row.femenino}</td> <td>{row.femenino}</td>
<td>{row.masculino}</td> <td>{row.masculino}</td>
+18
View File
@@ -696,6 +696,7 @@
"version": "2.5.6", "version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -735,6 +736,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -755,6 +757,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -775,6 +778,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -795,6 +799,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -815,6 +820,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -835,6 +841,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -855,6 +862,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -875,6 +883,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -895,6 +904,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -915,6 +925,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -935,6 +946,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -955,6 +967,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -975,6 +988,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2635,6 +2649,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
@@ -2677,6 +2692,7 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -3190,6 +3206,7 @@
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@@ -3290,6 +3307,7 @@
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {