Skip to content

Example: Sales Order Resource (HeaderLines + Lookups + Actions)

A document-style screen with header fields, editable line items, inline lookups, quick entry, formula columns, and screen actions.

php
<?php

namespace App\TwoWee\Resources;

use App\Models\Customer;
use App\Models\Item;
use App\Models\SalesOrder;
use App\Models\UnitOfMeasure;
use Illuminate\Database\Eloquent\Model;
use TwoWee\Laravel\Actions\Action;
use TwoWee\Laravel\Actions\ActionResult;
use TwoWee\Laravel\Columns\DecimalColumn;
use TwoWee\Laravel\Columns\OptionColumn;
use TwoWee\Laravel\Columns\TextColumn;
use TwoWee\Laravel\Enums\Color;
use TwoWee\Laravel\Fields\Date;
use TwoWee\Laravel\Fields\Decimal;
use TwoWee\Laravel\Fields\Integer;
use TwoWee\Laravel\Fields\Separator;
use TwoWee\Laravel\Fields\Text;
use TwoWee\Laravel\Resource;
use TwoWee\Laravel\Section;

class SalesOrderResource extends Resource
{
    protected static string $model = SalesOrder::class;

    protected static ?string $recordKey = 'no';

    protected static string $label = 'Sales Order';

    protected static ?string $slug = 'sales_orders';

    protected static ?string $navigationGroup = 'Sales';

    protected static int $navigationSort = 2;

    public static function title(Model $model): string
    {
        return 'Sales Order - ' . $model->no;
    }

    public static function layout(): string
    {
        return 'HeaderLines';
    }

    public static function linesRelation(): ?string
    {
        return 'lines';
    }

    public static function linesOverlayPct(): int
    {
        return 65;
    }

    // --- Form (header fields) ---

    public static function form(): array
    {
        return [
            Section::make('General')
                ->left()
                ->fields([
                    Text::make('no')
                        ->label('No.')
                        ->width(20)
                        ->disabled()
                        ->default('Auto'),
                    Separator::make(),
                    Date::make('order_date')
                        ->label('Order Date')
                        ->width(12)
                        ->default(now()->format('d-m-Y'))
                        ->required(),
                    Date::make('due_date')
                        ->label('Delivery Date')
                        ->width(12),
                    Separator::make(),
                    Text::make('your_reference')
                        ->label('Your Reference')
                        ->width(20)
                        ->nullable()
                        ->maxLength(50),
                    Integer::make('payment_terms_days')
                        ->label('Payment Terms (Days)')
                        ->width(10)
                        ->nullable()
                        ->min(0)
                        ->max(365),
                ]),

            Section::make('Customer')
                ->right()
                ->fields([
                    Text::make('customer_no')
                        ->label('Customer No.')
                        ->width(15)
                        ->required()
                        ->focus()                                     // ← initial cursor
                        ->lookup(Customer::class, valueColumn: 'no')
                        ->autofill(['name', 'address', 'city', 'country_code', 'currency_code']),
                    Separator::make(),
                    Text::make('name')
                        ->label('Name')
                        ->width(40)
                        ->quickEntry(false),                          // ← Enter skips
                    Text::make('address')->label('Address')->width(30)->quickEntry(false),
                    Text::make('city')->label('City')->width(20)->quickEntry(false),
                    Text::make('country_code')->label('Country')->width(10)->uppercase()->quickEntry(false),
                ]),

            Section::make('Totals')
                ->right()->rowGroup(1)
                ->fields([
                    Decimal::make('total_amount')
                        ->label('Total')
                        ->width(15)
                        ->disabled()
                        ->color(Color::Yellow)->bold(),
                    Decimal::make('total_profit')
                        ->label('Profit')
                        ->width(15)
                        ->disabled()
                        ->color(Color::Yellow)->bold(),
                ]),
        ];
    }

    // --- Line columns (editable grid) ---

    public static function lineColumns(): array
    {
        return [
            OptionColumn::make('line_type')
                ->label('Type')
                ->width(9)
                ->editable()
                ->quickEntry(false)                                   // ← Enter skips
                ->options(['', 'Item', 'Resource', 'Text']),

            TextColumn::make('item_id')
                ->label('No.')
                ->width(10)
                ->editable()
                ->lookup(Item::class, valueColumn: 'no')              // ← inline lookup
                ->autofill(['description', 'unit_of_measure', 'unit_price'])
                ->filterFrom('line_type'),                            // ← polymorphic context

            TextColumn::make('description')
                ->label('Description')
                ->width('fill')
                ->editable()
                ->quickEntry(false),                                  // ← autofilled

            DecimalColumn::make('quantity')
                ->label('Quantity')
                ->width(10)
                ->align('right')
                ->editable(),                                         // ← Enter stops here

            TextColumn::make('unit_of_measure')
                ->label('Unit')
                ->width(6)
                ->editable()
                ->quickEntry(false)
                ->lookup(UnitOfMeasure::class, valueColumn: 'code')
                ->modal(),                                            // ← small dataset

            DecimalColumn::make('unit_price')
                ->label('Unit Price')
                ->width(12)
                ->align('right')
                ->editable()
                ->quickEntry(false),                                  // ← autofilled

            DecimalColumn::make('discount_percent')
                ->label('Disc. %')
                ->width(8)
                ->align('right')
                ->editable()
                ->quickEntry(false),

            DecimalColumn::make('line_amount')
                ->label('Amount')
                ->width(14)
                ->align('right')
                ->formula('quantity * unit_price * (1 - discount_percent / 100)'), // ← live calc
        ];
    }

    // --- List columns ---

    public static function table(): array
    {
        return [
            TextColumn::make('no')->label('No.')->width(12),
            TextColumn::make('name')->label('Customer')->width('fill'),
            TextColumn::make('currency_code')->label('Currency')->width(8),
            DecimalColumn::make('total_amount')->label('Total')->width(15)->align('right'),
        ];
    }

    // --- Screen actions ---

    public static function screenActions(?Model $model = null): array
    {
        return [
            Action::make('release')
                ->label('Release')
                ->requiresConfirmation('Release this order for processing?')
                ->action(function (SalesOrder $record) {
                    $record->update(['status' => 'released']);
                    return ActionResult::success('Order released.');
                }),

            Action::make('post_invoice')
                ->label('Invoice')
                ->requiresConfirmation('Invoice the quantities in "Qty to Invoice"?')
                ->action(function (SalesOrder $record) {
                    // Post the invoice and redirect if the order is fully invoiced
                    return ActionResult::success('Invoice posted.');
                }),
        ];
    }
}

Key Features Demonstrated

  • $recordKey = 'no' — URLs use the order number, not the database ID
  • ->left() / ->right() — two-column layout; ->rowGroup(1) starts a second row of sections
  • ->focus() on Customer No. — cursor starts here on open, skipping the disabled No. field
  • ->quickEntry(false) on autofilled fields — Enter jumps: Customer No. → Order Date → Due Date → Your Reference → Payment Terms
  • Separator::make() — visual dividers between field groups
  • ->lookup(Customer::class, valueColumn: 'no') — inline lookup with autofill
  • ->filterFrom('line_type') — polymorphic grid lookup: lookup endpoint receives the type as context
  • ->modal() — UoM lookup opens as an overlay (good for small datasets)
  • ->formula(...) — client-side live calculation, server recalculates on save as source of truth
  • Action::make() — screen actions with confirmation dialogs

Enter Path (Quick Entry)

Header: Customer No. → Order Date → Due Date → Your Reference → Payment Terms

Lines: No. → Quantity → (next row) → No. → Quantity → ...

Tab and arrow keys reach all fields and columns when needed.