Se agregó el endpoint de Obras
This commit is contained in:
parent
8792c5b283
commit
df8a8f952d
@ -9,17 +9,38 @@ class ObrasController extends BaseController
|
||||
{
|
||||
public function Obras(Request $request)
|
||||
{
|
||||
$query = $request->only(['start', 'end', 'type']);
|
||||
$query = array_merge(['type' => 'api'], $query);
|
||||
$query = $request->only(['start', 'end', 'type', 'action']);
|
||||
$query = array_merge($query, ['action' => 'index', 'type' => 'api'], $query);
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php?action=getCounters', $query);
|
||||
$response = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php', $query);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['error' => 'External service error'], $response->status());
|
||||
}
|
||||
|
||||
return response()->json($response->json(), $response->status())
|
||||
$mainData = $response->json();
|
||||
|
||||
$counterQuery = array_merge($query, ['action' => 'getCounters']);
|
||||
$countersResponse = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php', $counterQuery);
|
||||
|
||||
$countersData = [];
|
||||
if ($countersResponse->successful()) {
|
||||
$countersData = $countersResponse->json();
|
||||
}
|
||||
|
||||
$usersQuery = array_merge($query, ['action' => 'actionsOfSupervisors']);
|
||||
$userResponse = Http::timeout(5)->get('https://obras-information.comalcalco.gob.mx/api/controller.php', $usersQuery);
|
||||
|
||||
$usersData = [];
|
||||
if($userResponse->successful()) {
|
||||
$usersData = $userResponse->json();
|
||||
}
|
||||
|
||||
// Combinar ambas respuestas
|
||||
$combinedData = array_merge($mainData, ['counters' => $countersData, 'users' => $usersData]);
|
||||
|
||||
return response()->json($combinedData, $response->status())
|
||||
->header('Content-Type', $response->header('Content-Type', 'application/json'));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Proxy error: '.$e->getMessage());
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@ -5,6 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@googlemaps/js-api-loader": "^1.16.10",
|
||||
"@inertiajs/vue3": "^1.0.0-beta.2",
|
||||
"@soketi/soketi": "^1.6.0",
|
||||
"@vueuse/core": "^9.6.0",
|
||||
@ -408,6 +409,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@googlemaps/js-api-loader": {
|
||||
"version": "1.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.10.tgz",
|
||||
"integrity": "sha512-c2erv2k7P2ilYzMmtYcMgAR21AULosQuUHJbStnrvRk2dG93k5cqptDrh9A8p+ZNlyhiqEOgHW7N9PAizdUM7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@inertiajs/core": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.0.14.tgz",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"vue": "^3.2.31"
|
||||
},
|
||||
"dependencies": {
|
||||
"@googlemaps/js-api-loader": "^1.16.10",
|
||||
"@inertiajs/vue3": "^1.0.0-beta.2",
|
||||
"@soketi/soketi": "^1.6.0",
|
||||
"@vueuse/core": "^9.6.0",
|
||||
|
||||
367
resources/js/Components/Dashboard/Maps.vue
Normal file
367
resources/js/Components/Dashboard/Maps.vue
Normal file
@ -0,0 +1,367 @@
|
||||
<template>
|
||||
<div class="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">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Mapa de Obras</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Ubicación geográfica de proyectos en Comalcalco, Tabasco</p>
|
||||
</div>
|
||||
|
||||
<!-- Filtros por estado -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="sin-avances"
|
||||
v-model="filters.sin_avances"
|
||||
@change="updateMarkers"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-red-600 focus:ring-red-500"
|
||||
/>
|
||||
<label for="sin-avances" class="flex items-center text-sm text-gray-700">
|
||||
<div class="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
|
||||
Sin avances ({{ counters.sin_avances.count }})
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="abiertas"
|
||||
v-model="filters.abiertas"
|
||||
@change="updateMarkers"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-yellow-600 focus:ring-yellow-500"
|
||||
/>
|
||||
<label for="abiertas" class="flex items-center text-sm text-gray-700">
|
||||
<div class="w-3 h-3 bg-yellow-500 rounded-full mr-2"></div>
|
||||
Abiertas ({{ counters.abiertas.count }})
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="finalizadas"
|
||||
v-model="filters.finalizadas"
|
||||
@change="updateMarkers"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
<label for="finalizadas" class="flex items-center text-sm text-gray-700">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||
Finalizadas ({{ counters.finalizadas.count }})
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mapa -->
|
||||
<div ref="mapContainer" class="w-full h-96 relative">
|
||||
<div v-if="loading" class="absolute inset-0 bg-gray-100 flex items-center justify-center z-10">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p class="mt-2 text-sm text-gray-600">Cargando mapa...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="absolute inset-0 bg-gray-100 flex items-center justify-center z-10">
|
||||
<div class="text-center">
|
||||
<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 el mapa</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ error }}</p>
|
||||
<button @click="initMap" class="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors">
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leyenda -->
|
||||
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
||||
<div class="flex items-center justify-between text-xs text-gray-600">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex items-center">
|
||||
<div class="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
|
||||
Sin avances
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<div class="w-2 h-2 bg-yellow-500 rounded-full mr-1"></div>
|
||||
En proceso
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
|
||||
Finalizadas
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { Loader } from '@googlemaps/js-api-loader';
|
||||
|
||||
const props = defineProps({
|
||||
counters: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const mapContainer = ref(null);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
const map = ref(null);
|
||||
const markers = ref([]);
|
||||
const geocoder = ref(null);
|
||||
|
||||
// Filtros activos
|
||||
const filters = reactive({
|
||||
sin_avances: true,
|
||||
abiertas: true,
|
||||
finalizadas: true
|
||||
});
|
||||
|
||||
// Configuración de colores por estado
|
||||
const markerColors = {
|
||||
sin_avances: '#EF4444', // Rojo
|
||||
abiertas: '#F59E0B', // Amarillo
|
||||
finalizadas: '#10B981' // Verde
|
||||
};
|
||||
|
||||
// Contador de marcadores visibles
|
||||
const visibleMarkersCount = computed(() => {
|
||||
return markers.value.filter(marker => marker.visible).length;
|
||||
});
|
||||
|
||||
// Coordenadas del centro de Comalcalco, Tabasco
|
||||
const COMALCALCO_CENTER = {
|
||||
lat: 18.2669,
|
||||
lng: -93.2072
|
||||
};
|
||||
|
||||
// Límites aproximados de Comalcalco
|
||||
const COMALCALCO_BOUNDS = {
|
||||
north: 18.35,
|
||||
south: 18.18,
|
||||
east: -93.10,
|
||||
west: -93.30
|
||||
};
|
||||
|
||||
const initMap = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const loader = new Loader({
|
||||
apiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
|
||||
version: 'weekly',
|
||||
libraries: ['places']
|
||||
});
|
||||
|
||||
// Cargar Google Maps
|
||||
const google = await loader.load();
|
||||
|
||||
// Verificar que google.maps esté disponible
|
||||
if (!google.maps) {
|
||||
throw new Error('Google Maps no se cargó correctamente');
|
||||
}
|
||||
|
||||
// Inicializar el mapa centrado en Comalcalco
|
||||
map.value = new google.maps.Map(mapContainer.value, {
|
||||
center: COMALCALCO_CENTER,
|
||||
zoom: 12,
|
||||
restriction: {
|
||||
latLngBounds: COMALCALCO_BOUNDS,
|
||||
strictBounds: false
|
||||
},
|
||||
mapTypeControl: true,
|
||||
streetViewControl: true,
|
||||
fullscreenControl: true,
|
||||
zoomControl: true
|
||||
});
|
||||
|
||||
// Inicializar el geocodificador
|
||||
geocoder.value = new google.maps.Geocoder();
|
||||
|
||||
// Cargar marcadores de obras
|
||||
await loadMarkers();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading Google Maps:', err);
|
||||
error.value = err.message || 'Error al cargar Google Maps';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMarkers = async () => {
|
||||
if (!map.value || !geocoder.value) return;
|
||||
|
||||
// Limpiar marcadores existentes
|
||||
clearMarkers();
|
||||
|
||||
const allObras = [
|
||||
...props.counters.sin_avances.detalles.map(obra => ({ ...obra, estado: 'sin_avances' })),
|
||||
...props.counters.abiertas.detalles.map(obra => ({ ...obra, estado: 'abiertas' })),
|
||||
...props.counters.finalizadas.detalles.map(obra => ({ ...obra, estado: 'finalizadas' }))
|
||||
];
|
||||
|
||||
for (const obra of allObras) {
|
||||
try {
|
||||
await createMarkerForObra(obra);
|
||||
} catch (err) {
|
||||
console.warn(`Error creating marker for obra ${obra.num_proyecto}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
updateMarkers();
|
||||
};
|
||||
|
||||
const createMarkerForObra = async (obra) => {
|
||||
if (!obra.direccion_obra || !geocoder.value) return;
|
||||
|
||||
const address = `${obra.direccion_obra}, Comalcalco, Tabasco, México`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
geocoder.value.geocode({ address }, (results, status) => {
|
||||
if (status === 'OK' && results[0]) {
|
||||
const position = results[0].geometry.location;
|
||||
|
||||
// Verificar que la ubicación esté dentro de los límites de Comalcalco
|
||||
const lat = position.lat();
|
||||
const lng = position.lng();
|
||||
|
||||
if (lat < COMALCALCO_BOUNDS.south || lat > COMALCALCO_BOUNDS.north ||
|
||||
lng < COMALCALCO_BOUNDS.west || lng > COMALCALCO_BOUNDS.east) {
|
||||
console.warn(`Obra ${obra.num_proyecto} fuera de los límites de Comalcalco`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = new google.maps.Marker({
|
||||
position,
|
||||
map: map.value,
|
||||
title: `${obra.num_proyecto} - ${obra.direccion_obra}`,
|
||||
icon: createCustomMarker(obra.estado),
|
||||
visible: filters[obra.estado]
|
||||
});
|
||||
|
||||
// Info window con detalles de la obra
|
||||
const infoWindow = new google.maps.InfoWindow({
|
||||
content: createInfoWindowContent(obra)
|
||||
});
|
||||
|
||||
marker.addListener('click', () => {
|
||||
// Cerrar otras ventanas abiertas
|
||||
markers.value.forEach(m => m.infoWindow?.close());
|
||||
infoWindow.open(map.value, marker);
|
||||
});
|
||||
|
||||
markers.value.push({
|
||||
marker,
|
||||
infoWindow,
|
||||
estado: obra.estado,
|
||||
obra,
|
||||
visible: filters[obra.estado]
|
||||
});
|
||||
|
||||
resolve();
|
||||
} else {
|
||||
console.warn(`Geocoding failed for ${address}: ${status}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createCustomMarker = (estado) => {
|
||||
const color = markerColors[estado];
|
||||
|
||||
return {
|
||||
url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
|
||||
<svg width="24" height="36" viewBox="0 0 24 36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.373 0 0 5.373 0 12c0 9 12 24 12 24s12-15 12-24c0-6.627-5.373-12-12-12z" fill="${color}"/>
|
||||
<circle cx="12" cy="12" r="6" fill="white"/>
|
||||
<circle cx="12" cy="12" r="3" fill="${color}"/>
|
||||
</svg>
|
||||
`)}`,
|
||||
scaledSize: new google.maps.Size(24, 36),
|
||||
anchor: new google.maps.Point(12, 36)
|
||||
};
|
||||
};
|
||||
|
||||
const createInfoWindowContent = (obra) => {
|
||||
const estadoConfig = {
|
||||
sin_avances: { color: 'bg-red-100 text-red-800', label: 'Sin avances' },
|
||||
abiertas: { color: 'bg-yellow-100 text-yellow-800', label: 'En proceso' },
|
||||
finalizadas: { color: 'bg-green-100 text-green-800', label: 'Finalizada' }
|
||||
};
|
||||
|
||||
const config = estadoConfig[obra.estado];
|
||||
const cumplimiento = obra.cumplimiento_total ? `${Number(obra.cumplimiento_total).toFixed(1)}%` : 'N/A';
|
||||
|
||||
return `
|
||||
<div class="p-4 max-w-sm">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-bold text-gray-900">${obra.num_proyecto || 'N/A'}</h3>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.color}">
|
||||
${config.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<p class="font-medium text-gray-700">Dirección:</p>
|
||||
<p class="text-gray-600">${obra.direccion_obra || 'N/A'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-gray-700">Cumplimiento:</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
||||
<div class="h-2 rounded-full ${obra.estado === 'finalizadas' ? 'bg-green-500' : obra.estado === 'abiertas' ? 'bg-yellow-500' : 'bg-red-500'}"
|
||||
style="width: ${Number(obra.cumplimiento_total) || 0}%"></div>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-gray-700">${cumplimiento}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const updateMarkers = () => {
|
||||
markers.value.forEach(({ marker, estado }) => {
|
||||
const shouldShow = filters[estado];
|
||||
marker.setVisible(shouldShow);
|
||||
|
||||
// Actualizar el estado visible del marcador
|
||||
const markerIndex = markers.value.findIndex(m => m.marker === marker);
|
||||
if (markerIndex !== -1) {
|
||||
markers.value[markerIndex].visible = shouldShow;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearMarkers = () => {
|
||||
markers.value.forEach(({ marker, infoWindow }) => {
|
||||
infoWindow?.close();
|
||||
marker.setMap(null);
|
||||
});
|
||||
markers.value = [];
|
||||
};
|
||||
|
||||
// Recargar marcadores cuando cambien los contadores
|
||||
watch(() => props.counters, () => {
|
||||
if (map.value) {
|
||||
loadMarkers();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
initMap();
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@ -20,7 +20,7 @@
|
||||
return $request->user();
|
||||
});
|
||||
|
||||
Route::middleware(['throttle:60,1'])->get('/reporte-especial', [TramiteController::class, 'tramiteEspecial']);
|
||||
|
||||
Route::middleware(['throttle:60,1'])->get('/reporte-obras', [ObrasController::class, 'Obras']);
|
||||
|
||||
Route::middleware('api')->group(function () {
|
||||
Route::get('/reporte-obras', [ObrasController::class, 'Obras'])->middleware(['throttle:60,1']);
|
||||
Route::get('/reporte-especial', [TramiteController::class, 'tramiteEspecial'])->middleware(['throttle:60,1']);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user