feat(project-map): clearer editor tabs + per-phase and per-layer visibility

1. Editor tabs restyled as spaced DaisyUI buttons (btn-primary when active,
   btn-ghost otherwise) — fixes cramped labels and missing active indicator.
2. Layer visibility now works at two levels:
   - Phase toggle calls togglePhase() and shows/hides ALL its layers
     (checked only when every layer of the phase is active)
   - Each layer has its own independent toggle calling toggleLayer()
   Map JS regrouped to build one Leaflet group per LAYER (keyed by layer id)
   instead of per phase, so activeLayers (layer ids) drives visibility
   correctly per layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 13:14:17 +02:00
parent 19fef5aa25
commit 558b1732aa
@@ -22,23 +22,34 @@
</div>
<div class="space-y-3">
@foreach($phases as $phase)
<div class="border rounded-lg p-2 {{ in_array($phase->id, $activeLayers) ? 'bg-base-200' : '' }}">
@php
$phaseLayerIds = $phase->layers->pluck('id')->map(fn($i) => (int) $i)->all();
$phaseAllActive = count($phaseLayerIds) > 0 && collect($phaseLayerIds)->every(fn($i) => in_array($i, $activeLayers));
@endphp
<div class="border rounded-lg p-2 {{ $phaseAllActive ? 'bg-base-200' : '' }}">
{{-- Fase: el toggle muestra/oculta TODAS sus capas --}}
<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">
wire:change="togglePhase({{ $phase->id }})"
@if($phaseAllActive) checked @endif
class="toggle toggle-xs toggle-primary"
title="{{ __('Show/hide all layers of this phase') }}">
<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 --}}
{{-- Capas de esta fase: cada una con su propio toggle independiente --}}
@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>
<div class="flex items-center gap-2 text-xs text-gray-600">
<input type="checkbox"
wire:change="toggleLayer({{ $layer->id }})"
@if(in_array((int) $layer->id, $activeLayers)) checked @endif
class="toggle toggle-xs toggle-primary"
title="{{ __('Show/hide layer') }}">
<span class="w-2 h-2 rounded-full inline-block shrink-0" 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>
@@ -91,11 +102,11 @@
<div class="card-body overflow-y-auto flex-1">
<div class="flex justify-between items-center gap-2 mb-4">
<!-- Tabs -->
<div class="tabs tabs-boxed flex-wrap">
<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">
<div class="flex flex-wrap gap-1">
<button wire:click="setActiveTab('edit')" class="btn btn-sm {{ $activeTab === 'edit' ? 'btn-primary' : 'btn-ghost' }}">{{ __('Edit') }}</button>
<button wire:click="setActiveTab('features')" class="btn btn-sm {{ $activeTab === 'features' ? 'btn-primary' : 'btn-ghost' }}">{{ __('Features') }}</button>
<button wire:click="setActiveTab('inspections')" class="btn btn-sm {{ $activeTab === 'inspections' ? 'btn-primary' : 'btn-ghost' }}">{{ __('Inspections') }}</button>
<button wire:click="setActiveTab('issues')" class="btn btn-sm gap-1 {{ $activeTab === 'issues' ? 'btn-primary' : 'btn-ghost' }}">
{{ __('Issues') }}
@if($openIssuesCount > 0)
<span class="badge badge-error badge-xs">{{ $openIssuesCount }}</span>
@@ -443,53 +454,54 @@
baseLayers['{{ __('Streets') }}'].addTo(map);
L.control.layers(baseLayers, null, { position: 'topleft' }).addTo(map);
// Cargar fases y sus features
// Cargar capas y sus features (cada capa = un grupo Leaflet independiente)
@foreach($phases as $phase)
@php
$phaseFeatures = $phase->features()->with('layer.phase')->get();
$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
(function() {
const data = @json($fc);
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;
const safeName = escapeHtml(props.name || '{{ __('Feature') }}');
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>`;
layer.bindPopup(content);
layer.on('click', function() { selectFeature(featId); });
}
});
layers[{{ $phase->id }}] = phaseLayer;
@if(in_array($phase->id, $activeLayers))
phaseLayer.addTo(map);
@endif
}
})()
@foreach($phase->layers as $layer)
@php
$fc = [
'type' => 'FeatureCollection',
'features' => $layer->features->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
(function() {
const data = @json($fc);
if (data && data.features && data.features.length > 0) {
const layerGroup = L.geoJSON(data, {
style: { color: '{{ $layer->color ?? $phase->color }}', weight: 3, opacity: 0.8, fillOpacity: 0.3 },
onEachFeature: function(feature, lyr) {
const props = feature.properties || {};
const featId = props._feature_id || feature.id;
const safeName = escapeHtml(props.name || '{{ __('Feature') }}');
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>`;
lyr.bindPopup(content);
lyr.on('click', function() { selectFeature(featId); });
}
});
layers[{{ $layer->id }}] = layerGroup;
@if(in_array((int) $layer->id, $activeLayers))
layerGroup.addTo(map);
@endif
}
})()
@endforeach
@endforeach
// Initialize combined bounds