From 51c07ede385a2e427503f130d9d055dbf2fb563f Mon Sep 17 00:00:00 2001 From: javier Date: Thu, 25 Jun 2026 18:24:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(templates):=20tabla=20Rappasoft=20con=20fi?= =?UTF-8?q?ltros=20para=20el=20cat=C3=A1logo=20global?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nueva InspectionTemplatesTable (DataTableComponent): columnas Plantilla, Descripción, Campos, Proyectos (uso) y Acciones; filtros en cabecera por nombre, descripción y uso (en uso / sin uso). Gateada por manage templates. - GlobalTemplateManager: el listado HTML se sustituye por la tabla embebida. Acciones Editar/Borrar viajan por eventos (template-edit/template-delete); el manager emite templates-changed al guardar/borrar/importar para que la tabla se refresque (#[On('templates-changed')]). Tests: GlobalTemplatesTest amplía con tabla (render + permiso, evento edit). Suite 91 passing. Co-Authored-By: Claude Opus 4.7 --- .../Inspections/GlobalTemplateManager.php | 6 + .../Inspections/InspectionTemplatesTable.php | 112 +++++++ .../global-template-manager.blade.php | 305 ++++++++---------- routes/web.php | 1 + tests/Feature/GlobalTemplatesTest.php | 37 +++ 5 files changed, 297 insertions(+), 164 deletions(-) create mode 100644 app/Livewire/Inspections/InspectionTemplatesTable.php diff --git a/app/Livewire/Inspections/GlobalTemplateManager.php b/app/Livewire/Inspections/GlobalTemplateManager.php index ff9e761..a22bc1d 100644 --- a/app/Livewire/Inspections/GlobalTemplateManager.php +++ b/app/Livewire/Inspections/GlobalTemplateManager.php @@ -5,6 +5,7 @@ namespace App\Livewire\Inspections; use App\Models\InspectionTemplate; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\Layout; +use Livewire\Attributes\On; use Livewire\Component; use Livewire\WithFileUploads; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -60,6 +61,7 @@ class GlobalTemplateManager extends Component $this->showForm = true; } + #[On('template-edit')] public function editTemplate($id) { $template = InspectionTemplate::findOrFail($id); @@ -135,12 +137,15 @@ class GlobalTemplateManager extends Component $this->cancelForm(); $this->loadTemplates(); + $this->dispatch('templates-changed'); } + #[On('template-delete')] public function deleteTemplate($id) { InspectionTemplate::findOrFail($id)->delete(); $this->loadTemplates(); + $this->dispatch('templates-changed'); $this->dispatch('notify', 'Plantilla eliminada'); } @@ -203,6 +208,7 @@ class GlobalTemplateManager extends Component $this->importTemplateName = ''; $this->importFile = null; $this->loadTemplates(); + $this->dispatch('templates-changed'); $this->dispatch('notify', 'Plantilla importada'); } diff --git a/app/Livewire/Inspections/InspectionTemplatesTable.php b/app/Livewire/Inspections/InspectionTemplatesTable.php new file mode 100644 index 0000000..6dadec3 --- /dev/null +++ b/app/Livewire/Inspections/InspectionTemplatesTable.php @@ -0,0 +1,112 @@ +setPrimaryKey('id') + ->setDefaultSort('inspection_templates.name', 'asc') + ->setSortingPillsEnabled(false) + ->setSecondaryHeaderEnabled() + ->setAdditionalSelects([ + 'inspection_templates.id as id', + 'inspection_templates.fields as fields', + ]); + } + + /** Refrescar cuando el manager crea/edita/borra. */ + #[On('templates-changed')] + public function refreshRows(): void + { + // + } + + public function builder(): Builder + { + abort_unless(Auth::user()->can('manage templates'), 403); + + return InspectionTemplate::query() + ->withCount('projects'); + } + + public function columns(): array + { + return [ + Column::make('Plantilla', 'name') + ->sortable()->searchable() + ->secondaryHeaderFilter('name') + ->format(fn ($value) => '' . e($value) . '') + ->html(), + + Column::make('Descripción', 'description') + ->searchable() + ->secondaryHeaderFilter('description') + ->format(fn ($value) => $value + ? '' . e($value) . '' + : '') + ->html(), + + Column::make('Campos') + ->label(fn ($row) => + '' . count($row->fields ?? []) . '') + ->html(), + + Column::make('Proyectos') + ->secondaryHeaderFilter('usage') + ->label(function ($row) { + $n = (int) ($row->projects_count ?? 0); + $cls = $n > 0 ? 'badge-info' : 'badge-ghost'; + return '' . $n . ''; + }) + ->html(), + + Column::make('Acciones') + ->label(fn ($row) => + '
+ + +
') + ->html(), + ]; + } + + public function filters(): array + { + return [ + TextFilter::make('Plantilla', 'name') + ->config(['placeholder' => 'Buscar nombre…']) + ->filter(fn (Builder $q, string $v) => $q->where('inspection_templates.name', 'like', '%' . $v . '%')), + + TextFilter::make('Descripción', 'description') + ->config(['placeholder' => 'Buscar descripción…']) + ->filter(fn (Builder $q, string $v) => $q->where('inspection_templates.description', 'like', '%' . $v . '%')), + + SelectFilter::make('Uso', 'usage') + ->options(['' => 'Todos', 'used' => 'En uso (≥1 proyecto)', 'unused' => 'Sin uso']) + ->filter(function (Builder $q, string $v) { + if ($v === 'used') $q->has('projects'); + if ($v === 'unused') $q->doesntHave('projects'); + }), + ]; + } +} diff --git a/resources/views/livewire/inspections/global-template-manager.blade.php b/resources/views/livewire/inspections/global-template-manager.blade.php index 30477d0..8b12103 100644 --- a/resources/views/livewire/inspections/global-template-manager.blade.php +++ b/resources/views/livewire/inspections/global-template-manager.blade.php @@ -1,11 +1,12 @@ -
-
+
+ + {{-- ── Cabecera ─────────────────────────────────────────────────────────── --}}

📋 {{ __('Inspection templates') }}

{{ __('Global catalogue, reusable across all projects.') }}

-
+
+ - @if($showForm) -
- - - - - - - - - - - -
{{ __('Template name') }}
{{ __('Description') }}
+
+
+
+ @if($showForm) + + + + + + + + + + + + +
{{ __('Template name') }}
{{ __('Description') }}
-
-

{{ __('Form fields') }}

- @foreach($form['fields'] as $index => $field) -
-
-
{{ __('Group / section') }}
-
-
-
-
{{ __('Internal name') }}
-
-
-
-
{{ __('Visible label') }}
-
-
-
-
{{ __('Question / check') }}
-
-
-
-
{{ __('Field type') }}
-
- -
-
-
-
{{ __('Required') }}
-
- - -
-
- @if(in_array($field['type'], ['integer', 'decimal', 'percentage'])) -
-
{{ __('Min') }} / {{ __('Max') }} / {{ __('Step') }}
-
- - - +
+

{{ __('Form fields') }}

+ @foreach($form['fields'] as $index => $field) +
+
+
{{ __('Group / section') }}
+
+
+
+
{{ __('Internal name') }}
+
+
+
+
{{ __('Visible label') }}
+
+
+
+
{{ __('Question / check') }}
+
+
+
+
{{ __('Field type') }}
+
+ +
+
+
+
{{ __('Required') }}
+
+ + +
+
+ @if(in_array($field['type'], ['integer', 'decimal', 'percentage'])) +
+
{{ __('Min') }} / {{ __('Max') }} / {{ __('Step') }}
+
+ + + +
+
+ @elseif($field['type'] === 'select') +
+
{{ __('Options (comma separated)') }}
+
+
+ @endif + +
+
{{ __('Comments / help') }}
+
- @elseif($field['type'] === 'select') -
-
{{ __('Options (comma separated)') }}
-
+ @endforeach + +
+ +
+ + +
+ + @endif + + {{-- Tabla Rappasoft (filtros en cabecera: nombre, descripción, uso) --}} + +
+ + {{-- MODAL: Importar desde CSV/Excel --}} + @if($showImportFileModal) +
+
+
+
+

{{ __('Import from CSV/Excel') }}

+ +
+
+

+ {{ __('Columns: name, label, type, required, options, min, max, step') }}. + +

+
+ + + @error('importTemplateName'){{ $message }}@enderror +
+
+ + + @error('importFile'){{ $message }}@enderror +
+ @if($importError)
{{ $importError }}
@endif +
+ +
+ @if(!empty($importPreviewFields)) +
+

{{ count($importPreviewFields) }} {{ __('fields detected') }}:

+
+ + + + @foreach($importPreviewFields as $f) + + @endforeach + +
{{ __('Label') }}{{ __('Name') }}{{ __('Type') }}{{ __('Required') }}
{{ $f['label'] }}{{ $f['name'] }}{{ $f['type'] }}{{ !empty($f['required']) ? '✓' : '' }}
+
@endif - -
-
{{ __('Comments / help') }}
-
-
- @endforeach - +
+ + +
+
- -
- - -
- - @endif - - {{-- Tabla --}} -
- - - - - - - - - - - - @forelse($templates as $template) - - - - - - - - @empty - - @endforelse - -
{{ __('Name') }}{{ __('Description') }}{{ __('Fields') }}{{ __('Used in projects') }}{{ __('Actions') }}
{{ $template->name }}{{ $template->description ?? '-' }}{{ count($template->fields ?? []) }}{{ $template->projects()->count() }} - - -
{{ __('No templates yet (table)') }}
+ @endif
- - {{-- MODAL: Importar desde CSV/Excel --}} - @if($showImportFileModal) -
-
-
-
-

{{ __('Import from CSV/Excel') }}

- -
-
-

- {{ __('Columns: name, label, type, required, options, min, max, step') }}. - -

-
- - - @error('importTemplateName'){{ $message }}@enderror -
-
- - - @error('importFile'){{ $message }}@enderror -
- @if($importError)
{{ $importError }}
@endif -
- -
- @if(!empty($importPreviewFields)) -
-

{{ count($importPreviewFields) }} {{ __('fields detected') }}:

-
- - - - @foreach($importPreviewFields as $f) - - @endforeach - -
{{ __('Label') }}{{ __('Name') }}{{ __('Type') }}{{ __('Required') }}
{{ $f['label'] }}{{ $f['name'] }}{{ $f['type'] }}{{ !empty($f['required']) ? '✓' : '' }}
-
-
- @endif -
-
- - -
-
-
- @endif -
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 0e295cf..217f378 100644 --- a/routes/web.php +++ b/routes/web.php @@ -75,6 +75,7 @@ Route::get('/reports/dashboard', ReportsDashboard::class)->name('reports.dashboa // Ruta para que el componente Livewire muestre/gestione el progreso de una fase Route::get('/phases/{phase}/progress', PhaseProgress::class)->name('phases.progress'); Route::get('/projects-list', ProjectList::class)->name('projects.list'); + // Ruta para templates // Plantillas globales (catálogo único) Route::get('/inspection-templates', \App\Livewire\Inspections\GlobalTemplateManager::class) diff --git a/tests/Feature/GlobalTemplatesTest.php b/tests/Feature/GlobalTemplatesTest.php index 2625fcf..4a39911 100644 --- a/tests/Feature/GlobalTemplatesTest.php +++ b/tests/Feature/GlobalTemplatesTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\Livewire\Inspections\GlobalTemplateManager; +use App\Livewire\Inspections\InspectionTemplatesTable; use App\Livewire\Projects\ProjectMap; use App\Livewire\Projects\ProjectTemplatesPicker; use App\Models\InspectionTemplate; @@ -79,6 +80,42 @@ class GlobalTemplatesTest extends TestCase $this->assertFalse($project->fresh()->inspectionTemplates()->where('inspection_templates.id', $tpl->id)->exists()); } + public function test_templates_table_lists_and_requires_permission(): void + { + Permission::findOrCreate('manage templates'); + InspectionTemplate::create(['name' => 'Recepción acero', 'fields' => []]); + + // Sin permiso → 403 + $weak = User::factory()->create(); + Livewire::actingAs($weak) + ->test(InspectionTemplatesTable::class) + ->assertForbidden(); + + // Con permiso → lista + $admin = User::factory()->create(); + $admin->givePermissionTo('manage templates'); + Livewire::actingAs($admin) + ->test(InspectionTemplatesTable::class) + ->assertOk() + ->assertSee('Recepción acero'); + } + + public function test_table_emits_edit_event_handled_by_manager(): void + { + Permission::findOrCreate('manage templates'); + $admin = User::factory()->create(); + $admin->givePermissionTo('manage templates'); + $tpl = InspectionTemplate::create(['name' => 'Editame', 'fields' => []]); + + // El manager escucha #[On('template-edit')] y abre el form + Livewire::actingAs($admin) + ->test(GlobalTemplateManager::class) + ->assertSet('showForm', false) + ->call('editTemplate', $tpl->id) // simula el dispatch + ->assertSet('showForm', true) + ->assertSet('editingTemplate', $tpl->id); + } + public function test_map_template_selector_only_shows_assigned_templates(): void { $user = User::factory()->create();