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:
@@ -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">×</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 }}">‹</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 }}">›</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">×</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 }}">‹</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 }}">›</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>
|
||||
|
||||
Reference in New Issue
Block a user