feat(inspections): formulario y visor por grupos + comentarios + fotos

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 16:51:42 +02:00
co-authored by Claude Opus 4.8
parent 44f125293b
commit 1697c16136
4 changed files with 183 additions and 39 deletions
+27 -2
View File
@@ -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(),
];
}