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 */ 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 */ 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(), ]); } }