"use client"; import { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "@/hooks/use-toast"; import { TrendingUp, TrendingDown, DollarSign, Building2, AlertTriangle, CheckCircle2, Loader2, BarChart3, } from "lucide-react"; import { CATEGORIA_GASTO_LABELS, ESTADO_OBRA_LABELS, type CategoriaGasto } from "@/types"; interface ObraComparativo { id: string; nombre: string; estado: string; porcentajeAvance: number; presupuestado: number; ejecutado: number; gastado: number; variacion: number; variacionPorcentaje: number; categorias: { categoria: string; presupuestado: number; gastado: number; variacion: number; variacionPorcentaje: string; }[]; } interface Resumen { totalObras: number; totalPresupuestado: number; totalEjecutado: number; totalGastado: number; variacionTotal: number; variacionPorcentaje: number; obrasConSobrecosto: number; obrasBajoPresupuesto: number; } interface ComparativoData { obras: ObraComparativo[]; resumen: Resumen; } export function ComparativoDashboard() { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [selectedObra, setSelectedObra] = useState("all"); useEffect(() => { fetchData(); }, []); const fetchData = async () => { try { const response = await fetch("/api/dashboard/comparativo"); if (!response.ok) { throw new Error("Error al cargar datos"); } const result = await response.json(); setData(result); } catch (error) { toast({ title: "Error", description: "No se pudieron cargar los datos del dashboard", variant: "destructive", }); } finally { setLoading(false); } }; const formatCurrency = (value: number) => value.toLocaleString("es-MX", { style: "currency", currency: "MXN", }); const formatPercentage = (value: number) => `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`; if (loading) { return (
); } if (!data) { return (

No hay datos disponibles

); } const { obras, resumen } = data; const filteredObras = selectedObra === "all" ? obras : obras.filter((o) => o.id === selectedObra); // Calcular resumen filtrado const filteredResumen = selectedObra === "all" ? resumen : { totalObras: 1, totalPresupuestado: filteredObras[0]?.presupuestado || 0, totalEjecutado: filteredObras[0]?.ejecutado || 0, totalGastado: filteredObras[0]?.gastado || 0, variacionTotal: filteredObras[0]?.variacion || 0, variacionPorcentaje: filteredObras[0]?.variacionPorcentaje || 0, obrasConSobrecosto: filteredObras[0]?.variacion < 0 ? 1 : 0, obrasBajoPresupuesto: filteredObras[0]?.variacion > 0 ? 1 : 0, }; // Combinar categorías de todas las obras filtradas const categoriasCombinadas: Record = {}; for (const obra of filteredObras) { for (const cat of obra.categorias) { if (!categoriasCombinadas[cat.categoria]) { categoriasCombinadas[cat.categoria] = { presupuestado: 0, gastado: 0 }; } categoriasCombinadas[cat.categoria].presupuestado += cat.presupuestado; categoriasCombinadas[cat.categoria].gastado += cat.gastado; } } return (
{/* Filtro de obra */}

Comparativo Presupuesto vs Ejecutado

{/* Resumen Cards */}
Total Presupuestado

{formatCurrency(filteredResumen.totalPresupuestado)}

{filteredResumen.totalObras} obra(s)

Total Gastado

{formatCurrency(filteredResumen.totalGastado)}

0 ? (filteredResumen.totalGastado / filteredResumen.totalPresupuestado) * 100 : 0 } className="mt-2" />
{filteredResumen.variacionTotal >= 0 ? ( ) : ( )} Variación

= 0 ? "text-green-600" : "text-red-600" }`} > {formatCurrency(filteredResumen.variacionTotal)}

= 0 ? "text-green-600" : "text-red-600" }`} > {formatPercentage(filteredResumen.variacionPorcentaje)}

Estado de Obras
{filteredResumen.obrasBajoPresupuesto} bajo
{filteredResumen.obrasConSobrecosto} sobre
{/* Desglose por Categoría */} Desglose por Categoría Comparación de presupuesto vs gasto real por categoría
Categoría Presupuestado Gastado Variación Uso {Object.entries(categoriasCombinadas) .sort((a, b) => b[1].presupuestado - a[1].presupuestado) .map(([categoria, datos]) => { const variacion = datos.presupuestado - datos.gastado; const porcentajeUso = datos.presupuestado > 0 ? (datos.gastado / datos.presupuestado) * 100 : 0; return ( {CATEGORIA_GASTO_LABELS[categoria as CategoriaGasto] || categoria} {formatCurrency(datos.presupuestado)} {formatCurrency(datos.gastado)} = 0 ? "text-green-600" : "text-red-600" }`} > {formatCurrency(variacion)}
100 ? "bg-red-100" : ""}`} /> {porcentajeUso.toFixed(0)}%
); })}
{/* Tabla de Obras */} {selectedObra === "all" && obras.length > 1 && ( Comparativo por Obra Resumen de presupuesto vs gasto para cada obra
Obra Estado Presupuesto Gastado Variación Avance {obras.map((obra) => ( {obra.nombre} {ESTADO_OBRA_LABELS[obra.estado as keyof typeof ESTADO_OBRA_LABELS] || obra.estado} {formatCurrency(obra.presupuestado)} {formatCurrency(obra.gastado)} = 0 ? "text-green-600" : "text-red-600" }`} > {formatCurrency(obra.variacion)} ({obra.variacionPorcentaje >= 0 ? "+" : ""} {obra.variacionPorcentaje.toFixed(1)}%)
{obra.porcentajeAvance.toFixed(0)}%
))}
)}
); }