Files
construprogress/resources/views/livewire/layers/layer-manager.blade.php
T
javier 7d854ffb0a feat: i18n, language switcher fix, DataTable improvements, blade translations
- Translation system: lang/es/ PHP files (auth, validation, pagination, passwords)
- Rappasoft vendor translations published (lang/vendor/livewire-tables/es/)
- JSON files synced to 391 keys (EN + ES, full parity)
- APP_LOCALE changed to 'es', users.locale column default changed to 'es'
- Language switcher fixed: JS event + window.location.reload() avoids /livewire/update redirect
- SetLocale middleware fallback uses config('app.locale') instead of hardcoded 'en'
- setSortingPillsEnabled(false) on ProjectTable, CompanyTable, UserTable
- Translated 17 blade views: project-map, template-manager, layer-manager,
  company-management, phase-list, media-manager, reports-dashboard,
  client-projects, layer-upload, project-form, project-map-editor-tab,
  admin/users, projects/media, projects/templates, layouts/client
- Navigation 'Empresas' link uses __('Companies')
- Fixed typo key 'Fases and layers' -> 'Phases and layers'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 18:05:53 +02:00

259 lines
12 KiB
PHP

<div class="flex flex-col h-screen">
{{-- Cabecera fija --}}
<div class="flex justify-between items-center mb-4 px-4 pt-4 flex-shrink-0">
<h1 class="text-2xl font-bold">{{ __("Manage Layers") }} - {{ $phase->name }}</h1>
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm"> {{ __("Back") }}</a>
</div>
<div class="flex-1 overflow-hidden px-4 pb-4">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2 h-full">
{{-- Columna izquierda --}}
<div class="space-y-4 overflow-y-auto h-full pr-2">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">{{ __("Import file") }}</h2>
<form wire:submit.prevent="importFile">
<div class="form-control">
<label class="label">{{ __("Layer name") }}</label>
<input type="text" wire:model="layerName" class="input input-bordered" required>
</div>
<div class="form-control">
<label class="label">Color</label>
<input type="color" wire:model="layerColor" class="input input-bordered">
</div>
<div class="form-control">
<label class="label">{{ __('File') }} (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered">
@error('uploadFile') <span class="text-error">{{ $message }}</span> @enderror
</div>
<button type="submit" class="btn btn-primary w-full mt-2">{{ __("Upload") }}</button>
</form>
<div class="divider"></div>
<button wire:click="createEmptyLayer" class="btn btn-secondary w-full">{{ __("Create empty layer") }}</button>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">{{ __("Layers") }}</h2>
<div class="space-y-2 max-h-96 overflow-y-auto">
@foreach($layers as $layer)
<div wire:key="layer-{{ $layer->id }}" class="flex justify-between items-center p-2 border rounded">
<div class="flex items-center gap-2">
<input type="checkbox"
wire:change="toggleLayerVisibility({{ $layer->id }})"
@if(in_array($layer->id, $visibleLayers)) checked @endif
class="checkbox checkbox-sm" />
<span class="w-4 h-4 rounded-full" style="background: {{ $layer->color ?? '#ccc' }}"></span>
<span class="{{ $selectedLayer && $selectedLayer->id == $layer->id ? 'font-bold text-primary' : '' }}">
{{ $layer->name }}
</span>
</div>
<div>
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info">✏️ {{ __('Edit') }}</button>
<button wire:click="deleteLayer({{ $layer->id }})" wire:confirm="{{ __('Delete layer') }}?" class="btn btn-xs btn-error">🗑️</button>
</div>
</div>
@endforeach
@if($layers->isEmpty())
<p class="text-center">{{ __("No layers. Create or import one.") }}</p>
@endif
</div>
</div>
</div>
</div>
{{-- Columna derecha: mapa PERSISTENTE --}}
<div class="lg:col-span-2 flex flex-col h-full">
<div class="card bg-base-100 shadow-xl flex-1 flex flex-col">
<div class="card-body flex-1 flex flex-col p-2">
<h2 class="card-title">{{ __("Edit") }}</h2>
@if($selectedLayer)
<div class="mt-3 flex gap-2">
<button id="saveDrawingBtn" class="btn btn-primary">💾 {{ __('Save changes') }}</button>
<button wire:click="cancelEditing" class="btn btn-outline">{{ __('Cancel') }}</button>
</div>
@endif
<div id="permanentMap" style="flex: 1; min-height: 500px; width: 100%; background: #e2e8f0;" wire:ignore></div>
</div>
</div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
let map, displayGroup, editableGroup;
let currentEditableLayerId = null;
let allLayersData = {}; // id -> {geojson, color}
let visibleLayerIds = [];
// Inicialización del mapa
function initMap() {
if (map) return;
const center = [{{ $project->lat }}, {{ $project->lng }}];
map = L.map('permanentMap').setView(center, 16);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> & CartoDB'
}).addTo(map);
displayGroup = L.layerGroup().addTo(map); // capas de solo lectura
editableGroup = L.featureGroup().addTo(map); // capa editable (drawnItems)
// Control de dibujo (solo para editableGroup)
const drawControl = new L.Control.Draw({
edit: { featureGroup: editableGroup },
draw: {
polygon: true,
polyline: true,
marker: true,
circle: false,
rectangle: false,
circlemarker: false
}
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, (e) => {
const layer = e.layer;
if (!layer.feature) {
layer.feature = {
type: 'Feature',
properties: { name: 'Nuevo elemento', progress: 0, responsible: '' }
};
}
editableGroup.addLayer(layer);
});
}
// Renderiza todas las capas de solo lectura según visibleLayers (excluyendo la editable actual)
function renderDisplayLayers() {
displayGroup.clearLayers();
for (let id of visibleLayerIds) {
if (id == currentEditableLayerId) continue; // la editable ya se muestra en editableGroup
const data = allLayersData[id];
if (!data) continue;
L.geoJSON(data.geojson, {
style: { color: data.color, weight: 3, opacity: 0.8, fillOpacity: 0.3 },
onEachFeature: (feature, layer) => {
const props = feature.properties;
const content = `<b>${props.name || 'Elemento'}</b><br>
Progreso: ${props.progress || 0}%<br>
Responsable: ${props.responsible || '-'}`;
layer.bindPopup(content);
}
}).addTo(displayGroup);
}
}
// Carga la capa editable (limpia editableGroup y añade los features de la capa seleccionada)
function loadEditableLayer(layerId, geojson, color) {
editableGroup.clearLayers();
currentEditableLayerId = layerId;
if (!geojson || !geojson.features) return;
geojson.features.forEach(feature => {
const tempLayer = L.geoJSON(feature, {
style: { color: color, weight: 4, opacity: 1, fillOpacity: 0.4 },
onEachFeature: (f, l) => {
l.feature = f;
const props = f.properties;
const content = `<b>${props.name || @js(__('Feature'))}</b><br>
@js(__('Progress')): ${props.progress || 0}%<br>
@js(__('Responsible')): ${props.responsible || '-'}<br>
<em>@js(__('Editable'))</em>`;
l.bindPopup(content);
}
});
tempLayer.eachLayer(subLayer => {
subLayer.feature = feature;
editableGroup.addLayer(subLayer);
});
});
}
// Guardar todos los elementos editables (actualiza la capa seleccionada)
function saveCurrentEditableLayer() {
if (!currentEditableLayerId) {
alert('No hay capa seleccionada para editar.');
return;
}
const features = [];
editableGroup.eachLayer(layer => {
let geojson = layer.toGeoJSON();
if (geojson && geojson.geometry) {
if (!geojson.properties && layer.feature?.properties) {
geojson.properties = layer.feature.properties;
} else if (!geojson.properties) {
geojson.properties = { name: 'Elemento', progress: 0, responsible: '' };
}
features.push(geojson);
}
});
if (features.length === 0) {
alert('No hay elementos en la capa editable.');
return;
}
const finalGeojson = {
type: 'FeatureCollection',
features: features,
style: { color: allLayersData[currentEditableLayerId]?.color || '#3b82f6' }
};
@this.saveManualGeojson(JSON.stringify(finalGeojson));
}
// Eventos Livewire
document.addEventListener('livewire:init', () => {
initMap();
// Botón guardar (se conecta dinámicamente)
const saveBtn = document.getElementById('saveDrawingBtn');
if (saveBtn) saveBtn.addEventListener('click', saveCurrentEditableLayer);
// Datos iniciales
Livewire.on('initialLayersData', (data) => {
const payload = Array.isArray(data) ? data[0] : data;
allLayersData = {};
payload.layers.forEach(layer => {
allLayersData[layer.id] = {
geojson: layer.geojson,
color: layer.color
};
});
visibleLayerIds = payload.visibleLayers;
renderDisplayLayers();
if (payload.selectedLayerId && allLayersData[payload.selectedLayerId]) {
const sel = allLayersData[payload.selectedLayerId];
loadEditableLayer(payload.selectedLayerId, sel.geojson, sel.color);
} else {
currentEditableLayerId = null;
editableGroup.clearLayers();
}
});
// Cambio de visibilidad
Livewire.on('visibilityChanged', (visibleIds) => {
visibleLayerIds = Array.isArray(visibleIds) ? visibleIds : visibleIds[0];
renderDisplayLayers();
});
// Selección de capa para editar
Livewire.on('layerSelectedForEdit', (data) => {
const payload = Array.isArray(data) ? data[0] : data;
if (!payload) {
currentEditableLayerId = null;
editableGroup.clearLayers();
renderDisplayLayers();
return;
}
if (!allLayersData[payload.layerId]) return;
loadEditableLayer(payload.layerId, payload.geojson, payload.color);
// Asegurar que la capa es visible
if (!visibleLayerIds.includes(payload.layerId)) {
visibleLayerIds.push(payload.layerId);
renderDisplayLayers();
}
});
});
</script>
@endpush