161 lines
6.6 KiB
PHP
161 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Filament\Pages;
|
|
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Lunar\Shipping\Filament\Resources\ShippingZoneResource\Pages\ManageShippingRates as BaseManageShippingRates;
|
|
use Lunar\Shipping\Models\ShippingMethod;
|
|
use Lunar\Shipping\Models\ShippingRate;
|
|
use Modules\Core\Shipping\Support\ShippingMethodName;
|
|
|
|
/**
|
|
* Bound in place of the vendor ManageShippingRates page via the container
|
|
* (see ShippingServiceProvider), since that page has no extension hook of
|
|
* its own. Every reference to the vendor class name — routes, sub-nav,
|
|
* ShippingZoneResource::getPages() — is untouched; the container simply
|
|
* hands back this subclass whenever the vendor class is resolved.
|
|
*
|
|
* Relabels the price / price-break fields for a rate whose method has
|
|
* charge_by = "live" (see ShippingMethodResourceExtension, which adds that
|
|
* option to methods whose driver supports live pricing) — they stay
|
|
* visible and editable, but as the fallback price used when the live API
|
|
* call fails (see AcsRateDriver::resolveLivePrice()), not the primary
|
|
* price. Pricing strategy (cart_total / weight / live) stays entirely on
|
|
* the Shipping Method, matching Lunar's own existing charge_by convention;
|
|
* nothing new is stored on the rate itself.
|
|
*
|
|
* Also re-binds the vendor price field's afterStateHydrated(): the vendor
|
|
* callback reads $record->basePrices->first()->price->decimal with no
|
|
* null-guard, which crashes on any rate with no basePrices row — routine
|
|
* for a live rate that has never had a fallback price configured. Same
|
|
* logic, just null-safe.
|
|
*
|
|
* Also replaces the vendor's `shipping_method_id` Select, which uses
|
|
* ->relationship(titleAttribute: 'name') — Filament builds that option
|
|
* list with `orderBy('name')`/`pluck('name', ...)` against the DB, but
|
|
* ShippingMethod.name is now a locale-keyed JSON column (see database/
|
|
* migrations/..._make_shipping_methods_name_translatable.php) that
|
|
* Postgres has no default ordering operator for, crashing with
|
|
* "could not identify an ordering operator for type json" the moment
|
|
* this page loads. Resolved app-side instead via ShippingMethodName,
|
|
* same as every other read site for this column.
|
|
*/
|
|
class ManageShippingRates extends BaseManageShippingRates
|
|
{
|
|
public function form(Schema $schema): Schema
|
|
{
|
|
$schema = parent::form($schema);
|
|
|
|
return $schema->components(
|
|
$this->labelPriceFieldsAsFallbackWhenLive(
|
|
$this->replaceShippingMethodField($schema->getComponents())
|
|
)
|
|
);
|
|
}
|
|
|
|
private function replaceShippingMethodField(array $components): array
|
|
{
|
|
return array_map(function ($component) {
|
|
if (method_exists($component, 'getName') && $component->getName() === 'shipping_method_id') {
|
|
return Select::make('shipping_method_id')
|
|
->label($component->getLabel())
|
|
->required()
|
|
->live()
|
|
->options(fn () => ShippingMethod::all()
|
|
->mapWithKeys(fn (ShippingMethod $method) => [$method->id => ShippingMethodName::resolve($method)]))
|
|
->searchable()
|
|
->columnSpan(2);
|
|
}
|
|
|
|
return $component;
|
|
}, $components);
|
|
}
|
|
|
|
private function labelPriceFieldsAsFallbackWhenLive(array $components): array
|
|
{
|
|
$isLive = fn (Get $get) => static::methodChargeBy($get('shipping_method_id')) === 'live';
|
|
|
|
foreach ($components as $component) {
|
|
if (! method_exists($component, 'getName')) {
|
|
continue;
|
|
}
|
|
|
|
if ($component->getName() === 'price') {
|
|
$component->required(fn (Get $get) => ! $isLive($get))
|
|
->helperText(fn (Get $get) => $isLive($get)
|
|
? 'Used only if the live API call fails.'
|
|
: null)
|
|
->afterStateHydrated(static function (TextInput $component, ?Model $record = null): void {
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$basePrice = $record->basePrices->first();
|
|
|
|
$component->state($basePrice?->price->decimal);
|
|
});
|
|
}
|
|
|
|
if ($component->getName() === 'prices') {
|
|
$component->helperText(fn (Get $get) => $isLive($get)
|
|
? 'Used only if the live API call fails.'
|
|
: null);
|
|
}
|
|
}
|
|
|
|
return $components;
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
$table = parent::table($table);
|
|
|
|
return $table->columns(
|
|
array_map(function ($column) {
|
|
if (method_exists($column, 'getName') && $column->getName() === 'shippingMethod.name') {
|
|
return TextColumn::make('shippingMethod.name')
|
|
->label(__('lunarpanel.shipping::relationmanagers.shipping_rates.table.shipping_method.label'))
|
|
->state(fn (ShippingRate $record) => $record->shippingMethod
|
|
? ShippingMethodName::resolve($record->shippingMethod)
|
|
: null);
|
|
}
|
|
|
|
if (method_exists($column, 'getName') && $column->getName() === 'basePrices.0') {
|
|
return TextColumn::make('basePrices.0')
|
|
->label(__('lunarpanel.shipping::relationmanagers.shipping_rates.table.price.label'))
|
|
->formatStateUsing(function ($state, ShippingRate $record) {
|
|
if (static::methodChargeBy($record->shipping_method_id) === 'live') {
|
|
return $state === null
|
|
? 'Live API pricing, no fallback set'
|
|
: $state->price->formatted.' (fallback)';
|
|
}
|
|
|
|
return $state?->price->formatted;
|
|
});
|
|
}
|
|
|
|
return $column;
|
|
}, $table->getColumns())
|
|
);
|
|
}
|
|
|
|
protected static function methodChargeBy(ShippingMethod|int|string|null $method): ?string
|
|
{
|
|
if (blank($method)) {
|
|
return null;
|
|
}
|
|
|
|
if (! $method instanceof ShippingMethod) {
|
|
$method = ShippingMethod::find($method);
|
|
}
|
|
|
|
return $method?->data['charge_by'] ?? null;
|
|
}
|
|
}
|