Merge branch 'Axel' of repositorio.acatlan.unam.mx:IO/front-AT
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const [hasta, setHasta] = useState<string | null>(null);
|
||||
const [periodo1, setPeriodo1] = useState<any>(null);
|
||||
const [periodo2, setPeriodo2] = useState<any>(null);
|
||||
|
||||
return (
|
||||
<section className="containerSection">
|
||||
<section className="containerSection" style={{ overflow: "visible", }}>
|
||||
<h2 className="title">REPORTES</h2>
|
||||
|
||||
<Toggle
|
||||
@@ -41,17 +44,23 @@ export default function ReportesClient({
|
||||
key: "Por Servicio",
|
||||
label: "Por Servicio",
|
||||
content: (
|
||||
<>
|
||||
<MonthYearPicker
|
||||
onChange={(d, h) => {
|
||||
setDesde(d);
|
||||
setHasta(h);
|
||||
<div style={{ maxHeight: "400px", width: "100%", overflow: "visible" }}>
|
||||
<PeriodoPicker
|
||||
onChange={(p1, p2) => {
|
||||
setPeriodo1(p1);
|
||||
setPeriodo2(p2);
|
||||
}}
|
||||
/>
|
||||
<PorServicios desde={desde} hasta={hasta} />
|
||||
</>
|
||||
{periodo1 && periodo2 && (
|
||||
<PorServicios
|
||||
key={`${periodo1?.id_periodo}-${periodo2?.id_periodo}`}
|
||||
periodo1={periodo1}
|
||||
periodo2={periodo2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -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<Periodo[]>([]);
|
||||
const [p1, setP1] = useState<number | null>(null);
|
||||
const [p2, setP2] = useState<number | null>(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 (
|
||||
<div className="flex gap-4">
|
||||
{/* Periodo 1 */}
|
||||
<select onChange={(e) => setP1(Number(e.target.value))}>
|
||||
<option value="">Periodo 1</option>
|
||||
{periodos.map(p => (
|
||||
<option key={p.id_periodo} value={p.id_periodo}>
|
||||
{p.semestre}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Periodo 2 */}
|
||||
<select onChange={(e) => setP2(Number(e.target.value))}>
|
||||
<option value="">Periodo 2</option>
|
||||
{periodos.map(p => (
|
||||
<option key={p.id_periodo} value={p.id_periodo}>
|
||||
{p.semestre}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<ServicioReporte[]>([]);
|
||||
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 (!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 (
|
||||
<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={{ position: "relative" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "30px" }}>
|
||||
|
||||
<div
|
||||
style={{
|
||||
overflow: "auto",
|
||||
height: "270px",
|
||||
scrollbarColor: "#2563eb white",
|
||||
}}
|
||||
>
|
||||
{/* TABLA */}
|
||||
<div style={{ overflow: "auto", maxHeight: "300px" }}>
|
||||
{loading && <p>Cargando...</p>}
|
||||
|
||||
<table>
|
||||
<table style={{ width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Servicio</th>
|
||||
<th>Monto total</th>
|
||||
|
||||
{periodos.map((p) => (
|
||||
<th key={p.id_periodo}>{p.semestre}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{!loading && servicios.length === 0 && (
|
||||
{!loading && data.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={2}>No hay servicios en este rango</td>
|
||||
<td colSpan={periodos.length + 1}>
|
||||
No hay datos en este rango
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{servicios.map((servicio) => (
|
||||
<tr key={servicio.id_servicio}>
|
||||
<td>{servicio.servicio}</td>
|
||||
<td>${Number(servicio.total).toFixed(2)}</td>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
//IO
|
||||
}
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user