2025-08-20 09:49:15 -06:00

407 lines
16 KiB
Vue

<script setup>
import { ref, watch, onMounted } from "vue";
import axios from "axios";
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
import Bars from "@/Components/Dashboard/Charts/Bars.vue";
import Pie from "@/Components/Dashboard/Charts/Pie.vue";
import Lines from "@/Components/Dashboard/Charts/Lines.vue";
const fmtToday = () => new Date().toISOString().slice(0, 10);
const today = fmtToday();
const dateRange = ref({ start: today, end: today });
const loading = ref(false);
const error = ref(null);
let debounceId = null;
let cancelTokenSource = null;
const totals = ref({ day: null, week: null, month: null });
const tipoChart = ref({ labels: [], datasets: [] });
const generoChart = ref({ labels: [], datasets: [] });
const edadChart = ref({ labels: [], datasets: [] });
const chartOptions = {
responsive: true,
scales: {
x: { beginAtZero: true },
y: { beginAtZero: true },
},
};
const normalizeResponse = (res) => {
const main = res?.data ?? {};
const counts = res?.counts ?? res?.counts_ac ?? [];
const porTipo =
main?.por_tipo_apoyo ??
main?.data?.por_tipo_apoyo ??
counts?.data?.por_tipo_apoyo ??
counts?.por_tipo_apoyo ??
{};
const porGenero =
main?.por_genero ??
main?.data?.por_genero ??
counts?.data?.por_genero ??
{};
const porEdad =
main?.por_rango_edad ??
main?.data?.por_rango_edad ??
counts?.data?.por_rango_edad ??
{};
const totalsShape = counts?.data_ac ?? counts?.data ?? counts ?? {};
return {
porGenero,
porEdad,
porTipo,
totalsShape,
};
};
const buildTipoChart = (porTipo) => {
const labels = Object.keys(porTipo || {});
const values = labels.map((k) => Number(porTipo[k] ?? 0));
tipoChart.value = {
labels,
datasets: [
{
label: "Apoyos entregados",
data: values,
backgroundColor: ["#3b82f6"],
},
],
};
};
const buildGeneroChart = (porGenero) => {
const map = {
hombres: porGenero?.hombres ?? porGenero?.Hombres ?? porGenero?.Hombre ?? 0,
mujeres: porGenero?.mujeres ?? porGenero?.Mujeres ?? porGenero?.Mujer ?? 0,
};
const labels = ["Hombres", "Mujeres"];
const values = [Number(map.hombres || 0), Number(map.mujeres || 0)];
generoChart.value = {
labels,
datasets: [
{
label: "Beneficiarios por género",
data: values,
backgroundColor: ["#3b82f6", "#fb7185"],
},
],
};
};
const buildEdadChart = (porEdad) => {
// orden predecible para la X de la gráfica
const ORDER = [
"0-17",
"18-29",
"30-39",
"40-49",
"50-59",
"60-69",
"70+",
"sin_fecha",
];
const labels = ORDER.filter((k) =>
Object.prototype.hasOwnProperty.call(porEdad || {}, k)
);
// si no hay claves exactamente como ORDER, fallback a keys actuales
const finalLabels = labels.length ? labels : Object.keys(porEdad || {});
const values = finalLabels.map((k) => Number(porEdad[k] ?? 0));
const color = "#10B981"; // verde
edadChart.value = {
labels: finalLabels,
datasets: [
{
label: "Beneficiarios por rango de edad",
data: values,
borderColor: color,
backgroundColor: "rgba(16,185,129,0.12)",
fill: true,
tension: 0.3,
pointRadius: 3,
},
],
};
};
const load = async () => {
loading.value = true;
error.value = null;
if (cancelTokenSource) {
try {
cancelTokenSource.cancel("cancel");
} catch (e) {}
}
cancelTokenSource = axios.CancelToken.source();
try {
const res = await axios.get("/api/reporte-atencion", {
params: { start: dateRange.value.start, end: dateRange.value.end },
cancelToken: cancelTokenSource.token,
headers: { "X-Requested-With": "XMLHttpRequest" },
});
const payload = res.data || {};
const { porTipo, porGenero, porEdad, totalsShape } =
normalizeResponse(payload);
totals.value.day = totalsShape?.today ?? totalsShape?.dia ?? null;
totals.value.week = totalsShape?.week ?? totalsShape?.semana ?? null;
totals.value.month = totalsShape?.month ?? totalsShape?.mes ?? null;
buildTipoChart(porTipo);
buildGeneroChart(porGenero);
buildEdadChart(porEdad);
} catch (e) {
if (!(axios.isCancel && axios.isCancel(e))) {
console.error(e);
error.value =
e.response?.data?.error || e.message || "Error al cargar datos";
tipoChart.value = { labels: [], datasets: [] };
generoChart.value = { labels: [], datasets: [] };
edadChart.value = { labels: [], datasets: [] };
totals.value = { day: null, week: null, month: null };
}
} finally {
loading.value = false;
}
};
watch(
dateRange,
() => {
clearTimeout(debounceId);
debounceId = setTimeout(load, 300);
},
{ deep: true }
);
onMounted(load);
</script>
<template>
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
<header class="relative">
<div class="bg-gradient-to-r from-gray-100 to-gray-50 py-3 shadow-sm">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between">
<span class="text-sm font-bold text-gray-800 tracking-wide">COMALCALCO.GOB.MX</span>
</div>
</div>
</div>
<div
style="background: linear-gradient(135deg, #621132 0%, #7d1a42 50%, #621132 100%)"
class="shadow-xl relative overflow-hidden"
>
<!-- Patrón decorativo de fondo -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-0 left-0 w-full h-full"
style="background-image: repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(255,255,255,.1) 35px, rgba(255,255,255,.1) 70px);"></div>
</div>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 relative">
<div class="flex items-center justify-between h-32">
<div class="flex items-center space-x-4">
<img
src="https://apoyos.comalcalco.gob.mx/images/logo_blanco.png"
alt="Logo Comalcalco"
class="h-20 w-auto object-contain filter drop-shadow-lg transition-transform hover:scale-105"
/>
<div class="hidden md:block">
<h1 class="text-2xl font-bold text-white">Atención Ciudadana y DIF</h1>
<p class="text-blue-100 text-sm">Sistema de seguimiento de apoyos</p>
</div>
</div>
</div>
</div>
</div>
</header>
<!-- Loading overlay mejorado -->
<div
v-if="loading"
class="fixed inset-0 bg-white/95 backdrop-blur-sm flex items-center justify-center z-50 transition-all duration-300"
>
<div class="text-center">
<div class="relative">
<div class="animate-spin rounded-full h-16 w-16 border-4 border-blue-200 border-t-blue-600 mx-auto"></div>
</div>
<p class="mt-6 text-gray-700 font-medium">Cargando datos...</p>
</div>
</div>
<section class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Controles mejorados -->
<div class="mb-8 flex flex-col sm:flex-row items-start sm:items-center gap-4 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<div class="flex-1">
<h2 class="text-lg font-semibold text-gray-900 mb-2">Filtros de fecha</h2>
<DateRange v-model="dateRange" :presets="true" />
</div>
<div class="flex items-center space-x-4">
<div v-if="loading" class="flex items-center text-blue-600">
<div class="animate-spin h-4 w-4 border-2 border-blue-600 border-t-transparent rounded-full mr-2"></div>
<span class="text-sm font-medium">Cargando...</span>
</div>
<div v-if="error" class="flex items-center text-red-600">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<span class="text-sm font-medium">{{ error }}</span>
</div>
</div>
</div>
<!-- Cards mejoradas -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-8">
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div class="p-6 text-white">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center space-x-2 mb-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold opacity-90">Apoyos del Mes</h3>
</div>
<p class="text-3xl font-bold">{{ totals.month ?? "--" }}</p>
<p class="text-xs text-blue-100 mt-1">Total mensual</p>
</div>
<div class="bg-white/20 rounded-full p-3">
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
</svg>
</div>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div class="p-6 text-white">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center space-x-2 mb-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold opacity-90">Apoyos Semanales</h3>
</div>
<p class="text-3xl font-bold">{{ totals.week ?? "--" }}</p>
<p class="text-xs text-emerald-100 mt-1">Últimos 7 días</p>
</div>
<div class="bg-white/20 rounded-full p-3">
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
</div>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div class="p-6 text-white">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center space-x-2 mb-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a8 8 0 100 16 8 8 0 000-16zM8 12a1 1 0 102 0V9a1 1 0 10-2 0v3zm2-8a1 1 0 100 2 1 1 0 000-2z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold opacity-90">Apoyos de Hoy</h3>
</div>
<p class="text-3xl font-bold">{{ totals.day ?? "--" }}</p>
<p class="text-xs text-amber-100 mt-1">En el día actual</p>
</div>
<div class="bg-white/20 rounded-full p-3">
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a8 8 0 100 16 8 8 0 000-16zM8 12a1 1 0 102 0V9a1 1 0 10-2 0v3zm2-8a1 1 0 100 2 1 1 0 000-2z" clip-rule="evenodd" />
</svg>
</div>
</div>
</div>
</div>
</div>
<!-- Contenedor de gráficas mejorado -->
<div class="max-w-7xl mx-auto">
<!-- Gráfica principal -->
<div class="bg-white rounded-2xl shadow-xl border border-gray-100 p-8 mb-8 transition-all duration-300 hover:shadow-2xl">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-2xl mb-4 shadow-lg">
<svg class="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">Distribución por Tipo de Apoyo</h2>
<p class="text-gray-600">Análisis detallado de los tipos de apoyos otorgados</p>
</div>
<div class="h-96">
<Bars :chartData="tipoChart" :chartOptions="chartOptions" />
</div>
</div>
<!-- Gráficas secundarias -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 transition-all duration-300 hover:shadow-2xl">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-pink-500 to-rose-600 rounded-xl mb-4 shadow-lg">
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
</svg>
</div>
<h3 class="text-lg font-bold text-gray-900 mb-1">Distribución por Género</h3>
<p class="text-sm text-gray-600">Beneficiarios por género</p>
</div>
<div>
<Pie
:chartData="generoChart"
:chartOptions="{ responsive: true, maintainAspectRatio: false }"
/>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 transition-all duration-300 hover:shadow-2xl">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-emerald-500 to-teal-600 rounded-xl mb-4 shadow-lg">
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<h3 class="text-lg font-bold text-gray-900 mb-1">Distribución por Edad</h3>
<p class="text-sm text-gray-600">Beneficiarios por rango etario</p>
</div>
<div class="h-64">
<Lines
:chartData="edadChart"
:chartOptions="{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}"
/>
</div>
</div>
</div>
</div>
</section>
<!-- Footer mejorado -->
<footer class="bg-gradient-to-r from-gray-800 to-gray-900 text-white py-8 mt-16">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
</div>
</div>
</footer>
</div>
</template>