Files
core/src/Cart/Filament/Resources/CartResource.php
T

126 lines
4.8 KiB
PHP

<?php
namespace Modules\Core\Cart\Filament\Resources;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\ViewAction;
use Modules\Core\Cart\Filament\Resources\CartResource\Pages\ListCarts;
use Modules\Core\Cart\Filament\Resources\CartResource\Pages\ViewCart;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Carbon;
use Lunar\Admin\Filament\Resources\CustomerResource;
use Lunar\Models\Cart;
use Modules\Core\Cart\Filament\Resources\CartResource\Pages;
use Modules\Core\Cart\Services\CartLifecycleService;
/**
* Read-only — a cart is managed entirely through the storefront (add/update/remove
* line, checkout), never hand-edited by staff. Lists every cart, guest carts
* included — see docs/cart.md ("Scope: every cart, identified or not"). An
* anonymous cart's Customer/User columns just render "—" (see table() below)
* rather than the row being hidden outright: most real traffic never reaches
* an identified user/customer, and "how many carts are ongoing/abandoned
* right now" is a real reporting need regardless of identity — excluding
* anonymous carts would silently undercount it. Lunar itself ships no cart
* admin view at all to follow a precedent from.
*/
class CartResource extends Resource
{
protected static ?string $model = Cart::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-shopping-cart';
protected static string | \UnitEnum | null $navigationGroup = 'Sales';
protected static ?string $modelLabel = 'Cart';
protected static ?string $pluralModelLabel = 'Carts';
/**
* Count only, not a fetch — no rows are loaded. Combines BOTH abandoned
* states (`active()` already covers "no order at all" and "draft order,
* never placed" together — see ListCarts::getTabs()'s "Abandoned Cart" /
* "Abandoned Checkout" tabs for where they're split apart), not "Ongoing"
* — the badge is meant to answer "how many carts might need following up
* on," not the total including ones someone is actively shopping in right
* now.
*/
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->active()->where('updated_at', '<=', static::abandonedCutoff())->count();
}
public static function lifecycle(): CartLifecycleService
{
return app(CartLifecycleService::class);
}
/**
* `Cart::scopeActive()` (not-yet-converted-to-an-order carts) mixes two very
* different things together: a cart someone is actively shopping in right now,
* and one that's genuinely been left behind. Lunar tracks no time-based
* staleness signal of its own — `Cart::updated_at` plus a configurable
* threshold (`config('core.cart.abandoned_after')`, default 1 hour) is what
* this resource uses to tell them apart. A cart with no recent activity is
* "Abandoned"; anything more recent is "Ongoing".
*/
public static function abandonedCutoff(): Carbon
{
return static::lifecycle()->abandonedCutoff();
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('Cart')
->sortable(),
TextColumn::make('customer.full_name')
->label('Customer')
->placeholder('—')
->searchable()
->url(fn (Cart $record) => $record->customer_id !== null
? CustomerResource::getUrl('view', ['record' => $record->customer_id])
: null),
TextColumn::make('user.email')
->label('User')
->placeholder('—')
->searchable(),
TextColumn::make('lines_count')
->label('Lines')
->counts('lines')
->sortable(),
TextColumn::make('lines_sum_quantity')
->label('Items')
->sum('lines', 'quantity')
->sortable(),
TextColumn::make('currency.code')
->label('Currency'),
TextColumn::make('updated_at')
->label('Last activity')
->dateTime()
->sortable(),
])
->recordActions([
ViewAction::make(),
])
->defaultSort('updated_at', 'desc');
}
public static function getPages(): array
{
return [
'index' => ListCarts::route('/'),
'view' => ViewCart::route('/{record}'),
];
}
public static function canCreate(): bool
{
return false;
}
}