security: fix 27 vulnerabilities + UI integration (Issues tab, project nav, validation)
Security fixes (27 vulnerabilities across 20 files): CRITICAL: - MediaManager: whitelist mediable types prevents RCE via class instantiation - MediaManager/OfflineSyncController: IDOR fixes, remove Auth::id()??1 fallback - ClientProjects: verify project ownership on all mutations (IDOR) - CompanyManagement: Admin role check on mount() and mutations (auth bypass) - ProjectMap: scope feature/template lookups to current project (IDOR x5) - PhaseList/TemplateManager/LayerManager: scope mutations to owned resources (IDOR) - ProjectEditTabs: Gate::authorize on mount() and updateProject() - routes/web.php: reports routes moved inside can:manage all middleware (auth bypass) MEDIUM: - layer-manager: escapeHtml() on Leaflet popup interpolations (XSS) - MediaManager: server-side MIME validation + 50MB limit - ProjectList/ProjectUsers/ProjectCompanies/PhaseProgress: auth checks added - AdminUsers/ReportsDashboard/ExportController: role/permission checks added LOW: - config/session.php: secure cookie tied to production env - OfflineSyncController: sanitize storage path (path traversal) UI integration: - project-map: Issues tab (4th) with open-count badge - project-map: project navigation bar (Dashboard/Map/Gantt/Report/Issues) - project-dashboard: action buttons for Map/Gantt/Report/Issues - project-form: validation error summary + per-field @error spans - template-manager: validation error display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<div x-data="{ formFullscreen: $wire.entangle('formFullscreen') }"
|
||||
<div x-data="{ formFullscreen: $wire.entangle('formFullscreen') }"
|
||||
class="flex flex-col lg:flex-row gap-0 h-screen p-1">
|
||||
<!-- Columna izquierda: Mapa -->
|
||||
<div x-show="!formFullscreen" x-cloak class="w-full lg:w-2/3 flex-1 relative">
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- 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">
|
||||
<h3 class="font-semibold text-base mb-2">{{ __('Phases and layers') }}</h3>
|
||||
<h3 class="font-semibold text-base mb-2">{{ __("Fases and layers") }}</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($phases as $phase)
|
||||
<div class="border rounded-lg p-2 {{ in_array($phase->id, $activeLayers) ? 'bg-base-200' : '' }}">
|
||||
@@ -27,7 +27,7 @@
|
||||
<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>
|
||||
<span class="badge badge-xs">{{ $layer->features_count ?? $layer->features->count() }} elem.</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -36,10 +36,10 @@
|
||||
{{-- 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">
|
||||
✏️ {{ __('Manage Layers') }}
|
||||
✏️ {{ __("Manage Layers") }}
|
||||
</a>
|
||||
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-outline">
|
||||
📊 {{ __('Progress') }}
|
||||
📊 {{ __("Progress") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +50,7 @@
|
||||
<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" />
|
||||
🖼️ {{ __('Show images on map') }}
|
||||
🖼️ {{ __("Show images on map") }}
|
||||
@if($featureImageMarkers)
|
||||
<span class="badge badge-xs">{{ count($featureImageMarkers) }}</span>
|
||||
@endif
|
||||
@@ -60,34 +60,67 @@
|
||||
{{-- Botones generales --}}
|
||||
<div class="mt-2 space-y-1">
|
||||
<a href="{{ route('projects.media', $project) }}" class="btn btn-sm btn-outline w-full">
|
||||
📁 {{ __('Project files') }}
|
||||
</a>
|
||||
📁 {{ __("Project files") }}
|
||||
</button>
|
||||
<button wire:click="$dispatch('centerMap')" class="btn btn-sm btn-outline w-full">
|
||||
📍 {{ __('Centered in project') }}
|
||||
📍 {{ __("Centered in project") }}
|
||||
</button>
|
||||
<button onclick="getUserLocation()" class="btn btn-sm btn-outline w-full">
|
||||
🧭 {{ __('My location') }}
|
||||
🧭 {{ __("My location") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna derecha: {{ __('Edit') }} de progreso / inspecciones -->
|
||||
<!-- Columna derecha: {{ __("Edit") }} de progreso / inspecciones -->
|
||||
<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">
|
||||
<h2 class="card-title">{{ __('Map') }}</h2>
|
||||
<button wire:click="toggleFullscreen" class="btn btn-circle btn-sm" title="{{ __('Fullscreen') }}">
|
||||
<h2 class="card-title">{{ __("Project Map") }}</h2>
|
||||
<button wire:click="toggleFullscreen" class="btn btn-circle btn-sm" title="Pantalla completa">
|
||||
<span x-text="formFullscreen ? '✕' : '⤢'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Project navigation bar -->
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
<a href="{{ route('projects.dashboard', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.dashboard') ? 'btn-primary' : 'btn-outline' }}">
|
||||
📊 {{ __('Dashboard') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.map', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.map') ? 'btn-primary' : 'btn-outline' }}">
|
||||
🗺️ {{ __('Map') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.gantt', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.gantt') ? 'btn-primary' : 'btn-outline' }}">
|
||||
📅 {{ __('Gantt') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.report', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.report') ? 'btn-primary' : 'btn-outline' }}">
|
||||
📄 {{ __('Report') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.issues', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.issues') ? 'btn-error' : 'btn-outline' }} gap-1">
|
||||
⚠️ {{ __('Issues') }}
|
||||
@if($openIssuesCount > 0)
|
||||
<span class="badge badge-error badge-xs">{{ $openIssuesCount }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs box mb-4">
|
||||
<button wire:click="setActiveTab('edit')" class="tab {{ $activeTab === 'edit' ? 'tab-active' : '' }}">{{ __('Edit') }}</button>
|
||||
<button wire:click="setActiveTab('features')" class="tab {{ $activeTab === 'features' ? 'tab-active' : '' }}">{{ __('Features') }}</button>
|
||||
<button wire:click="setActiveTab('inspections')" class="tab {{ $activeTab === 'inspections' ? 'tab-active' : '' }}">{{ __('Inspections') }}</button>
|
||||
<button wire:click="setActiveTab('edit')" class="tab {{ $activeTab === 'edit' ? 'tab-active' : '' }}">{{ __("Edit") }}</button>
|
||||
<button wire:click="setActiveTab('features')" class="tab {{ $activeTab === 'features' ? 'tab-active' : '' }}">{{ __("Features") }}</button>
|
||||
<button wire:click="setActiveTab('inspections')" class="tab {{ $activeTab === 'inspections' ? 'tab-active' : '' }}">{{ __("Inspections") }}</button>
|
||||
<button wire:click="setActiveTab('issues')" class="tab {{ $activeTab === 'issues' ? 'tab-active' : '' }} gap-1">
|
||||
{{ __('Issues') }}
|
||||
@if($openIssuesCount > 0)
|
||||
<span class="badge badge-error badge-xs">{{ $openIssuesCount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
@@ -96,14 +129,14 @@
|
||||
@if($selectedFeature)
|
||||
<!-- Feature seleccionado -->
|
||||
<div class="border rounded-lg p-3 mb-3 bg-base-200">
|
||||
<h3 class="font-bold text-sm">{{ $selectedFeature->name ?? __('Feature') }}</h3>
|
||||
<p class="text-xs text-gray-500">{{ __('Phase') }}: {{ $selectedFeature->layer?->phase?->name ?? '—' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ __('Layer') }}: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
<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>
|
||||
|
||||
{{-- Progreso --}}
|
||||
{{-- {{ __("Progress") }} --}}
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">{{ __('Progress') }}: {{ $editProgress }}%</label>
|
||||
<label class="label-text">{{ __("Progress") }}: {{ $editProgress }}%</label>
|
||||
<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>
|
||||
@@ -111,18 +144,18 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">{{ __('Responsible') }}</label>
|
||||
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="{{ __('Name of responsible') }}" />
|
||||
<label class="label-text">{{ __("Responsible") }}</label>
|
||||
<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">
|
||||
💾 {{ __('Save progress') }}
|
||||
💾 {{ __("Save progress") }}
|
||||
</button>
|
||||
|
||||
{{-- 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">
|
||||
📎 {{ __('Files of element') }}
|
||||
📎 {{ __("Files of element") }}
|
||||
</summary>
|
||||
<div class="p-2">
|
||||
@livewire('media-manager', [
|
||||
@@ -134,11 +167,11 @@
|
||||
|
||||
{{-- Templates / Inspecciones --}}
|
||||
@if($templates->isNotEmpty())
|
||||
<div class="divider text-xs">{{ __('Inspection') }}</div>
|
||||
<div class="divider text-xs">{{ __("Inspection") }}</div>
|
||||
<div class="form-control mb-2">
|
||||
<label class="label-text">{{ __('Template') }}</label>
|
||||
<label class="label-text">Plantilla</label>
|
||||
<select wire:model.live="selectedTemplateId" wire:change="onTemplateChange" class="select select-bordered select-sm">
|
||||
<option value="">{{ __('Select template...') }}</option>
|
||||
<option value="">Seleccionar plantilla...</option>
|
||||
@foreach($templates as $t)
|
||||
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
||||
@endforeach
|
||||
@@ -164,7 +197,7 @@
|
||||
@break
|
||||
@case('select')
|
||||
<select wire:model="inspectionFormData.{{ $field['name'] }}" class="select select-bordered select-sm w-full">
|
||||
<option value="">{{ __('Select') }}</option>
|
||||
<option value="">Seleccionar</option>
|
||||
@foreach(explode(',', $field['options'] ?? '') as $opt)
|
||||
<option value="{{ trim($opt) }}">{{ trim($opt) }}</option>
|
||||
@endforeach
|
||||
@@ -178,21 +211,21 @@
|
||||
@endswitch
|
||||
</div>
|
||||
@endforeach
|
||||
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">{{ __('Register inspection') }}</button>
|
||||
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">{{ __("Register inspection") }}</button>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Historial de inspecciones --}}
|
||||
{{-- {{ __("History") }} de inspecciones --}}
|
||||
@if($inspectionHistory->isNotEmpty())
|
||||
<div class="divider text-xs">{{ __('History') }}</div>
|
||||
<div class="divider text-xs">{{ __("History") }}</div>
|
||||
<div class="space-y-1 max-h-40 overflow-y-auto">
|
||||
@foreach($inspectionHistory as $ins)
|
||||
<div class="border rounded p-2 text-xs" wire:key="hist-{{ $ins->id }}">
|
||||
<div class="border rounded p-2 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">{{ $ins->template?->name ?? __('Inspection') }}</span>
|
||||
<span class="font-medium">{{ $ins->template?->name ?? __("Inspection") }}</span>
|
||||
<span class="text-gray-400">{{ $ins->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@if($ins->user)<span class="text-gray-500">{{ __('by') }} {{ $ins->user->name }}</span>@endif
|
||||
@if($ins->user)<span class="text-gray-500">por {{ $ins->user->name }}</span>@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -203,16 +236,16 @@
|
||||
<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>
|
||||
<h3 class="font-bold">{{ __('No templates yet') }}</h3>
|
||||
<div class="text-xs">{{ __('Create an inspection template') }}.</div>
|
||||
<h3 class="font-bold">{{ __("No templates yet") }}</h3>
|
||||
<div class="text-xs">{{ __("Create an inspection template") }}.</div>
|
||||
</div>
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __('Create') }}</a>
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __("Create") }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">👆</p>
|
||||
<p>{{ __('Click on a map element or search above to edit it') }}</p>
|
||||
<p>Haz clic en un elemento del mapa para editarlo</p>
|
||||
</div>
|
||||
@endif
|
||||
@elseif($activeTab === 'features')
|
||||
@@ -222,12 +255,12 @@
|
||||
<table class="table table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Feature') }}</th>
|
||||
<th>{{ __('Layer') }}</th>
|
||||
<th>{{ __('Phase') }}</th>
|
||||
<th>{{ __('Progress') }}</th>
|
||||
<th>{{ __('Responsible') }}</th>
|
||||
<th>{{ __('Template') }}</th>
|
||||
<th>{{ __("Feature") }}</th>
|
||||
<th>{{ __("Layer") }}</th>
|
||||
<th>{{ __("Phase") }}</th>
|
||||
<th>{{ __("Progress") }}</th>
|
||||
<th>{{ __("Responsible") }}</th>
|
||||
<th>{{ __("Template") }}</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -251,7 +284,7 @@
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">📋</p>
|
||||
<p>{{ __('No elements in this project') }}</p>
|
||||
<p>{{ __("No features found") }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@elseif($activeTab === 'inspections')
|
||||
@@ -261,10 +294,10 @@
|
||||
<table class="table table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Date') }}</th>
|
||||
<th>{{ __('Feature') }}</th>
|
||||
<th>{{ __('Template') }}</th>
|
||||
<th>{{ __('User') }}</th>
|
||||
<th>{{ __("Date") }}</th>
|
||||
<th>{{ __("Feature") }}</th>
|
||||
<th>{{ __("Template") }}</th>
|
||||
<th>{{ __("User") }}</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -286,14 +319,16 @@
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">📋</p>
|
||||
<p>{{ __('No inspections registered') }}</p>
|
||||
<p>{{ __("No inspections found") }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@elseif($activeTab === 'issues')
|
||||
<!-- Issues tab: render embedded IssueManager component -->
|
||||
@livewire('issue-manager', ['project' => $project], key('issues-tab-' . $project->id))
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
@@ -335,7 +370,7 @@
|
||||
// Prevent multiple initializations
|
||||
if (mapInitialized || map) return;
|
||||
mapInitialized = true;
|
||||
|
||||
|
||||
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
||||
map = L.map('map').setView(center, 16);
|
||||
|
||||
@@ -373,13 +408,14 @@
|
||||
onEachFeature: function(feature, layer) {
|
||||
const props = feature.properties || {};
|
||||
const featId = props._feature_id || feature.id;
|
||||
const safeName = escapeHtml(props.name || '{{ __('Feature') }}');
|
||||
// Escape all user-generated content for HTML context
|
||||
const safeName = escapeHtml(props.name || 'Elemento');
|
||||
const safeProgress = escapeHtml(props.progress || 0);
|
||||
const safeResponsible = escapeHtml(props.responsible || '-');
|
||||
let content = `<b>${safeName}</b><br>
|
||||
{{ __('Progress') }}: ${safeProgress}%<br>
|
||||
{{ __('Responsible') }}: ${safeResponsible}<br>
|
||||
<button class="btn btn-xs btn-primary mt-1" onclick="selectFeature('${featId}')">✏️ {{ __('Edit') }}</button>`;
|
||||
{{ __("Progress") }}: ${safeProgress}%<br>
|
||||
{{ __("Responsible") }}: ${safeResponsible}<br>
|
||||
<button class="btn btn-xs btn-primary mt-1" onclick="selectFeature('${featId}')">✏️ Editar</button>`;
|
||||
layer.bindPopup(content);
|
||||
layer.on('click', function() { selectFeature(featId); });
|
||||
}
|
||||
@@ -394,11 +430,11 @@
|
||||
|
||||
// Initialize combined bounds
|
||||
updateCombinedBounds();
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
zoomToAllFeatures();
|
||||
}, 100);
|
||||
}, 100); // Reduced from 200ms to 100ms
|
||||
}
|
||||
|
||||
function updateCombinedBounds() {
|
||||
@@ -409,9 +445,9 @@
|
||||
const layer = layers[id];
|
||||
if (map.hasLayer(layer) && typeof layer.getBounds === 'function') {
|
||||
const b = layer.getBounds();
|
||||
if (b.isValid()) {
|
||||
combinedBounds.extend(b);
|
||||
hasBounds = true;
|
||||
if (b.isValid()) {
|
||||
combinedBounds.extend(b);
|
||||
hasBounds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,7 +456,10 @@
|
||||
|
||||
function zoomToAllFeatures() {
|
||||
if (!map) return;
|
||||
|
||||
// Update combined bounds if needed
|
||||
updateCombinedBounds();
|
||||
|
||||
if (combinedBounds && combinedBounds.isValid()) {
|
||||
map.fitBounds(combinedBounds, { padding: [20, 20] });
|
||||
} else {
|
||||
@@ -436,29 +475,32 @@
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition((pos) => {
|
||||
const latlng = [pos.coords.latitude, pos.coords.longitude];
|
||||
L.marker(latlng).addTo(map).bindPopup('{{ __('My location') }}').openPopup();
|
||||
L.marker(latlng).addTo(map).bindPopup('Tu ubicación').openPopup();
|
||||
map.setView(latlng, 16);
|
||||
}, () => alert('{{ __('No results') }}'));
|
||||
}, () => alert('No se pudo obtener la ubicación'));
|
||||
} else {
|
||||
alert('{{ __('No results') }}');
|
||||
alert('Geolocalización no soportada');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('livewire:init', function () {
|
||||
setTimeout(initMap, 50);
|
||||
setTimeout(initMap, 50); // Reduced from 100ms to 50ms
|
||||
|
||||
Livewire.on('layersUpdated', (activeIds) => {
|
||||
// Livewire wraps single parameters in an array, so we need to extract the actual data
|
||||
const ids = Array.isArray(activeIds) ? activeIds[0] : activeIds;
|
||||
for (let id in layers) {
|
||||
const lid = parseInt(id);
|
||||
if (ids.includes(lid)) {
|
||||
if (!map.hasLayer(layers[id])) {
|
||||
layers[id].addTo(map);
|
||||
// Update combined bounds when adding a layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
} else {
|
||||
if (map.hasLayer(layers[id])) {
|
||||
map.removeLayer(layers[id]);
|
||||
// Update combined bounds when removing a layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
}
|
||||
@@ -467,8 +509,9 @@
|
||||
});
|
||||
|
||||
Livewire.on('centerMap', zoomToAllFeatures);
|
||||
Livewire.on('mapResize', () => {
|
||||
Livewire.on('mapResize', () => {
|
||||
if (map) {
|
||||
// Throttle resize events to prevent excessive calls
|
||||
if (!this.resizeTimeout) {
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
@@ -478,12 +521,14 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 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 (imageMarkersLayer) {
|
||||
map.removeLayer(imageMarkersLayer);
|
||||
imageMarkersLayer = null;
|
||||
// Update bounds when removing image markers layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
if (s && m && m.length > 0) {
|
||||
@@ -495,9 +540,10 @@
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
m.forEach(marker => {
|
||||
// Validate URL and sanitize name for security
|
||||
const safeUrl = isValidUrl(marker.image_url) ? marker.image_url : '';
|
||||
const safeName = escapeHtml(marker.image_name || '');
|
||||
if (safeUrl) {
|
||||
if (safeUrl) { // Only add marker if URL is valid
|
||||
const popupContent = `<b>${safeName}</b><br>
|
||||
<img src="${safeUrl}" class="max-w-[200px] max-h-[150px] rounded cursor-pointer"
|
||||
onclick="window.openViewer('${safeUrl}', '${safeName}')" />`;
|
||||
@@ -506,16 +552,21 @@
|
||||
.addTo(imageMarkersLayer);
|
||||
}
|
||||
});
|
||||
// Update bounds when adding image markers layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal para ver imagen al hacer clic
|
||||
window.openViewer = function(url, name) {
|
||||
// Validate URL and sanitize name for security
|
||||
if (!isValidUrl(url)) {
|
||||
console.error('Invalid URL provided to openViewer:', url);
|
||||
return;
|
||||
}
|
||||
|
||||
const safeName = escapeHtml(name);
|
||||
|
||||
if (imageViewerModal) imageViewerModal.remove();
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'imageViewerModal';
|
||||
@@ -531,4 +582,4 @@
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endpush
|
||||
Reference in New Issue
Block a user