feat(project-map): edit tab redesign, table cleanup, inspection viewer, base layers
1.1 Edit tab: in fullscreen the content splits into 2 columns (Alpine :class)
1.2 Full-width feature title with progress as a number badge on the left;
removed the progress slider and the 'Save progress' button; Responsible
now auto-saves on blur (wire:blur)
2. Features table: zebra/pinned-rows styling, progress badge; removed the
Responsible and Template columns
3. Inspections table: same styling; wired the eye button to viewInspection()
and added the inspection viewer modal (uses existing component state)
6a. Phases/layers collapse button moved into the panel title; a small floating
button reopens it when collapsed (saves space)
6b. Base-layer switcher on the map: Streets / OpenStreetMap / Satellite (Esri)
Issues tab (point 5) intentionally left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,18 +4,22 @@
|
||||
<div x-show="!formFullscreen" x-cloak x-data="{ showLayers: true }" class="w-full lg:w-2/3 flex-1 relative">
|
||||
<div id="map" style="height: 100%; min-height: 500px; width: 100%;" wire:ignore></div>
|
||||
|
||||
<!-- Toggle del panel de fases/capas -->
|
||||
<button @click="showLayers = !showLayers"
|
||||
<!-- Botón para reabrir el panel (solo cuando está colapsado) -->
|
||||
<button x-show="!showLayers" x-cloak @click="showLayers = true"
|
||||
class="absolute top-2 right-2 z-[1001] btn btn-sm btn-circle shadow-lg"
|
||||
title="{{ __('Show/hide panel') }}">
|
||||
<x-heroicon-o-x-mark class="w-5 h-5" x-show="showLayers" />
|
||||
<x-heroicon-o-bars-3 class="w-5 h-5" x-show="!showLayers" x-cloak />
|
||||
<x-heroicon-o-bars-3 class="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<!-- Panel lateral de capas -->
|
||||
<div x-show="showLayers" x-transition
|
||||
class="absolute top-14 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>
|
||||
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">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-semibold text-base">{{ __('Phases and layers') }}</h3>
|
||||
<button @click="showLayers = false" class="btn btn-xs btn-circle btn-ghost" title="{{ __('Show/hide panel') }}">
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
</button>
|
||||
</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' : '' }}">
|
||||
@@ -134,30 +138,23 @@
|
||||
<div class="mt-2">
|
||||
@if($activeTab === 'edit')
|
||||
@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>
|
||||
</div>
|
||||
|
||||
{{-- Progreso --}}
|
||||
<div class="form-control mb-3">
|
||||
<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>
|
||||
{{-- Título a todo el ancho: progreso (solo número) a la izquierda + nombre --}}
|
||||
<div class="flex items-center gap-3 mb-4 pb-2 border-b border-base-300">
|
||||
<span class="badge badge-lg shrink-0 {{ $editProgress >= 100 ? 'badge-success' : ($editProgress > 0 ? 'badge-warning' : 'badge-ghost') }}">{{ $editProgress }}%</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-base truncate">{{ $selectedFeature->name ?? __('Feature') }}</h3>
|
||||
<p class="text-xs text-gray-500 truncate">{{ __('Phase') }}: {{ $selectedFeature->layer?->phase?->name ?? '—' }} · {{ __('Layer') }}: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
</div>
|
||||
</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') }}" />
|
||||
</div>
|
||||
|
||||
<button wire:click="saveFeatureProgress" class="btn btn-primary btn-sm w-full mb-3 gap-1">
|
||||
<x-heroicon-o-check-circle class="w-4 h-4" /> {{ __('Save progress') }}
|
||||
</button>
|
||||
{{-- En pantalla completa el contenido se reparte en columnas --}}
|
||||
<div :class="formFullscreen ? 'grid grid-cols-1 lg:grid-cols-2 gap-x-8 items-start' : ''">
|
||||
<div>
|
||||
{{-- Responsable (se guarda al salir del campo) --}}
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">{{ __('Responsible') }}</label>
|
||||
<input type="text" wire:model="editResponsible" wire:blur="saveFeatureProgress" class="input input-bordered input-sm" placeholder="{{ __('Name of responsible') }}" />
|
||||
</div>
|
||||
|
||||
{{-- Gestor de archivos del feature --}}
|
||||
<details class="mb-3 border rounded-lg">
|
||||
@@ -171,7 +168,9 @@
|
||||
], key('media-feature-' . $selectedFeature->id))
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{-- Templates / Inspecciones --}}
|
||||
@if($templates->isNotEmpty())
|
||||
<div class="divider text-xs">{{ __('Inspection') }}</div>
|
||||
@@ -249,6 +248,8 @@
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __('Create') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<x-heroicon-o-cursor-arrow-rays class="w-10 h-10 mx-auto opacity-30" />
|
||||
@@ -258,30 +259,28 @@
|
||||
@elseif($activeTab === 'features')
|
||||
<!-- Features Table -->
|
||||
@if($allFeatures->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm table-compact">
|
||||
<div class="overflow-x-auto rounded-lg border border-base-300">
|
||||
<table class="table table-sm table-zebra table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Feature') }}</th>
|
||||
<th>{{ __('Layer') }}</th>
|
||||
<th>{{ __('Phase') }}</th>
|
||||
<th>{{ __('Progress') }}</th>
|
||||
<th>{{ __('Responsible') }}</th>
|
||||
<th>{{ __('Template') }}</th>
|
||||
<th class="w-16"></th>
|
||||
<th class="text-center">{{ __('Progress') }}</th>
|
||||
<th class="w-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($allFeatures as $feature)
|
||||
<tr class="hover" wire:click="selectFeature({{ $feature->id }})">
|
||||
<td>{{ $feature->name }}</td>
|
||||
<tr class="hover cursor-pointer" wire:click="selectFeature({{ $feature->id }})" wire:key="feat-{{ $feature->id }}">
|
||||
<td class="font-medium">{{ $feature->name }}</td>
|
||||
<td>{{ $feature->layer?->name ?? '—' }}</td>
|
||||
<td>{{ $feature->layer?->phase?->name ?? '—' }}</td>
|
||||
<td>{{ $feature->progress }}%</td>
|
||||
<td>{{ $feature->responsible ?? '-' }}</td>
|
||||
<td>{{ $feature->template?->name ?? '-' }}</td>
|
||||
<td class="justify-end">
|
||||
<button class="btn btn-xs btn-outline btn-primary"><x-heroicon-o-pencil class="w-3.5 h-3.5" /></button>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-sm {{ $feature->progress >= 100 ? 'badge-success' : ($feature->progress > 0 ? 'badge-warning' : 'badge-ghost') }}">{{ $feature->progress }}%</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<x-heroicon-o-chevron-right class="w-4 h-4 opacity-40" />
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -297,26 +296,28 @@
|
||||
@elseif($activeTab === 'inspections')
|
||||
<!-- Inspections Table -->
|
||||
@if($allInspections->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm table-compact">
|
||||
<div class="overflow-x-auto rounded-lg border border-base-300">
|
||||
<table class="table table-sm table-zebra table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Date') }}</th>
|
||||
<th>{{ __('Feature') }}</th>
|
||||
<th>{{ __('Template') }}</th>
|
||||
<th>{{ __('User') }}</th>
|
||||
<th class="w-16"></th>
|
||||
<th class="w-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($allInspections as $inspection)
|
||||
<tr>
|
||||
<td>{{ $inspection->created_at?->format('d/m/Y') ?? '—' }}</td>
|
||||
<td>{{ $inspection->feature?->name ?? '—' }}</td>
|
||||
<tr class="hover" wire:key="insp-{{ $inspection->id }}">
|
||||
<td class="whitespace-nowrap">{{ $inspection->created_at?->format('d/m/Y') ?? '—' }}</td>
|
||||
<td class="font-medium">{{ $inspection->feature?->name ?? '—' }}</td>
|
||||
<td>{{ $inspection->template?->name ?? '—' }}</td>
|
||||
<td>{{ $inspection->user?->name ?? '—' }}</td>
|
||||
<td class="justify-end">
|
||||
<button class="btn btn-xs btn-outline btn-info"><x-heroicon-o-eye class="w-3.5 h-3.5" /></button>
|
||||
<td class="text-right">
|
||||
<button wire:click="viewInspection({{ $inspection->id }})" class="btn btn-xs btn-ghost" title="{{ __('View') }}">
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -334,6 +335,51 @@
|
||||
@livewire('issue-manager', ['project' => $project], key('issues-tab-' . $project->id))
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Visor de inspección --}}
|
||||
@if($viewingInspection)
|
||||
<div class="modal modal-open" wire:key="ins-viewer-{{ $viewingInspection['id'] }}">
|
||||
<div class="modal-box max-w-lg">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h3 class="font-bold text-lg">{{ __('Inspection') }} #{{ $viewingInspection['id'] }}</h3>
|
||||
<button wire:click="closeViewInspection" class="btn btn-sm btn-circle btn-ghost">
|
||||
<x-heroicon-o-x-mark class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 text-sm mb-2">
|
||||
<div><span class="text-gray-500">{{ __('Feature') }}:</span> {{ $viewingInspection['feature_name'] }}</div>
|
||||
<div><span class="text-gray-500">{{ __('Template') }}:</span> {{ $viewingInspection['template_name'] }}</div>
|
||||
<div><span class="text-gray-500">{{ __('Phase') }}:</span> {{ $viewingInspection['phase_name'] }}</div>
|
||||
<div><span class="text-gray-500">{{ __('Layer') }}:</span> {{ $viewingInspection['layer_name'] }}</div>
|
||||
<div><span class="text-gray-500">{{ __('User') }}:</span> {{ $viewingInspection['user_name'] }}</div>
|
||||
<div><span class="text-gray-500">{{ __('Date') }}:</span> {{ $viewingInspection['date'] }}</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($viewingInspection['fields']))
|
||||
<div class="divider text-xs">{{ __('Data') }}</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
@foreach($viewingInspection['fields'] as $field)
|
||||
<div class="flex justify-between gap-3 border-b border-base-200 py-1">
|
||||
<span class="text-gray-500">{{ $field['label'] ?? ($field['name'] ?? '') }}</span>
|
||||
<span class="font-medium text-right">{{ $viewingInspection['data'][$field['name']] ?? '—' }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($viewingInspection['notes']))
|
||||
<div class="divider text-xs">{{ __('Notes') }}</div>
|
||||
<p class="text-sm whitespace-pre-line">{{ $viewingInspection['notes'] }}</p>
|
||||
@endif
|
||||
|
||||
<div class="modal-action">
|
||||
<button wire:click="closeViewInspection" class="btn btn-sm">{{ __('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop bg-black/40" wire:click="closeViewInspection"></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -382,9 +428,20 @@
|
||||
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
||||
map = L.map('map').setView(center, 16);
|
||||
|
||||
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);
|
||||
// Capas base seleccionables (calles / OSM / satélite)
|
||||
const baseLayers = {
|
||||
'{{ __('Streets') }}': 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'
|
||||
}),
|
||||
'OpenStreetMap': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}),
|
||||
'{{ __('Satellite') }}': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics'
|
||||
}),
|
||||
};
|
||||
baseLayers['{{ __('Streets') }}'].addTo(map);
|
||||
L.control.layers(baseLayers, null, { position: 'topleft' }).addTo(map);
|
||||
|
||||
// Cargar fases y sus features
|
||||
@foreach($phases as $phase)
|
||||
|
||||
Reference in New Issue
Block a user