Files
core/src/Shipping/Filament/Resources/ShipmentResource.php
T

242 lines
8.6 KiB
PHP

<?php
namespace Modules\Core\Shipping\Filament\Resources;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\URL;
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
use Modules\Core\Shipping\Contracts\SupportsBatchLabels;
use Modules\Core\Shipping\Contracts\SupportsManifestBatching;
use Modules\Core\Shipping\Filament\Resources\ShipmentResource\Pages\ListShipments;
use Modules\Core\Shipping\Models\Shipment;
use Throwable;
/**
* Replaces the standalone Modules\Core\Shipping\Filament\Pages\
* ManagePickupManifests page — a bare Page has no access to Filament's
* resource-level pill-tab UI (Filament\Resources\Concerns\HasTabs is
* scoped to ListRecords), so carrier-by-carrier separation
* (ListShipments::getTabs()) needed a real Resource to attach to.
*
* Shows only shipments created through a carrier's API that are NOT yet on
* an issued manifest — see Modules\Core\Shipping\Filament\Resources\
* ManifestResource for shipments that already are. Manual carriers, manual
* vouchers and carrier-reported (synced) vouchers have no label to print
* or manifest to join, so they never show here.
*
* Print opens the label through the same signed URL as the order page; for
* carriers that issue the voucher on print (ELTA), that first open is what
* gives the shipment its voucher number.
*/
class ShipmentResource extends Resource
{
protected static ?string $model = Shipment::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-truck';
protected static string | \UnitEnum | null $navigationGroup = 'Sales';
protected static ?string $navigationLabel = 'Pending Vouchers';
protected static ?string $modelLabel = 'Shipment';
protected static ?int $navigationSort = 100;
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->where('source', Shipment::SOURCE_CREATED)
->whereNotNull('order_id')
->whereNull('manifest_id')
->whereNull('cancelled_at');
}
public static function table(Table $table): Table
{
return $table
->defaultSort('created_at', 'desc')
->columns([
TextColumn::make('carrier')
->badge()
->formatStateUsing(fn (Shipment $record) => $record->carrierLabel()),
TextColumn::make('tracking_reference')
->label('Tracking #')
->placeholder('Not issued yet')
->copyable(),
TextColumn::make('order.reference')->label('Order'),
TextColumn::make('created_at')->label('Created')->dateTime(),
TextColumn::make('label_printed_at')->label('Printed')->dateTime()->placeholder('Not printed'),
])
->recordActions([
Action::make('print')
->label('Print')
->icon('heroicon-o-printer')
->url(fn (Shipment $record) => self::labelUrl($record), shouldOpenInNewTab: true),
Action::make('cancel')
->label('Cancel')
->icon('heroicon-o-x-circle')
->color('danger')
->requiresConfirmation()
->modalDescription('Cancels this shipment with the carrier. This cannot be undone.')
->action(fn (Shipment $record) => self::cancelShipment($record)),
])
->toolbarActions([
BulkAction::make('print_selected')
->label('Print selected')
->icon('heroicon-o-printer')
->deselectRecordsAfterCompletion()
->action(fn (Collection $records, $livewire) => self::printSelected($records, $livewire)),
BulkAction::make('issue_manifest')
->label('Issue Manifest')
->icon('heroicon-o-check-circle')
->visible(fn ($livewire) => filled($livewire->activeTab ?? null)
&& self::fulfillmentService($livewire->activeTab) instanceof SupportsManifestBatching)
->action(fn (Collection $records) => self::issueManifest($records)),
]);
}
public static function labelUrl(Shipment $shipment): string
{
return URL::temporarySignedRoute('shipments.label', now()->addMinutes(5), ['shipment' => $shipment->id]);
}
/**
* One combined PDF for carriers that support it (opened in a new tab,
* with a link in the notification in case the browser blocks the
* pop-up); anything else has to be printed row by row.
*/
public static function printSelected(Collection $shipments, $livewire): void
{
$carriers = $shipments->pluck('carrier')->unique();
if ($carriers->count() !== 1) {
Notification::make()
->title('Select shipments of a single carrier to print them together.')
->warning()
->send();
return;
}
$shipment = $shipments->first();
if (! self::fulfillmentService($shipment->carrier) instanceof SupportsBatchLabels) {
Notification::make()
->title("{$shipment->carrierLabel()} labels can't be combined — print them individually.")
->warning()
->send();
return;
}
$url = URL::temporarySignedRoute('shipments.labels', now()->addMinutes(5), [
'ids' => $shipments->pluck('id')->sort()->implode(','),
]);
$livewire->js('window.open('.json_encode($url).', "_blank")');
Notification::make()
->title('Printing '.$shipments->count().' labels.')
->success()
->actions([
Action::make('open')->label('Open PDF')->url($url, shouldOpenInNewTab: true),
])
->send();
}
public static function cancelShipment(Shipment $shipment): void
{
if ($shipment->cancelsLocallyOnly()) {
$shipment->update(['cancelled_at' => now()]);
Notification::make()
->title('Shipment marked as cancelled. Void the voucher with the courier as well.')
->success()
->send();
return;
}
$service = self::fulfillmentService($shipment->carrier);
try {
if (! $service) {
throw new \RuntimeException("No fulfillment integration configured for {$shipment->carrierLabel()}.");
}
$service->cancelShipment($shipment);
} catch (Throwable $e) {
report($e);
Notification::make()
->title('Failed to cancel shipment: '.$e->getMessage())
->danger()
->send();
return;
}
Notification::make()
->title('Shipment cancelled.')
->success()
->send();
}
public static function issueManifest(Collection $shipments): void
{
$shipments->groupBy('carrier')->each(function (Collection $group, string $carrier) {
$service = self::fulfillmentService($carrier);
if (! $service instanceof SupportsManifestBatching) {
return;
}
try {
$result = $service->issueManifest($group);
} catch (Throwable $e) {
report($e);
Notification::make()
->title("Failed to issue manifest for {$carrier}: {$e->getMessage()}")
->danger()
->send();
return;
}
if (! $result->success) {
Notification::make()
->title("Manifest blocked for {$carrier}: {$result->reason}")
->danger()
->send();
return;
}
Notification::make()
->title("Manifest issued for {$carrier}: {$result->reference}")
->success()
->send();
});
}
public static function fulfillmentService(string $carrier): ?CarrierFulfillmentInterface
{
return app(CarrierFulfillmentInterface::class, ['carrier' => $carrier]);
}
public static function getPages(): array
{
return [
'index' => ListShipments::route('/'),
];
}
}