Feature: Shipping Carrier Additions, Creating a voucher without an order, verifying order details before creating voucher, ELTA Courier voucher print updates

This commit is contained in:
2026-10-01 01:51:13 +03:00
parent c455c8adf2
commit 5ff0d60f54
27 changed files with 2189 additions and 376 deletions
@@ -76,11 +76,24 @@ class CarrierVoucherResource extends Resource
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->whereNotNull('tracking_reference')
return self::vouchers(parent::getEloquentQuery())
->with(['order.shippingAddress', 'shipmentInfo']);
}
/**
* Vouchers with a number, plus ours still waiting for one (an ELTA
* voucher gets its number when printed) so they can be printed from
* here too.
*/
public static function vouchers(Builder $query): Builder
{
return $query->where(fn (Builder $q) => $q
->whereNotNull('tracking_reference')
->orWhere(fn (Builder $pending) => $pending
->where('source', Shipment::SOURCE_CREATED)
->whereNull('cancelled_at')));
}
public static function table(Table $table): Table
{
return $table
@@ -88,8 +101,12 @@ class CarrierVoucherResource extends Resource
->columns([
TextColumn::make('tracking_reference')
->label('Tracking number')
->placeholder('Not issued yet')
->description(fn (Shipment $record) => $record->parcelDescription())
->copyable(),
TextColumn::make('carrier')
->label('Carrier')
->formatStateUsing(fn (Shipment $record) => $record->carrierLabel()),
TextColumn::make('source')
->label('Source')
->badge()
@@ -98,12 +115,12 @@ class CarrierVoucherResource extends Resource
TextColumn::make('status')
->label('Status')
->state(fn (Shipment $record) => self::status($record))
->placeholder('No updates yet')
->placeholder('None yet')
->badge()
->color(fn (Shipment $record) => self::statusColor($record)),
TextColumn::make('order.reference')
->label('Order')
->placeholder('Not linked'),
->placeholder('None'),
TextColumn::make('recipient')
->label('Recipient')
->state(fn (Shipment $record) => self::recipient($record))
@@ -114,6 +131,9 @@ class CarrierVoucherResource extends Resource
->where('tracking_reference', 'ilike', "%{$search}%")
->orWhere('meta->recipient_name', 'ilike', "%{$search}%")
->orWhere('meta->phone', 'ilike', "%{$search}%")
->orWhere('meta->recipient->name', 'ilike', "%{$search}%")
->orWhere('meta->recipient->phone', 'ilike', "%{$search}%")
->orWhere('meta->recipient->mobile', 'ilike', "%{$search}%")
->orWhereHas('order', fn (Builder $order) => $order
->where('reference', 'ilike', "%{$search}%")
->orWhereHas('shippingAddress', fn (Builder $address) => $address
@@ -131,6 +151,7 @@ class CarrierVoucherResource extends Resource
SelectFilter::make('source')->options(self::SOURCES),
])
->recordActions([
self::printAction(),
ViewAction::make(),
]);
}
@@ -145,6 +166,7 @@ class CarrierVoucherResource extends Resource
->formatStateUsing(fn (Shipment $record) => $record->carrierLabel()),
TextEntry::make('tracking_reference')
->label('Voucher')
->placeholder('Not issued yet — printing issues it')
->helperText(fn (Shipment $record) => $record->parcelDescription())
->copyable(),
TextEntry::make('source')
@@ -188,12 +210,30 @@ class CarrierVoucherResource extends Resource
->state(fn (Shipment $record) => self::recipient($record))
->placeholder('—'),
TextEntry::make('postcode')
->state(fn (Shipment $record) => $record->meta['postcode'] ?? $record->order?->shippingAddress?->postcode)
->state(fn (Shipment $record) => self::postcode($record))
->placeholder('—'),
TextEntry::make('phone')
->state(fn (Shipment $record) => self::phone($record))
->placeholder('—'),
]),
// What the carrier's tracking reports beyond the checkpoints
// (ELTA's pel_rec) — filled in by the tracking polls.
Section::make('Delivery')
->columns(3)
->schema([
TextEntry::make('meta.delivered_to')->label('Received by')->placeholder('—'),
TextEntry::make('meta.delivered_at')->label('Received at')->placeholder('—'),
TextEntry::make('cod_remitted')
->label('COD paid back')
->state(fn (Shipment $record) => ($amount = $record->meta['cod_remitted'] ?? null)
? trim('€'.number_format((float) $amount, 2).' '.($record->meta['cod_remittance'] ?? ''))
: null)
->placeholder('—'),
TextEntry::make('meta.return_vg')->label('Return voucher')->placeholder('—'),
])
->visible(fn (Shipment $record) => filled($record->meta['delivered_to'] ?? null)
|| filled($record->meta['cod_remitted'] ?? null)
|| filled($record->meta['return_vg'] ?? null)),
Section::make('Order')
->schema([
TextEntry::make('order.reference')
@@ -241,6 +281,21 @@ class CarrierVoucherResource extends Resource
]);
}
/**
* Opens the voucher's label (the same signed link as the order page and
* Pending Vouchers) — for vouchers created here, from an order or with
* "Create voucher". Carrier-reported and typed-in vouchers have no label
* of ours to print.
*/
public static function printAction(): Action
{
return Action::make('print')
->label('Print')
->icon('heroicon-o-printer')
->url(fn (Shipment $record) => ShipmentResource::labelUrl($record), shouldOpenInNewTab: true)
->visible(fn (Shipment $record) => $record->hasCarrierLabel());
}
/**
* Attaches an unlinked voucher to an order (suggestions first), from the
* view page.
@@ -328,20 +383,25 @@ class CarrierVoucherResource extends Resource
: $record->order->reference;
}
/*
* A synced voucher's recipient is what the carrier reported
* (meta.recipient_name / phone / postcode); our own vouchers' is the
* one saved when they were created (Shipment::recipient()).
*/
private static function phone(Shipment $record): ?string
{
return $record->meta['phone'] ?? $record->order?->shippingAddress?->contact_phone;
return $record->meta['phone'] ?? ($record->recipient()?->phones() ?: null);
}
private static function recipient(Shipment $record): ?string
{
if ($name = $record->meta['recipient_name'] ?? null) {
return $name;
}
return $record->meta['recipient_name'] ?? ($record->recipient()?->name ?: null);
}
$address = $record->order?->shippingAddress;
return $address ? trim("{$address->first_name} {$address->last_name}") : null;
private static function postcode(Shipment $record): ?string
{
return $record->meta['postcode'] ?? ($record->recipient()?->postcode ?: null);
}
/**