# Build a table with an AI agent

> Complete installation, Invoice table, React page and feature test in one short guide.

Source: https://docs.inertiax.dev/table/agent-guide/

This guide assumes a [connected InertiaX registry](https://docs.inertiax.dev/table/installation/#connect-the-inertiax-registry) and an existing Laravel + Inertia + React app that meets the [requirements](https://docs.inertiax.dev/table/installation/#requirements) and has the Invoice, Customer and InvoiceLine models it lists.

## Built-in controls

- Search, sort, filters: Column `searchable()`, `sortable()`, `filterable()`.
- Page sizes: Table `pageSizeOptions([10, 25, 50, 100])`; default 10.
- Visibility/copy: Column `toggleable()`, `copyable()`.
- Loading, empty, errors and invalid-URL reset: built in.
- URL prefix: Table `make('invoices')`.
- Labels, styling: React `customization` (`texts`, `theme`).

## Install

```sh
composer require inertiax/laravel-table
npm install @inertiax/react-table @inertiax/react @inertiax/react-inertia
```

`resources/js/app.tsx`:


```tsx
import '@inertiax/react-table/styles.css';
```


Wrap your persistent layout's content (kit: `resources/js/layouts/app-layout.tsx`):


```tsx
import { InertiaProvider } from '@inertiax/react-inertia';
import { TableProvider } from '@inertiax/react-table';
// ...
        <InertiaProvider>
            <TableProvider>
                <AppLayoutTemplate breadcrumbs={breadcrumbs}>
                    {children}
                </AppLayoutTemplate>
            </TableProvider>
        </InertiaProvider>
// ...
```


Inertia 3: return it from `createInertiaApp`'s `layout` option (the kit does). Inertia 2: set it below each table page's component, not in its body:

```tsx
import AppLayout from '@/layouts/app-layout';
import type { ReactNode } from 'react';
Invoices.layout = (page: ReactNode) => <AppLayout>{page}</AppLayout>;
```

Never put these providers in `withApp`.

`resources/js/components/app-table.tsx`:

```tsx
import { useQueryHost } from '@inertiax/react';
import { Table, type TableProps } from '@inertiax/react-table';
export function AppTable(props: Omit<TableProps, 'host'>) {
    return <Table {...props} host={useQueryHost()} />;
}
```

Rebuild.

## Define the table

`Invoice` needs `id`, `customer_id`, `reference`, `status`, `currency`, integer-cast
`total_cents`, a `customer()` BelongsTo to `Customer(id, name)`, and a `lines()` HasMany to
`InvoiceLine(invoice_id, description)` with fillable `description`; provide Invoice/Customer factories.
Apply authorization in `data()`; this example scopes to USD.

`app/Tables/InvoicesTable.php` (start PHP with `<?php`):


```php
namespace App\Tables;

use App\Models\Invoice;
use InertiaX\Table\Columns\NumberColumn;
use InertiaX\Table\Columns\TextColumn;
use InertiaX\Table\Filters\SelectFilter;
use InertiaX\Table\Table;

class InvoicesTable extends Table
{
    protected function data(): mixed
    {
        return Invoice::query()->where('currency', 'USD');
    }

    protected function columns(): array
    {
        return [
            NumberColumn::make('id')->numberCast('int')->visible(false),
            TextColumn::make('reference')->sortable()->searchable(),
            TextColumn::make('customer_name', 'Customer')
                ->relationship('customer', 'name')->sortable()->searchable(),
            NumberColumn::make('line_count', 'Items')->count('lines')->sortable(),
            NumberColumn::make('total_cents', 'Total')->numberCast('int')
                ->currency('USD', divideBy: 100)->sortable(),
            TextColumn::make('status'),
        ];
    }

    protected function filters(): array
    {
        return [SelectFilter::make('status')->pinned()->options([
            'draft' => 'Draft', 'paid' => 'Paid',
        ])];
    }
}
```


`relationship()` supports display/search/sort; `count()` counts related rows; `currency()` formats cents.
`DateColumn`: PHP formats `format('Y-m-d')` (default), `format('d/m/Y')`, `format('M j, Y')`; optional `timezone('UTC')`, `translate()`.
`BadgeColumn` variants via `variant('success')`: `primary`, `secondary`, `success`, `danger`, `warning`, `info`.
`initialSort('reference')` ascends; `initialSort('-reference')` descends (requires `sortable()`).

## Return and render

`app/Http/Controllers/InvoiceController.php`:


```php
namespace App\Http\Controllers;

use App\Tables\InvoicesTable;
use Inertia\Inertia;
use Inertia\Response;

class InvoiceController
{
    public function index(): Response
    {
        return Inertia::render('invoices', [
            InvoicesTable::make('invoices')->initialSort('reference'),
        ]);
    }
}
```


`routes/web.php` (keep its `Route` import):


```php
use App\Http\Controllers\InvoiceController;

Route::get('/invoices', [InvoiceController::class, 'index']);
```


`resources/js/pages/invoices.tsx` (match existing casing):


```tsx
import { AppTable } from '@/components/app-table';

export default function Invoices() {
    return <AppTable prop="invoices" />;
}
```


Open `/invoices`.
URL keys: `invoices_page`, `invoices_per_page`, `invoices_sort`, `invoices_filters` and
`invoices_search`.
Interactions replace history; Back does not step through table changes.

## Changing a table

Add fields in `columns()` or `filters()`. For a column and text filter:


```php
TextColumn::make('currency')->filterable(),
```


## Next table

Repeat the class, route and `<AppTable prop="…" />` (Inertia 2: and its `layout`) with the new prop name.

## Test

Pest: `tests/Feature/InvoicesTest.php`; omit `uses(...)` if set in `tests/Pest.php`.


```php
use App\Models\Customer;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\RefreshDatabase;
use InertiaX\Table\Testing\AssertableTable;
use InertiaX\Table\Testing\TableRequest;
use Tests\TestCase;

uses(TestCase::class, RefreshDatabase::class);

test('invoice search, sort and filter', function () {
    $customer = Customer::factory()->create(['name' => 'Acme']);
    $invoices = Invoice::factory()->for($customer)->state([
        'currency' => 'USD', 'total_cents' => 12500, 'status' => 'paid',
    ]);
    $first = $invoices->create(['reference' => 'INV-001']);
    $last = $invoices->create(['reference' => 'INV-002']);
    $first->lines()->createMany([['description' => 'Design'], ['description' => 'Review']]);
    $invoices->create(['reference' => 'INV-EUR', 'currency' => 'EUR']);
    $invoices->create(['reference' => 'INV-DRAFT', 'status' => 'draft']);
    $invoices->for(Customer::factory()->state(['name' => 'Other']))
        ->create(['reference' => 'INV-OTHER']);
    $url = TableRequest::url('/invoices', 'invoices',
        sort: '-reference', filters: ['status' => 'paid'], search: 'acme');

    AssertableTable::fromResponse($this->get($url), 'invoices')
        ->hasRows(2)->hasTotal(2)->hasRowIds([$last->id, $first->id])
        ->hasState('search', 'acme')
        ->hasCellValue($first->id, 'customer_name', 'Acme')
        ->hasCellValue($first->id, 'line_count', 2)
        ->hasCellValue($first->id, 'total_cents', 12500);
});
```


`php artisan test --filter=invoice`. `TableRequest::url()` encodes sort and ANDed equality filters (`null` means `is-not-set`); cells assert raw values.
Assertions: `isSuccessful()`, `hasRows()`, `hasTotal()`, `hasRowIds()`, `hasCellValue()`, `hasState()`, `hasProblem()`, `allowsReset()`.

Boost (optional): `php artisan boost:install` → **Agent Skills** → **InertiaX**. `inertiax-tables` reads
`vendor/inertiax/laravel-table/usage.md` (or your custom vendor directory); `php artisan boost:update` refreshes it.
