'Created here', Shipment::SOURCE_MANUAL_VOUCHER => 'Manual voucher', Shipment::SOURCE_MANUAL => 'Manual carrier', Shipment::SOURCE_SYNCED => 'From carrier', ]; public static function getEloquentQuery(): Builder { return parent::getEloquentQuery() ->whereNotNull('tracking_reference') ->with(['order.shippingAddress', 'shipmentInfo']); } public static function table(Table $table): Table { return $table ->defaultSort('created_at', 'desc') ->columns([ TextColumn::make('tracking_reference') ->label('Tracking number') ->copyable(), TextColumn::make('status') ->label('Status') ->state(fn (Shipment $record) => self::status($record)) ->placeholder('No updates yet') ->badge() ->color(fn (Shipment $record) => self::statusColor($record)), TextColumn::make('order.reference') ->label('Order') ->placeholder('Not linked'), TextColumn::make('recipient') ->label('Recipient') ->state(fn (Shipment $record) => self::recipient($record)) ->placeholder('—') // One search box for the voucher number, order reference, // recipient and phone. ->searchable(query: fn (Builder $query, string $search) => $query->where(fn (Builder $q) => $q ->where('tracking_reference', 'ilike', "%{$search}%") ->orWhere('meta->recipient_name', 'ilike', "%{$search}%") ->orWhere('meta->phone', 'ilike', "%{$search}%") ->orWhereHas('order', fn (Builder $order) => $order ->where('reference', 'ilike', "%{$search}%") ->orWhereHas('shippingAddress', fn (Builder $address) => $address ->where('first_name', 'ilike', "%{$search}%") ->orWhere('last_name', 'ilike', "%{$search}%") ->orWhere('contact_phone', 'ilike', "%{$search}%"))))), TextColumn::make('phone') ->label('Phone') ->state(fn (Shipment $record) => self::phone($record)) ->placeholder('—'), ]) ->filters([ SelectFilter::make('carrier') ->options(fn () => collect(Shipping::getSupportedDrivers())->map(fn ($driver) => $driver->name())->all()), SelectFilter::make('source')->options(self::SOURCES), ]) ->recordActions([ ViewAction::make(), ]); } public static function infolist(Schema $schema): Schema { return $schema->components([ Section::make('Voucher') ->columns(3) ->schema([ TextEntry::make('carrier') ->formatStateUsing(fn (Shipment $record) => $record->carrierLabel()), TextEntry::make('tracking_reference')->label('Voucher')->copyable(), TextEntry::make('source') ->badge() ->formatStateUsing(fn (string $state) => self::SOURCES[$state] ?? $state), TextEntry::make('status') ->state(fn (Shipment $record) => self::status($record)) ->placeholder('No updates yet') ->badge() ->color(fn (Shipment $record) => self::statusColor($record)), TextEntry::make('carrier_status') ->label('Carrier status') ->state(fn (Shipment $record) => self::carrierStatus($record)) ->placeholder('—'), TextEntry::make('meta.reference')->label('Reference sent to the carrier')->placeholder('—'), TextEntry::make('meta.voucher_date')->label('Voucher date')->date()->placeholder('—'), TextEntry::make('cod') ->label('Cash on delivery') ->state(fn (Shipment $record) => ($amount = $record->meta['cod_amount'] ?? null) ? '€'.number_format((float) $amount, 2) : null) ->placeholder('—'), TextEntry::make('services') ->label('Extra services') ->state(fn (Shipment $record) => collect($record->meta['services'] ?? []) ->map(fn (string $value) => ExtraService::tryFrom($value)?->label()) ->filter() ->implode(', ') ?: null) ->placeholder('—'), TextEntry::make('created_at')->label('Recorded')->dateTime(), ]), Section::make('Recipient') ->columns(3) ->schema([ TextEntry::make('recipient') ->label('Name') ->state(fn (Shipment $record) => self::recipient($record)) ->placeholder('—'), TextEntry::make('postcode') ->state(fn (Shipment $record) => $record->meta['postcode'] ?? $record->order?->shippingAddress?->postcode) ->placeholder('—'), TextEntry::make('phone') ->state(fn (Shipment $record) => self::phone($record)) ->placeholder('—'), ]), Section::make('Order') ->schema([ TextEntry::make('order.reference') ->label('Linked order') ->placeholder('Not linked to an order') ->url(fn (Shipment $record) => $record->order_id ? OrderResource::getUrl('order', ['record' => $record->order_id]) : null), TextEntry::make('suggested') ->label('Suggested orders') ->state(fn (Shipment $record) => array_values(self::orderOptions(Order::with('shippingAddress') ->whereKey($record->meta['suggested_order_ids'] ?? []) ->get()))) ->listWithLineBreaks() ->helperText('The order its reference points to first, then orders with the same postcode and phone or name.') ->visible(fn (Shipment $record) => ! $record->order_id && filled($record->meta['suggested_order_ids'] ?? [])), ]), Section::make('Return') ->columns(2) ->schema([ TextEntry::make('return_kind') ->label('Reported as') ->state(fn (Shipment $record) => $record->isReturn() ? 'Return voucher' : 'Being returned to sender'), TextEntry::make('meta.original_voucher')->label('Original voucher')->placeholder('—'), ]) ->visible(fn (Shipment $record) => self::isReturnLike($record)), Section::make('Tracking history') ->schema([ TextEntry::make('no_history') ->hiddenLabel() ->state('No tracking updates yet.') ->visible(fn (Shipment $record) => $record->shipmentInfo->isEmpty()), RepeatableEntry::make('history') ->hiddenLabel() ->state(fn (Shipment $record) => $record->shipmentInfo->sortByDesc('occurred_at')->values()) ->visible(fn (Shipment $record) => $record->shipmentInfo->isNotEmpty()) ->schema([ TextEntry::make('status') ->label(fn (ShipmentInfo $record) => $record->occurred_at->format('Y-m-d H:i')) ->inlineLabel() ->state(fn (ShipmentInfo $record) => (string) str($record->status->value)->replace('_', ' ')->title()) ->helperText(fn (ShipmentInfo $record) => collect([$record->carrier_status !== 'manual' ? $record->carrier_status : null, $record->location, $record->message])->filter()->unique()->implode(' · ') ?: null), ]), ]), ]); } /** * Attaches an unlinked voucher to an order (suggestions first), from the * view page. */ public static function linkOrderAction(): Action { return Action::make('link_order') ->label('Link to order') ->icon('heroicon-o-link') ->schema(fn (Shipment $record) => [ Select::make('order_id') ->label('Order') ->options(fn () => self::orderOptions(Order::with('shippingAddress') ->whereKey($record->meta['suggested_order_ids'] ?? []) ->get())) ->default(($record->meta['suggested_order_ids'] ?? [])[0] ?? null) ->searchable() ->getSearchResultsUsing(fn (string $search) => self::orderOptions(Order::with('shippingAddress') ->where('reference', 'ilike', "%{$search}%") ->orWhereHas('shippingAddress', fn ($q) => $q ->where('last_name', 'ilike', "%{$search}%") ->orWhere('postcode', $search)) ->latest('placed_at') ->limit(20) ->get())) ->getOptionLabelUsing(fn ($value) => self::orderOptions(Order::with('shippingAddress')->whereKey($value)->get())[$value] ?? $value) ->helperText('Suggestions: the order its reference points to, then orders with the same postcode and phone or name.') ->required(), ]) ->action(function (Shipment $record, array $data) { $record->update(['order_id' => $data['order_id']]); // Its history so far goes onto the order's Timeline. app(ShipmentTimelineLogger::class)->backfill($record->refresh()); Notification::make()->title('Voucher linked to the order.')->success()->send(); }) ->visible(fn (Shipment $record) => $record->order_id === null); } public static function unlinkOrderAction(): Action { return Action::make('unlink_order') ->label('Unlink from order') ->icon('heroicon-o-x-mark') ->color('gray') ->requiresConfirmation() ->action(fn (Shipment $record) => $record->update(['order_id' => null])) ->visible(fn (Shipment $record) => $record->order_id !== null && $record->source === Shipment::SOURCE_SYNCED); } /** * @param iterable $orders * @return array */ private static function orderOptions(iterable $orders): array { return collect($orders)->mapWithKeys(fn (Order $order) => [$order->id => collect([ $order->reference, trim($order->shippingAddress?->first_name.' '.$order->shippingAddress?->last_name), $order->shippingAddress?->postcode, $order->placed_at?->format('Y-m-d'), ])->filter()->implode(' · ')])->all(); } private static function phone(Shipment $record): ?string { return $record->meta['phone'] ?? $record->order?->shippingAddress?->contact_phone; } private static function recipient(Shipment $record): ?string { if ($name = $record->meta['recipient_name'] ?? null) { return $name; } $address = $record->order?->shippingAddress; return $address ? trim("{$address->first_name} {$address->last_name}") : null; } /** * Our status (TrackingStatus): cancelled here, else the latest * checkpoint in the voucher's history. */ private static function trackingStatus(Shipment $record): ?TrackingStatus { if ($record->isCancelled()) { return TrackingStatus::Cancelled; } return $record->shipmentInfo->sortBy('occurred_at')->last()?->status; } private static function status(Shipment $record): ?string { $status = self::trackingStatus($record); return $status ? (string) str($status->value)->replace('_', ' ')->title() : null; } private static function statusColor(Shipment $record): string { return match (self::trackingStatus($record)) { TrackingStatus::Delivered => 'success', TrackingStatus::Failed, TrackingStatus::Returned, TrackingStatus::Cancelled => 'danger', TrackingStatus::InTransit, TrackingStatus::OutForDelivery, TrackingStatus::CollectedFromSender => 'warning', default => 'gray', }; } /** * The carrier's own wording for the latest status, as it sent it. */ private static function carrierStatus(Shipment $record): ?string { $latest = $record->shipmentInfo->sortBy('occurred_at')->last(); return $latest?->carrier_status !== 'manual' ? $latest?->carrier_status : null; } public static function isReturnLike(Shipment $record): bool { return $record->isReturn() || (bool) ($record->meta['reported_return'] ?? false); } public static function getPages(): array { return [ 'index' => ListCarrierVouchers::route('/'), 'view' => ViewCarrierVoucher::route('/{record}'), ]; } }