Plugin Directory

Changeset 3494875


Ignore:
Timestamp:
03/30/2026 06:55:58 PM (7 months ago)
Author:
nayden4o
Message:

Prepare release 1.1.5

Location:
nc-custom-product-bundles-for-woocommerce/trunk
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • nc-custom-product-bundles-for-woocommerce/trunk/assets/front.css

    r3490389 r3494875  
    9292  font-weight: 600;
    9393  color: #213047;
     94}
     95
     96.fpbfw-component-badge {
     97  display: inline-flex;
     98  width: fit-content;
     99  padding: 2px 8px;
     100  border-radius: 999px;
     101  font-size: 0.75rem;
     102  font-weight: 700;
     103  color: #0f4c81;
     104  background: #e7f2fb;
     105}
     106
     107.fpbfw-component-variation-wrap {
     108  display: block;
     109}
     110
     111.fpbfw-variation-select {
     112  width: 100%;
     113  max-width: 420px;
     114  min-height: 40px;
     115  padding: 8px 10px;
     116  border: 1px solid #d2d8e0;
     117  border-radius: 10px;
     118  background: #fff;
     119  color: #26374f;
    94120}
    95121
  • nc-custom-product-bundles-for-woocommerce/trunk/assets/front.js

    r3490389 r3494875  
    4646
    4747    return parsePriceFloat($card.find('.fpbfw-component-price').text());
     48  }
     49
     50  function syncVariationCard($card){
     51    var $select = $card.find('.fpbfw-variation-select');
     52    var $input = $card.find('input[name="FPBFW_components_selected[]"]').first();
     53
     54    if (!$select.length || !$input.length) {
     55      return;
     56    }
     57
     58    var $option = $select.find('option:selected');
     59    var variationId = $option.val() || '';
     60    var price = parsePriceFloat($option.data('price') || 0);
     61
     62    if (variationId) {
     63      $input.val(variationId);
     64    }
     65
     66    $input.prop('checked', true);
     67
     68    $card.attr('data-price', price);
     69    $card.data('price', price);
     70    $card.find('.fpbfw-component-price').html('<span class="woocommerce-Price-amount amount">' + formatPrice(price) + '</span>');
    4871  }
    4972
    … …  
    161184  }
    162185
    163   $(document).on('change', 'input[name="FPBFW_components_selected[]"]', recalc);
    164   $(document).ready(recalc);
     186  $(document)
     187    .on('change', 'input[name="FPBFW_components_selected[]"]', recalc)
     188    .on('change', '.fpbfw-variation-select', function(e){
     189      syncVariationCard($(this).closest('.fpbfw-component-card'));
     190      recalc();
     191    })
     192    .on('click', '.fpbfw-component-card', function(e){
     193      var $target = $(e.target);
     194      if ($target.closest('a, button, select, option, input').length) {
     195        return;
     196      }
     197
     198      var $input = $(this).find('input[name="FPBFW_components_selected[]"]').first();
     199      if (!$input.length) {
     200        return;
     201      }
     202
     203      $input.prop('checked', !$input.prop('checked')).trigger('change');
     204    })
     205    .on('click mousedown', '.fpbfw-variation-select, .fpbfw-component-link', function(e){
     206      e.stopPropagation();
     207    });
     208
     209  $(document).ready(function(){
     210    $('.fpbfw-component-card--variable').each(function(){
     211      syncVariationCard($(this));
     212    });
     213    recalc();
     214  });
    165215})(jQuery);
  • nc-custom-product-bundles-for-woocommerce/trunk/includes/class-fpbfw-admin.php

    r3491273 r3494875  
    660660
    661661        echo '</select>';
    662         echo '<span class="description fpbfw-help">' . esc_html__('Selecciona los productos que el cliente podrá incluir en el bundle.', 'nc-custom-product-bundles-for-woocommerce') . '</span>';
     662        echo '<span class="description fpbfw-help">' . esc_html__('Selecciona productos simples, variables o variaciones concretas. Para bundles fijos, lo recomendable es usar variaciones concretas.', 'nc-custom-product-bundles-for-woocommerce') . '</span>';
    663663        echo '</p>';
    664664
  • nc-custom-product-bundles-for-woocommerce/trunk/includes/class-fpbfw-product-type-bundle.php

    r3490389 r3494875  
    104104    }
    105105
    106     protected static function get_allowed_component_ids($product_id)
     106    protected static function get_allowed_component_ids($product_id, $expand_variable_children = false)
    107107    {
    108108        $allowed = (array) get_post_meta($product_id, '_FPBFW_components', true);
    109         return array_values(array_unique(array_filter(array_map('intval', $allowed))));
     109        $allowed = array_values(array_unique(array_filter(array_map('intval', $allowed))));
     110
     111        if (!$expand_variable_children) {
     112            return $allowed;
     113        }
     114
     115        $expanded = [];
     116
     117        foreach ($allowed as $component_id) {
     118            $expanded[] = $component_id;
     119
     120            $component = wc_get_product($component_id);
     121            if (!$component || !$component->is_type('variable')) {
     122                continue;
     123            }
     124
     125            foreach ((array) $component->get_children() as $variation_id) {
     126                $variation_id = (int) $variation_id;
     127                if ($variation_id > 0) {
     128                    $expanded[] = $variation_id;
     129                }
     130            }
     131        }
     132
     133        return array_values(array_unique(array_filter(array_map('intval', $expanded))));
    110134    }
    111135
    … …  
    114138        $selected = isset($_REQUEST['FPBFW_components_selected']) ? (array) wp_unslash($_REQUEST['FPBFW_components_selected']) : [];
    115139        $selected = array_values(array_unique(array_filter(array_map('intval', $selected))));
    116         $allowed = self::get_allowed_component_ids($product_id);
     140        $allowed = self::get_allowed_component_ids($product_id, true);
    117141
    118142        if (empty($selected) || empty($allowed)) {
  • nc-custom-product-bundles-for-woocommerce/trunk/includes/class-fpbfw-templates.php

    r3491221 r3494875  
    5252        }
    5353
    54         protected static function get_components_for( $product_id ) {
    55                 $ids = (array) get_post_meta( $product_id, '_FPBFW_components', true );
    56                 return array_filter( array_map( 'wc_get_product', $ids ) );
     54        protected static function get_components_for( $product_id, $expand_variable_children = false ) {
     55                $ids        = (array) get_post_meta( $product_id, '_FPBFW_components', true );
     56                $ids        = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
     57                $products   = [];
     58                $seen_ids   = [];
     59
     60                foreach ( $ids as $component_id ) {
     61                        $product = wc_get_product( $component_id );
     62                        if ( ! $product ) {
     63                                continue;
     64                        }
     65
     66                        if ( $expand_variable_children && $product->is_type( 'variable' ) ) {
     67                                foreach ( (array) $product->get_children() as $variation_id ) {
     68                                        $variation_id = (int) $variation_id;
     69                                        if ( isset( $seen_ids[ $variation_id ] ) ) {
     70                                                continue;
     71                                        }
     72
     73                                        $variation = wc_get_product( $variation_id );
     74                                        if ( ! $variation || ! $variation->exists() ) {
     75                                                continue;
     76                                        }
     77
     78                                        $seen_ids[ $variation_id ] = true;
     79                                        $products[]                = $variation;
     80                                }
     81
     82                                continue;
     83                        }
     84
     85                        if ( isset( $seen_ids[ $component_id ] ) ) {
     86                                continue;
     87                        }
     88
     89                        $seen_ids[ $component_id ] = true;
     90                        $products[]                = $product;
     91                }
     92
     93                return $products;
    5794        }
    5895
    … …  
    92129
    93130                return 0.0;
     131        }
     132
     133        protected static function get_component_price_bounds( $product ) {
     134                if ( ! $product instanceof WC_Product ) {
     135                        return [ 0.0, 0.0 ];
     136                }
     137
     138                if ( $product->is_type( 'variable' ) ) {
     139                        $min = (float) $product->get_variation_price( 'min', true );
     140                        $max = (float) $product->get_variation_price( 'max', true );
     141
     142                        if ( $min > 0 || $max > 0 ) {
     143                                return [ max( 0, $min ), max( 0, $max ) ];
     144                        }
     145                }
     146
     147                $amount = self::get_component_price_amount( $product );
     148                return [ $amount, $amount ];
     149        }
     150
     151        protected static function get_variable_component_options( $product ) {
     152                if ( ! $product instanceof WC_Product || ! $product->is_type( 'variable' ) ) {
     153                        return [];
     154                }
     155
     156                $options = [];
     157
     158                foreach ( (array) $product->get_children() as $variation_id ) {
     159                        $variation = wc_get_product( $variation_id );
     160                        if ( ! $variation || ! $variation->exists() ) {
     161                                continue;
     162                        }
     163
     164                        if ( ! $variation->is_purchasable() ) {
     165                                continue;
     166                        }
     167
     168                        $label = wp_strip_all_tags( wc_get_formatted_variation( $variation, true, false, false ) );
     169                        if ( '' === $label ) {
     170                                $label = $variation->get_name();
     171                        }
     172
     173                        $price = self::get_component_price_amount( $variation );
     174
     175                        $options[] = [
     176                                'id'    => (int) $variation_id,
     177                                'label' => $label,
     178                                'price' => $price,
     179                        ];
     180                }
     181
     182                return $options;
    94183        }
    95184
    … …  
    145234
    146235        protected static function calculate_optional_price_range( $components, $pricing_mode, $min_select, $rules ) {
    147                 $prices = [];
     236                $min_prices = [];
     237                $max_prices = [];
    148238
    149239                foreach ( (array) $components as $component ) {
    150                         $amount = self::get_component_price_amount( $component );
    151                         if ( $amount > 0 ) {
    152                                 $prices[] = $amount;
    153                         }
    154                 }
    155 
    156                 if ( empty( $prices ) ) {
     240                        list( $min_amount, $max_amount ) = self::get_component_price_bounds( $component );
     241
     242                        if ( $min_amount > 0 ) {
     243                                $min_prices[] = $min_amount;
     244                        }
     245
     246                        if ( $max_amount > 0 ) {
     247                                $max_prices[] = $max_amount;
     248                        }
     249                }
     250
     251                if ( empty( $min_prices ) || empty( $max_prices ) ) {
    157252                        return [ 0.0, 0.0 ];
    158253                }
    159254
    160                 sort( $prices, SORT_NUMERIC );
    161 
    162                 $min_count = max( 1, min( (int) $min_select, count( $prices ) ) );
    163                 $max_count = count( $prices );
    164 
    165                 $min_total = array_sum( array_slice( $prices, 0, $min_count ) );
    166                 $max_total = array_sum( $prices );
     255                sort( $min_prices, SORT_NUMERIC );
     256                sort( $max_prices, SORT_NUMERIC );
     257
     258                $min_count = max( 1, min( (int) $min_select, count( $min_prices ) ) );
     259                $max_count = count( $max_prices );
     260
     261                $min_total = array_sum( array_slice( $min_prices, 0, $min_count ) );
     262                $max_total = array_sum( array_slice( $max_prices, 0, $max_count ) );
    167263
    168264                if ( 'rules' === $pricing_mode ) {
    … …  
    188284                }
    189285
    190                 $components = self::get_components_for( $product->get_id() );
     286                $components = self::get_components_for( $product->get_id(), false );
    191287                if ( empty( $components ) ) {
    192288                        return;
    … …  
    216312                foreach ( $components as $comp ) {
    217313                        $component_id        = $comp->get_id();
    218                         $component_link      = get_permalink( $component_id );
     314                        $is_variable         = $comp->is_type( 'variable' );
     315                        $is_variation        = $comp->is_type( 'variation' );
     316                        $variation_options   = $is_variable ? self::get_variable_component_options( $comp ) : [];
     317                        $selected_variation  = $is_variable && ! empty( $variation_options ) ? $variation_options[0] : null;
     318                        $component_link      = $is_variation ? get_permalink( $comp->get_parent_id() ) : get_permalink( $component_id );
    219319                        $component_name      = $comp->get_name();
    220                         $component_price_html = self::get_component_price_html( $comp );
    221                         $component_price_raw = self::get_component_price_amount( $comp );
     320                        $component_price_raw = $selected_variation ? (float) $selected_variation['price'] : self::get_component_price_amount( $comp );
     321                        $component_price_html = $component_price_raw > 0 ? self::format_plain_price_html( $component_price_raw ) : '';
    222322                        $component_image     = $comp->get_image( 'woocommerce_thumbnail', [ 'loading' => 'lazy' ] );
     323                        $component_badge     = $is_variable ? '<span class="fpbfw-component-badge">' . esc_html__( 'Variable', 'nc-custom-product-bundles-for-woocommerce' ) . '</span>' : ( $is_variation ? '<span class="fpbfw-component-badge">' . esc_html__( 'Variación', 'nc-custom-product-bundles-for-woocommerce' ) . '</span>' : '' );
     324                        $selected_value      = $selected_variation ? (int) $selected_variation['id'] : (int) $component_id;
     325                        $variation_select    = '';
    223326                        $input_id            = 'fpbfw_component_' . $component_id;
    224327
     328                        if ( $is_variable && ! empty( $variation_options ) ) {
     329                                $variation_select = '<span class="fpbfw-component-variation-wrap"><select class="fpbfw-variation-select" aria-label="' . esc_attr__( 'Elige una variación', 'nc-custom-product-bundles-for-woocommerce' ) . '">';
     330
     331                                foreach ( $variation_options as $index => $variation_option ) {
     332                                        $variation_select .= sprintf(
     333                                                '<option value="%1$d" data-price="%2$s"%3$s>%4$s</option>',
     334                                                (int) $variation_option['id'],
     335                                                esc_attr( wc_format_decimal( $variation_option['price'] ) ),
     336                                                selected( 0, $index, false ),
     337                                                esc_html( $variation_option['label'] )
     338                                        );
     339                                }
     340
     341                                $variation_select .= '</select></span>';
     342                        }
     343
    225344                        printf(
    226                                 '<label class="fpbfw-component-card" for="%1$s" data-price="%2$s">
     345                                '<div class="fpbfw-component-card%10$s" data-price="%2$s">
    227346                                        <span class="fpbfw-component-check">
    228                                                 <input id="%1$s" type="checkbox" name="FPBFW_components_selected[]" value="%3$d" />
     347                                                <input id="%1$s" type="checkbox" name="FPBFW_components_selected[]" value="%3$d" aria-label="%5$s" />
    229348                                        </span>
    230349                                        <span class="fpbfw-component-media">%4$s</span>
    231350                                        <span class="fpbfw-component-body">
    232351                                                <span class="fpbfw-component-title">%5$s</span>
    233                                                 <span class="fpbfw-component-price">%6$s</span>
     352                                                %6$s
     353                                                %7$s
    234354                                                <span class="fpbfw-component-actions">
    235                                                         <a class="fpbfw-component-link" href="%7$s" target="_blank" rel="noopener">%8$s</a>
     355                                                        <a class="fpbfw-component-link" href="%8$s" target="_blank" rel="noopener">%9$s</a>
    236356                                                </span>
    237357                                        </span>
    238                                 </label>',
     358                                </div>',
    239359                                esc_attr( $input_id ),
    240360                                esc_attr( wc_format_decimal( $component_price_raw ) ),
    241                                 (int) $component_id,
     361                                $selected_value,
    242362                                $component_image,
    243363                                esc_html( $component_name ),
    244                                 $component_price_html ? wp_kses_post( $component_price_html ) : '',
     364                                $component_badge,
     365                                ( $variation_select ? $variation_select : '' ) . '<span class="fpbfw-component-price">' . ( $component_price_html ? wp_kses_post( $component_price_html ) : '' ) . '</span>',
    245366                                esc_url( $component_link ),
    246                                 esc_html__( 'Ver producto', 'nc-custom-product-bundles-for-woocommerce' )
     367                                esc_html__( 'Ver producto', 'nc-custom-product-bundles-for-woocommerce' ),
     368                                $is_variable ? ' fpbfw-component-card--variable' : ''
    247369                        );
    248370                }
    … …  
    277399                foreach ( $components as $comp ) {
    278400                        $component_id = $comp->get_id();
    279                         $link         = get_permalink( $component_id );
     401                        $link         = $comp->is_type( 'variation' ) ? get_permalink( $comp->get_parent_id() ) : get_permalink( $component_id );
    280402                        $image        = $comp->get_image( 'woocommerce_thumbnail', [ 'loading' => 'lazy' ] );
    281403                        $name         = esc_html( $comp->get_name() );
  • nc-custom-product-bundles-for-woocommerce/trunk/languages/nc-custom-product-bundles-for-woocommerce-en_US.po

    r3491273 r3494875  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: NC Custom Product Bundles for WooCommerce 1.1.4\n"
     3"Project-Id-Version: NC Custom Product Bundles for WooCommerce 1.1.5\n"
    44"POT-Creation-Date: 2026-03-24 00:00+0000\n"
    55"PO-Revision-Date: 2026-03-24 00:00+0000\n"
  • nc-custom-product-bundles-for-woocommerce/trunk/languages/nc-custom-product-bundles-for-woocommerce.pot

    r3491273 r3494875  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: NC Custom Product Bundles for WooCommerce 1.1.4\n"
     3"Project-Id-Version: NC Custom Product Bundles for WooCommerce 1.1.5\n"
    44"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/nc-custom-product-bundles-for-woocommerce\n"
    55"POT-Creation-Date: 2026-03-24 00:00+0000\n"
  • nc-custom-product-bundles-for-woocommerce/trunk/nc-custom-product-bundles-for-woocommerce.php

    r3491273 r3494875  
    33 * Plugin Name: NC Custom Product Bundles for WooCommerce
    44 * Description: Create WooCommerce bundles with fixed or optional components, dynamic pricing, quantity discount rules, visual front-end selection, CSV import/export, and translation-ready support.
    5  * Version: 1.1.4
     5 * Version: 1.1.5
    66 * Author: Nayden4o
    77 * License: GPL-2.0-or-later
    … …  
    2020}
    2121
    22 define( 'FPBFW_VERSION', '1.1.4' );
     22define( 'FPBFW_VERSION', '1.1.5' );
    2323define( 'FPBFW_PLUGIN_FILE', __FILE__ );
    2424define( 'FPBFW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • nc-custom-product-bundles-for-woocommerce/trunk/readme.txt

    r3491273 r3494875  
    66Requires PHP: 8.1
    77Requires Plugins: woocommerce
    8 Stable tag: 1.1.4
     8Stable tag: 1.1.5
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6969[Kit Tuning V.3 Volkswagen Golf R Hatchback Mk8 Facelift](https://taller-flash.es/producto/kit-tuning-v-3-volkswagen-golf-r-hatchback-mk8-facelift)
    7070
     71== Video Demo ==
     72
     73[Watch the plugin video demo on YouTube](https://www.youtube.com/watch?v=8UuIKgSbdNs)
     74
    7175== CSV Import / Export ==
    7276
    … …  
    129133== Changelog ==
    130134
     135= 1.1.5 =
     136- Added front-end variation selection for variable products inside optional bundles.
     137- Improved optional bundle price recalculation when changing bundle variation selections.
     138- Clarified admin guidance for using concrete variations in fixed bundles.
     139
    131140= 1.1.4 =
    132141- Fixed Bundle admin interactions and restored quantity-rule row controls.
Note: See TracChangeset for help on using the changeset viewer.