From a65d4b12f2e9ac2d7795d7f2d9a8ffaf33e1b4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Wed, 29 Jul 2026 01:44:38 +0200 Subject: [PATCH] feat(inspections): add photos support with edit + lightbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validación robusta fotos (10MB, jpeg/png/webp, max 10) - Editar inspección existente: botón en historial, modal y tabla - Eliminación individual de fotos en edición (marcar + guardar) - Lightbox/carousel en modal ver + editar (Alpine.js) - Columna 'Fotos' en InspectionTable con thumbnails + contador - Permiso 'edit inspections' en seeders + checks en componente - Test actualizado: seed roles/permisos + attach template pivot - Fix: removed unsupported filterable() on secondaryHeaderFilter --- app/Livewire/Projects/InspectionTable.php | 34 +++- app/Livewire/Projects/ProjectMap.php | 136 ++++++++++++- database/seeders/PermissionCatalogSeeder.php | 1 + .../seeders/RolesAndPermissionsSeeder.php | 8 +- .../project-map-inspections-tab.blade.php | 12 +- .../livewire/projects/project-map.blade.php | 185 +++++++++++++++++- tests/Feature/InspectionFormTest.php | 11 +- 7 files changed, 374 insertions(+), 13 deletions(-) diff --git a/app/Livewire/Projects/InspectionTable.php b/app/Livewire/Projects/InspectionTable.php index 774d175..1e4a644 100644 --- a/app/Livewire/Projects/InspectionTable.php +++ b/app/Livewire/Projects/InspectionTable.php @@ -45,7 +45,7 @@ class InspectionTable extends DataTableComponent return Inspection::query() ->where('inspections.project_id', $this->projectId) - ->with(['feature', 'template', 'user']); + ->with(['feature', 'template', 'user', 'media']); } public function columns(): array @@ -78,18 +78,48 @@ class InspectionTable extends DataTableComponent ->secondaryHeaderFilter('usuario') ->label(fn ($row) => e($row->user?->name ?? '—')), + Column::make('Fotos') + ->label(fn ($row) => $this->renderPhotosColumn($row)) + ->html(), + Column::make('Acciones') ->label(fn ($row) => - '
+ '
+ @can("edit inspections") + + @endcan
') ->html(), ]; } + private function renderPhotosColumn($row): string + { + $images = $row->media->where('category', 'image')->values(); + $count = $images->count(); + + if ($count === 0) { + return ''; + } + + $thumbnails = $images->take(3)->map(fn ($m) => + ' + ' . e($m->name) . ' + ' + )->implode(''); + + $more = $count > 3 ? '+' . ($count - 3) . '' : ''; + + return '
' . $thumbnails . $more . '
'; + } + public function filters(): array { $results = Inspection::where('project_id', $this->projectId) diff --git a/app/Livewire/Projects/ProjectMap.php b/app/Livewire/Projects/ProjectMap.php index 207b6e4..7d608ab 100644 --- a/app/Livewire/Projects/ProjectMap.php +++ b/app/Livewire/Projects/ProjectMap.php @@ -65,6 +65,14 @@ class ProjectMap extends Component // Inspection viewer public $viewingInspection = null; + // Inspection editor (para editar inspecciones existentes) + public $editingInspection = null; + public $editInspectionFormData = []; + public $editInspectionResult = ''; + public $editInspectionNotes = ''; + public $editInspectionPhotos = []; + public $editInspectionPhotosToDelete = []; + public function mount(Project $project) { $this->project = $project; @@ -271,12 +279,19 @@ class ProjectMap extends Component $this->dispatch('notify', 'Selecciona un elemento y un template.'); return; } + + // Verificar permiso + if (!auth()->user()->can('create inspections')) { + $this->dispatch('notify', 'Sin permisos para crear inspecciones.'); + return; + } + $feature = Feature::with('layer.phase')->find($this->selectedFeature->id); if (!$feature || $feature->layer->phase->project_id !== $this->project->id) abort(403); $this->validate([ 'selectedTemplateId' => 'required|exists:inspection_templates,id', - 'inspectionPhotos.*' => 'nullable|image|max:20480', + 'inspectionPhotos.*' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:10240', ]); $template = InspectionTemplate::find($this->selectedTemplateId); @@ -406,7 +421,7 @@ class ProjectMap extends Component 'notes' => $ins->notes, 'data' => $ins->data ?? [], 'fields' => $ins->template?->fields ?? [], - 'photos' => $ins->media->map(fn ($m) => ['url' => $m->url, 'name' => $m->name])->values()->all(), + 'photos' => $ins->media->map(fn ($m) => ['url' => $m->url, 'name' => $m->name, 'id' => $m->id])->values()->all(), ]; } @@ -415,6 +430,123 @@ class ProjectMap extends Component $this->viewingInspection = null; } + // ─── Inspection Editor (editar inspección existente) ───────────────────────── + + #[On('edit-inspection')] + public function editInspection($id) + { + $ins = Inspection::where('project_id', $this->project->id) + ->with(['feature.layer.phase', 'template', 'user', 'media']) + ->find($id); + if (!$ins) return; + + $this->editingInspection = $ins; + $this->editInspectionFormData = $ins->data ?? []; + $this->editInspectionResult = $ins->result ?? ''; + $this->editInspectionNotes = $ins->notes ?? ''; + $this->editInspectionPhotos = []; + $this->editInspectionPhotosToDelete = []; + $this->selectedTemplateId = $ins->template_id; + + $this->dispatch('openEditInspectionModal'); + } + + public function closeEditInspection() + { + $this->editingInspection = null; + $this->editInspectionFormData = []; + $this->editInspectionResult = ''; + $this->editInspectionNotes = ''; + $this->editInspectionPhotos = []; + $this->editInspectionPhotosToDelete = []; + } + + public function deleteEditPhoto($mediaIndex) + { + if (!$this->editingInspection) return; + $media = $this->editingInspection->media; + if (isset($media[$mediaIndex])) { + $m = $media[$mediaIndex]; + if (!in_array($m->id, $this->editInspectionPhotosToDelete)) { + $this->editInspectionPhotosToDelete[] = $m->id; + } + } + } + + public function saveEditInspection() + { + if (!$this->editingInspection) return; + + // Verificar permiso + if (!auth()->user()->can('edit inspections')) { + $this->dispatch('notify', 'Sin permisos para editar inspecciones.'); + return; + } + + $this->validate([ + 'selectedTemplateId' => 'required|exists:inspection_templates,id', + 'editInspectionPhotos.*' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:10240', + 'editInspectionPhotosToDelete' => 'array', + 'editInspectionPhotosToDelete.*' => 'exists:media,id', + ]); + + $ins = $this->editingInspection; + if ($ins->project_id !== $this->project->id) abort(403); + + $template = InspectionTemplate::find($this->selectedTemplateId); + foreach ($template->fields as $field) { + if (($field['required'] ?? false) && empty($this->editInspectionFormData[$field['name']])) { + $this->dispatch('notify', "El campo {$field['label']} es obligatorio."); + return; + } + } + + // Eliminar fotos marcadas + if (!empty($this->editInspectionPhotosToDelete)) { + $mediaToDelete = Media::whereIn('id', $this->editInspectionPhotosToDelete) + ->where('mediable_type', Inspection::class) + ->where('mediable_id', $ins->id) + ->get(); + foreach ($mediaToDelete as $m) { + $m->delete(); + } + } + + // Actualizar datos + $ins->update([ + 'template_id' => $this->selectedTemplateId, + 'result' => $this->editInspectionResult ?: null, + 'notes' => $this->editInspectionNotes ?: null, + 'data' => $this->editInspectionFormData, + ]); + + // Añadir nuevas fotos + foreach ($this->editInspectionPhotos as $photo) { + $mime = $photo->getMimeType(); + $path = $photo->store("uploads/inspections/{$ins->id}", 'public'); + $ins->media()->create([ + 'name' => $photo->getClientOriginalName(), + 'file_path' => $path, + 'file_type' => $mime, + 'file_extension' => $photo->getClientOriginalExtension(), + 'file_size' => $photo->getSize(), + 'category' => str_starts_with($mime, 'image/') ? 'image' : 'document', + 'uploaded_by' => auth()->id(), + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + ]); + } + + // Refresh + $this->allInspections = Inspection::where('project_id', $this->project->id) + ->with(['feature.layer.phase', 'template', 'user']) + ->orderBy('created_at', 'desc') + ->get(); + $this->loadInspectionHistory(); + + $this->closeEditInspection(); + $this->dispatch('notify', 'Inspección actualizada correctamente'); + } + // ─── Feature images ────────────────────────────────────────────────────────── public function toggleFeatureImages() diff --git a/database/seeders/PermissionCatalogSeeder.php b/database/seeders/PermissionCatalogSeeder.php index f70fc09..1896bf8 100644 --- a/database/seeders/PermissionCatalogSeeder.php +++ b/database/seeders/PermissionCatalogSeeder.php @@ -39,6 +39,7 @@ class PermissionCatalogSeeder extends Seeder 'Inspecciones' => [ 'view inspections' => 'Ver inspecciones e historial', 'create inspections' => 'Registrar inspecciones', + 'edit inspections' => 'Editar inspecciones existentes', 'delete inspections' => 'Eliminar inspecciones', 'manage templates' => 'Gestionar plantillas de inspección', ], diff --git a/database/seeders/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php index b68298c..688037e 100644 --- a/database/seeders/RolesAndPermissionsSeeder.php +++ b/database/seeders/RolesAndPermissionsSeeder.php @@ -17,7 +17,8 @@ class RolesAndPermissionsSeeder extends Seeder // Create permissions $permissions = [ 'view projects', 'create projects', 'edit projects', 'delete projects', - 'assign users', 'upload layers', 'update progress', 'view reports', 'manage all' + 'assign users', 'upload layers', 'update progress', 'view reports', 'manage all', + 'view inspections', 'create inspections', 'edit inspections', 'delete inspections', 'manage templates', ]; foreach ($permissions as $perm) { Permission::firstOrCreate(['name' => $perm]); @@ -28,7 +29,10 @@ class RolesAndPermissionsSeeder extends Seeder $admin->givePermissionTo(Permission::all()); $supervisor = Role::firstOrCreate(['name' => 'Supervisor']); - $supervisor->givePermissionTo(['view projects', 'upload layers', 'update progress']); + $supervisor->givePermissionTo([ + 'view projects', 'upload layers', 'update progress', + 'view inspections', 'create inspections', 'edit inspections', + ]); $consultor = Role::firstOrCreate(['name' => 'Consultor']); $consultor->givePermissionTo(['view projects', 'view reports']); diff --git a/resources/views/livewire/projects/project-map-inspections-tab.blade.php b/resources/views/livewire/projects/project-map-inspections-tab.blade.php index ad4d702..ec93394 100644 --- a/resources/views/livewire/projects/project-map-inspections-tab.blade.php +++ b/resources/views/livewire/projects/project-map-inspections-tab.blade.php @@ -78,8 +78,18 @@ {{ __("by") }} {{ $inspection->user->name }} @endif
-
+
{{ __("View") }} + @can('edit inspections') + + @endcan
diff --git a/resources/views/livewire/projects/project-map.blade.php b/resources/views/livewire/projects/project-map.blade.php index 19b4700..2d013cb 100644 --- a/resources/views/livewire/projects/project-map.blade.php +++ b/resources/views/livewire/projects/project-map.blade.php @@ -374,22 +374,197 @@ @if(!empty($viewingInspection['photos']))
{{ __('Photos') }}
-
- @foreach($viewingInspection['photos'] as $ph) - - {{ $ph['name'] }} - +
+ @foreach($viewingInspection['photos'] as $idx => $ph) + @endforeach
+ + {{-- Lightbox Modal --}} +
+ + + + + + +
+ / {{ count($viewingInspection['photos']) }} +
+
@endif
@endif + + {{-- Modal Editar Inspección --}} + @if($editingInspection) + + @endif diff --git a/tests/Feature/InspectionFormTest.php b/tests/Feature/InspectionFormTest.php index 1be7ae6..d39aa4d 100644 --- a/tests/Feature/InspectionFormTest.php +++ b/tests/Feature/InspectionFormTest.php @@ -24,7 +24,13 @@ class InspectionFormTest extends TestCase { Storage::fake('public'); + // Seed roles and permissions + $this->seed(\Database\Seeders\RolesAndPermissionsSeeder::class); + $this->seed(\Database\Seeders\PermissionCatalogSeeder::class); + $user = User::factory()->create(); + $user->assignRole('Supervisor'); + $project = Project::create([ 'reference' => 'INS', 'name' => 'Proyecto Insp', 'address' => 'x', 'lat' => 40, 'lng' => -3, 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonth()->toDateString(), @@ -45,6 +51,9 @@ class InspectionFormTest extends TestCase ], ]); + // Attach template to project (pivot required for loadTemplates) + $project->inspectionTemplates()->attach($template->id); + Livewire::actingAs($user) ->test(ProjectMap::class, ['project' => $project]) ->call('selectFeature', $feature->id) @@ -60,4 +69,4 @@ class InspectionFormTest extends TestCase $this->assertEquals('5.2', $ins->data['altura']); $this->assertCount(1, $ins->media); } -} +} \ No newline at end of file