2026-05-07 23:31:33 +02:00
|
|
|
<div x-data="{ formFullscreen: $wire.entangle('formFullscreen') }"
|
|
|
|
|
class="flex flex-col lg:flex-row gap-0 h-screen p-1">
|
|
|
|
|
<!-- Columna izquierda: Mapa -->
|
2026-05-27 19:48:29 +02:00
|
|
|
<div x-show="!formFullscreen" x-cloak class="w-full lg:w-2/3 flex-1 relative">
|
2026-05-07 23:31:33 +02:00
|
|
|
<div id="map" style="height: 100%; min-height: 500px; width: 100%;" wire:ignore></div>
|
2026-05-09 21:30:46 +02:00
|
|
|
|
|
|
|
|
<!-- Panel lateral de capas -->
|
|
|
|
|
<div class="absolute top-2 right-2 z-[1000] bg-base-100 rounded-box shadow-xl p-4 w-72 border border-base-300 text-sm max-h-[calc(100vh-4rem)] overflow-y-auto">
|
2026-05-09 23:14:48 +02:00
|
|
|
<h3 class="font-semibold text-base mb-2">{{ __("Fases and layers") }}</h3>
|
2026-05-09 21:30:46 +02:00
|
|
|
<div class="space-y-3">
|
2026-05-07 23:31:33 +02:00
|
|
|
@foreach($phases as $phase)
|
2026-05-09 21:30:46 +02:00
|
|
|
<div class="border rounded-lg p-2 {{ in_array($phase->id, $activeLayers) ? 'bg-base-200' : '' }}">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<input type="checkbox"
|
|
|
|
|
wire:change="toggleLayer({{ $phase->id }})"
|
|
|
|
|
@if(in_array($phase->id, $activeLayers)) checked @endif
|
|
|
|
|
class="toggle toggle-xs toggle-primary">
|
|
|
|
|
<span style="color: {{ $phase->color }};" class="text-lg">⬤</span>
|
|
|
|
|
<span class="flex-1 font-medium text-sm truncate">{{ $phase->name }}</span>
|
|
|
|
|
<span class="badge badge-sm {{ $phase->progress_percent >= 100 ? 'badge-success' : 'badge-ghost' }}">{{ $phase->progress_percent }}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{-- Capas de esta fase --}}
|
|
|
|
|
@if($phase->layers->isNotEmpty())
|
|
|
|
|
<div class="ml-7 mt-1 space-y-1">
|
|
|
|
|
@foreach($phase->layers as $layer)
|
|
|
|
|
<div class="flex items-center gap-1 text-xs text-gray-600">
|
|
|
|
|
<span class="w-2 h-2 rounded-full inline-block" style="background: {{ $layer->color ?? '#ccc' }}"></span>
|
|
|
|
|
<span class="flex-1 truncate">{{ $layer->name }}</span>
|
|
|
|
|
<span class="badge badge-xs">{{ $layer->features_count ?? $layer->features->count() }} elem.</span>
|
|
|
|
|
</div>
|
|
|
|
|
@endforeach
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- Botón para ir a gestión de capas de esta fase --}}
|
|
|
|
|
<div class="mt-1 ml-7">
|
|
|
|
|
<a href="{{ route('layers.manage', [$project, $phase]) }}" class="btn btn-xs btn-outline btn-primary">
|
2026-05-09 23:14:48 +02:00
|
|
|
✏️ {{ __("Manage Layers") }}
|
2026-05-09 21:30:46 +02:00
|
|
|
</a>
|
|
|
|
|
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-outline">
|
2026-05-09 23:14:48 +02:00
|
|
|
📊 {{ __("Progress") }}
|
2026-05-09 21:30:46 +02:00
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-07 23:31:33 +02:00
|
|
|
@endforeach
|
|
|
|
|
</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
|
2026-05-09 22:28:20 +02:00
|
|
|
{{-- Checkbox imágenes en mapa --}}
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
<label class="flex items-center gap-2 text-xs cursor-pointer">
|
|
|
|
|
<input type="checkbox" wire:change="toggleFeatureImages" @if($showFeatureImages) checked @endif class="checkbox checkbox-xs checkbox-primary" />
|
2026-05-09 23:14:48 +02:00
|
|
|
🖼️ {{ __("Show images on map") }}
|
2026-05-09 22:28:20 +02:00
|
|
|
@if($featureImageMarkers)
|
|
|
|
|
<span class="badge badge-xs">{{ count($featureImageMarkers) }}</span>
|
|
|
|
|
@endif
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-09 21:30:46 +02:00
|
|
|
{{-- Botones generales --}}
|
2026-05-09 22:28:20 +02:00
|
|
|
<div class="mt-2 space-y-1">
|
|
|
|
|
<a href="{{ route('projects.media', $project) }}" class="btn btn-sm btn-outline w-full">
|
2026-05-09 23:14:48 +02:00
|
|
|
📁 {{ __("Project files") }}
|
2026-05-09 22:28:20 +02:00
|
|
|
</a>
|
2026-05-09 21:30:46 +02:00
|
|
|
<button wire:click="$dispatch('centerMap')" class="btn btn-sm btn-outline w-full">
|
2026-05-09 23:14:48 +02:00
|
|
|
📍 {{ __("Centered in project") }}
|
2026-05-07 23:31:33 +02:00
|
|
|
</button>
|
2026-05-09 21:30:46 +02:00
|
|
|
<button onclick="getUserLocation()" class="btn btn-sm btn-outline w-full">
|
2026-05-09 23:14:48 +02:00
|
|
|
🧭 {{ __("My location") }}
|
2026-05-07 23:31:33 +02:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-09 23:14:48 +02:00
|
|
|
<!-- Columna derecha: {{ __("Edit") }} de progreso / inspecciones -->
|
2026-05-07 23:31:33 +02:00
|
|
|
<div class="w-full lg:w-1/3 transition-all duration-300" :class="{'lg:w-full': formFullscreen}">
|
|
|
|
|
<div class="card bg-base-100 shadow-xl h-full flex flex-col">
|
|
|
|
|
<div class="card-body overflow-y-auto flex-1">
|
|
|
|
|
<div class="flex justify-between items-center mb-2">
|
2026-05-09 23:14:48 +02:00
|
|
|
<h2 class="card-title">{{ __("Edit") }}</h2>
|
2026-05-07 23:31:33 +02:00
|
|
|
<button wire:click="toggleFullscreen" class="btn btn-circle btn-sm" title="Pantalla completa">
|
|
|
|
|
<span x-text="formFullscreen ? '✕' : '⤢'"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
|
|
|
|
|
@if($selectedFeature)
|
|
|
|
|
{{-- Feature seleccionado --}}
|
|
|
|
|
<div class="border rounded-lg p-3 mb-3 bg-base-200">
|
|
|
|
|
<h3 class="font-bold text-sm">{{ $selectedFeature->name ?? 'Elemento' }}</h3>
|
|
|
|
|
<p class="text-xs text-gray-500">Fase: {{ $selectedFeature->layer?->phase?->name ?? '—' }}</p>
|
|
|
|
|
<p class="text-xs text-gray-500">Capa: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-09 23:14:48 +02:00
|
|
|
{{-- {{ __("Progress") }} --}}
|
2026-05-09 21:30:46 +02:00
|
|
|
<div class="form-control mb-3">
|
2026-05-09 23:14:48 +02:00
|
|
|
<label class="label-text">{{ __("Progress") }}: {{ $editProgress }}%</label>
|
2026-05-09 21:30:46 +02:00
|
|
|
<input type="range" min="0" max="100" wire:model.live="editProgress" class="range range-primary range-sm" />
|
|
|
|
|
<div class="flex justify-between text-xs">
|
|
|
|
|
<span>0%</span><span>50%</span><span>100%</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-control mb-3">
|
2026-05-09 23:14:48 +02:00
|
|
|
<label class="label-text">{{ __("Responsible") }}</label>
|
2026-05-09 21:30:46 +02:00
|
|
|
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="Nombre" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button wire:click="saveFeatureProgress" class="btn btn-primary btn-sm w-full mb-3">
|
2026-05-09 23:14:48 +02:00
|
|
|
💾 {{ __("Save progress") }}
|
2026-05-09 21:30:46 +02:00
|
|
|
</button>
|
|
|
|
|
|
2026-05-09 22:28:20 +02:00
|
|
|
{{-- Gestor de archivos del feature --}}
|
|
|
|
|
<details class="mb-3 border rounded-lg">
|
|
|
|
|
<summary class="text-xs font-semibold cursor-pointer p-2 bg-base-200 rounded-t-lg">
|
2026-05-09 23:14:48 +02:00
|
|
|
📎 {{ __("Files of element") }}
|
2026-05-09 22:28:20 +02:00
|
|
|
</summary>
|
|
|
|
|
<div class="p-2">
|
|
|
|
|
@livewire('media-manager', [
|
|
|
|
|
'mediableType' => 'App\\Models\\Feature',
|
|
|
|
|
'mediableId' => $selectedFeature->id,
|
|
|
|
|
], key('media-feature-' . $selectedFeature->id))
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
|
2026-05-09 21:30:46 +02:00
|
|
|
{{-- Templates / Inspecciones --}}
|
|
|
|
|
@if($templates->isNotEmpty())
|
2026-05-09 23:14:48 +02:00
|
|
|
<div class="divider text-xs">{{ __("Inspection") }}</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
<div class="form-control mb-2">
|
|
|
|
|
<label class="label-text">Plantilla</label>
|
|
|
|
|
<select wire:model.live="selectedTemplateId" wire:change="onTemplateChange" class="select select-bordered select-sm">
|
|
|
|
|
<option value="">Seleccionar plantilla...</option>
|
|
|
|
|
@foreach($templates as $t)
|
|
|
|
|
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
|
|
|
|
@endforeach
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if($selectedTemplateId && !empty($inspectionFormData))
|
|
|
|
|
@php $template = $templates->firstWhere('id', $selectedTemplateId); @endphp
|
|
|
|
|
@if($template)
|
|
|
|
|
@foreach($template->fields as $field)
|
|
|
|
|
<div class="mb-2">
|
|
|
|
|
<label class="label-text text-xs">{{ $field['label'] }} @if($field['required'] ?? false)<span class="text-error">*</span>@endif</label>
|
|
|
|
|
@switch($field['type'] ?? 'text')
|
|
|
|
|
@case('percentage')
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
<input type="number" wire:model="inspectionFormData.{{ $field['name'] }}" min="0" max="100" class="input input-bordered input-sm w-16" />
|
|
|
|
|
<span class="text-xs">%</span>
|
|
|
|
|
<input type="range" min="0" max="100" wire:model.live="inspectionFormData.{{ $field['name'] }}" class="range range-primary range-xs flex-1" />
|
|
|
|
|
</div>
|
|
|
|
|
@break
|
|
|
|
|
@case('boolean')
|
|
|
|
|
<input type="checkbox" wire:model="inspectionFormData.{{ $field['name'] }}" class="checkbox checkbox-sm" />
|
|
|
|
|
@break
|
|
|
|
|
@case('select')
|
|
|
|
|
<select wire:model="inspectionFormData.{{ $field['name'] }}" class="select select-bordered select-sm w-full">
|
|
|
|
|
<option value="">Seleccionar</option>
|
|
|
|
|
@foreach(explode(',', $field['options'] ?? '') as $opt)
|
|
|
|
|
<option value="{{ trim($opt) }}">{{ trim($opt) }}</option>
|
|
|
|
|
@endforeach
|
|
|
|
|
</select>
|
|
|
|
|
@break
|
|
|
|
|
@case('textarea')
|
|
|
|
|
<textarea wire:model="inspectionFormData.{{ $field['name'] }}" rows="2" class="textarea textarea-bordered textarea-sm w-full"></textarea>
|
|
|
|
|
@break
|
|
|
|
|
@default
|
|
|
|
|
<input type="{{ $field['type'] ?? 'text' }}" wire:model="inspectionFormData.{{ $field['name'] }}" class="input input-bordered input-sm w-full" />
|
|
|
|
|
@endswitch
|
|
|
|
|
</div>
|
|
|
|
|
@endforeach
|
2026-05-09 23:14:48 +02:00
|
|
|
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">{{ __("Register inspection") }}</button>
|
2026-05-09 21:30:46 +02:00
|
|
|
@endif
|
|
|
|
|
@endif
|
|
|
|
|
|
2026-05-09 23:14:48 +02:00
|
|
|
{{-- {{ __("History") }} de inspecciones --}}
|
2026-05-09 21:30:46 +02:00
|
|
|
@if($inspectionHistory->isNotEmpty())
|
2026-05-09 23:14:48 +02:00
|
|
|
<div class="divider text-xs">{{ __("History") }}</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
<div class="space-y-1 max-h-40 overflow-y-auto">
|
|
|
|
|
@foreach($inspectionHistory as $ins)
|
|
|
|
|
<div class="border rounded p-2 text-xs">
|
|
|
|
|
<div class="flex justify-between">
|
2026-05-09 23:14:48 +02:00
|
|
|
<span class="font-medium">{{ $ins->template?->name ?? '{{ __("Inspection") }}' }}</span>
|
2026-05-09 21:30:46 +02:00
|
|
|
<span class="text-gray-400">{{ $ins->created_at->diffForHumans() }}</span>
|
2026-05-07 23:31:33 +02:00
|
|
|
</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
@if($ins->user)<span class="text-gray-500">por {{ $ins->user->name }}</span>@endif
|
|
|
|
|
</div>
|
|
|
|
|
@endforeach
|
2026-05-07 23:31:33 +02:00
|
|
|
</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
@endif
|
|
|
|
|
@else
|
|
|
|
|
<div role="alert" class="alert alert-vertical sm:alert-horizontal">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<div>
|
2026-05-09 23:14:48 +02:00
|
|
|
<h3 class="font-bold">{{ __("No templates yet") }}</h3>
|
|
|
|
|
<div class="text-xs">{{ __("Create an inspection template") }}.</div>
|
2026-05-09 21:30:46 +02:00
|
|
|
</div>
|
2026-05-09 23:14:48 +02:00
|
|
|
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __("Create") }}</a>
|
2026-05-09 21:30:46 +02:00
|
|
|
</div>
|
2026-05-07 23:31:33 +02:00
|
|
|
@endif
|
|
|
|
|
@else
|
2026-05-09 21:30:46 +02:00
|
|
|
<div class="text-center text-gray-400 py-8">
|
|
|
|
|
<p class="text-lg">👆</p>
|
|
|
|
|
<p>Haz clic en un elemento del mapa para editarlo</p>
|
2026-05-07 23:31:33 +02:00
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@push('scripts')
|
|
|
|
|
<style>
|
2026-05-09 21:30:46 +02:00
|
|
|
.leaflet-container { z-index: 0 !important; }
|
2026-05-07 23:31:33 +02:00
|
|
|
</style>
|
|
|
|
|
<script>
|
|
|
|
|
let map;
|
2026-05-09 21:30:46 +02:00
|
|
|
const layers = {};
|
2026-05-09 22:28:20 +02:00
|
|
|
let imageMarkersLayer = null;
|
|
|
|
|
let imageViewerModal = null;
|
2026-05-07 23:31:33 +02:00
|
|
|
|
|
|
|
|
function initMap() {
|
2026-05-09 21:30:46 +02:00
|
|
|
if (map) return;
|
|
|
|
|
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
|
|
|
|
map = L.map('map').setView(center, 16);
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> & CartoDB'
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
|
2026-05-09 21:30:46 +02:00
|
|
|
// Cargar fases y sus features
|
2026-05-07 23:31:33 +02:00
|
|
|
@foreach($phases as $phase)
|
2026-05-08 01:16:20 +02:00
|
|
|
@php
|
2026-05-09 21:30:46 +02:00
|
|
|
$phaseFeatures = $phase->features()->with('layer.phase')->get();
|
2026-05-08 01:16:20 +02:00
|
|
|
$fc = [
|
|
|
|
|
'type' => 'FeatureCollection',
|
|
|
|
|
'features' => $phaseFeatures->map(function($f) {
|
|
|
|
|
return [
|
|
|
|
|
'type' => 'Feature',
|
|
|
|
|
'id' => $f->id,
|
|
|
|
|
'geometry' => $f->geometry,
|
|
|
|
|
'properties' => array_merge($f->properties ?? [], [
|
|
|
|
|
'name' => $f->name,
|
|
|
|
|
'progress' => $f->progress,
|
|
|
|
|
'responsible' => $f->responsible,
|
|
|
|
|
'template_id' => $f->template_id,
|
|
|
|
|
'_feature_id' => $f->id,
|
|
|
|
|
])
|
|
|
|
|
];
|
|
|
|
|
})->values()->toArray()
|
|
|
|
|
];
|
|
|
|
|
@endphp
|
2026-05-09 21:30:46 +02:00
|
|
|
(function() {
|
2026-05-11 23:27:14 +02:00
|
|
|
const data = @json($fc);
|
2026-05-09 21:30:46 +02:00
|
|
|
if (data && data.features && data.features.length > 0) {
|
|
|
|
|
const phaseLayer = L.geoJSON(data, {
|
|
|
|
|
style: { color: '{{ $phase->color }}', weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
|
|
|
|
onEachFeature: function(feature, layer) {
|
|
|
|
|
const props = feature.properties || {};
|
|
|
|
|
const featId = props._feature_id || feature.id;
|
|
|
|
|
let content = `<b>${props.name || 'Elemento'}</b><br>
|
2026-05-09 23:14:48 +02:00
|
|
|
{{ __("Progress") }}: ${props.progress || 0}%<br>
|
|
|
|
|
{{ __("Responsible") }}: ${props.responsible || '-'}<br>
|
2026-05-27 19:48:29 +02:00
|
|
|
<button class="btn btn-xs btn-primary mt-1" onclick="selectFeature('${featId}')">✏️ Editar</button>`;
|
2026-05-09 21:30:46 +02:00
|
|
|
layer.bindPopup(content);
|
2026-05-27 19:48:29 +02:00
|
|
|
layer.on('click', function() { selectFeature(' + featId + '); });
|
2026-05-09 21:30:46 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
layers[{{ $phase->id }}] = phaseLayer;
|
|
|
|
|
@if(in_array($phase->id, $activeLayers))
|
|
|
|
|
phaseLayer.addTo(map);
|
|
|
|
|
@endif
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
2026-05-09 21:30:46 +02:00
|
|
|
})();
|
|
|
|
|
@endforeach
|
2026-05-08 01:16:20 +02:00
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
|
map.invalidateSize();
|
|
|
|
|
zoomToAllFeatures();
|
2026-05-09 21:30:46 +02:00
|
|
|
}, 200);
|
|
|
|
|
}
|
2026-05-07 23:31:33 +02:00
|
|
|
|
2026-05-09 21:30:46 +02:00
|
|
|
function zoomToAllFeatures() {
|
|
|
|
|
if (!map) return;
|
|
|
|
|
const bounds = L.latLngBounds();
|
|
|
|
|
let hasBounds = false;
|
|
|
|
|
for (let id in layers) {
|
|
|
|
|
const layer = layers[id];
|
|
|
|
|
if (map.hasLayer(layer) && typeof layer.getBounds === 'function') {
|
|
|
|
|
const b = layer.getBounds();
|
|
|
|
|
if (b.isValid()) { bounds.extend(b); hasBounds = true; }
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-09 21:30:46 +02:00
|
|
|
if (hasBounds) map.fitBounds(bounds, { padding: [20, 20] });
|
|
|
|
|
else map.setView([{{ $project->lat }}, {{ $project->lng }}], 16);
|
|
|
|
|
}
|
2026-05-07 23:31:33 +02:00
|
|
|
|
2026-05-09 21:30:46 +02:00
|
|
|
function selectFeature(featureId) {
|
|
|
|
|
@this.selectFeature(featureId);
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getUserLocation() {
|
|
|
|
|
if (navigator.geolocation) {
|
|
|
|
|
navigator.geolocation.getCurrentPosition((pos) => {
|
|
|
|
|
const latlng = [pos.coords.latitude, pos.coords.longitude];
|
2026-05-09 21:30:46 +02:00
|
|
|
L.marker(latlng).addTo(map).bindPopup('Tu ubicación').openPopup();
|
2026-05-07 23:31:33 +02:00
|
|
|
map.setView(latlng, 16);
|
2026-05-09 21:30:46 +02:00
|
|
|
}, () => alert('No se pudo obtener la ubicación'));
|
2026-05-07 23:31:33 +02:00
|
|
|
} else {
|
|
|
|
|
alert('Geolocalización no soportada');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 21:30:46 +02:00
|
|
|
document.addEventListener('livewire:init', function () {
|
|
|
|
|
setTimeout(initMap, 100);
|
|
|
|
|
|
|
|
|
|
Livewire.on('layersUpdated', (activeIds) => {
|
2026-05-11 11:54:19 +02:00
|
|
|
// Livewire wraps single parameters in an array, so we need to extract the actual data
|
|
|
|
|
const ids = Array.isArray(activeIds) ? activeIds[0] : activeIds;
|
2026-05-09 21:30:46 +02:00
|
|
|
for (let id in layers) {
|
|
|
|
|
const lid = parseInt(id);
|
|
|
|
|
if (ids.includes(lid)) {
|
|
|
|
|
if (!map.hasLayer(layers[id])) layers[id].addTo(map);
|
|
|
|
|
} else {
|
|
|
|
|
if (map.hasLayer(layers[id])) map.removeLayer(layers[id]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
zoomToAllFeatures();
|
2026-05-11 11:54:19 +02:00
|
|
|
);
|
2026-05-09 21:30:46 +02:00
|
|
|
|
|
|
|
|
Livewire.on('centerMap', zoomToAllFeatures);
|
|
|
|
|
Livewire.on('mapResize', () => { if (map) setTimeout(() => map.invalidateSize(), 100); });
|
2026-05-09 22:28:20 +02:00
|
|
|
|
|
|
|
|
// Toggle imágenes en mapa
|
|
|
|
|
Livewire.on('featureImagesToggled', (show, markers) => {
|
|
|
|
|
const m = Array.isArray(markers) ? markers : markers[1];
|
|
|
|
|
const s = Array.isArray(show) ? show[0] : show;
|
|
|
|
|
if (imageMarkersLayer) { map.removeLayer(imageMarkersLayer); imageMarkersLayer = null; }
|
|
|
|
|
if (s && m && m.length > 0) {
|
|
|
|
|
imageMarkersLayer = L.layerGroup().addTo(map);
|
|
|
|
|
const photoIcon = L.divIcon({
|
|
|
|
|
html: '<span style="font-size: 20px; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5));">🖼️</span>',
|
|
|
|
|
className: '',
|
|
|
|
|
iconSize: [20, 20],
|
|
|
|
|
iconAnchor: [10, 10]
|
|
|
|
|
});
|
|
|
|
|
m.forEach(marker => {
|
|
|
|
|
const popupContent = `<b>${marker.name}</b><br>
|
|
|
|
|
<img src="${marker.image_url}" class="max-w-[200px] max-h-[150px] rounded cursor-pointer"
|
2026-05-27 19:48:29 +02:00
|
|
|
onclick="window.openViewer('${marker.image_url}', '${marker.image_name}')" />`;
|
2026-05-09 22:28:20 +02:00
|
|
|
L.marker([marker.lat, marker.lng], { icon: photoIcon })
|
|
|
|
|
.bindPopup(popupContent)
|
|
|
|
|
.addTo(imageMarkersLayer);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Modal para ver imagen al hacer clic
|
|
|
|
|
window.openViewer = function(url, name) {
|
|
|
|
|
if (imageViewerModal) imageViewerModal.remove();
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
overlay.id = 'imageViewerModal';
|
|
|
|
|
overlay.style.cssText = 'position:fixed;inset:0;z-index:5000;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;padding:20px;cursor:pointer';
|
|
|
|
|
overlay.innerHTML = `<div style="position:relative;max-width:90vw;max-height:90vh">
|
|
|
|
|
<button onclick="this.closest('#imageViewerModal').remove()" style="position:absolute;top:-30px;right:0;color:white;font-size:24px;background:none;border:none;cursor:pointer">✕</button>
|
|
|
|
|
<img src="${url}" alt="${name}" style="max-width:100%;max-height:85vh;object-fit:contain;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5)" />
|
|
|
|
|
<p style="color:white;text-align:center;margin-top:8px;font-size:14px">${name}</p>
|
|
|
|
|
</div>`;
|
|
|
|
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
imageViewerModal = overlay;
|
|
|
|
|
};
|
2026-05-09 21:30:46 +02:00
|
|
|
});
|
2026-05-07 23:31:33 +02:00
|
|
|
</script>
|
|
|
|
|
@endpush
|