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 TermsSeparator::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 truthAction::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.