diff --git a/src/controllers/ShippingCategoriesController.php b/src/controllers/ShippingCategoriesController.php index 0d27d8d158..00cee0963c 100644 --- a/src/controllers/ShippingCategoriesController.php +++ b/src/controllers/ShippingCategoriesController.php @@ -123,11 +123,16 @@ public function actionSave(): ?Response $shippingCategory->default = (bool)$this->request->getBodyParam('default'); // Set the new product types - $postedProductTypes = $this->request->getBodyParam('productTypes', []) ?: []; - $productTypes = []; - foreach ($postedProductTypes as $productTypeId) { - if ($productTypeId && $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($productTypeId)) { - $productTypes[] = $productType; + // If this is the default category, it should be available to all product types + if ($shippingCategory->default) { + $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); + } else { + $postedProductTypes = $this->request->getBodyParam('productTypes', []) ?: []; + $productTypes = []; + foreach ($postedProductTypes as $productTypeId) { + if ($productTypeId && $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($productTypeId)) { + $productTypes[] = $productType; + } } } $shippingCategory->setProductTypes($productTypes); diff --git a/src/services/ShippingCategories.php b/src/services/ShippingCategories.php index ff0282aaf4..4bf9481f0f 100644 --- a/src/services/ShippingCategories.php +++ b/src/services/ShippingCategories.php @@ -200,6 +200,42 @@ public function saveShippingCategory(ShippingCategory $shippingCategory, bool $r // Newly set product types this shipping category is available to $newProductTypeIds = ArrayHelper::getColumn($shippingCategory->getProductTypes(), 'id'); + // Find product types that are being removed from this shipping category + $removedProductTypeIds = array_diff($currentProductTypeIds, $newProductTypeIds); + + // Update purchasables to default shipping category when product types are removed + if (!empty($removedProductTypeIds)) { + $defaultShippingCategory = $this->getDefaultShippingCategory($shippingCategory->storeId); + + // Get all variant purchasables that currently have this shipping category but whose product type is being removed + $purchasableIds = (new Query()) + ->select(['ps.purchasableId']) + ->from(['ps' => Table::PURCHASABLES_STORES]) + ->innerJoin(['v' => Table::VARIANTS], '[[ps.purchasableId]] = [[v.id]]') + ->innerJoin(['p' => Table::PRODUCTS], '[[v.primaryOwnerId]] = [[p.id]]') + ->where([ + 'ps.shippingCategoryId' => $shippingCategory->id, + 'ps.storeId' => $shippingCategory->storeId, + 'p.typeId' => $removedProductTypeIds, + ]) + ->column(); + + if (!empty($purchasableIds)) { + // Update these purchasables to use the default shipping category + Craft::$app->getDb()->createCommand() + ->update( + Table::PURCHASABLES_STORES, + ['shippingCategoryId' => $defaultShippingCategory->id], + [ + 'purchasableId' => $purchasableIds, + 'storeId' => $shippingCategory->storeId, + 'shippingCategoryId' => $shippingCategory->id, + ] + ) + ->execute(); + } + } + foreach ($currentProductTypeIds as $oldProductTypeId) { // If we are removing a product type for this shipping category the products of that type should be re-saved if (!in_array($oldProductTypeId, $newProductTypeIds, false)) { diff --git a/src/templates/store-management/shipping/shippingcategories/_fields.twig b/src/templates/store-management/shipping/shippingcategories/_fields.twig index c0b6c8bce5..be92f16b7b 100644 --- a/src/templates/store-management/shipping/shippingcategories/_fields.twig +++ b/src/templates/store-management/shipping/shippingcategories/_fields.twig @@ -43,17 +43,27 @@ {% set warning = "" %} {% endif %} +{% set isDefault = shippingCategory is defined and shippingCategory.default %} +{% set productTypeValues = isDefault ? productTypesOptions|keys : (shippingCategory is defined ? shippingCategory.productTypeIds : []) %} + {{ forms.checkboxSelectField({ label: "Available to Product Types"|t('commerce'), - instructions: "Which product types should this category be available to?"|t('commerce'), + instructions: isDefault ? "The default shipping category is automatically available to all product types."|t('commerce') : "Which product types should this category be available to?"|t('commerce'), warning: warning, id: 'productTypes', name: 'productTypes', options: productTypesOptions, - values: shippingCategory is defined ? shippingCategory.productTypeIds : [], + values: productTypeValues, showAllOption: false, + disabled: isDefault, }) }} +{% if isDefault %} + {% for productTypeId in productTypesOptions|keys %} + {{ hiddenInput('productTypes[]', productTypeId) }} + {% endfor %} +{% endif %} + {% set defaultInput %} {{ forms.lightswitchField({ instructions: "This category will be used as the default for all purchasables in this store."|t('commerce'), @@ -80,3 +90,48 @@ new Craft.HandleGenerator('#{{ nameId }}', '#{{ handleId }}'); {% endjs %} {% endif %} + +{% set defaultSwitchId = 'default'|namespaceInputId|e('js') %} +{% set productTypesId = 'productTypes'|namespaceInputId|e('js') %} +{% js %} + (function() { + const defaultSwitch = document.getElementById('{{ defaultSwitchId }}'); + const productTypesContainer = document.getElementById('{{ productTypesId }}'); + + if (!defaultSwitch || !productTypesContainer) return; + + function updateProductTypes() { + const isDefault = defaultSwitch.classList.contains('on'); + const checkboxes = productTypesContainer.querySelectorAll('input[type="checkbox"]'); + + checkboxes.forEach(checkbox => { + if (isDefault) { + checkbox.checked = true; + checkbox.disabled = true; + } else { + checkbox.disabled = false; + } + }); + + // Update hidden inputs + const existingHiddenInputs = productTypesContainer.parentElement.querySelectorAll('input[type="hidden"][name="productTypes[]"]'); + existingHiddenInputs.forEach(input => input.remove()); + + if (isDefault) { + checkboxes.forEach(checkbox => { + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = 'productTypes[]'; + hiddenInput.value = checkbox.value; + productTypesContainer.parentElement.appendChild(hiddenInput); + }); + } + } + + // Listen for lightswitch changes + defaultSwitch.addEventListener('change', updateProductTypes); + + // Also listen for the custom lightswitch event + $(defaultSwitch).on('change', updateProductTypes); + })(); +{% endjs %}