@@ -428,23 +428,31 @@ private function render_bulk_pricing_group( $product_id ) {
428428 $ stored = get_post_meta ( $ product_id , $ meta_key , true );
429429 $ stored = is_array ( $ stored ) ? $ stored : array ();
430430
431- // Flatten the { tier => rows } map to { role, min_qty, price } rows.
432- $ rows = array ();
433- foreach ( $ stored as $ tier_key => $ tier_rows ) {
434- if ( ! isset ( $ tiers [ $ tier_key ] ) || ! is_array ( $ tier_rows ) ) {
431+ // Flatten the { role => rows } map, then group breaks that share the same
432+ // quantity range and price so one row can carry every role they apply to
433+ // (a break is stored once per role; multi-role rows fan out on save).
434+ $ groups = array ();
435+ foreach ( $ stored as $ role_key => $ role_rows ) {
436+ if ( ! is_array ( $ role_rows ) ) {
435437 continue ;
436438 }
437- foreach ( $ tier_rows as $ row ) {
439+ foreach ( $ role_rows as $ row ) {
438440 if ( ! is_array ( $ row ) || ! isset ( $ row ['min_qty ' ], $ row ['price ' ] ) ) {
439441 continue ;
440442 }
441- $ max = isset ( $ row ['max_qty ' ] ) ? (int ) $ row ['max_qty ' ] : 0 ;
442- $ rows [] = array (
443- 'role ' => $ tier_key ,
444- 'min_qty ' => (string ) (int ) $ row ['min_qty ' ],
445- 'max_qty ' => $ max > 0 ? (string ) $ max : '' ,
446- 'price ' => (string ) $ row ['price ' ],
447- );
443+ $ max = isset ( $ row ['max_qty ' ] ) && (int ) $ row ['max_qty ' ] > 0 ? (string ) (int ) $ row ['max_qty ' ] : '' ;
444+ $ min = (string ) (int ) $ row ['min_qty ' ];
445+ $ price = (string ) $ row ['price ' ];
446+ $ sig = $ min . '| ' . $ max . '| ' . $ price ;
447+ if ( ! isset ( $ groups [ $ sig ] ) ) {
448+ $ groups [ $ sig ] = array (
449+ 'roles ' => array (),
450+ 'min_qty ' => $ min ,
451+ 'max_qty ' => $ max ,
452+ 'price ' => $ price ,
453+ );
454+ }
455+ $ groups [ $ sig ]['roles ' ][] = (string ) $ role_key ;
448456 }
449457 }
450458 ?>
@@ -455,14 +463,14 @@ private function render_bulk_pricing_group( $product_id ) {
455463 <div data-repeater-list>
456464 <?php
457465 $ index = 0 ;
458- foreach ( $ rows as $ row ) {
459- $ this ->render_bulk_row ( (string ) $ index , $ row [ ' role ' ], $ row ['min_qty ' ], $ row ['max_qty ' ], $ row ['price ' ] );
466+ foreach ( $ groups as $ group ) {
467+ $ this ->render_bulk_row ( (string ) $ index , $ group [ ' roles ' ], $ group ['min_qty ' ], $ group ['max_qty ' ], $ group ['price ' ] );
460468 $ index ++;
461469 }
462470 ?>
463471 </div>
464472 <template data-repeater-template>
465- <?php $ this ->render_bulk_row ( '__INDEX__ ' , '' , '' , '' , '' ); ?>
473+ <?php $ this ->render_bulk_row ( '__INDEX__ ' , array () , '' , '' , '' ); ?>
466474 </template>
467475 <p class="form-field">
468476 <button type="button" class="button" data-repeater-add><?php esc_html_e ( 'Add quantity break ' , 'wc-pricebook ' ); ?> </button>
@@ -488,7 +496,7 @@ private function render_bulk_pricing_group( $product_id ) {
488496 * Any stored value not otherwise present is appended so an existing row never
489497 * silently changes on save.
490498 *
491- * @param string $selected Currently stored value for the row.
499+ * @param string|array<int,string> $selected Currently stored value(s) for the row.
492500 * @return array<string,string> value => label.
493501 */
494502 private function bulk_target_options ( $ selected = '' ) {
@@ -498,8 +506,11 @@ private function bulk_target_options( $selected = '' ) {
498506 $ options [ $ key ] = $ label ;
499507 }
500508 }
501- if ( '' !== (string ) $ selected && ! isset ( $ options [ (string ) $ selected ] ) ) {
502- $ options [ (string ) $ selected ] = (string ) $ selected ;
509+ foreach ( (array ) $ selected as $ sel ) {
510+ $ sel = (string ) $ sel ;
511+ if ( '' !== $ sel && ! isset ( $ options [ $ sel ] ) ) {
512+ $ options [ $ sel ] = $ sel ;
513+ }
503514 }
504515 return $ options ;
505516 }
@@ -519,19 +530,19 @@ private function is_valid_bulk_target( $key, array $tiers ) {
519530 return function_exists ( 'wp_roles ' ) && wp_roles ()->is_role ( $ key );
520531 }
521532
522- private function render_bulk_row ( $ index , $ role , $ min_qty , $ max_qty , $ price ) {
523- $ base = 'pricebook_bulk_price[ ' . $ index . '] ' ;
533+ private function render_bulk_row ( $ index , array $ roles , $ min_qty , $ max_qty , $ price ) {
534+ $ base = 'pricebook_bulk_price[ ' . $ index . '] ' ;
535+ $ roles = array_map ( 'strval ' , $ roles );
524536 ?>
525537 <div class="wc-pricebook-repeater__item" data-repeater-item>
526538 <?php $ this ->card_header ( __ ( 'Remove quantity break ' , 'wc-pricebook ' ) ); ?>
527539 <div class="wc-pricebook-repeater__body">
528540 <div class="wc-pricebook-repeater__grid">
529541 <div class="wc-pricebook-field wc-pricebook-field--full">
530- <label><?php esc_html_e ( 'Role ' , 'wc-pricebook ' ); ?> </label>
531- <select name="<?php echo esc_attr ( $ base . '[role] ' ); ?> ">
532- <option value=""><?php esc_html_e ( '— Select — ' , 'wc-pricebook ' ); ?> </option>
533- <?php foreach ( $ this ->bulk_target_options ( $ role ) as $ key => $ label ) : ?>
534- <option value="<?php echo esc_attr ( $ key ); ?> " <?php selected ( $ key , $ role ); ?> ><?php echo esc_html ( $ label ); ?> </option>
542+ <label><?php esc_html_e ( 'Roles ' , 'wc-pricebook ' ); ?> </label>
543+ <select multiple class="wc-enhanced-select" name="<?php echo esc_attr ( $ base . '[role][] ' ); ?> " style="width:100%;" data-placeholder="<?php esc_attr_e ( 'Select roles… ' , 'wc-pricebook ' ); ?> ">
544+ <?php foreach ( $ this ->bulk_target_options ( $ roles ) as $ key => $ label ) : ?>
545+ <option value="<?php echo esc_attr ( $ key ); ?> " <?php selected ( in_array ( (string ) $ key , $ roles , true ) ); ?> ><?php echo esc_html ( $ label ); ?> </option>
535546 <?php endforeach ; ?>
536547 </select>
537548 </div>
@@ -570,25 +581,44 @@ private function render_user_pricing_group( $product_id ) {
570581
571582 $ rows = get_post_meta ( $ product_id , $ meta_key , true );
572583 $ rows = is_array ( $ rows ) ? $ rows : array ();
584+
585+ // Group stored { user-id, price } entries by price so a single row can carry
586+ // every customer that shares it (a price is stored once per customer; multi-
587+ // customer rows fan out on save).
588+ $ groups = array ();
589+ foreach ( $ rows as $ row ) {
590+ if ( ! is_array ( $ row ) || ! isset ( $ row ['user-id ' ] ) ) {
591+ continue ;
592+ }
593+ $ uid = (int ) $ row ['user-id ' ];
594+ if ( $ uid <= 0 ) {
595+ continue ;
596+ }
597+ $ price = isset ( $ row ['price ' ] ) ? (string ) $ row ['price ' ] : '' ;
598+ if ( ! isset ( $ groups [ $ price ] ) ) {
599+ $ groups [ $ price ] = array (
600+ 'users ' => array (),
601+ 'price ' => $ price ,
602+ );
603+ }
604+ $ groups [ $ price ]['users ' ][] = $ uid ;
605+ }
573606 ?>
574607 <div class="options_group">
575608 <p class="form-field wc-pricebook-group-title"><strong><?php esc_html_e ( 'Customer-specific prices ' , 'wc-pricebook ' ); ?> </strong></p>
576- <p class="form-field"><?php esc_html_e ( 'A price set here for a customer overrides every tier and role price for that customer . ' , 'wc-pricebook ' ); ?> </p>
609+ <p class="form-field"><?php esc_html_e ( 'A price set here overrides every tier and role price for the selected customers . ' , 'wc-pricebook ' ); ?> </p>
577610 <div class="wc-pricebook-user-prices" data-repeater data-repeater-kind="user-price" data-currency="<?php echo esc_attr ( $ this ->currency_symbol () ); ?> ">
578611 <div data-repeater-list>
579612 <?php
580613 $ index = 0 ;
581- foreach ( $ rows as $ row ) {
582- if ( ! is_array ( $ row ) || ! isset ( $ row ['user-id ' ] ) ) {
583- continue ;
584- }
585- $ this ->render_user_row ( (string ) $ index , (int ) $ row ['user-id ' ], isset ( $ row ['price ' ] ) ? (string ) $ row ['price ' ] : '' );
614+ foreach ( $ groups as $ group ) {
615+ $ this ->render_user_row ( (string ) $ index , $ group ['users ' ], $ group ['price ' ] );
586616 $ index ++;
587617 }
588618 ?>
589619 </div>
590620 <template data-repeater-template>
591- <?php $ this ->render_user_row ( '__INDEX__ ' , 0 , '' ); ?>
621+ <?php $ this ->render_user_row ( '__INDEX__ ' , array () , '' ); ?>
592622 </template>
593623 <p class="form-field">
594624 <button type="button" class="button" data-repeater-add><?php esc_html_e ( 'Add customer price ' , 'wc-pricebook ' ); ?> </button>
@@ -606,27 +636,33 @@ private function render_user_pricing_group( $product_id ) {
606636 * @param string $price Price value.
607637 * @return void
608638 */
609- private function render_user_row ( $ index , $ user_id , $ price ) {
639+ private function render_user_row ( $ index , array $ user_ids , $ price ) {
610640 $ base = 'pricebook_user_price[ ' . $ index . '] ' ;
611- $ user = $ user_id > 0 ? get_userdata ( $ user_id ) : false ;
612641 ?>
613642 <div class="wc-pricebook-repeater__item" data-repeater-item>
614643 <?php $ this ->card_header ( __ ( 'Remove customer price ' , 'wc-pricebook ' ) ); ?>
615644 <div class="wc-pricebook-repeater__body">
616645 <div class="wc-pricebook-repeater__grid">
617646 <div class="wc-pricebook-field wc-pricebook-field--full">
618- <label><?php esc_html_e ( 'Customer ' , 'wc-pricebook ' ); ?> </label>
647+ <label><?php esc_html_e ( 'Customers ' , 'wc-pricebook ' ); ?> </label>
619648 <select
649+ multiple
620650 class="wc-customer-search"
621- name="<?php echo esc_attr ( $ base . '[user-id] ' ); ?> "
622- data-placeholder="<?php esc_attr_e ( 'Search for a customer … ' , 'wc-pricebook ' ); ?> "
651+ name="<?php echo esc_attr ( $ base . '[user-id][] ' ); ?> "
652+ data-placeholder="<?php esc_attr_e ( 'Search for customers … ' , 'wc-pricebook ' ); ?> "
623653 data-allow_clear="true"
624654 style="width:100%;">
625- <?php if ( $ user ) : ?>
626- <option value="<?php echo esc_attr ( (string ) $ user_id ); ?> " selected="selected">
627- <?php echo esc_html ( sprintf ( '%1$s (#%2$d – %3$s) ' , $ user ->display_name , $ user_id , $ user ->user_email ) ); ?>
655+ <?php
656+ foreach ( $ user_ids as $ user_id ) :
657+ $ user = (int ) $ user_id > 0 ? get_userdata ( (int ) $ user_id ) : false ;
658+ if ( ! $ user ) :
659+ continue ;
660+ endif ;
661+ ?>
662+ <option value="<?php echo esc_attr ( (string ) (int ) $ user_id ); ?> " selected="selected">
663+ <?php echo esc_html ( sprintf ( '%1$s (#%2$d – %3$s) ' , $ user ->display_name , (int ) $ user_id , $ user ->user_email ) ); ?>
628664 </option>
629- <?php endif ; ?>
665+ <?php endforeach ; ?>
630666 </select>
631667 </div>
632668 <div class="wc-pricebook-field">
@@ -793,28 +829,36 @@ private function save_bulk_pricing( $post_id ) {
793829 $ tiers = $ this ->config ->tiers ();
794830
795831 // Group by role, keyed within a role by quantity so a repeated quantity for
796- // the same role collapses to the last row entered.
832+ // the same role collapses to the last row entered. A row may target several
833+ // roles at once (role[]); it fans out to one stored break per role.
797834 $ by_role = array ();
798835 foreach ( $ rows as $ row ) {
799836 if ( ! is_array ( $ row ) ) {
800837 continue ;
801838 }
802- $ role = isset ( $ row ['role ' ] ) ? sanitize_key ( $ row ['role ' ] ) : '' ;
839+ $ roles = isset ( $ row ['role ' ] ) ? (array ) $ row ['role ' ] : array ();
840+ $ roles = array_values ( array_unique ( array_filter ( array_map ( 'sanitize_key ' , $ roles ) ) ) );
803841 $ qty = isset ( $ row ['min_qty ' ] ) ? absint ( $ row ['min_qty ' ] ) : 0 ;
804842 $ max = isset ( $ row ['max_qty ' ] ) ? absint ( $ row ['max_qty ' ] ) : 0 ;
805843 $ price = isset ( $ row ['price ' ] ) ? trim ( (string ) $ row ['price ' ] ) : '' ;
806844 // Drop incomplete rows and inverted ranges (a set max below the min).
807- if ( '' === $ role || ! $ this -> is_valid_bulk_target ( $ role , $ tiers ) || $ qty < 1 || '' === $ price ) {
845+ if ( empty ( $ roles ) || $ qty < 1 || '' === $ price ) {
808846 continue ;
809847 }
810848 if ( $ max > 0 && $ max < $ qty ) {
811849 continue ;
812850 }
813- $ by_role [ $ role ][ $ qty ] = array (
814- 'min_qty ' => $ qty ,
815- 'max_qty ' => $ max ,
816- 'price ' => $ this ->format ( $ price ),
817- );
851+ $ formatted = $ this ->format ( $ price );
852+ foreach ( $ roles as $ role ) {
853+ if ( ! $ this ->is_valid_bulk_target ( $ role , $ tiers ) ) {
854+ continue ;
855+ }
856+ $ by_role [ $ role ][ $ qty ] = array (
857+ 'min_qty ' => $ qty ,
858+ 'max_qty ' => $ max ,
859+ 'price ' => $ formatted ,
860+ );
861+ }
818862 }
819863
820864 if ( empty ( $ by_role ) ) {
@@ -851,20 +895,26 @@ private function save_user_pricing( $post_id ) {
851895 : array ();
852896
853897 // Keyed by user id so a duplicate customer collapses to the last row entered.
898+ // A row may target several customers at once (user-id[]); it fans out to one
899+ // stored entry per customer, all sharing the row's price.
854900 $ by_user = array ();
855901 foreach ( $ rows as $ row ) {
856902 if ( ! is_array ( $ row ) ) {
857903 continue ;
858904 }
859- $ user_id = isset ( $ row ['user-id ' ] ) ? absint ( $ row ['user-id ' ] ) : 0 ;
860- $ price = isset ( $ row ['price ' ] ) ? trim ( (string ) $ row ['price ' ] ) : '' ;
861- if ( $ user_id <= 0 || '' === $ price ) {
905+ $ user_ids = isset ( $ row ['user-id ' ] ) ? (array ) $ row ['user-id ' ] : array ();
906+ $ user_ids = array_values ( array_unique ( array_filter ( array_map ( 'absint ' , $ user_ids ) ) ) );
907+ $ price = isset ( $ row ['price ' ] ) ? trim ( (string ) $ row ['price ' ] ) : '' ;
908+ if ( empty ( $ user_ids ) || '' === $ price ) {
862909 continue ;
863910 }
864- $ by_user [ $ user_id ] = array (
865- 'user-id ' => $ user_id ,
866- 'price ' => $ this ->format ( $ price ),
867- );
911+ $ formatted = $ this ->format ( $ price );
912+ foreach ( $ user_ids as $ user_id ) {
913+ $ by_user [ $ user_id ] = array (
914+ 'user-id ' => $ user_id ,
915+ 'price ' => $ formatted ,
916+ );
917+ }
868918 }
869919
870920 if ( empty ( $ by_user ) ) {
0 commit comments