se agregaron las graficas por servicio ademas de poner un filtro para escoger que servicios se van a graficar

This commit is contained in:
2026-05-20 15:51:46 -06:00
parent c4f68aa1e0
commit 35245a9195
3 changed files with 190 additions and 39 deletions
+10 -8
View File
@@ -19,7 +19,7 @@ export default function ReportesClient({
const [periodo2, setPeriodo2] = useState<any>(null);
return (
<section className="containerSection">
<section className="containerSection" style={{ overflow: "visible", }}>
<h2 className="title">REPORTES</h2>
<Toggle
@@ -44,19 +44,21 @@ export default function ReportesClient({
key: "Por Servicio",
label: "Por Servicio",
content: (
<>
<div style={{ maxHeight: "400px", width: "100%", overflow: "visible" }}>
<PeriodoPicker
onChange={(p1, p2) => {
setPeriodo1(p1);
setPeriodo2(p2);
}}
/>
<PorServicios
periodo1={periodo1}
periodo2={periodo2}
/>
</>
{periodo1 && periodo2 && (
<PorServicios
key={`${periodo1?.id_periodo}-${periodo2?.id_periodo}`}
periodo1={periodo1}
periodo2={periodo2}
/>
)}
</div>
),
}
]}
+178 -30
View File
@@ -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<any[]>([]);
const [periodos, setPeriodos] = useState<Periodo[]>([]);
const [loading, setLoading] = useState(false);
const [serviciosSeleccionados, setServiciosSeleccionados] = useState<string[]>([]);
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 (
<div
style={{
background: "white",
padding: "10px",
border: "1px solid #ccc",
borderRadius: "8px",
}}
>
<p style={{ fontWeight: "bold" }}>
Periodo: {label}
</p>
{sorted.map((entry: any, index: number) => (
<div key={index} style={{ color: entry.color }}>
{entry.name}: ${formatNumber(entry.value)}
</div>
))}
</div>
);
}
return null;
};
const chartData = transformDataForChart();
const colors = ["black", "red", "blue", "orange", "purple", "green"];
return (
<div style={{ overflow: "auto", height: "300px" }}>
{loading && <p>Cargando...</p>}
<div style={{ display: "flex", flexDirection: "column", gap: "30px" }}>
<table>
<thead>
<tr>
<th>Servicio</th>
{/* TABLA */}
<div style={{ overflow: "auto", maxHeight: "300px" }}>
{loading && <p>Cargando...</p>}
{periodos.map((p) => (
<th key={p.id_periodo}>{p.semestre}</th>
))}
</tr>
</thead>
<tbody>
{!loading && data.length === 0 && (
<table style={{ width: "100%" }}>
<thead>
<tr>
<td colSpan={periodos.length + 1}>
No hay datos en este rango
</td>
</tr>
)}
{data.map((row) => (
<tr key={row.servicio}>
<td>{row.servicio}</td>
<th>Servicio</th>
{periodos.map((p) => (
<td key={p.id_periodo}>
${(row[p.semestre] || 0).toFixed(2)}
</td>
<th key={p.id_periodo}>{p.semestre}</th>
))}
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{!loading && data.length === 0 && (
<tr>
<td colSpan={periodos.length + 1}>
No hay datos en este rango
</td>
</tr>
)}
{data.map((row) => (
<tr key={row.servicio}>
<td>{row.servicio}</td>
{periodos.map((p) => (
<td key={p.id_periodo}>
${formatNumber(row[p.semestre] || 0)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "10px",
marginBottom: "20px",
}}
>
{data.map((servicio) => (
<label
key={servicio.servicio}
style={{
display: "flex",
alignItems: "center",
gap: "5px",
}}
>
<input
type="checkbox"
checked={serviciosSeleccionados.includes(servicio.servicio)}
onChange={(e) => {
if (e.target.checked) {
setServiciosSeleccionados((prev) => [
...prev,
servicio.servicio,
]);
} else {
setServiciosSeleccionados((prev) =>
prev.filter((s) => s !== servicio.servicio)
);
}
}}
/>
{servicio.servicio}
</label>
))}
</div>
{/* GRAFICA */}
<div
style={{
flex: 1,
minWidth: "700px",
background: "white",
}}
>
{chartData.length > 0 && (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="periodo" />
<YAxis tickFormatter={(value) => value.toLocaleString()} />
<Tooltip content={<CustomTooltip />} />
<Legend />
{data
.filter((servicio) =>
serviciosSeleccionados.includes(servicio.servicio)
)
.map((servicio, i) => (
<Line
key={servicio.servicio}
dataKey={servicio.servicio}
stroke={colors[i % colors.length]}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
)}
</div>
</div>
);
}
+2 -1
View File
@@ -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 {