Feat: Shipping functionalities redesign, elta courier integration, commenting out acs since no integration can happen, flow updates
This commit is contained in:
@@ -10,7 +10,9 @@ 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;
|
||||
@@ -21,12 +23,17 @@ use Throwable;
|
||||
* 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(), one tab per SupportsManifestBatching
|
||||
* implementer) needed a real Resource to attach to.
|
||||
* (ListShipments::getTabs()) needed a real Resource to attach to.
|
||||
*
|
||||
* Shows only shipments NOT yet on an issued manifest — see
|
||||
* Modules\Core\Shipping\Filament\Resources\ManifestResource for
|
||||
* shipments that already are.
|
||||
* 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
|
||||
{
|
||||
@@ -45,6 +52,8 @@ class ShipmentResource extends Resource
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()
|
||||
->where('source', Shipment::SOURCE_CREATED)
|
||||
->whereNotNull('order_id')
|
||||
->whereNull('manifest_id')
|
||||
->whereNull('cancelled_at');
|
||||
}
|
||||
@@ -52,53 +61,132 @@ class ShipmentResource extends Resource
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('created_at', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('carrier')->badge(),
|
||||
TextColumn::make('tracking_reference')->label('Tracking #'),
|
||||
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')
|
||||
->action(fn (Shipment $record) => self::printShipment($record)),
|
||||
->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')
|
||||
->action(fn (Collection $records) => $records->each(fn (Shipment $shipment) => self::printShipment($shipment))),
|
||||
->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 printShipment(Shipment $shipment): void
|
||||
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);
|
||||
|
||||
if (! $service) {
|
||||
try {
|
||||
if (! $service) {
|
||||
throw new \RuntimeException("No fulfillment integration configured for {$shipment->carrierLabel()}.");
|
||||
}
|
||||
|
||||
$service->cancelShipment($shipment);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
Notification::make()
|
||||
->title("No fulfillment integration configured for {$shipment->carrier}.")
|
||||
->title('Failed to cancel shipment: '.$e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$service->printLabel($shipment);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
Notification::make()
|
||||
->title("Failed to print label for {$shipment->tracking_reference}: {$e->getMessage()}")
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
Notification::make()
|
||||
->title('Shipment cancelled.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public static function issueManifest(Collection $shipments): void
|
||||
|
||||
Reference in New Issue
Block a user