Saltar a contenido

Crear módulo Index

Esta guía explica cómo crear un módulo de índice completo siguiendo los patrones del boilerplate, basado en la implementación del módulo de Auditoría.

1. Configuración de Permisos

Crear Archivo de Permisos

Crear config/permissions/{modulo}.php:

<?php

declare(strict_types=1);

return [
    'permissions' => [
        '{modulo}.view',
        '{modulo}.export',
        // Agregar create, update, delete si aplica
    ],
];

Registrar en PermissionsSeeder

Los permisos se integran automáticamente al PermissionsSeeder.

2. Modelo y Migración

Crear/Adaptar Modelo

Si usas un modelo existente (como Audit), créalo extendiendo la clase base:

<?php

declare(strict_types=1);

namespace App\Models;

use OwenIt\Auditing\Models\Audit as BaseAudit;

class Audit extends BaseAudit
{
    // Relaciones y métodos adicionales
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

3. Request de Validación

Crear IndexRequest

Extiende BaseIndexRequest:

<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Http\Requests\BaseIndexRequest;

class {Modulo}IndexRequest extends BaseIndexRequest
{
    protected function getAllowedSorts(): array
    {
        return [
            'id',
            'created_at',
            'campo1',
            'campo2',
        ];
    }

    protected function getFilterRules(): array
    {
        return [
            'campo1' => 'nullable|string|max:255',
            'campo2' => 'nullable|integer',
            'created_between' => 'nullable|array',
            'created_between.from' => 'nullable|date',
            'created_between.to' => 'nullable|date|after_or_equal:created_between.from',
        ];
    }
}

4. Policy de Autorización

Crear Policy

Extiende BaseResourcePolicy:

<?php

declare(strict_types=1);

namespace App\Policies;

use App\Policies\BaseResourcePolicy;

class {Modulo}Policy extends BaseResourcePolicy
{
    protected string $abilityPrefix = '{modulo}';
}

Registrar en AuthServiceProvider

protected $policies = [
    \App\Models\{Modelo}::class => \App\Policies\{Modulo}Policy::class,
];

5. Repository

Crear Interface

<?php

declare(strict_types=1);

namespace App\Contracts\Repositories;

use App\Contracts\Repositories\RepositoryInterface;

interface {Modulo}RepositoryInterface extends RepositoryInterface
{
}

Implementar Repository

<?php

declare(strict_types=1);

namespace App\Repositories;

use App\Contracts\Repositories\{Modulo}RepositoryInterface;
use App\Models\{Modelo};
use App\Repositories\BaseRepository;

class {Modulo}Repository extends BaseRepository implements {Modulo}RepositoryInterface
{
    public function __construct({Modelo} $model)
    {
        parent::__construct($model);
    }

    protected function getSearchableFields(): array
    {
        return [
            'campo1',
            'campo2',
        ];
    }

    protected function getAllowedSorts(): array
    {
        return [
            'id',
            'created_at',
            'campo1',
        ];
    }

    protected function getDefaultSort(): array
    {
        return ['created_at', 'desc'];
    }

    protected function getFilterMap(): array
    {
        return [
            'campo1' => 'campo1',
            'campo2' => 'campo2',
            'created_between' => function ($query, $value) {
                if (isset($value['from'])) {
                    $query->whereDate('created_at', '>=', $value['from']);
                }
                if (isset($value['to'])) {
                    $query->whereDate('created_at', '<=', $value['to']);
                }
            },
        ];
    }

    protected function getWithRelations(): array
    {
        return ['relacion1', 'relacion2'];
    }
}

6. Service

Crear Interface

<?php

declare(strict_types=1);

namespace App\Contracts\Services;

use App\Contracts\Services\ServiceInterface;

interface {Modulo}ServiceInterface extends ServiceInterface
{
}

Implementar Service

<?php

declare(strict_types=1);

namespace App\Services;

use App\Contracts\Services\{Modulo}ServiceInterface;
use App\Services\BaseService;

class {Modulo}Service extends BaseService implements {Modulo}ServiceInterface
{
    public function toRow($item): array
    {
        return [
            'id' => $item->id,
            'campo1' => $item->campo1,
            'campo2' => $item->campo2,
            'created_at' => $item->created_at?->format('d/m/Y H:i:s'),
            // Relaciones
            'relacion_name' => $item->relacion?->name,
        ];
    }

    protected function getDefaultExportColumns(): array
    {
        return [
            'ID',
            'Campo 1',
            'Campo 2',
            'Fecha de Creación',
        ];
    }

    protected function getExportFilename(): string
    {
        return '{modulo}_export_' . now()->format('Y_m_d_H_i_s');
    }
}

7. Controller

Crear Controller

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Controllers\BaseIndexController;
use App\Http\Requests\{Modulo}IndexRequest;
use App\Models\{Modelo};

class {Modulo}Controller extends BaseIndexController
{
    protected function policyModel(): string
    {
        return {Modelo}::class;
    }

    protected function view(): string
    {
        return '{modulo}/index';
    }

    protected function indexRequestClass(): string
    {
        return {Modulo}IndexRequest::class;
    }

    protected function getWithRelations(): array
    {
        return ['relacion1'];
    }

    protected function getAllowedExportFormats(): array
    {
        return ['csv', 'xlsx', 'pdf', 'json'];
    }
}

8. Routes

Crear Archivo de Rutas

Crear routes/{modulo}.php:

<?php

declare(strict_types=1);

use App\Http\Controllers\{Modulo}Controller;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth'])->group(function () {
    Route::get('/{modulo}', [{Modulo}Controller::class, 'index'])->name('{modulo}.index');
    Route::get('/{modulo}/export', [{Modulo}Controller::class, 'export'])
        ->middleware(['throttle:export'])
        ->name('{modulo}.export');
});

Registrar en web.php

// {Modulo} routes
require __DIR__ . '/{modulo}.php';

9. Frontend

Crear Página Principal

resources/js/pages/{modulo}/index.tsx:

import { DataTable } from '@/components/data-table'
import { PageHeader } from '@/components/page-header'
import AppLayout from '@/layouts/app-layout'
import { Head, router } from '@inertiajs/react'
import { columns } from './{modulo}-columns'
import { {Modulo}Filters } from './{Modulo}Filters'

interface Props {
  rows: any[]
  meta: {
    total: number
    per_page: number
    current_page: number
    last_page: number
  }
  filters?: any
  sort?: string
  dir?: string
  q?: string
}

export default function {Modulo}Index({ rows, meta, filters, sort, dir, q }: Props) {
  const handleFiltersChange = (newFilters: any) => {
    router.get(
      '/{modulo}',
      { ...newFilters, page: 1 },
      {
        only: ['rows', 'meta'],
        preserveState: true,
        preserveScroll: true,
      }
    )
  }

  const handleExport = (format: string) => {
    const params = new URLSearchParams(window.location.search)
    params.set('format', format)
    window.open(`/{modulo}/export?${params}`, '_blank')
  }

  return (
    <>
      <Head title="{Módulo}" />
      <PageHeader title="{Módulo}" />

      <div className="space-y-6">
        <{Modulo}Filters
          filters={filters}
          onFiltersChange={handleFiltersChange}
        />

        <DataTable
          columns={columns}
          data={rows}
          pagination={meta}
          onExport={handleExport}
          exportFormats={['csv', 'xlsx', 'pdf', 'json']}
        />
      </div>
    </>
  )
}

{Modulo}Index.layout = (page: React.ReactElement) => <AppLayout>{page}</AppLayout>

Crear Definición de Columnas

resources/js/pages/{modulo}/{modulo}-columns.tsx:

import { ColumnDef } from '@tanstack/react-table';
import { Badge } from '@/components/ui/badge';
import { DataTableColumnHeader } from '@/components/data-table-column-header';

export const columns: ColumnDef<any>[] = [
    {
        accessorKey: 'id',
        header: ({ column }) => <DataTableColumnHeader column={column} title="ID" />,
        cell: ({ row }) => row.getValue('id'),
    },
    {
        accessorKey: 'campo1',
        header: ({ column }) => <DataTableColumnHeader column={column} title="Campo 1" />,
        cell: ({ row }) => row.getValue('campo1'),
    },
    // Más columnas...
];

Crear Componente de Filtros

resources/js/pages/{modulo}/{Modulo}Filters.tsx:

import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Button } from '@/components/ui/button'

interface Props {
  filters?: any
  onFiltersChange: (filters: any) => void
}

export function {Modulo}Filters({ filters, onFiltersChange }: Props) {
  const handleFilterChange = (key: string, value: any) => {
    const newFilters = {
      ...filters,
      [key]: value || undefined,
    }

    // Limpiar filtros vacíos
    Object.keys(newFilters).forEach(k => {
      if (!newFilters[k]) delete newFilters[k]
    })

    onFiltersChange(newFilters)
  }

  const clearFilters = () => {
    onFiltersChange({})
  }

  return (
    <div className="grid gap-4 p-4 border rounded-lg">
      <div className="flex items-center justify-between">
        <h3 className="font-medium">Filtros</h3>
        <Button variant="outline" size="sm" onClick={clearFilters}>
          Limpiar filtros
        </Button>
      </div>

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        {/* Filtros específicos del módulo */}
      </div>
    </div>
  )
}

10. Sidebar

Agregar al Menu

En resources/js/components/app-sidebar.tsx:

{
    user?.permissions?.includes('{modulo}.view') && (
        <SidebarMenuItem>
            <SidebarMenuButton asChild>
                <Link href="/{modulo}">
                    <Icon />
                    <span>{Módulo}</span>
                </Link>
            </SidebarMenuButton>
        </SidebarMenuItem>
    );
}

11. Service Provider

Registrar Bindings

En app/Providers/DomainServiceProvider.php:

// Repositories
$this->app->bind({Modulo}RepositoryInterface::class, {Modulo}Repository::class);

// Services
$this->app->bind({Modulo}ServiceInterface::class, {Modulo}Service::class);

12. Tests

Feature Tests

Crear tests para Repository, Service, Controller y Permissions siguiendo los patrones del módulo de Auditoría.

13. Documentación

Crear Documentación del Módulo

Crear docs/modules/{modulo}.md con descripción completa de funcionalidades, permisos, campos, filtros, y arquitectura.

Checklist de Implementación

  • [ ] Configurar permisos
  • [ ] Crear/adaptar modelo
  • [ ] Implementar Request de validación
  • [ ] Crear Policy
  • [ ] Implementar Repository e Interface
  • [ ] Implementar Service e Interface
  • [ ] Crear Controller
  • [ ] Configurar rutas
  • [ ] Implementar frontend (página, columnas, filtros)
  • [ ] Agregar al sidebar
  • [ ] Registrar bindings
  • [ ] Crear tests completos
  • [ ] Documentar el módulo

Este patrón garantiza consistencia, reutilización de código, y adherencia a las mejores prácticas del boilerplate.