diff --git a/app/(private)/Reportes/ReportesClient.tsx b/app/(private)/Reportes/ReportesClient.tsx index c812b09..125b280 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,9 +15,11 @@ 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 ( -
+

REPORTES

- { - setDesde(d); - setHasta(h); +
+ { + setPeriodo1(p1); + setPeriodo2(p2); }} /> - - + {periodo1 && periodo2 && ( + + )} +
), - }, + } ]} />
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..b1e8fe6 100644 --- a/app/Components/Reportes/porServicio.tsx +++ b/app/Components/Reportes/porServicio.tsx @@ -4,84 +4,269 @@ import { useEffect, useState } from "react"; import axios from "axios"; import Cookies from "js-cookie"; -interface ServicioReporte { - id_servicio: number; - servicio: string; - total: string; +import { + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + Line, + LineChart, +} from "recharts"; + +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); + const [serviciosSeleccionados, setServiciosSeleccionados] = useState([]); 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); + setServiciosSeleccionados(finalData.map((item: any) => item.servicio)); } 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]); + + // 🔥 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 ( -
+
-
+ {/* TABLA */} +
{loading &&

Cargando...

} - +
- + + {periodos.map((p) => ( + + ))} - {!loading && servicios.length === 0 && ( + {!loading && data.length === 0 && ( - + )} - {servicios.map((servicio) => ( - - - + {data.map((row) => ( + + + + {periodos.map((p) => ( + + ))} ))}
ServicioMonto total{p.semestre}
No hay servicios en este rango + No hay datos en este rango +
{servicio.servicio}${Number(servicio.total).toFixed(2)}
{row.servicio} + ${formatNumber(row[p.semestre] || 0)} +
+
+ {data.map((servicio) => ( + + ))} +
+ {/* GRAFICA */} +
+ {chartData.length > 0 && ( + + + + + value.toLocaleString()} /> + + } /> + + + + {data + .filter((servicio) => + serviciosSeleccionados.includes(servicio.servicio) + ) + .map((servicio, i) => ( + + ))} + + + )} +
); -} -//IO \ No newline at end of file +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 6a9609a..4c0ba95 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: hidden; + overflow: visible; background-color: #f9f9f9; z-index: 0; animation: fadeInUp 1s ease forwards; @@ -121,6 +121,7 @@ footer p { height: 100%; display: flex; flex-direction: column; + overflow: visible; } .containerForm {