diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d164847..2e98458 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(mkdir:*)", "Bash(composer init:*)", "Bash(composer install:*)", - "Bash(composer update:*)" + "Bash(composer update:*)", + "Bash(git add:*)" ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index b9bf41c..85109c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to WooCommerce Tier and Package Prices will be documented in The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2025-12-21 + +### Added +- Package quantity restriction feature +- Global setting to restrict quantities to defined package sizes +- Per-product setting to restrict quantities to defined package sizes +- Frontend validation preventing non-package quantities +- Server-side cart validation for package quantities +- User-friendly error messages showing available package sizes +- Automatic quantity field hiding when restriction is enabled +- Package selection UI with highlighted states + +### Changed +- Enhanced package pricing display template with restriction mode support +- Improved JavaScript to handle restricted mode package selection +- Updated frontend to show "Choose a package size below" notice in restricted mode + +### Technical +- Added `validate_package_quantity()` method in WC_TPP_Cart class +- Added `maybe_hide_quantity_input()` method in WC_TPP_Frontend class +- Extended `woocommerce_add_to_cart_validation` filter hook +- Added `wc-tpp-restricted-mode` CSS class for styling +- New product meta: `_wc_tpp_restrict_to_packages` +- New global option: `wc_tpp_restrict_package_quantities` + +### Translations +- Added 7 new translatable strings +- Updated all translations (en_US, de_DE, de_CH_informal) +- Compiled all .mo files with new strings + ## [1.0.2] - 2025-12-21 ### Changed diff --git a/assets/js/frontend.js b/assets/js/frontend.js index 06492c2..4da8b16 100644 --- a/assets/js/frontend.js +++ b/assets/js/frontend.js @@ -8,8 +8,9 @@ $(document).ready(function() { const $quantityInput = $('input.qty'); const $priceDisplay = $('.woocommerce-Price-amount.amount').first(); + const isRestrictedMode = $('.wc-tpp-package-pricing-table').hasClass('wc-tpp-restricted-mode'); - if ($quantityInput.length === 0) { + if ($quantityInput.length === 0 && !isRestrictedMode) { return; } @@ -164,7 +165,30 @@ const $package = $(this).closest('.wc-tpp-package'); const qty = parseInt($package.data('qty')); - $quantityInput.val(qty).trigger('change'); + if (isRestrictedMode) { + // In restricted mode, we need to set a hidden input or use data attribute + // since the quantity field is hidden + if ($quantityInput.length === 0) { + // Create a hidden quantity input if it doesn't exist + if ($('.qty-hidden-input').length === 0) { + $('.single_add_to_cart_button').before(''); + } + $('.qty-hidden-input').val(qty); + } else { + $quantityInput.val(qty); + } + + // Highlight selected package + $('.wc-tpp-package').removeClass('wc-tpp-selected'); + $package.addClass('wc-tpp-selected'); + + // Update price display + const price = parseFloat($package.data('price')); + const unitPrice = price / qty; + updatePrice(unitPrice, 'Package price: ' + formatPrice(price) + ' total'); + } else { + $quantityInput.val(qty).trigger('change'); + } // Scroll to add to cart button $('html, body').animate({ @@ -172,8 +196,22 @@ }, 500); }); + // In restricted mode, prevent form submission if no package is selected + if (isRestrictedMode) { + $('form.cart').on('submit', function(e) { + const hasSelection = $('.wc-tpp-package.wc-tpp-selected').length > 0; + if (!hasSelection) { + e.preventDefault(); + alert('Please select a package size before adding to cart.'); + return false; + } + }); + } + // Initial update - updatePriceDisplay(); + if (!isRestrictedMode) { + updatePriceDisplay(); + } }); })(jQuery); diff --git a/composer.json b/composer.json index 63f3cf7..ac92b86 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "magdev/wc-tier-package-prices", "description": "WooCommerce plugin for tier pricing and package prices with Twig templates", - "version": "1.0.2", + "version": "1.1.0", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "authors": [ diff --git a/includes/class-wc-tpp-cart.php b/includes/class-wc-tpp-cart.php index 5d93a3a..ffeaf59 100644 --- a/includes/class-wc-tpp-cart.php +++ b/includes/class-wc-tpp-cart.php @@ -13,6 +13,7 @@ class WC_TPP_Cart { add_action('woocommerce_before_calculate_totals', array($this, 'apply_tier_package_pricing'), 10, 1); add_filter('woocommerce_cart_item_price', array($this, 'display_cart_item_price'), 10, 3); add_filter('woocommerce_cart_item_subtotal', array($this, 'display_cart_item_subtotal'), 10, 3); + add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_package_quantity'), 10, 3); } public function apply_tier_package_pricing($cart) { @@ -89,6 +90,53 @@ class WC_TPP_Cart { } return $subtotal; } + + public function validate_package_quantity($passed, $product_id, $quantity) { + // Check if restriction is enabled globally or for this product + $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; + $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; + + if (!$global_restrict && !$product_restrict) { + return $passed; + } + + // Get packages for this product + $packages = get_post_meta($product_id, '_wc_tpp_packages', true); + + if (empty($packages) || !is_array($packages)) { + return $passed; + } + + // Check if the quantity matches any package + $valid_quantity = false; + $available_quantities = array(); + + foreach ($packages as $package) { + $available_quantities[] = $package['qty']; + if ($quantity == $package['qty']) { + $valid_quantity = true; + break; + } + } + + if (!$valid_quantity) { + $product = wc_get_product($product_id); + $product_name = $product ? $product->get_name() : __('this product', 'wc-tier-package-prices'); + + wc_add_notice( + sprintf( + __('The quantity %1$d is not available for %2$s. Please choose from the available package sizes: %3$s', 'wc-tier-package-prices'), + $quantity, + $product_name, + implode(', ', $available_quantities) + ), + 'error' + ); + return false; + } + + return $passed; + } } new WC_TPP_Cart(); diff --git a/includes/class-wc-tpp-frontend.php b/includes/class-wc-tpp-frontend.php index f906d09..e80bfe1 100644 --- a/includes/class-wc-tpp-frontend.php +++ b/includes/class-wc-tpp-frontend.php @@ -14,6 +14,7 @@ class WC_TPP_Frontend { add_action('woocommerce_before_add_to_cart_button', array($this, 'display_pricing_table_before'), 20); add_action('woocommerce_after_add_to_cart_button', array($this, 'display_pricing_table_after'), 10); add_action('woocommerce_single_product_summary', array($this, 'display_pricing_table_after_price'), 15); + add_action('woocommerce_before_add_to_cart_quantity', array($this, 'maybe_hide_quantity_input')); } public function enqueue_scripts() { @@ -50,6 +51,24 @@ class WC_TPP_Frontend { } } + public function maybe_hide_quantity_input() { + global $product; + + if (!$product || !is_a($product, 'WC_Product')) { + return; + } + + $product_id = $product->get_id(); + $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; + $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; + $packages = get_post_meta($product_id, '_wc_tpp_packages', true); + + // Hide quantity input if restriction is enabled and packages exist + if (($global_restrict || $product_restrict) && !empty($packages)) { + echo ''; + } + } + public function display_pricing_table() { global $product; @@ -60,6 +79,8 @@ class WC_TPP_Frontend { $product_id = $product->get_id(); $tiers = get_post_meta($product_id, '_wc_tpp_tiers', true); $packages = get_post_meta($product_id, '_wc_tpp_packages', true); + $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; + $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; if (empty($tiers) && empty($packages)) { return; @@ -68,7 +89,8 @@ class WC_TPP_Frontend { WC_TPP_Template_Loader::get_instance()->display('frontend/pricing-table.twig', array( 'product' => $product, 'tiers' => $tiers, - 'packages' => $packages + 'packages' => $packages, + 'restrict_to_packages' => $global_restrict || $product_restrict )); } diff --git a/includes/class-wc-tpp-product-meta.php b/includes/class-wc-tpp-product-meta.php index 2e7e004..57a86b2 100644 --- a/includes/class-wc-tpp-product-meta.php +++ b/includes/class-wc-tpp-product-meta.php @@ -69,6 +69,15 @@ class WC_TPP_Product_Meta {
+ + '_wc_tpp_restrict_to_packages', + 'label' => __('Restrict to Package Quantities', 'wc-tier-package-prices'), + 'description' => __('Only allow quantities defined in packages above', 'wc-tier-package-prices'), + 'desc_tip' => true, + )); + ?>