feat(inspections): add photos support with edit + lightbox

- 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
This commit is contained in:
Javier Braña
2026-07-29 01:44:38 +02:00
parent 847ba1c2f8
commit a65d4b12f2
7 changed files with 374 additions and 13 deletions
@@ -78,8 +78,18 @@
<span class="text-xs text-gray-400">{{ __("by") }} {{ $inspection->user->name }}</span>
@endif
</div>
<div class="text-center">
<div class="flex items-center gap-1 ml-2">
<span class="badge badge-sm">{{ __("View") }}</span>
@can('edit inspections')
<button
wire:click="$dispatch('edit-inspection', { id: {{ $inspection->id } }})"
class="btn btn-xs btn-ghost btn-circle"
title="{{ __('Edit') }}"
onclick="event.stopPropagation();"
>
<x-heroicon-o-pencil class="w-4 h-4" />
</button>
@endcan
</div>
</div>
</div>
@@ -374,22 +374,197 @@
@if(!empty($viewingInspection['photos']))
<div class="divider text-xs">{{ __('Photos') }}</div>
<div class="grid grid-cols-3 gap-2">
@foreach($viewingInspection['photos'] as $ph)
<a href="{{ $ph['url'] }}" target="_blank">
<img src="{{ $ph['url'] }}" class="w-full h-20 object-cover rounded border border-base-300" alt="{{ $ph['name'] }}" />
</a>
<div class="grid grid-cols-3 gap-2" x-data="{ lightboxOpen: false, lightboxIndex: 0 }">
@foreach($viewingInspection['photos'] as $idx => $ph)
<button
type="button"
@click="lightboxOpen = true; lightboxIndex = {{ $idx }}"
class="relative w-full h-20 overflow-hidden rounded border border-base-300"
>
<img src="{{ $ph['url'] }}" class="w-full h-full object-cover" alt="{{ $ph['name'] }}" />
</button>
@endforeach
</div>
{{-- Lightbox Modal --}}
<div x-show="lightboxOpen" x-transition x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/90">
<button @click="lightboxOpen = false" class="absolute top-4 right-4 text-white/70 hover:text-white text-3xl" aria-label="Close">&times;</button>
<button @click="lightboxIndex = (lightboxIndex - 1 + {{ count($viewingInspection['photos']) }}) % {{ count($viewingInspection['photos']) }}" class="absolute left-4 text-white/70 hover:text-white text-4xl" aria-label="Previous" x-show="{{ count($viewingInspection['photos']) > 1 }}">&#8249;</button>
<button @click="lightboxIndex = (lightboxIndex + 1) % {{ count($viewingInspection['photos']) }}" class="absolute right-4 text-white/70 hover:text-white text-4xl" aria-label="Next" x-show="{{ count($viewingInspection['photos']) > 1 }}">&#8250;</button>
<template x-for="(ph, i) in @js($viewingInspection['photos'])" :key="i">
<img
x-show="lightboxIndex === i"
:src="ph.url"
class="max-h-[80vh] max-w-[90vw] object-contain rounded"
:alt="ph.name"
>
</template>
<div x-show="lightboxIndex !== null" class="absolute bottom-4 left-1/2 -translate-x-1/2 text-white/70 text-sm">
<span x-text="lightboxIndex + 1"></span> / {{ count($viewingInspection['photos']) }}
</div>
</div>
@endif
<div class="modal-action">
<button wire:click="closeViewInspection" class="btn btn-sm">{{ __('Close') }}</button>
@can('edit inspections')
<button
wire:click="$dispatch('edit-inspection', { id: {{ $viewingInspection['id'] }} })"
wire:loading.attr="disabled"
class="btn btn-primary btn-sm"
>
<x-heroicon-o-pencil class="w-4 h-4 mr-1" /> {{ __('Edit') }}
</button>
@endcan
</div>
</div>
<div class="modal-backdrop bg-black/40" wire:click="closeViewInspection"></div>
</div>
@endif
{{-- Modal Editar Inspección --}}
@if($editingInspection)
<div class="modal modal-open z-[2000]" wire:key="edit-inspection-{{ $editingInspection->id }}">
<div class="modal-box max-w-2xl max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-start mb-3">
<h3 class="font-bold text-lg">{{ __('Edit Inspection') }} #{{ $editingInspection->id }}</h3>
<button wire:click="closeEditInspection" 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> {{ $editingInspection->feature?->name ?? '—' }}</div>
<div><span class="text-gray-500">{{ __('Template') }}:</span> {{ $editingInspection->template?->name ?? '—' }}</div>
<div><span class="text-gray-500">{{ __('Phase') }}:</span> {{ $editingInspection->feature?->layer?->phase?->name ?? '—' }}</div>
<div><span class="text-gray-500">{{ __('Layer') }}:</span> {{ $editingInspection->feature?->layer?->name ?? '—' }}</div>
</div>
@if($editingInspection->template && !empty($editingInspection->template->fields))
<div class="divider text-xs">{{ __('Data') }}</div>
@php $eGrouped = collect($editingInspection->template->fields)->groupBy(fn ($f) => trim($f['group'] ?? '') !== '' ? $f['group'] : __('General')); @endphp
@foreach($eGrouped as $gName => $gFields)
<div class="mb-3">
<div class="text-[11px] font-semibold uppercase tracking-wide text-base-content/50 mb-1">{{ $gName }}</div>
<div class="space-y-1 text-sm">
@foreach($gFields as $field)
<div class="mb-2">
<label class="label-text text-xs font-medium">
{{ ($field['question'] ?? '') ?: $field['label'] }}
@if($field['required'] ?? false)<span class="text-error">*</span>@endif
</label>
@switch($field['type'] ?? 'text')
@case('percentage')
<div class="flex items-center gap-1">
<input type="number" wire:model="editInspectionFormData.{{ $field['name'] }}" min="0" max="100" class="input input-bordered input-sm w-16" />
<span class="text-xs">%</span>
<input type="range" min="0" max="100" wire:model.live="editInspectionFormData.{{ $field['name'] }}" class="range range-primary range-xs flex-1" />
</div>
@break
@case('boolean')
<input type="checkbox" wire:model="editInspectionFormData.{{ $field['name'] }}" class="checkbox checkbox-sm" />
@break
@case('select')
<select wire:model="editInspectionFormData.{{ $field['name'] }}" class="select select-bordered select-sm w-full">
<option value="">{{ __('Select') }}</option>
@foreach(explode(',', $field['options'] ?? '') as $opt)
<option value="{{ trim($opt) }}">{{ trim($opt) }}</option>
@endforeach
</select>
@break
@case('textarea')
<textarea wire:model="editInspectionFormData.{{ $field['name'] }}" rows="2" class="textarea textarea-bordered textarea-sm w-full"></textarea>
@break
@default
<input type="{{ $field['type'] ?? 'text' }}" wire:model="editInspectionFormData.{{ $field['name'] }}" class="input input-bordered input-sm w-full" />
@endswitch
@if(!empty($field['help']))
<div class="text-[11px] text-base-content/50 mt-0.5">{{ $field['help'] }}</div>
@endif
</div>
@endforeach
</div>
</div>
@endforeach
@endif
<div class="border-t border-base-300 pt-2 mt-2 space-y-2">
<div>
<label class="label-text text-xs font-medium">{{ __('Result') }}</label>
<select wire:model="editInspectionResult" class="select select-bordered select-sm w-full">
<option value="">{{ __('Select result') }}</option>
<option value="pass">{{ __('Pass') }}</option>
<option value="fail">{{ __('Fail') }}</option>
<option value="conditional">{{ __('Conditional') }}</option>
</select>
</div>
<div>
<label class="label-text text-xs font-medium">{{ __('Comments') }}</label>
<textarea wire:model="editInspectionNotes" rows="2" class="textarea textarea-bordered textarea-sm w-full" placeholder="{{ __('General comments...') }}"></textarea>
</div>
<div>
<label class="label-text text-xs font-medium">{{ __('Photos') }}</label>
<input type="file" wire:model="editInspectionPhotos" multiple accept="image/*" class="file-input file-input-bordered file-input-sm w-full" />
<div wire:loading wire:target="editInspectionPhotos" class="text-[11px] text-base-content/50">{{ __('Uploading…') }}</div>
@error('editInspectionPhotos.*')<div class="text-[11px] text-error">{{ $message }}</div>@enderror
@if($editInspectionPhotos)<div class="text-[11px] text-base-content/60 mt-0.5">{{ count($editInspectionPhotos) }} {{ __('photo(s) ready') }}</div>@endif
</div>
</div>
{{-- Fotos existentes --}}
@if($editingInspection->media->isNotEmpty())
<div class="divider text-xs">{{ __('Current photos') }}</div>
<div class="grid grid-cols-3 gap-2" x-data="{ lightboxOpen: false, lightboxIndex: 0 }">
@foreach($editingInspection->media as $idx => $m)
<div class="relative border border-base-300 rounded">
<button
type="button"
@click="lightboxOpen = true; lightboxIndex = {{ $idx }}"
class="w-full h-20 overflow-hidden rounded-t"
>
<img src="{{ $m->url }}" class="w-full h-full object-cover" alt="{{ $m->name }}" />
</button>
<div class="p-1 flex justify-between">
<span class="text-[10px] truncate">{{ $m->name }}</span>
<button wire:click="deleteEditPhoto({{ $idx }})" class="btn btn-xs btn-error btn-circle" title="{{ __('Delete photo') }}">
<x-heroicon-o-trash class="w-3 h-3" />
</button>
</div>
</div>
@endforeach
</div>
{{-- Lightbox Modal --}}
<div x-show="lightboxOpen" x-transition x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/90">
<button @click="lightboxOpen = false" class="absolute top-4 right-4 text-white/70 hover:text-white text-3xl" aria-label="Close">&times;</button>
<button @click="lightboxIndex = (lightboxIndex - 1 + {{ $editingInspection->media->count() }}) % {{ $editingInspection->media->count() }}" class="absolute left-4 text-white/70 hover:text-white text-4xl" aria-label="Previous" x-show="{{ $editingInspection->media->count() > 1 }}">&#8249;</button>
<button @click="lightboxIndex = (lightboxIndex + 1) % {{ $editingInspection->media->count() }}" class="absolute right-4 text-white/70 hover:text-white text-4xl" aria-label="Next" x-show="{{ $editingInspection->media->count() > 1 }}">&#8250;</button>
<template x-for="(m, i) in @js($editingInspection->media)" :key="i">
<img
x-show="lightboxIndex === i"
:src="m.url"
class="max-h-[80vh] max-w-[90vw] object-contain rounded"
:alt="m.name"
>
</template>
<div x-show="lightboxIndex !== null" class="absolute bottom-4 left-1/2 -translate-x-1/2 text-white/70 text-sm">
<span x-text="lightboxIndex + 1"></span> / {{ $editingInspection->media->count() }}
</div>
</div>
@endif
<div class="modal-action mt-3">
<button wire:click="closeEditInspection" class="btn btn-sm">{{ __('Cancel') }}</button>
<button wire:click="saveEditInspection" class="btn btn-primary btn-sm" wire:loading.attr="disabled" wire:target="saveEditInspection,editInspectionPhotos">{{ __('Save changes') }}</button>
</div>
</div>
<div class="modal-backdrop bg-black/40" wire:click="closeEditInspection"></div>
</div>
@endif
</div>
</div>
</div>