From 159b9f0dded1413b97fd341152003ad1f726e1d7 Mon Sep 17 00:00:00 2001 From: Arthur Monney Date: Wed, 10 Apr 2024 10:43:13 +0200 Subject: [PATCH] Products - Add variants features (add, save, manage stock, delete) (#246) * Products - Add variants features (add, save, manage stock, delete) * fix code formatting --------- Co-authored-by: mckenziearts --- packages/admin/config/components/product.php | 10 +- packages/admin/public/shopper.css | 28 ++ packages/admin/resources/css/base.css | 4 + .../resources/lang/en/pages/products.php | 1 + packages/admin/resources/lang/fr/layout.php | 4 +- .../resources/lang/fr/pages/products.php | 1 + packages/admin/resources/lang/fr/words.php | 2 +- .../views/components/badge.blade.php | 4 +- .../components/buttons/default.blade.php | 4 +- .../components/form-slider-over.blade.php | 9 +- .../components/forms/avatar-upload.blade.php | 12 - .../forms/checkbox-category.blade.php | 25 -- .../forms/multiple-upload.blade.php | 128 --------- .../components/forms/single-upload.blade.php | 39 --- .../views/components/stock-badge.blade.php | 2 +- ...able-td.blade.php => table-cell.blade.php} | 0 .../products/forms/form-variants.blade.php | 133 ---------- .../products/forms/inventory.blade.php | 2 +- .../products/forms/related.blade.php | 8 +- .../components/products/forms/seo.blade.php | 8 +- .../products/forms/variants.blade.php | 14 + .../products/variant-stock.blade.php | 42 +++ .../settings/team/users-role.blade.php | 24 +- .../livewire/modals/add-variant.blade.php | 195 -------------- .../modals/update-variant-stock.blade.php | 160 ------------ .../livewire/pages/products/edit.blade.php | 3 +- .../livewire/pages/products/variant.blade.php | 50 +++- .../views/livewire/products/variant.blade.php | 246 ------------------ .../slide-overs/add-variant.blade.php | 7 + packages/admin/routes/admin/product.php | 5 +- .../Store/InitialQuantityInventory.php | 28 ++ .../Http/Controllers/DashboardController.php | 15 -- .../Ecommerce/ProductController.php | 49 ---- .../Components/Products/Form/Inventory.php | 25 +- .../Components/Products/Form/Media.php | 3 + .../Components/Products/Form/Variants.php | 170 ++++++++---- .../Livewire/Components/Products/Variant.php | 112 -------- .../Components/Products/VariantStock.php | 103 ++++++-- .../admin/src/Livewire/Modals/AddVariant.php | 111 -------- .../Livewire/Modals/UpdateVariantStock.php | 55 ---- .../src/Livewire/Pages/Product/Create.php | 18 +- .../admin/src/Livewire/Pages/Product/Edit.php | 5 + .../src/Livewire/Pages/Product/Index.php | 1 + .../src/Livewire/Pages/Product/Variant.php | 153 +++++++++++ .../Livewire/SlideOvers/AddVariantForm.php | 208 +++++++++++++++ 45 files changed, 792 insertions(+), 1434 deletions(-) delete mode 100755 packages/admin/resources/views/components/forms/avatar-upload.blade.php delete mode 100755 packages/admin/resources/views/components/forms/checkbox-category.blade.php delete mode 100755 packages/admin/resources/views/components/forms/multiple-upload.blade.php delete mode 100755 packages/admin/resources/views/components/forms/single-upload.blade.php rename packages/admin/resources/views/components/tables/{table-td.blade.php => table-cell.blade.php} (100%) delete mode 100755 packages/admin/resources/views/livewire/components/products/forms/form-variants.blade.php create mode 100755 packages/admin/resources/views/livewire/components/products/forms/variants.blade.php create mode 100644 packages/admin/resources/views/livewire/components/products/variant-stock.blade.php delete mode 100755 packages/admin/resources/views/livewire/modals/add-variant.blade.php delete mode 100755 packages/admin/resources/views/livewire/modals/update-variant-stock.blade.php delete mode 100755 packages/admin/resources/views/livewire/products/variant.blade.php create mode 100755 packages/admin/resources/views/livewire/slide-overs/add-variant.blade.php create mode 100644 packages/admin/src/Actions/Store/InitialQuantityInventory.php delete mode 100755 packages/admin/src/Http/Controllers/DashboardController.php delete mode 100755 packages/admin/src/Http/Controllers/Ecommerce/ProductController.php delete mode 100755 packages/admin/src/Livewire/Components/Products/Variant.php delete mode 100755 packages/admin/src/Livewire/Modals/AddVariant.php delete mode 100755 packages/admin/src/Livewire/Modals/UpdateVariantStock.php create mode 100755 packages/admin/src/Livewire/Pages/Product/Variant.php create mode 100755 packages/admin/src/Livewire/SlideOvers/AddVariantForm.php diff --git a/packages/admin/config/components/product.php b/packages/admin/config/components/product.php index 1d869eca4..239adeb8f 100644 --- a/packages/admin/config/components/product.php +++ b/packages/admin/config/components/product.php @@ -17,6 +17,7 @@ 'product-index' => Livewire\Pages\Product\Index::class, 'product-create' => Livewire\Pages\Product\Create::class, 'product-edit' => Livewire\Pages\Product\Edit::class, + 'variant-edit' => Livewire\Pages\Product\Variant::class, ], /* @@ -32,22 +33,21 @@ 'attributes.values' => Components\Attributes\Values::class, 'products.form.attributes' => Components\Products\Form\Attributes::class, - 'products.form.edit' => Components\Products\Form\Edit::class, // Done - 'products.form.media' => Components\Products\Form\Media::class, // Done + 'products.form.edit' => Components\Products\Form\Edit::class, + 'products.form.media' => Components\Products\Form\Media::class, 'products.form.inventory' => Components\Products\Form\Inventory::class, 'products.form.related-products' => Components\Products\Form\RelatedProducts::class, 'products.form.seo' => Components\Products\Form\Seo::class, 'products.form.shipping' => Components\Products\Form\Shipping::class, 'products.form.variants' => Components\Products\Form\Variants::class, - 'products.variant' => Components\Products\Variant::class, 'products.variant-stock' => Components\Products\VariantStock::class, - 'modals.add-variant' => Livewire\Modals\AddVariant::class, 'modals.create-value' => Livewire\Modals\CreateValue::class, 'modals.products-lists' => Livewire\Modals\ProductsLists::class, 'modals.related-products-list' => Livewire\Modals\RelatedProductsList::class, 'modals.update-value' => Livewire\Modals\UpdateValue::class, - 'modals.update-variant-stock' => Livewire\Modals\UpdateVariantStock::class, + + 'slide-overs.add-variant' => Livewire\SlideOvers\AddVariantForm::class, ], ]; diff --git a/packages/admin/public/shopper.css b/packages/admin/public/shopper.css index 77a5cbdf9..1a92bcfda 100755 --- a/packages/admin/public/shopper.css +++ b/packages/admin/public/shopper.css @@ -4071,6 +4071,10 @@ html { display: table; } +.table-cell { + display: table-cell; +} + .table-row { display: table-row; } @@ -5782,6 +5786,16 @@ html { background-color: rgb(234 179 8 / var(--tw-bg-opacity)); } +.bg-success-100 { + --tw-bg-opacity: 1; + background-color: rgb(209 250 229 / var(--tw-bg-opacity)); +} + +.bg-warning-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); +} + .bg-opacity-10 { --tw-bg-opacity: 0.1; } @@ -6639,6 +6653,16 @@ html { color: rgb(133 77 14 / var(--tw-text-opacity)); } +.text-success-800 { + --tw-text-opacity: 1; + color: rgb(6 95 70 / var(--tw-text-opacity)); +} + +.text-warning-800 { + --tw-text-opacity: 1; + color: rgb(146 64 14 / var(--tw-text-opacity)); +} + .underline { text-decoration-line: underline; } @@ -7025,6 +7049,10 @@ input[type='number'] { flex: 1 1 0%; } +.sh-input-media .filepond--list-scroller { + z-index: 100; +} + .sidebar > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse))); diff --git a/packages/admin/resources/css/base.css b/packages/admin/resources/css/base.css index 979dffba4..cb0ea500c 100755 --- a/packages/admin/resources/css/base.css +++ b/packages/admin/resources/css/base.css @@ -61,3 +61,7 @@ input[type='number'] { .shopper-product-wizard > div { @apply flex-1 h-full; } + +.sh-input-media .filepond--list-scroller { + z-index: 100; +} diff --git a/packages/admin/resources/lang/en/pages/products.php b/packages/admin/resources/lang/en/pages/products.php index be4574e48..d6bbf51d1 100755 --- a/packages/admin/resources/lang/en/pages/products.php +++ b/packages/admin/resources/lang/en/pages/products.php @@ -165,5 +165,6 @@ ], 'images_helpText' => 'Add images to your product.', + 'variant_images_helpText' => 'Add images to your variant.', 'thumbnail_helpText' => 'Used to represent your product during checkout, social sharing and more.', ]; diff --git a/packages/admin/resources/lang/fr/layout.php b/packages/admin/resources/lang/fr/layout.php index 56f2804e8..da3a77ce4 100755 --- a/packages/admin/resources/lang/fr/layout.php +++ b/packages/admin/resources/lang/fr/layout.php @@ -60,8 +60,8 @@ 'invisible' => 'Invisible', 'search' => 'Recherche', 'price' => 'Prix', - 'price_amount' => 'Montant du prix', - 'compare_price' => 'Comparer au prix', + 'price_amount' => 'Prix', + 'compare_price' => 'Prix à comparer', 'cost_per_item' => 'Coût par pièce', 'sku' => 'SKU (Stock Keeping Unit)', 'barcode' => 'Barcode (ISBN, UPC, GTIN, etc.)', diff --git a/packages/admin/resources/lang/fr/pages/products.php b/packages/admin/resources/lang/fr/pages/products.php index aa1a96304..ed7fee1e0 100755 --- a/packages/admin/resources/lang/fr/pages/products.php +++ b/packages/admin/resources/lang/fr/pages/products.php @@ -165,6 +165,7 @@ ], 'images_helpText' => 'Ajouter des images à votre produit.', + 'variant_images_helpText' => 'Ajouter des images à votre variante.', 'thumbnail_helpText' => 'Utilisé pour représenter votre produit lors du paiement, du partage social, et plus encore.', ]; diff --git a/packages/admin/resources/lang/fr/words.php b/packages/admin/resources/lang/fr/words.php index dec3a9fe1..9bd4fb119 100755 --- a/packages/admin/resources/lang/fr/words.php +++ b/packages/admin/resources/lang/fr/words.php @@ -67,7 +67,7 @@ ], 'variants' => 'Variantes', - 'variant' => 'Variant', + 'variant' => 'Variante', 'overview' => 'Aperçu', 'seo' => 'SEO', 'in_stock' => 'en stock', diff --git a/packages/admin/resources/views/components/badge.blade.php b/packages/admin/resources/views/components/badge.blade.php index a51292940..fe86b6c42 100755 --- a/packages/admin/resources/views/components/badge.blade.php +++ b/packages/admin/resources/views/components/badge.blade.php @@ -3,9 +3,9 @@ @php $style = match($style ?? 'gray') { 'primary' => 'bg-primary-100 text-primary-800', - 'orange' => 'bg-orange-100 text-orange-800', + 'warning' => 'bg-warning-100 text-warning-800', 'danger' => 'bg-danger-100 text-danger-800', - 'success' => 'bg-green-100 text-green-800', + 'success' => 'bg-success-100 text-success-800', default => 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white', }; @endphp diff --git a/packages/admin/resources/views/components/buttons/default.blade.php b/packages/admin/resources/views/components/buttons/default.blade.php index 54580c308..1dc17b924 100755 --- a/packages/admin/resources/views/components/buttons/default.blade.php +++ b/packages/admin/resources/views/components/buttons/default.blade.php @@ -1,10 +1,10 @@ @isset($link) - twMerge(['class' => 'inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-700 shadow-sm text-sm font-medium rounded-lg text-gray-700 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:focus:ring-offset-gray-900']) }} > {{ $slot }} - + @else + @if($description) +
+

+ {{ $description }} +

+
+ @endif
{{ $slot }} diff --git a/packages/admin/resources/views/components/forms/avatar-upload.blade.php b/packages/admin/resources/views/components/forms/avatar-upload.blade.php deleted file mode 100755 index e5bf26f4b..000000000 --- a/packages/admin/resources/views/components/forms/avatar-upload.blade.php +++ /dev/null @@ -1,12 +0,0 @@ -
- {{ $slot }} - -
- - - - -
-
diff --git a/packages/admin/resources/views/components/forms/checkbox-category.blade.php b/packages/admin/resources/views/components/forms/checkbox-category.blade.php deleted file mode 100755 index e84c3b975..000000000 --- a/packages/admin/resources/views/components/forms/checkbox-category.blade.php +++ /dev/null @@ -1,25 +0,0 @@ -
-
- -
-
- -
-
- -@if($category->children->isNotEmpty()) -
- @foreach($category->children as $child) - @include('shopper::components.forms.checkbox-category', [ - 'parent' => $category->parent_id, - 'category' => $child, - 'statePath' => $statePath, - ]) - @endforeach -
-@endif diff --git a/packages/admin/resources/views/components/forms/multiple-upload.blade.php b/packages/admin/resources/views/components/forms/multiple-upload.blade.php deleted file mode 100755 index 47c1be285..000000000 --- a/packages/admin/resources/views/components/forms/multiple-upload.blade.php +++ /dev/null @@ -1,128 +0,0 @@ -@props(['files' => [], 'images' => []]) - -
- @if(collect($files)->isNotEmpty()) -
    - @foreach($files as $file) -
  • $loop->first, - 'col-span-1 h-20' => !$loop->first, - ]) - > - -
    - -
    -
  • - @endforeach - - @if(count($files) <= 8) -
  • -
    -
    - - - -
    - -
    -
  • - @endif -
- @else -
-
- - - -

- - {{ __('shopper::words.components.files.upload_file') }} - - {{ __('shopper::components.files.drag_n_drop') }} -

-

- {{ __('shopper::components.files.file_type_size', ['size' => 1]) }} -

-
- -
- @endif - - @if(collect($images)->isNotEmpty()) -
- @foreach($images as $image) -
- - -
- {{ $image->file_name }} - {{ $image->human_readable_size }} -
-
- -
-
- @endforeach -
- @endif - - -
- -@push('scripts') - -@endpush diff --git a/packages/admin/resources/views/components/forms/single-upload.blade.php b/packages/admin/resources/views/components/forms/single-upload.blade.php deleted file mode 100755 index 007a3546d..000000000 --- a/packages/admin/resources/views/components/forms/single-upload.blade.php +++ /dev/null @@ -1,39 +0,0 @@ -@props(['file' => false, 'error' => false]) - -
- @if($file) -
- -
- -
-
- @else - - @endif - @if($error) -

{{ $error }}

- @endif -
diff --git a/packages/admin/resources/views/components/stock-badge.blade.php b/packages/admin/resources/views/components/stock-badge.blade.php index 3aacb9909..dce79990a 100755 --- a/packages/admin/resources/views/components/stock-badge.blade.php +++ b/packages/admin/resources/views/components/stock-badge.blade.php @@ -3,7 +3,7 @@ $stock < 10, - 'bg-green-100 text-green-800' => $stock >= 10, + 'bg-success-100 text-success-800' => $stock >= 10, ])> {{ $stock }} diff --git a/packages/admin/resources/views/components/tables/table-td.blade.php b/packages/admin/resources/views/components/tables/table-cell.blade.php similarity index 100% rename from packages/admin/resources/views/components/tables/table-td.blade.php rename to packages/admin/resources/views/components/tables/table-cell.blade.php diff --git a/packages/admin/resources/views/livewire/components/products/forms/form-variants.blade.php b/packages/admin/resources/views/livewire/components/products/forms/form-variants.blade.php deleted file mode 100755 index 81afe0f36..000000000 --- a/packages/admin/resources/views/livewire/components/products/forms/form-variants.blade.php +++ /dev/null @@ -1,133 +0,0 @@ - -
-

- {{ __('shopper::pages/products.variants.title') }} -

-

- {{ __('shopper::pages/products.variants.description') }} -

-
- -
-
-
- -
- - - {{ __('shopper::pages/products.variants.add') }} - - -
-
- -
- - - - - - - - - - - - @forelse($variants as $variant) - - - - - - - - @empty - - - - @endforelse - -
- {{ __('shopper::words.variant') }} - - {{ __('shopper::words.price') }} -
-
-
-
- - - {{ $variant->name }} - -
-
-
- @if($variant->price_amount) {{ $variant->getPriceAmount()->formatted }} @else — @endif - - - - - - - - -
-
- -
-
-
-
-
- - - {{ __('shopper::pages/products.variants.empty') }} - -
-
-
-
-
- {{ $variants->links('shopper::livewire.wire-mobile-pagination-links') }} -
- -
-
-
-
- -
diff --git a/packages/admin/resources/views/livewire/components/products/forms/inventory.blade.php b/packages/admin/resources/views/livewire/components/products/forms/inventory.blade.php index f10b00ea8..6f590677b 100755 --- a/packages/admin/resources/views/livewire/components/products/forms/inventory.blade.php +++ b/packages/admin/resources/views/livewire/components/products/forms/inventory.blade.php @@ -1,5 +1,5 @@ - + {{ $this->form }}
diff --git a/packages/admin/resources/views/livewire/components/products/forms/related.blade.php b/packages/admin/resources/views/livewire/components/products/forms/related.blade.php index f9c6b2e5f..a6299cf93 100755 --- a/packages/admin/resources/views/livewire/components/products/forms/related.blade.php +++ b/packages/admin/resources/views/livewire/components/products/forms/related.blade.php @@ -1,12 +1,12 @@
-

+ {{ __('shopper::pages/products.related.title') }} -

-

+ + {{ __('shopper::pages/products.related.description') }} -

+
@if($relatedProducts->isNotEmpty())
diff --git a/packages/admin/resources/views/livewire/components/products/forms/seo.blade.php b/packages/admin/resources/views/livewire/components/products/forms/seo.blade.php index b9a211f25..506235c35 100755 --- a/packages/admin/resources/views/livewire/components/products/forms/seo.blade.php +++ b/packages/admin/resources/views/livewire/components/products/forms/seo.blade.php @@ -1,11 +1,11 @@
-

+ {{ __('shopper::pages/products.seo.title') }} -

-

+ + {{ __('shopper::pages/products.seo.description') }} -

+
diff --git a/packages/admin/resources/views/livewire/components/products/forms/variants.blade.php b/packages/admin/resources/views/livewire/components/products/forms/variants.blade.php new file mode 100755 index 000000000..c95e34ac0 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/products/forms/variants.blade.php @@ -0,0 +1,14 @@ + +
+ + {{ __('shopper::pages/products.variants.title') }} + + + {{ __('shopper::pages/products.variants.description') }} + +
+ +
+ {{ $this->table }} +
+
diff --git a/packages/admin/resources/views/livewire/components/products/variant-stock.blade.php b/packages/admin/resources/views/livewire/components/products/variant-stock.blade.php new file mode 100644 index 000000000..fdaeefe21 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/products/variant-stock.blade.php @@ -0,0 +1,42 @@ +
+
+

+ {{ __('shopper::pages/products.quantity_inventory') }} +

+
+ {{ $this->stockAction }} + + +
+
+ +
+ + + + {{ __('shopper::pages/products.inventory_name') }} + + + {{ __('shopper::words.available') }} + + + + @foreach($inventories as $inventory) + + +
+ {{ $inventory->name }} + @if($inventory->is_default) + + @endif +
+
+ + {{ $variant->stockInventory($inventory->id) }} + + + @endforeach + +
+
+
diff --git a/packages/admin/resources/views/livewire/components/settings/team/users-role.blade.php b/packages/admin/resources/views/livewire/components/settings/team/users-role.blade.php index b907f231c..015583f23 100755 --- a/packages/admin/resources/views/livewire/components/settings/team/users-role.blade.php +++ b/packages/admin/resources/views/livewire/components/settings/team/users-role.blade.php @@ -30,7 +30,7 @@ @forelse($users as $user) - +
User avatar @@ -44,8 +44,8 @@
- - + +
@if($user->email_verified_at) @@ -54,16 +54,16 @@ @endif {{ $user->email }}
- - +
+ + + @if($user->id === auth()->id()) @@ -88,11 +88,11 @@ @endif - + @empty - +
- +
@endforelse diff --git a/packages/admin/resources/views/livewire/modals/add-variant.blade.php b/packages/admin/resources/views/livewire/modals/add-variant.blade.php deleted file mode 100755 index f075034d7..000000000 --- a/packages/admin/resources/views/livewire/modals/add-variant.blade.php +++ /dev/null @@ -1,195 +0,0 @@ - - -
-
-
-
-

- {{ __('shopper::pages/products.variants.modal.title') }} -

-

- {{ __('shopper::pages/products.variants.modal.description') }} -

-
-
-
- - - -
-
- -
-
- -
-
-
-
- -
-
-
- -
- - -
- -
-
-
-
-
-

- {{ __('shopper::words.location') }} -

-
-
-
- - - -
-
- - - -
-
-
- @if($inventories->count() <= 1) -
- - - -
- @endif -
- - - -
-
- @if($inventories->count() > 1) -
-
-

- {{ __('shopper::pages/products.quantity_inventory') }} -

- - {{ __('shopper::pages/products.manage_inventories') }} - -
-
-
-
- - {{ __('shopper::pages/products.inventory_name') }} - -
-
- - {{ __('shopper::words.available') }} - -
-
- @foreach($inventories as $inventory) -
-
- - {{ $inventory->name }} - -
-
- -
-
- @endforeach -
-
- @endif -
-
-
-
-
- - - - - - {{ __('shopper::layout.forms.actions.save') }} - - - - - {{ __('shopper::layout.forms.actions.cancel') }} - - - -
diff --git a/packages/admin/resources/views/livewire/modals/update-variant-stock.blade.php b/packages/admin/resources/views/livewire/modals/update-variant-stock.blade.php deleted file mode 100755 index f631e310e..000000000 --- a/packages/admin/resources/views/livewire/modals/update-variant-stock.blade.php +++ /dev/null @@ -1,160 +0,0 @@ - - - - {{ __('shopper::pages/products.modals.variants.title') }} - - - -
-
- @if($inventories->isNotEmpty()) -
- - - - - - - @foreach($inventories as $inventory) - - @endforeach - - -
- - -
-
- @endif - -
-
- - {{ __('shopper::pages/products.current_qty_inventory') }} - - -
-
-
-
-

- {{ $realStock }} -

-
-
- -
- - -
-
-
- - - {{ __('shopper::layout.forms.actions.update') }} - - @if($histories->isNotEmpty()) -
- - - {{ __('shopper::layout.forms.actions.export') }} - -
- @endif -
-
- @error('value') -

- {{ __('shopper::words.validation.integer') }} -

- @enderror -
-
- - @if($histories->total() === 0) -
- - - -

- {{ __('shopper::pages/products.inventory.empty') }} -

-
- @else -
-
-
- - - - {{ __('shopper::words.date') }} - {{ __('shopper::words.event') }} - {{ __('shopper::words.adjustment') }} - {{ __('shopper::pages/products.inventory.movement') }} - - - - @foreach($histories as $inventoryHistory) - - - - - - - @endforeach - -
- {{ $inventoryHistory->created_at->diffForHumans() }} - - {{ __($inventoryHistory->event) }} - - {{ $inventoryHistory->adjustment }} - - {{ $inventoryHistory->quantity }} -
-
-
-
-
- {{ $histories->links('shopper::livewire.wire-mobile-pagination-links') }} -
- -
-
- @endif -
-
-
- - - - - {{ __('shopper::layout.forms.actions.close') }} - - - - -
diff --git a/packages/admin/resources/views/livewire/pages/products/edit.blade.php b/packages/admin/resources/views/livewire/pages/products/edit.blade.php index b5410eafb..a6ddaafa6 100755 --- a/packages/admin/resources/views/livewire/pages/products/edit.blade.php +++ b/packages/admin/resources/views/livewire/pages/products/edit.blade.php @@ -102,8 +102,7 @@ class="pt-6 pb-10"
- Variants - {{----}} +
Attributes diff --git a/packages/admin/resources/views/livewire/pages/products/variant.blade.php b/packages/admin/resources/views/livewire/pages/products/variant.blade.php index 43e4d8362..e50b748e3 100755 --- a/packages/admin/resources/views/livewire/pages/products/variant.blade.php +++ b/packages/admin/resources/views/livewire/pages/products/variant.blade.php @@ -1,3 +1,47 @@ - - - + + + + +
+
+
+

+ {{ $variant->name }} +

+ +
+
+
+ + +
+ {{ $this->form }} +
+ + + {{ __('shopper::pages/products.variants.actions.update') }} + +
+
+ + +
diff --git a/packages/admin/resources/views/livewire/products/variant.blade.php b/packages/admin/resources/views/livewire/products/variant.blade.php deleted file mode 100755 index da904cb15..000000000 --- a/packages/admin/resources/views/livewire/products/variant.blade.php +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - {{ $name }} - - - -
-
-
-

- {{ $variant->name }} -

- $product->is_visible, - 'bg-yellow-100 text-yellow-800' => ! $product->is_visible, - ])> - {{ $product->is_visible ? __('shopper::layout.forms.label.visible'): __('shopper::layout.forms.label.invisible') }} - -
- -
-
- -
-
-
-

- {{ __('shopper::pages/products.variants.variant_information') }} -

-
-
-
- - - -
-

- {{ __('shopper::words.images') }} -

-
- - @error('files.*') -

{{ $message }}

- @enderror -
-
-
-
-
-
- - - -
-
-
-

- {{ __('shopper::words.pricing') }} -

-
-
-
-
- -
-
- -
-
- -
-
-
-
-
- - - -
-
-
-

- {{ __('shopper::words.location') }} -

-
-
-
-
-
- - - - - - -
-
- - - -
-
-
-
-

- {{ __('shopper::pages/products.quantity_inventory') }} -

-
- - - {{ __('shopper::pages/products.variants.actions.manage_inventory') }} - -
-
- -
-
- - {{ __('shopper::pages/products.inventory_name') }} - -
-
- - {{ __('shopper::words.available') }} - -
-
- @foreach($inventories as $inventory) -
-
- - {{ $inventory->name }} - -
-
- - {{ $variant->stockInventory($inventory->id) }} - -
-
- @endforeach -
-
-
-
-
-
- -
-
- - - {{ __('shopper::pages/products.variants.actions.update') }} - -
-
-
diff --git a/packages/admin/resources/views/livewire/slide-overs/add-variant.blade.php b/packages/admin/resources/views/livewire/slide-overs/add-variant.blade.php new file mode 100755 index 000000000..31c224814 --- /dev/null +++ b/packages/admin/resources/views/livewire/slide-overs/add-variant.blade.php @@ -0,0 +1,7 @@ + + {{ $this->form }} + diff --git a/packages/admin/routes/admin/product.php b/packages/admin/routes/admin/product.php index 1f08f5b55..e00b0041a 100644 --- a/packages/admin/routes/admin/product.php +++ b/packages/admin/routes/admin/product.php @@ -3,18 +3,15 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; -use Shopper\Http\Controllers\AttributeController; -use Shopper\Http\Controllers\Ecommerce; Route::as('products.')->group(function (): void { Route::get('/', config('shopper.components.product.pages.product-index'))->name('index'); Route::get('/create', config('shopper.components.product.pages.product-create'))->name('create'); Route::get('/{product}/edit', config('shopper.components.product.pages.product-edit'))->name('edit'); - Route::get('/{product}/variants/{id}', [Ecommerce\ProductController::class, 'variant']) + Route::get('/{product}/variants/{variantId}', config('shopper.components.product.pages.variant-edit')) ->name('variant'); }); Route::prefix('attributes')->as('attributes.')->group(function (): void { Route::get('/', config('shopper.components.product.pages.attribute-index'))->name('index'); }); -// Route::resource('attributes', AttributeController::class)->except('destroy', 'store', 'update'); diff --git a/packages/admin/src/Actions/Store/InitialQuantityInventory.php b/packages/admin/src/Actions/Store/InitialQuantityInventory.php new file mode 100644 index 000000000..625614062 --- /dev/null +++ b/packages/admin/src/Actions/Store/InitialQuantityInventory.php @@ -0,0 +1,28 @@ +first(); + + if ($inventory) { + $product->mutateStock( + inventoryId: $inventory->id, + quantity: (int) $quantity, + arguments: [ + 'event' => __('shopper::pages/products.inventory.initial'), + 'old_quantity' => $quantity, + ] + ); + } + } +} diff --git a/packages/admin/src/Http/Controllers/DashboardController.php b/packages/admin/src/Http/Controllers/DashboardController.php deleted file mode 100755 index 73fa9a237..000000000 --- a/packages/admin/src/Http/Controllers/DashboardController.php +++ /dev/null @@ -1,15 +0,0 @@ -authorize('browse_products'); - - return view('shopper::pages.products.index'); - } - - public function create(): View - { - $this->authorize('add_products'); - - return view('shopper::pages.products.create'); - } - - public function edit(int $id): View - { - $this->authorize('edit_products'); - - return view('shopper::pages.products.edit', [ - 'product' => (new ProductRepository()) - ->with(['inventoryHistories', 'variations', 'categories', 'collections', 'channels', 'relatedProducts', 'attributes']) - ->getById($id), - ]); - } - - public function variant(int $product, int $id): View - { - $this->authorize('edit_products'); - - return view('shopper::pages.products.variant', [ - 'product' => (new ProductRepository())->getById($product), - 'variant' => (new ProductRepository()) - ->with('inventoryHistories') - ->getById($id), - ]); - } -} diff --git a/packages/admin/src/Livewire/Components/Products/Form/Inventory.php b/packages/admin/src/Livewire/Components/Products/Form/Inventory.php index 1c065b762..f520cd554 100755 --- a/packages/admin/src/Livewire/Components/Products/Form/Inventory.php +++ b/packages/admin/src/Livewire/Components/Products/Form/Inventory.php @@ -15,18 +15,15 @@ use Filament\Tables\Contracts\HasTable; use Filament\Tables\Table; use Illuminate\Contracts\View\View; -use Illuminate\Validation\Rule; use Livewire\Attributes\On; use Livewire\Component; use Shopper\Components; use Shopper\Core\Models\InventoryHistory; -use Shopper\Core\Traits\Attributes\WithStock; class Inventory extends Component implements HasForms, HasTable { use InteractsWithForms; use InteractsWithTable; - // use WithStock; public $product; @@ -37,10 +34,6 @@ public function mount($product): void $this->product = $product; $this->form->fill($this->product->toArray()); - /*$this->inventories = $inventories; - $this->inventory = $defaultInventory; - $this->stock = $product->stock; - $this->realStock = $product->stock;*/ } public function form(Form $form): Form @@ -199,28 +192,12 @@ public function table(Table $table): Table public function store(): void { - $this->validate([ - 'sku' => [ - 'nullable', - Rule::unique(shopper_table('products'), 'sku')->ignore($this->product->id), - ], - 'barcode' => [ - 'nullable', - Rule::unique(shopper_table('products'), 'barcode')->ignore($this->product->id), - ], - ]); - $this->product->update($this->form->getState()); - $this->product->update([ - 'sku' => $this->sku ?? null, - 'barcode' => $this->barcode ?? null, - 'security_stock' => $this->securityStock ?? null, - ]); $this->dispatch('productHasUpdated'); Notification::make() - ->body(__('shopper::pages/products.notifications.stock_update')) + ->title(__('shopper::pages/products.notifications.stock_update')) ->success() ->send(); } diff --git a/packages/admin/src/Livewire/Components/Products/Form/Media.php b/packages/admin/src/Livewire/Components/Products/Form/Media.php index 99c26f852..5c525484f 100644 --- a/packages/admin/src/Livewire/Components/Products/Form/Media.php +++ b/packages/admin/src/Livewire/Components/Products/Form/Media.php @@ -45,6 +45,9 @@ public function form(Form $form): Form ->image() ->maxSize(1024) ->imageEditor() + ->extraAttributes([ + 'class' => 'sh-input-media', + ]) ->columnSpan(['lg' => 1]), ]) ->columns(3) diff --git a/packages/admin/src/Livewire/Components/Products/Form/Variants.php b/packages/admin/src/Livewire/Components/Products/Form/Variants.php index d052be463..34498043a 100755 --- a/packages/admin/src/Livewire/Components/Products/Form/Variants.php +++ b/packages/admin/src/Livewire/Components/Products/Form/Variants.php @@ -4,72 +4,146 @@ namespace Shopper\Livewire\Components\Products\Form; +use Filament\Forms\Concerns\InteractsWithForms; +use Filament\Forms\Contracts\HasForms; use Filament\Notifications\Notification; +use Filament\Tables; +use Filament\Tables\Concerns\InteractsWithTable; +use Filament\Tables\Contracts\HasTable; +use Filament\Tables\Table; use Illuminate\Contracts\View\View; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\Blade; +use Illuminate\Support\HtmlString; +use Livewire\Attributes\On; use Livewire\Component; -use Livewire\WithFileUploads; -use Livewire\WithPagination; use Shopper\Core\Events\Products\Deleted as ProductDeleted; +use Shopper\Core\Models\Product; use Shopper\Core\Repositories\Store\ProductRepository; -use Shopper\Core\Traits\Attributes\WithUploadProcess; -use Shopper\Livewire\Components\Products\WithAttributes; -class Variants extends Component +class Variants extends Component implements HasForms, HasTable { - use WithAttributes; - use WithFileUploads; - use WithPagination; - use WithUploadProcess; - - public string $search = ''; + use InteractsWithForms; + use InteractsWithTable; public $product; - public $quantity; - - public string $currency; - - protected $listeners = ['onVariantAdded' => 'render']; - - public function mount($product, string $currency): void + public function mount($product): void { $this->product = $product; - $this->currency = $currency; } - public function paginationView(): string + public function table(Table $table): Table { - return 'shopper::livewire.wire-pagination-links'; - } - - public function remove(int $id): void - { - $product = (new ProductRepository())->getById($id); - - event(new ProductDeleted($product)); - - $product->forceDelete(); - - $this->dispatchBrowserEvent('item-removed'); - - Notification::make() - ->body(__('shopper::pages/products.notifications.variation_delete')) - ->success() - ->send(); + return $table + ->query( + (new ProductRepository()) + ->makeModel() + ->where('parent_id', $this->product->id) + ->newQuery() + ) + ->columns([ + Tables\Columns\SpatieMediaLibraryImageColumn::make('images') + ->collection(config('shopper.core.storage.collection_name')) + ->stacked() + ->circular() + ->wrap() + ->defaultImageUrl(config('shopper.media.fallback_url')), + + Tables\Columns\TextColumn::make('name') + ->label(__('shopper::layout.forms.label.name')) + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('sku') + ->label(__('shopper::layout.tables.sku')) + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('stock') + ->label(__('shopper::layout.tables.current_stock')) + ->formatStateUsing( + fn (Product $record): HtmlString => new HtmlString(Blade::render(<< + + {{ __('shopper::words.in_stock') }} +
+ BLADE)) + ), + + Tables\Columns\TextColumn::make('price_amount') + ->label(__('shopper::layout.forms.label.price')) + ->money(shopper_currency()) + ->sortable(), + ]) + ->actions([ + Tables\Actions\ActionGroup::make([ + Tables\Actions\Action::make('edit') + ->label(__('shopper::layout.forms.actions.edit')) + ->icon('untitledui-edit-04') + ->url( + fn (Product $record): string => route( + name: 'shopper.products.variant', + parameters: ['variantId' => $record->id, 'product' => $this->product] + ), + ), + Tables\Actions\Action::make(__('shopper::layout.forms.actions.delete')) + ->icon('untitledui-trash-03') + ->color('danger') + ->requiresConfirmation() + ->modalIcon('untitledui-trash-03') + ->action(function (Product $record): void { + event(new ProductDeleted($record)); + + $record->forceDelete(); + + $this->dispatch('onVariantsUpdated'); + + Notification::make() + ->title(__('shopper::pages/products.notifications.variation_delete')) + ->success() + ->send(); + }), + ]) + ->tooltip('Actions'), + ]) + ->groupedBulkActions([ + Tables\Actions\DeleteBulkAction::make() + ->label(__('shopper::layout.forms.actions.delete')) + ->requiresConfirmation() + ->action(function (Collection $records): void { + $records->each->delete(); + + Notification::make() + ->title(__('shopper::components.tables.status.delete')) + ->body( + __('shopper::components.tables.messages.delete', [ + 'name' => mb_strtolower(__('shopper::words.variant')), + ]) + ) + ->success() + ->send(); + }) + ->deselectRecordsAfterCompletion(), + ]) + ->headerActions([ + Tables\Actions\Action::make('add') + ->label(__('shopper::pages/products.variants.add')) + ->action( + fn () => $this->dispatch( + 'openPanel', + component: 'shopper-slide-overs.add-variant', + arguments: ['productId' => $this->product->id] + ) + ), + ]) + ->emptyStateHeading(__('shopper::pages/products.variants.empty')) + ->emptyStateIcon('untitledui-book-open'); } + #[On('onVariantsUpdated')] public function render(): View { - return view('shopper::livewire.products.forms.form-variants', [ - 'variants' => (new ProductRepository()) - ->makeModel() - ->where(function (Builder $query): void { - $query->where('name', 'like', '%' . $this->search . '%'); - $query->where('parent_id', $this->product->id); - }) - ->orderBy('created_at', 'desc') - ->paginate(10), - ]); + return view('shopper::livewire.components.products.forms.variants'); } } diff --git a/packages/admin/src/Livewire/Components/Products/Variant.php b/packages/admin/src/Livewire/Components/Products/Variant.php deleted file mode 100755 index f6041ab19..000000000 --- a/packages/admin/src/Livewire/Components/Products/Variant.php +++ /dev/null @@ -1,112 +0,0 @@ - 'render', - ]; - - public function mount($product, $variant, string $currency): void - { - $this->inventories = Inventory::query()->select(['name', 'id'])->get(); - $this->product = $product; - $this->variant = $variant; - $this->name = $variant->name; - $this->sku = $variant->sku; - $this->barcode = $variant->barcode; - $this->securityStock = $variant->security_stock; - $this->price_amount = $variant->price_amount; - $this->old_price_amount = $variant->old_price_amount; - $this->cost_amount = $variant->cost_amount; - $this->currency = $currency; - $this->images = $variant->getMedia(config('shopper.core.storage.collection_name')); - } - - public function store(): void - { - $this->validate([ - 'name' => [ - 'required', - 'max:150', - Rule::unique(shopper_table('products'), 'name')->ignore($this->variant->id), - ], - 'files.*' => 'nullable|image|max:10024', - 'sku' => [ - 'nullable', - Rule::unique(shopper_table('products'), 'sku')->ignore($this->variant->id), - ], - 'barcode' => [ - 'nullable', - Rule::unique(shopper_table('products'), 'barcode')->ignore($this->variant->id), - ], - ]); - - $this->variant->update([ - 'name' => $this->name, - 'slug' => $this->name, - 'old_price_amount' => $this->old_price_amount ?? null, - 'price_amount' => $this->price_amount ?? null, - 'cost_amount' => $this->cost_amount ?? null, - 'sku' => $this->sku ?? null, - 'barcode' => $this->barcode ?? null, - 'security_stock' => $this->securityStock ?? null, - ]); - - if (collect($this->files)->isNotEmpty()) { - collect($this->files)->each( - fn ($file) => $this->variant->addMedia($file->getRealPath()) - ->toMediaCollection(config('shopper.core.storage.collection_name')) - ); - } - - event(new ProductUpdated($this->variant)); - - $this->emitSelf('onVariantUpdated'); - - Notification::make() - ->body(__('shopper::pages/products.notifications.variation_update')) - ->success() - ->send(); - } - - public function mediaDeleted(): void - { - $this->images = $this->variant->getMedia(config('shopper.core.storage.collection_name')); - } - - public function render(): View - { - return view('shopper::livewire.products.variant', [ - 'media' => $this->variant->getFirstMedia(config('shopper.core.storage.collection_name')), - ]); - } -} diff --git a/packages/admin/src/Livewire/Components/Products/VariantStock.php b/packages/admin/src/Livewire/Components/Products/VariantStock.php index 95ae51574..f24bbc314 100755 --- a/packages/admin/src/Livewire/Components/Products/VariantStock.php +++ b/packages/admin/src/Livewire/Components/Products/VariantStock.php @@ -4,46 +4,101 @@ namespace Shopper\Livewire\Components\Products; +use Filament\Actions\Action; +use Filament\Actions\Concerns\InteractsWithActions; +use Filament\Actions\Contracts\HasActions; +use Filament\Forms; +use Filament\Forms\Concerns\InteractsWithForms; +use Filament\Forms\Contracts\HasForms; +use Filament\Notifications\Notification; +use Filament\Support\Enums\MaxWidth; use Illuminate\Contracts\View\View; +use Livewire\Attributes\On; use Livewire\Component; -use Livewire\WithPagination; use Shopper\Core\Models\Inventory; use Shopper\Core\Models\InventoryHistory; -use Shopper\Core\Traits\Attributes\WithStock; -class VariantStock extends Component +class VariantStock extends Component implements HasActions, HasForms { - use WithPagination; - use WithStock; + use InteractsWithActions; + use InteractsWithForms; - public $product; + public $variant; - public function mount($variant): void + public function mount($record): void { - $this->product = $variant; - $this->stock = $variant->stock; - $this->realStock = $variant->stock; + $this->variant = $record; } - public function paginationView(): string + public function stockAction(): Action { - return 'shopper::livewire.wire-pagination-links'; + return Action::make('stock') + ->label(__('shopper::pages/products.variants.actions.update_stock')) + ->color('gray') + ->modal() + ->icon('untitledui-package') + ->modalHeading(__('shopper::pages/products.modals.variants.title')) + ->modalWidth(MaxWidth::ExtraLarge) + ->form([ + Forms\Components\Select::make('inventory') + ->label(__('shopper::pages/products.inventory_name')) + ->options(Inventory::query()->pluck('name', 'id')) + ->native(false) + ->required(), + + Forms\Components\TextInput::make('quantity') + ->label(__('shopper::layout.forms.label.quantity')) + ->placeholder('-10 or -5 or 50, etc') + ->numeric() + ->required(), + ]) + ->action(function (array $data): void { + $inventoryId = (int) $data['inventory']; + $quantity = (int) $data['quantity']; + + $currentStock = InventoryHistory::query() + ->where('inventory_id', $inventoryId) + ->where('stockable_id', $this->variant->id) + ->where('stockable_type', 'product') + ->get() + ->sum('quantity'); + + $realTimeStock = $currentStock + $quantity; + + if ($realTimeStock >= $currentStock) { + $this->variant->mutateStock( + $inventoryId, + $quantity, + [ + 'event' => __('shopper::pages/products.inventory.add'), + 'old_quantity' => $quantity, + ] + ); + } else { + $this->variant->decreaseStock( + $inventoryId, + $quantity, + [ + 'event' => __('shopper::pages/products.inventory.remove'), + 'old_quantity' => $quantity, + ] + ); + } + + Notification::make() + ->title(__('Stock successfully Updated')) + ->success() + ->send(); + + $this->dispatch('updateVariantInventory'); + }); } + #[On('updateVariantInventory')] public function render(): View { - return view('shopper::livewire.products.variant-stock', [ - 'currentStock' => InventoryHistory::query() - ->where('inventory_id', $this->inventory) - ->where('stockable_id', $this->product->id) - ->get() - ->sum('quantity'), - 'histories' => InventoryHistory::query() - ->where('inventory_id', $this->inventory) - ->where('stockable_id', $this->product->id) - ->orderByDesc('created_at') - ->paginate(3), - 'inventories' => Inventory::all(), + return view('shopper::livewire.components.products.variant-stock', [ + 'inventories' => Inventory::query()->get(), ]); } } diff --git a/packages/admin/src/Livewire/Modals/AddVariant.php b/packages/admin/src/Livewire/Modals/AddVariant.php deleted file mode 100755 index d394e70c8..000000000 --- a/packages/admin/src/Livewire/Modals/AddVariant.php +++ /dev/null @@ -1,111 +0,0 @@ - 'onFilesUpdated', - ]; - - public function mount(int $productId, string $currency): void - { - $this->productId = $productId; - $this->currency = $currency; - } - - public function onFilesUpdated(array $files): void - { - $this->files = $files; - } - - public function rules(): array - { - return [ - 'name' => 'required|unique:' . shopper_table('products'), - 'sku' => 'nullable|unique:' . shopper_table('products'), - 'barcode' => 'nullable|unique:' . shopper_table('products'), - ]; - } - - public function save(): void - { - $this->validate($this->rules()); - - /** @var Product $product */ - $product = (new ProductRepository())->create([ - 'name' => $this->name, - 'slug' => $this->name, - 'sku' => $this->sku, - 'type' => $this->type, - 'barcode' => $this->barcode, - 'is_visible' => true, - 'security_stock' => $this->securityStock, - 'old_price_amount' => $this->old_price_amount, - 'price_amount' => $this->price_amount, - 'cost_amount' => $this->cost_amount, - 'parent_id' => $this->productId, - ]); - - if (collect($this->files)->isNotEmpty()) { - collect($this->files)->each( - fn ($file) => $product->addMedia($file)->toMediaCollection(config('shopper.core.storage.collection_name')) - ); - } - - if ($this->quantity && count($this->quantity) > 0) { - foreach ($this->quantity as $inventory => $value) { - $product->mutateStock( - $inventory, - (int) $value, - [ - 'event' => __('shopper::pages/products.inventory.initial'), - 'old_quantity' => $value, - ] - ); - } - } - - Notification::make() - ->title(__('shopper::layout.status.added')) - ->body(__('shopper::pages/products.notifications.variation_create')) - ->success() - ->send(); - - $this->emit('onVariantAdded'); - - $this->closeModal(); - } - - public static function modalMaxWidth(): string - { - return '3xl'; - } - - public function render(): View - { - return view('shopper::livewire.modals.add-variant', [ - 'inventories' => Inventory::all(), - ]); - } -} diff --git a/packages/admin/src/Livewire/Modals/UpdateVariantStock.php b/packages/admin/src/Livewire/Modals/UpdateVariantStock.php deleted file mode 100755 index ab77d60f3..000000000 --- a/packages/admin/src/Livewire/Modals/UpdateVariantStock.php +++ /dev/null @@ -1,55 +0,0 @@ -product = $variant = (new ProductRepository())->getById($id); - $this->stock = $variant->stock; // @phpstan-ignore-line - $this->realStock = $variant->stock; // @phpstan-ignore-line - } - - public static function modalMaxWidth(): string - { - return '4xl'; - } - - public function paginationView(): string - { - return 'shopper::livewire.wire-pagination-links'; - } - - public function render(): View - { - return view('shopper::livewire.modals.update-variant-stock', [ - 'currentStock' => InventoryHistory::query() - ->where('inventory_id', $this->inventory) - ->where('stockable_id', $this->product->id) - ->get() - ->sum('quantity'), - 'histories' => InventoryHistory::query() - ->where('inventory_id', $this->inventory) - ->where('stockable_id', $this->product->id) - ->orderByDesc('created_at') - ->paginate(3), - 'inventories' => Inventory::all(), - ]); - } -} diff --git a/packages/admin/src/Livewire/Pages/Product/Create.php b/packages/admin/src/Livewire/Pages/Product/Create.php index 046bd45dd..959aa88cd 100755 --- a/packages/admin/src/Livewire/Pages/Product/Create.php +++ b/packages/admin/src/Livewire/Pages/Product/Create.php @@ -15,8 +15,8 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; +use Shopper\Actions\Store\InitialQuantityInventory; use Shopper\Components; -use Shopper\Core\Models\Inventory; use Shopper\Core\Models\Product; use Shopper\Core\Repositories\ChannelRepository; use Shopper\Core\Repositories\Store\ProductRepository; @@ -296,20 +296,10 @@ public function store(): void $product->categories()->sync($data['categories']); } - $quantity = $data['quantity']; + $quantity = (int) $data['quantity']; - /** @var Inventory $inventory */ - $inventory = Inventory::default()->first(); - - if ($inventory && $quantity && $quantity > 0) { - $product->mutateStock( - inventoryId: $inventory->id, - quantity: (int) $quantity, - arguments: [ - 'event' => __('shopper::pages/products.inventory.initial'), - 'old_quantity' => $quantity, - ] - ); + if ($quantity && $quantity > 0) { + (new InitialQuantityInventory())->handle($quantity, $product); } Notification::make() diff --git a/packages/admin/src/Livewire/Pages/Product/Edit.php b/packages/admin/src/Livewire/Pages/Product/Edit.php index 9d7ab663a..e068123ea 100755 --- a/packages/admin/src/Livewire/Pages/Product/Edit.php +++ b/packages/admin/src/Livewire/Pages/Product/Edit.php @@ -22,6 +22,11 @@ class Edit extends AbstractPageComponent implements HasActions, HasForms public Product $product; + public function mount(): void + { + $this->authorize('edit_products'); + } + public function deleteAction(): Action { return Action::make(__('shopper::layout.forms.actions.delete')) diff --git a/packages/admin/src/Livewire/Pages/Product/Index.php b/packages/admin/src/Livewire/Pages/Product/Index.php index 4688dce94..a0a94c4c4 100755 --- a/packages/admin/src/Livewire/Pages/Product/Index.php +++ b/packages/admin/src/Livewire/Pages/Product/Index.php @@ -107,6 +107,7 @@ public function table(Table $table): Table ->successNotificationTitle(__('shopper::pages/products.notifications.replicated')), Tables\Actions\Action::make(__('shopper::layout.forms.actions.delete')) ->icon('untitledui-trash-03') + ->modalIcon('untitledui-trash-03') ->color('danger') ->requiresConfirmation() ->action(fn (Product $record) => $record->delete()), diff --git a/packages/admin/src/Livewire/Pages/Product/Variant.php b/packages/admin/src/Livewire/Pages/Product/Variant.php new file mode 100755 index 000000000..b620c005c --- /dev/null +++ b/packages/admin/src/Livewire/Pages/Product/Variant.php @@ -0,0 +1,153 @@ +authorize('edit_products'); + + $this->product = (new ProductRepository())->getById($product); + $this->variant = (new ProductRepository())->getById($variantId); + + $this->form->fill($this->variant->toArray()); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Components\Section::make(__('shopper::pages/products.variants.variant_information')) + ->compact() + ->aside() + ->schema([ + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('name') + ->label(__('shopper::layout.forms.label.name')) + ->placeholder('Model Y, Model S (Eg. for and Tesla car)') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function ($state, Forms\Set $set): void { + $set('slug', Str::slug($state)); + }), + + Forms\Components\TextInput::make('slug') + ->label(__('shopper::layout.forms.label.slug')) + ->disabled() + ->dehydrated() + ->required() + ->maxLength(255) + ->unique(config('shopper.models.product'), 'slug', ignoreRecord: true), + + Forms\Components\TextInput::make('price_amount') + ->label(__('shopper::layout.forms.label.price_amount')) + ->numeric() + ->rules(['regex:/^\d{1,6}(\.\d{0,2})?$/']) + ->suffix(shopper_currency()) + ->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2), + + Forms\Components\TextInput::make('old_price_amount') + ->label(__('shopper::layout.forms.label.compare_price')) + ->numeric() + ->rules(['regex:/^\d{1,6}(\.\d{0,2})?$/']) + ->suffix(shopper_currency()) + ->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2), + ]), + ]), + + Components\Separator::make(), + + Components\Section::make(__('shopper::words.images')) + ->compact() + ->aside() + ->schema([ + Forms\Components\SpatieMediaLibraryFileUpload::make('images') + ->multiple() + ->hiddenLabel() + ->helperText(__('shopper::pages/products.variant_images_helpText')) + ->collection(config('shopper.core.storage.collection_name')) + ->maxSize(1024), + ]), + + Components\Separator::make(), + + Components\Section::make(__('shopper::words.location')) + ->compact() + ->aside() + ->schema([ + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('sku') + ->label(__('shopper::layout.forms.label.sku')) + ->unique(config('shopper.models.product'), 'sku', ignoreRecord: true) + ->required() + ->maxLength(255), + + Forms\Components\TextInput::make('barcode') + ->label(__('shopper::layout.forms.label.barcode')) + ->unique(config('shopper.models.product'), 'barcode', ignoreRecord: true) + ->maxLength(255), + + Forms\Components\TextInput::make('security_stock') + ->label(__('shopper::layout.forms.label.safety_stock')) + ->numeric() + ->default(0) + ->rules(['integer', 'min:0']), + ]), + + Components\Separator::make(), + + Forms\Components\Livewire::make(VariantStock::class), + ]), + ]) + ->statePath('data') + ->model($this->variant); + } + + public function store(): void + { + $this->variant->update($this->form->getState()); + $this->form->model($this->variant)->saveRelationships(); + + event(new ProductUpdated($this->variant)); + + $this->dispatch('onVariantUpdated'); + + Notification::make() + ->title(__('shopper::pages/products.notifications.variation_update')) + ->success() + ->send(); + } + + public function render(): View + { + return view('shopper::livewire.pages.products.variant') + ->title(__('shopper::pages/products.variants.variant_title', ['name' => $this->variant->name])); + } +} diff --git a/packages/admin/src/Livewire/SlideOvers/AddVariantForm.php b/packages/admin/src/Livewire/SlideOvers/AddVariantForm.php new file mode 100755 index 000000000..3d0ad8200 --- /dev/null +++ b/packages/admin/src/Livewire/SlideOvers/AddVariantForm.php @@ -0,0 +1,208 @@ +authorize('add_products'); + + $this->productId = $productId; + + $this->form->fill(); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Hidden::make('parent_id') + ->default($this->productId), + + Components\Section::make(__('shopper::words.general')) + ->collapsible() + ->compact() + ->schema([ + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('name') + ->label(__('shopper::layout.forms.label.name')) + ->placeholder('Model Y, Model S (Eg. for and Tesla car)') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function ($state, Forms\Set $set): void { + $set('slug', Str::slug($state)); + }), + + Forms\Components\TextInput::make('slug') + ->label(__('shopper::layout.forms.label.slug')) + ->disabled() + ->dehydrated() + ->required() + ->maxLength(255) + ->unique(config('shopper.models.product'), 'slug'), + ]), + + Forms\Components\Group::make() + ->schema([ + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('price_amount') + ->label(__('shopper::layout.forms.label.price_amount')) + ->numeric() + ->rules(['regex:/^\d{1,6}(\.\d{0,2})?$/']) + ->suffix(shopper_currency()) + ->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2), + + Forms\Components\TextInput::make('old_price_amount') + ->label(__('shopper::layout.forms.label.compare_price')) + ->numeric() + ->rules(['regex:/^\d{1,6}(\.\d{0,2})?$/']) + ->suffix(shopper_currency()) + ->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2), + ]), + ]), + ]), + + Components\Section::make(__('shopper::words.location')) + ->collapsible() + ->compact() + ->schema([ + Forms\Components\Placeholder::make('stock') + ->label('Stock & Inventory') + ->content(new HtmlString(Blade::render(<<<'BLADE' +

+ {{ __('Configure the inventory and stock for this variant') }} +

+ BLADE))), + + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('sku') + ->label(__('shopper::layout.forms.label.sku')) + ->unique(config('shopper.models.product'), 'sku') + ->required() + ->maxLength(255), + + Forms\Components\TextInput::make('barcode') + ->label(__('shopper::layout.forms.label.barcode')) + ->unique(config('shopper.models.product'), 'barcode') + ->maxLength(255), + + Forms\Components\TextInput::make('quantity') + ->label(__('shopper::layout.forms.label.quantity')) + ->numeric() + ->required() + ->rules(['integer', 'min:1']), + + Forms\Components\TextInput::make('security_stock') + ->label(__('shopper::layout.forms.label.safety_stock')) + ->numeric() + ->default(0) + ->rules(['integer', 'min:0']), + ]) + ->columns(), + ]), + + Components\Section::make(__('shopper::words.media')) + ->collapsed() + ->compact() + ->schema([ + Forms\Components\SpatieMediaLibraryFileUpload::make('images') + ->multiple() + ->helperText(__('shopper::pages/products.variant_images_helpText')) + ->collection(config('shopper.core.storage.collection_name')) + ->maxSize(1024), + ]), + + Components\Section::make(__('shopper::words.shipping')) + ->collapsed() + ->compact() + ->schema([ + Forms\Components\Placeholder::make('shipping') + ->label(__('shopper::words.weight_dimension')) + ->content(new HtmlString(Blade::render(<<<'BLADE' +

+ {{ __('shopper::pages/products.weight_dimension_help_text') }} +

+ BLADE))), + + Forms\Components\Grid::make() + ->schema(Components\Form\ShippingField::make()), + ]), + + Components\Section::make('Metadata') + ->collapsed() + ->compact() + ->schema([ + Forms\Components\KeyValue::make('metadata') + ->reorderable(), + ]), + ]) + ->statePath('data') + ->model(config('shopper.models.product')); + } + + public function save(): void + { + $data = $this->form->getState(); + + /** @var Product $product */ + $product = (new ProductRepository())->create( + Arr::except($data, ['quantity']) + ); + $this->form->model($product)->saveRelationships(); + + $quantity = (int) $data['quantity']; + + if ($quantity && $quantity > 0) { + (new InitialQuantityInventory())->handle($quantity, $product); + } + + Notification::make() + ->title(__('shopper::layout.status.added')) + ->body(__('shopper::pages/products.notifications.variation_create')) + ->success() + ->send(); + + $this->dispatch('onVariantsUpdated'); + + $this->closePanel(); + } + + public static function panelMaxWidth(): string + { + return '3xl'; + } + + public function render(): View + { + return view('shopper::livewire.slide-overs.add-variant'); + } +}