1374 lines
50 KiB
Vue
1374 lines
50 KiB
Vue
<script setup>
|
|
import { ref, reactive, onMounted } from "vue";
|
|
|
|
import ObrasMap from "@/Components/Dashboard/Maps.vue";
|
|
import axios from "axios";
|
|
|
|
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) ? lastP : 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) ? lastE : 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>
|
|
<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>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div
|
|
v-if="loading"
|
|
class="fixed inset-0 bg-white/95 backdrop-blur-sm flex items-center justify-center z-50 transition-all duration-300"
|
|
>
|
|
<div class="text-center">
|
|
<div class="relative">
|
|
<div
|
|
class="animate-spin rounded-full h-16 w-16 border-4 border-blue-200 border-t-blue-600 mx-auto"
|
|
></div>
|
|
</div>
|
|
<p class="mt-6 text-gray-700 font-medium">Cargando datos...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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-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-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 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">Finalizadas</h3>
|
|
<p class="mt-2 text-gray-600">
|
|
Total:
|
|
<span class="font-bold text-red-600">{{
|
|
counters.finalizadas.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>
|
|
<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">
|
|
Supervisores y sus Últimas Acciones - Últimos 30 Días
|
|
</h3>
|
|
<p class="text-sm text-gray-600 mt-1">
|
|
Registro de actividad de supervisores 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"
|
|
>
|
|
Supervisor
|
|
</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 supervisores registrados
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|