Juan Felipe Zapata Moreno 885e5183a8 Correción Obras
2025-08-26 17:07:40 -06:00

1489 lines
57 KiB
Vue

<script setup>
import { ref, reactive, onMounted } from "vue";
import ObrasMap from "@/Components/Dashboard/Maps.vue";
import Footer from "@/Components/Dashboard/Footer.vue";
import axios from "axios";
import AppLayout from "@/Layouts/AppLayout.vue";
const loading = ref(false);
// Planeaciones
const planeaciones = ref([]);
const planeacionesCount = ref(0);
const planeacionesError = ref(false);
const planeacionesErrorMessage = ref("");
// Estimaciones
const estimaciones = ref([]);
const estimacionesGrouped = reactive({});
const estimacionesGroupedKeys = ref([]);
const estimacionesCount = ref(0);
const estimacionesError = ref(false);
const estimacionesErrorMessage = ref("");
//Contadores
const counters = reactive({
sin_avances: { count: 0, detalles: [] },
finalizadas: { count: 0, detalles: [] },
abiertas: { count: 0, detalles: [] },
});
//Users
const users = ref([]);
const usersStats = reactive({
total_supervisores: 0,
supervisores_con_acciones: 0,
ultima_accion_estimacion: 0,
ultima_action_evidencia: 0,
sin_acciones: 0,
});
// Últimas actualizaciones
const lastPlaneacion = ref(null);
const lastEstimacion = ref(null);
// UI
const activeTab = ref("proyectos");
const expandedProjects = ref(new Set());
let cancelTokenSource = null;
/**
* Carga datos (sin manejo de rango de fechas)
*/
const loadDashboardData = async () => {
loading.value = true;
planeacionesError.value = estimacionesError.value = false;
planeacionesErrorMessage.value = estimacionesErrorMessage.value = "";
// cancelar petición previa
if (cancelTokenSource) {
try {
cancelTokenSource.cancel("cancel");
} catch (e) {}
}
cancelTokenSource = axios.CancelToken.source();
try {
const res = await axios.get("/api/reporte-obras", {
cancelToken: cancelTokenSource.token,
headers: { "X-Requested-With": "XMLHttpRequest" },
});
const data = res.data;
if (!data || !data.success) {
planeacionesError.value = estimacionesError.value = true;
const msg = data?.error || "Error desconocido";
planeacionesErrorMessage.value = estimacionesErrorMessage.value = msg;
return;
}
// Planeaciones: normalizar campos
if (data.planeacions) {
planeacionesCount.value = data.planeacions.count || 0;
const rawPlaneaciones = Array.isArray(data.planeacions.data)
? data.planeacions.data
: [];
planeaciones.value = rawPlaneaciones.map((item) => ({
id: item.ID ?? item.id ?? null,
num_proyecto: item.num_proyecto ?? item.numProyecto ?? null,
proyecto: item.proyecto ?? null,
cumplimiento_total:
item.cumplimiento_total ?? item.cumplimiento ?? null,
}));
const lastP =
data.planeacions.lastUpdate ?? data.planeacions.last_update ?? 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
if (data.estimacions) {
estimacionesCount.value = data.estimacions.count || 0;
const rawEstimaciones = Array.isArray(data.estimacions.data)
? data.estimacions.data
: [];
estimaciones.value = rawEstimaciones.map((item) => ({
id: item.id ?? item.ID ?? null,
num_estimacion: item.num_estimacion ?? item.numEstimacion ?? null,
fecha_estimacion: item.fecha_estimacion ?? item.fechaEstimacion ?? null,
periodo_estimacion: item.periodo_estimacion ?? null,
monto_estimacion: item.monto_estimacion ?? item.montoEstimacion ?? "0",
monto_acumulado: item.monto_acumulado ?? item.montoAcumulado ?? "0",
status: item.status ?? null,
created_at: item.created_at ?? item.createdAt ?? null,
proyecto_id: String(
item.proyecto_id ?? item.proyectoId ?? "sin_proyecto"
),
num_proyecto: item.num_proyecto ?? item.numProyecto ?? null,
proyecto: item.proyecto ?? null,
}));
// Agrupar
Object.keys(estimacionesGrouped).forEach(
(k) => delete estimacionesGrouped[k]
);
estimaciones.value.forEach((est) => {
const proyectoId = est.proyecto_id || "sin_proyecto";
if (!estimacionesGrouped[proyectoId]) {
estimacionesGrouped[proyectoId] = {
proyecto: {
num_proyecto: est.num_proyecto,
proyecto: est.proyecto,
},
estimaciones: [],
};
}
estimacionesGrouped[proyectoId].estimaciones.push(est);
});
estimacionesGroupedKeys.value = Object.keys(estimacionesGrouped);
// inicializar proyectos expandidos (todos abiertos por defecto)
expandedProjects.value = new Set(estimacionesGroupedKeys.value);
const lastE =
data.estimacions.lastUpdate ?? data.estimacions.last_update ?? 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
if (data.counters) {
// Actualizar contadores sin avances
if (data.counters.sin_avances) {
counters.sin_avances.count = data.counters.sin_avances.count || 0;
counters.sin_avances.detalles = Array.isArray(
data.counters.sin_avances.detalles
)
? data.counters.sin_avances.detalles.map((item) => ({
id: item.ID ?? item.id ?? null,
num_proyecto: item.num_proyecto ?? null,
direccion_obra: item.direccion_obra ?? null,
cumplimiento_total: item.cumplimiento_total ?? null,
}))
: [];
}
// Agregar contadores finalizadas
if (data.counters.finalizadas) {
counters.finalizadas = {
count: data.counters.finalizadas.count || 0,
detalles: Array.isArray(data.counters.finalizadas.detalles)
? data.counters.finalizadas.detalles.map((item) => ({
id: item.ID ?? item.id ?? null,
num_proyecto: item.num_proyecto ?? null,
direccion_obra: item.direccion_obra ?? null,
cumplimiento_total: item.cumplimiento_total ?? null,
}))
: [],
};
}
// Agregar contadores abiertas
if (data.counters.abiertas) {
counters.abiertas = {
count: data.counters.abiertas.count || 0,
detalles: Array.isArray(data.counters.abiertas.detalles)
? data.counters.abiertas.detalles.map((item) => ({
id: item.ID ?? item.id ?? null,
num_proyecto: item.num_proyecto ?? null,
direccion_obra: item.direccion_obra ?? null,
cumplimiento_total: item.cumplimiento_total ?? null,
}))
: [],
};
}
}
if (data.users && data.users.success) {
if (Array.isArray(data.users.supervisores)) {
users.value = data.users.supervisores.map((user) => ({
id: user.supervisor_id ?? null,
nombre: user.supervisor_nombre ?? "Sin nombre",
correo: user.supervisor_correo ?? "Sin correo",
ultima_accion: user.supervisor_ultima_accion ?? "Sin acción reciente",
fecha_accion: user.fecha_accion ?? null,
detalle_accion: user.detalle_accion ?? null,
proyecto_info: user.proyecto_info ?? null,
}));
}
if (data.users.estadisticas) {
usersStats.total_supervisores =
data.users.estadisticas.total_supervisores || 0;
usersStats.supervisores_con_acciones =
data.users.estadisticas.supervisores_con_acciones || 0;
usersStats.ultima_accion_estimacion =
data.users.estadisticas.ultima_accion_estimacion || 0;
usersStats.ultima_accion_evidencia =
data.users.estadisticas.ultima_accion_evidencia || 0;
usersStats.sin_acciones = data.users.estadisticas.sin_acciones || 0;
}
}
} catch (err) {
console.error(err);
if (axios.isCancel && axios.isCancel(err)) return;
planeacionesError.value = estimacionesError.value = true;
planeacionesErrorMessage.value = estimacionesErrorMessage.value =
"Error de conexión con el servidor";
} finally {
loading.value = false;
}
};
const toggleProyecto = (id) => {
const key = String(id);
if (expandedProjects.value.has(key)) expandedProjects.value.delete(key);
else expandedProjects.value.add(key);
expandedProjects.value = new Set(Array.from(expandedProjects.value));
};
const selectTab = (tab) => {
activeTab.value = tab;
};
const badgeClass = (tab) => {
if (tab === "proyectos")
return activeTab.value === "proyectos"
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800";
return activeTab.value === "estimaciones"
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800";
};
const formatMoney = (amount) => {
return new Intl.NumberFormat("es-MX", {
style: "currency",
currency: "MXN",
minimumFractionDigits: 2,
}).format(Number(amount) || 0);
};
const formatDate = (s) => {
if (!s) return "--";
return new Date(s).toLocaleDateString("es-ES", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
const formatDateTime = (s) => {
if (!s) return "--";
return new Date(s).toLocaleString("es-ES", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatPercent = (v) => (Number(v) || 0).toFixed(1) + "%";
const percentColor = (v) =>
Number(v) >= 80 ? "#16a34a" : Number(v) >= 50 ? "#f59e0b" : "#ef4444";
const percentColorClass = (v) =>
Number(v) >= 80
? "text-green-600"
: Number(v) >= 50
? "text-yellow-600"
: "text-red-600";
onMounted(() => {
loadDashboardData();
activeTab.value = "proyectos";
});
</script>
<template>
<AppLayout>
<div class="min-h-screen bg-gray-50">
<!-- Header -->
<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, 0.1) 35px,
rgba(255, 255, 255, 0.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">
Información de Obras
</h1>
<p class="text-blue-100 text-sm">
Sistema de seguimiento de obras
</p>
</div>
</div>
</div>
</div>
</div>
</header>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Stats Cards Grid -->
<div class="mb-8 grid grid-cols-1 md:grid-cols-2 gap-6">
<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">
Total de Proyectos
</h2>
<p class="text-blue-100 text-sm mt-1">
Proyectos en seguimiento
</p>
</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">{{
planeacionesCount
}}</span>
</div>
</div>
</div>
<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">
Total de Estimaciones
</h2>
<p class="text-green-100 text-sm mt-1">
Estimaciones registradas
</p>
</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">{{
estimacionesCount
}}</span>
</div>
</div>
</div>
</div>
<!-- Last Updates Grid -->
<div class="mb-8 grid grid-cols-1 xl:grid-cols-2 gap-6">
<!-- Last Planeación Update -->
<div>
<div
class="bg-white rounded-lg shadow-lg overflow-hidden border-l-4 border-blue-500"
>
<div class="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50">
<div class="flex items-center space-x-2">
<svg
class="w-5 h-5 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<h3 class="text-lg font-semibold text-gray-900">
Último Avance de Proyecto
</h3>
</div>
<p class="text-sm text-gray-600 mt-1">
Registro más reciente de progreso en obra
</p>
</div>
<div v-if="!lastPlaneacion" class="px-6 py-8 text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h4 class="mt-2 text-sm font-medium text-gray-900">
Sin actualizaciones recientes
</h4>
<p class="mt-1 text-sm text-gray-500">
No se encontraron registros de avances
</p>
</div>
<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">
{{ lastPlaneacion.name }}
</p>
</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
class="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center"
>
<svg
class="w-4 h-4 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900">Partida</p>
<p class="text-sm text-gray-600">
{{ lastPlaneacion.desc_partida }}
</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<div
class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center"
>
<svg
class="w-4 h-4 text-orange-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900">Concepto</p>
<p class="text-sm text-gray-600">
<span
class="font-mono text-xs bg-gray-100 px-2 py-1 rounded"
>{{ lastPlaneacion.clave }}</span
>
<span class="ml-2">{{
lastPlaneacion.desc_concepto
}}</span>
</p>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-900"
>Porcentaje de Cumplimiento</span
>
<span
class="text-lg font-bold"
:class="percentColorClass(lastPlaneacion.cumplimiento)"
>{{ formatPercent(lastPlaneacion.cumplimiento) }}</span
>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="h-2 rounded-full transition-all duration-300"
:style="{
width:
(Number(lastPlaneacion.cumplimiento) || 0) + '%',
backgroundColor: percentColor(
lastPlaneacion.cumplimiento
),
}"
></div>
</div>
</div>
<div class="bg-blue-50 rounded-lg p-4">
<div class="flex items-center space-x-2 mb-2">
<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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-3.582 8-8 8a8.959 8.959 0 01-4.906-1.459L3 21l1.459-5.094A8.959 8.959 0 013 12c0-4.418 3.582-8 8-8s8 3.582 8 8z"
/>
</svg>
<span class="text-sm font-medium text-gray-900"
>Descripción del Avance</span
>
</div>
<p class="text-sm text-gray-600 leading-relaxed">
{{ lastPlaneacion.desc_evidencia }}
</p>
</div>
<div
class="flex items-center space-x-4 text-xs text-gray-500"
>
<div class="flex items-center space-x-1">
<svg
class="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span
>Fecha:
{{ formatDate(lastPlaneacion.fecha_evidencia) }}</span
>
</div>
<div class="flex items-center space-x-1">
<svg
class="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span
>Registrado:
{{ formatDateTime(lastPlaneacion.created_at) }}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Last Estimación Update -->
<div>
<div
class="bg-white rounded-lg shadow-lg overflow-hidden border-l-4 border-green-500"
>
<div
class="px-6 py-4 bg-gradient-to-r from-green-50 to-emerald-50"
>
<div class="flex items-center space-x-2">
<svg
class="w-5 h-5 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3-.895-3-2s1.343-2 3-2 3 .895 3 2-1.343 2-3 2zm0 0v.01M12 8v8a2 2 0 002 2h2a2 2 0 002-2V8h-6z"
/>
</svg>
<h3 class="text-lg font-semibold text-gray-900">
Última Estimación Registrada
</h3>
</div>
<p class="text-sm text-gray-600 mt-1">
Estimación más reciente del proyecto
</p>
</div>
<div v-if="!lastEstimacion" class="px-6 py-8 text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3-.895-3-2s1.343-2 3-2 3 .895 3 2-1.343 2-3 2zm0 0v.01M12 8v8a2 2 0 002 2h2a2 2 0 002-2V8h-6z"
/>
</svg>
<h4 class="mt-2 text-sm font-medium text-gray-900">
Sin estimaciones recientes
</h4>
<p class="mt-1 text-sm text-gray-500">
No se encontraron registros de estimaciones
</p>
</div>
<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
class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center"
>
<svg
class="w-4 h-4 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900">
Número de Estimación
</p>
<p class="text-sm text-gray-600 font-mono">
{{ lastEstimacion.num_estimacion }}
</p>
</div>
</div>
<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="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">
{{
lastEstimacion.num_proyecto
? `${lastEstimacion.num_proyecto} - ${lastEstimacion.proyecto}`
: lastEstimacion.proyecto
}}
</p>
</div>
</div>
<div class="bg-green-50 rounded-lg p-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p class="text-sm font-medium text-gray-900">
Monto Estimado
</p>
<p class="text-lg font-bold text-green-600">
{{ formatMoney(lastEstimacion.monto_estimacion) }}
</p>
</div>
<div>
<p class="text-sm font-medium text-gray-900">
Monto Acumulado
</p>
<p class="text-lg font-bold text-green-700">
{{ formatMoney(lastEstimacion.monto_acumulado) }}
</p>
</div>
</div>
</div>
<div
class="flex items-center space-x-4 text-xs text-gray-500"
>
<div class="flex items-center space-x-1">
<svg
class="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span
>Periodo: {{ lastEstimacion.periodo_estimacion }}</span
>
</div>
<div class="flex items-center space-x-1">
<svg
class="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span
>Registrado:
{{ formatDateTime(lastEstimacion.created_at) }}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabbed Interface -->
<div class="bg-white rounded-lg shadow-lg overflow-hidden">
<div class="border-b border-gray-200">
<nav class="flex space-x-8 px-6" aria-label="Tabs">
<button
@click="selectTab('proyectos')"
class="tab-button py-4 px-1 text-sm font-medium whitespace-nowrap"
>
<svg
class="w-5 h-5 inline-block mr-2 -mt-0.5"
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>
Lista de Proyectos
<span
:class="badgeClass('proyectos')"
class="ml-2 px-2.5 py-0.5 rounded-full text-xs font-medium"
>{{ planeacionesCount }}</span
>
</button>
<button
@click="selectTab('estimaciones')"
class="tab-button py-4 px-1 text-sm font-medium whitespace-nowrap"
>
<svg
class="w-5 h-5 inline-block mr-2 -mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3-.895-3-2s1.343-2 3-2 3 .895 3 2-1.343 2-3 2zm0 0v.01M12 8v8a2 2 0 002 2h2a2 2 0 002-2V8h-6z"
/>
</svg>
Lista de Estimaciones
<span
:class="badgeClass('estimaciones')"
class="ml-2 px-2.5 py-0.5 rounded-full text-xs font-medium"
>{{ estimacionesCount }}</span
>
</button>
</nav>
</div>
<!-- Proyectos Tab -->
<div v-show="activeTab === 'proyectos'" class="tab-content">
<div
class="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200"
>
<h3 class="text-lg font-medium text-gray-900">
Gestión de Proyectos
</h3>
<p class="text-sm text-gray-600 mt-1">
Seguimiento detallado del progreso de cada proyecto
</p>
</div>
<div v-if="planeacionesError" class="text-center py-12">
<svg
class="mx-auto h-12 w-12 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">
Error al cargar proyectos
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ planeacionesErrorMessage }}
</p>
<button
@click="loadDashboardData"
class="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
>
Reintentar
</button>
</div>
<div
v-else-if="!planeaciones || !planeaciones.length"
class="text-center py-12"
>
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">
No hay proyectos
</h3>
<p class="mt-1 text-sm text-gray-500">
Comenzar agregando un nuevo proyecto.
</p>
</div>
<div v-else class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32"
>
Número
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Proyecto
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48"
>
Progreso
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr
v-for="(p, idx) in planeaciones"
:key="p.id ?? idx"
class="hover:bg-gray-50 transition-colors"
>
<td
class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-mono"
>
{{ p.num_proyecto || "--" }}
</td>
<td class="px-6 py-4 text-sm text-gray-900">
<div class="max-w-md truncate" :title="p.proyecto">
{{ p.proyecto || "--" }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="flex items-center space-x-3">
<div
class="flex-1 bg-gray-200 rounded-full h-2 min-w-[100px]"
>
<div
class="h-2 rounded-full bg-blue-600 transition-all duration-300"
:style="{
width: (Number(p.cumplimiento_total) || 0) + '%',
}"
></div>
</div>
<span
class="text-sm font-medium text-gray-900 min-w-[60px] text-right"
>{{
(Number(p.cumplimiento_total) || 0).toFixed(1)
}}%</span
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Estimaciones Tab -->
<div v-show="activeTab === 'estimaciones'" class="tab-content">
<div
class="px-6 py-4 bg-gradient-to-r from-green-50 to-emerald-50 border-b border-gray-200"
>
<h3 class="text-lg font-medium text-gray-900">
Gestión de Estimaciones
</h3>
<p class="text-sm text-gray-600 mt-1">
Estimaciones detalladas por proyecto, concepto y partida
</p>
</div>
<div v-if="estimacionesError" class="text-center py-12">
<svg
class="mx-auto h-12 w-12 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">
Error al cargar estimaciones
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ estimacionesErrorMessage }}
</p>
<button
@click="loadDashboardData"
class="mt-4 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 transition-colors"
>
Reintentar
</button>
</div>
<div
v-else-if="!estimacionesGroupedKeys.length"
class="text-center py-12"
>
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3-.895-3-2s1.343-2 3-2 3 .895 3 2-1.343 2-3 2zm0 0v.01M12 8v8a2 2 0 002 2h2a2 2 0 002-2V8h-6z"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">
No hay estimaciones
</h3>
<p class="mt-1 text-sm text-gray-500">
Comenzar agregando una nueva estimación.
</p>
</div>
<div v-else class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-16"
></th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Proyecto / Estimación
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32"
>
Estimación
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48"
>
Período
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-40"
>
Monto
</th>
</tr>
</thead>
<tbody class="bg-white">
<template
v-for="proyectoId in estimacionesGroupedKeys"
:key="proyectoId"
>
<tr
class="proyecto-row border-b-2 border-gray-100 hover:bg-blue-50 cursor-pointer"
@click="toggleProyecto(proyectoId)"
>
<td class="px-6 py-4 text-center">
<svg
:class="{
'transform rotate-180': expandedProjects.has(
String(proyectoId)
),
}"
class="w-4 h-4 text-gray-400 transition-transform duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
></path>
</svg>
</td>
<td class="px-6 py-4">
<div class="font-bold text-gray-900">
{{
estimacionesGrouped[proyectoId].proyecto
.num_proyecto || "--"
}}
</div>
<div class="text-sm text-gray-600 mt-1 leading-tight">
{{
estimacionesGrouped[proyectoId].proyecto.proyecto ||
"--"
}}
</div>
</td>
<td class="px-6 py-4 text-center">
<div class="text-sm font-medium text-gray-700">
{{
estimacionesGrouped[proyectoId].estimaciones.length
}}
Estimación{{
estimacionesGrouped[proyectoId].estimaciones
.length > 1
? "es"
: ""
}}
</div>
</td>
<td class="px-6 py-4"></td>
<td class="px-6 py-4"></td>
</tr>
<tr
v-for="(est, idx) in estimacionesGrouped[proyectoId]
.estimaciones"
:key="`${proyectoId}-${idx}`"
v-show="expandedProjects.has(String(proyectoId))"
class="estimacion-row hover:bg-gray-50 border-b border-gray-100"
>
<td class="px-6 py-3"></td>
<td class="px-6 py-3 pl-12">
<div class="text-sm text-gray-700">{{ idx + 1 }}</div>
<div class="text-xs text-gray-500">
{{ formatDate(est.created_at) }}
</div>
</td>
<td class="px-6 py-3 text-center">
<div class="text-sm font-medium">{{ idx + 1 }}</div>
</td>
<td class="px-6 py-3">
<div class="text-sm font-medium text-gray-900">
{{ (est.periodo_estimacion || "--").toUpperCase() }}
</div>
</td>
<td class="px-6 py-3">
<div class="text-sm font-semibold text-gray-900">
{{ formatMoney(est.monto_estimacion) }}
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-12 -mb-10">
<h3 class="text-lg font-medium text-gray-900">
Obras - Últimos 30 Días
</h3>
</div>
<div class="mt-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-lg">Sin avances</h3>
<p class="mt-2 text-gray-600">
Total:
<span class="font-bold text-red-600">{{
counters.sin_avances.count
}}</span>
</p>
</div>
<div class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-lg">Obras abiertas</h3>
<p class="mt-2 text-gray-600">
Total:
<span class="font-bold text-red-600">{{
counters.abiertas.count
}}</span>
</p>
</div>
<div class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-lg">Finalizadas</h3>
<p class="mt-2 text-gray-600">
Total:
<span class="font-bold text-red-600">{{
counters.finalizadas.count
}}</span>
</p>
</div>
</div>
<div class="mb-8">
<ObrasMap :counters="counters" />
</div>
<div class="mt-8 bg-white rounded-lg shadow-lg overflow-hidden">
<div
class="px-6 py-4 bg-gradient-to-r from-gray-50 to-gray-100 border-b border-gray-200"
>
<h3 class="text-lg font-medium text-gray-900">
Residentes y sus Últimas Acciones - Últimos 30 Días
</h3>
<p class="text-sm text-gray-600 mt-1">
Registro de actividad de residentes en proyectos
</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Residente
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Tipo Acción
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Detalle Acción
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Fecha
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Proyecto
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr
v-for="supervisor in users"
:key="supervisor.id"
class="hover:bg-gray-50 transition-colors"
>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div
class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center"
>
<span class="text-sm font-medium text-gray-700">
{{
supervisor.nombre
.split(" ")
.map((n) => n.charAt(0))
.join("")
.substring(0, 2)
.toUpperCase()
}}
</span>
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{{ supervisor.nombre }}
</div>
<div class="text-sm text-gray-500">
{{ supervisor.correo }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4">
<div
v-if="supervisor.ultima_accion"
class="text-xs text-gray-500 mt-1 max-w-xs truncate"
:title="supervisor.ultima_accion"
>
{{ supervisor.ultima_accion }}
</div>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900">
{{ supervisor.detalle_accion }}
</div>
<div
v-if="supervisor.detalle_accion"
class="text-xs text-gray-500 mt-1 max-w-xs truncate"
:title="supervisor.detalle_accion"
>
{{ supervisor.detalle_accion }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{
supervisor.fecha_accion
? formatDateTime(supervisor.fecha_accion)
: "--"
}}
</td>
<td class="px-6 py-4">
<div
v-if="supervisor.proyecto_info"
class="text-sm text-gray-900 max-w-xs truncate"
:title="supervisor.proyecto_info"
>
{{ supervisor.proyecto_info }}
</div>
<div v-else class="text-sm text-gray-500">--</div>
</td>
</tr>
<!-- Empty state -->
<tr v-if="!users.length">
<td colspan="5" class="px-6 py-8 text-center">
<div class="text-gray-500">
No hay residentes registrados
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<Footer />
</div>
</AppLayout>
</template>