From 1697c161360e5802dd87e70f906c930558c8a0ca Mon Sep 17 00:00:00 2001 From: javier Date: Thu, 25 Jun 2026 16:51:42 +0200 Subject: [PATCH] feat(inspections): formulario y visor por grupos + comentarios + fotos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parte 2 del bloque de plantillas/inspecciones: - Formulario de inspección (mapa): campos agrupados por sección, muestra la "pregunta" del campo (con la etiqueta como subtexto) y la ayuda; al final, apartado de Comentarios (notes) y subida de Fotos (media en la inspección). - ProjectMap: WithFileUploads + inspectionPhotos; saveInspection adjunta las fotos como media; Inspection gana relación media(). - Visor de inspección: datos agrupados por sección con pregunta/valor, comentarios y galería de fotos. Tests: InspectionFormTest (registra inspección con comentarios + foto). Suite 91 passing. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Livewire/Projects/ProjectMap.php | 29 +++- app/Models/Inspection.php | 5 + .../livewire/projects/project-map.blade.php | 125 ++++++++++++------ tests/Feature/InspectionFormTest.php | 63 +++++++++ 4 files changed, 183 insertions(+), 39 deletions(-) create mode 100644 tests/Feature/InspectionFormTest.php diff --git a/app/Livewire/Projects/ProjectMap.php b/app/Livewire/Projects/ProjectMap.php index dd6bfb3..ef843a5 100644 --- a/app/Livewire/Projects/ProjectMap.php +++ b/app/Livewire/Projects/ProjectMap.php @@ -4,6 +4,7 @@ namespace App\Livewire\Projects; use Livewire\Component; use Livewire\Attributes\On; +use Livewire\WithFileUploads; use Illuminate\Support\Facades\Auth; use App\Models\Project; use App\Models\Phase; @@ -15,6 +16,8 @@ use App\Models\Issue; class ProjectMap extends Component { + use WithFileUploads; + public Project $project; public $phases; public $activeLayers = []; // Now stores Layer IDs (not Phase IDs) @@ -54,6 +57,7 @@ class ProjectMap extends Component // Inspection workflow public $inspectionResult = ''; public $inspectionNotes = ''; + public $inspectionPhotos = []; // Issues public $openIssuesCount = 0; @@ -247,6 +251,7 @@ class ProjectMap extends Component $this->inspectionFormData = []; $this->inspectionResult = ''; $this->inspectionNotes = ''; + $this->inspectionPhotos = []; if ($this->selectedTemplateId) { $template = InspectionTemplate::find($this->selectedTemplateId); if ($template) { @@ -266,7 +271,10 @@ class ProjectMap extends Component $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']); + $this->validate([ + 'selectedTemplateId' => 'required|exists:inspection_templates,id', + 'inspectionPhotos.*' => 'nullable|image|max:20480', + ]); $template = InspectionTemplate::find($this->selectedTemplateId); foreach ($template->fields as $field) { @@ -290,6 +298,22 @@ class ProjectMap extends Component 'data' => $this->inspectionFormData, ]); + // Fotos adjuntas a la inspección + foreach ($this->inspectionPhotos as $photo) { + $mime = $photo->getMimeType(); + $path = $photo->store("uploads/inspections/{$inspection->id}", 'public'); + $inspection->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(), + ]); + } + if ($this->inspectionResult === 'fail') { Issue::create([ 'project_id' => $this->project->id, @@ -363,7 +387,7 @@ class ProjectMap extends Component public function viewInspection($id) { $ins = Inspection::where('project_id', $this->project->id) - ->with(['feature.layer.phase', 'template', 'user']) + ->with(['feature.layer.phase', 'template', 'user', 'media']) ->find($id); if (!$ins) return; $this->viewingInspection = [ @@ -379,6 +403,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(), ]; } diff --git a/app/Models/Inspection.php b/app/Models/Inspection.php index 76b735c..d04d3d7 100644 --- a/app/Models/Inspection.php +++ b/app/Models/Inspection.php @@ -49,6 +49,11 @@ class Inspection extends Model return $this->belongsTo(User::class, 'inspector_user_id'); } + public function media() + { + return $this->morphMany(Media::class, 'mediable'); + } + public function feature() { return $this->belongsTo(Feature::class, 'feature_id'); diff --git a/resources/views/livewire/projects/project-map.blade.php b/resources/views/livewire/projects/project-map.blade.php index 15fcc21..19b4700 100644 --- a/resources/views/livewire/projects/project-map.blade.php +++ b/resources/views/livewire/projects/project-map.blade.php @@ -214,37 +214,68 @@ @if($selectedTemplateId && !empty($inspectionFormData)) @php $template = $templates->firstWhere('id', $selectedTemplateId); @endphp @if($template) - @foreach($template->fields as $field) -
- - @switch($field['type'] ?? 'text') - @case('percentage') -
- - % - -
- @break - @case('boolean') - - @break - @case('select') - - @break - @case('textarea') - - @break - @default - - @endswitch + @php $grouped = collect($template->fields)->groupBy(fn ($f) => trim($f['group'] ?? '') !== '' ? $f['group'] : __('General')); @endphp + @foreach($grouped as $groupName => $groupFields) +
+
{{ $groupName }}
+ @foreach($groupFields as $field) +
+ + @if(!empty($field['question']) && !empty($field['label']) && $field['question'] !== $field['label']) +
{{ $field['label'] }}
+ @endif + @switch($field['type'] ?? 'text') + @case('percentage') +
+ + % + +
+ @break + @case('boolean') + + @break + @case('select') + + @break + @case('textarea') + + @break + @default + + @endswitch + @if(!empty($field['help'])) +
{{ $field['help'] }}
+ @endif +
+ @endforeach
@endforeach - + + {{-- Comentarios + fotos de la inspección --}} +
+
+ + +
+
+ + +
{{ __('Uploading…') }}
+ @error('inspectionPhotos.*')
{{ $message }}
@enderror + @if($inspectionPhotos)
{{ count($inspectionPhotos) }} {{ __('photo(s) ready') }}
@endif +
+
+ + @endif @endif @@ -317,21 +348,41 @@ @if(!empty($viewingInspection['fields']))
{{ __('Data') }}
-
- @foreach($viewingInspection['fields'] as $field) -
- {{ $field['label'] ?? ($field['name'] ?? '') }} - {{ $viewingInspection['data'][$field['name']] ?? '—' }} + @php $vGrouped = collect($viewingInspection['fields'])->groupBy(fn ($f) => trim($f['group'] ?? '') !== '' ? $f['group'] : __('General')); @endphp + @foreach($vGrouped as $gName => $gFields) +
+
{{ $gName }}
+
+ @foreach($gFields as $field) + @php $val = $viewingInspection['data'][$field['name']] ?? null; @endphp +
+ {{ ($field['question'] ?? '') ?: ($field['label'] ?? ($field['name'] ?? '')) }} + + @if(($field['type'] ?? '') === 'boolean'){{ $val ? '✓' : '—' }}@else{{ ($val === '' || $val === null) ? '—' : $val }}@endif + +
+ @endforeach
- @endforeach -
+
+ @endforeach @endif @if(!empty($viewingInspection['notes'])) -
{{ __('Notes') }}
+
{{ __('Comments') }}

{{ $viewingInspection['notes'] }}

@endif + @if(!empty($viewingInspection['photos'])) +
{{ __('Photos') }}
+
+ @foreach($viewingInspection['photos'] as $ph) + + {{ $ph['name'] }} + + @endforeach +
+ @endif + diff --git a/tests/Feature/InspectionFormTest.php b/tests/Feature/InspectionFormTest.php new file mode 100644 index 0000000..1be7ae6 --- /dev/null +++ b/tests/Feature/InspectionFormTest.php @@ -0,0 +1,63 @@ +create(); + $project = Project::create([ + 'reference' => 'INS', 'name' => 'Proyecto Insp', 'address' => 'x', 'lat' => 40, 'lng' => -3, + 'start_date' => now()->toDateString(), 'end_date_estimated' => now()->addMonth()->toDateString(), + 'status' => 'in_progress', 'created_by' => $user->id, + ]); + $project->users()->attach($user->id, ['role_in_project' => 'supervisor']); + + $phase = Phase::create(['project_id' => $project->id, 'name' => 'F1', 'order' => 1, 'color' => '#000', 'progress_percent' => 0]); + $layer = Layer::create(['project_id' => $project->id, 'phase_id' => $phase->id, 'name' => 'L', 'color' => '#111', 'uploaded_by' => $user->id]); + $feature = Feature::create([ + 'layer_id' => $layer->id, 'name' => 'Pilar', 'geometry' => ['type' => 'Point', 'coordinates' => [-3, 40]], + 'progress' => 0, 'status' => 'planned', + ]); + $template = InspectionTemplate::create([ + 'project_id' => $project->id, 'name' => 'Recepción', + 'fields' => [ + ['group' => 'Geometría', 'name' => 'altura', 'label' => 'Altura', 'question' => '¿Cota OK?', 'type' => 'text', 'required' => false, 'help' => 'Medir'], + ], + ]); + + Livewire::actingAs($user) + ->test(ProjectMap::class, ['project' => $project]) + ->call('selectFeature', $feature->id) + ->set('selectedTemplateId', $template->id) + ->set('inspectionFormData.altura', '5.2') + ->set('inspectionNotes', 'Todo conforme') + ->set('inspectionPhotos', [UploadedFile::fake()->image('foto.jpg')]) + ->call('saveInspection'); + + $ins = Inspection::where('feature_id', $feature->id)->first(); + $this->assertNotNull($ins); + $this->assertEquals('Todo conforme', $ins->notes); + $this->assertEquals('5.2', $ins->data['altura']); + $this->assertCount(1, $ins->media); + } +}