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:
2026-06-16 18:25:36 +02:00
parent 7d854ffb0a
commit f8a1310c0f
26 changed files with 1166 additions and 1195 deletions
@@ -1,4 +1,4 @@
<div>
<div>
<x-slot name="header">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
@@ -25,15 +25,19 @@
<div class="flex gap-2">
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm gap-1">
<x-heroicon-o-map class="w-4 h-4" />
Mapa
{{ __('Map') }}
</a>
<a href="{{ route('projects.gantt', $project) }}" class="btn btn-outline btn-sm gap-1">
<x-heroicon-o-calendar-days class="w-4 h-4" />
Gantt
{{ __('Gantt') }}
</a>
<a href="{{ route('projects.report', $project) }}" class="btn btn-outline btn-sm gap-1">
<x-heroicon-o-document-chart-bar class="w-4 h-4" />
{{ __('Report') }}
</a>
<a href="{{ route('projects.issues', $project) }}" class="btn btn-outline btn-sm gap-1">
<x-heroicon-o-exclamation-triangle class="w-4 h-4" />
Issues
{{ __('Issues') }}
</a>
</div>
</div>
@@ -303,7 +307,7 @@
<x-heroicon-o-exclamation-triangle class="w-4 h-4 text-orange-500" />
Issues abiertos
</h3>
<a href="{{ route('projects.issues', $project) }}" class="btn btn-xs btn-outline">Ver todos</a>
<a href="{{ route('projects.issues', $project) }}" class="btn btn-xs btn-outline">{{ __('View all') }}</a>
</div>
@if($recentIssues->isEmpty())
<div class="text-center py-4 text-gray-400">
@@ -346,7 +350,7 @@
<x-heroicon-o-clipboard-document-list class="w-4 h-4 text-yellow-500" />
Inspecciones recientes
</h3>
<a href="{{ route('projects.map', $project) }}?tab=inspections" class="btn btn-xs btn-outline">Ver en mapa</a>
<a href="{{ route('projects.map', $project) }}?tab=inspections" class="btn btn-xs btn-outline">{{ __('View on map') }}</a>
</div>
@if($recentInspections->isEmpty())
<div class="text-center py-4 text-gray-400">
@@ -2,27 +2,40 @@
<div class="max-w-2xl mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">{{ $projectId ? __('Edit Project') : __('New Project') }}</h1>
@if($errors->any())
<div class="alert alert-error text-sm mb-4">
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
<ul class="list-disc pl-3 space-y-0.5">
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
</ul>
</div>
@endif
<form wire:submit.prevent="save" class="space-y-6">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block text-sm font-medium mb-2">{{ __('Name') }}</label>
<input type="text" wire:model="name" class="input input-bordered w-full" placeholder="{{ __('Project name') }}" required>
<input type="text" wire:model="name" class="input input-bordered w-full {{ $errors->has('name') ? 'input-error' : '' }}" placeholder="{{ __('Project name') }}" required>
@error('name') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">{{ __('Address') }}</label>
<input type="text" wire:model="address" class="input input-bordered w-full" placeholder="{{ __('Address') }}">
<input type="text" wire:model="address" class="input input-bordered w-full" placeholder="{{ __('Street address, city, etc.') }}">
@error('address') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">{{ __('Coordinates') }}</label>
<input type="text" wire:model="country" class="input input-bordered w-full" placeholder="{{ __('No country') }}" readonly>
<label class="block text-sm font-medium mb-2">{{ __('Country') }}</label>
<input type="text" wire:model="country" class="input input-bordered w-full" placeholder="{{ __('Country (auto-filled)') }}" readonly>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">{{ __('Start date') }}</label>
<input type="date" wire:model="start_date" class="input input-bordered w-full" required>
<label class="block text-sm font-medium mb-2">{{ __('Start Date') }}</label>
<input type="date" wire:model="start_date" class="input input-bordered w-full {{ $errors->has('start_date') ? 'input-error' : '' }}" required>
@error('start_date') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">{{ __('Estimated end date') }}</label>
<input type="date" wire:model="end_date_estimated" class="input input-bordered w-full">
<label class="block text-sm font-medium mb-2">{{ __('End Date (estimated)') }}</label>
<input type="date" wire:model="end_date_estimated" class="input input-bordered w-full {{ $errors->has('end_date_estimated') ? 'input-error' : '' }}">
@error('end_date_estimated') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">{{ __('Status') }}</label>
@@ -32,13 +45,14 @@
<option value="paused">{{ __('Paused') }}</option>
<option value="completed">{{ __('Completed') }}</option>
</select>
@error('status') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
</div>
<div class="border rounded-lg p-4">
<h2 class="text-xl font-bold mb-4">{{ __('Location') }}</h2>
<h2 class="text-xl font-bold mb-4">{{ __('Project Location') }}</h2>
<p class="text-sm text-gray-500 mb-2">
{{ __('Click on the map or drag the marker to update the location') }}
{{ __('Click on the map to set the project location. The address and country will be filled automatically.') }}
</p>
<div id="projectMap" style="height: 400px; width: 100%; background: #e2e8f0; border-radius: 0.5rem;"></div>
<input type="hidden" wire:model="lat">
@@ -47,7 +61,7 @@
<div class="flex justify-end space-x-4">
<button type="button" wire:click="resetForm" class="btn btn-outline">
{{ __('Cancel') }}
{{ __('Reset') }}
</button>
<button type="submit" class="btn btn-primary">
{{ $projectId ? __('Update') : __('Create') }}
@@ -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