Se agregó las gráficas a Tramites #1
@ -144,7 +144,7 @@ const COMALCALCO_CENTER = {
|
|||||||
|
|
||||||
// Límites aproximados de Comalcalco
|
// Límites aproximados de Comalcalco
|
||||||
const COMALCALCO_BOUNDS = {
|
const COMALCALCO_BOUNDS = {
|
||||||
north: 18.35,
|
north: 19.35,
|
||||||
south: 18.18,
|
south: 18.18,
|
||||||
east: -93.10,
|
east: -93.10,
|
||||||
west: -93.30
|
west: -93.30
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const submit = () => {
|
|||||||
<AuthenticationCard>
|
<AuthenticationCard>
|
||||||
<template #logo>
|
<template #logo>
|
||||||
<AppLogo
|
<AppLogo
|
||||||
class="text-2xl"
|
class="text-2xl text-[#621132]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ const submit = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center justify-end space-y-2 mt-4">
|
<div class="flex flex-col items-center justify-end space-y-2 mt-4">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
class="w-full"
|
class="w-full bg-[#621132]"
|
||||||
:class="{ 'opacity-25': form.processing }"
|
:class="{ 'opacity-25': form.processing }"
|
||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
v-text="$t('auth.login')"
|
v-text="$t('auth.login')"
|
||||||
|
|||||||
278
resources/js/Pages/Dashboard/AtencionCiudadana.vue
Normal file
278
resources/js/Pages/Dashboard/AtencionCiudadana.vue
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, computed } from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
|
||||||
|
import Bars from "@/Components/Dashboard/Charts/Bars.vue";
|
||||||
|
import { data } from "autoprefixer";
|
||||||
|
|
||||||
|
const dateRange = ref({
|
||||||
|
start: new Date().toISOString().slice(0, 10),
|
||||||
|
end: new Date().toISOString().slice(0, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
const raw = ref([]);
|
||||||
|
|
||||||
|
// Toggle para usar datos falsos
|
||||||
|
const USE_MOCK = ref(false);
|
||||||
|
|
||||||
|
// Tipos de apoyo (inventados)
|
||||||
|
const supportTypes = [
|
||||||
|
"Despensas",
|
||||||
|
"Despensas Funerarias",
|
||||||
|
"Laboratorio",
|
||||||
|
"Rayos X",
|
||||||
|
"USG",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Distribución por defecto para mock (suman 1)
|
||||||
|
const mockDistribution = [0.35, 0.08, 0.2, 0.22, 0.15];
|
||||||
|
|
||||||
|
const buildMockCounts = (total) => {
|
||||||
|
const counts = supportTypes.map((_, i) =>
|
||||||
|
Math.round(total * (mockDistribution[i] || 0))
|
||||||
|
);
|
||||||
|
// Ajustar diferencia por redondeo
|
||||||
|
const diff = total - counts.reduce((s, v) => s + v, 0);
|
||||||
|
if (diff !== 0) counts[0] += diff;
|
||||||
|
return counts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// helpers para sumar por rango
|
||||||
|
const isoDate = (d) => new Date(d + "T00:00:00");
|
||||||
|
const totalForRange = (startIso, endIso) => {
|
||||||
|
const s = isoDate(startIso);
|
||||||
|
const e = isoDate(endIso);
|
||||||
|
return raw.value.reduce((acc, r) => {
|
||||||
|
const rd = isoDate(r.date);
|
||||||
|
if (rd >= s && rd <= e) acc += Number(r.available || 0);
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dayStart = (iso) => iso; // ya vienen en formato YYYY-MM-DD
|
||||||
|
const weekStartFrom = (endIso) => {
|
||||||
|
const d = new Date(endIso + "T00:00:00");
|
||||||
|
d.setDate(d.getDate() - 6); // 7 días inclusive
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
};
|
||||||
|
const monthRangeFrom = (endIso) => {
|
||||||
|
const d = new Date(endIso + "T00:00:00");
|
||||||
|
d.setDate(1);
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construye chartData ahora por tipos de apoyo
|
||||||
|
const chartData = computed(() => {
|
||||||
|
const colors = ["#ef4444", "#3b82f6", "#f59e0b"];
|
||||||
|
|
||||||
|
if(USE_MOCK.value) {
|
||||||
|
const todayCounts = buildMockCounts(20);
|
||||||
|
const weekCounts = buildMockCounts(100);
|
||||||
|
const monthCounts = buildMockCounts(400);
|
||||||
|
return {
|
||||||
|
labels: supportTypes,
|
||||||
|
datasets: [
|
||||||
|
{ label: "Hoy", data: todayCounts, backgroundColor: colors[0] },
|
||||||
|
{ label: "Semana", data: weekCounts, backgroundColor: colors[1] },
|
||||||
|
{ label: "Mes", data: monthCounts, backgroundColor: colors[2] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const end = dateRange.value.end;
|
||||||
|
const todayTotal = totalForRange(end, end);
|
||||||
|
const weekTotal = totalForRange(end, weekStartFrom(end));
|
||||||
|
const monthTotal = totalForRange(end, monthRangeFrom(end));
|
||||||
|
|
||||||
|
const todayCounts = buildMockCounts(todayTotal || 0);
|
||||||
|
const weekCounts = buildMockCounts(weekTotal || 0);
|
||||||
|
const monthCounts = buildMockCounts(monthTotal || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: supportTypes,
|
||||||
|
datasets: [
|
||||||
|
{ label: "Hoy", data: todayCounts, backgroundColor: colors[0] },
|
||||||
|
{ label: "Semana", data: weekCounts, backgroundColor: colors[1] },
|
||||||
|
{ label: "Mes", data: monthCounts, backgroundColor: colors[2] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: { stacked: false },
|
||||||
|
y: { beginAtZero: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildEmptyRange = (start, end) => {
|
||||||
|
const s = new Date(start);
|
||||||
|
const e = new Date(end);
|
||||||
|
const list = [];
|
||||||
|
for (let d = new Date(s); d <= e; d.setDate(d.getDate() + 1)) {
|
||||||
|
list.push({
|
||||||
|
date: new Date(d).toISOString().slice(0, 10),
|
||||||
|
available: 0,
|
||||||
|
unavailable: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapApiToSeries = (items, start, end) => {
|
||||||
|
const base = buildEmptyRange(start, end);
|
||||||
|
const byDate = {};
|
||||||
|
(items || []).forEach((it) => {
|
||||||
|
const key = (it.date || it.fecha || it.day || "").slice(0, 10);
|
||||||
|
if (key)
|
||||||
|
byDate[key] = {
|
||||||
|
date: key,
|
||||||
|
available: Number(
|
||||||
|
it.available ?? it.disponibles ?? it.available_count ?? 0
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return base.map((b) => byDate[b.date] || b);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
const res = await axios.get("/api/atencion-ciudadana", {
|
||||||
|
params: {
|
||||||
|
start: dateRange.value.start,
|
||||||
|
end: dateRange.value.end,
|
||||||
|
},
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
|
});
|
||||||
|
const payload = res.data;
|
||||||
|
const items = payload?.data ?? payload?.items ?? payload ?? [];
|
||||||
|
raw.value = mapApiToSeries(
|
||||||
|
Array.isArray(items) ? items : [],
|
||||||
|
dateRange.value.start,
|
||||||
|
dateRange.value.end
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
error.value =
|
||||||
|
e.response?.data?.message || e.message || "Error al cargar datos";
|
||||||
|
raw.value = mapApiToSeries([], dateRange.value.start, dateRange.value.end);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(dateRange, loadData, { deep: true });
|
||||||
|
onMounted(loadData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<header>
|
||||||
|
<div class="bg-gray-100 py-2">
|
||||||
|
<div class="text-left container mx-auto sm:px-6 lg:px-8">
|
||||||
|
<span class="text-sm text-gray-700 font-bold">COMALCALCO.GOB.MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background-color: #621132" class="shadow-lg">
|
||||||
|
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center h-28">
|
||||||
|
<img
|
||||||
|
src="https://apoyos.comalcalco.gob.mx/images/logo_blanco.png"
|
||||||
|
alt="Logo Comalcalco"
|
||||||
|
class="h-24 w-auto object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="container mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h1 class="text-xl font-semibold mb-4">Atención Ciudadana</h1>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<DateRange v-model="dateRange" :presets="true" />
|
||||||
|
<!-- Toggle para usar datos falsos -->
|
||||||
|
<label class="inline-flex items-center text-sm text-gray-600 ml-4">
|
||||||
|
<input type="checkbox" v-model="USE_MOCK" class="mr-2 rounded border-gray-300" />
|
||||||
|
Usar datos falsos
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 flex items-center gap-4">
|
||||||
|
<div v-if="loading" class="ml-auto text-sm text-gray-500">
|
||||||
|
Cargando...
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="ml-auto text-sm text-red-600">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-64">
|
||||||
|
<Bars :chartData="chartData" :chartOptions="chartOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-r from-green-600 to-emerald-700 rounded-lg shadow-lg p-6 text-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-medium opacity-90">Apoyos Mes</h2>
|
||||||
|
<span >Entregados en el mes</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-full w-16 h-16 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold text-green-600">20</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-lg p-6 text-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-medium opacity-90">Apoyos Semana</h2>
|
||||||
|
<span class="text-sm">Entregados en la semana</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-full w-16 h-16 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold text-blue-600">20</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-r from-yellow-600 to-amber-700 rounded-lg shadow-lg p-6 text-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-medium opacity-90">Apoyos Hoy</h2>
|
||||||
|
<span class="text-sm">Entregados hoy</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-full w-16 h-16 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold text-amber-600">20</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,17 +1,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { router } from '@inertiajs/vue3';
|
import { router } from "@inertiajs/vue3";
|
||||||
|
|
||||||
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
|
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
|
||||||
import NotificationController from '@/Controllers/NotificationController';
|
import NotificationController from "@/Controllers/NotificationController";
|
||||||
|
|
||||||
const notificationCtl = NotificationController;
|
const notificationCtl = NotificationController;
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
router.post(route('logout'), {}, {
|
router.post(
|
||||||
|
route("logout"),
|
||||||
|
{},
|
||||||
|
{
|
||||||
onBefore: () => {
|
onBefore: () => {
|
||||||
notificationCtl.stop();
|
notificationCtl.stop();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -19,13 +23,13 @@ const logout = () => {
|
|||||||
<div
|
<div
|
||||||
class="min-h-screen font-sans bg-gradient-to-br from-[#621132] via-[#621132] to-[#621132] text-[#621132] antialiased"
|
class="min-h-screen font-sans bg-gradient-to-br from-[#621132] via-[#621132] to-[#621132] text-[#621132] antialiased"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-end p-5 text-white">
|
<button
|
||||||
<div class="border p-1 rounded">
|
class="fixed top-4 right-4 z-50 inline-flex items-center bg-white/10 text-white rounded-lg hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30 transition p-2"
|
||||||
<button type="button" @click.prevent="logout">
|
type="button"
|
||||||
|
@click.prevent="logout"
|
||||||
|
>
|
||||||
{{ $t("auth.logout") }}
|
{{ $t("auth.logout") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main class="grid min-h-screen place-items-center p-4">
|
<main class="grid min-h-screen place-items-center p-4">
|
||||||
<!-- Tarjeta -->
|
<!-- Tarjeta -->
|
||||||
<section
|
<section
|
||||||
|
|||||||
@ -218,7 +218,6 @@ const loadDashboardData = async () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// si la petición fue cancelada no sobreescribimos el error de UI
|
|
||||||
if (axios.isCancel && axios.isCancel(err)) return;
|
if (axios.isCancel && axios.isCancel(err)) return;
|
||||||
planeacionesError.value = estimacionesError.value = true;
|
planeacionesError.value = estimacionesError.value = true;
|
||||||
planeacionesErrorMessage.value = estimacionesErrorMessage.value =
|
planeacionesErrorMessage.value = estimacionesErrorMessage.value =
|
||||||
|
|||||||
@ -221,7 +221,7 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
<p class="text-gray-600">Resumen por unidad administrativa</p>
|
<p class="text-gray-600">Resumen por unidad administrativa</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-3">
|
<!-- <div class="ml-auto flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
@click="toggleBars"
|
@click="toggleBars"
|
||||||
class="bg-primary text-white px-4 py-2 rounded"
|
class="bg-primary text-white px-4 py-2 rounded"
|
||||||
@ -234,7 +234,7 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
>
|
>
|
||||||
Más recaudados
|
Más recaudados
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
Route::inertia('/api-tramite', 'Dashboard/Tramites')->name('api-tramite');
|
Route::inertia('/api-tramite', 'Dashboard/Tramites')->name('api-tramite');
|
||||||
Route::inertia('/api-obra', 'Dashboard/Obras')->name('api-obra');
|
Route::inertia('/api-obra', 'Dashboard/Obras')->name('api-obra');
|
||||||
|
Route::inertia('/api-atencion', 'Dashboard/AtencionCiudadana')->name('api-atencion');
|
||||||
|
|
||||||
# Log de Acciones
|
# Log de Acciones
|
||||||
Route::resource('histories', HistoryLogController::class)->only([
|
Route::resource('histories', HistoryLogController::class)->only([
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user