Se agregaron las gráficas en tramites
This commit is contained in:
parent
bec4f6082f
commit
de7676da26
@ -4,6 +4,7 @@
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@ -11,84 +12,40 @@ class AtencionController extends BaseController
|
||||
{
|
||||
public function Atencion(Request $request)
|
||||
{
|
||||
$query = $request->only(['start', 'end', 'type', 'action']);
|
||||
$query = $request->only(['start', 'end', 'type', 'action', 'period', 'charts_only']);
|
||||
$input = array_merge(['action' => 'index', 'type' => 'api'], $query);
|
||||
|
||||
$startDate = $input['start'] ?? date('Y-01-01');
|
||||
$endDate = $input['end'] ?? date('Y-12-31');
|
||||
$period = $input['period'] ?? null;
|
||||
$chartsOnly = $input['charts_only'] ?? false;
|
||||
|
||||
$baseParams = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'type' => $input['type'] ?? 'api',
|
||||
'action' => $input['action'] ?? 'index',
|
||||
];
|
||||
// Cache key para diferentes tipos de peticiones
|
||||
$cacheKey = 'atencion_' . md5(serialize([
|
||||
'start' => $startDate,
|
||||
'end' => $endDate,
|
||||
'period' => $period,
|
||||
'charts_only' => $chartsOnly
|
||||
]));
|
||||
|
||||
$baseUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/stats-by-date-range';
|
||||
$countsUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/counts-by-date';
|
||||
$dashboardUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/dashboard/api';
|
||||
$dashboardUrl2 = 'https://apoyos.comalcalco.gob.mx/beneficiaries/dashboard/api?type=servicio';
|
||||
// Intentar obtener desde cache (2 minutos)
|
||||
if (Cache::has($cacheKey)) {
|
||||
return response()->json(Cache::get($cacheKey), 200)
|
||||
->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::get($baseUrl, $baseParams);
|
||||
|
||||
if (!$response->successful()) {
|
||||
return response()->json(['error' => 'Error del servicio'], $response->status());
|
||||
// Si solo necesitamos gráficas, hacer menos peticiones
|
||||
if ($chartsOnly) {
|
||||
$result = $this->getChartsData($startDate, $endDate);
|
||||
} else {
|
||||
$result = $this->getFullData($startDate, $endDate, $period);
|
||||
}
|
||||
|
||||
$mainData = $response->json();
|
||||
$countsAc = [];
|
||||
try {
|
||||
$countsResp = Http::get($countsUrl, [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
]);
|
||||
// Cachear resultado por 2 minutos
|
||||
Cache::put($cacheKey, $result, 120);
|
||||
|
||||
if ($countsResp->successful()) {
|
||||
$countsAc = $countsResp->json();
|
||||
} else {
|
||||
Log::error('Error al obtener los conteos de atención', ['status' => $countsResp->status()]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error al obtener los conteos de atención', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$dashboardData = [];
|
||||
try {
|
||||
$dashboardResp = Http::get($dashboardUrl);
|
||||
|
||||
if ($dashboardResp->successful()) {
|
||||
$dashboardData = $dashboardResp->json();
|
||||
} else {
|
||||
Log::error('Error al obtener datos del dashboard', ['status' => $dashboardResp->status()]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error al obtener datos del dashboard', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$dashboardServicioData = [];
|
||||
try {
|
||||
$dashboardResp2 = Http::get($dashboardUrl2);
|
||||
|
||||
if ($dashboardResp2->successful()) {
|
||||
$dashboardServicioData = $dashboardResp2->json();
|
||||
} else {
|
||||
Log::error('Error al obtener datos del dashboard servicio', ['status' => $dashboardResp2->status()]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error al obtener datos del dashboard servicio', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
// Combina todos los resultados
|
||||
$combinedData = array_merge(
|
||||
is_array($mainData) ? $mainData : [],
|
||||
[
|
||||
'counts' => $countsAc,
|
||||
'dashboard' => $dashboardData,
|
||||
'dashboard_servicio' => $dashboardServicioData,
|
||||
]
|
||||
);
|
||||
return response()->json($combinedData, 200)
|
||||
return response()->json($result, 200)
|
||||
->header('Content-Type', 'application/json');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error en la consulta de atención', ['exception' => $e->getMessage()]);
|
||||
@ -96,6 +53,96 @@ public function Atencion(Request $request)
|
||||
}
|
||||
}
|
||||
|
||||
private function getChartsData($startDate, $endDate)
|
||||
{
|
||||
$baseParams = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'type' => 'api',
|
||||
'action' => 'index',
|
||||
];
|
||||
|
||||
$baseUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/stats-by-date-range';
|
||||
|
||||
// Solo hacer la petición principal para gráficas
|
||||
$response = Http::timeout(15)->get($baseUrl, $baseParams);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Error del servicio principal');
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
private function getFullData($startDate, $endDate, $period)
|
||||
{
|
||||
$baseParams = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'type' => 'api',
|
||||
'action' => 'index',
|
||||
];
|
||||
|
||||
$urls = [
|
||||
'main' => 'https://apoyos.comalcalco.gob.mx/beneficiaries/stats-by-date-range',
|
||||
'counts' => 'https://apoyos.comalcalco.gob.mx/beneficiaries/counts-by-date',
|
||||
'dashboard' => 'https://apoyos.comalcalco.gob.mx/beneficiaries/dashboard/api',
|
||||
'dashboard_servicio' => 'https://apoyos.comalcalco.gob.mx/beneficiaries/dashboard/api?type=servicio'
|
||||
];
|
||||
|
||||
// Hacer peticiones en paralelo usando HTTP Pool
|
||||
$responses = Http::pool(fn($pool) => [
|
||||
$pool->timeout(15)->get($urls['main'], $baseParams),
|
||||
$pool->timeout(15)->get($urls['counts'], [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
]),
|
||||
$pool->timeout(15)->get($urls['dashboard']),
|
||||
$pool->timeout(15)->get($urls['dashboard_servicio'])
|
||||
]);
|
||||
|
||||
$mainData = [];
|
||||
$countsAc = [];
|
||||
$dashboardData = [];
|
||||
$dashboardServicioData = [];
|
||||
|
||||
// Procesar respuestas
|
||||
if ($responses[0]->successful()) {
|
||||
$mainData = $responses[0]->json();
|
||||
} else {
|
||||
Log::error('Error en petición principal', ['status' => $responses[0]->status()]);
|
||||
}
|
||||
|
||||
if ($responses[1]->successful()) {
|
||||
$countsAc = $responses[1]->json();
|
||||
} else {
|
||||
Log::error('Error en petición counts', ['status' => $responses[1]->status()]);
|
||||
}
|
||||
|
||||
if ($responses[2]->successful()) {
|
||||
$dashboardData = $responses[2]->json();
|
||||
} else {
|
||||
Log::error('Error en petición dashboard', ['status' => $responses[2]->status()]);
|
||||
}
|
||||
|
||||
if ($responses[3]->successful()) {
|
||||
$dashboardServicioData = $responses[3]->json();
|
||||
} else {
|
||||
Log::error('Error en petición dashboard servicio', ['status' => $responses[3]->status()]);
|
||||
}
|
||||
|
||||
// Combinar todos los resultados
|
||||
return array_merge(
|
||||
is_array($mainData) ? $mainData : [],
|
||||
[
|
||||
'counts' => $countsAc,
|
||||
'dashboard' => $dashboardData,
|
||||
'dashboard_servicio' => $dashboardServicioData,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function getSupportOptions(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ObrasController extends BaseController
|
||||
{
|
||||
@ -12,10 +13,23 @@ public function Obras(Request $request)
|
||||
$query = $request->only(['start', 'end', 'type', 'action']);
|
||||
$query = array_merge($query, ['action' => 'index', 'type' => 'api'], $query);
|
||||
|
||||
// Caché por 2 minutos (datos más dinámicos)
|
||||
$cacheKey = "obras_data_" . md5(serialize($query));
|
||||
|
||||
if ($cachedData = Cache::get($cacheKey)) {
|
||||
return response()->json($cachedData, 200)
|
||||
->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php', $query);
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('Obras service error', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
'query' => $query
|
||||
]);
|
||||
return response()->json(['error' => 'External service error'], $response->status());
|
||||
}
|
||||
|
||||
@ -40,10 +54,16 @@ public function Obras(Request $request)
|
||||
// Combinar ambas respuestas
|
||||
$combinedData = array_merge($mainData, ['counters' => $countersData, 'users' => $usersData]);
|
||||
|
||||
// Cachear por 2 minutos
|
||||
Cache::put($cacheKey, $combinedData, now()->addMinutes(2));
|
||||
|
||||
return response()->json($combinedData, $response->status())
|
||||
->header('Content-Type', $response->header('Content-Type', 'application/json'));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Proxy error: '.$e->getMessage());
|
||||
Log::error('Proxy error: '.$e->getMessage(), [
|
||||
'query' => $query,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return response()->json(['error' => 'Unable to contact external service'], 502);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
<?php namespace App\Http\Controllers\Dashboard;
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Dashboard;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class TramiteController extends BaseController
|
||||
{
|
||||
@ -12,17 +15,39 @@ public function tramiteEspecial(Request $request)
|
||||
$query = $request->only(['start', 'end', 'type']);
|
||||
$query = array_merge(['type' => 'api'], $query);
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get('https://tramites.comalcalco.gob.mx/reporte-especial', $query);
|
||||
// Caché por 2 minutos (datos más dinámicos)
|
||||
$cacheKey = "tramites_data_" . md5(serialize($query));
|
||||
|
||||
if (! $response->successful()) {
|
||||
if ($cachedData = Cache::get($cacheKey)) {
|
||||
return response()->json($cachedData, 200)
|
||||
->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(20)->get('https://tramites.comalcalco.gob.mx/reporte-especial', $query);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::warning('Tramites service error', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
'query' => $query
|
||||
]);
|
||||
return response()->json(['error' => 'External service error'], $response->status());
|
||||
}
|
||||
|
||||
return response()->json($response->json(), $response->status())
|
||||
$data = $response->json();
|
||||
|
||||
// Cachear por 2 minutos
|
||||
Cache::put($cacheKey, $data, now()->addMinutes(2));
|
||||
|
||||
return response()->json($data, $response->status())
|
||||
->header('Content-Type', $response->header('Content-Type', 'application/json'));
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Proxy error: '.$e->getMessage());
|
||||
Log::error('Proxy error: ' . $e->getMessage(), [
|
||||
'query' => $query,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return response()->json(['error' => 'Unable to contact external service'], 502);
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,11 +67,10 @@ const periodOptions = [
|
||||
{ id: "semana", name: "Esta Semana" },
|
||||
];
|
||||
|
||||
// NUEVO: Función para cargar datos de gráficas
|
||||
//Función para cargar datos de gráficas
|
||||
const loadChartsData = async () => {
|
||||
const cacheKey = getChartsCacheKey();
|
||||
|
||||
// Verificar cache de gráficas
|
||||
if (chartsCache.has(cacheKey)) {
|
||||
const cachedData = chartsCache.get(cacheKey);
|
||||
buildChartsFromData(cachedData);
|
||||
@ -87,19 +86,16 @@ const loadChartsData = async () => {
|
||||
|
||||
const res = await axios.get("/api/reporte-atencion", {
|
||||
params,
|
||||
timeout: 10000,
|
||||
timeout: 20000, // Aumentar timeout
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
|
||||
const payload = res.data || {};
|
||||
|
||||
// Guardar en cache de gráficas
|
||||
chartsCache.set(cacheKey, payload);
|
||||
|
||||
buildChartsFromData(payload);
|
||||
} catch (e) {
|
||||
console.error("Error cargando datos de gráficas:", e);
|
||||
// En caso de error, mantener gráficas actuales o limpiar
|
||||
// En caso de error, mantener gráficas actuales
|
||||
}
|
||||
};
|
||||
|
||||
@ -139,17 +135,15 @@ const normalizeResponse = (res) => {
|
||||
const load = async () => {
|
||||
const cacheKey = getCacheKey();
|
||||
|
||||
// Verificar cache primero
|
||||
if (cache.has(cacheKey)) {
|
||||
const cachedData = cache.get(cacheKey);
|
||||
applyDataToComponents(cachedData);
|
||||
applyDataToComponents(cachedData, false); // Sin gráficas
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
// Cancelar petición anterior
|
||||
if (cancelTokenSource) {
|
||||
cancelTokenSource.cancel("cancel");
|
||||
}
|
||||
@ -158,28 +152,22 @@ const load = async () => {
|
||||
try {
|
||||
const params = {
|
||||
period: selectedPeriod.value,
|
||||
// NO incluir fechas personalizadas aquí
|
||||
};
|
||||
|
||||
const res = await axios.get("/api/reporte-atencion", {
|
||||
params,
|
||||
timeout: 10000,
|
||||
timeout: 20000, // Aumentar timeout
|
||||
cancelToken: cancelTokenSource.token,
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
|
||||
const payload = res.data || {};
|
||||
|
||||
// Guardar en cache
|
||||
cache.set(cacheKey, payload);
|
||||
|
||||
// Aplicar datos (SIN gráficas)
|
||||
applyDataToComponents(payload, false);
|
||||
applyDataToComponents(payload, false); // Sin gráficas para evitar conflicto
|
||||
} catch (e) {
|
||||
if (!axios.isCancel(e)) {
|
||||
console.error("Error en la petición:", e);
|
||||
error.value =
|
||||
e.response?.data?.error || e.message || "Error al cargar datos";
|
||||
error.value = e.response?.data?.error || e.message || "Error al cargar datos";
|
||||
clearComponentData();
|
||||
}
|
||||
} finally {
|
||||
@ -421,13 +409,32 @@ const closeExportModal = () => {
|
||||
showExportModal.value = false;
|
||||
};
|
||||
|
||||
const toggleGroup = (typeName) => {
|
||||
if (expandedGroups.value.has(typeName)) {
|
||||
expandedGroups.value.delete(typeName);
|
||||
} else {
|
||||
expandedGroups.value.add(typeName);
|
||||
}
|
||||
};
|
||||
|
||||
// Watchers para recargar datos cuando cambien los filtros
|
||||
watch([selectedDepartment, selectedType, selectedPeriod], () => {
|
||||
clearTimeout(debounceId);
|
||||
debounceId = setTimeout(load, 100);
|
||||
});
|
||||
|
||||
onMounted(load);
|
||||
onMounted(() => {
|
||||
// Cargar datos iniciales
|
||||
load();
|
||||
// Cargar gráficas iniciales
|
||||
loadChartsData();
|
||||
|
||||
// Limpiar caches
|
||||
setInterval(() => {
|
||||
cache.clear();
|
||||
chartsCache.clear();
|
||||
}, 600000); // 10 minutos
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -94,7 +94,34 @@ const loadDashboardData = async () => {
|
||||
}));
|
||||
const lastP =
|
||||
data.planeacions.lastUpdate ?? data.planeacions.last_update ?? null;
|
||||
lastPlaneacion.value = lastP && (lastP.id ?? lastP.ID) ? lastP : null;
|
||||
lastPlaneacion.value =
|
||||
lastP && (lastP.id ?? lastP.ID)
|
||||
? {
|
||||
name:
|
||||
lastP.name ??
|
||||
lastP.user_name ??
|
||||
lastP.nombre_usuario ??
|
||||
"Usuario no disponible",
|
||||
num_proyecto: lastP.num_proyecto ?? lastP.numProyecto ?? null,
|
||||
proyecto: lastP.proyecto ?? "Proyecto no disponible",
|
||||
desc_partida:
|
||||
lastP.desc_partida ??
|
||||
lastP.descripcion_partida ??
|
||||
"Partida no disponible",
|
||||
clave: lastP.clave ?? lastP.clave_concepto ?? "--",
|
||||
desc_concepto:
|
||||
lastP.desc_concepto ??
|
||||
lastP.descripcion_concepto ??
|
||||
"Concepto no disponible",
|
||||
cumplimiento: lastP.cumplimiento ?? lastP.cumplimiento_total ?? 0,
|
||||
desc_evidencia:
|
||||
lastP.desc_evidencia ??
|
||||
lastP.descripcion_evidencia ??
|
||||
"Sin descripción disponible",
|
||||
fecha_evidencia: lastP.fecha_evidencia ?? lastP.fecha ?? null,
|
||||
created_at: lastP.created_at ?? lastP.createdAt ?? null,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
// Estimaciones: normalizar y agrupar por proyecto_id
|
||||
@ -143,7 +170,27 @@ const loadDashboardData = async () => {
|
||||
|
||||
const lastE =
|
||||
data.estimacions.lastUpdate ?? data.estimacions.last_update ?? null;
|
||||
lastEstimacion.value = lastE && (lastE.id ?? lastE.ID) ? lastE : null;
|
||||
lastEstimacion.value =
|
||||
lastE && (lastE.id ?? lastE.ID)
|
||||
? {
|
||||
user_name:
|
||||
lastE.user_name ??
|
||||
lastE.nombre_usuario ??
|
||||
lastE.registrado_por ??
|
||||
"Usuario no disponible",
|
||||
num_estimacion:
|
||||
lastE.num_estimacion ?? lastE.numEstimacion ?? null,
|
||||
num_proyecto: lastE.num_proyecto ?? lastE.numProyecto ?? null,
|
||||
proyecto: lastE.proyecto ?? "Proyecto no disponible",
|
||||
monto_estimacion:
|
||||
lastE.monto_estimacion ?? lastE.montoEstimacion ?? "0",
|
||||
monto_acumulado:
|
||||
lastE.monto_acumulado ?? lastE.montoAcumulado ?? "0",
|
||||
periodo_estimacion:
|
||||
lastE.periodo_estimacion ?? lastE.periodo ?? null,
|
||||
created_at: lastE.created_at ?? lastE.createdAt ?? null,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
// NUEVO: Procesar datos de contadores del segundo endpoint
|
||||
@ -486,6 +533,40 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-indigo-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900">Proyecto</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
<span
|
||||
class="font-mono text-xs bg-gray-100 px-2 py-1 rounded"
|
||||
>{{ lastPlaneacion.num_proyecto || "--" }}</span
|
||||
>
|
||||
<span class="ml-2">{{
|
||||
lastPlaneacion.proyecto || "Proyecto no disponible"
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
@ -699,6 +780,36 @@ onMounted(() => {
|
||||
|
||||
<div v-else class="p-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
Registrado por
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 truncate">
|
||||
{{ lastEstimacion.user_name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
|
||||
@ -5,20 +5,21 @@ import axios from "axios";
|
||||
import Bars from "@/Components/Dashboard/Charts/Bars.vue";
|
||||
import Pie from "@/Components/Dashboard/Charts/Pie.vue";
|
||||
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
|
||||
import Footer from "@/Components/Dashboard/Footer.vue";
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
|
||||
const data = ref(null);
|
||||
const loading = ref(true);
|
||||
const data = ref({
|
||||
procedures_opened_today: 0,
|
||||
procedures_with_movements_today: 0,
|
||||
procedures_by_administration: [],
|
||||
movements_by_procedure: [],
|
||||
});
|
||||
const error = ref(null);
|
||||
|
||||
// Rango inicial: Hoy→Hoy (coincide con preset del DateRange)
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const dateRange = ref({ start: today, end: today });
|
||||
|
||||
// toggles
|
||||
const showBars = ref(false);
|
||||
const showPie = ref(false);
|
||||
|
||||
// datasets
|
||||
const barChartData = ref({ labels: [], datasets: [] });
|
||||
const pieChartData = ref({ labels: [], datasets: [] });
|
||||
@ -86,27 +87,31 @@ const fetchReport = async ({ start, end }) => {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
|
||||
// axios lanza en error si status >= 400, aquí devolvemos data directamente
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const payload = await fetchReport({
|
||||
start: dateRange.value.start,
|
||||
end: dateRange.value.end,
|
||||
});
|
||||
data.value = payload;
|
||||
mapToCharts(payload, mode.value);
|
||||
data.value = {
|
||||
procedures_opened_today: payload?.procedures_opened_today || 0,
|
||||
procedures_with_movements_today: payload?.procedures_with_movements_today || 0,
|
||||
procedures_by_administration: Array.isArray(payload?.procedures_by_administration)
|
||||
? payload.procedures_by_administration : [],
|
||||
movements_by_procedure: Array.isArray(payload?.movements_by_procedure)
|
||||
? payload.movements_by_procedure : [],
|
||||
};
|
||||
mapToCharts(data.value, mode.value);
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError") {
|
||||
if (!axios.isCancel(e)) {
|
||||
console.error(e);
|
||||
error.value = e.message || "Error desconocido";
|
||||
mapToCharts(data.value, mode.value);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -119,60 +124,76 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(load);
|
||||
onMounted(() => {
|
||||
mapToCharts(data.value, mode.value);
|
||||
load();
|
||||
});
|
||||
|
||||
// --- mapping charts: usa SIEMPRE el rango ---
|
||||
function mapToCharts(api, currentMode) {
|
||||
const list = Array.isArray(api?.movements_by_procedure)
|
||||
const proceduresList = Array.isArray(api?.movements_by_procedure)
|
||||
? api.movements_by_procedure
|
||||
: [];
|
||||
|
||||
const labels = list.map((it) => (it.name || "").toUpperCase());
|
||||
// Filtrar procedimientos con datos significativos y ordenar por total descendente
|
||||
const filteredProcedures = proceduresList
|
||||
.filter((proc) => (proc.opened || 0) + (proc.closed || 0) > 0)
|
||||
.sort((a, b) => {
|
||||
const totalA = (a.opened || 0) + (a.closed || 0);
|
||||
const totalB = (b.opened || 0) + (b.closed || 0);
|
||||
return totalB - totalA;
|
||||
});
|
||||
|
||||
// Pie: total (opened + closed). Si tu API devuelve valores por rango, puedes sustituir aquí.
|
||||
const pieValues = list.map((it) => (it.opened || 0) + (it.closed || 0));
|
||||
const labels = filteredProcedures.map((proc) =>
|
||||
(proc.name || "").toUpperCase().substring(0, 30) +
|
||||
((proc.name || "").length > 30 ? "..." : "")
|
||||
);
|
||||
|
||||
// Pie: Distribución de trámites por procedimiento
|
||||
const pieValues = filteredProcedures.map((proc) => (proc.opened || 0) + (proc.closed || 0));
|
||||
pieChartData.value = {
|
||||
labels,
|
||||
labels: labels.length > 0 ? labels : ["Sin datos"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Recaudados",
|
||||
data: pieValues,
|
||||
backgroundColor: labels.map(
|
||||
(_, i) => `hsl(${(i * 360) / Math.max(1, labels.length)}, 70%, 50%)`
|
||||
),
|
||||
label: "Distribución de Trámites por Procedimiento",
|
||||
data: pieValues.length > 0 ? pieValues : [0],
|
||||
backgroundColor: labels.length > 0
|
||||
? labels.map((_, i) => `hsl(${(i * 360) / Math.max(1, labels.length)}, 70%, 50%)`)
|
||||
: ["#e5e7eb"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Bars: dataset según modo
|
||||
// Bars: Trámites más solicitados según el modo
|
||||
let barLabel = "";
|
||||
let barValues = [];
|
||||
|
||||
if (currentMode === "día") {
|
||||
barLabel = "Solicitados Hoy";
|
||||
barValues = list.map(
|
||||
(it) => it.procedures_opened_today ?? it.opened_today ?? 0
|
||||
);
|
||||
barLabel = "Trámites Abiertos Hoy";
|
||||
barValues = filteredProcedures.map((proc) => proc.opened_today || 0);
|
||||
} else if (currentMode === "semana") {
|
||||
barLabel = "Solicitados Semana";
|
||||
barValues = list.map((it) => it.total ?? 0);
|
||||
barLabel = "Trámites de la Semana";
|
||||
barValues = filteredProcedures.map((proc) => proc.week_total || (proc.opened || 0));
|
||||
} else {
|
||||
barLabel = "Solicitados en Rango";
|
||||
barValues = list.map((it) => {
|
||||
if (typeof it.range_total === "number") return it.range_total;
|
||||
return (it.opened || 0) + (it.closed || 0);
|
||||
barLabel = "Trámites en el Rango Seleccionado";
|
||||
barValues = filteredProcedures.map((proc) => {
|
||||
// Priorizar datos específicos del rango si están disponibles
|
||||
if (typeof proc.range_total === "number") return proc.range_total;
|
||||
return (proc.opened || 0) + (proc.closed || 0);
|
||||
});
|
||||
}
|
||||
|
||||
barChartData.value = {
|
||||
labels,
|
||||
datasets: [barChartOptions.makeDataset(barLabel, barValues)],
|
||||
labels: labels.length > 0 ? labels : ["Sin datos"],
|
||||
datasets: [
|
||||
{
|
||||
...barChartOptions.makeDataset(barLabel, barValues.length > 0 ? barValues : [0]),
|
||||
backgroundColor: "rgba(99, 102, 241, 0.2)",
|
||||
borderColor: "rgba(99, 102, 241, 1)",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// UI helpers
|
||||
const toggleBars = () => (showBars.value = !showBars.value);
|
||||
const togglePie = () => (showPie.value = !showPie.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -243,14 +264,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
||||
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>
|
||||
<div v-else>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
@ -264,21 +277,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
||||
</h1>
|
||||
<p class="text-gray-600">Resumen por unidad administrativa</p>
|
||||
</div>
|
||||
|
||||
<!-- <div class="ml-auto flex items-center gap-3">
|
||||
<button
|
||||
@click="toggleBars"
|
||||
class="bg-primary text-white px-4 py-2 rounded"
|
||||
>
|
||||
Gráfica de Barras
|
||||
</button>
|
||||
<button
|
||||
@click="togglePie"
|
||||
class="bg-primary text-white px-4 py-2 rounded"
|
||||
>
|
||||
Más recaudados
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -487,7 +485,7 @@ const togglePie = () => (showPie.value = !showPie.value);
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tbody class="bg-white divide-y divide-gray-200 uppercase">
|
||||
<tr
|
||||
v-for="procedure in data.movements_by_procedure || []"
|
||||
:key="procedure.id"
|
||||
@ -554,7 +552,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
||||
|
||||
<!-- Bars -->
|
||||
<div
|
||||
v-if="showBars"
|
||||
class="max-w-7xl mx-auto mt-12 px-4 sm:px-6 lg:px-8 mb-8 bg-white rounded-lg shadow p-6"
|
||||
>
|
||||
<!-- DateRange mejorado -->
|
||||
@ -565,7 +562,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
||||
|
||||
<!-- Pie -->
|
||||
<div
|
||||
v-if="showPie"
|
||||
class="max-w-7xl mx-auto mt-8 px-4 sm:px-6 lg:px-8 bg-white rounded-lg shadow p-6"
|
||||
>
|
||||
<Pie :chartData="pieChartData" :chartOptions="pieChartOptions" />
|
||||
@ -579,6 +575,7 @@ const togglePie = () => (showPie.value = !showPie.value);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user