From 7920a55b0ebfb7f4a00abc3dec8a0e48004b67a4 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Tue, 16 Jan 2024 13:51:06 +0000 Subject: [PATCH 01/34] Feat - Product option refactoring (#1477) --- .../admin/resources/lang/en/productoption.php | 6 -- .../Resources/ProductOptionResource.php | 32 ++++------ .../ValuesRelationManager.php | 13 +++- .../factories/ProductOptionFactory.php | 2 +- ...00_create_product_product_option_table.php | 22 +++++++ ...100010_update_product_option_relations.php | 60 +++++++++++++++++++ ...ve_position_from_product_options_table.php | 22 +++++++ ...nd_set_shared_to_product_options_table.php | 28 +++++++++ packages/core/src/Models/Product.php | 10 ++++ packages/core/src/Models/ProductOption.php | 12 ++++ tests/core/Unit/Models/ProductOptionTest.php | 21 +------ tests/core/Unit/Models/ProductTest.php | 27 +++++++++ 12 files changed, 205 insertions(+), 50 deletions(-) create mode 100644 packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php create mode 100644 packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php create mode 100644 packages/core/database/migrations/2024_01_16_100020_remove_position_from_product_options_table.php create mode 100644 packages/core/database/migrations/2024_01_16_100030_add_and_set_shared_to_product_options_table.php diff --git a/packages/admin/resources/lang/en/productoption.php b/packages/admin/resources/lang/en/productoption.php index bb025e50d0..ddb2d3c384 100644 --- a/packages/admin/resources/lang/en/productoption.php +++ b/packages/admin/resources/lang/en/productoption.php @@ -16,9 +16,6 @@ 'handle' => [ 'label' => 'Handle', ], - 'position' => [ - 'label' => 'Position', - ], ], 'form' => [ @@ -31,9 +28,6 @@ 'handle' => [ 'label' => 'Handle', ], - 'position' => [ - 'label' => 'Position', - ], ], ]; diff --git a/packages/admin/src/Filament/Resources/ProductOptionResource.php b/packages/admin/src/Filament/Resources/ProductOptionResource.php index 1d5a2f7804..871ee8e6b0 100644 --- a/packages/admin/src/Filament/Resources/ProductOptionResource.php +++ b/packages/admin/src/Filament/Resources/ProductOptionResource.php @@ -46,7 +46,6 @@ protected static function getMainFormComponents(): array static::getNameFormComponent(), static::getLabelFormComponent(), static::getHandleFormComponent(), - static::getPositionFormComponent(), ]; } @@ -76,29 +75,21 @@ protected static function getHandleFormComponent(): Component ->maxLength(255); } - protected static function getPositionFormComponent(): Component - { - return Forms\Components\TextInput::make('position') - ->label(__('lunarpanel::productoption.form.position.label')) - ->numeric() - ->minValue(1) - ->maxValue(100) - ->required(); - } - public static function getDefaultTable(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('name.en') // TODO: Need to determine correct way to localise, maybe custom column type? - ->label(__('lunarpanel::productoption.table.name.label')), - Tables\Columns\TextColumn::make('label.en') // TODO: Need to determine correct way to localise, maybe custom column type? + Tables\Columns\TextColumn::make('name') + ->formatStateUsing( + fn (ProductOption $option) => $option->translate('name'), + )->label(__('lunarpanel::productoption.table.name.label')), + Tables\Columns\TextColumn::make('label') + ->formatStateUsing( + fn (ProductOption $option) => $option->translate('label'), + ) ->label(__('lunarpanel::productoption.table.label.label')), Tables\Columns\TextColumn::make('handle') ->label(__('lunarpanel::productoption.table.handle.label')), - Tables\Columns\TextColumn::make('position') - ->label(__('lunarpanel::productoption.table.position.label')) - ->sortable(), ]) ->filters([ // @@ -111,9 +102,10 @@ public static function getDefaultTable(Table $table): Table Tables\Actions\DeleteBulkAction::make(), ]), ]) - ->searchable() - ->defaultSort('position', 'asc') - ->reorderable('position'); + ->modifyQueryUsing( + fn ($query) => $query->shared() + ) + ->searchable(); } public static function getRelations(): array diff --git a/packages/admin/src/Filament/Resources/ProductOptionResource/RelationManagers/ValuesRelationManager.php b/packages/admin/src/Filament/Resources/ProductOptionResource/RelationManagers/ValuesRelationManager.php index 16f3f66016..12e130a4e6 100644 --- a/packages/admin/src/Filament/Resources/ProductOptionResource/RelationManagers/ValuesRelationManager.php +++ b/packages/admin/src/Filament/Resources/ProductOptionResource/RelationManagers/ValuesRelationManager.php @@ -7,12 +7,17 @@ use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Model; +use Lunar\Models\ProductOptionValue; class ValuesRelationManager extends RelationManager { protected static string $relationship = 'values'; - protected static ?string $recordTitleAttribute = 'name.en'; // TODO: localise somehow + public function getTableRecordTitle(Model $record): ?string + { + return $record->translate('name'); + } public function form(Form $form): Form { @@ -29,8 +34,10 @@ public function table(Table $table): Table return $table ->columns([ - Tables\Columns\TextColumn::make('name.en'), - Tables\Columns\TextColumn::make('handle'), + Tables\Columns\TextColumn::make('name') + ->formatStateUsing( + fn (ProductOptionValue $productOption) => $productOption->translate('name') + ), Tables\Columns\TextColumn::make('position'), ]) ->filters([ diff --git a/packages/core/database/factories/ProductOptionFactory.php b/packages/core/database/factories/ProductOptionFactory.php index 94d782c35a..e0aeca3f48 100644 --- a/packages/core/database/factories/ProductOptionFactory.php +++ b/packages/core/database/factories/ProductOptionFactory.php @@ -24,7 +24,7 @@ public function definition(): array 'label' => [ 'en' => $name, ], - 'position' => self::$position++, + 'shared' => true, ]; } } diff --git a/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php b/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php new file mode 100644 index 0000000000..9e5b691e6f --- /dev/null +++ b/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php @@ -0,0 +1,22 @@ +prefix.'product_product_option', function (Blueprint $table) { + $table->foreignId('product_id')->constrained($this->prefix.'products'); + $table->foreignId('product_option_id')->constrained($this->prefix.'products'); + $table->smallInteger('position')->index(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'product_product_option'); + } +} diff --git a/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php b/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php new file mode 100644 index 0000000000..b845b2fbfd --- /dev/null +++ b/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php @@ -0,0 +1,60 @@ +prefix.'product_variants'; + $productsTable = $this->prefix.'products'; + $optionsTable = $this->prefix.'product_options'; + $optionValueTable = $this->prefix.'product_option_values'; + $variantOptionsValueTable = $this->prefix.'product_option_value_product_variant'; + + DB::table($variantOptionsValueTable)->join( + $variantsTable, + "{$variantOptionsValueTable}.variant_id", + '=', + "{$variantsTable}.id" + )->join( + $optionValueTable, + "{$variantOptionsValueTable}.value_id", + '=', + "{$optionValueTable}.id" + )->join( + $optionsTable, + "{$optionValueTable}.product_option_id", + '=', + "{$optionsTable}.id" + )->join( + $productsTable, + "{$variantsTable}.product_id", + '=', + "{$productsTable}.id" + )->select([ + "{$productsTable}.id as product_id", + "{$optionsTable}.id as product_option_id", + "{$optionsTable}.position", + ])->groupBy(['product_id', 'product_option_id']) + ->orderBy('product_id') + ->chunk(2, function ($rows) { + DB::table( + $this->prefix.'product_product_option' + )->insert( + $rows->map( + fn ($row) => (array) $row + )->toArray() + ); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'product_product_option'); + } +} diff --git a/packages/core/database/migrations/2024_01_16_100020_remove_position_from_product_options_table.php b/packages/core/database/migrations/2024_01_16_100020_remove_position_from_product_options_table.php new file mode 100644 index 0000000000..5a7a008b1c --- /dev/null +++ b/packages/core/database/migrations/2024_01_16_100020_remove_position_from_product_options_table.php @@ -0,0 +1,22 @@ +prefix.'product_options', function (Blueprint $table) { + $table->dropColumn('position'); + }); + } + + public function down() + { + Schema::table($this->prefix.'product_options', function (Blueprint $table) { + $table->smallInteger('position')->after('label'); + }); + } +} diff --git a/packages/core/database/migrations/2024_01_16_100030_add_and_set_shared_to_product_options_table.php b/packages/core/database/migrations/2024_01_16_100030_add_and_set_shared_to_product_options_table.php new file mode 100644 index 0000000000..3f70da28b5 --- /dev/null +++ b/packages/core/database/migrations/2024_01_16_100030_add_and_set_shared_to_product_options_table.php @@ -0,0 +1,28 @@ +prefix.'product_options', function (Blueprint $table) { + $table->boolean('shared')->after('handle')->default(false)->index(); + }); + + \Lunar\Facades\DB::table($this->prefix.'product_options')->update([ + 'shared' => true, + ]); + } + + public function down() + { + Schema::table($this->prefix.'product_options', function (Blueprint $table) { + $table->dropColumn('shared'); + }); + } +} diff --git a/packages/core/src/Models/Product.php b/packages/core/src/Models/Product.php index ae61229621..0133f37a9b 100644 --- a/packages/core/src/Models/Product.php +++ b/packages/core/src/Models/Product.php @@ -213,4 +213,14 @@ public function prices(): HasManyThrough 'priceable_id' )->wherePriceableType(ProductVariant::class); } + + public function productOptions(): BelongsToMany + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany( + ProductOption::class, + "{$prefix}product_product_option" + )->withPivot(['position'])->orderByPivot('position'); + } } diff --git a/packages/core/src/Models/ProductOption.php b/packages/core/src/Models/ProductOption.php index 8bb7402a66..6e22b99dc9 100644 --- a/packages/core/src/Models/ProductOption.php +++ b/packages/core/src/Models/ProductOption.php @@ -2,6 +2,7 @@ namespace Lunar\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\AsCollection; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -39,6 +40,7 @@ class ProductOption extends BaseModel implements SpatieHasMedia protected $casts = [ 'name' => AsCollection::class, 'label' => AsCollection::class, + 'shared' => 'boolean', ]; /** @@ -75,6 +77,16 @@ protected function label(): Attribute */ protected $guarded = []; + public function scopeShared(Builder $builder) + { + return $builder->where('shared', '=', true); + } + + public function scopeExclusive(Builder $builder) + { + return $builder->where('shared', '=', false); + } + /** * Get the values. * diff --git a/tests/core/Unit/Models/ProductOptionTest.php b/tests/core/Unit/Models/ProductOptionTest.php index 88c7fa504e..45577be4fe 100644 --- a/tests/core/Unit/Models/ProductOptionTest.php +++ b/tests/core/Unit/Models/ProductOptionTest.php @@ -1,6 +1,7 @@ $productOption->id, 'name' => json_encode($productOption->name), 'handle' => $productOption->handle, - 'position' => $productOption->position, ]); $this->assertDatabaseCount((new ProductOption)->getTable(), 1); @@ -46,25 +46,6 @@ $this->assertDatabaseCount((new ProductOption)->getTable(), 2); }); -test('can update all product option positions', function () { - $productOptions = ProductOption::factory(10)->create()->each(function ($productOption) { - $productOption->update([ - 'position' => $productOption->id, - ]); - }); - - expect($productOptions->pluck('position')->toArray())->toEqual(range(1, 10)); - - $position = 10; - foreach ($productOptions as $productOption) { - $productOption->position = $position; - $productOption->save(); - $position--; - } - - expect($productOptions->pluck('position')->toArray())->toEqual(array_reverse(range(1, 10))); -}); - test('can delete product option', function () { $productOption = ProductOption::factory()->create(); $this->assertDatabaseCount((new ProductOption)->getTable(), 1); diff --git a/tests/core/Unit/Models/ProductTest.php b/tests/core/Unit/Models/ProductTest.php index d2b4e684a3..737a8e119b 100644 --- a/tests/core/Unit/Models/ProductTest.php +++ b/tests/core/Unit/Models/ProductTest.php @@ -1,6 +1,7 @@ attribute_data)->toEqual($attribute_data); }); +test('can fetch product options', function () { + $attribute_data = collect([ + 'meta_title' => new \Lunar\FieldTypes\Text('I like cake'), + 'pack_qty' => new \Lunar\FieldTypes\Number(12345), + 'description' => new \Lunar\FieldTypes\TranslatedText(collect([ + 'en' => new \Lunar\FieldTypes\Text('Blue'), + 'fr' => new \Lunar\FieldTypes\Text('Bleu'), + ])), + ]); + + $product = Product::factory() + ->for(ProductType::factory()) + ->create([ + 'attribute_data' => $attribute_data, + ]); + + $productOptions = \Lunar\Models\ProductOption::factory(2)->create(); + + foreach ($productOptions as $index => $productOption) { + $product->productOptions()->attach($productOption, ['position' => $index + 1]); + } + + expect($product->refresh()->productOptions)->toHaveCount(2); + +})->group('momo'); + test('can fetch using status scope', function () { $attribute_data = collect([ 'meta_title' => new \Lunar\FieldTypes\Text('I like cake'), From b423f1c5d2729302a39a82f16d5a933877af8538 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Wed, 17 Jan 2024 11:08:57 +0000 Subject: [PATCH 02/34] Update 2024_01_16_100000_create_product_product_option_table.php --- .../2024_01_16_100000_create_product_product_option_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php b/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php index 9e5b691e6f..f8c0734ba8 100644 --- a/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php +++ b/packages/core/database/migrations/2024_01_16_100000_create_product_product_option_table.php @@ -10,7 +10,7 @@ public function up() { Schema::create($this->prefix.'product_product_option', function (Blueprint $table) { $table->foreignId('product_id')->constrained($this->prefix.'products'); - $table->foreignId('product_option_id')->constrained($this->prefix.'products'); + $table->foreignId('product_option_id')->constrained($this->prefix.'product_options'); $table->smallInteger('position')->index(); }); } From c6c70e582d86da1be67b67a86125fdbffeb853b4 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Wed, 17 Jan 2024 11:31:31 +0000 Subject: [PATCH 03/34] Update upgrading.md --- docs/core/upgrading.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/core/upgrading.md b/docs/core/upgrading.md index 91ac3e14f9..c49d4d2165 100644 --- a/docs/core/upgrading.md +++ b/docs/core/upgrading.md @@ -27,6 +27,10 @@ The `lunar.media.conversions` configuration has been removed, in favour of regis Media definition classes allow you to register media collections, conversions and much more. See [Media Collections](/core/reference/media.html#media-collections) for further information. +#### Product Options +The `position` field has been removed from the `product_options` table and is now found on the `product_product_option` +pivot table. Any position data will be automatically adjusted when running migrations. + ## 0.7 ### High Impact From dd50f982a99edc2b48bee26f6ea8e6bd2a5b819b Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 17 Jan 2024 12:23:22 +0000 Subject: [PATCH 04/34] Feature - Shipping Addon (#1454) Co-authored-by: Glenn Jacobs Co-authored-by: glennjacobs --- packages/core/src/Base/ShippingModifiers.php | 2 +- packages/core/src/Base/Traits/HasPrices.php | 5 + packages/table-rate-shipping/README.md | 115 ++++++- .../factories/ShippingRateFactory.php | 16 + ...8_110000_create_shipping_methods_table.php | 5 +- ..._28_111000_create_shipping_rates_table.php | 28 ++ ..._create_shipping_exclusion_lists_table.php | 4 - ...ng_exclusion_list_shipping_zone_table.php} | 10 +- ...0_add_cutoff_to_shipping_methods_table.php | 22 -- ...ck_available_to_shipping_methods_table.php | 22 -- .../table-rate-shipping/lang/en/create.php | 5 - .../table-rate-shipping/lang/en/index.php | 8 - .../lang/en/shipping-zones.php | 10 - .../resources/lang/en/plugin.php | 7 + .../resources/lang/en/relationmanagers.php | 72 +++++ .../lang/en/shippingexclusionlist.php | 19 ++ .../resources/lang/en/shippingmethod.php | 52 +++ .../resources/lang/en/shippingzone.php | 50 +++ .../ShippingOptionLookup.php | 14 +- .../ShippingOptionRequest.php | 4 +- .../Drivers/ShippingMethods/Collection.php | 30 +- .../src/Drivers/ShippingMethods/FlatRate.php | 31 +- .../Drivers/ShippingMethods/FreeShipping.php | 43 ++- .../src/Drivers/ShippingMethods/ShipBy.php | 36 ++- .../Events/ShippingOptionResolvedEvent.php | 8 +- .../ShippingExclusionListResource.php | 116 +++++++ .../Pages/EditShippingExclusionList.php | 24 ++ .../Pages/ListShippingExclusionLists.php | 21 ++ .../ShippingExclusionRelationManager.php | 93 ++++++ .../Resources/ShippingMethodResource.php | 180 +++++++++++ .../Pages/EditShippingMethod.php | 24 ++ .../Pages/ListShippingMethod.php | 27 ++ .../Resources/ShippingZoneResource.php | 303 ++++++++++++++++++ .../Pages/EditShippingZone.php | 24 ++ .../Pages/ListShippingZones.php | 22 ++ .../Pages/ManageShippingExclusions.php | 62 ++++ .../Pages/ManageShippingRates.php | 192 +++++++++++ ...nterface.php => ShippingRateInterface.php} | 8 +- .../src/Managers/ShippingManager.php | 17 +- .../src/Models/ShippingExclusionList.php | 24 +- .../src/Models/ShippingMethod.php | 134 +------- .../src/Models/ShippingRate.php | 138 ++++++++ .../src/Models/ShippingZone.php | 19 ++ .../src/Resolvers/ShippingOptionResolver.php | 8 +- ...dResolver.php => ShippingRateResolver.php} | 24 +- .../src/ShippingModifier.php | 4 +- .../src/ShippingPlugin.php | 57 ++++ .../src/ShippingServiceProvider.php | 4 +- .../ShippingMethods/CollectionTest.php | 8 +- .../Drivers/ShippingMethods/FlatRateTest.php | 11 +- .../ShippingMethods/FreeShippingTest.php | 27 +- .../Drivers/ShippingMethods/ShipByTest.php | 93 +++++- .../Unit/Managers/ShippingManagerTest.php | 15 +- .../Resolvers/ShippingOptionResolverTest.php | 13 +- ...rTest.php => ShippingRateResolverTest.php} | 66 ++-- 55 files changed, 1985 insertions(+), 391 deletions(-) create mode 100644 packages/table-rate-shipping/database/factories/ShippingRateFactory.php create mode 100644 packages/table-rate-shipping/database/migrations/2022_04_28_111000_create_shipping_rates_table.php rename packages/table-rate-shipping/database/migrations/{2022_04_28_140000_create_shipping_exclusion_list_shipping_method_table.php => 2022_04_28_140000_create_shipping_exclusion_list_shipping_zone_table.php} (54%) delete mode 100644 packages/table-rate-shipping/database/migrations/2022_08_09_100000_add_cutoff_to_shipping_methods_table.php delete mode 100644 packages/table-rate-shipping/database/migrations/2023_02_28_100000_add_stock_available_to_shipping_methods_table.php delete mode 100644 packages/table-rate-shipping/lang/en/create.php delete mode 100644 packages/table-rate-shipping/lang/en/index.php delete mode 100644 packages/table-rate-shipping/lang/en/shipping-zones.php create mode 100644 packages/table-rate-shipping/resources/lang/en/plugin.php create mode 100644 packages/table-rate-shipping/resources/lang/en/relationmanagers.php create mode 100644 packages/table-rate-shipping/resources/lang/en/shippingexclusionlist.php create mode 100644 packages/table-rate-shipping/resources/lang/en/shippingmethod.php create mode 100644 packages/table-rate-shipping/resources/lang/en/shippingzone.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/EditShippingExclusionList.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/ListShippingExclusionLists.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/EditShippingMethod.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/ListShippingMethod.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/EditShippingZone.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ListShippingZones.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingExclusions.php create mode 100644 packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php rename packages/table-rate-shipping/src/Interfaces/{ShippingMethodInterface.php => ShippingRateInterface.php} (76%) create mode 100644 packages/table-rate-shipping/src/Models/ShippingRate.php rename packages/table-rate-shipping/src/Resolvers/{ShippingMethodResolver.php => ShippingRateResolver.php} (85%) create mode 100644 packages/table-rate-shipping/src/ShippingPlugin.php rename tests/shipping/Unit/Resolvers/{ShippingMethodResolverTest.php => ShippingRateResolverTest.php} (80%) diff --git a/packages/core/src/Base/ShippingModifiers.php b/packages/core/src/Base/ShippingModifiers.php index 3f1b06e3db..60ab1da31a 100644 --- a/packages/core/src/Base/ShippingModifiers.php +++ b/packages/core/src/Base/ShippingModifiers.php @@ -32,7 +32,7 @@ public function getModifiers() /** * Add a shipping modifier. * - * @param $modifier Class reference to the modifier. + * @param string $modifier Class reference to the modifier. * @return void */ public function add($modifier) diff --git a/packages/core/src/Base/Traits/HasPrices.php b/packages/core/src/Base/Traits/HasPrices.php index b72065b51c..4f8fa16bae 100644 --- a/packages/core/src/Base/Traits/HasPrices.php +++ b/packages/core/src/Base/Traits/HasPrices.php @@ -28,6 +28,11 @@ public function basePrices(): MorphMany return $this->prices()->whereTier(1)->whereNull('customer_group_id'); } + public function tieredPrices(): MorphMany + { + return $this->prices()->where('tier', '>', 1); + } + /** * Return a PricingManager for this model. */ diff --git a/packages/table-rate-shipping/README.md b/packages/table-rate-shipping/README.md index 899886b091..4b90525994 100644 --- a/packages/table-rate-shipping/README.md +++ b/packages/table-rate-shipping/README.md @@ -1,3 +1,116 @@ # Lunar Table Rate Shipping -WIP + +# Requirements + +- LunarPHP Admin `>` `1.x` + +# Installation + +Install via Composer + +``` +composer require lunarphp/table-rate-shipping +``` + +Then register the plugin in your service provider + +```php +use Lunar\Admin\Support\Facades\LunarPanel; +use Lunar\Shipping\ShippingPlugin; +// ... + +public function register(): void +{ + LunarPanel::panel(function (Panel $panel) { + return $panel->plugin(new ShippingPlugin()); + })->register(); + + // ... +} +``` +# Getting Started +This addon provides an easy way for you to add different shipping to your storefront and allow your customers to choose from the different shipping rates you have set up, based on various factors such as zones, minimum spend etc + +## Shipping Methods + +Shipping Methods are the different ways in which your storefront can send orders to customers, you could also allow customers to collect their order from your store which this addon supports. + +## Shipping Zones + +Shipping Zones allow you to section of area's of the countries you ship to, providing you with an easy way to offer distinct shipping methods and pricing to each zone, each zone can be restricted by the following: + +- Postal codes +- Country +- State/Province (based on country) + +## Shipping Rates + +Shipping Rates are the prices you offer for each of your shipping zones, they are linked to a shipping method. So for example you might have a Courier Area Shipping Zone and an Everywhere Else Shipping Zone, you can offer different pricing restrictions using the same shipping methods. + +## Shipping Exclusion Lists + +Sometimes, you might not want to ship certain items to particular Shipping Zone, this is where exclusion lists come in. You can associate purchasables to a list which you can then associate to a shipping zone, if a cart contains any of them then they won't be able to select a shipping rate. + +# Storefront usage + +This addon uses the shipping modifiers provided by the Lunar core, so you shouldn't need to change your existing implementation. + +```php +$options = \Lunar\Base\ShippingManifest::getOptions( + $cart +); +``` + +# Advanced usage + +## Return available drivers + +```php +\Lunar\Shipping\Facades\Shipping::getSupportedDrivers(); +``` + +## Using the driver directly + +```php +\Lunar\Shipping\Facades\Shipping::with('ship-by')->resolve( + new \Lunar\Shipping\DataTransferObjects\ShippingOptionRequest( + shippingRate: \Lunar\Shipping\Models\ShippingRate $shippingRate, + cart: \Lunar\Models\Cart $cart + ) +); +``` + +## Shipping Zones + +Each method is optional, the more you add the more strict it becomes. + +```php +$shippingZones = Lunar\Shipping\Facades\Shipping::zones() + ->country(\Lunar\Models\Country $country) + ->state(\Lunar\Models\State $state) + ->postcode( + new \Lunar\Shipping\DataTransferObjects\PostcodeLookup( + country: \Lunar\Models\Country $country, + postcode: 'NW1' + ) + )->get() + +$shippingZones->map(/* .. */); +``` + +## Shipping Rates + +```php +$shippingRates = \Lunar\Shipping\Facades\Shipping::shippingRates( + \Lunar\Models\Cart $cart +); +``` + +## Shipping Options + +```php +$shippingOptions = \Lunar\Shipping\Facades\Shipping::shippingOptions( + \Lunar\Models\Cart $cart +); +``` \ No newline at end of file diff --git a/packages/table-rate-shipping/database/factories/ShippingRateFactory.php b/packages/table-rate-shipping/database/factories/ShippingRateFactory.php new file mode 100644 index 0000000000..28c27ca3de --- /dev/null +++ b/packages/table-rate-shipping/database/factories/ShippingRateFactory.php @@ -0,0 +1,16 @@ +prefix.'shipping_methods', function (Blueprint $table) { $table->bigIncrements('id'); - $table->foreignId('shipping_zone_id')->constrained( - $this->prefix.'shipping_zones' - ); $table->string('name'); $table->text('description')->nullable(); $table->string('code')->index()->nullable(); $table->boolean('enabled')->default(true); + $table->boolean('stock_available')->default(false); + $table->time('cutoff')->nullable(); $table->json('data')->nullable(); $table->string('driver'); $table->timestamps(); diff --git a/packages/table-rate-shipping/database/migrations/2022_04_28_111000_create_shipping_rates_table.php b/packages/table-rate-shipping/database/migrations/2022_04_28_111000_create_shipping_rates_table.php new file mode 100644 index 0000000000..9f49cbd487 --- /dev/null +++ b/packages/table-rate-shipping/database/migrations/2022_04_28_111000_create_shipping_rates_table.php @@ -0,0 +1,28 @@ +prefix.'shipping_rates', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->foreignId('shipping_method_id')->constrained( + $this->prefix.'shipping_methods' + ); + $table->foreignId('shipping_zone_id')->constrained( + $this->prefix.'shipping_zones' + ); + $table->boolean('enabled')->default(true); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'shipping_rates'); + } +} diff --git a/packages/table-rate-shipping/database/migrations/2022_04_28_120000_create_shipping_exclusion_lists_table.php b/packages/table-rate-shipping/database/migrations/2022_04_28_120000_create_shipping_exclusion_lists_table.php index bc68089668..5d79885974 100644 --- a/packages/table-rate-shipping/database/migrations/2022_04_28_120000_create_shipping_exclusion_lists_table.php +++ b/packages/table-rate-shipping/database/migrations/2022_04_28_120000_create_shipping_exclusion_lists_table.php @@ -10,10 +10,6 @@ public function up() { Schema::create($this->prefix.'shipping_exclusion_lists', function (Blueprint $table) { $table->bigIncrements('id'); - // $table->foreignId('shipping_exclusion_list_id')->constrained( - // $this->prefix.'shipping_exclusion_lists' - // ); - // $table->morphs('purchasable', 'shipping_exclusions_purchasable_type_purchasable_id_index'); $table->string('name')->unique(); $table->timestamps(); }); diff --git a/packages/table-rate-shipping/database/migrations/2022_04_28_140000_create_shipping_exclusion_list_shipping_method_table.php b/packages/table-rate-shipping/database/migrations/2022_04_28_140000_create_shipping_exclusion_list_shipping_zone_table.php similarity index 54% rename from packages/table-rate-shipping/database/migrations/2022_04_28_140000_create_shipping_exclusion_list_shipping_method_table.php rename to packages/table-rate-shipping/database/migrations/2022_04_28_140000_create_shipping_exclusion_list_shipping_zone_table.php index eda8dfd212..c1e8372e4e 100644 --- a/packages/table-rate-shipping/database/migrations/2022_04_28_140000_create_shipping_exclusion_list_shipping_method_table.php +++ b/packages/table-rate-shipping/database/migrations/2022_04_28_140000_create_shipping_exclusion_list_shipping_zone_table.php @@ -4,17 +4,17 @@ use Illuminate\Support\Facades\Schema; use Lunar\Base\Migration; -class CreateShippingExclusionListShippingMethodTable extends Migration +class CreateShippingExclusionListShippingZoneTable extends Migration { public function up() { - Schema::create($this->prefix.'exclusion_list_shipping_method', function (Blueprint $table) { + Schema::create($this->prefix.'exclusion_list_shipping_zone', function (Blueprint $table) { $table->bigIncrements('id'); $table->foreignId('exclusion_id')->constrained( $this->prefix.'shipping_exclusion_lists' ); - $table->foreignId('method_id')->constrained( - $this->prefix.'shipping_methods' + $table->foreignId('shipping_zone_id')->constrained( + $this->prefix.'shipping_zones' ); $table->timestamps(); }); @@ -22,6 +22,6 @@ public function up() public function down() { - Schema::dropIfExists($this->prefix.'shipping_exclusion_list_shipping_method'); + Schema::dropIfExists($this->prefix.'exclusion_list_shipping_zone'); } } diff --git a/packages/table-rate-shipping/database/migrations/2022_08_09_100000_add_cutoff_to_shipping_methods_table.php b/packages/table-rate-shipping/database/migrations/2022_08_09_100000_add_cutoff_to_shipping_methods_table.php deleted file mode 100644 index abcf8719fd..0000000000 --- a/packages/table-rate-shipping/database/migrations/2022_08_09_100000_add_cutoff_to_shipping_methods_table.php +++ /dev/null @@ -1,22 +0,0 @@ -prefix.'shipping_methods', function (Blueprint $table) { - $table->time('cutoff')->nullable()->after('enabled'); - }); - } - - public function down() - { - Schema::table($this->prefix.'shipping_methods', function (Blueprint $table) { - $table->dropColumn('cutoff'); - }); - } -} diff --git a/packages/table-rate-shipping/database/migrations/2023_02_28_100000_add_stock_available_to_shipping_methods_table.php b/packages/table-rate-shipping/database/migrations/2023_02_28_100000_add_stock_available_to_shipping_methods_table.php deleted file mode 100644 index b07f1f46a4..0000000000 --- a/packages/table-rate-shipping/database/migrations/2023_02_28_100000_add_stock_available_to_shipping_methods_table.php +++ /dev/null @@ -1,22 +0,0 @@ -prefix.'shipping_methods', function (Blueprint $table) { - $table->boolean('stock_available')->default(false)->after('cutoff'); - }); - } - - public function down() - { - Schema::table($this->prefix.'shipping_methods', function (Blueprint $table) { - $table->dropColumn('stock_available'); - }); - } -} diff --git a/packages/table-rate-shipping/lang/en/create.php b/packages/table-rate-shipping/lang/en/create.php deleted file mode 100644 index 609739b221..0000000000 --- a/packages/table-rate-shipping/lang/en/create.php +++ /dev/null @@ -1,5 +0,0 @@ - 'Create Shipping Zone', -]; diff --git a/packages/table-rate-shipping/lang/en/index.php b/packages/table-rate-shipping/lang/en/index.php deleted file mode 100644 index a0e8be75e9..0000000000 --- a/packages/table-rate-shipping/lang/en/index.php +++ /dev/null @@ -1,8 +0,0 @@ - 'Shipping', - 'title' => 'Shipping Zones', - 'no_results' => 'There are no Shipping Zones in the system', - 'add_zone_btn' => 'Add shipping zone', -]; diff --git a/packages/table-rate-shipping/lang/en/shipping-zones.php b/packages/table-rate-shipping/lang/en/shipping-zones.php deleted file mode 100644 index d02178f500..0000000000 --- a/packages/table-rate-shipping/lang/en/shipping-zones.php +++ /dev/null @@ -1,10 +0,0 @@ - 'Delete Shipping Method', - 'show.shipping_method.delete_confirm' => 'Are you sure? This action cannot be undone, consider disabling the Shipping Method instead to preserve the data.', - 'show.shipping_zone.delete' => 'Delete Shipping Zones', - 'show.shipping_zone.delete_message' => 'This action cannot be undone', - 'show.shipping_methods.title' => 'Shipping Methods', - 'show.shipping_methods.add_btn' => 'Add shipping method', -]; diff --git a/packages/table-rate-shipping/resources/lang/en/plugin.php b/packages/table-rate-shipping/resources/lang/en/plugin.php new file mode 100644 index 0000000000..3b131322d4 --- /dev/null +++ b/packages/table-rate-shipping/resources/lang/en/plugin.php @@ -0,0 +1,7 @@ + [ + 'group' => 'Shipping', + ], +]; diff --git a/packages/table-rate-shipping/resources/lang/en/relationmanagers.php b/packages/table-rate-shipping/resources/lang/en/relationmanagers.php new file mode 100644 index 0000000000..76d2b1f942 --- /dev/null +++ b/packages/table-rate-shipping/resources/lang/en/relationmanagers.php @@ -0,0 +1,72 @@ + [ + 'title_plural' => 'Shipping Rates', + 'actions' => [ + 'create' => [ + 'label' => 'Create Shipping Rate', + ], + ], + 'notices' => [ + 'prices_incl_tax' => 'All prices include tax, which will be considered when calculating minimum spend.', + 'prices_excl_tax' => 'All prices exclude tax, the minimum spend will be based on the cart sub total.', + ], + 'form' => [ + 'shipping_method_id' => [ + 'label' => 'Shipping Method', + ], + 'price' => [ + 'label' => 'Price', + ], + 'prices' => [ + 'label' => 'Pricing Tiers', + 'repeater' => [ + 'customer_group_id' => [ + 'label' => 'Customer Group', + 'placeholder' => 'Any', + ], + 'currency_id' => [ + 'label' => 'Currency', + ], + 'tier' => [ + 'label' => 'Min. Spend', + ], + 'price' => [ + 'label' => 'Price', + ], + ], + ], + ], + 'table' => [ + 'shipping_method' => [ + 'label' => 'Shipping Method', + ], + 'price' => [ + 'label' => 'Price', + ], + 'tiered_prices_count' => [ + 'label' => 'No. Tiers', + ], + ], + ], + 'exclusions' => [ + 'title_plural' => 'Shipping Exclusions', + 'form' => [ + 'purchasable' => [ + 'label' => 'Product', + ], + ], + 'actions' => [ + 'create' => [ + 'label' => 'Add shipping exclusion list', + ], + 'attach' => [ + 'label' => 'Add exclusion list', + ], + 'detach' => [ + 'label' => 'Remove', + ], + ], + ], +]; diff --git a/packages/table-rate-shipping/resources/lang/en/shippingexclusionlist.php b/packages/table-rate-shipping/resources/lang/en/shippingexclusionlist.php new file mode 100644 index 0000000000..cddaf6bd95 --- /dev/null +++ b/packages/table-rate-shipping/resources/lang/en/shippingexclusionlist.php @@ -0,0 +1,19 @@ + 'Shipping Exclusion List', + 'label_plural' => 'Shipping Exclusion Lists', + 'form' => [ + 'name' => [ + 'label' => 'Name', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'exclusions_count' => [ + 'label' => 'No. Products', + ], + ], +]; diff --git a/packages/table-rate-shipping/resources/lang/en/shippingmethod.php b/packages/table-rate-shipping/resources/lang/en/shippingmethod.php new file mode 100644 index 0000000000..ec2b560317 --- /dev/null +++ b/packages/table-rate-shipping/resources/lang/en/shippingmethod.php @@ -0,0 +1,52 @@ + 'Shipping Methods', + 'label' => 'Shipping Method', + 'form' => [ + 'name' => [ + 'label' => 'Name', + ], + 'description' => [ + 'label' => 'Description', + ], + 'code' => [ + 'label' => 'Code', + ], + 'cutoff' => [ + 'label' => 'Cutoff', + ], + 'charge_by' => [ + 'label' => 'Charge By', + 'options' => [ + 'cart_total' => 'Cart Total', + 'weight' => 'Weight', + ], + ], + 'driver' => [ + 'label' => 'Type', + 'options' => [ + 'ship-by' => 'Standard', + 'collection' => 'Collection', + ], + ], + 'stock_available' => [ + 'label' => 'Stock of all basket items must be available', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'code' => [ + 'label' => 'Code', + ], + 'driver' => [ + 'label' => 'Type', + 'options' => [ + 'ship-by' => 'Standard', + 'collection' => 'Collection', + ], + ], + ], +]; diff --git a/packages/table-rate-shipping/resources/lang/en/shippingzone.php b/packages/table-rate-shipping/resources/lang/en/shippingzone.php new file mode 100644 index 0000000000..f291f27826 --- /dev/null +++ b/packages/table-rate-shipping/resources/lang/en/shippingzone.php @@ -0,0 +1,50 @@ + 'Shipping Zone', + 'label_plural' => 'Shipping Zones', + 'form' => [ + 'unrestricted' => [ + 'content' => 'This shipping zone has no restrictions in place and will be available to all customers at checkout.', + ], + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'options' => [ + 'unrestricted' => 'Unrestricted', + 'countries' => 'Limit to Countries', + 'states' => 'Limit to States / Provinces', + 'postcodes' => 'Limit to Postcodes', + ], + ], + 'country' => [ + 'label' => 'Country', + ], + 'states' => [ + 'label' => 'States', + ], + 'countries' => [ + 'label' => 'States', + ], + 'postcodes' => [ + 'label' => 'Postcodes', + 'helper' => 'List each postcode on a new line. Supports wildcards such as NW*', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'options' => [ + 'unrestricted' => 'Unrestricted', + 'countries' => 'Limit to Countries', + 'states' => 'Limit to States / Provinces', + 'postcodes' => 'Limit to Postcodes', + ], + ], + ], +]; diff --git a/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionLookup.php b/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionLookup.php index 8b5805d35e..edaba3b848 100644 --- a/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionLookup.php +++ b/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionLookup.php @@ -4,23 +4,19 @@ use Doctrine\Common\Cache\Psr6\InvalidArgument; use Illuminate\Support\Collection; -use Lunar\Models\Country; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Models\ShippingRate; class ShippingOptionLookup { /** - * Initialise the postcode lookup class. - * - * @param Country Country description - * @param public string description + * Initialise the shipping option lookup class. */ public function __construct( - public Collection $shippingMethods + public Collection $shippingRates ) { throw_if( - $shippingMethods->filter( - fn ($method) => get_class($method) != ShippingMethod::class + $this->shippingRates->filter( + fn ($method) => get_class($method) != ShippingRate::class )->count(), new InvalidArgument() ); diff --git a/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionRequest.php b/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionRequest.php index 201af2fd29..eb61f39e4f 100644 --- a/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionRequest.php +++ b/packages/table-rate-shipping/src/DataTransferObjects/ShippingOptionRequest.php @@ -3,7 +3,7 @@ namespace Lunar\Shipping\DataTransferObjects; use Lunar\Models\Cart; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Models\ShippingRate; class ShippingOptionRequest { @@ -11,7 +11,7 @@ class ShippingOptionRequest * Initialise the shipping option request class. */ public function __construct( - public ShippingMethod $shippingMethod, + public ShippingRate $shippingRate, public Cart $cart ) { // diff --git a/packages/table-rate-shipping/src/Drivers/ShippingMethods/Collection.php b/packages/table-rate-shipping/src/Drivers/ShippingMethods/Collection.php index 0ff5149233..95989904fb 100644 --- a/packages/table-rate-shipping/src/Drivers/ShippingMethods/Collection.php +++ b/packages/table-rate-shipping/src/Drivers/ShippingMethods/Collection.php @@ -6,15 +6,15 @@ use Lunar\DataTypes\ShippingOption; use Lunar\Models\Product; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; -use Lunar\Shipping\Interfaces\ShippingMethodInterface; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Interfaces\ShippingRateInterface; +use Lunar\Shipping\Models\ShippingRate; -class Collection implements ShippingMethodInterface +class Collection implements ShippingRateInterface { /** - * The shipping method for context. + * The shipping rate for context. */ - public ShippingMethod $shippingMethod; + public ShippingRate $shippingRate; /** * {@inheritdoc} @@ -34,14 +34,16 @@ public function description(): string public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption { - $shippingMethod = $shippingOptionRequest->shippingMethod; + $shippingRate = $shippingOptionRequest->shippingRate; + $shippingMethod = $shippingRate->shippingMethod; + $shippingZone = $shippingRate->shippingZone; $cart = $shippingOptionRequest->cart; // Do we have any products in our exclusions list? // If so, we do not want to return this option regardless. $productIds = $cart->lines->load('purchasable')->pluck('purchasable.product_id'); - $hasExclusions = $shippingMethod->shippingExclusions() + $hasExclusions = $shippingZone->shippingExclusions() ->whereHas('exclusions', function ($query) use ($productIds) { $query->wherePurchasableType(Product::class)->whereIn('purchasable_id', $productIds); })->exists(); @@ -53,26 +55,26 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping return new ShippingOption( name: $shippingMethod->name, description: $shippingMethod->description, - identifier: $shippingMethod->getIdentifier(), + identifier: $shippingRate->getIdentifier(), price: new Price( value: 0, currency: $cart->currency, unitQty: 1 ), - taxClass: $shippingMethod->getTaxClass(), - taxReference: $shippingMethod->getTaxReference(), - option: $shippingMethod->shippingZone->name, + taxClass: $shippingRate->getTaxClass(), + taxReference: $shippingRate->getTaxReference(), + option: $shippingZone->name, collect: true, - meta: ['shipping_zone' => $shippingMethod->shippingZone->name] + meta: ['shipping_zone' => $shippingZone->name] ); } /** * {@inheritDoc} */ - public function on(ShippingMethod $shippingMethod): self + public function on(ShippingRate $shippingRate): self { - $this->shippingMethod = $shippingMethod; + $this->shippingRate = $shippingRate; return $this; } diff --git a/packages/table-rate-shipping/src/Drivers/ShippingMethods/FlatRate.php b/packages/table-rate-shipping/src/Drivers/ShippingMethods/FlatRate.php index 6b4836023f..711cd6cdcc 100644 --- a/packages/table-rate-shipping/src/Drivers/ShippingMethods/FlatRate.php +++ b/packages/table-rate-shipping/src/Drivers/ShippingMethods/FlatRate.php @@ -6,15 +6,15 @@ use Lunar\Facades\Pricing; use Lunar\Models\Product; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; -use Lunar\Shipping\Interfaces\ShippingMethodInterface; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Interfaces\ShippingRateInterface; +use Lunar\Shipping\Models\ShippingRate; -class FlatRate implements ShippingMethodInterface +class FlatRate implements ShippingRateInterface { /** * The shipping method for context. */ - public ShippingMethod $shippingMethod; + public ShippingRate $shippingRate; /** * {@inheritdoc} @@ -34,15 +34,16 @@ public function description(): string public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption { - $data = $shippingOptionRequest->shippingMethod->data; + $shippingRate = $shippingOptionRequest->shippingRate; + $shippingMethod = $shippingRate->shippingMethod; + $shippingZone = $shippingRate->shippingZone; $cart = $shippingOptionRequest->cart; - $shippingMethod = $shippingOptionRequest->shippingMethod; // Do we have any products in our exclusions list? // If so, we do not want to return this option regardless. $productIds = $cart->lines->load('purchasable')->pluck('purchasable.product_id'); - $hasExclusions = $shippingMethod->shippingExclusions() + $hasExclusions = $shippingZone->shippingExclusions() ->whereHas('exclusions', function ($query) use ($productIds) { $query->wherePurchasableType(Product::class)->whereIn('purchasable_id', $productIds); })->exists(); @@ -53,7 +54,7 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping $subTotal = $cart->lines->sum('subTotal.value'); - $pricing = Pricing::for($shippingMethod)->qty($subTotal)->get(); + $pricing = Pricing::for($shippingRate)->qty($subTotal)->get(); if (! $pricing->matched) { return null; @@ -62,21 +63,21 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping return new ShippingOption( name: $shippingMethod->name ?: $this->name(), description: $shippingMethod->description ?: $this->description(), - identifier: $shippingMethod->getIdentifier(), + identifier: $shippingRate->getIdentifier(), price: $pricing->matched->price, - taxClass: $shippingMethod->getTaxClass(), - taxReference: $shippingMethod->getTaxReference(), - option: $shippingMethod->shippingZone->name, - meta: ['shipping_zone' => $shippingMethod->shippingZone->name] + taxClass: $shippingRate->getTaxClass(), + taxReference: $shippingRate->getTaxReference(), + option: $shippingZone->name, + meta: ['shipping_zone' => $shippingZone->name] ); } /** * {@inheritDoc} */ - public function on(ShippingMethod $shippingMethod): self + public function on(ShippingRate $shippingRate): self { - $this->shippingMethod = $shippingMethod; + $this->shippingRate = $shippingRate; return $this; } diff --git a/packages/table-rate-shipping/src/Drivers/ShippingMethods/FreeShipping.php b/packages/table-rate-shipping/src/Drivers/ShippingMethods/FreeShipping.php index 355447492f..1b8a8be6db 100644 --- a/packages/table-rate-shipping/src/Drivers/ShippingMethods/FreeShipping.php +++ b/packages/table-rate-shipping/src/Drivers/ShippingMethods/FreeShipping.php @@ -5,17 +5,16 @@ use Lunar\DataTypes\Price; use Lunar\DataTypes\ShippingOption; use Lunar\Models\Product; -use Lunar\Models\TaxClass; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; -use Lunar\Shipping\Interfaces\ShippingMethodInterface; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Interfaces\ShippingRateInterface; +use Lunar\Shipping\Models\ShippingRate; -class FreeShipping implements ShippingMethodInterface +class FreeShipping implements ShippingRateInterface { /** * The shipping method for context. */ - public ShippingMethod $shippingMethod; + public ShippingRate $shippingRate; /** * {@inheritDoc} @@ -35,7 +34,9 @@ public function description(): string public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption { - $shippingMethod = $shippingOptionRequest->shippingMethod; + $shippingRate = $shippingOptionRequest->shippingRate; + $shippingMethod = $shippingRate->shippingMethod; + $shippingZone = $shippingRate->shippingZone; $data = $shippingMethod->data; $cart = $shippingOptionRequest->cart; @@ -43,7 +44,7 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping // If so, we do not want to return this option regardless. $productIds = $cart->lines->load('purchasable')->pluck('purchasable.product_id'); - $hasExclusions = $shippingMethod->shippingExclusions() + $hasExclusions = $shippingZone->shippingExclusions() ->whereHas('exclusions', function ($query) use ($productIds) { $query->wherePurchasableType(Product::class)->whereIn('purchasable_id', $productIds); })->exists(); @@ -54,41 +55,37 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping $subTotal = $cart->lines->sum('subTotal.value'); - if ($data->use_discount_amount ?? false) { + if ($data['use_discount_amount'] ?? false) { $subTotal -= $cart->discountTotal->value; } - if (empty($data)) { - $minSpend = 0; - } else { - if (is_array($data->minimum_spend)) { - $minSpend = ($data->minimum_spend[$cart->currency->code] ?? null); - } else { - $minSpend = ($data->minimum_spend->{$cart->currency->code} ?? null); - } + $minSpend = $data['minimum_spend'] ?? null; + + if (is_array($minSpend)) { + $minSpend = $minSpend[$cart->currency->code] ?? null; } - if (is_null($minSpend) || ($minSpend) > $subTotal) { + if (is_null($minSpend) || $minSpend > $subTotal) { return null; } return new ShippingOption( name: $shippingMethod->name, description: $shippingMethod->description, - identifier: $shippingMethod->code, + identifier: $shippingRate->getIdentifier(), price: new Price(0, $cart->currency, 1), - taxClass: TaxClass::getDefault(), - option: $shippingMethod->shippingZone->name, - meta: ['shipping_zone' => $shippingMethod->shippingZone->name] + taxClass: $shippingRate->getTaxClass(), + option: $shippingZone->name, + meta: ['shipping_zone' => $shippingZone->name] ); } /** * {@inheritDoc} */ - public function on(ShippingMethod $shippingMethod): self + public function on(ShippingRate $shippingRate): self { - $this->shippingMethod = $shippingMethod; + $this->shippingRate = $shippingRate; return $this; } diff --git a/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php b/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php index 266480cc64..0aec735a14 100644 --- a/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php +++ b/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php @@ -6,15 +6,15 @@ use Lunar\Facades\Pricing; use Lunar\Models\Product; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; -use Lunar\Shipping\Interfaces\ShippingMethodInterface; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Interfaces\ShippingRateInterface; +use Lunar\Shipping\Models\ShippingRate; -class ShipBy implements ShippingMethodInterface +class ShipBy implements ShippingRateInterface { /** - * The shipping method for context. + * The shipping rate for context. */ - public ShippingMethod $shippingMethod; + public ShippingRate $shippingRate; /** * {@inheritdoc} @@ -34,9 +34,11 @@ public function description(): string public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption { - $data = $shippingOptionRequest->shippingMethod->data; + $shippingRate = $shippingOptionRequest->shippingRate; + $shippingMethod = $shippingRate->shippingMethod; + $shippingZone = $shippingRate->shippingZone; + $data = $shippingMethod->data; $cart = $shippingOptionRequest->cart; - $shippingMethod = $shippingOptionRequest->shippingMethod; $customerGroups = collect([]); if ($user = $cart->user) { @@ -49,7 +51,7 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping // If so, we do not want to return this option regardless. $productIds = $cart->lines->load('purchasable')->pluck('purchasable.product_id'); - $hasExclusions = $shippingMethod->shippingExclusions() + $hasExclusions = $shippingZone->shippingExclusions() ->whereHas('exclusions', function ($query) use ($productIds) { $query->wherePurchasableType(Product::class)->whereIn('purchasable_id', $productIds); })->exists(); @@ -58,7 +60,7 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping return null; } - $chargeBy = $data->charge_by ?? null; + $chargeBy = $data['charge_by'] ?? null; if (! $chargeBy) { $chargeBy = 'cart_total'; @@ -73,7 +75,7 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping } // Do we have a suitable tier price? - $pricing = Pricing::for($shippingMethod)->customerGroups($customerGroups)->qty($tier)->get(); + $pricing = Pricing::for($shippingRate)->customerGroups($customerGroups)->qty($tier)->get(); $prices = $pricing->tiered; @@ -95,21 +97,21 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping return new ShippingOption( name: $shippingMethod->name, description: $shippingMethod->description, - identifier: $shippingMethod->getIdentifier(), + identifier: $shippingRate->getIdentifier(), price: $price, - taxClass: $shippingMethod->getTaxClass(), - taxReference: $shippingMethod->getTaxReference(), - option: $shippingMethod->shippingZone->name, - meta: ['shipping_zone' => $shippingMethod->shippingZone->name] + taxClass: $shippingRate->getTaxClass(), + taxReference: $shippingRate->getTaxReference(), + option: $shippingZone->name, + meta: ['shipping_zone' => $shippingZone->name] ); } /** * {@inheritDoc} */ - public function on(ShippingMethod $shippingMethod): self + public function on(ShippingRate $shippingRate): self { - $this->shippingMethod = $shippingMethod; + $this->shippingRate = $shippingRate; return $this; } diff --git a/packages/table-rate-shipping/src/Events/ShippingOptionResolvedEvent.php b/packages/table-rate-shipping/src/Events/ShippingOptionResolvedEvent.php index 0abbb07107..962e337f47 100644 --- a/packages/table-rate-shipping/src/Events/ShippingOptionResolvedEvent.php +++ b/packages/table-rate-shipping/src/Events/ShippingOptionResolvedEvent.php @@ -7,7 +7,7 @@ use Illuminate\Queue\SerializesModels; use Lunar\DataTypes\ShippingOption; use Lunar\Models\Cart; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Models\ShippingRate; class ShippingOptionResolvedEvent { @@ -21,17 +21,17 @@ class ShippingOptionResolvedEvent /** * The instance of the shipping method. */ - public ShippingMethod $shippingMethod; + public ShippingRate $shippingRate; /** * The instance of the cart. */ public Cart $cart; - public function __construct(Cart $cart, ShippingMethod $shippingMethod, ShippingOption $shippingOption) + public function __construct(Cart $cart, ShippingRate $shippingRate, ShippingOption $shippingOption) { $this->cart = $cart; - $this->shippingMethod = $shippingMethod; + $this->shippingRate = $shippingRate; $this->shippingOption = $shippingOption; } } diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource.php new file mode 100644 index 0000000000..283395f6f2 --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource.php @@ -0,0 +1,116 @@ +schema([ + Forms\Components\Section::make()->schema( + static::getMainFormComponents(), + ), + ]); + } + + protected static function getDefaultRelations(): array + { + return [ + ShippingExclusionRelationManager::class, + ]; + } + + protected static function getMainFormComponents(): array + { + return [ + static::getNameFormComponent(), + ]; + } + + public static function getNameFormComponent(): Component + { + return Forms\Components\TextInput::make('name') + ->label(__('lunarpanel.shipping::shippingexclusionlist.form.name.label')) + ->required() + ->maxLength(255) + ->autofocus(); + } + + public static function getDefaultTable(Table $table): Table + { + return $table + ->columns(static::getTableColumns()) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getTableColumns(): array + { + return [ + Tables\Columns\TextColumn::make('name') + ->label( + __('lunarpanel.shipping::shippingexclusionlist.table.name.label') + ), + Tables\Columns\TextColumn::make('exclusions_count') + ->label( + __('lunarpanel.shipping::shippingexclusionlist.table.exclusions_count.label') + ) + ->counts('exclusions'), + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListShippingExclusionLists::route('/'), + 'edit' => Pages\EditShippingExclusionList::route('/{record}/edit'), + ]; + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/EditShippingExclusionList.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/EditShippingExclusionList.php new file mode 100644 index 0000000000..321757e87a --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/EditShippingExclusionList.php @@ -0,0 +1,24 @@ +getResource()::getUrl('index'); + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/ListShippingExclusionLists.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/ListShippingExclusionLists.php new file mode 100644 index 0000000000..0508c247cb --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/Pages/ListShippingExclusionLists.php @@ -0,0 +1,21 @@ +form([ + ShippingExclusionListResource::getNameFormComponent(), + ]), + ]; + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php new file mode 100644 index 0000000000..dc305c41fb --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php @@ -0,0 +1,93 @@ +schema([ + Forms\Components\MorphToSelect::make('purchasable') + ->types([ + Forms\Components\MorphToSelect\Type::make(Product::class) + ->titleAttribute('name.en') + ->getSearchResultsUsing(static function (Forms\Components\Select $component, string $search): array { + return Product::search($search) + ->get() + ->mapWithKeys(fn (Product $record): array => [$record->getKey() => $record->translateAttribute('name')]) + ->all(); + }), + ]) + ->label( + __('lunarpanel.shipping::relationmanagers.exclusions.form.purchasable.label') + ) + ->required() + ->searchable(true), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') + ->collection('images') + ->conversion('small') + ->limit(1) + ->square() + ->label(''), + Tables\Columns\TextColumn::make('purchasable.attribute_data.name') + ->formatStateUsing(fn (Model $record): string => $record->purchasable->translateAttribute('name')) + ->limit(50) + ->tooltip(function (Tables\Columns\TextColumn $column, Model $record): ?string { + $state = $column->getState(); + $record = $record->purchasable; + + if (strlen($record->translateAttribute('name')) <= $column->getCharacterLimit()) { + return null; + } + + return $record->translateAttribute('name'); + }) + ->label(__('lunarpanel::product.table.name.label')), + Tables\Columns\TextColumn::make('purchasable.variants.sku') + ->label(__('lunarpanel::product.table.sku.label')) + ->toggleable(), + ]) + ->filters([ + // + ]) + ->headerActions([ + Tables\Actions\CreateAction::make()->mutateFormDataUsing(function (array $data, RelationManager $livewire) { + return $data; + }), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource.php new file mode 100644 index 0000000000..b7c34f678e --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource.php @@ -0,0 +1,180 @@ +schema([ + Forms\Components\Section::make()->schema( + static::getMainFormComponents(), + ), + ]); + } + + protected static function getMainFormComponents(): array + { + return [ + static::getNameFormComponent(), + Forms\Components\Group::make([ + static::getCodeFormComponent(), + static::getDriverFormComponent(), + ])->columns(2), + Forms\Components\Group::make([ + static::getCutoffFormComponent(), + static::getChargeByFormComponent(), + ])->columns(2), + static::getStockAvailableFormComponent(), + static::getDescriptionFormComponent(), + + ]; + } + + public static function getNameFormComponent(): Component + { + return Forms\Components\TextInput::make('name') + ->label(__('lunarpanel.shipping::shippingmethod.form.name.label')) + ->required() + ->maxLength(255) + ->autofocus(); + } + + public static function getDescriptionFormComponent(): Component + { + return Forms\Components\RichEditor::make('description') + ->label(__('lunarpanel.shipping::shippingmethod.form.description.label')); + } + + public static function getCodeFormComponent(): Component + { + return Forms\Components\TextInput::make('code') + ->label(__('lunarpanel.shipping::shippingmethod.form.code.label')) + ->required() + ->unique(ignoreRecord: true); + } + + public static function getCutoffFormComponent(): Component + { + return Forms\Components\TimePicker::make('cutoff') + ->label(__('lunarpanel.shipping::shippingmethod.form.cutoff.label')); + } + + public static function getStockAvailableFormComponent(): Component + { + return Forms\Components\Toggle::make('stock_available') + ->label(__('lunarpanel.shipping::shippingmethod.form.stock_available.label')); + } + + public static function getChargeByFormComponent(): Component + { + return Forms\Components\Group::make([ + Forms\Components\Select::make('charge_by') + ->label( + __('lunarpanel.shipping::shippingmethod.form.charge_by.label') + ) + ->options([ + 'cart_total' => __('lunarpanel.shipping::shippingmethod.form.charge_by.options.cart_total'), + 'weight' => __('lunarpanel.shipping::shippingmethod.form.charge_by.options.weight'), + ]), + + ])->columns(1)->statePath('data'); + } + + public static function getDriverFormComponent(): Component + { + return Forms\Components\Select::make('driver') + ->label(__('lunarpanel.shipping::shippingmethod.form.driver.label')) + ->options([ + 'ship-by' => __('lunarpanel.shipping::shippingmethod.form.driver.options.ship-by'), + 'collection' => __('lunarpanel.shipping::shippingmethod.form.driver.options.collection'), + ])->label('Type') + ->default('ship-by'); + } + + public static function getDefaultTable(Table $table): Table + { + return $table + ->columns(static::getTableColumns()) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + protected static function getTableColumns(): array + { + return [ + Tables\Columns\TextColumn::make('name') + ->label( + __('lunarpanel.shipping::shippingmethod.table.name.label') + ), + Tables\Columns\TextColumn::make('code') + ->label( + __('lunarpanel.shipping::shippingmethod.table.code.label') + ), + Tables\Columns\TextColumn::make('driver') + ->label( + __('lunarpanel.shipping::shippingmethod.table.driver.label') + )->formatStateUsing( + fn ($state) => __("lunarpanel.shipping::shippingmethod.table.driver.options.{$state}") + ), + ]; + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListShippingMethod::route('/'), + 'edit' => Pages\EditShippingMethod::route('/{record}/edit'), + ]; + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/EditShippingMethod.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/EditShippingMethod.php new file mode 100644 index 0000000000..7279be485a --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/EditShippingMethod.php @@ -0,0 +1,24 @@ +getResource()::getUrl('index'); + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/ListShippingMethod.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/ListShippingMethod.php new file mode 100644 index 0000000000..3808546f1e --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingMethodResource/Pages/ListShippingMethod.php @@ -0,0 +1,27 @@ +form([ + ShippingMethodResource::getNameFormComponent(), + Group::make([ + ShippingMethodResource::getCodeFormComponent(), + ShippingMethodResource::getDriverFormComponent(), + ])->columns(2), + ShippingMethodResource::getDescriptionFormComponent(), + ]), + ]; + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource.php new file mode 100644 index 0000000000..877e4ff38f --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource.php @@ -0,0 +1,303 @@ +generateNavigationItems([ + Pages\EditShippingZone::class, + Pages\ManageShippingRates::class, + Pages\ManageShippingExclusions::class, + ]); + } + + public static function getDefaultForm(Form $form): Form + { + return $form->schema([ + Forms\Components\Section::make()->schema( + static::getMainFormComponents(), + ), + ]); + } + + protected static function getMainFormComponents(): array + { + return [ + static::getNameFormComponent(), + static::getTypeFormComponent(), + static::getCountryFormComponent(), + static::getPostcodesFormComponent(), + static::getStatesFormComponent(), + static::getCountriesFormComponent(), + Shout::make('unrestricted')->content( + __('lunarpanel.shipping::shippingzone.form.unrestricted.content') + )->hidden( + fn (Forms\Get $get) => $get('type') != 'unrestricted' + ), + ]; + } + + public static function getNameFormComponent(): Component + { + return Forms\Components\TextInput::make('name') + ->label(__('lunarpanel.shipping::shippingzone.form.name.label')) + ->required() + ->maxLength(255) + ->autofocus(); + } + + public static function getTypeFormComponent(): Component + { + return Forms\Components\Select::make('type') + ->label(__('lunarpanel.shipping::shippingzone.form.type.label')) + ->required() + ->options([ + 'unrestricted' => __('lunarpanel.shipping::shippingzone.form.type.options.unrestricted'), + 'countries' => __('lunarpanel.shipping::shippingzone.form.type.options.countries'), + 'states' => __('lunarpanel.shipping::shippingzone.form.type.options.states'), + 'postcodes' => __('lunarpanel.shipping::shippingzone.form.type.options.postcodes'), + ])->live(); + } + + protected static function getCountryFormComponent(): Component + { + return Forms\Components\Select::make('country') + ->label(__('lunarpanel.shipping::shippingzone.form.country.label')) + ->dehydrated(false) + ->visible( + fn (Forms\Get $get) => ! in_array($get('type'), ['countries', 'unrestricted']) + ) + ->options(Country::get()->pluck('name', 'id')) + + ->required() + ->searchable() + ->loadStateFromRelationshipsUsing(static function (Forms\Components\Select $component, Model $record): void { + $record->loadMissing('countries'); + + /** @var Collection $relatedModels */ + $country = $record->countries->first(); + + $component->state( + $country?->id + ); + })->getOptionLabelsUsing(static function (Model $record): array { + $record->loadMissing('countries.country'); + + return $record->countries + ->pluck('country.name', 'country.id') + ->toArray(); + }) + ->saveRelationshipsUsing(static function (Model $record, $state) { + $selectedCountry = Country::where('id', $state)->first(); + + $record->countries()->sync($selectedCountry->id); + }); + } + + protected static function getCountriesFormComponent(): Component + { + return Forms\Components\Select::make('countries') + ->label(__('lunarpanel.shipping::shippingzone.form.countries.label')) + ->visible(fn ($get) => $get('type') == 'countries') + ->dehydrated(false) + ->options(Country::get()->pluck('name', 'id')) + ->multiple() + ->required() + ->loadStateFromRelationshipsUsing(static function (Forms\Components\Select $component, Model $record): void { + $record->loadMissing('countries'); + /** @var Collection $relatedModels */ + $relatedModels = $record->countries; + + $component->state( + $relatedModels + ->pluck('id') + ->map(static fn ($key): string => strval($key)) + ->toArray(), + ); + })->getOptionLabelsUsing(static function (Model $record): array { + $record->loadMissing('countries'); + + return $record->countries + ->pluck('name', 'id') + ->toArray(); + }) + ->saveRelationshipsUsing(static function (Model $record, $state) { + $record->countries()->sync($state); + }); + } + + protected static function getStatesFormComponent(): Component + { + return Forms\Components\Select::make('states') + ->label(__('lunarpanel.shipping::shippingzone.form.states.label')) + ->visible(fn ($get) => $get('type') == 'states') + ->dehydrated(false) + ->options(fn ($get) => State::where('country_id', $get('country'))->get()->pluck('name', 'id')) + ->multiple() + ->required() + ->loadStateFromRelationshipsUsing(static function (Forms\Components\Select $component, Model $record): void { + $record->loadMissing('states'); + + /** @var Collection $relatedModels */ + $relatedModels = $record->states; + + $component->state( + $relatedModels + ->pluck('id') + ->map(static fn ($key): string => strval($key)) + ->toArray(), + ); + })->getOptionLabelsUsing(static function (Model $record): array { + $record->loadMissing('states'); + + return $record->states + ->pluck('name', 'id') + ->toArray(); + }) + ->saveRelationshipsUsing(static function (Model $record, $state, $get) { + $record->states()->sync($state); + }); + } + + protected static function getPostcodesFormComponent(): Component + { + return Forms\Components\Textarea::make('postcodes') + ->label(__('lunarpanel.shipping::shippingzone.form.postcodes.label')) + ->visible(fn ($get) => $get('type') == 'postcodes') + ->dehydrated(false) + ->rows(10) + ->helperText(__('lunarpanel.shipping::shippingzone.form.postcodes.helper')) + ->required() + ->afterStateHydrated(static function (Forms\Components\Textarea $component, Model $record): void { + /** @var Collection $relatedModels */ + $relatedModels = $record->postcodes; + + $component->state( + $relatedModels + ->pluck('postcode') + ->join("\n"), + ); + }) + ->saveRelationshipsUsing(static function (Model $record, $state, $get) { + static::syncPostcodes($record, $get('zone_country'), $state); + + $record->states()->detach(); + }); + } + + private static function syncPostcodes(ShippingZone $shippingZone, $countryId, $postcodes) + { + $postcodes = collect( + explode( + "\n", + str_replace(' ', '', $postcodes) + ) + )->unique()->filter(); + + $shippingZone->postcodes()->delete(); + + $shippingZone->postcodes()->createMany( + $postcodes->map(function ($postcode) { + return [ + 'postcode' => $postcode, + ]; + }) + ); + } + + public static function getDefaultTable(Table $table): Table + { + return $table + ->columns(static::getTableColumns()) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + protected static function getTableColumns(): array + { + return [ + Tables\Columns\TextColumn::make('name') + ->label( + __('lunarpanel.shipping::shippingzone.table.name.label') + ), + Tables\Columns\TextColumn::make('type') + ->label( + __('lunarpanel.shipping::shippingzone.table.type.label') + ) + ->formatStateUsing( + fn ($state) => __("lunarpanel.shipping::shippingzone.table.type.options.{$state}") + ), + ]; + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListShippingZones::route('/'), + 'edit' => Pages\EditShippingZone::route('/{record}/edit'), + 'rates' => Pages\ManageShippingRates::route('/{record}/rates'), + 'exclusions' => Pages\ManageShippingExclusions::route('/{record}/exclusions'), + ]; + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/EditShippingZone.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/EditShippingZone.php new file mode 100644 index 0000000000..754106f684 --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/EditShippingZone.php @@ -0,0 +1,24 @@ +getResource()::getUrl('index'); + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ListShippingZones.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ListShippingZones.php new file mode 100644 index 0000000000..6df578862d --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ListShippingZones.php @@ -0,0 +1,22 @@ +form([ + ShippingZoneResource::getNameFormComponent(), + ShippingZoneResource::getTypeFormComponent(), + ]), + ]; + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingExclusions.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingExclusions.php new file mode 100644 index 0000000000..6dc14185e7 --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingExclusions.php @@ -0,0 +1,62 @@ +schema([]); + } + + public function table(Table $table): Table + { + return $table->columns( + ShippingExclusionListResource::getTableColumns(), + )->headerActions([ + Tables\Actions\AttachAction::make() + ->color('primary') + ->label( + __('lunarpanel.shipping::relationmanagers.exclusions.actions.attach.label') + ) + ->preloadRecordSelect() + ->recordTitleAttribute('name'), + ])->actions([ + Tables\Actions\DetachAction::make('detach') + ->label( + __('lunarpanel.shipping::relationmanagers.exclusions.actions.detach.label') + ), + + ]); + } +} diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php new file mode 100644 index 0000000000..92ab1a180f --- /dev/null +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php @@ -0,0 +1,192 @@ +schema([ + Shout::make('')->content( + function () { + $pricesIncTax = config('lunar.pricing.stored_inclusive_of_tax', false); + + if ($pricesIncTax) { + return __('lunarpanel.shipping::relationmanagers.shipping_rates.notices.prices_inc_tax'); + } + + return __('lunarpanel.shipping::relationmanagers.shipping_rates.notices.prices_excl_tax'); + } + ), + Forms\Components\Select::make('shipping_method_id') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.shipping_method_id.label') + ) + ->relationship(name: 'shippingMethod', titleAttribute: 'name') + ->columnSpan(2), + Forms\Components\TextInput::make('price') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.price.label') + ) + ->numeric() + ->required() + ->columnSpan(2) + ->afterStateHydrated(static function (Forms\Components\TextInput $component, Model $record = null): void { + if ($record) { + $basePrice = $record->basePrices->first(); + + $component->state( + $basePrice->price->decimal + ); + } + }), + Forms\Components\Repeater::make('prices') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.label') + )->schema([ + Forms\Components\Select::make('customer_group_id') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.customer_group_id.label') + ) + ->options( + fn () => CustomerGroup::all()->pluck('name', 'id') + )->placeholder( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.customer_group_id.placeholder') + )->preload(), + Forms\Components\Select::make('currency_id') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.currency_id.label') + ) + ->options( + fn () => Currency::all()->pluck('name', 'id') + )->default( + Currency::getDefault()->id + )->required()->preload(), + Forms\Components\TextInput::make('tier') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.tier.label') + ) + ->numeric() + ->required(), + Forms\Components\TextInput::make('price') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.price.label') + ) + ->numeric() + ->required(), + ])->afterStateHydrated( + static function (Forms\Components\Repeater $component, Model $record = null): void { + if ($record) { + $component->state( + $record->tieredPrices->map(function ($price) { + return [ + 'customer_group_id' => $price->customer_group_id, + 'price' => $price->price->decimal, + 'currency_id' => $price->currency_id, + 'tier' => $price->tier / 100, + ]; + })->toArray() + ); + } + } + )->columns(4), + ])->columns(1); + } + + public function table(Table $table): Table + { + return $table->columns([ + TextColumn::make('shippingMethod.name') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.table.shipping_method.label') + ), + TextColumn::make('basePrices.0')->formatStateUsing( + fn ($state = null) => $state->price->formatted + )->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.table.price.label') + ), + TextColumn::make('tiered_prices_count') + ->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.table.tiered_prices_count.label') + )->counts('tieredPrices'), + ])->headerActions([ + Tables\Actions\CreateAction::make()->label( + __('lunarpanel.shipping::relationmanagers.shipping_rates.actions.create.label') + )->action(function (Table $table, ShippingRate $shippingRate = null, array $data = []) { + $relationship = $table->getRelationship(); + + $record = new ShippingRate(); + $record->shipping_method_id = $data['shipping_method_id']; + $relationship->save($record); + + static::saveShippingRate($record, $data); + })->slideOver(), + ])->actions([ + Tables\Actions\EditAction::make()->slideOver()->action(function (ShippingRate $shippingRate, array $data) { + static::saveShippingRate($shippingRate, $data); + }), + + ]); + } + + protected static function saveShippingRate(ShippingRate $shippingRate = null, array $data = []): void + { + $currency = Currency::getDefault(); + + $basePrice = $shippingRate->basePrices->first() ?: new Price; + + $basePrice->price = (int) ($data['price'] * $currency->factor); + $basePrice->priceable_type = get_class($shippingRate); + $basePrice->currency_id = $currency->id; + $basePrice->priceable_id = $shippingRate->id; + $basePrice->customer_group_id = null; + $basePrice->save(); + + $shippingRate->tieredPrices()->delete(); + + $tiers = collect($data['prices'] ?? [])->map( + function ($price) { + $price['tier'] = $price['tier'] * 100; + + return $price; + } + ); + + $shippingRate->prices()->createMany($tiers->toArray()); + } +} diff --git a/packages/table-rate-shipping/src/Interfaces/ShippingMethodInterface.php b/packages/table-rate-shipping/src/Interfaces/ShippingRateInterface.php similarity index 76% rename from packages/table-rate-shipping/src/Interfaces/ShippingMethodInterface.php rename to packages/table-rate-shipping/src/Interfaces/ShippingRateInterface.php index 6c0d45d467..a78aba5bb3 100644 --- a/packages/table-rate-shipping/src/Interfaces/ShippingMethodInterface.php +++ b/packages/table-rate-shipping/src/Interfaces/ShippingRateInterface.php @@ -4,9 +4,9 @@ use Lunar\DataTypes\ShippingOption; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; -use Lunar\Shipping\Models\ShippingMethod; +use Lunar\Shipping\Models\ShippingRate; -interface ShippingMethodInterface +interface ShippingRateInterface { /** * Return the name of the shipping method. @@ -21,12 +21,10 @@ public function description(): string; /** * Set the context for the driver. */ - public function on(ShippingMethod $shippingMethod): self; + public function on(ShippingRate $shippingRate): self; /** * Return the shipping option price. - * - * @return ShippingOption */ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption; } diff --git a/packages/table-rate-shipping/src/Managers/ShippingManager.php b/packages/table-rate-shipping/src/Managers/ShippingManager.php index 9ba48b39bd..ae7444726f 100644 --- a/packages/table-rate-shipping/src/Managers/ShippingManager.php +++ b/packages/table-rate-shipping/src/Managers/ShippingManager.php @@ -9,8 +9,8 @@ use Lunar\Shipping\Drivers\ShippingMethods\FreeShipping; use Lunar\Shipping\Drivers\ShippingMethods\ShipBy; use Lunar\Shipping\Interfaces\ShippingMethodManagerInterface; -use Lunar\Shipping\Resolvers\ShippingMethodResolver; use Lunar\Shipping\Resolvers\ShippingOptionResolver; +use Lunar\Shipping\Resolvers\ShippingRateResolver; use Lunar\Shipping\Resolvers\ShippingZoneResolver; class ShippingManager extends Manager implements ShippingMethodManagerInterface @@ -35,7 +35,7 @@ public function createCollectionDriver() return $this->buildProvider(Collection::class); } - public function getSupportedDrivers() + public function getSupportedDrivers(): \Illuminate\Support\Collection { return collect([ 'free-shipping' => $this->createDriver('free-shipping'), @@ -53,27 +53,24 @@ public function getSupportedDrivers() /** * Find the zone for a given address. - * - * @param Cart $cart - * @return Collection */ - public function zones() + public function zones(): ShippingZoneResolver { return new ShippingZoneResolver(); } - public function shippingMethods(Cart $cart = null) + public function shippingRates(Cart $cart = null): ShippingRateResolver { - return new ShippingMethodResolver($cart); + return new ShippingRateResolver($cart); } - public function shippingOptions(Cart $cart = null) + public function shippingOptions(Cart $cart = null): ShippingOptionResolver { return new ShippingOptionResolver($cart); } /** - * Build a tax provider instance. + * Build a shipping provider instance * * @param string $provider * @return mixed diff --git a/packages/table-rate-shipping/src/Models/ShippingExclusionList.php b/packages/table-rate-shipping/src/Models/ShippingExclusionList.php index 664d784258..e367d20105 100644 --- a/packages/table-rate-shipping/src/Models/ShippingExclusionList.php +++ b/packages/table-rate-shipping/src/Models/ShippingExclusionList.php @@ -3,6 +3,8 @@ namespace Lunar\Shipping\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Lunar\Base\BaseModel; use Lunar\Shipping\Factories\ShippingExclusionListFactory; @@ -28,28 +30,32 @@ protected static function newFactory(): ShippingExclusionListFactory return ShippingExclusionListFactory::new(); } + protected static function booted() + { + static::deleting(function (ShippingExclusionList $list) { + $list->exclusions()->delete(); + $list->shippingZones()->detach(); + }); + } + /** * Return the shipping zone relationship. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ - public function exclusions() + public function exclusions(): HasMany { return $this->hasMany(ShippingExclusion::class); } /** * Return the shipping methods relationship. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public function shippingMethods() + public function shippingZones(): BelongsToMany { return $this->belongsToMany( - ShippingMethod::class, - config('lunar.database.table_prefix').'exclusion_list_shipping_method', + ShippingZone::class, + config('lunar.database.table_prefix').'exclusion_list_shipping_zone', 'exclusion_id', - 'method_id', + 'shipping_zone_id', ); } } diff --git a/packages/table-rate-shipping/src/Models/ShippingMethod.php b/packages/table-rate-shipping/src/Models/ShippingMethod.php index 18b3d03c77..bb7be31610 100644 --- a/packages/table-rate-shipping/src/Models/ShippingMethod.php +++ b/packages/table-rate-shipping/src/Models/ShippingMethod.php @@ -2,21 +2,16 @@ namespace Lunar\Shipping\Models; +use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Relations\HasMany; use Lunar\Base\BaseModel; -use Lunar\Base\Purchasable; -use Lunar\Base\Traits\HasPrices; -use Lunar\DataTypes\ShippingOption; -use Lunar\Models\Cart; -use Lunar\Models\TaxClass; use Lunar\Shipping\Database\Factories\ShippingMethodFactory; -use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; use Lunar\Shipping\Facades\Shipping; -class ShippingMethod extends BaseModel implements Purchasable +class ShippingMethod extends BaseModel { - use HasFactory, HasPrices; + use HasFactory; /** * Define which attributes should be @@ -27,139 +22,24 @@ class ShippingMethod extends BaseModel implements Purchasable protected $guarded = []; protected $casts = [ - 'data' => 'object', + 'data' => AsArrayObject::class, ]; /** * Return a new factory instance for the model. - * - * @return \Lunar\Shipping\Factories\ShippingMethodFactory */ protected static function newFactory(): ShippingMethodFactory { return ShippingMethodFactory::new(); } - /** - * Return the shipping zone relationship. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function shippingZone() - { - return $this->belongsTo(ShippingZone::class); - } - - /** - * Return the shipping method driver. - */ - public function getShippingOption(Cart $cart): ?ShippingOption + public function shippingRates(): HasMany { - return $this->driver()->resolve( - new ShippingOptionRequest( - cart: $cart, - shippingMethod: $this - ) - ); + return $this->hasMany(ShippingRate::class); } public function driver() { return Shipping::driver($this->driver); } - - /** - * Return the shipping exclusions property. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function shippingExclusions() - { - return $this->belongsToMany( - ShippingExclusionList::class, - config('lunar.database.table_prefix').'exclusion_list_shipping_method', - 'method_id', - 'exclusion_id', - // 'method_id', - )->withTimestamps(); - } - - public function getPrices(): Collection - { - return $this->prices; - } - - /** - * Return the unit quantity for the variant. - */ - public function getUnitQuantity(): int - { - return 1; - } - - /** - * Return the tax class. - */ - public function getTaxClass(): TaxClass - { - return TaxClass::getDefault(); - } - - public function getTaxReference() - { - return $this->code; - } - - /** - * {@inheritDoc} - */ - public function getType() - { - return 'shipping'; - } - - /** - * {@inheritDoc} - */ - public function isShippable() - { - return false; - } - - /** - * {@inheritDoc} - */ - public function getDescription() - { - return $this->name ?: $this->driver()->name(); - } - - /** - * {@inheritDoc} - */ - public function getOption() - { - return $this->code; - } - - /** - * {@inheritDoc} - */ - public function getOptions() - { - return collect(); - } - - /** - * {@inheritDoc} - */ - public function getIdentifier() - { - return $this->code; - } - - public function getThumbnail() - { - return null; - } } diff --git a/packages/table-rate-shipping/src/Models/ShippingRate.php b/packages/table-rate-shipping/src/Models/ShippingRate.php new file mode 100644 index 0000000000..ce4c4bda7e --- /dev/null +++ b/packages/table-rate-shipping/src/Models/ShippingRate.php @@ -0,0 +1,138 @@ +belongsTo(ShippingZone::class); + } + + public function shippingMethod() + { + return $this->belongsTo(ShippingMethod::class); + } + + public function getPrices(): Collection + { + return $this->prices; + } + + /** + * Return the unit quantity for the variant. + */ + public function getUnitQuantity(): int + { + return 1; + } + + /** + * Return the tax class. + */ + public function getTaxClass(): TaxClass + { + return TaxClass::getDefault(); + } + + public function getTaxReference() + { + return $this->shippingMethod->code; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return 'shipping'; + } + + /** + * {@inheritDoc} + */ + public function isShippable() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getDescription() + { + return $this->shippingMethod->name ?: $this->driver()->name(); + } + + /** + * {@inheritDoc} + */ + public function getOption() + { + return $this->shippingMethod->code; + } + + /** + * {@inheritDoc} + */ + public function getOptions() + { + return collect(); + } + + /** + * {@inheritDoc} + */ + public function getIdentifier() + { + return $this->shippingMethod->code; + } + + public function getThumbnail() + { + return null; + } + + /** + * Return the shipping method driver. + */ + public function getShippingOption(Cart $cart): ?ShippingOption + { + return $this->shippingMethod->driver()->resolve( + new ShippingOptionRequest( + shippingRate: $this, + cart: $cart, + ) + ); + } +} diff --git a/packages/table-rate-shipping/src/Models/ShippingZone.php b/packages/table-rate-shipping/src/Models/ShippingZone.php index 7dc102f380..cee495a6b9 100644 --- a/packages/table-rate-shipping/src/Models/ShippingZone.php +++ b/packages/table-rate-shipping/src/Models/ShippingZone.php @@ -3,6 +3,7 @@ namespace Lunar\Shipping\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Lunar\Base\BaseModel; use Lunar\Models\Country; @@ -76,4 +77,22 @@ public function postcodes() { return $this->hasMany(ShippingZonePostcode::class); } + + public function rates() + { + return $this->hasMany(ShippingRate::class); + } + + /** + * Return the shipping exclusions property. + */ + public function shippingExclusions(): BelongsToMany + { + return $this->belongsToMany( + ShippingExclusionList::class, + config('lunar.database.table_prefix').'exclusion_list_shipping_zone', + 'shipping_zone_id', + 'exclusion_id', + )->withTimestamps(); + } } diff --git a/packages/table-rate-shipping/src/Resolvers/ShippingOptionResolver.php b/packages/table-rate-shipping/src/Resolvers/ShippingOptionResolver.php index a0c9f50a24..03986491bd 100644 --- a/packages/table-rate-shipping/src/Resolvers/ShippingOptionResolver.php +++ b/packages/table-rate-shipping/src/Resolvers/ShippingOptionResolver.php @@ -47,10 +47,10 @@ public function get(ShippingOptionLookup $shippingOptionLookup): Collection return collect(); } - foreach ($shippingOptionLookup->shippingMethods as $shippingMethod) { + foreach ($shippingOptionLookup->shippingRates as $shippingRate) { $shippingOptions->push((object) [ - 'shippingMethod' => $shippingMethod, - 'option' => $shippingMethod->getShippingOption($this->cart), + 'shippingRate' => $shippingRate, + 'option' => $shippingRate->getShippingOption($this->cart), ]); } @@ -61,7 +61,7 @@ public function get(ShippingOptionLookup $shippingOptionLookup): Collection })->each(function ($option) { ShippingOptionResolvedEvent::dispatch( $this->cart, - $option->shippingMethod, + $option->shippingRate, $option->option ); }); diff --git a/packages/table-rate-shipping/src/Resolvers/ShippingMethodResolver.php b/packages/table-rate-shipping/src/Resolvers/ShippingRateResolver.php similarity index 85% rename from packages/table-rate-shipping/src/Resolvers/ShippingMethodResolver.php rename to packages/table-rate-shipping/src/Resolvers/ShippingRateResolver.php index 1b6f42d0a3..8acbca8758 100644 --- a/packages/table-rate-shipping/src/Resolvers/ShippingMethodResolver.php +++ b/packages/table-rate-shipping/src/Resolvers/ShippingRateResolver.php @@ -9,7 +9,7 @@ use Lunar\Shipping\DataTransferObjects\PostcodeLookup; use Lunar\Shipping\Facades\Shipping; -class ShippingMethodResolver +class ShippingRateResolver { /** * The cart to use when resolving. @@ -143,11 +143,13 @@ public function get(): Collection ) )->get(); - $shippingMethods = collect(); + $shippingRates = collect(); foreach ($zones as $zone) { - $shippingMethods = $zone->shippingMethods - ->reject(function ($method) { + $shippingRates = $zone->rates + ->reject(function ($rate) { + $method = $rate->shippingMethod; + if (! $method->cutoff) { return false; } @@ -159,23 +161,23 @@ public function get(): Collection ->set('second', $s) ->isPast(); }) - ->reject(function ($method) { - if ($this->allCartItemsAreInStock || ! ($method->stock_available ?? false)) { + ->reject(function ($rate) { + if ($this->allCartItemsAreInStock || ! ($rate->shippingMethod->stock_available ?? false)) { return false; } return true; }); - foreach ($shippingMethods as $shippingMethod) { - $shippingMethods->push( - $shippingMethod + foreach ($shippingRates as $shippingRate) { + $shippingRates->push( + $shippingRate ); } } - return $shippingMethods->filter()->unique(function ($method) { - return $method->code; + return $shippingRates->filter()->unique(function ($rate) { + return $rate->shippingMethod->code; }); } } diff --git a/packages/table-rate-shipping/src/ShippingModifier.php b/packages/table-rate-shipping/src/ShippingModifier.php index c1d984f2c2..7283fac565 100644 --- a/packages/table-rate-shipping/src/ShippingModifier.php +++ b/packages/table-rate-shipping/src/ShippingModifier.php @@ -11,11 +11,11 @@ class ShippingModifier { public function handle(Cart $cart) { - $shippingMethods = Shipping::shippingMethods($cart)->get(); + $shippingRates = Shipping::shippingRates($cart)->get(); $options = Shipping::shippingOptions($cart)->get( new ShippingOptionLookup( - shippingMethods: $shippingMethods + shippingRates: $shippingRates ) ); diff --git a/packages/table-rate-shipping/src/ShippingPlugin.php b/packages/table-rate-shipping/src/ShippingPlugin.php new file mode 100644 index 0000000000..6ffd06719a --- /dev/null +++ b/packages/table-rate-shipping/src/ShippingPlugin.php @@ -0,0 +1,57 @@ +navigationGroups([ + NavigationGroup::make('shipping') + ->label( + fn () => __('lunarpanel.shipping::plugin.navigation.group') + ), + ])->resources([ + ShippingMethodResource::class, + ShippingZoneResource::class, + ShippingExclusionListResource::class, + ]); + + FilamentIcon::register([ + 'lunar::shipping-rates' => 'lucide-coins', + 'lunar::shipping-zones' => 'lucide-globe-2', + 'lunar::shipping-methods' => 'lucide-truck', + 'lunar::shipping-exclusion-lists' => 'lucide-package-minus', + ]); + } + + public static function make(): static + { + return app(static::class); + } + + public function panel(Panel $panel): Panel + { + return $panel; + } + + // ... +} diff --git a/packages/table-rate-shipping/src/ShippingServiceProvider.php b/packages/table-rate-shipping/src/ShippingServiceProvider.php index 8bddb50e25..0b2227df05 100644 --- a/packages/table-rate-shipping/src/ShippingServiceProvider.php +++ b/packages/table-rate-shipping/src/ShippingServiceProvider.php @@ -22,14 +22,14 @@ public function boot(ShippingModifiers $shippingModifiers) return; } - $this->loadTranslationsFrom(__DIR__.'/../lang', 'shipping'); + $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'lunarpanel.shipping'); $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); $this->loadViewsFrom(__DIR__.'/../resources/views', 'shipping'); $shippingModifiers->add( - ShippingModifier::class, // TODO: param typed incorrectly + ShippingModifier::class, ); Order::observe(OrderObserver::class); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php index 2ada975c9e..a16cb110c1 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php @@ -27,18 +27,22 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'free-shipping', 'data' => [], ]); + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + $cart = $this->createCart($currency, 500); $driver = new Collection(); $request = new ShippingOptionRequest( cart: $cart, - shippingMethod: $shippingMethod + shippingRate: $shippingRate ); $shippingOption = $driver->resolve($request); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php index f8a9f678d7..d23748c133 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php @@ -27,7 +27,6 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'flat-rate', 'data' => [ 'minimum_spend' => [ @@ -36,7 +35,13 @@ ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -50,7 +55,7 @@ $request = new ShippingOptionRequest( cart: $cart, - shippingMethod: $shippingMethod + shippingRate: $shippingRate ); $shippingOption = $driver->resolve($request); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php index 387a3c5c2c..2aeccd90e7 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php @@ -27,7 +27,6 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'free-shipping', 'data' => [ 'minimum_spend' => [ @@ -36,13 +35,19 @@ ], ]); + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + $cart = $this->createCart($currency, 500); $driver = new FreeShipping(); $request = new ShippingOptionRequest( cart: $cart, - shippingMethod: $shippingMethod + shippingRate: $shippingRate ); $shippingOption = $driver->resolve($request); @@ -64,7 +69,6 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'free-shipping', 'data' => [ 'minimum_spend' => [ @@ -73,13 +77,19 @@ ], ]); + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + $cart = $this->createCart($currency, 50); $driver = new FreeShipping(); $request = new ShippingOptionRequest( cart: $cart, - shippingMethod: $shippingMethod + shippingRate: $shippingRate ); $shippingOption = $driver->resolve($request); @@ -101,7 +111,6 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'free-shipping', 'data' => [ 'minimum_spend' => [ @@ -110,13 +119,19 @@ ], ]); + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + $cart = $this->createCart($currency, 10000); $driver = new FreeShipping(); $request = new ShippingOptionRequest( + shippingRate: $shippingRate, cart: $cart, - shippingMethod: $shippingMethod ); $shippingOption = $driver->resolve($request); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php index 05cdafd179..abd16e9293 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php @@ -27,14 +27,19 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'charge_by' => 'cart_total', ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 1000, 'tier' => 1, @@ -47,15 +52,15 @@ ], ]); - expect($shippingMethod->prices)->toHaveCount(2); + expect($shippingRate->prices)->toHaveCount(2); $cart = $this->createCart($currency, 100); $driver = new ShipBy(); $request = new ShippingOptionRequest( + shippingRate: $shippingRate, cart: $cart, - shippingMethod: $shippingMethod ); $shippingOption = $driver->resolve($request); @@ -69,8 +74,8 @@ $driver = new ShipBy(); $request = new ShippingOptionRequest( - cart: $cart, - shippingMethod: $shippingMethod + shippingRate: $shippingRate, + cart: $cart ); $shippingOption = $driver->resolve($request); @@ -80,6 +85,67 @@ expect($shippingOption->price->value)->toEqual(500); }); +test('can get shipping option by cart total when prices include tax', function () { + + \Illuminate\Support\Facades\Config::set('lunar.pricing.stored_inclusive_of_tax', true); + + $currency = Currency::factory()->create([ + 'default' => true, + ]); + + TaxClass::factory()->create([ + 'default' => true, + ]); + + $shippingZone = ShippingZone::factory()->create([ + 'type' => 'countries', + ]); + + $shippingMethod = ShippingMethod::factory()->create([ + 'driver' => 'ship-by', + 'data' => [ + 'charge_by' => 'cart_total', + ], + ]); + + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ + [ + 'price' => 1000, + 'tier' => 1, + 'currency_id' => $currency->id, + ], + [ + 'price' => 500, + 'tier' => 700, + 'currency_id' => $currency->id, + ], + ]); + + expect($shippingRate->prices)->toHaveCount(2); + + $cart = $this->createCart($currency, 700); + + $driver = new ShipBy(); + + $request = new ShippingOptionRequest( + shippingRate: $shippingRate, + cart: $cart, + ); + + $shippingOption = $driver->resolve($request); + + expect($shippingOption)->toBeInstanceOf(ShippingOption::class); + + expect($shippingOption->price->value)->toEqual(500); + +})->group('thisone'); + test('can get shipping option if outside tier without default price', function () { // Boom. $currency = Currency::factory()->create([ @@ -95,14 +161,19 @@ ]); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'charge_by' => 'cart_total', ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 500, 'tier' => 700, @@ -110,15 +181,15 @@ ], ]); - expect($shippingMethod->prices)->toHaveCount(1); + expect($shippingRate->prices)->toHaveCount(1); $cart = $this->createCart($currency, 100); $driver = new ShipBy(); $request = new ShippingOptionRequest( - cart: $cart, - shippingMethod: $shippingMethod + shippingRate: $shippingRate, + cart: $cart ); $this->expectException(\ErrorException::class); diff --git a/tests/shipping/Unit/Managers/ShippingManagerTest.php b/tests/shipping/Unit/Managers/ShippingManagerTest.php index 63b349f203..832533c965 100644 --- a/tests/shipping/Unit/Managers/ShippingManagerTest.php +++ b/tests/shipping/Unit/Managers/ShippingManagerTest.php @@ -19,7 +19,7 @@ expect($resolver)->toBeInstanceOf(ShippingZoneResolver::class); }); -test('can fetch expected shipping methods', function () { +test('can fetch expected shipping rates', function () { $currency = Currency::factory()->create([ 'default' => true, ]); @@ -37,7 +37,6 @@ $shippingZone->countries()->attach($country); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'minimum_spend' => [ @@ -46,7 +45,13 @@ ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -73,9 +78,9 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); - expect($shippingMethods)->toHaveCount(1); + expect($shippingRates)->toHaveCount(1); }); diff --git a/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php index db60ef882c..e929d36e8f 100644 --- a/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php @@ -32,7 +32,6 @@ $shippingZone->countries()->attach($country); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'minimum_spend' => [ @@ -41,7 +40,13 @@ ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory() + ->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -68,7 +73,7 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); @@ -76,7 +81,7 @@ $cart->refresh()->calculate() )->get( new ShippingOptionLookup( - shippingMethods: $shippingMethods + shippingRates: $shippingRates ) ); diff --git a/tests/shipping/Unit/Resolvers/ShippingMethodResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php similarity index 80% rename from tests/shipping/Unit/Resolvers/ShippingMethodResolverTest.php rename to tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php index 9a11a9edfc..87355fda3b 100644 --- a/tests/shipping/Unit/Resolvers/ShippingMethodResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php @@ -16,7 +16,7 @@ uses(\Illuminate\Foundation\Testing\RefreshDatabase::class); uses(\Lunar\Tests\Shipping\TestUtils::class); -test('can fetch shipping methods by country', function () { +test('can fetch shipping rates by country', function () { $currency = Currency::factory()->create([ 'default' => true, ]); @@ -34,7 +34,6 @@ $shippingZone->countries()->attach($country); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'minimum_spend' => [ @@ -43,7 +42,12 @@ ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -70,12 +74,12 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); - expect($shippingMethods)->toHaveCount(1); - expect($shippingMethods->first()->id)->toEqual($shippingMethod->id); + expect($shippingRates)->toHaveCount(1); + expect($shippingRates->first()->id)->toEqual($shippingRate->id); $cart = $this->createCart($currency, 500); @@ -88,14 +92,14 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); - expect($shippingMethods)->toBeEmpty(); + expect($shippingRates)->toBeEmpty(); }); -test('can fetch shipping methods by state', function () { +test('can fetch shipping rates by state', function () { $currency = Currency::factory()->create([ 'default' => true, ]); @@ -117,7 +121,6 @@ $shippingZone->states()->attach($state); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'minimum_spend' => [ @@ -126,7 +129,12 @@ ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -153,15 +161,15 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); - expect($shippingMethods)->toHaveCount(1); - expect($shippingMethods->first()->id)->toEqual($shippingMethod->id); + expect($shippingRates)->toHaveCount(1); + expect($shippingRates->first()->id)->toEqual($shippingRate->id); }); -test('can fetch shipping methods by postcode', function () { +test('can fetch shipping rates by postcode', function () { $currency = Currency::factory()->create([ 'default' => true, ]); @@ -183,7 +191,6 @@ $shippingZone->countries()->attach($country); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'minimum_spend' => [ @@ -192,7 +199,12 @@ ], ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -220,15 +232,15 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); - expect($shippingMethods)->toHaveCount(1); - expect($shippingMethods->first()->id)->toEqual($shippingMethod->id); + expect($shippingRates)->toHaveCount(1); + expect($shippingRates->first()->id)->toEqual($shippingRate->id); }); -test('can reject shipping methods when stock is not available', function () { +test('can reject shipping rates when stock is not available', function () { $currency = Currency::factory()->create([ 'default' => true, ]); @@ -250,7 +262,6 @@ $shippingZone->countries()->attach($country); $shippingMethod = ShippingMethod::factory()->create([ - 'shipping_zone_id' => $shippingZone->id, 'driver' => 'ship-by', 'data' => [ 'minimum_spend' => [ @@ -260,7 +271,12 @@ 'stock_available' => 1, ]); - $shippingMethod->prices()->createMany([ + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ [ 'price' => 600, 'tier' => 1, @@ -305,9 +321,9 @@ ])->toArray() ); - $shippingMethods = Shipping::shippingMethods( + $shippingRates = Shipping::shippingRates( $cart->refresh()->calculate() )->get(); - expect($shippingMethods)->toHaveCount(0); + expect($shippingRates)->toHaveCount(0); }); From 163b8f6954ea60a1967b0bf55d61a06e84805966 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Tue, 23 Jan 2024 10:46:04 +0000 Subject: [PATCH 05/34] Bump Apex Charts version --- composer.json | 4 ++-- packages/admin/composer.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 91a731ba04..e4ae1a1887 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "kalnoy/nestedset": "^6.0", "laravel/framework": "^10.0", "laravel/scout": "^10.0", - "leandrocfe/filament-apex-charts": "^3.0", + "leandrocfe/filament-apex-charts": "^3.1.2", "lukascivil/treewalker": "0.9.1", "marvinosswald/filament-input-select-affix": "^0.1.0", "php": "^8.1", @@ -122,4 +122,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/packages/admin/composer.json b/packages/admin/composer.json index e728cf3af1..b08e6b463a 100644 --- a/packages/admin/composer.json +++ b/packages/admin/composer.json @@ -21,7 +21,7 @@ "barryvdh/laravel-dompdf": "^2.0", "technikermathe/blade-lucide-icons": "^v2.24.0", "marvinosswald/filament-input-select-affix": "^0.1.0", - "leandrocfe/filament-apex-charts": "^3.0", + "leandrocfe/filament-apex-charts": "^3.1.2", "awcodes/shout": "^2.0.2" }, "extra": { From dac63b70986cca7d3de4dab971d5c9d00183d58a Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 24 Jan 2024 13:26:42 +0000 Subject: [PATCH 06/34] Hotfix - Fix tests (#1496) --- .../ProductResource/Pages/ManageProductPricingTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php index 5128174540..8b35b9623a 100644 --- a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php +++ b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php @@ -96,8 +96,6 @@ \Livewire\Livewire::test( \Lunar\Admin\Filament\Resources\ProductResource\Pages\ManageProductPricing::class, [ 'record' => $record->getRouteKey(), - ])->assertFormSet([ - 'price' => null, ])->callTableAction('create', data: [ 'price' => 10.99, 'currency_id' => $currency->id, From efbc4ca57d248ec3944e09e798a3394a78bc3fa0 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Fri, 26 Jan 2024 10:22:27 +0100 Subject: [PATCH 07/34] Fix - ListPageExtension not trigger the headerActions method (#1497) --- .../Filament/Resources/ChannelResource/Pages/ListChannels.php | 2 +- .../CollectionGroupResource/Pages/ListCollectionGroups.php | 2 +- .../Resources/CollectionResource/Pages/ListCollections.php | 2 +- .../Resources/CurrencyResource/Pages/ListCurrencies.php | 2 +- .../CustomerGroupResource/Pages/ListCustomerGroups.php | 2 +- .../src/Filament/Resources/OrderResource/Pages/ListOrders.php | 2 +- .../src/Filament/Resources/StaffResource/Pages/ListStaff.php | 2 +- .../Resources/TaxClassResource/Pages/ListTaxClasses.php | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/admin/src/Filament/Resources/ChannelResource/Pages/ListChannels.php b/packages/admin/src/Filament/Resources/ChannelResource/Pages/ListChannels.php index 06fffa13d4..d4ce4bc81d 100644 --- a/packages/admin/src/Filament/Resources/ChannelResource/Pages/ListChannels.php +++ b/packages/admin/src/Filament/Resources/ChannelResource/Pages/ListChannels.php @@ -10,7 +10,7 @@ class ListChannels extends BaseListRecords { protected static string $resource = ChannelResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ Actions\CreateAction::make(), diff --git a/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php b/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php index d370a75189..b17fc5a0c4 100644 --- a/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php +++ b/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php @@ -10,7 +10,7 @@ class ListCollectionGroups extends ListRecords { protected static string $resource = CollectionGroupResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ Actions\CreateAction::make(), diff --git a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php index ce671637d4..48f7f64f15 100644 --- a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php +++ b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php @@ -14,7 +14,7 @@ public function mount(): void abort(404); } - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return []; } diff --git a/packages/admin/src/Filament/Resources/CurrencyResource/Pages/ListCurrencies.php b/packages/admin/src/Filament/Resources/CurrencyResource/Pages/ListCurrencies.php index eed3a7fdd3..b399e3eb1a 100644 --- a/packages/admin/src/Filament/Resources/CurrencyResource/Pages/ListCurrencies.php +++ b/packages/admin/src/Filament/Resources/CurrencyResource/Pages/ListCurrencies.php @@ -10,7 +10,7 @@ class ListCurrencies extends BaseListRecords { protected static string $resource = CurrencyResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ Actions\CreateAction::make(), diff --git a/packages/admin/src/Filament/Resources/CustomerGroupResource/Pages/ListCustomerGroups.php b/packages/admin/src/Filament/Resources/CustomerGroupResource/Pages/ListCustomerGroups.php index 2c638d4276..30a8ff9ff4 100644 --- a/packages/admin/src/Filament/Resources/CustomerGroupResource/Pages/ListCustomerGroups.php +++ b/packages/admin/src/Filament/Resources/CustomerGroupResource/Pages/ListCustomerGroups.php @@ -10,7 +10,7 @@ class ListCustomerGroups extends BaseListRecords { protected static string $resource = CustomerGroupResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ Actions\CreateAction::make(), diff --git a/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php b/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php index 19c1d16669..203dcafe4d 100644 --- a/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php +++ b/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php @@ -13,7 +13,7 @@ class ListOrders extends BaseListRecords { protected static string $resource = OrderResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ // Actions\CreateAction::make(), diff --git a/packages/admin/src/Filament/Resources/StaffResource/Pages/ListStaff.php b/packages/admin/src/Filament/Resources/StaffResource/Pages/ListStaff.php index 03eff6d798..1870003fa1 100644 --- a/packages/admin/src/Filament/Resources/StaffResource/Pages/ListStaff.php +++ b/packages/admin/src/Filament/Resources/StaffResource/Pages/ListStaff.php @@ -11,7 +11,7 @@ class ListStaff extends BaseListRecords { protected static string $resource = StaffResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ Actions\Action::make('access-control') diff --git a/packages/admin/src/Filament/Resources/TaxClassResource/Pages/ListTaxClasses.php b/packages/admin/src/Filament/Resources/TaxClassResource/Pages/ListTaxClasses.php index 0f2010ed40..4f60c80e35 100644 --- a/packages/admin/src/Filament/Resources/TaxClassResource/Pages/ListTaxClasses.php +++ b/packages/admin/src/Filament/Resources/TaxClassResource/Pages/ListTaxClasses.php @@ -10,7 +10,7 @@ class ListTaxClasses extends BaseListRecords { protected static string $resource = TaxClassResource::class; - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ Actions\CreateAction::make(), From 0d3f59951505e982d6098c07cd377febc2cd3075 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Fri, 26 Jan 2024 13:11:28 +0100 Subject: [PATCH 08/34] Fix min / max value on FieldType number (#1491) --- packages/admin/src/Support/FieldTypes/Number.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/admin/src/Support/FieldTypes/Number.php b/packages/admin/src/Support/FieldTypes/Number.php index bac2d70314..fe24ab5e42 100644 --- a/packages/admin/src/Support/FieldTypes/Number.php +++ b/packages/admin/src/Support/FieldTypes/Number.php @@ -19,11 +19,11 @@ public static function getFilamentComponent(Attribute $attribute): Component $input = TextField::getFilamentComponent($attribute)->numeric(); if ($min) { - $input->min($min); + $input->minValue($min); } if ($max) { - $input->max($max); + $input->maxValue($max); } return $input; From 8a92100e004712d1692e3823ee06cd186a740c75 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Mon, 29 Jan 2024 14:38:10 +0000 Subject: [PATCH 09/34] Make currency id not nullable on prices (#1511) --- .../ProductResource/Pages/ListProducts.php | 1 + ...date_nullable_currency_on_prices_table.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 packages/core/database/migrations/2024_01_29_100000_update_nullable_currency_on_prices_table.php diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php index 22d11b789f..68f323f304 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php @@ -79,6 +79,7 @@ public static function createRecord(array $data, string $model): Model ]); $variant->prices()->create([ 'tier' => 1, + 'currency_id' => $currency->id, 'price' => (int) bcmul($data['base_price'], $currency->factor), ]); DB::commit(); diff --git a/packages/core/database/migrations/2024_01_29_100000_update_nullable_currency_on_prices_table.php b/packages/core/database/migrations/2024_01_29_100000_update_nullable_currency_on_prices_table.php new file mode 100644 index 0000000000..472f19f1fd --- /dev/null +++ b/packages/core/database/migrations/2024_01_29_100000_update_nullable_currency_on_prices_table.php @@ -0,0 +1,22 @@ +-prefix.'prices', function (Blueprint $table) { + $table->foreignId('currency_id')->nullable(false)->change(); + }); + } + + public function down() + { + Schema::table($this->prefix.'prices', function (Blueprint $table) { + $table->foreignId('currency_id')->nullable(true)->change(); + }); + } +} From d8bbaeeacc17202d49d090fb72f09e1a64d052bb Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Mon, 29 Jan 2024 15:19:07 +0000 Subject: [PATCH 10/34] Feat - Variant Generation (#1488) This PR looks to add variant generation to the filament hub. This isn't a port of the current offering in `0.7` but is instead a complete reimagining of how it should work, inspired by some other popular systems out there. --- packages/admin/resources/css/index.css | 2 +- packages/admin/resources/dist/lunar-panel.css | 2058 ++++++++++++++++- packages/admin/resources/dist/lunar-panel.js | 1 + .../admin/resources/lang/en/components.php | 21 + packages/admin/resources/lang/en/product.php | 71 - .../admin/resources/lang/en/productoption.php | 88 + .../resources/lang/en/productvariant.php | 102 + packages/admin/resources/lang/en/widgets.php | 11 + .../views/actions/switch-variant.blade.php | 5 + .../product-option-list-values.blade.php | 92 + .../variants/product-options-list.blade.php | 98 + .../forms/components/media-select.blade.php | 18 + .../widgets/product-options.blade.php | 229 ++ .../Products/MapVariantsToProductOptions.php | 82 + .../Filament/Resources/ProductResource.php | 12 + .../Pages/ManageProductIdentifiers.php | 19 +- .../Pages/ManageProductInventory.php | 49 +- .../Pages/ManageProductShipping.php | 40 +- .../Pages/ManageProductVariants.php | 9 + .../Widgets/ProductOptionsWidget.php | 460 ++++ .../Resources/ProductVariantResource.php | 316 +++ .../Pages/EditProductVariant.php | 78 + .../Pages/ListProductVariants.php | 26 + .../Pages/ManageVariantIdentifiers.php | 85 + .../Pages/ManageVariantInventory.php | 85 + .../Pages/ManageVariantMedia.php | 122 + .../Pages/ManageVariantPricing.php | 158 ++ .../Pages/ManageVariantShipping.php | 135 ++ .../Widgets/Products/VariantSwitcherTable.php | 102 + packages/admin/src/LunarPanelManager.php | 1 + .../Support/Forms/Components/MediaSelect.php | 15 + ...100010_update_product_option_relations.php | 2 +- ...100000_update_product_option_handle_fk.php | 28 + packages/core/src/LunarServiceProvider.php | 6 + packages/core/src/Models/ProductVariant.php | 2 + .../src/Observers/ProductOptionObserver.php | 18 + .../src/Observers/ProductVariantObserver.php | 20 + .../MapVariantsToProductOptionsTest.php | 79 + 38 files changed, 4585 insertions(+), 160 deletions(-) create mode 100644 packages/admin/resources/lang/en/productvariant.php create mode 100644 packages/admin/resources/views/actions/switch-variant.blade.php create mode 100644 packages/admin/resources/views/components/products/variants/product-option-list-values.blade.php create mode 100644 packages/admin/resources/views/components/products/variants/product-options-list.blade.php create mode 100644 packages/admin/resources/views/forms/components/media-select.blade.php create mode 100644 packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php create mode 100644 packages/admin/src/Actions/Products/MapVariantsToProductOptions.php create mode 100644 packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/EditProductVariant.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantIdentifiers.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantInventory.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantMedia.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php create mode 100644 packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantShipping.php create mode 100644 packages/admin/src/Filament/Widgets/Products/VariantSwitcherTable.php create mode 100644 packages/admin/src/Support/Forms/Components/MediaSelect.php create mode 100644 packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php create mode 100644 packages/core/src/Observers/ProductOptionObserver.php create mode 100644 packages/core/src/Observers/ProductVariantObserver.php create mode 100644 tests/admin/Unit/Actions/Products/MapVariantsToProductOptionsTest.php diff --git a/packages/admin/resources/css/index.css b/packages/admin/resources/css/index.css index 29244ec365..b3949d56cd 100644 --- a/packages/admin/resources/css/index.css +++ b/packages/admin/resources/css/index.css @@ -1 +1 @@ -@import '../../vendor/filament/filament/resources/css/theme.css'; \ No newline at end of file +@import '../../vendor/filament/filament/resources/css/theme.css'; diff --git a/packages/admin/resources/dist/lunar-panel.css b/packages/admin/resources/dist/lunar-panel.css index e606e06e68..c9914e7c44 100644 --- a/packages/admin/resources/dist/lunar-panel.css +++ b/packages/admin/resources/dist/lunar-panel.css @@ -1 +1,2057 @@ -/*! tailwindcss v3.4.0 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:rgba(var(--gray-200),1)}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family),ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:rgba(var(--gray-400),1)}input::placeholder,textarea::placeholder{opacity:1;color:rgba(var(--gray-400),1)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}input::placeholder,textarea::placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='rgba(var(--gray-500), var(--tw-stroke-opacity, 1))' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}@media (forced-colors:active){[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}@media (forced-colors:active){[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:#0000;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active){[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}:root.dark{color-scheme:dark}[data-field-wrapper]{scroll-margin-top:8rem}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.-left-\[calc\(0\.5rem_-_1px\)\]{left:calc(-.5rem - -1px)}.-left-\[calc\(0\.75rem_-_1px\)\]{left:calc(-.75rem - -1px)}.left-0{left:0}.left-5{left:1.25rem}.right-0{right:0}.top-\[2px\]{top:2px}.-my-8{margin-top:-2rem;margin-bottom:-2rem}.-ml-\[5px\]{margin-left:-5px}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-8{margin-left:2rem}.mt-4{margin-top:1rem}.flow-root{display:flow-root}.w-1\/3{width:33.333333%}.w-\[2px\]{width:2px}.min-w-\[50vw\]{min-width:50vw}.min-w-full{min-width:100%}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-y-2{--tw-translate-y:-0.5rem}.-translate-y-2,.-translate-y-px{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-px{--tw-translate-y:-1px}.\!cursor-default{cursor:default!important}.cursor-grab{cursor:grab}.scroll-mt-32{scroll-margin-top:8rem}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-2\.5{gap:.625rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.divide-y-2>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(2px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(2px*var(--tw-divide-y-reverse))}.\!overflow-auto{overflow:auto!important}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity))}.border-orange-300{--tw-border-opacity:1;border-color:rgb(253 186 116/var(--tw-border-opacity))}.border-sky-300{--tw-border-opacity:1;border-color:rgb(125 211 252/var(--tw-border-opacity))}.border-white\/10{border-color:#ffffff1a}.bg-gray-300\/20{background-color:rgba(var(--gray-300),.2)}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity))}.bg-sky-50{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity))}.bg-sky-500{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity))}.bg-teal-500{--tw-bg-opacity:1;background-color:rgb(20 184 166/var(--tw-bg-opacity))}.bg-white\/70{background-color:#ffffffb3}.\!p-0{padding:0!important}.\!p-3{padding:.75rem!important}.\!ps-6{padding-inline-start:1.5rem!important}.pl-2{padding-left:.5rem}.pl-8{padding-left:2rem}.pt-8{padding-top:2rem}.pt-\[1px\]{padding-top:1px}.pt-\[5px\]{padding-top:5px}.\!text-red-400\/60{color:#f8717199!important}.\!text-red-400\/80{color:#f87171cc!important}.text-gray-900{--tw-text-opacity:1;color:rgba(var(--gray-900),var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-sky-600{--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-1,.ring-4{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-100),var(--tw-ring-opacity))}.ring-purple-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity))}.ring-sky-100{--tw-ring-opacity:1;--tw-ring-color:rgb(224 242 254/var(--tw-ring-opacity))}.ring-teal-100{--tw-ring-opacity:1;--tw-ring-color:rgb(204 251 241/var(--tw-ring-opacity))}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:bg-danger-100\/80:hover{background-color:rgba(var(--danger-100),.8)}.hover\:bg-primary-400\/10:hover{background-color:rgba(var(--primary-400),.1)}.hover\:bg-primary-50:hover{--tw-bg-opacity:1;background-color:rgba(var(--primary-50),var(--tw-bg-opacity))}.hover\:bg-primary-50\/50:hover{background-color:rgba(var(--primary-50),.5)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.group:hover .group-hover\:flex{display:flex}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/button:hover .group-hover\/button\:text-gray-500{--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}.group:hover .group-hover\:text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.group:hover .group-hover\:text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}@media (min-width:768px){.md\:min-w-\[32rem\]{min-width:32rem}.md\:justify-between{justify-content:space-between}}:is(:where([dir=ltr]) .ltr\:rotate-90){--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is(:where([dir=rtl]) .rtl\:\!rotate-90){--tw-rotate:90deg!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}:is(:where([dir=rtl]) .rtl\:rotate-180){--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is(:where([dir=rtl]) .rtl\:space-x-reverse)>:not([hidden])~:not([hidden]){--tw-space-x-reverse:1}:is(:where([dir=rtl]) .rtl\:text-right){text-align:right}:is(:where(.dark) .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgba(var(--gray-600),var(--tw-divide-opacity))}:is(:where(.dark) .dark\:divide-white\/10)>:not([hidden])~:not([hidden]){border-color:#ffffff1a}:is(:where(.dark) .dark\:border-b){border-bottom-width:1px}:is(:where(.dark) .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgba(var(--gray-600),var(--tw-border-opacity))}:is(:where(.dark) .dark\:border-white\/10){border-color:#ffffff1a}:is(:where(.dark) .dark\:bg-custom-400\/10){background-color:rgba(var(--c-400),.1)}:is(:where(.dark) .dark\:bg-gray-400\/10){background-color:rgba(var(--gray-400),.1)}:is(:where(.dark) .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgba(var(--gray-600),var(--tw-bg-opacity))}:is(:where(.dark) .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgba(var(--gray-800),var(--tw-bg-opacity))}:is(:where(.dark) .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}:is(:where(.dark) .dark\:bg-green-400\/10){background-color:#4ade801a}:is(:where(.dark) .dark\:bg-orange-400\/10){background-color:#fb923c1a}:is(:where(.dark) .dark\:bg-sky-400\/10){background-color:#38bdf81a}:is(:where(.dark) .dark\:bg-white\/10){background-color:#ffffff1a}:is(:where(.dark) .dark\:bg-white\/5){background-color:#ffffff0d}:is(:where(.dark) .dark\:\!text-red-400\/60){color:#f8717199!important}:is(:where(.dark) .dark\:text-custom-400){--tw-text-opacity:1;color:rgba(var(--c-400),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-100){--tw-text-opacity:1;color:rgba(var(--gray-100),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-200){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-300){--tw-text-opacity:1;color:rgba(var(--gray-300),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-500){--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-green-400){--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-green-400\/80){color:#4ade80cc}:is(:where(.dark) .dark\:text-orange-400){--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-primary-400){--tw-text-opacity:1;color:rgba(var(--primary-400),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-primary-400\/80){color:rgba(var(--primary-400),.8)}:is(:where(.dark) .dark\:text-red-400\/80){color:#f87171cc}:is(:where(.dark) .dark\:text-sky-400){--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(:where(.dark) .dark\:ring-custom-400\/30){--tw-ring-color:rgba(var(--c-400),0.3)}:is(:where(.dark) .dark\:ring-gray-400\/20){--tw-ring-color:rgba(var(--gray-400),0.2)}:is(:where(.dark) .dark\:ring-gray-600){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-600),var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-gray-700){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-700),var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-purple-800){--tw-ring-opacity:1;--tw-ring-color:rgb(107 33 168/var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-sky-800){--tw-ring-opacity:1;--tw-ring-color:rgb(7 89 133/var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-teal-800){--tw-ring-opacity:1;--tw-ring-color:rgb(17 94 89/var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-white\/10){--tw-ring-color:#ffffff1a}:is(:where(.dark) .dark\:ring-white\/20){--tw-ring-color:#fff3}:is(:where(.dark) .dark\:hover\:bg-danger-300\/20:hover){background-color:rgba(var(--danger-300),.2)}:is(:where(.dark) .dark\:hover\:bg-white\/5:hover){background-color:#ffffff0d}:is(:where(.dark) .dark\:focus-visible\:ring-primary-500:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--primary-500),var(--tw-ring-opacity))}:is(:where(.dark) .group\/button:hover .dark\:group-hover\/button\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(:where(.dark) .group:hover .dark\:group-hover\:text-green-400){--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}:is(:where(.dark) .group:hover .dark\:group-hover\:text-red-400){--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.\[\&_table\]\:h-\[1px\] table{height:1px} \ No newline at end of file +/* +! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: rgba(var(--gray-200), 1); + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: var(--font-family), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: rgba(var(--gray-400), 1); + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: rgba(var(--gray-400), 1); + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: rgba(var(--gray-500), var(--tw-border-opacity, 1)); + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: rgba(var(--gray-500), var(--tw-text-opacity, 1)); + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: rgba(var(--gray-500), var(--tw-text-opacity, 1)); + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; + text-align: inherit; +} + +::-webkit-datetime-edit { + display: inline-flex; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='rgba(var(--gray-500)%2c var(--tw-stroke-opacity%2c 1))' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +[multiple],[size]:where(select:not([size="1"])) { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: rgba(var(--gray-500), var(--tw-border-opacity, 1)); + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +@media (forced-colors: active) { + [type='checkbox']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +@media (forced-colors: active) { + [type='radio']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +@media (forced-colors: active) { + [type='checkbox']:indeterminate { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +:root.dark { + color-scheme: dark; +} + +/* When scrolling to validation error, do not hide element behind the top bar */ + +[data-field-wrapper] { + scroll-margin-top: 8rem; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.pointer-events-none { + pointer-events: none; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.inset-y-0 { + top: 0px; + bottom: 0px; +} + +.-left-\[calc\(0\.5rem_-_1px\)\] { + left: calc(calc(0.5rem - 1px) * -1); +} + +.-left-\[calc\(0\.75rem_-_1px\)\] { + left: calc(calc(0.75rem - 1px) * -1); +} + +.bottom-0 { + bottom: 0px; +} + +.left-0 { + left: 0px; +} + +.left-5 { + left: 1.25rem; +} + +.right-0 { + right: 0px; +} + +.top-\[2px\] { + top: 2px; +} + +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + +.-my-8 { + margin-top: -2rem; + margin-bottom: -2rem; +} + +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.-ml-\[5px\] { + margin-left: -5px; +} + +.-mt-3 { + margin-top: -0.75rem; +} + +.-mt-3\.5 { + margin-top: -0.875rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.ml-5 { + margin-left: 1.25rem; +} + +.ml-7 { + margin-left: 1.75rem; +} + +.ml-8 { + margin-left: 2rem; +} + +.mt-0 { + margin-top: 0px; +} + +.mt-0\.5 { + margin-top: 0.125rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.flow-root { + display: flow-root; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-10 { + height: 2.5rem; +} + +.h-3 { + height: 0.75rem; +} + +.h-3\.5 { + height: 0.875rem; +} + +.h-4 { + height: 1rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-8 { + height: 2rem; +} + +.h-full { + height: 100%; +} + +.w-1\/3 { + width: 33.333333%; +} + +.w-10 { + width: 2.5rem; +} + +.w-3 { + width: 0.75rem; +} + +.w-3\.5 { + width: 0.875rem; +} + +.w-32 { + width: 8rem; +} + +.w-4 { + width: 1rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-8 { + width: 2rem; +} + +.w-\[2px\] { + width: 2px; +} + +.w-full { + width: 100%; +} + +.min-w-\[50vw\] { + min-width: 50vw; +} + +.min-w-full { + min-width: 100%; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.shrink-0 { + flex-shrink: 0; +} + +.grow { + flex-grow: 1; +} + +.table-auto { + table-layout: auto; +} + +.-translate-y-px { + --tw-translate-y: -1px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.\!cursor-default { + cursor: default !important; +} + +.cursor-default { + cursor: default; +} + +.cursor-grab { + cursor: grab; +} + +.scroll-mt-32 { + scroll-margin-top: 8rem; +} + +.appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-0 { + gap: 0px; +} + +.gap-0\.5 { + gap: 0.125rem; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-2\.5 { + gap: 0.625rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-x-3 { + -moz-column-gap: 0.75rem; + column-gap: 0.75rem; +} + +.gap-y-1 { + row-gap: 0.25rem; +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + +.space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + +.space-y-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1rem * var(--tw-space-y-reverse)); +} + +.space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + +.divide-x > :not([hidden]) ~ :not([hidden]) { + --tw-divide-x-reverse: 0; + border-right-width: calc(1px * var(--tw-divide-x-reverse)); + border-left-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); +} + +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(2px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(2px * var(--tw-divide-y-reverse)); +} + +.divide-gray-200 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgba(var(--gray-200), var(--tw-divide-opacity)); +} + +.divide-gray-950\/10 > :not([hidden]) ~ :not([hidden]) { + border-color: rgba(var(--gray-950), 0.1); +} + +.justify-self-end { + justify-self: end; +} + +.\!overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.whitespace-normal { + white-space: normal; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.rounded-b-lg { + border-bottom-right-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} + +.border { + border-width: 1px; +} + +.border-2 { + border-width: 2px; +} + +.border-x-\[0\.5px\] { + border-left-width: 0.5px; + border-right-width: 0.5px; +} + +.border-t { + border-top-width: 1px; +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgba(var(--gray-100), var(--tw-border-opacity)); +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgba(var(--gray-200), var(--tw-border-opacity)); +} + +.border-gray-600 { + --tw-border-opacity: 1; + border-color: rgba(var(--gray-600), var(--tw-border-opacity)); +} + +.border-green-300 { + --tw-border-opacity: 1; + border-color: rgb(134 239 172 / var(--tw-border-opacity)); +} + +.border-orange-300 { + --tw-border-opacity: 1; + border-color: rgb(253 186 116 / var(--tw-border-opacity)); +} + +.border-sky-300 { + --tw-border-opacity: 1; + border-color: rgb(125 211 252 / var(--tw-border-opacity)); +} + +.border-white\/10 { + border-color: rgb(255 255 255 / 0.1); +} + +.border-blue-500 { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + +.border-primary-500 { + --tw-border-opacity: 1; + border-color: rgba(var(--primary-500), var(--tw-border-opacity)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-200), var(--tw-bg-opacity)); +} + +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-300), var(--tw-bg-opacity)); +} + +.bg-gray-300\/20 { + background-color: rgba(var(--gray-300), 0.2); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-50), var(--tw-bg-opacity)); +} + +.bg-green-50 { + --tw-bg-opacity: 1; + background-color: rgb(240 253 244 / var(--tw-bg-opacity)); +} + +.bg-orange-50 { + --tw-bg-opacity: 1; + background-color: rgb(255 247 237 / var(--tw-bg-opacity)); +} + +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity)); +} + +.bg-sky-50 { + --tw-bg-opacity: 1; + background-color: rgb(240 249 255 / var(--tw-bg-opacity)); +} + +.bg-sky-500 { + --tw-bg-opacity: 1; + background-color: rgb(14 165 233 / var(--tw-bg-opacity)); +} + +.bg-teal-500 { + --tw-bg-opacity: 1; + background-color: rgb(20 184 166 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-white\/70 { + background-color: rgb(255 255 255 / 0.7); +} + +.bg-custom-600 { + --tw-bg-opacity: 1; + background-color: rgba(var(--c-600), var(--tw-bg-opacity)); +} + +.\!p-0 { + padding: 0px !important; +} + +.\!p-3 { + padding: 0.75rem !important; +} + +.p-0 { + padding: 0px; +} + +.p-0\.5 { + padding: 0.125rem; +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-4 { + padding: 1rem; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-3\.5 { + padding-top: 0.875rem; + padding-bottom: 0.875rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.\!ps-6 { + padding-inline-start: 1.5rem !important; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pe-2 { + padding-inline-end: 0.5rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.pl-8 { + padding-left: 2rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-8 { + padding-top: 2rem; +} + +.pt-\[1px\] { + padding-top: 1px; +} + +.pt-\[5px\] { + padding-top: 5px; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.font-bold { + font-weight: 700; +} + +.font-medium { + font-weight: 500; +} + +.font-normal { + font-weight: 400; +} + +.font-semibold { + font-weight: 600; +} + +.leading-6 { + line-height: 1.5rem; +} + +.\!text-red-400\/60 { + color: rgb(248 113 113 / 0.6) !important; +} + +.\!text-red-400\/80 { + color: rgb(248 113 113 / 0.8) !important; +} + +.text-gray-200 { + --tw-text-opacity: 1; + color: rgba(var(--gray-200), var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgba(var(--gray-400), var(--tw-text-opacity)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgba(var(--gray-500), var(--tw-text-opacity)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgba(var(--gray-600), var(--tw-text-opacity)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgba(var(--gray-700), var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgba(var(--gray-900), var(--tw-text-opacity)); +} + +.text-gray-950 { + --tw-text-opacity: 1; + color: rgba(var(--gray-950), var(--tw-text-opacity)); +} + +.text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.text-orange-500 { + --tw-text-opacity: 1; + color: rgb(249 115 22 / var(--tw-text-opacity)); +} + +.text-orange-600 { + --tw-text-opacity: 1; + color: rgb(234 88 12 / var(--tw-text-opacity)); +} + +.text-primary-500 { + --tw-text-opacity: 1; + color: rgba(var(--primary-500), var(--tw-text-opacity)); +} + +.text-primary-600 { + --tw-text-opacity: 1; + color: rgba(var(--primary-600), var(--tw-text-opacity)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + +.text-sky-600 { + --tw-text-opacity: 1; + color: rgb(2 132 199 / var(--tw-text-opacity)); +} + +.opacity-0 { + opacity: 0; +} + +.opacity-50 { + opacity: 0.5; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-4 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-inset { + --tw-ring-inset: inset; +} + +.ring-gray-100 { + --tw-ring-opacity: 1; + --tw-ring-color: rgba(var(--gray-100), var(--tw-ring-opacity)); +} + +.ring-gray-200 { + --tw-ring-opacity: 1; + --tw-ring-color: rgba(var(--gray-200), var(--tw-ring-opacity)); +} + +.ring-gray-950\/10 { + --tw-ring-color: rgba(var(--gray-950), 0.1); +} + +.ring-gray-950\/5 { + --tw-ring-color: rgba(var(--gray-950), 0.05); +} + +.ring-purple-100 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(243 232 255 / var(--tw-ring-opacity)); +} + +.ring-sky-100 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(224 242 254 / var(--tw-ring-opacity)); +} + +.ring-teal-100 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(204 251 241 / var(--tw-ring-opacity)); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-75 { + transition-duration: 75ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.first\:border-s-0:first-child { + border-inline-start-width: 0px; +} + +.last\:border-e-0:last-child { + border-inline-end-width: 0px; +} + +.hover\:cursor-pointer:hover { + cursor: pointer; +} + +.hover\:bg-danger-100\/80:hover { + background-color: rgba(var(--danger-100), 0.8); +} + +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-50), var(--tw-bg-opacity)); +} + +.hover\:bg-primary-50:hover { + --tw-bg-opacity: 1; + background-color: rgba(var(--primary-50), var(--tw-bg-opacity)); +} + +.hover\:bg-primary-50\/50:hover { + background-color: rgba(var(--primary-50), 0.5); +} + +.hover\:text-gray-500:hover { + --tw-text-opacity: 1; + color: rgba(var(--gray-500), var(--tw-text-opacity)); +} + +.hover\:underline:hover { + text-decoration-line: underline; +} + +.focus-visible\:z-10:focus-visible { + z-index: 10; +} + +.focus-visible\:ring-2:focus-visible { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus-visible\:ring-primary-600:focus-visible { + --tw-ring-opacity: 1; + --tw-ring-color: rgba(var(--primary-600), var(--tw-ring-opacity)); +} + +.group\/item:first-child .group-first\/item\:rounded-s-lg { + border-start-start-radius: 0.5rem; + border-end-start-radius: 0.5rem; +} + +.group\/item:last-child .group-last\/item\:rounded-e-lg { + border-start-end-radius: 0.5rem; + border-end-end-radius: 0.5rem; +} + +.group:hover .group-hover\:flex { + display: flex; +} + +.group:hover .group-hover\:scale-110 { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group\/button:hover .group-hover\/button\:text-gray-500 { + --tw-text-opacity: 1; + color: rgba(var(--gray-500), var(--tw-text-opacity)); +} + +.group:hover .group-hover\:text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.group:hover .group-hover\:text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:divide-gray-600) > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgba(var(--gray-600), var(--tw-divide-opacity)); +} + +:is(.dark .dark\:divide-white\/10) > :not([hidden]) ~ :not([hidden]) { + border-color: rgb(255 255 255 / 0.1); +} + +:is(.dark .dark\:divide-white\/5) > :not([hidden]) ~ :not([hidden]) { + border-color: rgb(255 255 255 / 0.05); +} + +:is(.dark .dark\:border-b) { + border-bottom-width: 1px; +} + +:is(.dark .dark\:border-gray-600) { + --tw-border-opacity: 1; + border-color: rgba(var(--gray-600), var(--tw-border-opacity)); +} + +:is(.dark .dark\:border-white\/10) { + border-color: rgb(255 255 255 / 0.1); +} + +:is(.dark .dark\:border-t-white\/10) { + border-top-color: rgb(255 255 255 / 0.1); +} + +:is(.dark .dark\:bg-gray-600) { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-600), var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-gray-800) { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-800), var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-gray-900) { + --tw-bg-opacity: 1; + background-color: rgba(var(--gray-900), var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-green-400\/10) { + background-color: rgb(74 222 128 / 0.1); +} + +:is(.dark .dark\:bg-orange-400\/10) { + background-color: rgb(251 146 60 / 0.1); +} + +:is(.dark .dark\:bg-sky-400\/10) { + background-color: rgb(56 189 248 / 0.1); +} + +:is(.dark .dark\:bg-white\/10) { + background-color: rgb(255 255 255 / 0.1); +} + +:is(.dark .dark\:bg-white\/5) { + background-color: rgb(255 255 255 / 0.05); +} + +:is(.dark .dark\:\!text-red-400\/60) { + color: rgb(248 113 113 / 0.6) !important; +} + +:is(.dark .dark\:text-gray-100) { + --tw-text-opacity: 1; + color: rgba(var(--gray-100), var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-gray-200) { + --tw-text-opacity: 1; + color: rgba(var(--gray-200), var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-gray-300) { + --tw-text-opacity: 1; + color: rgba(var(--gray-300), var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-gray-400) { + --tw-text-opacity: 1; + color: rgba(var(--gray-400), var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-gray-500) { + --tw-text-opacity: 1; + color: rgba(var(--gray-500), var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-green-400) { + --tw-text-opacity: 1; + color: rgb(74 222 128 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-green-400\/80) { + color: rgb(74 222 128 / 0.8); +} + +:is(.dark .dark\:text-orange-400) { + --tw-text-opacity: 1; + color: rgb(251 146 60 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-primary-400) { + --tw-text-opacity: 1; + color: rgba(var(--primary-400), var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-primary-400\/80) { + color: rgba(var(--primary-400), 0.8); +} + +:is(.dark .dark\:text-red-400\/80) { + color: rgb(248 113 113 / 0.8); +} + +:is(.dark .dark\:text-sky-400) { + --tw-text-opacity: 1; + color: rgb(56 189 248 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-white) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:ring-gray-600) { + --tw-ring-opacity: 1; + --tw-ring-color: rgba(var(--gray-600), var(--tw-ring-opacity)); +} + +:is(.dark .dark\:ring-gray-700) { + --tw-ring-opacity: 1; + --tw-ring-color: rgba(var(--gray-700), var(--tw-ring-opacity)); +} + +:is(.dark .dark\:ring-purple-800) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(107 33 168 / var(--tw-ring-opacity)); +} + +:is(.dark .dark\:ring-sky-800) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(7 89 133 / var(--tw-ring-opacity)); +} + +:is(.dark .dark\:ring-teal-800) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(17 94 89 / var(--tw-ring-opacity)); +} + +:is(.dark .dark\:ring-white\/10) { + --tw-ring-color: rgb(255 255 255 / 0.1); +} + +:is(.dark .dark\:ring-white\/20) { + --tw-ring-color: rgb(255 255 255 / 0.2); +} + +:is(.dark .dark\:hover\:bg-danger-300\/20:hover) { + background-color: rgba(var(--danger-300), 0.2); +} + +:is(.dark .dark\:hover\:bg-white\/5:hover) { + background-color: rgb(255 255 255 / 0.05); +} + +:is(.dark .dark\:focus-visible\:ring-primary-500:focus-visible) { + --tw-ring-opacity: 1; + --tw-ring-color: rgba(var(--primary-500), var(--tw-ring-opacity)); +} + +:is(.dark .group\/button:hover .dark\:group-hover\/button\:text-gray-400) { + --tw-text-opacity: 1; + color: rgba(var(--gray-400), var(--tw-text-opacity)); +} + +:is(.dark .group:hover .dark\:group-hover\:text-green-400) { + --tw-text-opacity: 1; + color: rgb(74 222 128 / var(--tw-text-opacity)); +} + +:is(.dark .group:hover .dark\:group-hover\:text-red-400) { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity)); +} + +@media (min-width: 640px) { + .sm\:ms-auto { + margin-inline-start: auto; + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:items-center { + align-items: center; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .sm\:first-of-type\:ps-6:first-of-type { + padding-inline-start: 1.5rem; + } + + .sm\:last-of-type\:pe-6:last-of-type { + padding-inline-end: 1.5rem; + } +} + +@media (min-width: 768px) { + .md\:min-w-\[32rem\] { + min-width: 32rem; + } +} + +.ltr\:rotate-90:where([dir="ltr"], [dir="ltr"] *) { + --tw-rotate: 90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rtl\:\!rotate-90:where([dir="rtl"], [dir="rtl"] *) { + --tw-rotate: 90deg !important; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; +} + +.rtl\:rotate-180:where([dir="rtl"], [dir="rtl"] *) { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 1; +} + +.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) { + text-align: right; +} + +.\[\&_table\]\:h-\[1px\] table { + height: 1px; +} diff --git a/packages/admin/resources/dist/lunar-panel.js b/packages/admin/resources/dist/lunar-panel.js index e69de29bb2..38d4cc9aab 100644 --- a/packages/admin/resources/dist/lunar-panel.js +++ b/packages/admin/resources/dist/lunar-panel.js @@ -0,0 +1 @@ +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFtdLAogICJzb3VyY2VzQ29udGVudCI6IFtdLAogICJtYXBwaW5ncyI6ICIiLAogICJuYW1lcyI6IFtdCn0K diff --git a/packages/admin/resources/lang/en/components.php b/packages/admin/resources/lang/en/components.php index 4ad2ba1e23..28295b696d 100644 --- a/packages/admin/resources/lang/en/components.php +++ b/packages/admin/resources/lang/en/components.php @@ -93,4 +93,25 @@ ], ], ], + + 'product-options-list' => [ + 'add-option' => [ + 'label' => 'Add Option', + ], + 'delete-option' => [ + 'label' => 'Delete Option', + ], + 'remove-shared-option' => [ + 'label' => 'Remove Shared Option', + ], + 'add-value' => [ + 'label' => 'Add Another Value', + ], + 'name' => [ + 'label' => 'Name', + ], + 'values' => [ + 'label' => 'Values', + ], + ], ]; diff --git a/packages/admin/resources/lang/en/product.php b/packages/admin/resources/lang/en/product.php index 52e87ac94c..2b36e83e8a 100644 --- a/packages/admin/resources/lang/en/product.php +++ b/packages/admin/resources/lang/en/product.php @@ -87,83 +87,12 @@ ], 'identifiers' => [ 'label' => 'Product Identifiers', - 'form' => [ - 'sku' => [ - 'label' => 'SKU', - ], - 'gtin' => [ - 'label' => 'Global Trade Item Number (GTIN)', - ], - 'mpn' => [ - 'label' => 'Manufacturer Part Number (MPN)', - ], - 'ean' => [ - 'label' => 'UPC/EAN', - ], - ], ], 'inventory' => [ 'label' => 'Inventory', - 'form' => [ - 'stock' => [ - 'label' => 'In Stock', - ], - 'backorder' => [ - 'label' => 'On Backorder', - ], - 'purchasable' => [ - 'label' => 'Purchasability', - 'options' => [ - 'always' => 'Always', - 'in_stock' => 'In Stock', - 'backorder' => 'Backorder Only', - ], - ], - 'unit_quantity' => [ - 'label' => 'Unit Quantity', - 'helper_text' => 'How many individual items make up 1 unit.', - ], - 'min_quantity' => [ - 'label' => 'Minimum Quantity', - 'helper_text' => 'The minimum quantity of a product variant that can be bought in a single purchase.', - ], - 'quantity_increment' => [ - 'label' => 'Quantity Increment', - 'helper_text' => 'The product variant must be purchased in multiples of this quantity.', - ], - ], ], 'shipping' => [ 'label' => 'Shipping', - 'form' => [ - 'shippable' => [ - 'label' => 'Shippable', - ], - 'length_value' => [ - 'label' => 'Length', - ], - 'length_unit' => [ - 'label' => 'Length Unit', - ], - 'width_value' => [ - 'label' => 'Width', - ], - 'width_unit' => [ - 'label' => 'Width Unit', - ], - 'height_value' => [ - 'label' => 'Height', - ], - 'height_unit' => [ - 'label' => 'Height Unit', - ], - 'weight_value' => [ - 'label' => 'Weight', - ], - 'weight_unit' => [ - 'label' => 'Weight Unit', - ], - ], ], ], diff --git a/packages/admin/resources/lang/en/productoption.php b/packages/admin/resources/lang/en/productoption.php index ddb2d3c384..7d4d9c799e 100644 --- a/packages/admin/resources/lang/en/productoption.php +++ b/packages/admin/resources/lang/en/productoption.php @@ -30,4 +30,92 @@ ], ], + 'widgets' => [ + 'product-options' => [ + 'notifications' => [ + 'save-variants' => [ + 'success' => [ + 'title' => 'Product Variants Saved', + ], + ], + ], + 'actions' => [ + 'cancel' => [ + 'label' => 'Cancel', + ], + 'save-options' => [ + 'label' => 'Save Options', + ], + 'add-shared-option' => [ + 'label' => 'Add Shared Option', + 'form' => [ + 'product_option' => [ + 'label' => 'Product Option', + ], + 'no_shared_components' => [ + 'label' => 'No shared options are available.', + ], + ], + ], + 'add-restricted-option' => [ + 'label' => 'Add Option', + ], + ], + 'options-list' => [ + 'empty' => [ + 'heading' => 'There are no product options configured', + 'description' => 'Add a shared or restricted product option to start generating some variants.', + ], + ], + 'options-table' => [ + 'title' => 'Product Options', + 'configure-options' => [ + 'label' => 'Configure Options', + ], + 'table' => [ + 'option' => [ + 'label' => 'Option', + ], + 'values' => [ + 'label' => 'Values', + ], + ], + ], + 'variants-table' => [ + 'title' => 'Product Variants', + 'actions' => [ + 'create' => [ + 'label' => 'Create Variant', + ], + 'edit' => [ + 'label' => 'Edit', + ], + 'delete' => [ + 'label' => 'Delete', + ], + ], + 'empty' => [ + 'heading' => 'No Variants Configured', + ], + 'table' => [ + 'new' => [ + 'label' => 'NEW', + ], + 'option' => [ + 'label' => 'Option', + ], + 'sku' => [ + 'label' => 'SKU', + ], + 'price' => [ + 'label' => 'Price', + ], + 'stock' => [ + 'label' => 'Stock', + ], + ], + ], + ], + ], + ]; diff --git a/packages/admin/resources/lang/en/productvariant.php b/packages/admin/resources/lang/en/productvariant.php new file mode 100644 index 0000000000..116ecda864 --- /dev/null +++ b/packages/admin/resources/lang/en/productvariant.php @@ -0,0 +1,102 @@ + 'Product Variant', + 'plural_label' => 'Product Variants', + 'pages' => [ + 'edit' => [ + 'title' => 'Basic Information', + ], + 'media' => [ + 'title' => 'Media', + 'form' => [ + 'no_selection' => [ + 'label' => 'You do not currently have an image selected for this variant.', + ], + 'no_media_available' => [ + 'label' => 'There is currently no media available on this product.', + ], + 'images' => [ + 'label' => 'Primary Image', + 'helper_text' => 'Select the product image which represents this variant.', + ], + ], + ], + 'identifiers' => [ + 'title' => 'Identifiers', + ], + 'inventory' => [ + 'title' => 'Inventory', + ], + 'shipping' => [ + 'title' => 'Shipping', + ], + ], + 'form' => [ + 'sku' => [ + 'label' => 'SKU', + ], + 'gtin' => [ + 'label' => 'Global Trade Item Number (GTIN)', + ], + 'mpn' => [ + 'label' => 'Manufacturer Part Number (MPN)', + ], + 'ean' => [ + 'label' => 'UPC/EAN', + ], + 'stock' => [ + 'label' => 'In Stock', + ], + 'backorder' => [ + 'label' => 'On Backorder', + ], + 'purchasable' => [ + 'label' => 'Purchasability', + 'options' => [ + 'always' => 'Always', + 'in_stock' => 'In Stock', + 'backorder' => 'Backorder Only', + ], + ], + 'unit_quantity' => [ + 'label' => 'Unit Quantity', + 'helper_text' => 'How many individual items make up 1 unit.', + ], + 'min_quantity' => [ + 'label' => 'Minimum Quantity', + 'helper_text' => 'The minimum quantity of a product variant that can be bought in a single purchase.', + ], + 'quantity_increment' => [ + 'label' => 'Quantity Increment', + 'helper_text' => 'The product variant must be purchased in multiples of this quantity.', + ], + 'shippable' => [ + 'label' => 'Shippable', + ], + 'length_value' => [ + 'label' => 'Length', + ], + 'length_unit' => [ + 'label' => 'Length Unit', + ], + 'width_value' => [ + 'label' => 'Width', + ], + 'width_unit' => [ + 'label' => 'Width Unit', + ], + 'height_value' => [ + 'label' => 'Height', + ], + 'height_unit' => [ + 'label' => 'Height Unit', + ], + 'weight_value' => [ + 'label' => 'Weight', + ], + 'weight_unit' => [ + 'label' => 'Weight Unit', + ], + ], +]; diff --git a/packages/admin/resources/lang/en/widgets.php b/packages/admin/resources/lang/en/widgets.php index 91be2b13ec..71e042746f 100644 --- a/packages/admin/resources/lang/en/widgets.php +++ b/packages/admin/resources/lang/en/widgets.php @@ -104,4 +104,15 @@ ], ], ], + 'variant_switcher' => [ + 'label' => 'Switch Variant', + 'table' => [ + 'sku' => [ + 'label' => 'SKU', + ], + 'values' => [ + 'label' => 'Values', + ], + ], + ], ]; diff --git a/packages/admin/resources/views/actions/switch-variant.blade.php b/packages/admin/resources/views/actions/switch-variant.blade.php new file mode 100644 index 0000000000..4a1e60346e --- /dev/null +++ b/packages/admin/resources/views/actions/switch-variant.blade.php @@ -0,0 +1,5 @@ +
+ @livewire(\Lunar\Admin\Filament\Widgets\Products\VariantSwitcherTable::class, [ + 'record' => $record, + ]) +
\ No newline at end of file diff --git a/packages/admin/resources/views/components/products/variants/product-option-list-values.blade.php b/packages/admin/resources/views/components/products/variants/product-option-list-values.blade.php new file mode 100644 index 0000000000..d2f1ecec74 --- /dev/null +++ b/packages/admin/resources/views/components/products/variants/product-option-list-values.blade.php @@ -0,0 +1,92 @@ +@props(['items', 'statePath', 'key', 'canAddValues', 'readonly' => false]) +
+
+ @foreach($items as $itemIndex => $valueItem) +
+
+ @if(!$readonly) +
!$readonly, + 'text-gray-200' => $readonly, + ]) + x-sortable-handle + > + +
+ @endif +
!$valueItem['enabled'] + ]) + > + + + +
+
+ @if(!$readonly) +
+ +
+ @else +
+ +
+ @endif +
+
+
+ @endforeach +
+ @if($canAddValues) +
+ + {{ __('lunarpanel::components.product-options-list.add-value.label') }} + +
+
+ @endif +
\ No newline at end of file diff --git a/packages/admin/resources/views/components/products/variants/product-options-list.blade.php b/packages/admin/resources/views/components/products/variants/product-options-list.blade.php new file mode 100644 index 0000000000..4cf4f74746 --- /dev/null +++ b/packages/admin/resources/views/components/products/variants/product-options-list.blade.php @@ -0,0 +1,98 @@ +@props(['items', 'group', 'statePath', 'context' => 'options', 'optionKey' => null]) +
+ @foreach($items as $itemIndex => $item) +
+
+
+
+ + {{ __('lunarpanel::components.product-options-list.name.label') }} + +
+
!$item['readonly'] || $context == 'options', + ' text-gray-200' => $item['readonly'] && $context == 'values', + ]) + @if(!$item['readonly'] || $context == 'options') x-sortable-option-handle @endif + > + +
+
+ + + + +
+
+
+
+
+ + {{ __('lunarpanel::components.product-options-list.values.label') }} + +
+ +
+
+
+
+ @endforeach + + + {{ __('lunarpanel::components.product-options-list.add-option.label') }} + +
\ No newline at end of file diff --git a/packages/admin/resources/views/forms/components/media-select.blade.php b/packages/admin/resources/views/forms/components/media-select.blade.php new file mode 100644 index 0000000000..7dd817cacf --- /dev/null +++ b/packages/admin/resources/views/forms/components/media-select.blade.php @@ -0,0 +1,18 @@ + +
+ @foreach($getOptions() as $value => $label) + + @endforeach +
+
\ No newline at end of file diff --git a/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php b/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php new file mode 100644 index 0000000000..c5e1c1db15 --- /dev/null +++ b/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php @@ -0,0 +1,229 @@ + + + @if(!$this->configuringOptions) +
+
+
+
+

+ {{ __('lunarpanel::productoption.widgets.product-options.options-table.title') }} +

+
+
+ + {{ __('lunarpanel::productoption.widgets.product-options.options-table.configure-options.label') }} + +
+
+
+ @if(count($this->configuredOptions)) + + + + + + {{ __('lunarpanel::productoption.widgets.product-options.options-table.table.option.label') }} + + + + + {{ __('lunarpanel::productoption.widgets.product-options.options-table.table.values.label') }} + + + + + + @foreach($this->configuredOptions as $option) + + +
+ + {{ $option['value'] }} + +
+
+ +
+ + {{ collect($option['option_values']) + ->filter( + fn ($value) => $value['enabled'] + )->map( + fn ($value) => $value['value'] + )->join(', ') }} + +
+
+
+ @endforeach + +
+ @else + + @endif +
+
+ +
+
+
+

+ {{ __('lunarpanel::productoption.widgets.product-options.variants-table.title') }} +

+
+
+
+ @if(count($this->variants)) + + + + @if($this->hasNewVariants) + + + @endif + + + {{ __('lunarpanel::productoption.widgets.product-options.variants-table.table.option.label') }} + + + + + {{ __('lunarpanel::productoption.widgets.product-options.variants-table.table.sku.label') }} + + + + + {{ __('lunarpanel::productoption.widgets.product-options.variants-table.table.price.label') }} + + + + + {{ __('lunarpanel::productoption.widgets.product-options.variants-table.table.stock.label') }} + + + + + + + + + @foreach($this->variants as $permutationIndex => $permutation) + + @if($this->hasNewVariants) + +
+ @if(!$permutation['variant_id']) + + {{ __('lunarpanel::productoption.widgets.product-options.variants-table.table.new.label') }} + + @endif +
+
+ @endif + +
+ + @foreach($permutation['values'] as $option => $value) + {{ $option }}: {{ $value }} + @endforeach + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ @if($permutation['variant_id']) + + {{ __('lunarpanel::productoption.widgets.product-options.variants-table.actions.edit.label') }} + + @endif + +
+
+ +
+ @endforeach + +
+ @else + + @endif +
+
+
+ +
+ {{ $this->saveVariantsAction }} +
+ + @else +
+
+
+ + {{ __('lunarpanel::productoption.widgets.product-options.actions.add-restricted-option.label') }} + + {{ $this->addSharedOptionAction }} +
+
+ @if(!count($this->configuredOptions)) +
+ +
+ @else +
+ +
+ @endif + +
+ + {{ __('lunarpanel::productoption.widgets.product-options.actions.save-options.label') }} + + + {{ __('lunarpanel::productoption.widgets.product-options.actions.cancel.label') }} + +
+
+ + @endif +
diff --git a/packages/admin/src/Actions/Products/MapVariantsToProductOptions.php b/packages/admin/src/Actions/Products/MapVariantsToProductOptions.php new file mode 100644 index 0000000000..69bb4fbcb2 --- /dev/null +++ b/packages/admin/src/Actions/Products/MapVariantsToProductOptions.php @@ -0,0 +1,82 @@ + $p, + ]; + } + $permutations = $newPermutations; + } + + $variantPermutations = []; + + foreach ($permutations as $permutation) { + $variantIndex = collect($variants)->search(function ($variant) use ($permutation) { + $valueDifference = array_diff_assoc($permutation, $variant['values']); + + if (! count($valueDifference)) { + return $variant; + } + + $amountMatched = count($permutation) - count($valueDifference); + + return $amountMatched == count($variant['values']); + }); + + $variant = $variants[$variantIndex] ?? null; + + $variantId = $variant['id'] ?? null; + $sku = $variant['sku'] ?? null; + $copiedFrom = null; + $shouldFill = true; + + if ($variant) { + // Does this variant already exist in our permutations? + // if so we want to mark it as new but + $existing = collect($variantPermutations) + ->first( + fn ($p) => $p['variant_id'] == $variant['id'] + ); + + // Now what? + if ($existing) { + $diff = array_diff_assoc($permutation, $variant['values']); + $sku = $existing['sku'].'-'.implode('-', array_values($diff)); + $variantId = null; + $copiedFrom = $variant['id']; + } + + if ($existing && ! $fillMissing) { + $shouldFill = false; + } + } + + if ($shouldFill) { + $variantPermutations[] = [ + 'key' => Str::random(), + 'variant_id' => $variantId, + 'copied_id' => $copiedFrom, + 'sku' => $sku, + 'price' => $variant['price'] ?? 0, + 'stock' => $variant['stock'] ?? 0, + 'values' => $permutation, + ]; + } + } + + return $variantPermutations; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductResource.php b/packages/admin/src/Filament/Resources/ProductResource.php index e884be8afe..b6db375341 100644 --- a/packages/admin/src/Filament/Resources/ProductResource.php +++ b/packages/admin/src/Filament/Resources/ProductResource.php @@ -18,6 +18,8 @@ use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\ProductResource\Pages; use Lunar\Admin\Filament\Resources\ProductResource\RelationManagers\CustomerGroupRelationManager; +use Lunar\Admin\Filament\Resources\ProductResource\Widgets\ProductOptionsWidget; +use Lunar\Admin\Filament\Widgets\Products\VariantSwitcherTable; use Lunar\Admin\Support\Forms\Components\Attributes; use Lunar\Admin\Support\Forms\Components\Tags as TagsComponent; use Lunar\Admin\Support\RelationManagers\ChannelRelationManager; @@ -34,6 +36,8 @@ class ProductResource extends BaseResource protected static ?string $model = Product::class; + protected static ?string $recordTitleAttribute = 'recordTitle'; + protected static ?int $navigationSort = 1; protected static int $globalSearchResultsLimit = 5; @@ -77,6 +81,14 @@ public static function getRecordSubNavigation(Page $page): array ]); } + public static function getWidgets(): array + { + return [ + ProductOptionsWidget::class, + VariantSwitcherTable::class, + ]; + } + public static function getDefaultForm(Form $form): Form { return $form diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductIdentifiers.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductIdentifiers.php index 91499b384b..6e2215dbd3 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductIdentifiers.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductIdentifiers.php @@ -3,12 +3,12 @@ namespace Lunar\Admin\Filament\Resources\ProductResource\Pages; use Filament\Forms\Components\Section; -use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Support\Facades\FilamentIcon; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\ProductResource; +use Lunar\Admin\Filament\Resources\ProductVariantResource; use Lunar\Admin\Support\Pages\BaseEditRecord; use Lunar\Models\ProductVariant; @@ -94,24 +94,15 @@ public function form(Form $form): Form return $form->schema([ Section::make()->schema([ - TextInput::make('sku') - ->label( - __('lunarpanel::product.pages.identifiers.form.sku.label') - ) + ProductVariantResource::getSkuFormComponent() ->live()->unique( table: fn () => $variant->getTable(), ignorable: $variant, ignoreRecord: true, ), - TextInput::make('gtin')->label( - __('lunarpanel::product.pages.identifiers.form.gtin.label') - ), - TextInput::make('mpn')->label( - __('lunarpanel::product.pages.identifiers.form.mpn.label') - ), - TextInput::make('ean')->label( - __('lunarpanel::product.pages.identifiers.form.ean.label') - ), + ProductVariantResource::getGtinFormComponent(), + ProductVariantResource::getMpnFormComponent(), + ProductVariantResource::getEanFormComponent(), ])->columns(1), ])->statePath(''); } diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductInventory.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductInventory.php index 405ab713a5..893a899b25 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductInventory.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductInventory.php @@ -2,14 +2,12 @@ namespace Lunar\Admin\Filament\Resources\ProductResource\Pages; -use Filament\Forms\Components\Section; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Support\Facades\FilamentIcon; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\ProductResource; +use Lunar\Admin\Filament\Resources\ProductVariantResource\Pages\ManageVariantInventory; use Lunar\Admin\Support\Pages\BaseEditRecord; use Lunar\Models\ProductVariant; @@ -96,50 +94,7 @@ protected function getFormActions(): array public function form(Form $form): Form { - $variant = $this->getVariant(); - - return $form->schema([ - Section::make()->schema([ - TextInput::make('stock') - ->label( - __('lunarpanel::product.pages.inventory.form.stock.label') - )->numeric(), - TextInput::make('backorder') - ->label( - __('lunarpanel::product.pages.inventory.form.backorder.label') - )->numeric(), - Select::make('purchasable') - ->options([ - 'always' => __('lunarpanel::product.pages.inventory.form.purchasable.options.always'), - 'in_stock' => __('lunarpanel::product.pages.inventory.form.purchasable.options.in_stock'), - 'backorder' => __('lunarpanel::product.pages.inventory.form.purchasable.options.backorder'), - ]) - ->label( - __('lunarpanel::product.pages.inventory.form.purchasable.label') - ), - TextInput::make('unit_quantity') - ->label( - __('lunarpanel::product.pages.inventory.form.unit_quantity.label') - )->helperText( - __('lunarpanel::product.pages.inventory.form.unit_quantity.helper_text') - )->numeric(), - TextInput::make('quantity_increment') - ->label( - __('lunarpanel::product.pages.inventory.form.quantity_increment.label') - )->helperText( - __('lunarpanel::product.pages.inventory.form.quantity_increment.helper_text') - )->numeric(), - TextInput::make('min_quantity') - ->label( - __('lunarpanel::product.pages.inventory.form.min_quantity.label') - )->helperText( - __('lunarpanel::product.pages.inventory.form.min_quantity.helper_text') - )->numeric(), - ])->columns([ - 'sm' => 1, - 'xl' => 3, - ]), - ])->statePath(''); + return (new ManageVariantInventory())->form($form)->statePath(''); } public function getRelationManagers(): array diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductShipping.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductShipping.php index cbd5c20ad1..fcf9429219 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductShipping.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductShipping.php @@ -11,6 +11,7 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\ProductResource; +use Lunar\Admin\Filament\Resources\ProductVariantResource\Pages\ManageVariantShipping; use Lunar\Admin\Support\Pages\BaseEditRecord; use Lunar\Models\ProductVariant; use Marvinosswald\FilamentInputSelectAffix\TextInputSelectAffix; @@ -83,7 +84,20 @@ protected function handleRecordUpdate(Model $record, array $data): Model ...[ 'shippable' => $this->shippable, 'volume_unit' => 'l', - 'volume_value' => $this->volume, + 'volume_value' => ManageVariantShipping::getVolume( + [ + 'value' => $this->dimensions['width_value'], + 'unit' => $this->dimensions['width_unit'], + ], + [ + 'value' => $this->dimensions['length_value'], + 'unit' => $this->dimensions['length_unit'], + ], + [ + 'value' => $this->dimensions['height_value'], + 'unit' => $this->dimensions['height_unit'], + ] + ), ], ...$this->dimensions, ]); @@ -103,30 +117,6 @@ protected function getFormActions(): array ]; } - public function getVolumeProperty() - { - $dimensions = $this->dimensions; - - $width = Converter::value($dimensions['width_value']) - ->from('length.'.$dimensions['width_unit']) - ->to('length.cm') - ->convert() - ->getValue(); - $length = Converter::value($dimensions['length_value']) - ->from('length.'.$dimensions['length_unit']) - ->to('length.cm') - ->convert() - ->getValue(); - - $height = Converter::value($dimensions['height_value']) - ->from('length.'.$dimensions['height_unit']) - ->to('length.cm') - ->convert() - ->getValue(); - - return Converter::from('volume.ml')->to('volume.l')->value($length * $width * $height)->convert()->getValue(); - } - public function form(Form $form): Form { $measurements = Converter::getMeasurements(); diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductVariants.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductVariants.php index 4c7e47b78e..79ef65c030 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductVariants.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductVariants.php @@ -18,6 +18,13 @@ class ManageProductVariants extends ManageRelatedRecords protected static ?string $title = 'Variants'; + protected function getHeaderWidgets(): array + { + return [ + ProductResource\Widgets\ProductOptionsWidget::class, + ]; + } + public static function getNavigationIcon(): ?string { return FilamentIcon::resolve('lunar::product-variants'); @@ -54,6 +61,8 @@ public function form(Form $form): Form public function table(Table $table): Table { + return $table; + return $table ->recordTitleAttribute('name') ->columns([ diff --git a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php new file mode 100644 index 0000000000..a57ea1ce6f --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php @@ -0,0 +1,460 @@ +configureBaseOptions(); + } + + public function addSharedOptionAction() + { + $existing = collect($this->configuredOptions)->pluck('id'); + $options = ProductOption::whereNotIn('id', $existing) + ->shared() + ->get(); + + return Action::make('addSharedOption') + ->form([ + Shout::make('no_shared_components') + ->content( + __('lunarpanel::productoption.widgets.product-options.actions.add-shared-option.form.no_shared_components.label') + ) + ->visible( + $options->isEmpty() + ), + Select::make('product_option') + ->options( + fn () => $options->mapWithKeys( + fn ($option) => [$option->id => $option->translate('name')] + ) + )->label( + __('lunarpanel::productoption.widgets.product-options.actions.add-shared-option.form.product_option.label') + )->visible( + $options->isNotEmpty() + ), + ])->action(function (array $data) { + $productOption = ProductOption::with(['values'])->find($data['product_option']); + $this->configuredOptions[] = $this->mapOption( + $productOption, + $productOption->values->map( + fn ($value) => $this->mapOptionValue($value, true) + )->toArray() + ); + }); + } + + public function configureBaseOptions(): void + { + $productOptions = $this->query()->get(); + + $sharedOptionIds = $productOptions->filter( + fn ($option) => $option->shared + )->pluck('id'); + + $disabledSharedOptionValues = ProductOptionValue::whereIn( + 'product_option_id', + $sharedOptionIds + )->whereNotIn( + 'id', + $productOptions->pluck('values')->flatten()->pluck('id') + )->get(); + + $options = []; + + foreach ($productOptions as $productOption) { + $values = $productOption->values->map(function ($value) { + return $this->mapOptionValue($value, true); + })->merge( + $disabledSharedOptionValues->filter( + fn ($value) => $value->product_option_id == $productOption->id + )->map( + fn ($value) => $this->mapOptionValue($value, false) + ) + )->sortBy('position')->values()->toArray(); + + $options[] = $this->mapOption($productOption, $values); + } + + $this->configuredOptions = $options; + + $this->mapVariantPermutations(fillMissing: false); + } + + public function cancelOptionConfiguring(): void + { + $this->configuringOptions = false; + $this->configureBaseOptions(); + } + + public function query() + { + return $this->record->productOptions() + ->with('values', function ($query) { + $query->whereHas('variants', function ($relation) { + $relation->whereIn($relation->getModel()->getTable().'.id', $this->record->variants()->pluck('id')); + }); + }); + } + + public function addRestrictedOption() + { + $this->configuredOptions[] = [ + 'id' => null, + 'value' => '', + 'position' => count($this->configuredOptions) + 1, + 'readonly' => false, + 'option_values' => [ + [ + 'id' => null, + 'value' => '', + 'position' => 1, + 'enabled' => true, + ], + ], + ]; + } + + public function updateConfiguredOptions() + { + $this->validate([ + 'configuredOptions' => 'array', + 'configuredOptions.*.value' => 'required|string', + 'configuredOptions.*.option_values.*.value' => 'required|string', + ]); + + // Go through each one and if a configuration has none enabled, then just + // remove it from the array. + $options = collect(); + + foreach ($this->configuredOptions as $configuredOption) { + $enabledCount = collect($configuredOption['option_values']) + ->filter( + fn ($value) => $value['enabled'] + )->count(); + + if ($enabledCount) { + $options->push($configuredOption); + } + } + + $this->configuredOptions = $options->values()->toArray(); + + $this->mapVariantPermutations(); + + $this->configuringOptions = false; + } + + public function removeVariant($key): void + { + unset($this->variants[$key]); + } + + public function addOptionValue($path) + { + $option = $this->configuredOptions[$path]; + + if ($option['readonly']) { + return; + } + + $this->configuredOptions[$path]['option_values'][] = [ + 'value' => '', + 'position' => count($this->configuredOptions[$path]['option_values']) + 1, + 'readonly' => false, + 'enabled' => true, + ]; + } + + public function removeOptionValue($index, $valueIndex) + { + unset($this->configuredOptions[$index]['option_values'][$valueIndex]); + } + + public function removeOption($index) + { + $options = collect($this->configuredOptions)->forget($index); + $this->configuredOptions = $options->values()->toArray(); + } + + public function updateValuePositions($optionKey, $rows) + { + $this->configuredOptions[$optionKey]['option_values'] = $rows; + } + + public function updateOptionPositions($rows) + { + $this->configuredOptions = $rows; + } + + public function mapVariantPermutations($fillMissing = true): void + { + $optionValues = collect($this->configuredOptions) + ->filter( + fn ($option) => $option['value'] + ) + ->mapWithKeys( + fn ($option) => [$option['value'] => collect($option['option_values']) + ->filter( + fn ($value) => $value['enabled'] + ) + ->map( + fn ($value) => $value['value'] + )] + )->toArray(); + + $variants = $this->record->variants->load('values.option')->map(function ($variant) { + return [ + 'id' => $variant->id, + 'sku' => $variant->sku, + 'price' => $variant->basePrices->first()?->price->decimal ?: 0, + 'stock' => $variant->stock, + 'values' => $variant->values->mapWithKeys( + fn ($value) => [$value->option->translate('name') => $value->translate('name')] + )->toArray(), + ]; + })->toArray(); + + $this->variants = MapVariantsToProductOptions::map($optionValues, $variants, $fillMissing); + } + + public function getHasNewVariantsProperty() + { + return collect($this->variants) + ->reject( + fn ($variant) => $variant['variant_id'] + )->isNotEmpty(); + } + + protected function storeConfiguredOptions(): void + { + $language = Language::getDefault(); + /** + * Go through our configured options and if they don't + * exist in the database i.e. they are new, create and map them + * so they are ready. + */ + foreach ($this->configuredOptions as $optionIndex => $option) { + + $optionModel = empty($option['id']) ? + new ProductOption([ + 'shared' => false, + ]) : + ProductOption::find($option['id']); + + $optionValue = $option['value']; + + $optionModel->name = [ + $language->code => $optionValue, + ]; + $optionModel->label = [ + $language->code => $optionValue, + ]; + $optionModel->handle = Str::slug($optionValue); + $optionModel->save(); + + $this->configuredOptions[$optionIndex]['id'] = $optionModel->id; + $option['id'] = $optionModel->id; + + foreach ($option['option_values'] as $optionValueIndex => $value) { + $optionValueModel = empty($value['id']) ? + new ProductOptionValue([ + 'product_option_id' => $option['id'], + ]) : + ProductOptionValue::find($value['id']); + + $optionValueModel->name = [ + $language->code => $value['value'], + ]; + $optionValueModel->position = $value['position']; + $optionValueModel->save(); + + $this->configuredOptions[$optionIndex]['option_values'][$optionValueIndex]['id'] = + $optionValueModel->id; + } + } + } + + protected function mapOptionValuesToIds(array $values): array + { + $valueIds = []; + foreach ($values as $option => $value) { + $configuredOption = collect( + $this->configuredOptions + )->first( + fn ($o) => $o['value'] == $option + ); + + $valueId = collect($configuredOption['option_values'])->first( + fn ($v) => $v['value'] == $value + )['id']; + $valueIds[] = $valueId; + } + + return $valueIds; + } + + public function saveVariantsAction() + { + return Action::make('saveVariants') + ->action(function () { + DB::beginTransaction(); + + $this->storeConfiguredOptions(); + + /** + * If there are no variants, then all the configured option + * have been removed. In this case we still want to keep a + * variant at least one is needed for Lunar to function. + */ + if (! count($this->variants)) { + $variant = $this->record->variants()->first(); + $variant->values()->detach(); + $this->record->productOptions()->exclusive()->delete(); + $this->record->productOptions()->shared()->detach(); + $this->record->variants() + ->where('id', '!=', $variant->id) + ->get() + ->each( + fn ($variant) => $variant->delete() + ); + + DB::commit(); + + Notification::make()->title( + __('lunarpanel::productoption.widgets.product-options.notifications.save-variants.success.title') + )->success()->send(); + + return; + } + + foreach ($this->variants as $variantIndex => $variantData) { + $variant = new ProductVariant([ + 'product_id' => $this->record->id, + ]); + $basePrice = null; + + if (! empty($variantData['variant_id'])) { + $variant = ProductVariant::find($variantData['variant_id']); + $basePrice = $variant->basePrices->first(); + } + + if (! empty($variantData['copied_id'])) { + $copiedVariant = ProductVariant::find( + $variantData['copied_id'] + ); + + $variant = $copiedVariant->replicate(); + $variant->save(); + + $basePrice = $copiedVariant->basePrices->first()->replicate(); + $basePrice->priceable_id = $variant->id; + } + + $variant->sku = $variantData['sku']; + $variant->stock = $variantData['stock']; + $variant->save(); + + $basePrice->price = (int) bcmul($variantData['price'], $basePrice->currency->factor); + $basePrice->save(); + + $optionsValues = $this->mapOptionValuesToIds($variantData['values']); + + $variant->values()->sync($optionsValues); + + $this->variants[$variantIndex]['variant_id'] = $variant->id; + } + + $productOptions = collect($this->configuredOptions) + ->mapWithKeys(function ($option) { + return [ + $option['id'] => [ + 'position' => $option['position'], + ], + ]; + }); + + $this->record->productOptions()->sync($productOptions); + + $variantIds = collect($this->variants)->pluck('variant_id'); + + $this->record->variants()->whereNotIn('id', $variantIds) + ->get() + ->each( + fn ($variant) => $variant->delete() + ); + DB::commit(); + + Notification::make()->title( + __('lunarpanel::productoption.widgets.product-options.notifications.save-variants.success.title') + )->success()->send(); + }); + } + + public function getVariantLink($variantId) + { + return ProductVariantResource::getUrl('edit', [ + 'product' => $this->record, + 'record' => $variantId, + ]); + } + + protected function mapOptionValue(ProductOptionValue $value, bool $enabled = true) + { + return [ + 'id' => $value->id, + 'enabled' => $enabled, + 'value' => $value->translate('name'), + 'position' => $value->position, + ]; + } + + protected function mapOption(ProductOption $option, array $values = []): array + { + return [ + 'id' => $option->id, + 'key' => "option_{$option->id}", + 'value' => $option->translate('name'), + 'position' => $option->pivot?->position ?: count($this->configuredOptions) + 1, + 'readonly' => $option->shared, + 'option_values' => $values, + ]; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource.php b/packages/admin/src/Filament/Resources/ProductVariantResource.php new file mode 100644 index 0000000000..de29b30f15 --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource.php @@ -0,0 +1,316 @@ +generateNavigationItems([ + Pages\EditProductVariant::class, + Pages\ManageVariantMedia::class, + Pages\ManageVariantPricing::class, + Pages\ManageVariantIdentifiers::class, + Pages\ManageVariantInventory::class, + Pages\ManageVariantShipping::class, + ]); + } + + public static function getBaseBreadcrumbs(ProductVariant $productVariant): array + { + return [ + ProductResource::getUrl('edit', [ + 'record' => $productVariant->product, + ]) => $productVariant->product->attr('name'), + ProductResource::getUrl('variants', [ + 'record' => $productVariant->product, + ]) => 'Variants', + ProductVariantResource::getUrl('edit', [ + 'record' => $productVariant, + ]) => $productVariant->sku, + ]; + } + + public static function getDefaultForm(Form $form): Form + { + return $form + ->schema([ + static::getAttributeDataFormComponent(), + ]) + ->columns(1); + } + + protected static function getMainFormComponents(): array + { + return [ + static::getSkuFormComponent(), + ]; + } + + public static function getSkuFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('sku'); + } + + public static function getGtinFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('gtin')->label( + __('lunarpanel::productvariant.form.gtin.label') + ); + } + + public static function getMpnFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('mpn')->label( + __('lunarpanel::productvariant.form.mpn.label') + ); + } + + public static function getEanFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('ean')->label( + __('lunarpanel::productvariant.form.ean.label') + ); + } + + public static function getStockFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('stock') + ->label( + __('lunarpanel::productvariant.form.stock.label') + )->numeric(); + } + + public static function getBackorderFormComponent(): Forms\Components\TextInput + { + return + Forms\Components\TextInput::make('backorder') + ->label( + __('lunarpanel::productvariant.form.backorder.label') + )->numeric(); + } + + public static function getPurchasableFormComponent(): Forms\Components\Select + { + return Forms\Components\Select::make('purchasable') + ->options([ + 'always' => __('lunarpanel::productvariant.form.purchasable.options.always'), + 'in_stock' => __('lunarpanel::productvariant.form.purchasable.options.in_stock'), + 'backorder' => __('lunarpanel::productvariant.form.purchasable.options.backorder'), + ]) + ->label( + __('lunarpanel::productvariant.form.purchasable.label') + ); + } + + public static function getUnitQtyFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('unit_quantity') + ->label( + __('lunarpanel::productvariant.form.unit_quantity.label') + )->helperText( + __('lunarpanel::productvariant.form.unit_quantity.helper_text') + )->numeric(); + } + + public static function getQuantityIncrementFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('quantity_increment') + ->label( + __('lunarpanel::productvariant.form.quantity_increment.label') + )->helperText( + __('lunarpanel::productvariant.form.quantity_increment.helper_text') + )->numeric(); + } + + public static function getMinQuantityFormComponent(): Forms\Components\TextInput + { + return Forms\Components\TextInput::make('min_quantity') + ->label( + __('lunarpanel::productvariant.form.min_quantity.label') + )->helperText( + __('lunarpanel::productvariant.form.min_quantity.helper_text') + )->numeric(); + } + + public static function getShippableFormComponent(): Forms\Components\Toggle + { + return Forms\Components\Toggle::make('shippable')->label( + __('lunarpanel::productvariant.form.shippable.label') + )->columnSpan(2); + } + + public static function getMeasurements($key = null): array + { + $measurements = Converter::getMeasurements(); + + return collect( + array_keys($measurements[$key] ?? []) + )->mapWithKeys( + fn ($value) => [$value => $value] + )->toArray(); + } + + public static function getLengthFormComponent(): TextInputSelectAffix + { + return TextInputSelectAffix::make('length_value') + ->label( + __('lunarpanel::productvariant.form.length_value.label') + ) + ->numeric() + ->select( + fn () => Forms\Components\Select::make('length_unit') + ->options( + static::getMeasurements('length') + ) + ->label( + __('lunarpanel::productvariant.form.length_unit.label') + )->selectablePlaceholder(false) + ); + } + + public static function getWidthFormComponent(): TextInputSelectAffix + { + return TextInputSelectAffix::make('width_value') + ->label( + __('lunarpanel::productvariant.form.width_value.label') + ) + ->numeric() + ->select( + fn () => Forms\Components\Select::make('width_unit') + ->options( + static::getMeasurements('length') + ) + ->label( + __('lunarpanel::productvariant.form.width_unit.label') + )->selectablePlaceholder(false) + ); + } + + public static function getHeightFormComponent(): TextInputSelectAffix + { + return TextInputSelectAffix::make('height_value') + ->label( + __('lunarpanel::productvariant.form.height_value.label') + ) + ->numeric() + ->select( + fn () => Forms\Components\Select::make('height_unit') + ->options( + static::getMeasurements('length') + ) + ->label( + __('lunarpanel::productvariant.form.height_unit.label') + )->selectablePlaceholder(false) + ); + } + + public static function getWeightFormComponent(): TextInputSelectAffix + { + return TextInputSelectAffix::make('weight_value') + ->label( + __('lunarpanel::productvariant.form.weight_value.label') + ) + ->numeric() + ->select( + fn () => Forms\Components\Select::make('weight_unit') + ->options( + static::getMeasurements('weight') + ) + ->label( + __('lunarpanel::productvariant.form.weight_unit.label') + )->selectablePlaceholder(false) + ); + } + + public static function getVariantSwitcherWidget(Model $record): Action + { + return Action::make('switch_variant') + ->label( + __('lunarpanel::widgets.variant_switcher.label') + ) + ->modalContent(function () use ($record) { + return view('lunarpanel::actions.switch-variant', [ + 'record' => $record->product, + ]); + }) + ->slideOver(); + } + + protected static function getAttributeDataFormComponent(): Component + { + return Attributes::make()->statePath('attribute_data'); + } + + public static function getDefaultTable(Table $table): Table + { + return $table + ->columns(static::getTableColumns()) + ->filters([]) + ->actions([]) + ->bulkActions([]) + ->selectCurrentPageOnly() + ->deferLoading(); + } + + protected static function getTableColumns(): array + { + return [ + + ]; + } + + public static function getDefaultRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListProductVariants::route('/'), + 'edit' => Pages\EditProductVariant::route('/{record}/edit'), + 'pricing' => Pages\ManageVariantPricing::route('/{record}/pricing'), + 'media' => Pages\ManageVariantMedia::route('/{record}/media'), + 'identifiers' => Pages\ManageVariantIdentifiers::route('/{record}/identifiers'), + 'inventory' => Pages\ManageVariantInventory::route('/{record}/inventory'), + 'shipping' => Pages\ManageVariantShipping::route('/{record}/shipping'), + ]; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/EditProductVariant.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/EditProductVariant.php new file mode 100644 index 0000000000..d609ceb7f2 --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/EditProductVariant.php @@ -0,0 +1,78 @@ +getRecord() + ); + } + + protected function getCancelFormAction(): Action + { + return parent::getCancelFormAction()->url(function (Model $record) { + return ProductResource::getUrl('variants', [ + 'record' => $record->product, + ]); + }); + } + + public function mount(int|string $record): void + { + parent::mount($record); + + $variant = $this->getRecord(); + + if ($variant->mappedAttributes->isEmpty()) { + redirect()->to( + ProductVariantResource::getUrl('identifiers', [ + 'record' => $this->getRecord(), + ]) + ); + } + } + + public static function shouldRegisterNavigation(array $parameters = []): bool + { + return $parameters['record']->mappedAttributes->isNotEmpty(); + } + + protected function getDefaultHeaderActions(): array + { + return [ + ProductVariantResource::getVariantSwitcherWidget( + $this->getRecord() + ), + ]; + } + + public function getRelationManagers(): array + { + return []; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php new file mode 100644 index 0000000000..85b7384c77 --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php @@ -0,0 +1,26 @@ +url(function (Model $record) { + return ProductResource::getUrl('variants', [ + 'record' => $record->product, + ]); + }); + } + + public function getBreadcrumbs(): array + { + return [ + ...ProductVariantResource::getBaseBreadcrumbs( + $this->getRecord() + ), + ProductVariantResource::getUrl('inventory', [ + 'record' => $this->getRecord(), + ]) => $this->getTitle(), + ]; + } + + public static function getNavigationIcon(): ?string + { + return FilamentIcon::resolve('lunar::product-identifiers'); + } + + public function form(Form $form): Form + { + return $form->schema([ + Section::make()->schema([ + ProductVariantResource::getSkuFormComponent() + ->live()->unique( + table: fn () => $this->getRecord()->getTable(), + ignorable: $this->getRecord(), + ignoreRecord: true, + ), + ProductVariantResource::getGtinFormComponent(), + ProductVariantResource::getMpnFormComponent(), + ProductVariantResource::getEanFormComponent(), + ])->columns(1), + ]); + } + + protected function getDefaultHeaderActions(): array + { + return [ + ProductVariantResource::getVariantSwitcherWidget( + $this->getRecord() + ), + ]; + } + + public function getRelationManagers(): array + { + return []; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantInventory.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantInventory.php new file mode 100644 index 0000000000..3dec22cadd --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantInventory.php @@ -0,0 +1,85 @@ +url(function (Model $record) { + return ProductResource::getUrl('variants', [ + 'record' => $record->product, + ]); + }); + } + + public function getBreadcrumbs(): array + { + return [ + ...ProductVariantResource::getBaseBreadcrumbs( + $this->getRecord() + ), + ProductVariantResource::getUrl('inventory', [ + 'record' => $this->getRecord(), + ]) => $this->getTitle(), + ]; + } + + public static function getNavigationIcon(): ?string + { + return FilamentIcon::resolve('lunar::product-inventory'); + } + + protected function getDefaultHeaderActions(): array + { + return [ + ProductVariantResource::getVariantSwitcherWidget( + $this->getRecord() + ), + ]; + } + + public function form(Form $form): Form + { + return $form->schema([ + Section::make()->schema([ + ProductVariantResource::getStockFormComponent(), + ProductVariantResource::getBackorderFormComponent(), + ProductVariantResource::getPurchasableFormComponent(), + ProductVariantResource::getUnitQtyFormComponent(), + ProductVariantResource::getQuantityIncrementFormComponent(), + ProductVariantResource::getMinQuantityFormComponent(), + ])->columns([ + 'sm' => 1, + 'xl' => 3, + ]), + ]); + } + + public function getRelationManagers(): array + { + return []; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantMedia.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantMedia.php new file mode 100644 index 0000000000..0828ab9d06 --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantMedia.php @@ -0,0 +1,122 @@ +getRecord() + ), + ]; + } + + protected function getCancelFormAction(): Action + { + return parent::getCancelFormAction()->url(function (Model $record) { + return ProductResource::getUrl('variants', [ + 'record' => $record->product, + ]); + }); + } + + public function getBreadcrumbs(): array + { + return [ + ...ProductVariantResource::getBaseBreadcrumbs( + $this->getRecord() + ), + ProductVariantResource::getUrl('media', [ + 'record' => $this->getRecord(), + ]) => $this->getTitle(), + ]; + } + + protected function handleRecordUpdate(Model $record, array $data): Model + { + $record->images()->sync([ + $data['images'] => ['primary' => true], + ]); + + return $record; + } + + public function form(Form $form): Form + { + return $form->schema([ + Section::make()->schema([ + Shout::make('no_selection')->content( + __('lunarpanel::productvariant.pages.media.form.no_selection.label') + )->visible( + fn (Get $get) => ! $get('images') && $this->getRecord()->product->media()->count() + ), + Shout::make('no_media_available')->content( + __('lunarpanel::productvariant.pages.media.form.no_media_available.label') + )->visible( + fn (Get $get) => ! $this->getRecord()->product->media()->count() + ), + MediaSelect::make('images') + ->visible( + fn () => $this->getRecord()->product->media()->count() + ) + ->label( + __('lunarpanel::productvariant.pages.media.form.images.label') + ) + ->helperText( + __('lunarpanel::productvariant.pages.media.form.images.helper_text') + ) + ->afterStateHydrated(function (ProductVariant $record, MediaSelect $component) { + $image = $record->images->first(function ($media) { + return (bool) $media->pivot?->primary; + }); + $component->state($image?->id); + }) + ->options( + $this->getRecord()->product->media->mapWithKeys( + fn ($media) => [ + $media->id => $media->getUrl('small'), + ] + ) + ), + ]), + ]); + } + + public function getRelationManagers(): array + { + return []; + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php new file mode 100644 index 0000000000..ec7ce02ff3 --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php @@ -0,0 +1,158 @@ +getRecord() + ), + ]; + } + + protected function getCancelFormAction(): Action + { + return parent::getCancelFormAction()->url(function (Model $record) { + return ProductResource::getUrl('variants', [ + 'record' => $record->product, + ]); + }); + } + + public function getBreadcrumbs(): array + { + return [ + ...ProductVariantResource::getBaseBreadcrumbs( + $this->getRecord() + ), + ProductVariantResource::getUrl('pricing', [ + 'record' => $this->getRecord(), + ]) => $this->getTitle(), + ]; + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\TextInput::make('price')->formatStateUsing( + fn ($state) => $state?->decimal(rounding: false) + )->numeric()->unique( + modifyRuleUsing: function (Unique $rule, Forms\Get $get) { + $owner = $this->getOwnerRecord(); + + return $rule->where('customer_group_id', $get('customer_group_id')) + ->where('tier', $get('tier')) + ->where('currency_id', $get('currency_id')) + ->where('priceable_type', get_class($owner)) + ->where('priceable_id', $owner->id); + } + )->required(), + Forms\Components\TextInput::make('tier') + ->label( + __('lunarpanel::relationmanagers.pricing.form.tier.label') + )->numeric()->minValue(1)->required(), + Forms\Components\Select::make('currency_id') + ->label( + __('lunarpanel::relationmanagers.pricing.form.currency_id.label') + )->relationship(name: 'currency', titleAttribute: 'name')->required(), + Forms\Components\Select::make('customer_group_id') + ->label( + __('lunarpanel::relationmanagers.pricing.form.customer_group_id.label') + )->relationship(name: 'customerGroup', titleAttribute: 'name'), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->modifyQueryUsing( + fn ($query) => $query->orderBy('tier', 'asc') + ) + ->columns([ + Tables\Columns\TextColumn::make('price') + ->label( + __('lunarpanel::relationmanagers.pricing.table.price.label') + )->formatStateUsing( + fn ($state) => $state->formatted, + ), + Tables\Columns\TextColumn::make('currency.code')->label( + __('lunarpanel::relationmanagers.pricing.table.currency.label') + ), + Tables\Columns\TextColumn::make('tier')->label( + __('lunarpanel::relationmanagers.pricing.table.tier.label') + ), + Tables\Columns\TextColumn::make('customerGroup.name')->label( + __('lunarpanel::relationmanagers.pricing.table.customer_group.label') + ), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('currency') + ->relationship(name: 'currency', titleAttribute: 'name') + ->preload(), + Tables\Filters\SelectFilter::make('tier')->options( + Price::where('priceable_id', $this->getOwnerRecord()->id) + ->where('priceable_type', get_class($this->getOwnerRecord())) + ->get() + ->pluck('tier', 'tier') + ), + ]) + ->headerActions([ + Tables\Actions\CreateAction::make()->mutateFormDataUsing(function (array $data) { + $currencyModel = Currency::find($data['currency_id']); + + $data['price'] = (int) ($data['price'] * $currencyModel->factor); + + return $data; + }), + ]) + ->actions([ + Tables\Actions\EditAction::make()->mutateFormDataUsing(function (array $data): array { + $currencyModel = Currency::find($data['currency_id']); + + $data['price'] = (int) ($data['price'] * $currencyModel->factor); + + return $data; + }), + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantShipping.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantShipping.php new file mode 100644 index 0000000000..b5fa30a954 --- /dev/null +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantShipping.php @@ -0,0 +1,135 @@ +getRecord() + ), + ]; + } + + protected function getCancelFormAction(): Action + { + return parent::getCancelFormAction()->url(function (Model $record) { + return ProductResource::getUrl('variants', [ + 'record' => $record->product, + ]); + }); + } + + public function getBreadcrumbs(): array + { + return [ + ...ProductVariantResource::getBaseBreadcrumbs( + $this->getRecord() + ), + ProductVariantResource::getUrl('shipping', [ + 'record' => $this->getRecord(), + ]) => $this->getTitle(), + ]; + } + + protected function handleRecordUpdate(Model $record, array $data): Model + { + $volume = static::getVolume( + [ + 'value' => $data['width_value'], + 'unit' => $data['width_unit'] ?? $record->width_unit, + ], + [ + 'value' => $data['length_value'], + 'unit' => $data['length_unit'] ?? $record->length_unit, + ], + [ + 'value' => $data['height_value'], + 'unit' => $data['height_unit'] ?? $record->height_unit, + ] + ); + + $record->update([ + ...$data, + ...[ + 'volume_unit' => 'l', + 'volume_value' => $volume, + ], + ]); + + return $record; + } + + public static function getVolume($width = [], $length = [], $height = []) + { + $width = Converter::value($width['value']) + ->from('length.'.$width['unit']) + ->to('length.cm') + ->convert() + ->getValue(); + $length = Converter::value($length['value']) + ->from('length.'.$length['unit']) + ->to('length.cm') + ->convert() + ->getValue(); + + $height = Converter::value($height['value']) + ->from('length.'.$height['unit']) + ->to('length.cm') + ->convert() + ->getValue(); + + return Converter::from('volume.ml')->to('volume.l')->value($length * $width * $height)->convert()->getValue(); + } + + public function form(Form $form): Form + { + return $form->schema([ + Section::make()->schema([ + ProductVariantResource::getShippableFormComponent(), + ProductVariantResource::getLengthFormComponent(), + ProductVariantResource::getWidthFormComponent(), + ProductVariantResource::getHeightFormComponent(), + ProductVariantResource::getWeightFormComponent(), + ])->columns([ + 'sm' => 1, + 'xl' => 2, + ]), + ]); + } + + public function getRelationManagers(): array + { + return []; + } +} diff --git a/packages/admin/src/Filament/Widgets/Products/VariantSwitcherTable.php b/packages/admin/src/Filament/Widgets/Products/VariantSwitcherTable.php new file mode 100644 index 0000000000..bff6c1caf0 --- /dev/null +++ b/packages/admin/src/Filament/Widgets/Products/VariantSwitcherTable.php @@ -0,0 +1,102 @@ +record->id); + } + + protected function getTableFilters(): array + { + $optionValues = ProductOptionValue::whereHas( + 'variants', + fn ($query) => $query->whereIn( + 'variant_id', + $this->getTableQuery()->pluck('id') + )) + ->with(['option']) + ->get() + ->groupBy('product_option_id'); + + $filters = []; + + foreach ($optionValues as $values) { + $option = $values->first()->option; + + $filters[] = Tables\Filters\SelectFilter::make( + $option->handle + )->label($option->translate('name')) + ->options( + $values->mapWithKeys( + fn ($value) => [$value->id => $value->translate('name')] + ) + )->modifyQueryUsing(function (Builder $query, array $data) { + $value = $data['value']; + + return $query->when( + $value, + function ($query) use ($value) { + $query->whereHas('values', function ($relation) use ($value) { + $table = $relation->getQuery()->from; + + $relation->where("{$table}.id", '=', $value); + }); + } + ); + }); + } + + return $filters; + } + + protected function getTableColumns(): array + { + return [ + Tables\Columns\TextColumn::make('sku') + ->label( + __('lunarpanel::widgets.variant_switcher.table.sku.label') + )->searchable(), + Tables\Columns\TextColumn::make('values') + ->label( + __('lunarpanel::widgets.variant_switcher.table.values.label') + ) + ->formatStateUsing( + function (Model $record) { + return $record->values->map( + fn ($value) => $value->translate('name') + )->join(', '); + } + ), + ]; + } + + protected function getTableRecordUrlUsing(): ?Closure + { + return function (ProductVariant $variant) { + return ProductVariantResource::getUrl('edit', [ + 'record' => $variant, + ]); + }; + } + + protected function getTableHeading(): string|Htmlable|null + { + return ''; + } +} diff --git a/packages/admin/src/LunarPanelManager.php b/packages/admin/src/LunarPanelManager.php index bfb880fe68..abefae1135 100644 --- a/packages/admin/src/LunarPanelManager.php +++ b/packages/admin/src/LunarPanelManager.php @@ -56,6 +56,7 @@ class LunarPanelManager Resources\ProductOptionResource::class, Resources\ProductResource::class, Resources\ProductTypeResource::class, + Resources\ProductVariantResource::class, Resources\StaffResource::class, Resources\TagResource::class, Resources\TaxClassResource::class, diff --git a/packages/admin/src/Support/Forms/Components/MediaSelect.php b/packages/admin/src/Support/Forms/Components/MediaSelect.php new file mode 100644 index 0000000000..b62d70982a --- /dev/null +++ b/packages/admin/src/Support/Forms/Components/MediaSelect.php @@ -0,0 +1,15 @@ +groupBy(['product_id', 'product_option_id']) ->orderBy('product_id') - ->chunk(2, function ($rows) { + ->chunk(200, function ($rows) { DB::table( $this->prefix.'product_product_option' )->insert( diff --git a/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php b/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php new file mode 100644 index 0000000000..e0a5b2e6ec --- /dev/null +++ b/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php @@ -0,0 +1,28 @@ +prefix.'product_options', function (Blueprint $table) { + $table->dropUnique( + $this->prefix.'product_options_handle_unique' + ); + }); + + Schema::table($this->prefix.'product_options', function (Blueprint $table) { + $table->index('handle'); + }); + } + + public function down() + { + // .. + } +} diff --git a/packages/core/src/LunarServiceProvider.php b/packages/core/src/LunarServiceProvider.php index e74400fca4..c72f0f8f54 100644 --- a/packages/core/src/LunarServiceProvider.php +++ b/packages/core/src/LunarServiceProvider.php @@ -64,6 +64,8 @@ use Lunar\Models\Language; use Lunar\Models\Order; use Lunar\Models\OrderLine; +use Lunar\Models\ProductOption; +use Lunar\Models\ProductVariant; use Lunar\Models\Transaction; use Lunar\Models\Url; use Lunar\Observers\AddressObserver; @@ -75,6 +77,8 @@ use Lunar\Observers\LanguageObserver; use Lunar\Observers\OrderLineObserver; use Lunar\Observers\OrderObserver; +use Lunar\Observers\ProductOptionObserver; +use Lunar\Observers\ProductVariantObserver; use Lunar\Observers\TransactionObserver; use Lunar\Observers\UrlObserver; @@ -277,6 +281,8 @@ protected function registerObservers(): void Url::observe(UrlObserver::class); Collection::observe(CollectionObserver::class); CartLine::observe(CartLineObserver::class); + ProductOption::observe(ProductOptionObserver::class); + ProductVariant::observe(ProductVariantObserver::class); Order::observe(OrderObserver::class); OrderLine::observe(OrderLineObserver::class); Address::observe(AddressObserver::class); diff --git a/packages/core/src/Models/ProductVariant.php b/packages/core/src/Models/ProductVariant.php index 6759c1573e..74b4287e53 100644 --- a/packages/core/src/Models/ProductVariant.php +++ b/packages/core/src/Models/ProductVariant.php @@ -9,6 +9,7 @@ use Lunar\Base\BaseModel; use Lunar\Base\Casts\AsAttributeData; use Lunar\Base\Purchasable; +use Lunar\Base\Traits\HasAttributes; use Lunar\Base\Traits\HasDimensions; use Lunar\Base\Traits\HasMacros; use Lunar\Base\Traits\HasPrices; @@ -51,6 +52,7 @@ */ class ProductVariant extends BaseModel implements Purchasable { + use HasAttributes; use HasDimensions; use HasFactory; use HasMacros; diff --git a/packages/core/src/Observers/ProductOptionObserver.php b/packages/core/src/Observers/ProductOptionObserver.php new file mode 100644 index 0000000000..6708c74fb4 --- /dev/null +++ b/packages/core/src/Observers/ProductOptionObserver.php @@ -0,0 +1,18 @@ +values()->delete(); + } +} diff --git a/packages/core/src/Observers/ProductVariantObserver.php b/packages/core/src/Observers/ProductVariantObserver.php new file mode 100644 index 0000000000..5d58b8e347 --- /dev/null +++ b/packages/core/src/Observers/ProductVariantObserver.php @@ -0,0 +1,20 @@ +prices()->delete(); + $productVariant->values()->detach(); + $productVariant->images()->detach(); + } +} diff --git a/tests/admin/Unit/Actions/Products/MapVariantsToProductOptionsTest.php b/tests/admin/Unit/Actions/Products/MapVariantsToProductOptionsTest.php new file mode 100644 index 0000000000..66cda680e0 --- /dev/null +++ b/tests/admin/Unit/Actions/Products/MapVariantsToProductOptionsTest.php @@ -0,0 +1,79 @@ +group('support.actions'); + +it('can map variants given one set of option values', function () { + + $optionValues = [ + 'Shoe Size' => [ + 'UK-5', + 'UK-10', + 'UK-15', + ], + ]; + + $variants = [ + [ + 'id' => 1, + 'sku' => 'ABC', + 'values' => [ + 'Shoe Size' => 'UK-5', + ], + ], + [ + 'id' => 2, + 'sku' => 'DEF', + 'values' => [ + 'Shoe Size' => 'UK-10', + ], + ], + [ + 'id' => 3, + 'sku' => 'GHI', + 'values' => [ + 'Shoe Size' => 'UK-15', + ], + ], + ]; + + $result = MapVariantsToProductOptions::map($optionValues, $variants); + + expect($result[0]['sku'])->toBe('ABC'); + expect($result[1]['sku'])->toBe('DEF'); + expect($result[2]['sku'])->toBe('GHI'); +}); + +it('can map variants given three sets of option values', function () { + + $optionValues = [ + 'Size' => [ + 'Small', + 'Medium', + ], + 'Colour' => [ + 'Blue', + 'Black', + ], + 'Material' => [ + 'Black', + ], + ]; + + $variants = [ + [ + 'id' => 1, + 'sku' => 'SMBLK', + 'values' => [ + 'Size' => 'Small', + 'Colour' => 'Black', + ], + ], + ]; + + $result = MapVariantsToProductOptions::map($optionValues, $variants); + + expect($result)->toHaveCount(4); +})->group('momo'); From e83df15b1483df2af256ebda70a43ac93b5d533e Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Tue, 30 Jan 2024 09:12:48 +0100 Subject: [PATCH 11/34] Fix - Migration rollback (#1514) --- ...24_01_24_100000_update_product_option_handle_fk.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php b/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php index e0a5b2e6ec..5d47915358 100644 --- a/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php +++ b/packages/core/database/migrations/2024_01_24_100000_update_product_option_handle_fk.php @@ -23,6 +23,14 @@ public function up() public function down() { - // .. + Schema::table($this->prefix.'product_options', function (Blueprint $table) { + $table->dropIndex( + $this->prefix.'product_options_handle_index' + ); + }); + + Schema::table($this->prefix.'product_options', function (Blueprint $table) { + $table->unique('handle'); + }); } } From 32c97195767e49f511de0d931f16b339a4ee13be Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Tue, 30 Jan 2024 09:16:38 +0100 Subject: [PATCH 12/34] Fix - Create shared product option (#1512) --- .../ProductOptionResource/Pages/CreateProductOption.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php b/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php index bde975dca9..5a480614b7 100644 --- a/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php +++ b/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php @@ -8,4 +8,11 @@ class CreateProductOption extends BaseCreateRecord { protected static string $resource = ProductOptionResource::class; + + protected function mutateFormDataBeforeCreate(array $data): array + { + $data['shared'] = true; + + return $data; + } } From 223cf86fa934d1b3192120add04d93c2220ac447 Mon Sep 17 00:00:00 2001 From: alecritson Date: Tue, 30 Jan 2024 08:18:53 +0000 Subject: [PATCH 13/34] chore: fix code style --- .../ProductOptionResource/Pages/CreateProductOption.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php b/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php index 5a480614b7..b362ee385c 100644 --- a/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php +++ b/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/CreateProductOption.php @@ -12,7 +12,7 @@ class CreateProductOption extends BaseCreateRecord protected function mutateFormDataBeforeCreate(array $data): array { $data['shared'] = true; - + return $data; } } From d3fe0f0e47a7687e5fff175082e1edc913f6c9ef Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Tue, 30 Jan 2024 12:53:07 +0000 Subject: [PATCH 14/34] Fix product option deletion (#1516) --- .../Widgets/ProductOptionsWidget.php | 7 +++++-- packages/core/src/LunarServiceProvider.php | 3 +++ packages/core/src/Models/ProductOption.php | 11 +++++++++++ .../src/Observers/ProductOptionObserver.php | 8 ++++++-- .../Observers/ProductOptionValueObserver.php | 18 ++++++++++++++++++ 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/Observers/ProductOptionValueObserver.php diff --git a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php index a57ea1ce6f..cc5602dfb5 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php @@ -347,13 +347,16 @@ public function saveVariantsAction() if (! count($this->variants)) { $variant = $this->record->variants()->first(); $variant->values()->detach(); - $this->record->productOptions()->exclusive()->delete(); + $this->record->productOptions()->exclusive()->each( + fn (ProductOption $productOption) => $productOption->delete() + ); + $this->record->productOptions()->shared()->detach(); $this->record->variants() ->where('id', '!=', $variant->id) ->get() ->each( - fn ($variant) => $variant->delete() + fn (ProductVariant $variant) => $variant->delete() ); DB::commit(); diff --git a/packages/core/src/LunarServiceProvider.php b/packages/core/src/LunarServiceProvider.php index c72f0f8f54..77329f07ac 100644 --- a/packages/core/src/LunarServiceProvider.php +++ b/packages/core/src/LunarServiceProvider.php @@ -65,6 +65,7 @@ use Lunar\Models\Order; use Lunar\Models\OrderLine; use Lunar\Models\ProductOption; +use Lunar\Models\ProductOptionValue; use Lunar\Models\ProductVariant; use Lunar\Models\Transaction; use Lunar\Models\Url; @@ -78,6 +79,7 @@ use Lunar\Observers\OrderLineObserver; use Lunar\Observers\OrderObserver; use Lunar\Observers\ProductOptionObserver; +use Lunar\Observers\ProductOptionValueObserver; use Lunar\Observers\ProductVariantObserver; use Lunar\Observers\TransactionObserver; use Lunar\Observers\UrlObserver; @@ -282,6 +284,7 @@ protected function registerObservers(): void Collection::observe(CollectionObserver::class); CartLine::observe(CartLineObserver::class); ProductOption::observe(ProductOptionObserver::class); + ProductOptionValue::observe(ProductOptionValueObserver::class); ProductVariant::observe(ProductVariantObserver::class); Order::observe(OrderObserver::class); OrderLine::observe(OrderLineObserver::class); diff --git a/packages/core/src/Models/ProductOption.php b/packages/core/src/Models/ProductOption.php index 6e22b99dc9..b10d97c13e 100644 --- a/packages/core/src/Models/ProductOption.php +++ b/packages/core/src/Models/ProductOption.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Casts\AsCollection; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Lunar\Base\BaseModel; use Lunar\Base\Traits\HasMacros; @@ -96,4 +97,14 @@ public function values(): HasMany { return $this->hasMany(ProductOptionValue::class)->orderBy('position'); } + + public function products(): BelongsToMany + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany( + Product::class, + "{$prefix}product_product_option" + )->withPivot(['position'])->orderByPivot('position'); + } } diff --git a/packages/core/src/Observers/ProductOptionObserver.php b/packages/core/src/Observers/ProductOptionObserver.php index 6708c74fb4..4826b99f5d 100644 --- a/packages/core/src/Observers/ProductOptionObserver.php +++ b/packages/core/src/Observers/ProductOptionObserver.php @@ -3,16 +3,20 @@ namespace Lunar\Observers; use Lunar\Models\ProductOption; +use Lunar\Models\ProductOptionValue; class ProductOptionObserver { /** - * Handle the ProductVariant "deleted" event. + * Handle the ProductOption "deleting" event. * * @return void */ public function deleting(ProductOption $productOption) { - $productOption->values()->delete(); + $productOption->products()->detach(); + $productOption->values()->each( + fn (ProductOptionValue $optionValue) => $optionValue->delete() + ); } } diff --git a/packages/core/src/Observers/ProductOptionValueObserver.php b/packages/core/src/Observers/ProductOptionValueObserver.php new file mode 100644 index 0000000000..b372caab40 --- /dev/null +++ b/packages/core/src/Observers/ProductOptionValueObserver.php @@ -0,0 +1,18 @@ +variants()->detach(); + } +} From af50317d967adc6a71a3c87c168f1b426477fc2c Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Tue, 30 Jan 2024 14:30:41 +0100 Subject: [PATCH 15/34] Fix - Remove redirect like Product and Collection resource (#1506) --- .../src/Filament/Resources/BrandResource/Pages/EditBrand.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/admin/src/Filament/Resources/BrandResource/Pages/EditBrand.php b/packages/admin/src/Filament/Resources/BrandResource/Pages/EditBrand.php index a3b6a9cbfe..686829ca4e 100644 --- a/packages/admin/src/Filament/Resources/BrandResource/Pages/EditBrand.php +++ b/packages/admin/src/Filament/Resources/BrandResource/Pages/EditBrand.php @@ -16,9 +16,4 @@ protected function getDefaultHeaderActions(): array Actions\DeleteAction::make(), ]; } - - protected function getRedirectUrl(): string - { - return $this->getResource()::getUrl('index'); - } } From 88dc5747f7d69affc6299312c7acabb23d4f459d Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Tue, 30 Jan 2024 15:50:08 +0100 Subject: [PATCH 16/34] Feature - Display new / returning customer (#1507) --- packages/admin/resources/lang/en/customer.php | 6 +++++ packages/admin/resources/lang/en/order.php | 3 +++ .../src/Filament/Resources/OrderResource.php | 7 +++++ packages/admin/src/Support/CustomerStatus.php | 27 +++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 packages/admin/src/Support/CustomerStatus.php diff --git a/packages/admin/resources/lang/en/customer.php b/packages/admin/resources/lang/en/customer.php index 71dc438490..6dfe6c5cd5 100644 --- a/packages/admin/resources/lang/en/customer.php +++ b/packages/admin/resources/lang/en/customer.php @@ -28,6 +28,12 @@ 'account_reference' => [ 'label' => 'Account Reference', ], + 'new' => [ + 'label' => 'New', + ], + 'returning' => [ + 'label' => 'Returning', + ], ], 'form' => [ diff --git a/packages/admin/resources/lang/en/order.php b/packages/admin/resources/lang/en/order.php index d0fd9cb2ea..25d8db99e2 100644 --- a/packages/admin/resources/lang/en/order.php +++ b/packages/admin/resources/lang/en/order.php @@ -44,6 +44,9 @@ 'date' => [ 'label' => 'Date', ], + 'new_customer' => [ + 'label' => 'Customer Type', + ], ], 'form' => [ diff --git a/packages/admin/src/Filament/Resources/OrderResource.php b/packages/admin/src/Filament/Resources/OrderResource.php index 07a9cedfd4..257c45cee0 100644 --- a/packages/admin/src/Filament/Resources/OrderResource.php +++ b/packages/admin/src/Filament/Resources/OrderResource.php @@ -11,6 +11,7 @@ use Lunar\Admin\Filament\Resources\OrderResource\Pages; use Lunar\Admin\Filament\Resources\OrderResource\Pages\ManageOrder; use Lunar\Admin\Support\Actions\Orders\UpdateStatusBulkAction; +use Lunar\Admin\Support\CustomerStatus; use Lunar\Admin\Support\OrderStatus; use Lunar\Admin\Support\Resources\BaseResource; use Lunar\Models\Order; @@ -89,6 +90,12 @@ public static function getTableColumns(): array ->label(__('lunarpanel::order.table.customer_reference.label')), Tables\Columns\TextColumn::make('shippingAddress.fullName') ->label(__('lunarpanel::order.table.customer.label')), + Tables\Columns\TextColumn::make('new_customer') + ->label(__('lunarpanel::order.table.new_customer.label')) + ->formatStateUsing(fn (bool $state) => CustomerStatus::getLabel($state)) + ->color(fn (bool $state) => CustomerStatus::getColor($state)) + ->icon(fn (bool $state) => CustomerStatus::getIcon($state)) + ->badge(), Tables\Columns\TextColumn::make('shippingAddress.postcode') ->label(__('lunarpanel::order.table.postcode.label')), Tables\Columns\TextColumn::make('shippingAddress.contact_email') diff --git a/packages/admin/src/Support/CustomerStatus.php b/packages/admin/src/Support/CustomerStatus.php new file mode 100644 index 0000000000..1f8203afac --- /dev/null +++ b/packages/admin/src/Support/CustomerStatus.php @@ -0,0 +1,27 @@ + Date: Wed, 31 Jan 2024 10:18:15 +0100 Subject: [PATCH 17/34] Fix - Collection group not trigger actions (#1519) --- .../CollectionGroupResource/Pages/ListCollectionGroups.php | 4 ++-- .../Resources/CollectionResource/Pages/ListCollections.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php b/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php index b17fc5a0c4..75e155f7a2 100644 --- a/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php +++ b/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php @@ -3,10 +3,10 @@ namespace Lunar\Admin\Filament\Resources\CollectionGroupResource\Pages; use Filament\Actions; -use Filament\Resources\Pages\ListRecords; +use Lunar\Admin\Support\Pages\BaseListRecords; use Lunar\Admin\Filament\Resources\CollectionGroupResource; -class ListCollectionGroups extends ListRecords +class ListCollectionGroups extends BaseListRecords { protected static string $resource = CollectionGroupResource::class; diff --git a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php index 48f7f64f15..e9881d5087 100644 --- a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php +++ b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php @@ -2,10 +2,10 @@ namespace Lunar\Admin\Filament\Resources\CollectionResource\Pages; -use Filament\Resources\Pages\ListRecords; +use Lunar\Admin\Support\Pages\BaseListRecords; use Lunar\Admin\Filament\Resources\CollectionResource; -class ListCollections extends ListRecords +class ListCollections extends BaseListRecords { protected static string $resource = CollectionResource::class; From 3e8db73e47f8de9840f0fd0d6cfca9926ee4d1c5 Mon Sep 17 00:00:00 2001 From: alecritson Date: Wed, 31 Jan 2024 09:20:30 +0000 Subject: [PATCH 18/34] chore: fix code style --- .../CollectionGroupResource/Pages/ListCollectionGroups.php | 2 +- .../Resources/CollectionResource/Pages/ListCollections.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php b/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php index 75e155f7a2..64537bdd73 100644 --- a/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php +++ b/packages/admin/src/Filament/Resources/CollectionGroupResource/Pages/ListCollectionGroups.php @@ -3,8 +3,8 @@ namespace Lunar\Admin\Filament\Resources\CollectionGroupResource\Pages; use Filament\Actions; -use Lunar\Admin\Support\Pages\BaseListRecords; use Lunar\Admin\Filament\Resources\CollectionGroupResource; +use Lunar\Admin\Support\Pages\BaseListRecords; class ListCollectionGroups extends BaseListRecords { diff --git a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php index e9881d5087..e71d6b519d 100644 --- a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php +++ b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ListCollections.php @@ -2,8 +2,8 @@ namespace Lunar\Admin\Filament\Resources\CollectionResource\Pages; -use Lunar\Admin\Support\Pages\BaseListRecords; use Lunar\Admin\Filament\Resources\CollectionResource; +use Lunar\Admin\Support\Pages\BaseListRecords; class ListCollections extends BaseListRecords { From e8ca9c6591ccda034489a85a422cbe653912bdb3 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 31 Jan 2024 15:56:53 +0100 Subject: [PATCH 19/34] Feature - Support of widget on a resource page (#1475) This PR add new hook to extend the ressource page with Filament header and footer widgets --- docs/admin/extending/pages.md | 64 ++++++++++++++++++- packages/admin/src/LunarPanelManager.php | 4 +- .../src/Support/Pages/BaseCreateRecord.php | 2 + .../src/Support/Pages/BaseEditRecord.php | 2 + .../src/Support/Pages/BaseListRecords.php | 2 + .../Pages/Concerns/ExtendsFooterWidgets.php | 16 +++++ .../Pages/Concerns/ExtendsHeaderWidgets.php | 16 +++++ 7 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 packages/admin/src/Support/Pages/Concerns/ExtendsFooterWidgets.php create mode 100644 packages/admin/src/Support/Pages/Concerns/ExtendsHeaderWidgets.php diff --git a/docs/admin/extending/pages.md b/docs/admin/extending/pages.md index 6e31510c2a..d2ea5309ac 100644 --- a/docs/admin/extending/pages.md +++ b/docs/admin/extending/pages.md @@ -31,9 +31,20 @@ use Filament\Actions; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Lunar\Admin\Support\Extending\CreatePageExtension; +use Lunar\Admin\Filament\Widgets; class MyCreateExtension extends CreatePageExtension { + public function headerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\OrderStatsOverview::make(), + ]; + + return $widgets; + } + public function headerActions(array $actions): array { $actions = [ @@ -54,6 +65,16 @@ class MyCreateExtension extends CreatePageExtension return $actions; } + public function footerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\LatestOrdersTable::make(), + ]; + + return $widgets; + } + public function beforeCreate(array $data): array { $data['model_code'] .= 'ABC'; @@ -85,9 +106,20 @@ use Filament\Actions; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Lunar\Admin\Support\Extending\EditPageExtension; +use Lunar\Admin\Filament\Widgets; class MyEditExtension extends EditPageExtension { + public function headerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\OrderStatsOverview::make(), + ]; + + return $widgets; + } + public function headerActions(array $actions): array { $actions = [ @@ -112,6 +144,16 @@ class MyEditExtension extends EditPageExtension return $actions; } + public function footerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\LatestOrdersTable::make(), + ]; + + return $widgets; + } + public function beforeFill(array $data): array { $data['model_code'] .= 'ABC'; @@ -153,9 +195,20 @@ use Filament\Actions; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Lunar\Admin\Support\Extending\ListPageExtension; +use Lunar\Admin\Filament\Widgets; class MyListExtension extends ListPageExtension { + public function headerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\OrderStatsOverview::make(), + ]; + + return $widgets; + } + public function headerActions(array $actions): array { $actions = [ @@ -169,7 +222,16 @@ class MyListExtension extends ListPageExtension return $actions; } - + + public function footerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\LatestOrdersTable::make(), + ]; + + return $widgets; + } } // Typically placed in your AppServiceProvider file... diff --git a/packages/admin/src/LunarPanelManager.php b/packages/admin/src/LunarPanelManager.php index abefae1135..5e31915baf 100644 --- a/packages/admin/src/LunarPanelManager.php +++ b/packages/admin/src/LunarPanelManager.php @@ -264,7 +264,9 @@ public function callHook(string $class, string $hookName, ...$args): mixed { if (isset($this->extensions[$class])) { foreach ($this->extensions[$class] as $extension) { - $args[0] = $extension->{$hookName}(...$args); + if (method_exists($extension, $hookName)) { + $args[0] = $extension->{$hookName}(...$args); + } } } diff --git a/packages/admin/src/Support/Pages/BaseCreateRecord.php b/packages/admin/src/Support/Pages/BaseCreateRecord.php index 3d60bebf96..679e2e7849 100644 --- a/packages/admin/src/Support/Pages/BaseCreateRecord.php +++ b/packages/admin/src/Support/Pages/BaseCreateRecord.php @@ -7,8 +7,10 @@ abstract class BaseCreateRecord extends CreateRecord { + use Concerns\ExtendsFooterWidgets; use Concerns\ExtendsFormActions; use Concerns\ExtendsHeaderActions; + use Concerns\ExtendsHeaderWidgets; use \Lunar\Admin\Support\Concerns\CallsHooks; protected function mutateFormDataBeforeCreate(array $data): array diff --git a/packages/admin/src/Support/Pages/BaseEditRecord.php b/packages/admin/src/Support/Pages/BaseEditRecord.php index b52991fc07..a51b632e07 100644 --- a/packages/admin/src/Support/Pages/BaseEditRecord.php +++ b/packages/admin/src/Support/Pages/BaseEditRecord.php @@ -7,8 +7,10 @@ abstract class BaseEditRecord extends EditRecord { + use Concerns\ExtendsFooterWidgets; use Concerns\ExtendsFormActions; use Concerns\ExtendsHeaderActions; + use Concerns\ExtendsHeaderWidgets; use \Lunar\Admin\Support\Concerns\CallsHooks; protected function mutateFormDataBeforeFill(array $data): array diff --git a/packages/admin/src/Support/Pages/BaseListRecords.php b/packages/admin/src/Support/Pages/BaseListRecords.php index b199b793fa..4d7f21ffab 100644 --- a/packages/admin/src/Support/Pages/BaseListRecords.php +++ b/packages/admin/src/Support/Pages/BaseListRecords.php @@ -8,7 +8,9 @@ abstract class BaseListRecords extends ListRecords { + use Concerns\ExtendsFooterWidgets; use Concerns\ExtendsHeaderActions; + use Concerns\ExtendsHeaderWidgets; use \Lunar\Admin\Support\Concerns\CallsHooks; protected function applySearchToTableQuery(Builder $query): Builder diff --git a/packages/admin/src/Support/Pages/Concerns/ExtendsFooterWidgets.php b/packages/admin/src/Support/Pages/Concerns/ExtendsFooterWidgets.php new file mode 100644 index 0000000000..0c026e854f --- /dev/null +++ b/packages/admin/src/Support/Pages/Concerns/ExtendsFooterWidgets.php @@ -0,0 +1,16 @@ +callLunarHook('footerWidgets', $this->getDefaultFooterWidgets()); + } +} diff --git a/packages/admin/src/Support/Pages/Concerns/ExtendsHeaderWidgets.php b/packages/admin/src/Support/Pages/Concerns/ExtendsHeaderWidgets.php new file mode 100644 index 0000000000..66f992a213 --- /dev/null +++ b/packages/admin/src/Support/Pages/Concerns/ExtendsHeaderWidgets.php @@ -0,0 +1,16 @@ +callLunarHook('headerWidgets', $this->getDefaultHeaderWidgets()); + } +} From b1ec933a400f2b94a493434f011dec6b06c1814d Mon Sep 17 00:00:00 2001 From: Nguyen Viet Date: Wed, 31 Jan 2024 22:02:49 +0700 Subject: [PATCH 20/34] fix/Update return types of translate methods (#1476) I updated the return type of these methods, so they should be `string` or `null`. I'm not sure you'll change the return type to the PHP type system, so I update the doc blocks only. This could help us avoid warnings when working on the template which is wrong image Co-authored-by: Alec Ritson --- packages/core/src/Base/Traits/HasTranslations.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/Base/Traits/HasTranslations.php b/packages/core/src/Base/Traits/HasTranslations.php index e9bbb5f922..c09592ae2e 100644 --- a/packages/core/src/Base/Traits/HasTranslations.php +++ b/packages/core/src/Base/Traits/HasTranslations.php @@ -12,7 +12,7 @@ trait HasTranslations * * @param string $attribute * @param string $locale - * @return string + * @return string|null */ public function translate($attribute, $locale = null) { @@ -43,7 +43,7 @@ public function translate($attribute, $locale = null) * * @param string $attribute * @param string $locale - * @return string + * @return string|null */ public function translateAttribute($attribute, $locale = null) { @@ -61,9 +61,9 @@ public function translateAttribute($attribute, $locale = null) $value = Arr::get($translations, $locale ?: app()->getLocale(), Arr::first($translations)); - // We we don't have a value, we just return null as it may not have a value. + // When we don't have a value, we just return null as it may not have a value. if (! $value) { - return; + return null; } /** @@ -80,7 +80,7 @@ public function translateAttribute($attribute, $locale = null) /** * Shorthand to translate an attribute. * - * @return void + * @return string|null */ public function attr(...$params) { From 656a1542b75e6896789ede7eb852f96031c3c7d5 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 31 Jan 2024 16:27:02 +0100 Subject: [PATCH 21/34] Feature - Add helper text below render attribute (#1478) After speaking about this feature with @glennjacobs. I created this PR add new description field on attribute to display the Filament helper text. --- .../admin/resources/lang/en/attribute.php | 7 ++++++ .../AttributesRelationManager.php | 13 ++++++++++- .../admin/src/Support/FieldTypes/Dropdown.php | 3 ++- .../admin/src/Support/FieldTypes/File.php | 3 ++- .../src/Support/FieldTypes/ListField.php | 2 +- .../src/Support/FieldTypes/TextField.php | 6 +++-- .../admin/src/Support/FieldTypes/Toggle.php | 6 ++++- .../src/Support/FieldTypes/TranslatedText.php | 3 ++- .../admin/src/Support/FieldTypes/YouTube.php | 2 +- .../database/factories/AttributeFactory.php | 3 +++ ...00_add_description_to_attributes_table.php | 22 +++++++++++++++++++ packages/core/src/Models/Attribute.php | 1 + .../Pages/ManageCollectionChildrenTest.php | 3 +++ .../ProductResource/ListProductsTest.php | 3 +++ tests/core/Unit/Models/AttributeTest.php | 3 +++ 15 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php diff --git a/packages/admin/resources/lang/en/attribute.php b/packages/admin/resources/lang/en/attribute.php index dd629d27d0..773a8e5cd5 100644 --- a/packages/admin/resources/lang/en/attribute.php +++ b/packages/admin/resources/lang/en/attribute.php @@ -10,6 +10,9 @@ 'name' => [ 'label' => 'Name', ], + 'description' => [ + 'label' => 'Description', + ], 'handle' => [ 'label' => 'Handle', ], @@ -25,6 +28,10 @@ 'name' => [ 'label' => 'Name', ], + 'description' => [ + 'label' => 'Description', + 'helper' => 'Use to display the helper text below the entry' + ], 'handle' => [ 'label' => 'Handle', ], diff --git a/packages/admin/src/Filament/Resources/AttributeGroupResource/RelationManagers/AttributesRelationManager.php b/packages/admin/src/Filament/Resources/AttributeGroupResource/RelationManagers/AttributesRelationManager.php index 6a1c83824a..332703a358 100644 --- a/packages/admin/src/Filament/Resources/AttributeGroupResource/RelationManagers/AttributesRelationManager.php +++ b/packages/admin/src/Filament/Resources/AttributeGroupResource/RelationManagers/AttributesRelationManager.php @@ -27,7 +27,7 @@ public function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('name.en') + Forms\Components\TextInput::make('name.en') // TODO: localise ->label( __('lunarpanel::attribute.form.name.label') ) @@ -40,6 +40,14 @@ public function form(Form $form): Form } $set('handle', Str::slug($state)); }), + Forms\Components\TextInput::make('description.en') // TODO: localise + ->label( + __('lunarpanel::attribute.form.description.label') + ) + ->helperText( + __('lunarpanel::attribute.form.description.helper') + ) + ->maxLength(255), Forms\Components\TextInput::make('handle') ->label( __('lunarpanel::attribute.form.handle.label') @@ -96,6 +104,9 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('name.en')->label( __('lunarpanel::attribute.table.name.label') ), + Tables\Columns\TextColumn::make('description.en')->label( + __('lunarpanel::attribute.table.description.label') + ), Tables\Columns\TextColumn::make('handle') ->label( __('lunarpanel::attribute.table.handle.label') diff --git a/packages/admin/src/Support/FieldTypes/Dropdown.php b/packages/admin/src/Support/FieldTypes/Dropdown.php index 1685d0e15c..cbbb98d601 100644 --- a/packages/admin/src/Support/FieldTypes/Dropdown.php +++ b/packages/admin/src/Support/FieldTypes/Dropdown.php @@ -18,7 +18,8 @@ public static function getFilamentComponent(Attribute $attribute): Component collect($attribute->configuration->get('lookups'))->mapWithKeys( fn ($lookup) => [$lookup['value'] => $lookup['label'] ?? $lookup['value']] ) - ); + ) + ->helperText($attribute->translate('description')); } public static function getConfigurationFields(): array diff --git a/packages/admin/src/Support/FieldTypes/File.php b/packages/admin/src/Support/FieldTypes/File.php index aeba801d7c..dda293608b 100644 --- a/packages/admin/src/Support/FieldTypes/File.php +++ b/packages/admin/src/Support/FieldTypes/File.php @@ -13,6 +13,7 @@ class File extends BaseFieldType public static function getFilamentComponent(Attribute $attribute): Component { - return TextInput::make($attribute->handle); + return TextInput::make($attribute->handle) + ->helperText($attribute->translate('description')); } } diff --git a/packages/admin/src/Support/FieldTypes/ListField.php b/packages/admin/src/Support/FieldTypes/ListField.php index 8ae0992141..fd6b675981 100644 --- a/packages/admin/src/Support/FieldTypes/ListField.php +++ b/packages/admin/src/Support/FieldTypes/ListField.php @@ -15,6 +15,6 @@ public static function getFilamentComponent(Attribute $attribute): Component { return KeyValue::make($attribute->handle)->dehydrateStateUsing(function ($state) { return $state; - }); + })->helperText($attribute->translate('description')); } } diff --git a/packages/admin/src/Support/FieldTypes/TextField.php b/packages/admin/src/Support/FieldTypes/TextField.php index 4e76dea6ad..ef79a6be58 100644 --- a/packages/admin/src/Support/FieldTypes/TextField.php +++ b/packages/admin/src/Support/FieldTypes/TextField.php @@ -24,9 +24,11 @@ public static function getConfigurationFields(): array public static function getFilamentComponent(Attribute $attribute): Component { if ($attribute->configuration->get('richtext')) { - return RichEditor::make($attribute->handle); + return RichEditor::make($attribute->handle) + ->helperText($attribute->translate('description')); } - return TextInput::make($attribute->handle); + return TextInput::make($attribute->handle) + ->helperText($attribute->translate('description')); } } diff --git a/packages/admin/src/Support/FieldTypes/Toggle.php b/packages/admin/src/Support/FieldTypes/Toggle.php index 401a0b0a48..87316a407b 100644 --- a/packages/admin/src/Support/FieldTypes/Toggle.php +++ b/packages/admin/src/Support/FieldTypes/Toggle.php @@ -13,6 +13,10 @@ class Toggle extends BaseFieldType public static function getFilamentComponent(Attribute $attribute): Component { - return ToggleInput::make($attribute->handle)->default('true')->live(); + return ToggleInput::make($attribute->handle)->default('true') + ->helperText( + $attribute->translate('description') + ) + ->live(); } } diff --git a/packages/admin/src/Support/FieldTypes/TranslatedText.php b/packages/admin/src/Support/FieldTypes/TranslatedText.php index 3cb9bd67d3..5dc3465332 100644 --- a/packages/admin/src/Support/FieldTypes/TranslatedText.php +++ b/packages/admin/src/Support/FieldTypes/TranslatedText.php @@ -18,6 +18,7 @@ public static function getConfigurationFields(): array public static function getFilamentComponent(Attribute $attribute): Component { - return TranslatedTextInput::make($attribute->handle); + return TranslatedTextInput::make($attribute->handle) + ->helperText($attribute->translate('description')); } } diff --git a/packages/admin/src/Support/FieldTypes/YouTube.php b/packages/admin/src/Support/FieldTypes/YouTube.php index e25994a121..571f4d3789 100644 --- a/packages/admin/src/Support/FieldTypes/YouTube.php +++ b/packages/admin/src/Support/FieldTypes/YouTube.php @@ -16,7 +16,7 @@ public static function getFilamentComponent(Attribute $attribute): Component return YouTubeInput::make($attribute->handle) ->live(debounce: 200) ->helperText( - __('lunarpanel::components.forms.youtube.helperText') + $attribute->translate('description') ?? __('lunarpanel::components.forms.youtube.helperText') ); } } diff --git a/packages/core/database/factories/AttributeFactory.php b/packages/core/database/factories/AttributeFactory.php index a178a49a24..4c692617ed 100644 --- a/packages/core/database/factories/AttributeFactory.php +++ b/packages/core/database/factories/AttributeFactory.php @@ -23,6 +23,9 @@ public function definition(): array 'name' => [ 'en' => $this->faker->name(), ], + 'description' => [ + 'en' => Str::limit($this->faker->text(), 100), + ], 'handle' => Str::slug($this->faker->name()), 'section' => $this->faker->name(), 'type' => \Lunar\FieldTypes\Text::class, diff --git a/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php b/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php new file mode 100644 index 0000000000..be85c88ede --- /dev/null +++ b/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php @@ -0,0 +1,22 @@ +prefix.'attributes', function (Blueprint $table) { + $table->json('description')->after('name'); + }); + } + + public function down() + { + Schema::table($this->prefix.'attributes', function ($table) { + $table->dropColumn('description'); + }); + } +} diff --git a/packages/core/src/Models/Attribute.php b/packages/core/src/Models/Attribute.php index 1b0420eedf..4f322e2a1c 100644 --- a/packages/core/src/Models/Attribute.php +++ b/packages/core/src/Models/Attribute.php @@ -72,6 +72,7 @@ protected static function newFactory(): AttributeFactory */ protected $casts = [ 'name' => AsCollection::class, + 'description' => AsCollection::class, 'configuration' => AsCollection::class, ]; diff --git a/tests/admin/Feature/Filament/Resources/CollectionResources/Pages/ManageCollectionChildrenTest.php b/tests/admin/Feature/Filament/Resources/CollectionResources/Pages/ManageCollectionChildrenTest.php index 404a4444f8..20b5162deb 100644 --- a/tests/admin/Feature/Filament/Resources/CollectionResources/Pages/ManageCollectionChildrenTest.php +++ b/tests/admin/Feature/Filament/Resources/CollectionResources/Pages/ManageCollectionChildrenTest.php @@ -30,6 +30,9 @@ 'name' => [ 'en' => 'Name', ], + 'description' => [ + 'en' => 'Description', + ], 'handle' => 'name', 'type' => \Lunar\FieldTypes\TranslatedText::class, 'attribute_type' => \Lunar\Models\Collection::class, diff --git a/tests/admin/Feature/Filament/Resources/ProductResource/ListProductsTest.php b/tests/admin/Feature/Filament/Resources/ProductResource/ListProductsTest.php index c160ff7cfd..bc0ee2fc21 100644 --- a/tests/admin/Feature/Filament/Resources/ProductResource/ListProductsTest.php +++ b/tests/admin/Feature/Filament/Resources/ProductResource/ListProductsTest.php @@ -13,6 +13,9 @@ 'name' => [ 'en' => 'Name', ], + 'description' => [ + 'en' => 'Description', + ], ]); \Lunar\Models\TaxClass::factory()->create([ 'default' => true, diff --git a/tests/core/Unit/Models/AttributeTest.php b/tests/core/Unit/Models/AttributeTest.php index 783dee1b2c..f21d757d90 100644 --- a/tests/core/Unit/Models/AttributeTest.php +++ b/tests/core/Unit/Models/AttributeTest.php @@ -20,6 +20,9 @@ 'name' => [ 'en' => 'Meta Description', ], + 'description' => [ + 'en' => 'Meta Description', + ], 'handle' => 'meta_description', 'section' => 'product_variant', 'type' => \Lunar\FieldTypes\Text::class, From 5d45b61a5dca86061baa45ce9cb2a50880884fb7 Mon Sep 17 00:00:00 2001 From: glennjacobs Date: Wed, 31 Jan 2024 15:29:14 +0000 Subject: [PATCH 22/34] chore: fix code style --- packages/admin/resources/lang/en/attribute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/resources/lang/en/attribute.php b/packages/admin/resources/lang/en/attribute.php index 773a8e5cd5..7ca5d16abe 100644 --- a/packages/admin/resources/lang/en/attribute.php +++ b/packages/admin/resources/lang/en/attribute.php @@ -30,7 +30,7 @@ ], 'description' => [ 'label' => 'Description', - 'helper' => 'Use to display the helper text below the entry' + 'helper' => 'Use to display the helper text below the entry', ], 'handle' => [ 'label' => 'Handle', From 69a7683ed92f1c3af8aa63e3ddd3a48b12bc64ab Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 31 Jan 2024 16:30:38 +0100 Subject: [PATCH 23/34] Fix - Add limit with selectCurrentPageOnly (#1481) As Product and Orders --- packages/admin/src/Filament/Resources/CustomerResource.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/admin/src/Filament/Resources/CustomerResource.php b/packages/admin/src/Filament/Resources/CustomerResource.php index c9036fc822..0419f0ff6a 100644 --- a/packages/admin/src/Filament/Resources/CustomerResource.php +++ b/packages/admin/src/Filament/Resources/CustomerResource.php @@ -195,7 +195,8 @@ protected static function getDefaultTable(Table $table): Table Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), - ]); + ]) + ->selectCurrentPageOnly(); } public static function getDefaultRelations(): array From f7400b4fac7517c3c9b1eb789800ddaa2d658b4b Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 31 Jan 2024 16:41:20 +0100 Subject: [PATCH 24/34] Feature - Add new ViewPageExtension to authorize extend headers actions of ViewRecord (#1498) --- docs/admin/extending/pages.md | 30 +++++++++++++++++++ .../ActivityResource/Pages/ViewActivity.php | 4 +-- .../OrderResource/Pages/ManageOrder.php | 6 ++-- .../Support/Extending/ViewPageExtension.php | 11 +++++++ .../src/Support/Pages/BaseViewRecord.php | 11 +++++++ 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 packages/admin/src/Support/Extending/ViewPageExtension.php create mode 100644 packages/admin/src/Support/Pages/BaseViewRecord.php diff --git a/docs/admin/extending/pages.md b/docs/admin/extending/pages.md index d2ea5309ac..037fd84c37 100644 --- a/docs/admin/extending/pages.md +++ b/docs/admin/extending/pages.md @@ -238,6 +238,36 @@ class MyListExtension extends ListPageExtension LunarPanel::registerExtension(new MyListExtension, \Lunar\Admin\Filament\Resources\ProductResource\Pages\ListProducts::class); ``` +## ViewPageExtension + +An example of extending a view page. + +```php +use Filament\Actions; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Table; +use Lunar\Admin\Support\Extending\ViewPageExtension; + +class MyViewExtension extends ViewPageExtension +{ + public function headerActions(array $actions): array + { + $actions = [ + ...$actions, + Actions\ActionGroup::make([ + Actions\Action::make('Download PDF') + ]) + ]; + + return $actions; + } + +} + +// Typically placed in your AppServiceProvider file... +LunarPanel::registerExtension(new MyViewExtension, \Lunar\Admin\Filament\Resources\OrderResource\Pages\ManageOrder::class); +``` + ## Extending Pages In Addons If you are building an addon for Lunar, you may need to take a slightly different approach when modifying forms, etc. diff --git a/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php b/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php index 44cab11384..3be171d1a9 100644 --- a/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php +++ b/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php @@ -2,10 +2,10 @@ namespace Lunar\Admin\Filament\Resources\ActivityResource\Pages; -use Filament\Resources\Pages\ViewRecord; +use Lunar\Admin\Support\Pages\BaseViewRecord; use Lunar\Admin\Filament\Resources\ActivityResource; -class ViewActivity extends ViewRecord +class ViewActivity extends BaseViewRecord { protected static string $resource = ActivityResource::class; } diff --git a/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php b/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php index 6e9e5746b8..74c7473c2d 100644 --- a/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php +++ b/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php @@ -14,7 +14,6 @@ use Filament\Infolists\Components\TextEntry\TextEntrySize; use Filament\Infolists\Infolist; use Filament\Notifications\Notification; -use Filament\Resources\Pages\ViewRecord; use Filament\Support\Colors\Color; use Filament\Support\Enums\ActionSize; use Filament\Support\Enums\FontWeight; @@ -35,6 +34,7 @@ use Lunar\Admin\Support\Infolists\Components\Timeline; use Lunar\Admin\Support\Infolists\Components\Transaction as InfolistsTransaction; use Lunar\Admin\Support\OrderStatus; +use Lunar\Admin\Support\Pages\BaseViewRecord; use Lunar\DataTypes\Price; use Lunar\Models\Country; use Lunar\Models\State; @@ -55,7 +55,7 @@ * @property float $availableToRefund * @property bool $canBeRefunded */ -class ManageOrder extends ViewRecord +class ManageOrder extends BaseViewRecord { use CanDispatchActivityUpdated; @@ -655,7 +655,7 @@ public function getEditAddressAction(string $type): Action ->slideOver(); } - protected function getHeaderActions(): array + protected function getDefaultHeaderActions(): array { return [ $this->getCaptureAction(), diff --git a/packages/admin/src/Support/Extending/ViewPageExtension.php b/packages/admin/src/Support/Extending/ViewPageExtension.php new file mode 100644 index 0000000000..675ecacf54 --- /dev/null +++ b/packages/admin/src/Support/Extending/ViewPageExtension.php @@ -0,0 +1,11 @@ + Date: Wed, 31 Jan 2024 15:43:31 +0000 Subject: [PATCH 25/34] chore: fix code style --- .../Filament/Resources/ActivityResource/Pages/ViewActivity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php b/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php index 3be171d1a9..e0b7d61175 100644 --- a/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php +++ b/packages/admin/src/Filament/Resources/ActivityResource/Pages/ViewActivity.php @@ -2,8 +2,8 @@ namespace Lunar\Admin\Filament\Resources\ActivityResource\Pages; -use Lunar\Admin\Support\Pages\BaseViewRecord; use Lunar\Admin\Filament\Resources\ActivityResource; +use Lunar\Admin\Support\Pages\BaseViewRecord; class ViewActivity extends BaseViewRecord { From 87851598eb252d66802326673bb3e3f104e51f25 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 31 Jan 2024 16:49:04 +0100 Subject: [PATCH 26/34] Fix - Limit the list of sku with line break (#1508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture d’écran 2024-01-28 à 18 30 30 --------- Co-authored-by: Alec Ritson --- .../src/Filament/Resources/ProductResource.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/admin/src/Filament/Resources/ProductResource.php b/packages/admin/src/Filament/Resources/ProductResource.php index b6db375341..9c7369c9bf 100644 --- a/packages/admin/src/Filament/Resources/ProductResource.php +++ b/packages/admin/src/Filament/Resources/ProductResource.php @@ -248,7 +248,6 @@ protected static function getTableColumns(): array ->formatStateUsing(fn (Model $record): string => $record->translateAttribute('name')) ->limit(50) ->tooltip(function (Tables\Columns\TextColumn $column, Model $record): ?string { - $state = $column->getState(); if (strlen($record->translateAttribute('name')) <= $column->getCharacterLimit()) { return null; @@ -264,6 +263,22 @@ protected static function getTableColumns(): array ->searchable(), Tables\Columns\TextColumn::make('variants.sku') ->label(__('lunarpanel::product.table.sku.label')) + ->tooltip(function (Tables\Columns\TextColumn $column, Model $record): ?string { + + if ($record->variants->count() <= $column->getListLimit()) { + return null; + } + + if ($record->variants->count() > 30) { + $record->variants = $record->variants->slice(0, 30); + } + + return $record->variants + ->map(fn ($variant) => $variant->sku) + ->implode(', '); + }) + ->listWithLineBreaks() + ->limitList(1) ->toggleable(), Tables\Columns\TextColumn::make('variants_sum_stock') ->label(__('lunarpanel::product.table.stock.label')) From 487aa155bdf04453bb086184c81fd42c140853e7 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 31 Jan 2024 16:51:02 +0100 Subject: [PATCH 27/34] Feature - Change default Chart to area mode (#1510) I change the default chart configuration to area mode. it's easier to visualize the information on each chart --- .../Widgets/Dashboard/Orders/AverageOrderValueChart.php | 2 +- .../Filament/Widgets/Dashboard/Orders/OrderTotalsChart.php | 4 +--- .../Filament/Widgets/Dashboard/Orders/OrdersSalesChart.php | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/admin/src/Filament/Widgets/Dashboard/Orders/AverageOrderValueChart.php b/packages/admin/src/Filament/Widgets/Dashboard/Orders/AverageOrderValueChart.php index 648bdfc7cd..a663d213ac 100644 --- a/packages/admin/src/Filament/Widgets/Dashboard/Orders/AverageOrderValueChart.php +++ b/packages/admin/src/Filament/Widgets/Dashboard/Orders/AverageOrderValueChart.php @@ -115,7 +115,7 @@ protected function getOptions(): array return [ 'chart' => [ - 'type' => 'bar', + 'type' => 'area', 'toolbar' => [ 'show' => false, ], diff --git a/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrderTotalsChart.php b/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrderTotalsChart.php index fd815809f2..7393bd8978 100644 --- a/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrderTotalsChart.php +++ b/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrderTotalsChart.php @@ -47,11 +47,9 @@ protected function getOptions(): array $currentPeriod = $this->getTotalsForPeriod($from, $date); $previousPeriod = $this->getTotalsForPeriod($from->clone()->subYear(), $date->clone()->subYear()); - // dd($currentPeriod); - return [ 'chart' => [ - 'type' => 'bar', + 'type' => 'area', 'toolbar' => [ 'show' => false, ], diff --git a/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrdersSalesChart.php b/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrdersSalesChart.php index 0626ed1dd2..ec711e2883 100644 --- a/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrdersSalesChart.php +++ b/packages/admin/src/Filament/Widgets/Dashboard/Orders/OrdersSalesChart.php @@ -67,7 +67,7 @@ protected function getOptions(): array return [ 'chart' => [ - 'type' => 'bar', + 'type' => 'area', 'stacked' => false, 'toolbar' => [ 'show' => false, From ade14105d91efa5fd951225b77de10eff5417dfc Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Wed, 31 Jan 2024 23:57:22 +0800 Subject: [PATCH 28/34] fix migration for product option relation (#1520) Fix `SQLSTATE[42000]: Syntax error or access violation: 1055 'schema.lunar_products.id' isn't in GROUP BY ` `Syntax error or access violation: 1055 'schema.lunar_product_options.position' isn't in GROUP BY ` this happens when `config/databases.php` `mysql.strict => true` or `ONLY_FULL_GROUP_BY` mode enabled I verify the before and after by checking the count(*) and some eyeballing. --- .../2024_01_16_100010_update_product_option_relations.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php b/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php index 9598175c9f..502fb2760e 100644 --- a/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php +++ b/packages/core/database/migrations/2024_01_16_100010_update_product_option_relations.php @@ -40,8 +40,12 @@ public function up() "{$productsTable}.id as product_id", "{$optionsTable}.id as product_option_id", "{$optionsTable}.position", - ])->groupBy(['product_id', 'product_option_id']) - ->orderBy('product_id') + ])->groupBy([ + "{$productsTable}.id", + "{$optionsTable}.id", + "{$optionsTable}.position", + ]) + ->orderBy("{$productsTable}.id") ->chunk(200, function ($rows) { DB::table( $this->prefix.'product_product_option' From 5db295f39ce325adfc0d98f7890d1ce623b9c48d Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Thu, 1 Feb 2024 00:00:04 +0800 Subject: [PATCH 29/34] remove lazy load for page with only relation manager or widget (#1521) some pages are using widget & relation manager as the only section/component in the page, and with the lazy load enabled by default in Filament, this causes those pages to be empty on initial load and have slight flicker --- .../RelationManagers/CustomerGroupRelationManager.php | 2 ++ .../Resources/ProductResource/Widgets/ProductOptionsWidget.php | 2 ++ .../src/Support/RelationManagers/ChannelRelationManager.php | 2 ++ .../admin/src/Support/RelationManagers/MediaRelationManager.php | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/admin/src/Filament/Resources/ProductResource/RelationManagers/CustomerGroupRelationManager.php b/packages/admin/src/Filament/Resources/ProductResource/RelationManagers/CustomerGroupRelationManager.php index 77e71a254c..8c4ca12437 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/RelationManagers/CustomerGroupRelationManager.php +++ b/packages/admin/src/Filament/Resources/ProductResource/RelationManagers/CustomerGroupRelationManager.php @@ -10,6 +10,8 @@ class CustomerGroupRelationManager extends RelationManager { + protected static bool $isLazy = false; + protected static string $relationship = 'customerGroups'; public function isReadOnly(): bool diff --git a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php index cc5602dfb5..541b93204d 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php @@ -39,6 +39,8 @@ class ProductOptionsWidget extends BaseWidget implements HasActions, HasForms public bool $configuringOptions = false; + protected static bool $isLazy = false; + public function mount() { $this->configureBaseOptions(); diff --git a/packages/admin/src/Support/RelationManagers/ChannelRelationManager.php b/packages/admin/src/Support/RelationManagers/ChannelRelationManager.php index e346dbcaf4..cc0af2095a 100644 --- a/packages/admin/src/Support/RelationManagers/ChannelRelationManager.php +++ b/packages/admin/src/Support/RelationManagers/ChannelRelationManager.php @@ -11,6 +11,8 @@ class ChannelRelationManager extends RelationManager { + protected static bool $isLazy = false; + protected static string $relationship = 'channels'; public function isReadOnly(): bool diff --git a/packages/admin/src/Support/RelationManagers/MediaRelationManager.php b/packages/admin/src/Support/RelationManagers/MediaRelationManager.php index ea394cad36..4338e12ff2 100644 --- a/packages/admin/src/Support/RelationManagers/MediaRelationManager.php +++ b/packages/admin/src/Support/RelationManagers/MediaRelationManager.php @@ -15,6 +15,8 @@ class MediaRelationManager extends RelationManager { + protected static bool $isLazy = false; + protected static string $relationship = 'media'; public string $mediaCollection = 'default'; From bd5f5ec5b26653dc786b3061fb81e9d95538d25b Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Thu, 1 Feb 2024 09:09:34 +0100 Subject: [PATCH 30/34] Fix - Create a child collection (#1525) --- .../src/Support/Actions/Traits/CreatesChildCollections.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/admin/src/Support/Actions/Traits/CreatesChildCollections.php b/packages/admin/src/Support/Actions/Traits/CreatesChildCollections.php index 70d5faa5ff..9f302a47aa 100644 --- a/packages/admin/src/Support/Actions/Traits/CreatesChildCollections.php +++ b/packages/admin/src/Support/Actions/Traits/CreatesChildCollections.php @@ -26,12 +26,12 @@ public function createChildCollection(Collection $parent, string $name) ]); } - Collection::create([ + $parent->appendNode(Collection::create([ 'collection_group_id' => $parent->collection_group_id, 'attribute_data' => [ 'name' => new $fieldType($nameValue), ], - ], $parent); + ])); DB::commit(); } From a8dfd68f109dd711f1f5fa61abab66668026d0da Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Thu, 1 Feb 2024 10:04:18 +0100 Subject: [PATCH 31/34] Fix - Little change in the documentation (#1529) --- docs/admin/extending/resources.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/admin/extending/resources.md b/docs/admin/extending/resources.md index eaf712a4a6..38b76c80dd 100644 --- a/docs/admin/extending/resources.md +++ b/docs/admin/extending/resources.md @@ -31,12 +31,10 @@ class MyProductResourceExtension extends \Lunar\Panel\Support\Extending\Resource public function extendTable(\Filament\Tables\Table $table): \Filament\Tables\Table { - $actions = [ + return $table->columns([ ...$table->getColumns(), \Filament\Tables\Columns\TextColumn::make('product_code') - ]; - - return $table; + ]); } public function getRelations(array $managers) : array From 207851d9195040432190bcebf7b48e3c00cd6a4d Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Thu, 1 Feb 2024 10:20:49 +0000 Subject: [PATCH 32/34] Feat - Rename Tier to Quantity Break (#1526) --- docs/core/reference/pricing.md | 2 +- docs/core/reference/products.md | 24 +++---- docs/core/upgrading.md | 27 ++++++++ .../resources/lang/en/relationmanagers.php | 8 +-- .../ProductResource/Pages/ListProducts.php | 2 +- .../Pages/ManageProductPricing.php | 16 ++--- .../Pages/ManageVariantPricing.php | 16 ++--- ...tier_to_quantity_break_on_prices_table.php | 22 +++++++ .../DataTransferObjects/PricingResponse.php | 2 +- packages/core/src/Base/Traits/HasPrices.php | 6 +- packages/core/src/Managers/PricingManager.php | 16 ++--- packages/core/src/Models/Price.php | 2 +- .../resources/lang/en/relationmanagers.php | 8 +-- .../views/partials/ship-by-total.blade.php | 2 +- .../views/shipping-methods/ship-by.blade.php | 18 ++--- .../src/Drivers/ShippingMethods/ShipBy.php | 6 +- .../Pages/ManageShippingRates.php | 18 ++--- .../Pages/ManageProductPricingTest.php | 4 +- .../Carts/AddOrUpdatePurchasableTest.php | 7 +- .../Unit/Actions/Carts/CalculateLineTest.php | 11 ++-- .../Unit/Actions/Carts/CreateOrderTest.php | 5 +- .../Actions/Carts/GenerateFingerprintTest.php | 3 +- .../Actions/Carts/GetExistingCartLineTest.php | 5 +- .../core/Unit/Actions/Carts/MergeCartTest.php | 5 +- .../Actions/Carts/RemovePurchasableTest.php | 3 +- .../Unit/Actions/Carts/UpdateCartLineTest.php | 3 +- .../Collections/SortProductsByPriceTest.php | 3 +- tests/core/Unit/Base/ShippingManifestTest.php | 3 +- .../Unit/Base/Traits/CachesPropertiesTest.php | 3 +- .../core/Unit/DiscountTypes/AmountOffTest.php | 66 +++++++++---------- .../core/Unit/DiscountTypes/BuyXGetYTest.php | 27 ++++---- .../UpdateProductPositionsTest.php | 3 +- .../Unit/Managers/DiscountManagerTest.php | 2 +- .../core/Unit/Managers/PricingManagerTest.php | 52 +++++++-------- tests/core/Unit/Models/CartTest.php | 26 ++++---- tests/core/Unit/Models/PriceTest.php | 29 ++++---- tests/core/Unit/Models/ProductVariantTest.php | 21 +++--- .../Unit/Pipelines/Cart/ApplyShippingTest.php | 5 +- .../Pipelines/Cart/CalculateLinesTest.php | 3 +- .../Order/Creation/CleanUpOrderLinesTest.php | 5 +- .../Order/Creation/CreateOrderLinesTest.php | 3 +- .../Order/Creation/FillOrderFromCartTest.php | 3 +- tests/shipping/TestUtils.php | 2 +- .../Drivers/ShippingMethods/FlatRateTest.php | 2 +- .../Drivers/ShippingMethods/ShipByTest.php | 10 +-- .../Unit/Managers/ShippingManagerTest.php | 6 +- .../Resolvers/ShippingOptionResolverTest.php | 6 +- .../Resolvers/ShippingRateResolverTest.php | 26 ++++---- 48 files changed, 308 insertions(+), 239 deletions(-) create mode 100644 packages/core/database/migrations/2024_01_31_100000_update_tier_to_quantity_break_on_prices_table.php diff --git a/docs/core/reference/pricing.md b/docs/core/reference/pricing.md index d7e74d8564..0c7d20c871 100644 --- a/docs/core/reference/pricing.md +++ b/docs/core/reference/pricing.md @@ -55,7 +55,7 @@ them, first we'll create a standard price model. $priceModel = \Lunar\Models\Price::create([ // ... 'price' => 1000, // Price is an int and should be in the lowest common denominator - 'tier' => 1, + 'quantity_break' => 1, ]); // Lunar\DataTypes\Price diff --git a/docs/core/reference/products.md b/docs/core/reference/products.md index 727ead25a7..599d08deef 100644 --- a/docs/core/reference/products.md +++ b/docs/core/reference/products.md @@ -440,7 +440,7 @@ front end. | `price` | A integer value for the price | `null` | yes | | `compare_price` | For display purposes, allows you to show a comparison price, e.g. RRP. | `null` | no | | `currency_id` | The ID of the related currency | `null` | yes | -| `tier` | The lower limit to get this price, 1 is the default for base pricing. | `1` | no | +| `quantity_break` | The lower limit to get this price, 1 is the default for base pricing. | `1` | no | | `customer_group_id` | The customer group this price relates to, leaving as `null` means any customer group | `null` | no | | `priceable_type` | This is the class reference to the related model which owns the price | `null` | yes | | `priceable_id` | This is the id of the related model which owns the price | `null` | yes | @@ -450,7 +450,7 @@ $price = \Lunar\Models\Price::create([ 'price' => 199, 'compare_price' => 299, 'currency_id' => 1, - 'tier' => 1, + 'quantity_break' => 1, 'customer_group_id' => null, 'priceable_type' => 'Lunar\Models\ProductVariant', 'priceable_id' => 1, @@ -476,7 +476,7 @@ relationship method. 'price' => 199, 'compare_price' => 299, 'currency_id' => 1, - 'tier' => 1, + 'quantity_break' => 1, 'customer_group_id' => null, 'priceable_type' => 'Lunar\Models\ProductVariant', 'priceable_id' => 1, @@ -491,26 +491,26 @@ $variant->prices()->create([/* .. */]); You can specify which customer group the price applies to by setting the `customer_group_id` column. If left as `null` the price will apply to all customer groups. This is useful if you want to have different pricing for certain customer -groups and also different price tiers per customer group. +groups and also different price quantity breaks per customer group. -### Tiered Pricing +### Quantity Break Pricing -Tiered pricing is a concept in which when you buy in bulk, the cost per item will change (usually go down). With Pricing -on Lunar, this is determined by the `tier` column when creating prices. For example: +Quantity Break pricing is a concept in which when you buy in bulk, the cost per item will change (usually go down). With Pricing +on Lunar, this is determined by the `quantity_break` column when creating prices. For example: ```php Price::create([ // ... 'price' => 199, 'compare_price' => 399, - 'tier' => 1, + 'quantity_break' => 1, ]); Price::create([ // ... 'price' => 150, 'compare_price' => 399, - 'tier' => 10, + 'quantity_break' => 10, ]); ``` @@ -522,7 +522,7 @@ will pay `1.50` per item. Once you've got your pricing all set up, you're likely going to want to display it on your storefront. We've created a `PricingManager` which is available via a facade to make this process as painless as possible. -To get the pricing for a product you can simple use the following helpers: +To get the pricing for a product you can simply use the following helpers: #### Minimum example @@ -605,9 +605,9 @@ $pricing->matched; $pricing->base; /** - * A collection of all the price tiers available for the given criteria. + * A collection of all the price quantity breaks available for the given criteria. */ -$pricing->tiers; +$pricing->quantityBreaks; /** * All customer group pricing available for the given criteria. diff --git a/docs/core/upgrading.md b/docs/core/upgrading.md index c49d4d2165..b879138ebc 100644 --- a/docs/core/upgrading.md +++ b/docs/core/upgrading.md @@ -31,6 +31,33 @@ for further information. The `position` field has been removed from the `product_options` table and is now found on the `product_product_option` pivot table. Any position data will be automatically adjusted when running migrations. +#### Tiers renamed to Quantity Breaks + +The `tier` column on pricing has been renamed to `quantity_break`, any references in code to `tiers` needs to be updated. + +##### Price Model + +```php +// Old +$priceModel->tier +// New +$priceModel->quantity_break + +// Old +$priceModel->tiers +// New +$priceModel->quantityBreaks +``` + +##### Lunar\Base\DataTransferObjects\PricingResponse + +```php +// Old +public Collection $tiered, +// New +public Collection $quantityBreaks, +``` + ## 0.7 ### High Impact diff --git a/packages/admin/resources/lang/en/relationmanagers.php b/packages/admin/resources/lang/en/relationmanagers.php index 1a4a9244ab..3d332b713c 100644 --- a/packages/admin/resources/lang/en/relationmanagers.php +++ b/packages/admin/resources/lang/en/relationmanagers.php @@ -131,8 +131,8 @@ 'customer_group' => [ 'label' => 'Customer Group', ], - 'tier' => [ - 'label' => 'Tier', + 'quantity_break' => [ + 'label' => 'Quantity Break', ], 'currency' => [ 'label' => 'Currency', @@ -145,8 +145,8 @@ 'customer_group_id' => [ 'label' => 'Customer Group', ], - 'tier' => [ - 'label' => 'Tier', + 'quantity_break' => [ + 'label' => 'Quantity Break', ], 'currency_id' => [ 'label' => 'Currency', diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php index 68f323f304..3434203ecd 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php @@ -78,7 +78,7 @@ public static function createRecord(array $data, string $model): Model 'sku' => $data['sku'], ]); $variant->prices()->create([ - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'price' => (int) bcmul($data['base_price'], $currency->factor), ]); diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductPricing.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductPricing.php index a7e344cf37..5a674cefed 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductPricing.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ManageProductPricing.php @@ -59,15 +59,15 @@ public function form(Form $form): Form $owner = $this->getOwnerRecord(); return $rule->where('customer_group_id', $get('customer_group_id')) - ->where('tier', $get('tier')) + ->where('quantity_break', $get('quantity_break')) ->where('currency_id', $get('currency_id')) ->where('priceable_type', get_class($owner)) ->where('priceable_id', $owner->id); } )->required(), - Forms\Components\TextInput::make('tier') + Forms\Components\TextInput::make('quantity_break') ->label( - __('lunarpanel::relationmanagers.pricing.form.tier.label') + __('lunarpanel::relationmanagers.pricing.form.quantity_break.label') )->numeric()->minValue(1)->required(), Forms\Components\Select::make('currency_id') ->label( @@ -85,7 +85,7 @@ public function table(Table $table): Table return $table ->recordTitleAttribute('name') ->modifyQueryUsing( - fn ($query) => $query->orderBy('tier', 'asc') + fn ($query) => $query->orderBy('quantity_break', 'asc') ) ->columns([ Tables\Columns\TextColumn::make('price') @@ -97,8 +97,8 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('currency.code')->label( __('lunarpanel::relationmanagers.pricing.table.currency.label') ), - Tables\Columns\TextColumn::make('tier')->label( - __('lunarpanel::relationmanagers.pricing.table.tier.label') + Tables\Columns\TextColumn::make('quantity_break')->label( + __('lunarpanel::relationmanagers.pricing.table.quantity_break.label') ), Tables\Columns\TextColumn::make('customerGroup.name')->label( __('lunarpanel::relationmanagers.pricing.table.customer_group.label') @@ -108,11 +108,11 @@ public function table(Table $table): Table Tables\Filters\SelectFilter::make('currency') ->relationship(name: 'currency', titleAttribute: 'name') ->preload(), - Tables\Filters\SelectFilter::make('tier')->options( + Tables\Filters\SelectFilter::make('quantity_break')->options( Price::where('priceable_id', $this->getOwnerRecord()->id) ->where('priceable_type', get_class($this->getOwnerRecord())) ->get() - ->pluck('tier', 'tier') + ->pluck('quantity_break', 'quantity_break') ), ]) ->headerActions([ diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php index ec7ce02ff3..bbf6e3c205 100644 --- a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ManageVariantPricing.php @@ -79,15 +79,15 @@ public function form(Form $form): Form $owner = $this->getOwnerRecord(); return $rule->where('customer_group_id', $get('customer_group_id')) - ->where('tier', $get('tier')) + ->where('quantity_break', $get('quantity_break')) ->where('currency_id', $get('currency_id')) ->where('priceable_type', get_class($owner)) ->where('priceable_id', $owner->id); } )->required(), - Forms\Components\TextInput::make('tier') + Forms\Components\TextInput::make('quantity_break') ->label( - __('lunarpanel::relationmanagers.pricing.form.tier.label') + __('lunarpanel::relationmanagers.pricing.form.quantity_break.label') )->numeric()->minValue(1)->required(), Forms\Components\Select::make('currency_id') ->label( @@ -105,7 +105,7 @@ public function table(Table $table): Table return $table ->recordTitleAttribute('name') ->modifyQueryUsing( - fn ($query) => $query->orderBy('tier', 'asc') + fn ($query) => $query->orderBy('quantity_break', 'asc') ) ->columns([ Tables\Columns\TextColumn::make('price') @@ -117,8 +117,8 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('currency.code')->label( __('lunarpanel::relationmanagers.pricing.table.currency.label') ), - Tables\Columns\TextColumn::make('tier')->label( - __('lunarpanel::relationmanagers.pricing.table.tier.label') + Tables\Columns\TextColumn::make('quantity_break')->label( + __('lunarpanel::relationmanagers.pricing.table.quantity_break.label') ), Tables\Columns\TextColumn::make('customerGroup.name')->label( __('lunarpanel::relationmanagers.pricing.table.customer_group.label') @@ -128,11 +128,11 @@ public function table(Table $table): Table Tables\Filters\SelectFilter::make('currency') ->relationship(name: 'currency', titleAttribute: 'name') ->preload(), - Tables\Filters\SelectFilter::make('tier')->options( + Tables\Filters\SelectFilter::make('quantity_break')->options( Price::where('priceable_id', $this->getOwnerRecord()->id) ->where('priceable_type', get_class($this->getOwnerRecord())) ->get() - ->pluck('tier', 'tier') + ->pluck('quantity_break', 'quantity_break') ), ]) ->headerActions([ diff --git a/packages/core/database/migrations/2024_01_31_100000_update_tier_to_quantity_break_on_prices_table.php b/packages/core/database/migrations/2024_01_31_100000_update_tier_to_quantity_break_on_prices_table.php new file mode 100644 index 0000000000..43582f3963 --- /dev/null +++ b/packages/core/database/migrations/2024_01_31_100000_update_tier_to_quantity_break_on_prices_table.php @@ -0,0 +1,22 @@ +-prefix.'prices', function (Blueprint $table) { + $table->renameColumn('tier', 'quantity_break'); + }); + } + + public function down() + { + Schema::table($this->prefix.'prices', function (Blueprint $table) { + $table->renameColumn('quantity_break', 'tier'); + }); + } +} diff --git a/packages/core/src/Base/DataTransferObjects/PricingResponse.php b/packages/core/src/Base/DataTransferObjects/PricingResponse.php index 894fba842f..18e84a39e6 100644 --- a/packages/core/src/Base/DataTransferObjects/PricingResponse.php +++ b/packages/core/src/Base/DataTransferObjects/PricingResponse.php @@ -10,7 +10,7 @@ class PricingResponse public function __construct( public Price $matched, public Price $base, - public Collection $tiered, + public Collection $quantityBreaks, public Collection $customerGroupPrices, ) { // diff --git a/packages/core/src/Base/Traits/HasPrices.php b/packages/core/src/Base/Traits/HasPrices.php index 4f8fa16bae..a3f20963ce 100644 --- a/packages/core/src/Base/Traits/HasPrices.php +++ b/packages/core/src/Base/Traits/HasPrices.php @@ -25,12 +25,12 @@ public function prices(): MorphMany */ public function basePrices(): MorphMany { - return $this->prices()->whereTier(1)->whereNull('customer_group_id'); + return $this->prices()->whereQuantityBreak(1)->whereNull('customer_group_id'); } - public function tieredPrices(): MorphMany + public function quantityBreaks(): MorphMany { - return $this->prices()->where('tier', '>', 1); + return $this->prices()->where('quantity_break', '>', 1); } /** diff --git a/packages/core/src/Managers/PricingManager.php b/packages/core/src/Managers/PricingManager.php index 9bd1c2065f..a63e7d7282 100644 --- a/packages/core/src/Managers/PricingManager.php +++ b/packages/core/src/Managers/PricingManager.php @@ -195,25 +195,25 @@ public function get() })->sortBy('price'); // Get our base price - $basePrice = $prices->first(fn ($price) => $price->tier == 1 && ! $price->customer_group_id); + $basePrice = $prices->first(fn ($price) => $price->quantity_break == 1 && ! $price->customer_group_id); // To start, we'll set the matched price to the base price. $matched = $basePrice; // If we have customer group prices, we should find the cheapest one and send that back. $potentialGroupPrice = $prices->filter(function ($price) { - return (bool) $price->customer_group_id && $price->tier == 1; + return (bool) $price->customer_group_id && ($price->quantity_break == 1); })->sortBy('price'); $matched = $potentialGroupPrice->first() ?: $matched; - // Get all tiers that match for the given quantity. These take priority over the other steps + // Get all quantity breaks that match for the given quantity. These take priority over the other steps // as we could be bulk purchasing. - $tieredPricing = $prices->filter(function ($price) { - return $price->tier > 1 && $this->qty >= $price->tier; + $quantityBreaks = $prices->filter(function ($price) { + return $price->quantity_break > 1 && $this->qty >= $price->quantity_break; })->sortBy('price'); - $matched = $tieredPricing->first() ?: $matched; + $matched = $quantityBreaks->first() ?: $matched; if (! $matched) { throw new \ErrorException('No price set.'); @@ -221,8 +221,8 @@ public function get() $this->pricing = new PricingResponse( matched: $matched, - base: $prices->first(fn ($price) => $price->tier == 1), - tiered: $prices->filter(fn ($price) => $price->tier > 1), + base: $prices->first(fn ($price) => $price->quantity_break == 1), + quantityBreaks: $prices->filter(fn ($price) => $price->quantity_break > 1), customerGroupPrices: $prices->filter(fn ($price) => (bool) $price->customer_group_id) ); diff --git a/packages/core/src/Models/Price.php b/packages/core/src/Models/Price.php index 5554ea0e26..3807b77e3b 100644 --- a/packages/core/src/Models/Price.php +++ b/packages/core/src/Models/Price.php @@ -19,7 +19,7 @@ * @property int $priceable_id * @property int $price * @property ?int $compare_price - * @property int $tier + * @property int $quantity_break * @property ?\Illuminate\Support\Carbon $created_at * @property ?\Illuminate\Support\Carbon $updated_at */ diff --git a/packages/table-rate-shipping/resources/lang/en/relationmanagers.php b/packages/table-rate-shipping/resources/lang/en/relationmanagers.php index 76d2b1f942..f24eb5b3b3 100644 --- a/packages/table-rate-shipping/resources/lang/en/relationmanagers.php +++ b/packages/table-rate-shipping/resources/lang/en/relationmanagers.php @@ -20,7 +20,7 @@ 'label' => 'Price', ], 'prices' => [ - 'label' => 'Pricing Tiers', + 'label' => 'Quantity Breaks', 'repeater' => [ 'customer_group_id' => [ 'label' => 'Customer Group', @@ -29,7 +29,7 @@ 'currency_id' => [ 'label' => 'Currency', ], - 'tier' => [ + 'quantity_break' => [ 'label' => 'Min. Spend', ], 'price' => [ @@ -45,8 +45,8 @@ 'price' => [ 'label' => 'Price', ], - 'tiered_prices_count' => [ - 'label' => 'No. Tiers', + 'quantity_breaks_count' => [ + 'label' => 'Quantity Breaks', ], ], ], diff --git a/packages/table-rate-shipping/resources/views/partials/ship-by-total.blade.php b/packages/table-rate-shipping/resources/views/partials/ship-by-total.blade.php index 989b2e1c32..ec590d0a45 100644 --- a/packages/table-rate-shipping/resources/views/partials/ship-by-total.blade.php +++ b/packages/table-rate-shipping/resources/views/partials/ship-by-total.blade.php @@ -18,7 +18,7 @@
- Tiers
+ Quantity Breaks
diff --git a/packages/table-rate-shipping/resources/views/shipping-methods/ship-by.blade.php b/packages/table-rate-shipping/resources/views/shipping-methods/ship-by.blade.php index 3ca4d033cc..84d46ef218 100644 --- a/packages/table-rate-shipping/resources/views/shipping-methods/ship-by.blade.php +++ b/packages/table-rate-shipping/resources/views/shipping-methods/ship-by.blade.php @@ -50,7 +50,7 @@
- @if(count($tieredPrices)) + @if(count($quantityBreaks))
@if(!$this->currency->default) @@ -74,11 +74,11 @@
- @foreach($tieredPrices as $index => $tier) -
+ @foreach($quantityBreaks as $index => $quantityBreak) +
- + @foreach($this->customerGroups as $group) @@ -86,24 +86,24 @@ - +
- @foreach($errors->get('tieredPrices.'.$index.'*') as $error) + @foreach($errors->get('quantityBreaks.'.$index.'*') as $error) @foreach($error as $text)

{{ $text }}

@endforeach diff --git a/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php b/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php index 0aec735a14..db076d9af8 100644 --- a/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php +++ b/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php @@ -77,7 +77,7 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping // Do we have a suitable tier price? $pricing = Pricing::for($shippingRate)->customerGroups($customerGroups)->qty($tier)->get(); - $prices = $pricing->tiered; + $prices = $pricing->quantityBreaks; // If there are customer group prices, they need to take priority. if (! $pricing->customerGroupPrices->isEmpty()) { @@ -85,8 +85,8 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping } $matched = $prices->filter(function ($price) use ($tier) { - return $tier >= $price->tier; - })->sortByDesc('tier')->first() ?: $pricing->base; + return $tier >= $price->quantity_break; + })->sortByDesc('quantity_break')->first() ?: $pricing->base; if (! $matched) { return null; diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php index 92ab1a180f..8269a33380 100644 --- a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php @@ -97,9 +97,9 @@ function () { )->default( Currency::getDefault()->id )->required()->preload(), - Forms\Components\TextInput::make('tier') + Forms\Components\TextInput::make('quantity_break') ->label( - __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.tier.label') + __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.quantity_break.label') ) ->numeric() ->required(), @@ -113,12 +113,12 @@ function () { static function (Forms\Components\Repeater $component, Model $record = null): void { if ($record) { $component->state( - $record->tieredPrices->map(function ($price) { + $record->quantityBreaks->map(function ($price) { return [ 'customer_group_id' => $price->customer_group_id, 'price' => $price->price->decimal, 'currency_id' => $price->currency_id, - 'tier' => $price->tier / 100, + 'quantity_break' => $price->quantity_break / 100, ]; })->toArray() ); @@ -140,10 +140,10 @@ public function table(Table $table): Table )->label( __('lunarpanel.shipping::relationmanagers.shipping_rates.table.price.label') ), - TextColumn::make('tiered_prices_count') + TextColumn::make('quantity_breaks_count') ->label( - __('lunarpanel.shipping::relationmanagers.shipping_rates.table.tiered_prices_count.label') - )->counts('tieredPrices'), + __('lunarpanel.shipping::relationmanagers.shipping_rates.table.quantity_breaks_count.label') + )->counts('quantityBreaks'), ])->headerActions([ Tables\Actions\CreateAction::make()->label( __('lunarpanel.shipping::relationmanagers.shipping_rates.actions.create.label') @@ -177,11 +177,11 @@ protected static function saveShippingRate(ShippingRate $shippingRate = null, ar $basePrice->customer_group_id = null; $basePrice->save(); - $shippingRate->tieredPrices()->delete(); + $shippingRate->quantityBreaks()->delete(); $tiers = collect($data['prices'] ?? [])->map( function ($price) { - $price['tier'] = $price['tier'] * 100; + $price['quantity_break'] = $price['quantity_break'] * 100; return $price; } diff --git a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php index 8b35b9623a..f087c53e0c 100644 --- a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php +++ b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/ManageProductPricingTest.php @@ -99,12 +99,12 @@ ])->callTableAction('create', data: [ 'price' => 10.99, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ])->assertHasNoErrors(); $this->assertDatabaseHas((new \Lunar\Models\Price())->getTable(), [ 'price' => 1099, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'customer_group_id' => null, ]); diff --git a/tests/core/Unit/Actions/Carts/AddOrUpdatePurchasableTest.php b/tests/core/Unit/Actions/Carts/AddOrUpdatePurchasableTest.php index ac6807c01d..dad27ad3f2 100644 --- a/tests/core/Unit/Actions/Carts/AddOrUpdatePurchasableTest.php +++ b/tests/core/Unit/Actions/Carts/AddOrUpdatePurchasableTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -48,7 +49,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -74,7 +75,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Actions/Carts/CalculateLineTest.php b/tests/core/Unit/Actions/Carts/CalculateLineTest.php index a0c14d5f85..2850b3d6ea 100644 --- a/tests/core/Unit/Actions/Carts/CalculateLineTest.php +++ b/tests/core/Unit/Actions/Carts/CalculateLineTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, ]); @@ -107,7 +108,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -172,7 +173,7 @@ Price::factory()->create([ 'price' => 1000, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -237,7 +238,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -304,7 +305,7 @@ function check_for_know_rounding_error_on_unit_price_with_unit_quantity_of_one() Price::factory()->create([ 'price' => 912, //Known failing value 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, ]); diff --git a/tests/core/Unit/Actions/Carts/CreateOrderTest.php b/tests/core/Unit/Actions/Carts/CreateOrderTest.php index eae4cde2a1..9c5b02dad3 100644 --- a/tests/core/Unit/Actions/Carts/CreateOrderTest.php +++ b/tests/core/Unit/Actions/Carts/CreateOrderTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -290,7 +291,7 @@ function can_update_draft_order() Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Actions/Carts/GenerateFingerprintTest.php b/tests/core/Unit/Actions/Carts/GenerateFingerprintTest.php index 69f033a6bc..620acf03a5 100644 --- a/tests/core/Unit/Actions/Carts/GenerateFingerprintTest.php +++ b/tests/core/Unit/Actions/Carts/GenerateFingerprintTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($variant), 'priceable_id' => $variant->id, diff --git a/tests/core/Unit/Actions/Carts/GetExistingCartLineTest.php b/tests/core/Unit/Actions/Carts/GetExistingCartLineTest.php index dc24e98c06..07c86bb23d 100644 --- a/tests/core/Unit/Actions/Carts/GetExistingCartLineTest.php +++ b/tests/core/Unit/Actions/Carts/GetExistingCartLineTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -53,7 +54,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Actions/Carts/MergeCartTest.php b/tests/core/Unit/Actions/Carts/MergeCartTest.php index 82d12d10a0..8e33a24ddc 100644 --- a/tests/core/Unit/Actions/Carts/MergeCartTest.php +++ b/tests/core/Unit/Actions/Carts/MergeCartTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, ]); @@ -121,7 +122,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, ]); diff --git a/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php b/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php index e8a88c8051..53361a4140 100644 --- a/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php +++ b/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php b/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php index e414c1efca..b94237ba4a 100644 --- a/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php +++ b/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Actions/Collections/SortProductsByPriceTest.php b/tests/core/Unit/Actions/Collections/SortProductsByPriceTest.php index 01feed396a..666312157c 100644 --- a/tests/core/Unit/Actions/Collections/SortProductsByPriceTest.php +++ b/tests/core/Unit/Actions/Collections/SortProductsByPriceTest.php @@ -1,6 +1,7 @@ ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'price' => $prices[$index], ]); } diff --git a/tests/core/Unit/Base/ShippingManifestTest.php b/tests/core/Unit/Base/ShippingManifestTest.php index bafdc4a5b2..cc6001343e 100644 --- a/tests/core/Unit/Base/ShippingManifestTest.php +++ b/tests/core/Unit/Base/ShippingManifestTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Base/Traits/CachesPropertiesTest.php b/tests/core/Unit/Base/Traits/CachesPropertiesTest.php index 79ec4de087..77fb306e88 100644 --- a/tests/core/Unit/Base/Traits/CachesPropertiesTest.php +++ b/tests/core/Unit/Base/Traits/CachesPropertiesTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/DiscountTypes/AmountOffTest.php b/tests/core/Unit/DiscountTypes/AmountOffTest.php index 9186eebd6e..565a686a81 100644 --- a/tests/core/Unit/DiscountTypes/AmountOffTest.php +++ b/tests/core/Unit/DiscountTypes/AmountOffTest.php @@ -71,7 +71,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -85,7 +85,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -173,7 +173,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -187,7 +187,7 @@ Price::factory()->create([ 'price' => 2000, // £20 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -271,7 +271,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -285,7 +285,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -368,7 +368,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -382,7 +382,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -465,7 +465,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -479,7 +479,7 @@ Price::factory()->create([ 'price' => 2000, // £20 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -562,7 +562,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -576,7 +576,7 @@ Price::factory()->create([ 'price' => 2000, // £20 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -642,7 +642,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -707,7 +707,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -715,7 +715,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -723,7 +723,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableC), 'priceable_id' => $purchasableC->id, @@ -801,7 +801,7 @@ Price::factory()->create([ 'price' => 1000, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -879,7 +879,7 @@ Price::factory()->create([ 'price' => 1000, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -956,7 +956,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1018,7 +1018,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1080,7 +1080,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1143,7 +1143,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1205,7 +1205,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1273,7 +1273,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1335,7 +1335,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1420,7 +1420,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1498,7 +1498,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1570,7 +1570,7 @@ Price::factory()->create([ 'price' => 15, // £0.15 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -1578,7 +1578,7 @@ Price::factory()->create([ 'price' => 20, // £0.20 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -1586,7 +1586,7 @@ Price::factory()->create([ 'price' => 40, // £0.40 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableC), 'priceable_id' => $purchasableC->id, @@ -1594,7 +1594,7 @@ Price::factory()->create([ 'price' => 40, // £0.40 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableD), 'priceable_id' => $purchasableD->id, @@ -1602,7 +1602,7 @@ Price::factory()->create([ 'price' => 40, // £0.40 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableE), 'priceable_id' => $purchasableE->id, @@ -1702,7 +1702,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, diff --git a/tests/core/Unit/DiscountTypes/BuyXGetYTest.php b/tests/core/Unit/DiscountTypes/BuyXGetYTest.php index 50ad29953b..e887f34236 100644 --- a/tests/core/Unit/DiscountTypes/BuyXGetYTest.php +++ b/tests/core/Unit/DiscountTypes/BuyXGetYTest.php @@ -1,6 +1,7 @@ create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -159,7 +160,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -244,7 +245,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -252,7 +253,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -340,7 +341,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -348,7 +349,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -452,7 +453,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -460,7 +461,7 @@ Price::factory()->create([ 'price' => 500, // £5 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -587,7 +588,7 @@ Price::factory()->create([ 'price' => 1064, // $10.64 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -595,7 +596,7 @@ Price::factory()->create([ 'price' => 2280, // $22.80 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -724,7 +725,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, @@ -732,7 +733,7 @@ Price::factory()->create([ 'price' => 500, // £5 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, @@ -740,7 +741,7 @@ Price::factory()->create([ 'price' => 200, // £2 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableC), 'priceable_id' => $purchasableC->id, diff --git a/tests/core/Unit/Jobs/Collections/UpdateProductPositionsTest.php b/tests/core/Unit/Jobs/Collections/UpdateProductPositionsTest.php index 4856197961..74461f262a 100644 --- a/tests/core/Unit/Jobs/Collections/UpdateProductPositionsTest.php +++ b/tests/core/Unit/Jobs/Collections/UpdateProductPositionsTest.php @@ -1,6 +1,7 @@ ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'price' => $prices[$index], ]); } diff --git a/tests/core/Unit/Managers/DiscountManagerTest.php b/tests/core/Unit/Managers/DiscountManagerTest.php index 60b2ceb8ec..ad825cf688 100644 --- a/tests/core/Unit/Managers/DiscountManagerTest.php +++ b/tests/core/Unit/Managers/DiscountManagerTest.php @@ -298,7 +298,7 @@ Price::factory()->create([ 'price' => 1000, // £10 - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableA), 'priceable_id' => $purchasableA->id, diff --git a/tests/core/Unit/Managers/PricingManagerTest.php b/tests/core/Unit/Managers/PricingManagerTest.php index 19b214050e..cf5451778d 100644 --- a/tests/core/Unit/Managers/PricingManagerTest.php +++ b/tests/core/Unit/Managers/PricingManagerTest.php @@ -40,7 +40,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); Price::factory()->create([ @@ -48,7 +48,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 10, + 'quantity_break' => 10, ]); Price::factory()->create([ @@ -56,7 +56,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'customer_group_id' => CustomerGroup::factory(), ]); @@ -64,7 +64,7 @@ expect($pricing)->toBeInstanceOf(PricingResponse::class); expect($pricing->customerGroupPrices)->toHaveCount(0); - expect($pricing->tiered)->toHaveCount(1); + expect($pricing->quantityBreaks)->toHaveCount(1); expect($pricing->base->id)->toEqual($base->id); expect($pricing->matched->id)->toEqual($base->id); }); @@ -90,7 +90,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); $pricing = $manager->for($variant)->get(); @@ -123,7 +123,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); $customerGroupPrice = Price::factory()->create([ @@ -131,7 +131,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'customer_group_id' => $customerGroups->first()->id, ]); @@ -152,7 +152,7 @@ expect($pricing->matched->id)->toEqual($base->id); }); -test('can fetch tiered price', function () { +test('can fetch quantity break price', function () { $manager = new PricingManager(); $currency = Currency::factory()->create([ @@ -173,31 +173,31 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); - $tiered10 = Price::factory()->create([ + $break10 = Price::factory()->create([ 'price' => 90, 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 10, + 'quantity_break' => 10, ]); - $tiered20 = Price::factory()->create([ + $break20 = Price::factory()->create([ 'price' => 80, 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 20, + 'quantity_break' => 20, ]); - $tiered30 = Price::factory()->create([ + $break30 = Price::factory()->create([ 'price' => 70, 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 30, + 'quantity_break' => 30, ]); $pricing = $manager->qty(1)->for($variant)->get(); @@ -213,32 +213,32 @@ $pricing = $manager->qty(10)->for($variant)->get(); expect($pricing->base->id)->toEqual($base->id); - expect($pricing->matched->id)->toEqual($tiered10->id); + expect($pricing->matched->id)->toEqual($break10->id); $pricing = $manager->qty(15)->for($variant)->get(); expect($pricing->base->id)->toEqual($base->id); - expect($pricing->matched->id)->toEqual($tiered10->id); + expect($pricing->matched->id)->toEqual($break10->id); $pricing = $manager->qty(20)->for($variant)->get(); expect($pricing->base->id)->toEqual($base->id); - expect($pricing->matched->id)->toEqual($tiered20->id); + expect($pricing->matched->id)->toEqual($break20->id); $pricing = $manager->qty(25)->for($variant)->get(); expect($pricing->base->id)->toEqual($base->id); - expect($pricing->matched->id)->toEqual($tiered20->id); + expect($pricing->matched->id)->toEqual($break20->id); $pricing = $manager->qty(30)->for($variant)->get(); expect($pricing->base->id)->toEqual($base->id); - expect($pricing->matched->id)->toEqual($tiered30->id); + expect($pricing->matched->id)->toEqual($break30->id); $pricing = $manager->qty(100)->for($variant)->get(); expect($pricing->base->id)->toEqual($base->id); - expect($pricing->matched->id)->toEqual($tiered30->id); + expect($pricing->matched->id)->toEqual($break30->id); }); test('can match based on currency', function () { @@ -267,7 +267,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $defaultCurrency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); $additional = Price::factory()->create([ @@ -275,7 +275,7 @@ 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $secondCurrency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); $pricing = $manager->qty(1)->for($variant)->get(); @@ -318,7 +318,7 @@ function can_fetch_correct_price_for_user() 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $defaultCurrency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); $groupPrice = Price::factory()->create([ @@ -326,7 +326,7 @@ function can_fetch_correct_price_for_user() 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $defaultCurrency->id, - 'tier' => 1, + 'quantity_break' => 1, 'customer_group_id' => $group->id, ]); @@ -372,7 +372,7 @@ function can_fetch_correct_price_for_user() 'priceable_type' => ProductVariant::class, 'priceable_id' => $variant->id, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ]); $pricing = $manager->for($variant)->get(); diff --git a/tests/core/Unit/Models/CartTest.php b/tests/core/Unit/Models/CartTest.php index c76645566d..58ad5642c7 100644 --- a/tests/core/Unit/Models/CartTest.php +++ b/tests/core/Unit/Models/CartTest.php @@ -398,7 +398,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -419,7 +419,7 @@ Price::factory()->create([ 'price' => 158, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -468,7 +468,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -489,7 +489,7 @@ Price::factory()->create([ 'price' => 158, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -531,7 +531,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -555,7 +555,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -583,7 +583,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -607,7 +607,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -684,7 +684,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -773,7 +773,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($variant), 'priceable_id' => $variant->id, @@ -812,7 +812,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($variant), 'priceable_id' => $variant->id, @@ -871,7 +871,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -932,7 +932,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Models/PriceTest.php b/tests/core/Unit/Models/PriceTest.php index 9bd7567955..23aaeeed82 100644 --- a/tests/core/Unit/Models/PriceTest.php +++ b/tests/core/Unit/Models/PriceTest.php @@ -1,6 +1,7 @@ $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 123, - 'tier' => 1, + 'quantity_break' => 1, ]; Price::factory()->create($data); @@ -42,7 +43,7 @@ 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 123, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->price)->toBeInstanceOf(DataTypesPrice::class); @@ -63,7 +64,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 12.99, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->price->value)->toEqual(1299); @@ -80,7 +81,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 12.995, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->price->value)->toEqual(12995); @@ -92,7 +93,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 1299, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->price->value)->toEqual(1299); @@ -109,7 +110,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => '1,250.950', - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->price->value)->toEqual(1250950); @@ -121,7 +122,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => '1,250.955', - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->price->value)->toEqual(1250955); @@ -143,7 +144,7 @@ function can_handle_non_int_values() 'priceable_type' => ProductVariant::class, 'price' => 12.99, 'compare_price' => 13.99, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->compare_price)->toBeInstanceOf(DataTypesPrice::class); @@ -176,7 +177,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 123, - 'tier' => 1, + 'quantity_break' => 1, ]); Price::factory()->create([ @@ -184,7 +185,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 99, - 'tier' => 1, + 'quantity_break' => 1, ]); Price::factory()->create([ @@ -192,7 +193,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 101, - 'tier' => 5, + 'quantity_break' => 5, ]); Price::factory()->create([ @@ -201,7 +202,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 75, - 'tier' => 1, + 'quantity_break' => 1, ]); // Check we get the default currency price @@ -241,7 +242,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 999, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->priceExTax()->value)->toEqual(833); @@ -263,7 +264,7 @@ function can_handle_non_int_values() 'priceable_id' => $variant->id, 'priceable_type' => ProductVariant::class, 'price' => 833, - 'tier' => 1, + 'quantity_break' => 1, ]); expect($price->priceIncTax()->value)->toEqual(1000); diff --git a/tests/core/Unit/Models/ProductVariantTest.php b/tests/core/Unit/Models/ProductVariantTest.php index 25e36e3c36..2b4ccaca64 100644 --- a/tests/core/Unit/Models/ProductVariantTest.php +++ b/tests/core/Unit/Models/ProductVariantTest.php @@ -1,6 +1,7 @@ 100, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, ], [ 'price' => 90, 'currency_id' => $currency->id, 'customer_group_id' => $groupA->id, - 'tier' => 1, + 'quantity_break' => 1, ], [ 'price' => 80, 'currency_id' => $currency->id, 'customer_group_id' => $groupB->id, - 'tier' => 1, + 'quantity_break' => 1, ], [ 'price' => 30, 'currency_id' => $currency->id, 'customer_group_id' => $groupB->id, - 'tier' => 5, + 'quantity_break' => 5, ], [ 'price' => 60, 'currency_id' => $currency->id, - 'tier' => 5, + 'quantity_break' => 5, ], ]); @@ -118,12 +119,12 @@ [ 'price' => 100, 'currency_id' => $currencyA->id, - 'tier' => 1, + 'quantity_break' => 1, ], [ 'price' => 200, 'currency_id' => $currencyB->id, - 'tier' => 1, + 'quantity_break' => 1, ], ]); @@ -209,21 +210,21 @@ Price::factory()->create([ 'price' => 10000, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'priceable_type' => ProductVariant::class, 'priceable_id' => $genericProductVariant->id, ]); Price::factory()->create([ 'price' => 8000, 'currency_id' => $currency->id, - 'tier' => 10, + 'quantity_break' => 10, 'priceable_type' => ProductVariant::class, 'priceable_id' => $genericProductVariant->id, ]); Price::factory()->create([ 'price' => 400, 'currency_id' => $currency->id, - 'tier' => 1, + 'quantity_break' => 1, 'priceable_type' => ProductVariant::class, 'priceable_id' => $foodProductVariant->id, ]); diff --git a/tests/core/Unit/Pipelines/Cart/ApplyShippingTest.php b/tests/core/Unit/Pipelines/Cart/ApplyShippingTest.php index 6181832e4d..12a60bbd72 100644 --- a/tests/core/Unit/Pipelines/Cart/ApplyShippingTest.php +++ b/tests/core/Unit/Pipelines/Cart/ApplyShippingTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -107,7 +108,7 @@ Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Pipelines/Cart/CalculateLinesTest.php b/tests/core/Unit/Pipelines/Cart/CalculateLinesTest.php index 0c03648c3e..f8f2af2c89 100644 --- a/tests/core/Unit/Pipelines/Cart/CalculateLinesTest.php +++ b/tests/core/Unit/Pipelines/Cart/CalculateLinesTest.php @@ -1,6 +1,7 @@ create([ 'price' => $incomingUnitPrice, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php b/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php index fc215275a2..cbcb172e87 100644 --- a/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php +++ b/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, @@ -55,7 +56,7 @@ \Lunar\Models\Price::factory()->create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasableB), 'priceable_id' => $purchasableB->id, diff --git a/tests/core/Unit/Pipelines/Order/Creation/CreateOrderLinesTest.php b/tests/core/Unit/Pipelines/Order/Creation/CreateOrderLinesTest.php index 48b4a6d9cf..641fffc4fa 100644 --- a/tests/core/Unit/Pipelines/Order/Creation/CreateOrderLinesTest.php +++ b/tests/core/Unit/Pipelines/Order/Creation/CreateOrderLinesTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/core/Unit/Pipelines/Order/Creation/FillOrderFromCartTest.php b/tests/core/Unit/Pipelines/Order/Creation/FillOrderFromCartTest.php index e1fbd62b31..340dda9d96 100644 --- a/tests/core/Unit/Pipelines/Order/Creation/FillOrderFromCartTest.php +++ b/tests/core/Unit/Pipelines/Order/Creation/FillOrderFromCartTest.php @@ -1,6 +1,7 @@ create([ 'price' => 100, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/shipping/TestUtils.php b/tests/shipping/TestUtils.php index 716f4d6d26..bc20010335 100644 --- a/tests/shipping/TestUtils.php +++ b/tests/shipping/TestUtils.php @@ -26,7 +26,7 @@ public function createCart($currency = null, $price = 100, $quantity = 1) Price::factory()->create([ 'price' => $price, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php index d23748c133..2b8d3619b5 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php @@ -44,7 +44,7 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], ]); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php index abd16e9293..a4c9ebdadf 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php @@ -42,12 +42,12 @@ $shippingRate->prices()->createMany([ [ 'price' => 1000, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], ]); @@ -117,12 +117,12 @@ $shippingRate->prices()->createMany([ [ 'price' => 1000, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], ]); @@ -176,7 +176,7 @@ $shippingRate->prices()->createMany([ [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], ]); diff --git a/tests/shipping/Unit/Managers/ShippingManagerTest.php b/tests/shipping/Unit/Managers/ShippingManagerTest.php index 832533c965..a2e28e71c5 100644 --- a/tests/shipping/Unit/Managers/ShippingManagerTest.php +++ b/tests/shipping/Unit/Managers/ShippingManagerTest.php @@ -54,17 +54,17 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], [ 'price' => 0, - 'tier' => 800, + 'quantity_break' => 800, 'currency_id' => $currency->id, ], ]); diff --git a/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php index e929d36e8f..0f51e1de3a 100644 --- a/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php @@ -49,17 +49,17 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], [ 'price' => 0, - 'tier' => 800, + 'quantity_break' => 800, 'currency_id' => $currency->id, ], ]); diff --git a/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php index 87355fda3b..cc5d5329f5 100644 --- a/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php @@ -50,17 +50,17 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], [ 'price' => 0, - 'tier' => 800, + 'quantity_break' => 800, 'currency_id' => $currency->id, ], ]); @@ -137,17 +137,17 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], [ 'price' => 0, - 'tier' => 800, + 'quantity_break' => 800, 'currency_id' => $currency->id, ], ]); @@ -207,17 +207,17 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], [ 'price' => 0, - 'tier' => 800, + 'quantity_break' => 800, 'currency_id' => $currency->id, ], ]); @@ -279,17 +279,17 @@ $shippingRate->prices()->createMany([ [ 'price' => 600, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, ], [ 'price' => 500, - 'tier' => 700, + 'quantity_break' => 700, 'currency_id' => $currency->id, ], [ 'price' => 0, - 'tier' => 800, + 'quantity_break' => 800, 'currency_id' => $currency->id, ], ]); @@ -301,7 +301,7 @@ Price::factory()->create([ 'price' => 200, - 'tier' => 1, + 'quantity_break' => 1, 'currency_id' => $currency->id, 'priceable_type' => get_class($purchasable), 'priceable_id' => $purchasable->id, From 7ea8e820e6eab26cec27b942563ead3a364742fa Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Fri, 2 Feb 2024 12:37:04 +0100 Subject: [PATCH 33/34] Fix - Remove parallel option (#1533) --- .github/workflows/table_rate_shipping_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/table_rate_shipping_tests.yml b/.github/workflows/table_rate_shipping_tests.yml index dba54c38f1..2d80589c67 100644 --- a/.github/workflows/table_rate_shipping_tests.yml +++ b/.github/workflows/table_rate_shipping_tests.yml @@ -43,4 +43,4 @@ jobs: APP_ENV: testing DB_CONNECTION: testing DB_DATABASE: ":memory:" - run: vendor/bin/pest --testsuite shipping --parallel + run: vendor/bin/pest --testsuite shipping From 0b4d5eae7d27c82426229f69f996e4faaec63fa2 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:46:38 +0800 Subject: [PATCH 34/34] fix panel id (#1540) --- packages/admin/composer.json | 2 +- packages/admin/src/LunarPanelManager.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/admin/composer.json b/packages/admin/composer.json index b08e6b463a..10316f504d 100644 --- a/packages/admin/composer.json +++ b/packages/admin/composer.json @@ -15,7 +15,7 @@ "minimum-stability": "dev", "require": { "lunarphp/core": "self.version", - "filament/filament": "^3.1.23", + "filament/filament": "^3.2.25", "filament/spatie-laravel-media-library-plugin": "^3.0-stable", "spatie/laravel-permission": "^5.10", "barryvdh/laravel-dompdf": "^2.0", diff --git a/packages/admin/src/LunarPanelManager.php b/packages/admin/src/LunarPanelManager.php index 5e31915baf..bbf7592b80 100644 --- a/packages/admin/src/LunarPanelManager.php +++ b/packages/admin/src/LunarPanelManager.php @@ -86,8 +86,6 @@ public function register(): self $panel = $fn($panel); } - $panel->id($this->panelId); - Filament::registerPanel($panel); FilamentIcon::register([