---
name: filament-admin
description: >
  Laravel Filament v3: optimización de paneles admin, formularios con Tabs y
  Grid, Infolist para vistas de detalle, Tables con filtros avanzados y bulk
  actions, Resources con relaciones anidadas. Cargar cuando se construya o
  rediseñe un panel Filament, se implementen formularios complejos, tablas con
  filtros o Resources con relaciones.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para paneles admin genéricos sin Filament (Nova, Voyager, backpack) — cada panel tiene su propia API; cargar el skill del framework correspondiente."
  - "No cargar para formularios frontend en la app pública de Laravel (Livewire sin Filament, Inertia/Vue) — para formularios de usuario final cargar `livewire-avanzado` o el skill del framework JS."
  - "No cargar para migración de datos o seeds de base de datos en proyectos Laravel — para migraciones y seeds cargar `postgresql-experto` o el skill del ORM."
  - "No cargar para autenticación y autorización en Laravel sin relación con paneles Filament — para auth cargar `auth-patrones` o `laravel-auth`."
---
# Filament Admin v3 — Paneles, Formularios y Tablas

Skill para construir paneles de administración con Laravel Filament v3.
Cubre optimización de UX, formularios multi-tab, infolistas de detalle,
tablas con filtros y acciones masivas.

---

## Cuándo NO cargar

- El proyecto usa un panel admin diferente (Nova, Voyager, Backpack): cargar el skill del framework correspondiente.
- Los formularios son de la app pública del usuario final (Livewire sin Filament, Inertia/Vue): cargar `livewire-avanzado` o el skill del framework JS.
- La tarea es migración de datos, seeds o esquema de BD sin relación con el panel: cargar `postgresql-experto`.
- La tarea es autenticación y autorización sin relación con el panel Filament: cargar `auth-patrones`.

## Cuándo cargar este skill

- Al crear o modificar un `Resource` de Filament (formulario, tabla o infolist)
- Al implementar formularios con más de 6 campos
- Al configurar filtros, ordenamiento o bulk actions en tablas
- Al separar modo vista del modo edición en un panel admin
- Al implementar relaciones anidadas (`HasMany`, `BelongsToMany`) en Forms o Tables

---

## 1. Jerarquía de optimización de UX (en orden de impacto)

```
1. Separar en Tabs secciones distintas        → 60% menos scroll
2. Grid::make(2) o Grid::make(3) dentro de sección → campos legibles en paralelo
3. Collapsible en secciones secundarias       → foco en lo importante
4. Infolist separado del formulario           → modo lectura sin inputs desactivados
5. Repeater con itemLabel                     → listas identificables
6. Placeholder para campos vacíos frecuentes  → contexto sin campos en blanco
```

---

## 2. Formulario con Tabs optimizados

```php
use Filament\Forms\Components\{Tabs, Grid, TextInput, Select, Toggle, Section, Placeholder};

public static function form(Form $form): Form
{
    return $form->schema([
        Tabs::make('formulario')
            ->tabs([
                Tabs\Tab::make('Datos básicos')
                    ->icon('heroicon-o-user')
                    ->schema([
                        Grid::make(2)->schema([
                            TextInput::make('nombre')
                                ->required()
                                ->maxLength(100),
                            TextInput::make('email')
                                ->email()
                                ->unique(ignoreRecord: true),
                        ]),
                        Grid::make(3)->schema([
                            Select::make('rol')
                                ->options(Rol::class)
                                ->searchable()
                                ->preload(),
                            Select::make('empresa_id')
                                ->relationship('empresa', 'nombre')
                                ->searchable()
                                ->preload(),
                            Toggle::make('activo')
                                ->default(true),
                        ]),
                    ]),

                Tabs\Tab::make('Configuración')
                    ->icon('heroicon-o-cog-6-tooth')
                    ->schema([
                        Section::make('Notificaciones')
                            ->collapsible()
                            ->collapsed()
                            ->schema([
                                Toggle::make('notif_email')->label('Por correo'),
                                Toggle::make('notif_sms')->label('Por SMS'),
                            ]),
                    ]),

                Tabs\Tab::make('Historial')
                    ->icon('heroicon-o-clock')
                    ->schema([
                        Placeholder::make('creado_en')
                            ->label('Registrado')
                            ->content(fn ($record) => $record?->created_at?->diffForHumans() ?? '—'),
                        Placeholder::make('ultimo_acceso')
                            ->content(fn ($record) => $record?->last_login_at?->format('d/m/Y H:i') ?? 'Nunca'),
                    ]),
            ])
            ->columnSpanFull(),
    ]);
}
```

---

## 3. Infolist para modo vista (separar del formulario)

No desactivar inputs para modo lectura. Usar `Infolist` como método dedicado.

```php
use Filament\Infolists\Components\{TextEntry, Section, Grid, IconEntry};

public static function infolist(Infolist $infolist): Infolist
{
    return $infolist->schema([
        Grid::make(3)->schema([
            Section::make('Información personal')->schema([
                TextEntry::make('nombre')->label('Nombre completo'),
                TextEntry::make('email')->copyable(),
                IconEntry::make('activo')
                    ->boolean()
                    ->trueColor('success')
                    ->falseColor('danger'),
            ])->columnSpan(2),

            Section::make('Estadísticas')->schema([
                TextEntry::make('ordenes_count')
                    ->label('Órdenes')
                    ->badge()
                    ->color('primary'),
                TextEntry::make('total_facturado')
                    ->label('Total facturado')
                    ->money('MXN'),
            ])->columnSpan(1),
        ]),
    ]);
}
```

---

## 4. Table con filtros y bulk actions

```php
use Filament\Tables\Columns\{TextColumn, BadgeColumn, IconColumn};
use Filament\Tables\Filters\{SelectFilter, TernaryFilter};
use Filament\Tables\Actions\BulkAction;
use Illuminate\Support\Collection;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('nombre')->searchable()->sortable(),
            TextColumn::make('empresa.nombre')->searchable()->toggleable(),
            BadgeColumn::make('rol')
                ->colors([
                    'primary' => 'admin',
                    'success' => 'usuario',
                    'warning' => 'editor',
                ]),
            IconColumn::make('activo')->boolean(),
            TextColumn::make('created_at')
                ->dateTime('d/m/Y')
                ->sortable()
                ->toggleable(isToggledHiddenByDefault: true),
        ])
        ->filters([
            SelectFilter::make('rol')->options(Rol::class),
            SelectFilter::make('empresa')
                ->relationship('empresa', 'nombre')
                ->searchable()
                ->preload(),
            TernaryFilter::make('activo'),
        ])
        ->actions([
            Tables\Actions\EditAction::make(),
            Tables\Actions\DeleteAction::make(),
        ])
        ->bulkActions([
            Tables\Actions\BulkActionGroup::make([
                Tables\Actions\DeleteBulkAction::make(),
                BulkAction::make('activar')
                    ->label('Activar seleccionados')
                    ->icon('heroicon-o-check')
                    ->action(fn (Collection $records) => $records->each->update(['activo' => true]))
                    ->requiresConfirmation(),
            ]),
        ])
        ->defaultSort('created_at', 'desc')
        ->poll('30s');
}
```

---

## 5. Relaciones anidadas — HasMany con Repeater

```php
use Filament\Forms\Components\Repeater;

Repeater::make('telefonos')
    ->relationship('telefonos')
    ->schema([
        Grid::make(2)->schema([
            Select::make('tipo')
                ->options(['movil' => 'Móvil', 'oficina' => 'Oficina', 'casa' => 'Casa'])
                ->required(),
            TextInput::make('numero')->tel()->required(),
        ]),
    ])
    ->itemLabel(fn (array $state): ?string => $state['numero'] ?? null)
    ->collapsible()
    ->defaultItems(1)
    ->minItems(1),
```

---

## 6. Reglas obligatorias

| Regla | Justificación | Verificación |
|-------|--------------|--------------|
| Tabs cuando formulario > 8 campos | Scroll interminable degrada UX | Contar campos en `form()` |
| `Grid::make(2+)` dentro de cada sección | Columna única desperdicia espacio horizontal | Revisar que no haya secciones sin Grid |
| `Infolist` separado para modo vista | `->disabled()` no es modo lectura real | Verificar que existe método `infolist()` en Resources con vista |
| `->searchable()->preload()` en selects de relación | Sin preload, el dropdown carga vacío | Revisar todo Select con `->relationship()` |
| `->unique(ignoreRecord: true)` en campos únicos | Sin esto, la edición siempre falla validación | Revisar campos `->unique()` en formularios de edición |
| `->collapsible()->collapsed()` en secciones secundarias | Reduce ruido visual en el formulario | Secciones de configuración, historial y metadatos |

---

## 7. Anti-patrones

| MAL | BIEN |
|-----|------|
| Formulario de 15 campos en una sola columna | Tabs con Grid::make(2) por sección |
| `TextInput::make()->disabled()` en modo vista | Método `infolist()` con TextEntry |
| `Select::make()->relationship()` sin `->preload()` | `->searchable()->preload()` siempre |
| Hardcodear opciones en Select: `->options(['a' => 'Admin'])` | `->options(Rol::class)` usando Enum |
| Table con columna de relación sin `->searchable()` | N+1 queries en listados — usar eager loading |
| `BulkAction` sin `->requiresConfirmation()` en acciones destructivas | Siempre confirmar acciones masivas |

---

## 8. Checklist de verificación

- [ ] Formulario con más de 8 campos usa Tabs
- [ ] Cada sección tiene Grid::make(2) o Grid::make(3)
- [ ] Secciones secundarias tienen `->collapsible()->collapsed()`
- [ ] Resource con vista de detalle implementa `infolist()`
- [ ] Todos los `Select` con relación usan `->searchable()->preload()`
- [ ] Campos únicos en edición tienen `->unique(ignoreRecord: true)`
- [ ] Bulk actions destructivas tienen `->requiresConfirmation()`
- [ ] Table usa `->defaultSort()` explícito

---

## 9. Referencias

| Tema | Recurso |
|------|---------|
| Documentación oficial Forms, Tables, Infolists | [filamentphp.com/docs/3.x](https://filamentphp.com/docs/3.x) |
| Heroicons para íconos de Tabs y Actions | [heroicons.com](https://heroicons.com) |
| FluxUI components para UI premium | [fluxui.dev](https://fluxui.dev) |
| Livewire v3 + Alpine.js integración | [livewire.laravel.com](https://livewire.laravel.com) |

## Gotchas / Errores comunes no obvios

**`->unique(ignoreRecord: true)` no funciona en formularios de creación cuando el record no existe aún y el campo es único en la BD**: Filament pasa `ignoreRecord: true` para la edición (excluir el propio registro del chequeo), pero en creación este flag no tiene efecto si la regla unique no está configurada correctamente para el modelo. Causa: `ignoreRecord: true` necesita que el Resource tenga acceso al modelo del registro activo. En formularios de creación, el record es `null` y la regla unique valida correctamente, pero si el campo tiene valor `null` en la BD y se permite `nullable`, la validación puede pasar cuando no debería. Fix: usar `->unique(table: 'usuarios', column: 'email', ignorable: fn ($record) => $record)` para asegurarse del comportamiento correcto en creación y edición.

**`Select::make()->relationship()->searchable()` hace una query por cada pulsación de tecla sin debounce por defecto**: en producción con tablas grandes (>10,000 registros), el autocompletado del Select dispara una query SQL al servidor en cada keystroke. Con varios usuarios simultáneos, esto satura la BD. Causa: Filament no configura debounce en los selects de relación. Fix: agregar `->searchDebounce(500)` (milisegundos) en todos los Select con `->relationship()` en producción. Verificar con el query log de Laravel (`DB::enableQueryLog()`) cuántas queries se ejecutan al escribir en el campo.

**`Infolist` no se actualiza en tiempo real cuando el record cambia en la BD mientras el usuario lo tiene abierto**: Filament usa Livewire para reactividad, pero el `Infolist` carga los datos una sola vez al abrir el panel. Si otro usuario modifica el registro simultáneamente, el primer usuario sigue viendo datos desactualizados sin ninguna advertencia. Causa: los componentes Filament no tienen polling habilitado por defecto. Fix: agregar `->refreshable()` en el `ViewAction` del panel o configurar `->polling('30s')` en el componente de página si la frescura de datos es crítica para el caso de uso.

**`BulkAction` en tablas con `->paginated()` aplica la acción solo a los registros de la página actual, no a todos los que coinciden con el filtro**: si el usuario selecciona "todos" en la tabla pero hay filtros aplicados y múltiples páginas, Filament por defecto solo envía los IDs visibles en la página actual. Causa: la selección de "todos" en Filament opera sobre el conjunto paginado, no sobre la query completa. Fix: implementar `->deselectRecordsAfterCompletion()` y procesar la query completa dentro del `->action()` usando el filtro activo directamente en la BD, no la colección recibida por parámetro cuando el volumen puede ser mayor que una página.
