From c4f68aa1e011ddc3e7a704e2d9e41ef9e7a226d7 Mon Sep 17 00:00:00 2001 From: Tyrannusss Date: Tue, 14 Apr 2026 14:17:08 -0600 Subject: [PATCH 1/2] se modifico la manera en que se traen los datos del back-end para reportes y se creo un componente llamado PeriodoPicker, para poder traer la informacion de los periodos por rango --- app/(private)/Reportes/ReportesClient.tsx | 19 ++- .../PeriodoPicker/periodoPicker.tsx | 65 +++++++++ app/Components/Reportes/porServicio.tsx | 137 +++++++++++------- 3 files changed, 165 insertions(+), 56 deletions(-) create mode 100644 app/Components/PeriodoPicker/periodoPicker.tsx diff --git a/app/(private)/Reportes/ReportesClient.tsx b/app/(private)/Reportes/ReportesClient.tsx index c812b09..8919e67 100644 --- a/app/(private)/Reportes/ReportesClient.tsx +++ b/app/(private)/Reportes/ReportesClient.tsx @@ -6,6 +6,7 @@ import SearchDateBetween from "@/app/Components/SearchDateBetween/SearchDateBetw import PorServicios from "@/app/Components/Reportes/porServicio"; import PorRecibos from "@/app/Components/Reportes/porRecibo"; import MonthYearPicker from "@/app/Components/MonthYearPicker/MonthYearPicker"; +import PeriodoPicker from "@/app/Components/PeriodoPicker/periodoPicker"; export default function ReportesClient({ defaultKey, @@ -14,6 +15,8 @@ export default function ReportesClient({ }) { const [desde, setDesde] = useState(null); const [hasta, setHasta] = useState(null); + const [periodo1, setPeriodo1] = useState(null); + const [periodo2, setPeriodo2] = useState(null); return (
@@ -42,16 +45,20 @@ export default function ReportesClient({ label: "Por Servicio", content: ( <> - { - setDesde(d); - setHasta(h); + { + setPeriodo1(p1); + setPeriodo2(p2); }} /> - + + ), - }, + } ]} />
diff --git a/app/Components/PeriodoPicker/periodoPicker.tsx b/app/Components/PeriodoPicker/periodoPicker.tsx new file mode 100644 index 0000000..0b45e3d --- /dev/null +++ b/app/Components/PeriodoPicker/periodoPicker.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import Cookies from "js-cookie"; +import { envConfig } from "@/app/lib/config"; +interface Periodo { + id_periodo: number; + semestre: string; + fecha_inicio_servicio: string; + fecha_fin_servicio: string; +} + +export default function PeriodoPicker({ + onChange, +}: { + onChange: (p1: Periodo | null, p2: Periodo | null) => void; +}) { + const [periodos, setPeriodos] = useState([]); + const [p1, setP1] = useState(null); + const [p2, setP2] = useState(null); + + + const token = Cookies.get("token"); + const headers = { Authorization: `Bearer ${token}` }; + + useEffect(() => { + const fetchPeriodos = async () => { + const res = await axios.get(`${envConfig.apiUrl}/periodo`, { headers }); + setPeriodos(res.data); + }; + fetchPeriodos(); + }, []); + + useEffect(() => { + const periodo1 = periodos.find(p => p.id_periodo === p1) || null; + const periodo2 = periodos.find(p => p.id_periodo === p2) || null; + + onChange(periodo1, periodo2); + }, [p1, p2]); + + return ( +
+ {/* Periodo 1 */} + + + {/* Periodo 2 */} + +
+ ); +} \ No newline at end of file diff --git a/app/Components/Reportes/porServicio.tsx b/app/Components/Reportes/porServicio.tsx index d0f5237..c1e85f3 100644 --- a/app/Components/Reportes/porServicio.tsx +++ b/app/Components/Reportes/porServicio.tsx @@ -4,84 +4,121 @@ import { useEffect, useState } from "react"; import axios from "axios"; import Cookies from "js-cookie"; -interface ServicioReporte { - id_servicio: number; - servicio: string; - total: string; +interface Periodo { + id_periodo: number; + semestre: string; + fecha_inicio_servicio: string; + fecha_fin_servicio: string; } interface Props { - desde: string | null; - hasta: string | null; + periodo1: Periodo | null; + periodo2: Periodo | null; } -export default function PorServicio({ desde, hasta }: Props) { - const [servicios, setServicios] = useState([]); +export default function PorServicio({ periodo1, periodo2 }: Props) { + const [data, setData] = useState([]); + const [periodos, setPeriodos] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { - if (!desde || !hasta) return; + if (!periodo1 || !periodo2) return; const fetchServicios = async () => { setLoading(true); + try { const token = Cookies.get("token"); const headers = { Authorization: `Bearer ${token}` }; - const res = await axios.post( - `${process.env.NEXT_PUBLIC_API_URL}/detalle-servicio/rango`, - { desde, hasta }, - { headers }, + // 🔥 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 } ); - setServicios(res.data); + 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); } catch (error) { - console.error("Error al obtener reporte por servicio", error); + console.error("Error comparando servicios", error); } finally { setLoading(false); } }; fetchServicios(); - }, [desde, hasta]); + }, [periodo1, periodo2]); return ( -
+
+ {loading &&

Cargando...

} -
- {loading &&

Cargando...

} + + + + -
Servicio
- - - - - - - - - {!loading && servicios.length === 0 && ( - - - - )} - - {servicios.map((servicio) => ( - - - - + {periodos.map((p) => ( + ))} - -
ServicioMonto total
No hay servicios en este rango
{servicio.servicio}${Number(servicio.total).toFixed(2)}
{p.semestre}
-
+ + + + + {!loading && data.length === 0 && ( + + + No hay datos en este rango + + + )} + + {data.map((row) => ( + + {row.servicio} + + {periodos.map((p) => ( + + ${(row[p.semestre] || 0).toFixed(2)} + + ))} + + ))} + +
); -} -//IO \ No newline at end of file +} \ No newline at end of file From 35245a9195256a5d0eb08aaefec0f318b3610850 Mon Sep 17 00:00:00 2001 From: Tyrannusss Date: Wed, 20 May 2026 15:51:46 -0600 Subject: [PATCH 2/2] se agregaron las graficas por servicio ademas de poner un filtro para escoger que servicios se van a graficar --- app/(private)/Reportes/ReportesClient.tsx | 18 +- app/Components/Reportes/porServicio.tsx | 208 ++++++++++++++++++---- app/globals.css | 3 +- 3 files changed, 190 insertions(+), 39 deletions(-) diff --git a/app/(private)/Reportes/ReportesClient.tsx b/app/(private)/Reportes/ReportesClient.tsx index 8919e67..125b280 100644 --- a/app/(private)/Reportes/ReportesClient.tsx +++ b/app/(private)/Reportes/ReportesClient.tsx @@ -19,7 +19,7 @@ export default function ReportesClient({ const [periodo2, setPeriodo2] = useState(null); return ( -
+

REPORTES

+
{ setPeriodo1(p1); setPeriodo2(p2); }} /> - - - + {periodo1 && periodo2 && ( + + )} +
), } ]} diff --git a/app/Components/Reportes/porServicio.tsx b/app/Components/Reportes/porServicio.tsx index c1e85f3..b1e8fe6 100644 --- a/app/Components/Reportes/porServicio.tsx +++ b/app/Components/Reportes/porServicio.tsx @@ -4,6 +4,17 @@ 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"; + interface Periodo { id_periodo: number; semestre: string; @@ -20,6 +31,7 @@ export default function PorServicio({ periodo1, periodo2 }: Props) { const [data, setData] = useState([]); const [periodos, setPeriodos] = useState([]); const [loading, setLoading] = useState(false); + const [serviciosSeleccionados, setServiciosSeleccionados] = useState([]); useEffect(() => { if (!periodo1 || !periodo2) return; @@ -70,8 +82,8 @@ export default function PorServicio({ periodo1, periodo2 }: Props) { }); const finalData = Array.from(map.values()); - setData(finalData); + setServiciosSeleccionados(finalData.map((item: any) => item.servicio)); } catch (error) { console.error("Error comparando servicios", error); } finally { @@ -82,43 +94,179 @@ export default function PorServicio({ periodo1, periodo2 }: Props) { 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; + }); + }; + + // 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 ( -
- {loading &&

Cargando...

} +
- - - - + {/* TABLA */} +
+ {loading &&

Cargando...

} - {periodos.map((p) => ( -
- ))} - - - - - {!loading && data.length === 0 && ( +
Servicio{p.semestre}
+ - - - )} - - {data.map((row) => ( - - + {periodos.map((p) => ( - + ))} - ))} - -
- No hay datos en este rango -
{row.servicio}Servicio - ${(row[p.semestre] || 0).toFixed(2)} - {p.semestre}
+ + + + {!loading && data.length === 0 && ( + + + No hay datos en este rango + + + )} + + {data.map((row) => ( + + {row.servicio} + + {periodos.map((p) => ( + + ${formatNumber(row[p.semestre] || 0)} + + ))} + + ))} + + +
+
+ {data.map((servicio) => ( + + ))} +
+ {/* GRAFICA */} +
+ {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 eea513f..e0f1915 100644 --- a/app/globals.css +++ b/app/globals.css @@ -87,7 +87,7 @@ footer p { height: 600px; min-height: 520px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - overflow: hidden; + overflow: visible; background-color: #f9f9f9; z-index: 0; animation: fadeInUp 1s ease forwards; @@ -99,6 +99,7 @@ footer p { height: 100%; display: flex; flex-direction: column; + overflow: visible; } .containerForm {