From dc05eb31ac36f1a15abee85aad0316b9459350c9 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Sun, 5 Oct 2025 09:39:27 +0200 Subject: [PATCH] new: add channel management --- .../Controllers/Shop/CustomerController.php | 45 +++++ app/Repositories/Shop/Articles.php | 6 + app/Repositories/Shop/Baskets.php | 27 ++- app/Repositories/Shop/Customers.php | 39 +++- app/Repositories/Shop/Deliveries.php | 6 +- app/Repositories/Shop/Offers.php | 40 ++++ app/Repositories/Shop/SaleChannels.php | 39 +++- app/Repositories/Shop/Shop.php | 47 +++++ ...ale_channel_id_to_shop_customers_table.php | 28 +++ resources/lang/fr/shop.php | 3 + resources/views/Shop/Articles/show.blade.php | 31 ++++ resources/views/Shop/Baskets/basket.blade.php | 13 +- .../Customers/partials/deliveries.blade.php | 171 ++++++++++++++++-- .../Shop/Customers/partials/sale.blade.php | 98 ++++++---- .../Shop/Orders/partials/deliveries.blade.php | 12 +- resources/views/Shop/layout/layout.blade.php | 1 + 16 files changed, 541 insertions(+), 65 deletions(-) create mode 100644 app/Repositories/Shop/Shop.php create mode 100644 database/migrations/shop/2025_03_06_000001_add_default_sale_channel_id_to_shop_customers_table.php diff --git a/app/Http/Controllers/Shop/CustomerController.php b/app/Http/Controllers/Shop/CustomerController.php index e5fbf25f..7a07e89f 100644 --- a/app/Http/Controllers/Shop/CustomerController.php +++ b/app/Http/Controllers/Shop/CustomerController.php @@ -2,8 +2,11 @@ namespace App\Http\Controllers\Shop; +use App\Repositories\Core\User\ShopCart; +use App\Repositories\Shop\Baskets; use App\Repositories\Shop\CustomerAddresses; use App\Repositories\Shop\Customers; +use App\Repositories\Shop\Offers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -44,8 +47,50 @@ class CustomerController extends Controller public function storeProfileAjax(Request $request) { $data = $request->all(); + + if (array_key_exists('default_sale_channel_id', $data)) { + $customer = Customers::get(Customers::getId()); + $newSaleChannelId = (int) $data['default_sale_channel_id']; + $currentSaleChannelId = (int) ($customer->default_sale_channel_id ?? 0); + + if ($newSaleChannelId && $newSaleChannelId !== $currentSaleChannelId && ShopCart::count() > 0) { + $cartItems = ShopCart::getContent(); + $unavailable = []; + + foreach ($cartItems as $item) { + $offerId = (int) $item->id; + + if (! Offers::getPrice($offerId, 1, $newSaleChannelId)) { + $offer = Offers::get($offerId, ['article']); + $unavailable[] = $offer->article->name ?? $item->name; + + if (count($unavailable) >= 3) { + break; + } + } + } + + if (! empty($unavailable)) { + $list = implode(', ', $unavailable); + + return response()->json([ + 'error' => 1, + 'message' => __('Certains articles de votre panier ne sont pas disponibles dans ce canal : :products. Merci de finaliser votre commande ou de retirer ces articles avant de changer de canal.', ['products' => $list]), + ], 422); + } + } + } + $customer = Customers::store($data); + if ($customer) { + Customers::guard()->setUser($customer->fresh(['sale_channels'])); + if (array_key_exists('default_sale_channel_id', $data)) { + session(['shop.default_sale_channel_id' => $data['default_sale_channel_id']]); + Baskets::refreshPrices((int) $data['default_sale_channel_id']); + } + } + return response()->json(['error' => 0]); } diff --git a/app/Repositories/Shop/Articles.php b/app/Repositories/Shop/Articles.php index a3ba0736..6ec4cf4d 100644 --- a/app/Repositories/Shop/Articles.php +++ b/app/Repositories/Shop/Articles.php @@ -5,6 +5,7 @@ namespace App\Repositories\Shop; use App\Models\Shop\Article; use App\Repositories\Botanic\Species; use App\Repositories\Botanic\Varieties; +use App\Repositories\Shop\SaleChannels; use App\Repositories\Core\Comments; use App\Traits\Model\Basic; use App\Traits\Repository\Imageable; @@ -70,9 +71,14 @@ class Articles public static function getArticleToSell($id, $saleChannelId = false) { + $saleChannelId = $saleChannelId ?: SaleChannels::getDefaultID(); $data = self::getArticle($id); $data['offers'] = self::getOffersGroupedByNature($id, $saleChannelId); + $currentSaleChannel = $saleChannelId ? SaleChannels::get($saleChannelId) : null; + $data['current_sale_channel'] = $currentSaleChannel ? $currentSaleChannel->toArray() : null; + $data['available_sale_channels'] = Offers::getSaleChannelsForArticle($id); + return $data; } diff --git a/app/Repositories/Shop/Baskets.php b/app/Repositories/Shop/Baskets.php index 96b962f6..53b04957 100644 --- a/app/Repositories/Shop/Baskets.php +++ b/app/Repositories/Shop/Baskets.php @@ -100,12 +100,19 @@ class Baskets $offers = Offers::getWithPricesByIds(self::getIds(), $saleChannelId); foreach ($basket as $item) { $offer = $offers->where('id', $item->id)->first(); + if (! $offer) { + continue; + } + + $priceValue = Offers::getPrice($item->id, $item->quantity, $saleChannelId); + $unitPrice = $priceValue ? (float) $priceValue->price_taxed : (float) $item->price; + $article_nature = strtolower($offer->article->article_nature->name); $data[$article_nature][] = [ 'id' => (int) $item->id, 'name' => $item->name, 'quantity' => (int) $item->quantity, - 'price' => $item->price, + 'price' => $unitPrice, 'variation' => $offer->variation->name, 'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)), 'latin' => $offer->article->product->specie->latin ?? false, @@ -115,6 +122,24 @@ class Baskets return $data ?? false; } + public static function refreshPrices($saleChannelId = false) + { + $saleChannelId = $saleChannelId ? $saleChannelId : SaleChannels::getDefaultID(); + $basket = ShopCart::getContent(); + + foreach ($basket as $item) { + $priceValue = Offers::getPrice($item->id, $item->quantity, $saleChannelId); + + if (! $priceValue) { + continue; + } + + ShopCart::get()->update($item->id, [ + 'price' => $priceValue->price_taxed, + ]); + } + } + public static function getBasketData($id, $quantity = 1) { $offer = Offers::get($id, ['article', 'variation']); diff --git a/app/Repositories/Shop/Customers.php b/app/Repositories/Shop/Customers.php index 8625d58c..7c7ca9b8 100644 --- a/app/Repositories/Shop/Customers.php +++ b/app/Repositories/Shop/Customers.php @@ -31,16 +31,31 @@ class Customers public static function getSaleChannels($customerId = false) { $customer = $customerId ? self::get($customerId) : self::getAuth(); - $saleChannels = $customer ? $customer->sale_channels : false; + $saleChannels = collect(); - return $saleChannels ? $saleChannels : SaleChannels::getDefault(); + if ($customer) { + $customer->loadMissing('sale_channels'); + $saleChannels = $customer->sale_channels ?? collect(); + + if ($saleChannels instanceof \Illuminate\Support\Collection && $saleChannels->isNotEmpty()) { + return $saleChannels; + } + } + + $default = SaleChannels::getDefault($customerId); + + return $default ? collect([$default]) : collect(); } public static function getSaleChannel($customerId = false) { $saleChannels = self::getSaleChannels($customerId); - return $saleChannels->first(); + if ($saleChannels instanceof \Illuminate\Support\Collection) { + return $saleChannels->first(); + } + + return $saleChannels; } public static function getDeliveries() @@ -58,12 +73,22 @@ class Customers public static function editProfile($id = false) { - return $id ? [ - 'customer' => self::get($id, ['addresses', 'deliveries'])->toArray(), - 'deliveries' => Deliveries::getAllWithSaleChannel()->toArray(), + if (! $id) { + abort('403'); + } + + $customer = self::get($id, ['addresses', 'deliveries', 'sale_channels']); + + $saleChannels = self::getSaleChannels($id); + + return [ + 'customer' => $customer->toArray(), + 'sale_channels' => $saleChannels->toArray(), + 'deliveries' => Deliveries::getByCustomer($id)->toArray(), + 'sale_channel_checks' => Shop::getSaleChannelAvailabilitySummary($saleChannels->pluck('id')->toArray()), 'orders' => (new CustomerOrdersDataTable())->html(), 'invoices' => (new CustomerInvoicesDataTable())->html(), - ] : abort('403'); + ]; } public static function getAddresses($id = false) diff --git a/app/Repositories/Shop/Deliveries.php b/app/Repositories/Shop/Deliveries.php index 5bcfceaa..5d84ff4b 100644 --- a/app/Repositories/Shop/Deliveries.php +++ b/app/Repositories/Shop/Deliveries.php @@ -21,12 +21,12 @@ class Deliveries $customer = $customerId ? Customers::get($customerId) : Customers::getAuth(); $saleChannels = $customer ? $customer->sale_channels->pluck('id')->toArray() : [SaleChannels::getDefaultID()]; - return $saleChannels ? self::getBySaleChannels($saleChannels) : false; + return $saleChannels ? self::getBySaleChannels($saleChannels) : collect(); } public static function getBySaleChannels($saleChannels) { - return Delivery::bySaleChannels($saleChannels)->with('sale_channel')->get(); + return Delivery::bySaleChannels($saleChannels)->active()->with('sale_channel')->get(); } public static function getSaleChannelId($deliveryId) @@ -41,7 +41,7 @@ class Deliveries public static function getAllWithSaleChannel() { - return Delivery::orderBy('name', 'asc')->active()->public()->with('sale_channel')->get(); + return Delivery::orderBy('name', 'asc')->active()->with('sale_channel')->get(); } public static function toggleActive($id, $active) diff --git a/app/Repositories/Shop/Offers.php b/app/Repositories/Shop/Offers.php index 87795fe5..181e5080 100644 --- a/app/Repositories/Shop/Offers.php +++ b/app/Repositories/Shop/Offers.php @@ -3,6 +3,8 @@ namespace App\Repositories\Shop; use App\Models\Shop\Offer; +use App\Models\Shop\PriceListValue; +use App\Models\Shop\SaleChannel; use App\Traits\Model\Basic; class Offers @@ -166,4 +168,42 @@ class Offers { return Offer::query(); } + + public static function getSaleChannelsForArticle($articleId) + { + $channels = SaleChannel::query() + ->whereHas('price_lists', function ($query) use ($articleId) { + $query->whereHas('tariff.offers', function ($subQuery) use ($articleId) { + $subQuery->byArticle($articleId); + })->whereHas('price_list_values'); + }) + ->orderBy('name') + ->get(); + + $offers = Offer::query() + ->byArticle($articleId) + ->with('article') + ->get(); + + return $channels->map(function ($channel) use ($offers) { + $priceValue = null; + + foreach ($offers as $offer) { + $priceCandidate = self::getPrice($offer->id, 1, $channel->id); + + if ($priceCandidate && (float) $priceCandidate->price_taxed > 0) { + $priceValue = $priceCandidate; + break; + } + } + + return [ + 'id' => $channel->id, + 'name' => $channel->name, + 'code' => $channel->code, + 'price_taxed' => $priceValue ? (float) $priceValue->price_taxed : null, + 'quantity' => $priceValue ? (int) $priceValue->quantity : null, + ]; + })->toArray(); + } } diff --git a/app/Repositories/Shop/SaleChannels.php b/app/Repositories/Shop/SaleChannels.php index 1cd1aca8..02716289 100644 --- a/app/Repositories/Shop/SaleChannels.php +++ b/app/Repositories/Shop/SaleChannels.php @@ -3,21 +3,52 @@ namespace App\Repositories\Shop; use App\Models\Shop\SaleChannel; +use App\Repositories\Shop\Customers; use App\Traits\Model\Basic; class SaleChannels { use Basic; - public static function getDefaultID() + public static function getDefaultID($customerId = false) { - $default = self::getDefault(); + $default = self::getDefault($customerId); - return $default ? self::getDefault()->id : false; + return $default ? $default->id : false; } - public static function getDefault() + public static function getDefault($customerId = false) { + $sessionChannelId = session('shop.default_sale_channel_id'); + if ($sessionChannelId) { + $sessionChannel = SaleChannel::find($sessionChannelId); + if ($sessionChannel) { + return $sessionChannel; + } + } + + $customer = $customerId ? Customers::get($customerId) : Customers::getAuth(); + + if ($customer) { + $customer->loadMissing('sale_channels'); + + if ($customer->default_sale_channel_id) { + $preferred = $customer->sale_channels->firstWhere('id', $customer->default_sale_channel_id); + if (! $preferred) { + $preferred = SaleChannel::find($customer->default_sale_channel_id); + } + if ($preferred) { + session(['shop.default_sale_channel_id' => $preferred->id]); + return $preferred; + } + } + + if ($customer->sale_channels->isNotEmpty()) { + session(['shop.default_sale_channel_id' => $customer->sale_channels->first()->id]); + return $customer->sale_channels->first(); + } + } + return self::getByCode('EXP'); } diff --git a/app/Repositories/Shop/Shop.php b/app/Repositories/Shop/Shop.php new file mode 100644 index 00000000..d43769a3 --- /dev/null +++ b/app/Repositories/Shop/Shop.php @@ -0,0 +1,47 @@ +id; + + if (! Offers::getPrice($offerId, 1, $saleChannelId)) { + $offer = Offers::get($offerId, ['article']); + $issues[] = $offer->article->name ?? $item->name; + $issueCount++; + + if (count($issues) >= 3) { + continue; + } + } + } + + if (! empty($issues)) { + $summary[$saleChannelId] = [ + 'full_count' => $issueCount, + 'names' => array_slice($issues, 0, 3), + ]; + } + } + + return $summary; + } +} diff --git a/database/migrations/shop/2025_03_06_000001_add_default_sale_channel_id_to_shop_customers_table.php b/database/migrations/shop/2025_03_06_000001_add_default_sale_channel_id_to_shop_customers_table.php new file mode 100644 index 00000000..36ac35e7 --- /dev/null +++ b/database/migrations/shop/2025_03_06_000001_add_default_sale_channel_id_to_shop_customers_table.php @@ -0,0 +1,28 @@ +unsignedInteger('default_sale_channel_id')->nullable()->after('settings'); + $table->index('default_sale_channel_id', 'shop_customers_default_sale_channel_id_index'); + } + }); + } + + public function down() + { + Schema::table('shop_customers', function (Blueprint $table) { + if (Schema::hasColumn('shop_customers', 'default_sale_channel_id')) { + $table->dropIndex('shop_customers_default_sale_channel_id_index'); + $table->dropColumn('default_sale_channel_id'); + } + }); + } +}; diff --git a/resources/lang/fr/shop.php b/resources/lang/fr/shop.php index 28837194..36ab2964 100644 --- a/resources/lang/fr/shop.php +++ b/resources/lang/fr/shop.php @@ -223,6 +223,9 @@ return [ 'successmod' => 'Le canal de vente a été correctement modifié', 'successdel' => 'Le canal de vente a été correctement effacé', 'confirmdelete' => 'Confirmez-vous la suppression du canal de vente ?', + 'missing_offers' => '{1} Ce canal de vente n\'a pas d\'offre pour :count produit.|[2,*] Ce canal de vente n\'a pas d\'offre pour :count produits.', + 'missing_offers_all' => 'Ce canal de vente n\'a aucune offre pour tous les produits de votre panier.', + 'cannot_select_with_cart' => 'Vous ne pouvez pas sélectionner ce mode d\'achat tant que votre panier contient des produits non disponibles dans ce mode.', ], 'shelves' => [ 'title' => 'Rayons', diff --git a/resources/views/Shop/Articles/show.blade.php b/resources/views/Shop/Articles/show.blade.php index 647e2cc7..333d1a88 100644 --- a/resources/views/Shop/Articles/show.blade.php +++ b/resources/views/Shop/Articles/show.blade.php @@ -48,6 +48,37 @@
+ @if (config('app.debug') && ($article['current_sale_channel'] ?? false)) +
+ Canal actif : + {{ $article['current_sale_channel']['name'] ?? 'N/A' }} + + ID {{ $article['current_sale_channel']['id'] ?? '–' }} · Code {{ $article['current_sale_channel']['code'] ?? '–' }} + + @if (!empty($article['available_sale_channels'])) +
+ Offres disponibles dans : +
    + @foreach ($article['available_sale_channels'] as $channel) +
  • + + • {{ $channel['name'] }} + code {{ $channel['code'] }} + + @if (isset($channel['price_taxed'])) + + {{ number_format($channel['price_taxed'], 2, ',', ' ') }} € TTC + @if (! empty($channel['quantity'])) + Qté min. {{ $channel['quantity'] }} + @endif + + @endif +
  • + @endforeach +
+ @endif +
+ @endif @include('Shop.Articles.partials.ArticleAddBasket')
diff --git a/resources/views/Shop/Baskets/basket.blade.php b/resources/views/Shop/Baskets/basket.blade.php index 702e55dd..2f67f1a0 100644 --- a/resources/views/Shop/Baskets/basket.blade.php +++ b/resources/views/Shop/Baskets/basket.blade.php @@ -58,7 +58,7 @@ updateBasket(offer_id, quantity, function() { calculatePrice($row); calculateTotal(); - }); + }, $row); }); $('.basket-delete').click(function() { @@ -70,13 +70,20 @@ }); } - function updateBasket(offer_id, quantity, callback) { + function updateBasket(offer_id, quantity, callback, $row) { var data = { offer_id: offer_id, quantity: quantity, update: true }; - $.post("{{ route('Shop.Basket.addBasket') }}", data, callback); + $.post("{{ route('Shop.Basket.addBasket') }}", data, function(response) { + if ($row && response && response.added && typeof response.added.price !== 'undefined') { + $row.find('.basket-price').text(fixNumber(response.added.price)); + $row.find('.basket-total-row').text(fixNumber(response.added.price * $row.find('.basket-quantity').val())); + } + callback(response); + refreshBasketTop(); + }); } function calculatePrice($that) { diff --git a/resources/views/Shop/Customers/partials/deliveries.blade.php b/resources/views/Shop/Customers/partials/deliveries.blade.php index d45862b7..abb225cc 100644 --- a/resources/views/Shop/Customers/partials/deliveries.blade.php +++ b/resources/views/Shop/Customers/partials/deliveries.blade.php @@ -1,25 +1,168 @@ -@foreach ($deliveries as $delivery) -
-
+@push('styles') + +@endpush + +@php + $saleChannelsCollection = collect($sale_channels); + $firstSaleChannel = $saleChannelsCollection->first(); + $selectedSaleChannelId = $customer['default_sale_channel_id'] ?? ($firstSaleChannel['id'] ?? null); + $cartCount = app('App\\Repositories\\Core\\User\\ShopCart')::count(); +@endphp + +@if ($cartCount > 0) +
+ Note : en changeant votre mode d'achat, les articles de votre panier seront transférés sur la liste de prix correspondant au nouveau canal de vente sélectionné. +
+@endif + +@foreach ($saleChannelsCollection as $saleChannel) + @php + $check = $sale_channel_checks[$saleChannel['id']] ?? null; + $isBlocked = $check && $saleChannel['id'] !== $selectedSaleChannelId; + @endphp +
+
+
+
+ @include('components.form.radios.icheck', [ - 'name' => 'delivery_id', - 'id_name' => 'delivery_id_' . $delivery['id'], - 'value' => $delivery['id'], - 'checked' => $customer['sale_delivery_id'] ?? false, - 'class' => 'delivery', + 'name' => 'sale_channel_id', + 'id_name' => 'sale_channel_id_' . $saleChannel['id'], + 'val' => $saleChannel['id'], + 'value' => $selectedSaleChannelId, + 'class' => 'sale-channel', + 'disabled' => $isBlocked, ]) -
-
- {{ $delivery['name'] }} - {{ $delivery['sale_channel']['name'] }}
-

{{ $delivery['description'] }}

+
+
+ {{ $saleChannel['name'] }}
+

{!! $saleChannel['description'] ?? '' !!}

+ @if ($check) +
+ @php $missingCount = $check['full_count'] ?? count($check['names']); @endphp + @if ($cartCount > 0 && $missingCount >= $cartCount) + {{ __('shop.sale_channels.missing_offers_all') }} + @else + {{ trans_choice('shop.sale_channels.missing_offers', $missingCount, ['count' => $missingCount]) }} +
+ @if ($missingCount > 3) + {{ implode(', ', array_slice($check['names'], 0, 3)) }}, … + @else + {{ implode(', ', $check['names']) }} + @endif + @endif +
+ ⚠️ + {{ __('shop.sale_channels.cannot_select_with_cart') }} +
+
+ @endif +
+
@endforeach @push('js') @endpush diff --git a/resources/views/Shop/Customers/partials/sale.blade.php b/resources/views/Shop/Customers/partials/sale.blade.php index fc988f0b..783bd222 100644 --- a/resources/views/Shop/Customers/partials/sale.blade.php +++ b/resources/views/Shop/Customers/partials/sale.blade.php @@ -1,35 +1,69 @@ - +@php + $saleChannels = $sale_channels ?? []; +@endphp -
-
- - @include('Shop.Customers.partials.deliveries') - +@if (count($saleChannels) > 1) + + +
+
+ + @include('Shop.Customers.partials.deliveries') + +
+
+ + @include('Shop.Orders.partials.list', [ + 'dataTable' => $orders, + ]) + +
+
+ + @include('Shop.Invoices.partials.list', [ + 'dataTable' => $invoices, + ]) + +
-
- - @include('Shop.Orders.partials.list', [ - 'dataTable' => $orders, - ]) - +@else + + +
+
+ + @include('Shop.Orders.partials.list', [ + 'dataTable' => $orders, + ]) + +
+
+ + @include('Shop.Invoices.partials.list', [ + 'dataTable' => $invoices, + ]) + +
-
- - @include('Shop.Invoices.partials.list', [ - 'dataTable' => $invoices, - ]) - -
-
+@endif diff --git a/resources/views/Shop/Orders/partials/deliveries.blade.php b/resources/views/Shop/Orders/partials/deliveries.blade.php index 7667943e..64843a51 100644 --- a/resources/views/Shop/Orders/partials/deliveries.blade.php +++ b/resources/views/Shop/Orders/partials/deliveries.blade.php @@ -1,10 +1,20 @@ +@php + $defaultSaleChannelId = $customer['default_sale_channel_id'] ?? null; + $preselectedDeliveryId = old('delivery_id'); + + if (! $preselectedDeliveryId && $defaultSaleChannelId) { + $match = collect($deliveries)->firstWhere('sale_channel_id', $defaultSaleChannelId); + $preselectedDeliveryId = $match['id'] ?? null; + } +@endphp + @foreach ($deliveries as $delivery)
@include('components.form.radios.icheck', [ 'name' => 'delivery_id', 'val' => $delivery['id'], - 'value' => (int) old('delivery_id') === $delivery['id'] ? $delivery['id'] : null, + 'value' => $preselectedDeliveryId, 'id' => 'delivery_' . $delivery['id'], 'class' => 'delivery_mode' . ($delivery['at_house'] ? ' at_house' : ''), ]) diff --git a/resources/views/Shop/layout/layout.blade.php b/resources/views/Shop/layout/layout.blade.php index 85c35bc8..9a289881 100644 --- a/resources/views/Shop/layout/layout.blade.php +++ b/resources/views/Shop/layout/layout.blade.php @@ -17,6 +17,7 @@ + @stack('styles') @stack('css')