diff --git a/app/(private)/Inscritos/Inscripciones.tsx b/app/(private)/Inscritos/Inscripciones.tsx index 79d5fe4..35d1166 100644 --- a/app/(private)/Inscritos/Inscripciones.tsx +++ b/app/(private)/Inscritos/Inscripciones.tsx @@ -48,6 +48,8 @@ export default function Inscripciones() { const [anio, setAnio] = useState(null); const [tablaData, setTablaData] = useState([]); + const [periodosUnicos, setPeriodosUnicos] = useState([]); + const [tablaResumen, setTablaResumen] = useState([]); const [rawData, setRawData] = useState([]); const token = Cookies.get("token"); @@ -139,50 +141,80 @@ export default function Inscripciones() { }); setTablaData(Object.values(agrupado)); + + const periodosUnicosList = Array.from(new Set(data.map((t) => t.semestre))); + setPeriodosUnicos(periodosUnicosList); + + const carrerasUnicas = Array.from(new Set(data.map((t) => t.carrera))); + + const resumen = carrerasUnicas.map((carrera) => { + const fila: any = { Carrera: carrera }; + let totalFila = 0; + periodosUnicosList.forEach((periodo) => { + const items = data.filter((t) => t.carrera === carrera && t.semestre === periodo); + const totalPeriodo = items.reduce((acc, item) => acc + Number(item.total), 0); + fila[periodo] = totalPeriodo; + totalFila += totalPeriodo; + }); + fila["Total"] = totalFila; + return fila; + }); + + setTablaResumen(resumen); } catch (error) { console.error("Error al obtener inscritos", error); } }; const exportarExcel = () => { - if (!rawData.length) return; + if (!rawData.length || !tablaResumen.length) return; - const periodosUnicos = Array.from( - new Set(rawData.map((t) => t.semestre)) - ); + // HOJA 1: Inscritos (resumen) + const worksheet1 = XLSX.utils.json_to_sheet(tablaResumen); - const carrerasUnicas = Array.from( - new Set(rawData.map((t) => t.carrera)) - ); + const carrerasUnicas = Array.from(new Set(rawData.map((t) => t.carrera))); - const dataExcel = carrerasUnicas.map((carrera) => { + // HOJA 2: genero + const dataGenero = carrerasUnicas.map((carrera) => { const fila: any = { Carrera: carrera }; - - let totalFila = 0; - periodosUnicos.forEach((periodo) => { - const items = rawData.filter( - (t) => t.carrera === carrera && t.semestre === periodo - ); - - const totalPeriodo = items.reduce( - (acc, item) => acc + Number(item.total), - 0 - ); - - fila[periodo] = totalPeriodo; - totalFila += totalPeriodo; + const items = rawData.filter((t) => t.carrera === carrera && t.semestre === periodo); + const masc = items.reduce((acc, item) => acc + Number(item.masculino), 0); + const fem = items.reduce((acc, item) => acc + Number(item.femenino), 0); + fila[`Masculino - ${periodo}`] = masc; + fila[`Femenino - ${periodo}`] = fem; }); - - fila["Total"] = totalFila; - return fila; }); + const worksheet2 = XLSX.utils.json_to_sheet(dataGenero); + + // HOJA 3: areas + const dataAreas = carrerasUnicas.map((carrera) => { + const fila: any = { Carrera: carrera }; + periodosUnicos.forEach((periodo) => { + const items = rawData.filter((t) => t.carrera === carrera && t.semestre === periodo); + let windows = 0, mac = 0, linux = 0, profes = 0; + + items.forEach((item) => { + if (item.profesor === "WINDOWS") windows += Number(item.total); + if (item.profesor === "MACINTOSH") mac += Number(item.total); + if (item.profesor === "LINUX") linux += Number(item.total); + if (item.profesor === "PROFESORES") profes += Number(item.total); + }); + + fila[`WINDOWS - ${periodo}`] = windows; + fila[`MACINTOSH - ${periodo}`] = mac; + fila[`LINUX - ${periodo}`] = linux; + fila[`PROFESORES - ${periodo}`] = profes; + }); + return fila; + }); + const worksheet3 = XLSX.utils.json_to_sheet(dataAreas); - const worksheet = XLSX.utils.json_to_sheet(dataExcel); const workbook = XLSX.utils.book_new(); - - XLSX.utils.book_append_sheet(workbook, worksheet, "Inscritos"); + XLSX.utils.book_append_sheet(workbook, worksheet1, "Inscritos"); + XLSX.utils.book_append_sheet(workbook, worksheet2, "genero"); + XLSX.utils.book_append_sheet(workbook, worksheet3, "areas"); const excelBuffer = XLSX.write(workbook, { bookType: "xlsx", diff --git a/app/(private)/Periodo-Graficas/page.tsx b/app/(private)/Periodo-Graficas/page.tsx deleted file mode 100644 index af09b3d..0000000 --- a/app/(private)/Periodo-Graficas/page.tsx +++ /dev/null @@ -1,272 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { envConfig } from "@/app/lib/config"; -import axios from "axios"; -import Cookies from "js-cookie"; -import * as XLSX from "xlsx"; -import { saveAs } from "file-saver"; - -import { - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, - ResponsiveContainer, - Line, - LineChart, -} from "recharts"; - -interface TotalServicioPeriodo { - nombre_servicio: string; - periodo: string; - monto_total: number; -} - -interface Servicio { - id_servicio: number; - servicio: string; -} - -interface Periodo { - id_periodo: number; - semestre: string; -} - -export default function ReporteTotalesPage() { - const [totales, setTotales] = useState([]); - const [servicios, setServicios] = useState([]); - const [periodos, setPeriodos] = useState([]); - const [cargando, setCargando] = useState(true); - const [periodoInicio, setPeriodoInicio] = useState(""); - const [periodoFin, setPeriodoFin] = useState(""); - const [servicioId, setServicioId] = useState(""); - - const token = Cookies.get("token"); - const headers = { Authorization: `Bearer ${token}` }; - - // TOOLTIP ORDENADO - const CustomTooltip = ({ active, payload, label }: any) => { - if (active && payload && payload.length) { - const sorted = [...payload] - .filter((item) => item.value > 0) - .sort((a, b) => b.value - a.value); - - return ( -
-

Periodo: {label}

- {sorted.map((entry: any, index: number) => ( -
- {entry.name}: ${entry.value.toFixed(2)} -
- ))} -
- ); - } - return null; - }; - - // EXPORTAR EXCEL PIVOT - const exportarExcelPivot = () => { - if (!totales.length) return; - - const periodosUnicos = Array.from(new Set(totales.map((t) => t.periodo))).sort(); - const serviciosUnicos = Array.from(new Set(totales.map((t) => t.nombre_servicio))); - - const dataExcel = serviciosUnicos.map((servicio) => { - const fila: any = { Servicio: servicio }; - - periodosUnicos.forEach((periodo) => { - const item = totales.find( - (t) => t.nombre_servicio === servicio && t.periodo === periodo - ); - fila[periodo] = item ? item.monto_total : 0; - }); - - // total por fila - fila["Total"] = periodosUnicos.reduce( - (acc, periodo) => acc + (fila[periodo] || 0), - 0 - ); - - return fila; - }); - - const worksheet = XLSX.utils.json_to_sheet(dataExcel); - - worksheet["!cols"] = [ - { wch: 30 }, - ...periodosUnicos.map(() => ({ wch: 15 })), - { wch: 15 }, - ]; - - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, "Pivot"); - - const excelBuffer = XLSX.write(workbook, { - bookType: "xlsx", - type: "array", - }); - - const data = new Blob([excelBuffer], { - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }); - - saveAs(data, "reporte_periodo.xlsx"); - }; - - // FETCH - useEffect(() => { - const fetchData = async () => { - try { - const [serviciosRes, periodosRes] = await Promise.all([ - axios.get(`${envConfig.apiUrl}/servicio`, { headers }), - axios.get(`${envConfig.apiUrl}/periodo`, { headers }), - ]); - setServicios(serviciosRes.data); - setPeriodos(periodosRes.data); - } catch (error) { - console.error("Error al cargar datos", error); - } - }; - fetchData(); - }, []); - - const obtenerTotales = async () => { - setCargando(true); - try { - const params = new URLSearchParams(); - if (periodoInicio) params.append("periodoInicio", periodoInicio); - if (periodoFin) params.append("periodoFin", periodoFin); - if (servicioId) params.append("servicioId", servicioId); - - const url = `${envConfig.apiUrl}/detalle-servicio/totales-por-periodo?${params.toString()}`; - const res = await axios.get(url, { headers }); - setTotales(res.data); - } catch (error) { - console.error("Error al obtener totales", error); - } finally { - setCargando(false); - } - }; - - const aplicarFiltros = () => { - obtenerTotales(); - }; - - // DATA PARA GRAFICA - const transformDataForChart = () => { - const periodosUnicos = Array.from(new Set(totales.map((t) => t.periodo))).sort(); - const serviciosUnicos = Array.from(new Set(totales.map((t) => t.nombre_servicio))); - - const data = periodosUnicos.map((periodo) => { - const row: any = { periodo }; - serviciosUnicos.forEach((servicio) => { - const item = totales.find( - (t) => t.periodo === periodo && t.nombre_servicio === servicio - ); - row[servicio] = item ? item.monto_total : 0; - }); - return row; - }); - - return { data, serviciosUnicos }; - }; - - const { data: chartData, serviciosUnicos } = transformDataForChart(); - - const colors = ["black", "red", "blue", "orange", "purple", "green"]; - - return ( -
-

- REPORTE DE TOTALES POR SERVICIO Y PERIODO -

- -
- - - - - - - - -
- - {cargando ? ( -

Cargando...

- ) : ( -
- - {/* TABLA */} -
- - - - - - - - - - {totales.map((item, i) => ( - - - - - - ))} - -
ServicioPeriodoMonto
{item.nombre_servicio}{item.periodo}${item.monto_total.toFixed(2)}
-
- - {/* GRAFICA */} -
- - - - - - } /> - - - {serviciosUnicos.map((servicio, i) => ( - - ))} - - -
- -
- )} -
- ); -} \ No newline at end of file diff --git a/app/(private)/Reportes-graficas/page.tsx b/app/(private)/Reportes-graficas/page.tsx new file mode 100644 index 0000000..22e77a7 --- /dev/null +++ b/app/(private)/Reportes-graficas/page.tsx @@ -0,0 +1,395 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import Cookies from "js-cookie"; + +import { + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + Line, + LineChart, +} from "recharts"; +import PeriodoPicker from "@/app/Components/PeriodoPicker/periodoPicker"; +import * as XLSX from "xlsx"; + +interface Periodo { + id_periodo: number; + semestre: string; + fecha_inicio_servicio: string; + fecha_fin_servicio: string; +} + + +export default function PorServicio() { + const [data, setData] = useState([]); + const [periodos, setPeriodos] = useState([]); + const [loading, setLoading] = useState(false); + const [serviciosSeleccionados, setServiciosSeleccionados] = useState([]); + + const [periodo1, setPeriodo1] = useState(null); + const [periodo2, setPeriodo2] = useState(null); + + useEffect(() => { + if (!periodo1 || !periodo2) return; + + const fetchServicios = async () => { + setLoading(true); + + try { + const token = Cookies.get("token"); + const headers = { Authorization: `Bearer ${token}` }; + + // 🔥 1. traer TODOS los periodos en rango + const resPeriodos = await axios.get( + `${process.env.NEXT_PUBLIC_API_URL}/periodo/range?p1=${periodo1.id_periodo}&p2=${periodo2.id_periodo}`, + { headers } + ); + + const periodosRango = resPeriodos.data; + setPeriodos(periodosRango); + + // 🔥 2. hacer requests por cada periodo + const resultados = await Promise.all( + periodosRango.map((p: Periodo) => + axios.post( + `${process.env.NEXT_PUBLIC_API_URL}/detalle-servicio/rango`, + { + desde: p.fecha_inicio_servicio, + hasta: p.fecha_fin_servicio, + }, + { headers } + ) + ) + ); + + // 🔥 3. merge dinámico + const map = new Map(); + + periodosRango.forEach((p: Periodo, index: number) => { + resultados[index].data.forEach((item: any) => { + if (!map.has(item.servicio)) { + map.set(item.servicio, { + servicio: item.servicio, + }); + } + + map.get(item.servicio)[p.semestre] = Number(item.total); + }); + }); + + const finalData = Array.from(map.values()); + setData(finalData); + setServiciosSeleccionados(finalData.map((item: any) => item.servicio)); + } catch (error) { + console.error("Error comparando servicios", error); + } finally { + setLoading(false); + } + }; + + fetchServicios(); + }, [periodo1, periodo2]); + + // 🔥 TRANSFORMAR DATA PARA GRAFICA + const transformDataForChart = () => { + if (!data.length || !periodos.length) return []; + + return periodos.map((p) => { + const row: any = { periodo: p.semestre }; + + data.forEach((servicio) => { + row[servicio.servicio] = servicio[p.semestre] || 0; + }); + + return row; + }); + }; + + // EXPORTAR A EXCEL + const exportToExcel = () => { + if (!data || data.length === 0) return; + + const exportData = data.map((row) => { + const newRow: any = { Servicio: row.servicio }; + periodos.forEach((p) => { + newRow[p.semestre] = row[p.semestre] || 0; + }); + return newRow; + }); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Ingresos"); + + XLSX.writeFile(workbook, "Reporte_Ingresos_Servicio.xlsx"); + }; + + // FORMATO DE NUMEROS + const formatNumber = (value: number) => { + return value.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + }; + + // TOOLTIP ORDENADO + const CustomTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + const sorted = [...payload] + .filter((item) => item.value > 0) + .sort((a, b) => b.value - a.value); + + return ( +
+

+ Periodo: {label} +

+ + {sorted.map((entry: any, index: number) => ( +
+ {entry.name}: ${formatNumber(entry.value)} +
+ ))} +
+ ); + } + + return null; + }; + + + const chartData = transformDataForChart(); + + const colors = ["black", "red", "blue", "orange", "purple", "green"]; + + return ( +
+ + { + setPeriodo1(p1); + setPeriodo2(p2); + }} + /> + + {!periodo1 || !periodo2 ? ( +

Seleccione un rango de periodos

+ ) : ( + <> + {/* CONTROLES SUPERIORES */} +
+ + + {data.map((servicio) => ( + + ))} +
+ + {/* TABLA Y GRÁFICA */} +
+ {/* COLUMNA IZQUIERDA: TABLA */} +
+
+ {loading &&

Cargando...

} + + + + + + + {periodos.map((p) => ( + + ))} + + + + + {!loading && data.length === 0 && ( + + + + )} + + {data.map((row) => ( + + + + {periodos.map((p) => ( + + ))} + + ))} + +
Servicio + {p.semestre} +
+ No hay datos en este rango +
{row.servicio} + $ + {formatNumber( + row[ + p.semestre + ] || 0 + )} +
+
+
+ + {/* COLUMNA DERECHA: GRÁFICA */} +
+ {chartData.length > 0 && ( + + + + + + + + value.toLocaleString() + } + /> + + } + /> + + + + {data + .filter((servicio) => + serviciosSeleccionados.includes( + servicio.servicio + ) + ) + .map((servicio, i) => ( + + ))} + + + )} +
+
+ + )} +
+ ); +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 4c0ba95..b4c9e2b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -109,7 +109,7 @@ footer p { height: 600px; min-height: 520px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - overflow: visible; + overflow: auto; background-color: #f9f9f9; z-index: 0; animation: fadeInUp 1s ease forwards;