Files
sembara/app/Filament/Pages/Transactions/Orders.php
T

299 lines
14 KiB
PHP

<?php
namespace App\Filament\Pages\Transactions;
use App\Services\CustomerService;
use App\Services\DriverService;
use App\Services\OrderService;
use App\Services\ProductService;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Validation\ValidationException;
use UnitEnum;
class Orders extends Page implements HasTable
{
use InteractsWithTable;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static string|UnitEnum|null $navigationGroup = 'Transaksi';
protected static ?int $navigationSort = 0;
protected static ?string $navigationLabel = 'Pesanan';
protected static ?string $slug = 'transactions/orders';
protected string $view = 'filament.pages.blank';
public function getTitle(): string|Htmlable
{
return 'Pesanan';
}
public function table(Table $table): Table
{
/** @var OrderService $service */
$service = app(OrderService::class);
return $table
->records(fn (?string $search, ?string $sortColumn, ?string $sortDirection, int|string $page, int|string $recordsPerPage, ?array $filters) => $service->paginate(
search: $search,
sortColumn: $sortColumn,
sortDirection: $sortDirection,
page: (int) $page,
perPage: (int) $recordsPerPage,
filters: ['deleted_at' => null, ...$this->statusFilter($filters)],
))
->columns([
TextColumn::make('order_number')->label('No. Order')->badge()->weight(FontWeight::SemiBold)->sortable(),
TextColumn::make('customers.name')->label('Pelanggan'),
TextColumn::make('order_date')->label('Tgl Order')->date('d M Y')->sortable(),
TextColumn::make('drivers.name')->label('Pengemudi')->placeholder('—'),
TextColumn::make('total')
->label('Total')
->state(fn (array $record): int => collect($record['order_items'] ?? [])->sum(fn ($item) => (int) ($item['subtotal'] ?? 0)))
->numeric()->prefix('Rp '),
TextColumn::make('payment_status')
->label('Bayar')
->badge()
->color(fn (string $state): string => match ($state) {
'paid' => 'success',
'partial' => 'warning',
default => 'gray',
}),
TextColumn::make('order_status')
->label('Status')
->badge()
->color(fn (string $state): string => match ($state) {
OrderService::STATUS_PENDING => 'gray',
OrderService::STATUS_PROCESSING => 'info',
OrderService::STATUS_DELIVERING => 'warning',
OrderService::STATUS_COMPLETED => 'success',
OrderService::STATUS_CANCELLED => 'danger',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => match ($state) {
OrderService::STATUS_PENDING => 'Pending',
OrderService::STATUS_PROCESSING => 'Diproses',
OrderService::STATUS_DELIVERING => 'Dikirim',
OrderService::STATUS_COMPLETED => 'Selesai',
OrderService::STATUS_CANCELLED => 'Batal',
default => $state,
})
->sortable(),
])
->filters([
Filter::make('order_status')
->schema([
Select::make('value')->label('Status')->options([
OrderService::STATUS_PENDING => 'Pending',
OrderService::STATUS_PROCESSING => 'Diproses',
OrderService::STATUS_DELIVERING => 'Dikirim',
OrderService::STATUS_COMPLETED => 'Selesai',
OrderService::STATUS_CANCELLED => 'Batal',
]),
]),
])
->recordActions([
Action::make('detail')
->label('Detail')
->icon('heroicon-o-eye')
->modalDescription(fn (array $record): string => $this->detailSummary($record))
->modalSubmitAction(false),
Action::make('proses')
->label('Proses')
->color('info')
->visible(fn (array $record): bool => ($record['order_status'] ?? '') === OrderService::STATUS_PENDING)
->requiresConfirmation()
->action(function (array $record) use ($service): void {
$this->runTransition($service, $record['id'], OrderService::STATUS_PROCESSING);
}),
Action::make('kirim')
->label('Kirim')
->color('warning')
->visible(fn (array $record): bool => ($record['order_status'] ?? '') === OrderService::STATUS_PROCESSING)
->schema(fn (Schema $schema): Schema => $schema->components([
Select::make('driver_id')->label('Pengemudi')->options(fn (): array => app(DriverService::class)->optionsForSelect())->required(),
]))
->action(function (array $data, array $record) use ($service): void {
$service->updateDetails($record['id'], ['driver_id' => $data['driver_id']], auth()->user());
$this->runTransition($service, $record['id'], OrderService::STATUS_DELIVERING);
}),
Action::make('selesai')
->label('Selesaikan')
->color('success')
->icon('heroicon-o-check-circle')
->visible(fn (array $record): bool => ($record['order_status'] ?? '') === OrderService::STATUS_DELIVERING)
->requiresConfirmation()
->modalDescription('Stok akan dikurangi otomatis dan tidak dapat diulang. Lanjutkan?')
->action(function (array $record) use ($service): void {
$this->runTransition($service, $record['id'], OrderService::STATUS_COMPLETED);
}),
Action::make('batalkan')
->label('Batalkan')
->color('danger')
->visible(fn (array $record): bool => $service->canCancel($record, auth()->user()))
->schema(fn (Schema $schema): Schema => $schema->components([
Textarea::make('reason')->label('Alasan Pembatalan')->required(),
]))
->action(function (array $data, array $record) use ($service): void {
$this->runTransition($service, $record['id'], OrderService::STATUS_CANCELLED, $data['reason']);
}),
Action::make('surat_jalan')
->label('Surat Jalan')
->icon('heroicon-o-printer')
->url(fn (array $record): string => '/orders/'.$record['id'].'/delivery-note')
->openUrlInNewTab()
->visible(fn (array $record): bool => in_array($record['order_status'], [OrderService::STATUS_DELIVERING, OrderService::STATUS_COMPLETED], true)),
Action::make('hapus')
->label('Hapus')
->color('danger')
->icon('heroicon-o-trash')
->visible(fn (array $record): bool => auth()->user()?->isAdmin() ?? false)
->requiresConfirmation()
->modalDescription('Order akan dihapus (soft delete). Lanjutkan?')
->action(function (array $record) use ($service): void {
$service->softDelete($record['id'], auth()->user());
$this->notifySuccess('Order dihapus.');
}),
])
->headerActions([
CreateAction::make()
->label('Buat Pesanan')
->schema(fn (Schema $schema): Schema => $schema->components(self::orderForm()))
->using(function (array $data): void {
app(OrderService::class)->createOrder($data, $data['items'] ?? [], auth()->user());
$this->notifySuccess('Pesanan dibuat.');
}),
])
->paginated([10, 25, 50]);
}
/**
* @return list<Component>
*/
private static function orderForm(): array
{
return [
Select::make('customer_id')->label('Pelanggan')->options(fn (): array => app(CustomerService::class)->optionsForSelect())->required()->searchable(),
DatePicker::make('order_date')->label('Tgl Order')->default(now()->toDateString())->required(),
DatePicker::make('delivery_date')->label('Tgl Kirim'),
Select::make('payment_method')->label('Metode Bayar')->options(['cash' => 'Cash', 'transfer' => 'Transfer', 'qris' => 'QRIS'])->default('cash')->required(),
Select::make('payment_status')->label('Status Bayar')->options(['unpaid' => 'Belum Bayar', 'partial' => 'DP', 'paid' => 'Lunas'])->default('unpaid')->required(),
Repeater::make('items')
->label('Item Pesanan')
->schema([
Select::make('product_id')
->label('Produk')
->options(fn (): array => app(ProductService::class)->optionsForSelect())
->required()
->searchable()
->live()
->afterStateUpdated(function (?string $state, callable $set): void {
if ($state !== null && $state !== '') {
$product = app(ProductService::class)->find($state);
$set('unit_price', (int) ($product['cost_price'] ?? 0));
}
}),
TextInput::make('quantity')->label('Qty')->numeric()->minValue(1)->required(),
TextInput::make('unit_price')->label('Harga/Satuan')->numeric()->minValue(0)->required(),
])
->columns(3)
->defaultItems(1)
->minItems(1)
->required()
->columnSpanFull(),
Textarea::make('notes')->label('Catatan')->columnSpanFull(),
];
}
private function runTransition(OrderService $service, string $orderId, string $to, ?string $notes = null): void
{
try {
$service->transition($orderId, $to, $notes, auth()->user());
} catch (ValidationException $exception) {
Notification::make()->title('Gagal')->body(collect($exception->errors())->flatten()->implode(' '))->danger()->send();
return;
} catch (AuthorizationException) {
Notification::make()->title('Akses ditolak')->body('Hanya admin atau operator pembuat yang dapat membatalkan order ini.')->danger()->send();
return;
}
$this->notifySuccess('Status order diperbarui.');
}
private function detailSummary(array $record): string
{
$lines = ["No: {$record['order_number']}"];
foreach ($record['order_items'] ?? [] as $item) {
$lines[] = sprintf('- %s x%d @ Rp%s = Rp%s',
data_get($item, 'products.name', '?'),
$item['quantity'],
number_format((int) $item['unit_price'], 0, ',', '.'),
number_format((int) $item['subtotal'], 0, ',', '.'),
);
}
$total = collect($record['order_items'] ?? [])->sum(fn ($item) => (int) ($item['subtotal'] ?? 0));
if (($record['survey_status'] ?? null) !== null) {
$lines[] = 'Survei: '.$record['survey_status'];
}
$lines[] = 'Total: Rp '.number_format($total, 0, ',', '.');
return implode("\n", $lines);
}
/** @return array<string, mixed> */
private function statusFilter(?array $filters): array
{
$status = $filters['order_status']['value'] ?? $filters['order_status'] ?? null;
return is_array($status) ? [] : (filled($status) ? ['order_status' => $status] : []);
}
private function notifySuccess(string $message): void
{
Notification::make()->title('Berhasil')->body($message)->success()->send();
}
public function content(Schema $schema): Schema
{
return $schema
->components([
EmbeddedTable::make(),
]);
}
}