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\Http\Request;
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@ -11,84 +12,40 @@ class AtencionController extends BaseController
|
|||||||
{
|
{
|
||||||
public function Atencion(Request $request)
|
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);
|
$input = array_merge(['action' => 'index', 'type' => 'api'], $query);
|
||||||
|
|
||||||
$startDate = $input['start'] ?? date('Y-01-01');
|
$startDate = $input['start'] ?? date('Y-01-01');
|
||||||
$endDate = $input['end'] ?? date('Y-12-31');
|
$endDate = $input['end'] ?? date('Y-12-31');
|
||||||
|
$period = $input['period'] ?? null;
|
||||||
|
$chartsOnly = $input['charts_only'] ?? false;
|
||||||
|
|
||||||
$baseParams = [
|
// Cache key para diferentes tipos de peticiones
|
||||||
'start_date' => $startDate,
|
$cacheKey = 'atencion_' . md5(serialize([
|
||||||
'end_date' => $endDate,
|
'start' => $startDate,
|
||||||
'type' => $input['type'] ?? 'api',
|
'end' => $endDate,
|
||||||
'action' => $input['action'] ?? 'index',
|
'period' => $period,
|
||||||
];
|
'charts_only' => $chartsOnly
|
||||||
|
]));
|
||||||
|
|
||||||
$baseUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/stats-by-date-range';
|
// Intentar obtener desde cache (2 minutos)
|
||||||
$countsUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/counts-by-date';
|
if (Cache::has($cacheKey)) {
|
||||||
$dashboardUrl = 'https://apoyos.comalcalco.gob.mx/beneficiaries/dashboard/api';
|
return response()->json(Cache::get($cacheKey), 200)
|
||||||
$dashboardUrl2 = 'https://apoyos.comalcalco.gob.mx/beneficiaries/dashboard/api?type=servicio';
|
->header('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = Http::get($baseUrl, $baseParams);
|
// Si solo necesitamos gráficas, hacer menos peticiones
|
||||||
|
if ($chartsOnly) {
|
||||||
if (!$response->successful()) {
|
$result = $this->getChartsData($startDate, $endDate);
|
||||||
return response()->json(['error' => 'Error del servicio'], $response->status());
|
} else {
|
||||||
|
$result = $this->getFullData($startDate, $endDate, $period);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mainData = $response->json();
|
// Cachear resultado por 2 minutos
|
||||||
$countsAc = [];
|
Cache::put($cacheKey, $result, 120);
|
||||||
try {
|
|
||||||
$countsResp = Http::get($countsUrl, [
|
|
||||||
'start_date' => $startDate,
|
|
||||||
'end_date' => $endDate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($countsResp->successful()) {
|
return response()->json($result, 200)
|
||||||
$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)
|
|
||||||
->header('Content-Type', 'application/json');
|
->header('Content-Type', 'application/json');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error en la consulta de atención', ['exception' => $e->getMessage()]);
|
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)
|
public function getSupportOptions(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class ObrasController extends BaseController
|
class ObrasController extends BaseController
|
||||||
{
|
{
|
||||||
@ -12,10 +13,23 @@ public function Obras(Request $request)
|
|||||||
$query = $request->only(['start', 'end', 'type', 'action']);
|
$query = $request->only(['start', 'end', 'type', 'action']);
|
||||||
$query = array_merge($query, ['action' => 'index', 'type' => 'api'], $query);
|
$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 {
|
try {
|
||||||
$response = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php', $query);
|
$response = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php', $query);
|
||||||
|
|
||||||
if (! $response->successful()) {
|
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());
|
return response()->json(['error' => 'External service error'], $response->status());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,10 +54,16 @@ public function Obras(Request $request)
|
|||||||
// Combinar ambas respuestas
|
// Combinar ambas respuestas
|
||||||
$combinedData = array_merge($mainData, ['counters' => $countersData, 'users' => $usersData]);
|
$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())
|
return response()->json($combinedData, $response->status())
|
||||||
->header('Content-Type', $response->header('Content-Type', 'application/json'));
|
->header('Content-Type', $response->header('Content-Type', 'application/json'));
|
||||||
} catch (\Throwable $e) {
|
} 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);
|
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\Http\Request;
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class TramiteController extends BaseController
|
class TramiteController extends BaseController
|
||||||
{
|
{
|
||||||
@ -12,17 +15,39 @@ public function tramiteEspecial(Request $request)
|
|||||||
$query = $request->only(['start', 'end', 'type']);
|
$query = $request->only(['start', 'end', 'type']);
|
||||||
$query = array_merge(['type' => 'api'], $query);
|
$query = array_merge(['type' => 'api'], $query);
|
||||||
|
|
||||||
try {
|
// Caché por 2 minutos (datos más dinámicos)
|
||||||
$response = Http::timeout(5)->get('https://tramites.comalcalco.gob.mx/reporte-especial', $query);
|
$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(['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'));
|
->header('Content-Type', $response->header('Content-Type', 'application/json'));
|
||||||
|
|
||||||
} catch (\Throwable $e) {
|
} 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);
|
return response()->json(['error' => 'Unable to contact external service'], 502);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,11 +67,10 @@ const periodOptions = [
|
|||||||
{ id: "semana", name: "Esta Semana" },
|
{ 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 loadChartsData = async () => {
|
||||||
const cacheKey = getChartsCacheKey();
|
const cacheKey = getChartsCacheKey();
|
||||||
|
|
||||||
// Verificar cache de gráficas
|
|
||||||
if (chartsCache.has(cacheKey)) {
|
if (chartsCache.has(cacheKey)) {
|
||||||
const cachedData = chartsCache.get(cacheKey);
|
const cachedData = chartsCache.get(cacheKey);
|
||||||
buildChartsFromData(cachedData);
|
buildChartsFromData(cachedData);
|
||||||
@ -87,19 +86,16 @@ const loadChartsData = async () => {
|
|||||||
|
|
||||||
const res = await axios.get("/api/reporte-atencion", {
|
const res = await axios.get("/api/reporte-atencion", {
|
||||||
params,
|
params,
|
||||||
timeout: 10000,
|
timeout: 20000, // Aumentar timeout
|
||||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = res.data || {};
|
const payload = res.data || {};
|
||||||
|
|
||||||
// Guardar en cache de gráficas
|
|
||||||
chartsCache.set(cacheKey, payload);
|
chartsCache.set(cacheKey, payload);
|
||||||
|
|
||||||
buildChartsFromData(payload);
|
buildChartsFromData(payload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error cargando datos de gráficas:", 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 load = async () => {
|
||||||
const cacheKey = getCacheKey();
|
const cacheKey = getCacheKey();
|
||||||
|
|
||||||
// Verificar cache primero
|
|
||||||
if (cache.has(cacheKey)) {
|
if (cache.has(cacheKey)) {
|
||||||
const cachedData = cache.get(cacheKey);
|
const cachedData = cache.get(cacheKey);
|
||||||
applyDataToComponents(cachedData);
|
applyDataToComponents(cachedData, false); // Sin gráficas
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
// Cancelar petición anterior
|
|
||||||
if (cancelTokenSource) {
|
if (cancelTokenSource) {
|
||||||
cancelTokenSource.cancel("cancel");
|
cancelTokenSource.cancel("cancel");
|
||||||
}
|
}
|
||||||
@ -158,28 +152,22 @@ const load = async () => {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
period: selectedPeriod.value,
|
period: selectedPeriod.value,
|
||||||
// NO incluir fechas personalizadas aquí
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await axios.get("/api/reporte-atencion", {
|
const res = await axios.get("/api/reporte-atencion", {
|
||||||
params,
|
params,
|
||||||
timeout: 10000,
|
timeout: 20000, // Aumentar timeout
|
||||||
cancelToken: cancelTokenSource.token,
|
cancelToken: cancelTokenSource.token,
|
||||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = res.data || {};
|
const payload = res.data || {};
|
||||||
|
|
||||||
// Guardar en cache
|
|
||||||
cache.set(cacheKey, payload);
|
cache.set(cacheKey, payload);
|
||||||
|
applyDataToComponents(payload, false); // Sin gráficas para evitar conflicto
|
||||||
// Aplicar datos (SIN gráficas)
|
|
||||||
applyDataToComponents(payload, false);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!axios.isCancel(e)) {
|
if (!axios.isCancel(e)) {
|
||||||
console.error("Error en la petición:", e);
|
console.error("Error en la petición:", e);
|
||||||
error.value =
|
error.value = e.response?.data?.error || e.message || "Error al cargar datos";
|
||||||
e.response?.data?.error || e.message || "Error al cargar datos";
|
|
||||||
clearComponentData();
|
clearComponentData();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -421,13 +409,32 @@ const closeExportModal = () => {
|
|||||||
showExportModal.value = false;
|
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
|
// Watchers para recargar datos cuando cambien los filtros
|
||||||
watch([selectedDepartment, selectedType, selectedPeriod], () => {
|
watch([selectedDepartment, selectedType, selectedPeriod], () => {
|
||||||
clearTimeout(debounceId);
|
clearTimeout(debounceId);
|
||||||
debounceId = setTimeout(load, 100);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -94,7 +94,34 @@ const loadDashboardData = async () => {
|
|||||||
}));
|
}));
|
||||||
const lastP =
|
const lastP =
|
||||||
data.planeacions.lastUpdate ?? data.planeacions.last_update ?? null;
|
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
|
// Estimaciones: normalizar y agrupar por proyecto_id
|
||||||
@ -143,7 +170,27 @@ const loadDashboardData = async () => {
|
|||||||
|
|
||||||
const lastE =
|
const lastE =
|
||||||
data.estimacions.lastUpdate ?? data.estimacions.last_update ?? null;
|
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
|
// NUEVO: Procesar datos de contadores del segundo endpoint
|
||||||
@ -486,6 +533,40 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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 items-start space-x-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
@ -699,6 +780,36 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div v-else class="p-6">
|
<div v-else class="p-6">
|
||||||
<div class="space-y-4">
|
<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 items-start space-x-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -5,20 +5,21 @@ import axios from "axios";
|
|||||||
import Bars from "@/Components/Dashboard/Charts/Bars.vue";
|
import Bars from "@/Components/Dashboard/Charts/Bars.vue";
|
||||||
import Pie from "@/Components/Dashboard/Charts/Pie.vue";
|
import Pie from "@/Components/Dashboard/Charts/Pie.vue";
|
||||||
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
|
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
|
||||||
|
import Footer from "@/Components/Dashboard/Footer.vue";
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||||
|
|
||||||
const data = ref(null);
|
const data = ref({
|
||||||
const loading = ref(true);
|
procedures_opened_today: 0,
|
||||||
|
procedures_with_movements_today: 0,
|
||||||
|
procedures_by_administration: [],
|
||||||
|
movements_by_procedure: [],
|
||||||
|
});
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
|
|
||||||
// Rango inicial: Hoy→Hoy (coincide con preset del DateRange)
|
// Rango inicial: Hoy→Hoy (coincide con preset del DateRange)
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const dateRange = ref({ start: today, end: today });
|
const dateRange = ref({ start: today, end: today });
|
||||||
|
|
||||||
// toggles
|
|
||||||
const showBars = ref(false);
|
|
||||||
const showPie = ref(false);
|
|
||||||
|
|
||||||
// datasets
|
// datasets
|
||||||
const barChartData = ref({ labels: [], datasets: [] });
|
const barChartData = ref({ labels: [], datasets: [] });
|
||||||
const pieChartData = ref({ labels: [], datasets: [] });
|
const pieChartData = ref({ labels: [], datasets: [] });
|
||||||
@ -86,27 +87,31 @@ const fetchReport = async ({ start, end }) => {
|
|||||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// axios lanza en error si status >= 400, aquí devolvemos data directamente
|
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const payload = await fetchReport({
|
const payload = await fetchReport({
|
||||||
start: dateRange.value.start,
|
start: dateRange.value.start,
|
||||||
end: dateRange.value.end,
|
end: dateRange.value.end,
|
||||||
});
|
});
|
||||||
data.value = payload;
|
data.value = {
|
||||||
mapToCharts(payload, mode.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) {
|
} catch (e) {
|
||||||
if (e.name !== "AbortError") {
|
if (!axios.isCancel(e)) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
error.value = e.message || "Error desconocido";
|
error.value = e.message || "Error desconocido";
|
||||||
|
mapToCharts(data.value, mode.value);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,60 +124,76 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(() => {
|
||||||
|
mapToCharts(data.value, mode.value);
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
// --- mapping charts: usa SIEMPRE el rango ---
|
// --- mapping charts: usa SIEMPRE el rango ---
|
||||||
function mapToCharts(api, currentMode) {
|
function mapToCharts(api, currentMode) {
|
||||||
const list = Array.isArray(api?.movements_by_procedure)
|
const proceduresList = Array.isArray(api?.movements_by_procedure)
|
||||||
? 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 labels = filteredProcedures.map((proc) =>
|
||||||
const pieValues = list.map((it) => (it.opened || 0) + (it.closed || 0));
|
(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 = {
|
pieChartData.value = {
|
||||||
labels,
|
labels: labels.length > 0 ? labels : ["Sin datos"],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Recaudados",
|
label: "Distribución de Trámites por Procedimiento",
|
||||||
data: pieValues,
|
data: pieValues.length > 0 ? pieValues : [0],
|
||||||
backgroundColor: labels.map(
|
backgroundColor: labels.length > 0
|
||||||
(_, i) => `hsl(${(i * 360) / Math.max(1, labels.length)}, 70%, 50%)`
|
? 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 barLabel = "";
|
||||||
let barValues = [];
|
let barValues = [];
|
||||||
|
|
||||||
if (currentMode === "día") {
|
if (currentMode === "día") {
|
||||||
barLabel = "Solicitados Hoy";
|
barLabel = "Trámites Abiertos Hoy";
|
||||||
barValues = list.map(
|
barValues = filteredProcedures.map((proc) => proc.opened_today || 0);
|
||||||
(it) => it.procedures_opened_today ?? it.opened_today ?? 0
|
|
||||||
);
|
|
||||||
} else if (currentMode === "semana") {
|
} else if (currentMode === "semana") {
|
||||||
barLabel = "Solicitados Semana";
|
barLabel = "Trámites de la Semana";
|
||||||
barValues = list.map((it) => it.total ?? 0);
|
barValues = filteredProcedures.map((proc) => proc.week_total || (proc.opened || 0));
|
||||||
} else {
|
} else {
|
||||||
barLabel = "Solicitados en Rango";
|
barLabel = "Trámites en el Rango Seleccionado";
|
||||||
barValues = list.map((it) => {
|
barValues = filteredProcedures.map((proc) => {
|
||||||
if (typeof it.range_total === "number") return it.range_total;
|
// Priorizar datos específicos del rango si están disponibles
|
||||||
return (it.opened || 0) + (it.closed || 0);
|
if (typeof proc.range_total === "number") return proc.range_total;
|
||||||
|
return (proc.opened || 0) + (proc.closed || 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
barChartData.value = {
|
barChartData.value = {
|
||||||
labels,
|
labels: labels.length > 0 ? labels : ["Sin datos"],
|
||||||
datasets: [barChartOptions.makeDataset(barLabel, barValues)],
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -243,14 +264,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
v-if="loading"
|
v-if="loading"
|
||||||
class="fixed inset-0 bg-white/95 backdrop-blur-sm flex items-center justify-center z-50 transition-all duration-300"
|
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>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="min-h-screen bg-gray-50 py-8">
|
<div class="min-h-screen bg-gray-50 py-8">
|
||||||
@ -264,21 +277,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
</h1>
|
</h1>
|
||||||
<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">
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -487,7 +485,7 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200 uppercase">
|
||||||
<tr
|
<tr
|
||||||
v-for="procedure in data.movements_by_procedure || []"
|
v-for="procedure in data.movements_by_procedure || []"
|
||||||
:key="procedure.id"
|
:key="procedure.id"
|
||||||
@ -554,7 +552,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
|
|
||||||
<!-- Bars -->
|
<!-- Bars -->
|
||||||
<div
|
<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"
|
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 -->
|
<!-- DateRange mejorado -->
|
||||||
@ -565,7 +562,6 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
|
|
||||||
<!-- Pie -->
|
<!-- Pie -->
|
||||||
<div
|
<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"
|
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" />
|
<Pie :chartData="pieChartData" :chartOptions="pieChartOptions" />
|
||||||
@ -579,6 +575,7 @@ const togglePie = () => (showPie.value = !showPie.value);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user