feat(features): edición de elementos + catálogo de tipos de elemento

- Migración: tabla feature_types + columnas feature_type_id (FK) e is_active en features.
  Modelo FeatureType; Feature gana featureType() + casts is_active.
- Catálogo de tipos (FeatureTypeManager, ruta /feature-types): CRUD con modal
  (nombre, descripción, color), contador de usos; gateado por edit layers.
- Editor de elementos del proyecto (FeatureManager, ruta projects.features):
  tabla Rappasoft ProjectFeaturesTable (filtros nombre/capa/tipo en cabecera),
  toggle activo/inactivo y borrado individual; modal para editar nombre/tipo/activo.
- Acceso: botón "Elementos" en el dashboard del proyecto (edit layers) y enlace a
  "Tipos de elemento" desde el editor.

Tests: FeatureManagementTest (6). Suite 87 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 16:12:47 +02:00
co-authored by Claude Opus 4.8
parent 1decb19bba
commit 3ee70cf48d
11 changed files with 640 additions and 2 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Livewire\Projects;
use App\Models\Feature;
use App\Models\FeatureType;
use App\Models\Project;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;
#[Layout('layouts.app')]
class FeatureManager extends Component
{
public Project $project;
public $featureTypes = [];
// Edit modal
public bool $showForm = false;
public $editingId = null;
public string $name = '';
public $featureTypeId = '';
public bool $isActive = true;
public function mount(Project $project)
{
$this->project = $project;
abort_unless($this->canManage(), 403);
$this->featureTypes = FeatureType::orderBy('name')->get();
}
private function canManage(): bool
{
$user = Auth::user();
return $user->can('manage all')
|| ($user->can('edit layers') && $this->project->users()->where('user_id', $user->id)->exists());
}
#[On('feature-edit')]
public function editFeature($id): void
{
abort_unless($this->canManage(), 403);
$feature = $this->findFeature($id);
$this->editingId = $feature->id;
$this->name = $feature->name ?? '';
$this->featureTypeId = $feature->feature_type_id ?? '';
$this->isActive = (bool) $feature->is_active;
$this->resetErrorBag();
$this->showForm = true;
}
public function save(): void
{
abort_unless($this->canManage(), 403);
$this->validate([
'name' => 'required|string|max:255',
'featureTypeId' => 'nullable|exists:feature_types,id',
]);
$this->findFeature($this->editingId)->update([
'name' => $this->name,
'feature_type_id' => $this->featureTypeId ?: null,
'is_active' => $this->isActive,
]);
$this->showForm = false;
$this->dispatch('features-changed');
$this->dispatch('notify', 'Elemento actualizado');
}
private function findFeature($id): Feature
{
return Feature::whereHas('layer.phase', fn ($q) => $q->where('project_id', $this->project->id))->findOrFail($id);
}
public function render()
{
return view('livewire.projects.feature-manager');
}
}