272 lines
7.0 KiB
TypeScript
272 lines
7.0 KiB
TypeScript
"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";
|
|
|
|
interface Periodo {
|
|
id_periodo: number;
|
|
semestre: string;
|
|
fecha_inicio_servicio: string;
|
|
fecha_fin_servicio: string;
|
|
}
|
|
|
|
interface Props {
|
|
periodo1: Periodo | null;
|
|
periodo2: Periodo | null;
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
});
|
|
};
|
|
|
|
// 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={{ display: "flex", flexDirection: "column", gap: "30px" }}>
|
|
|
|
{/* TABLA */}
|
|
<div style={{ overflow: "auto", maxHeight: "300px" }}>
|
|
{loading && <p>Cargando...</p>}
|
|
|
|
<table style={{ width: "100%" }}>
|
|
<thead>
|
|
<tr>
|
|
<th>Servicio</th>
|
|
|
|
{periodos.map((p) => (
|
|
<th key={p.id_periodo}>{p.semestre}</th>
|
|
))}
|
|
</tr>
|
|
</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>
|
|
);
|
|
} |