Changeset 3794384
- Timestamp:
- 11/10/2026 12:30:19 PM (28 hours ago)
- Location:
- patchwork-order-split
- Files:
-
- 24 edited
- 1 copied
-
tags/1.1.2 (copied) (copied from patchwork-order-split/trunk )
-
tags/1.1.2/assets/js/manager.js (modified) (3 diffs)
-
tags/1.1.2/includes/class-admin.php (modified) (1 diff)
-
tags/1.1.2/includes/class-backorders.php (modified) (2 diffs)
-
tags/1.1.2/includes/class-module.php (modified) (1 diff)
-
tags/1.1.2/lib/patchwork-core/assets/core.css (modified) (2 diffs)
-
tags/1.1.2/lib/patchwork-core/bootstrap.php (modified) (1 diff)
-
tags/1.1.2/lib/patchwork-core/src/class-admin.php (modified) (4 diffs)
-
tags/1.1.2/lib/patchwork-core/src/class-module.php (modified) (3 diffs)
-
tags/1.1.2/lib/patchwork-core/src/class-orders.php (modified) (7 diffs)
-
tags/1.1.2/lib/patchwork-core/src/class-support.php (modified) (5 diffs)
-
tags/1.1.2/patchwork-order-split.php (modified) (2 diffs)
-
tags/1.1.2/readme.txt (modified) (5 diffs)
-
trunk/assets/js/manager.js (modified) (3 diffs)
-
trunk/includes/class-admin.php (modified) (1 diff)
-
trunk/includes/class-backorders.php (modified) (2 diffs)
-
trunk/includes/class-module.php (modified) (1 diff)
-
trunk/lib/patchwork-core/assets/core.css (modified) (2 diffs)
-
trunk/lib/patchwork-core/bootstrap.php (modified) (1 diff)
-
trunk/lib/patchwork-core/src/class-admin.php (modified) (4 diffs)
-
trunk/lib/patchwork-core/src/class-module.php (modified) (3 diffs)
-
trunk/lib/patchwork-core/src/class-orders.php (modified) (7 diffs)
-
trunk/lib/patchwork-core/src/class-support.php (modified) (5 diffs)
-
trunk/patchwork-order-split.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
patchwork-order-split/tags/1.1.2/assets/js/manager.js
r3783125 r3794384 64 64 // ── Edit screen: toolbar above the items table ───────────────────────────── 65 65 66 function openFromEditScreen(e) { 67 e.preventDefault(); 68 const $btn = $(this).prop('disabled', true); 69 loadItems(CFG.order_id) 70 .then(openModal) 71 .catch(err => alert((err && err.message) || i18n.err_load)) 72 .finally(() => $btn.prop('disabled', false)); 73 } 74 66 75 function initEditScreen() { 76 // Patchwork Core 0.6+ puts "Split order" in the shared Patchwork bar (the bar can be 77 // re-added when WooCommerce reloads the items, so the click is bound on document). 78 if (document.getElementById('pwc-order-bar-tpl')) { 79 $(document).on('click', '#pwc-order-bar #pwsplit-open-btn', openFromEditScreen); 80 return; 81 } 67 82 const $anchor = $('#woocommerce-order-items .inside').first(); 68 83 if (!$anchor.length || $('#pwsplit-toolbar').length) return; … … 86 101 </div>`); 87 102 88 $('#pwsplit-open-btn').on('click', function (e) { 89 e.preventDefault(); 90 const $btn = $(this).prop('disabled', true); 91 loadItems(CFG.order_id) 92 .then(openModal) 93 .catch(err => alert((err && err.message) || i18n.err_load)) 94 .finally(() => $btn.prop('disabled', false)); 95 }); 103 $('#pwsplit-open-btn').on('click', openFromEditScreen); 96 104 } 97 105 … … 144 152 if (target === 'wc-modal-view-order') setTimeout(injectPreviewButton, 50); 145 153 }); 154 // Patchwork Core 0.6+: "Split order" is in the preview's Patchwork section. 155 $(document).on('click', '#pwc-preview [data-pwsplit-open="manager"]', function () { 156 const orderId = parseInt($(this).data('order-id'), 10); 157 if (!orderId) return; 158 const $btn = $(this).prop('disabled', true); 159 loadItems(orderId) 160 .then(data => { 161 $('#wc-backbone-modal-dialog .modal-close').first().trigger('click'); 162 openModal(data); 163 }) 164 .catch(err => { 165 alert((err && err.message) || i18n.err_load); 166 $btn.prop('disabled', false); 167 }); 168 }); 146 169 } 147 170 148 171 function injectPreviewButton() { 149 172 const $dialog = $('#wc-backbone-modal-dialog'); 150 if (!$dialog.length || $dialog.find('#pwsplit-preview-btn ').length) return;173 if (!$dialog.length || $dialog.find('#pwsplit-preview-btn, #pwc-preview').length) return; 151 174 152 175 // Order ID from the modal's Edit link (HPOS: &id=, legacy: post=) -
patchwork-order-split/tags/1.1.2/includes/class-admin.php
r3783125 r3794384 23 23 if ( ! $order_id && ! $on_list ) return; 24 24 25 wp_enqueue_style( 'patchwork-split-admin', PATCHWORK_SPLIT_URL . 'assets/css/admin.css', [], PATCHWORK_SPLIT_VERSION );26 wp_enqueue_script( 'patchwork-split-manager', PATCHWORK_SPLIT_URL . 'assets/js/manager.js', [ 'jquery', 'wp-api-fetch' ], PATCHWORK_SPLIT_VERSION , true );25 wp_enqueue_style( 'patchwork-split-admin', PATCHWORK_SPLIT_URL . 'assets/css/admin.css', [], PATCHWORK_SPLIT_VERSION . '.' . filemtime( PATCHWORK_SPLIT_DIR . 'assets/css/admin.css' ) ); 26 wp_enqueue_script( 'patchwork-split-manager', PATCHWORK_SPLIT_URL . 'assets/js/manager.js', [ 'jquery', 'wp-api-fetch' ], PATCHWORK_SPLIT_VERSION . '.' . filemtime( PATCHWORK_SPLIT_DIR . 'assets/js/manager.js' ), true ); 27 27 28 28 wp_localize_script( 'patchwork-split-manager', 'PatchworkSplitConfig', [ -
patchwork-order-split/tags/1.1.2/includes/class-backorders.php
r3783125 r3794384 32 32 add_action( 'woocommerce_reduce_order_stock', [ $this, 'record' ] ); 33 33 add_action( 'patchwork_split_completed', [ $this, 'after_split' ], 10, 3 ); 34 // Patchwork Inventory: stock ordered for this order has arrived, so fewer units are on backorder. 35 add_action( 'patchwork_inventory_stock_in', [ __CLASS__, 'stock_arrived' ], 10, 3 ); 34 36 35 37 add_filter( 'patchwork_split_order_badges', [ $this, 'badge' ], 5, 2 ); … … 143 145 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound, WordPress.WP.I18n.TextDomainMismatch 144 146 return (string) apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), null ); 147 } 148 149 /** 150 * Stock has arrived for this order (Patchwork Inventory): take the units off the recorded 151 * backorder for items of that product, so the badge and the notice stop asking to split. 152 * $line_id is the product that holds the stock (a variation, or a parent sharing its stock). 153 */ 154 public static function stock_arrived( $order, $line_id, $qty ): void { 155 if ( ! $order instanceof \WC_Order ) return; 156 $recorded = Splitter::meta_list( $order, self::QTY_META ); 157 if ( ! $recorded ) return; 158 $left = (float) $qty; 159 foreach ( $order->get_items( 'line_item' ) as $item_id => $item ) { 160 if ( $left <= 0 || empty( $recorded[ $item_id ] ) ) continue; 161 $p = $item instanceof \WC_Order_Item_Product ? $item->get_product() : null; 162 if ( ! $p || (int) $p->get_stock_managed_by_id() !== (int) $line_id ) continue; 163 $take = (int) min( (float) $recorded[ $item_id ], floor( $left ) ); 164 $recorded[ $item_id ] = (int) $recorded[ $item_id ] - $take; 165 $left -= $take; 166 if ( $recorded[ $item_id ] <= 0 ) unset( $recorded[ $item_id ] ); 167 } 168 if ( $recorded ) { 169 $order->update_meta_data( self::QTY_META, $recorded ); 170 } else { 171 // Nothing left on backorder: same as after a split, the order needs nothing more. 172 $order->delete_meta_data( self::QTY_META ); 173 $order->update_meta_data( self::DONE_META, time() ); 174 } 175 $order->save(); 145 176 } 146 177 -
patchwork-order-split/tags/1.1.2/includes/class-module.php
r3783125 r3794384 172 172 'tone' => 'warning', 173 173 ]; 174 } 175 176 /** Guides, shown at the foot of Split's page. */ 177 public function links(): array { 178 return [ 179 [ 'label' => __( 'Guide: how to split an order', 'patchwork-order-split' ), 'url' => 'https://patchwork.guru/guides/how-to-split-a-woocommerce-order/' ], 180 [ 'label' => __( 'Guide: merging orders', 'patchwork-order-split' ), 'url' => 'https://patchwork.guru/guides/how-to-merge-woocommerce-orders/' ], 181 ]; 182 } 183 184 // ─── Patchwork bar (core 0.6.0) ───────────────────────────────────────── 185 186 /** 187 * Split's buttons in the shared Patchwork bar on the order screen (and in the orders list 188 * preview). Pro adds Merge through the patchwork_split_order_bar filter. 189 */ 190 public function order_bar( \WC_Order $order ): array { 191 if ( ! Settings::current_user_can_split() || ! Settings::current_user_can_edit_order( $order ) ) return []; 192 $buttons = [ [ 193 'id' => 'pwsplit-open-btn', 194 'label' => __( 'Split order', 'patchwork-order-split' ), 195 'title' => __( 'Split into a new order, split part of a quantity, or mark items you can\'t fulfil', 'patchwork-order-split' ), 196 'primary' => true, 197 'data' => [ 'pwsplit-open' => 'manager' ], 198 'preview' => true, 199 ] ]; 200 return (array) apply_filters( 'patchwork_split_order_bar', $buttons, $order ); 174 201 } 175 202 -
patchwork-order-split/tags/1.1.2/lib/patchwork-core/assets/core.css
r3783125 r3794384 47 47 } 48 48 img.pwc-module-icon { padding: 6px; box-sizing: border-box; } 49 .pwc-actions { margin: 14px 0 0; display: flex; gap: 8px; }49 .pwc-actions { margin: 14px 0 0; display: flex; flex-wrap: wrap; gap: 8px; } 50 50 .pwc-catalog-card { background: #fafafa; } 51 51 .pwc-pill { … … 112 112 .pwc-support-privacy { font-size: 12px; margin: 4px 0 14px; } 113 113 .pwc-support .pwc-sysinfo th, .pwc-support .pwc-sysinfo td { padding: 5px 10px; line-height: 1.4; font-size: 12px; } 114 115 /* 0.5.0: back link, shortcuts, notice actions, custom work */ 116 .pwc-back { margin: 14px 0 -6px; font-size: 13px; } 117 .pwc-back a { text-decoration: none; } 118 .pwc-page-actions { margin: 4px 0 12px; } 119 .pwc-button-count { display: inline-block; min-width: 18px; margin-left: 4px; padding: 0 5px; border-radius: 9px; background: rgba(0,0,0,.12); font-size: 11px; line-height: 18px; text-align: center; font-weight: 600; } 120 .button-primary .pwc-button-count { background: rgba(255,255,255,.25); } 121 .pwc-notice { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; justify-content: space-between; } 122 .pwc-notice-action { font-weight: 600; white-space: nowrap; color: inherit; } 123 .pwc-custom-work { max-width: 900px; margin-top: 20px; } 124 125 /* 0.6.0: order screen bar, item info, preview */ 126 .pwc-order-bar { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; justify-content: space-between; margin: 0 0 1px; padding: 10px 12px; background: var(--pwc-accent-soft, #ecfdf5); border-bottom: 1px solid #dcdcde; } 127 .pwc-order-bar-info { display: flex; gap: 10px; align-items: center; min-width: 0; } 128 .pwc-order-bar-logo { flex: none; width: 30px; height: 30px; border-radius: 7px; background-color: var(--pwc-accent, #16a34a); background-repeat: no-repeat; background-position: center; background-size: 18px; } 129 .pwc-order-bar-title { font-weight: 600; font-size: 13px; line-height: 1.3; } 130 .pwc-order-bar-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; } 131 .pwc-order-bar-buttons { display: flex; flex-wrap: wrap; gap: 6px; } 132 .pwc-item-info { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; font-size: 12px; color: #50575e; } 133 .pwc-item-info-part { display: flex; flex-wrap: wrap; gap: 2px 10px; align-items: center; } 134 .pwc-item-info .button { min-height: 24px; line-height: 22px; padding: 0 8px; font-size: 12px; } 135 .pwc-item-info strong { color: #1d2327; } 136 .pwc-preview { margin: 16px 0 0; padding: 14px 1.5em 6px; border-top: 1px solid #dcdcde; } 137 .pwc-preview h2 { margin: 0 0 8px; font-size: 14px; } 138 .pwc-preview-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; } 139 .pwc-preview-section { margin: 4px 0; } 140 .pwc-preview-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } -
patchwork-order-split/tags/1.1.2/lib/patchwork-core/bootstrap.php
r3783125 r3794384 52 52 } 53 53 54 patchwork_core_register( '0. 4.0', __DIR__ );54 patchwork_core_register( '0.6.0', __DIR__ ); -
patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-admin.php
r3783125 r3794384 93 93 if ( ! $is_patchwork && ! $is_orders ) return; 94 94 95 wp_enqueue_style( 'patchwork-core', PATCHWORK_CORE_URL . 'assets/core.css', [], PATCHWORK_CORE_VERSION );95 wp_enqueue_style( 'patchwork-core', PATCHWORK_CORE_URL . 'assets/core.css', [], PATCHWORK_CORE_VERSION . '.' . filemtime( PATCHWORK_CORE_DIR . 'assets/core.css' ) ); 96 96 } 97 97 … … 135 135 136 136 <?php if ( ! empty( $card['notice'] ) ) : ?> 137 <p class="pwc-notice pwc-tone-<?php echo esc_attr( $card['tone'] ?? 'ok' ); ?>"><?php echo esc_html( $card['notice'] ); ?></p> 138 <?php endif; ?> 139 140 <?php if ( $can ) : ?> 137 <div class="pwc-notice pwc-tone-<?php echo esc_attr( $card['tone'] ?? 'ok' ); ?>"> 138 <span><?php echo esc_html( $card['notice'] ); ?></span> 139 <?php if ( $can && ! empty( $card['action']['url'] ) && ! empty( $card['action']['label'] ) ) : ?> 140 <a class="pwc-notice-action" href="<?php echo esc_url( $card['action']['url'] ); ?>"><?php echo esc_html( $card['action']['label'] ); ?> →</a> 141 <?php endif; ?> 142 </div> 143 <?php endif; ?> 144 145 <?php if ( $can ) : 146 $actions = self::actions( $module ); 147 ?> 141 148 <p class="pwc-actions"> 142 <a class="button button-primary" href="<?php echo esc_url( $module->admin_url( 'settings' ) ); ?>"><?php esc_html_e( 'Settings', 'patchwork-order-split' ); ?></a> 149 <?php self::action_buttons( $actions ); ?> 150 <a class="button <?php echo $actions ? '' : 'button-primary'; ?>" href="<?php echo esc_url( $module->admin_url( 'settings' ) ); ?>"><?php esc_html_e( 'Settings', 'patchwork-order-split' ); ?></a> 143 151 <a class="button" href="<?php echo esc_url( $module->admin_url( 'activity' ) ); ?>"><?php esc_html_e( 'Activity', 'patchwork-order-split' ); ?></a> 144 152 </p> … … 178 186 179 187 // ─── Module page ────────────────────────────────────────────────────────── 188 189 /** "← All Patchwork plugins": back to the overview. For module pages and a module's own screens. */ 190 public static function back_link(): void { 191 printf( 192 '<p class="pwc-back"><a href="%s">← %s</a></p>', 193 esc_url( add_query_arg( 'page', self::SLUG, admin_url( 'admin.php' ) ) ), 194 esc_html__( 'All Patchwork plugins', 'patchwork-order-split' ) 195 ); 196 } 197 198 /** A module's shortcuts, cleaned (an older or broken module returning junk shows nothing). */ 199 private static function actions( Module $module ): array { 200 if ( ! method_exists( $module, 'actions' ) ) return []; 201 $out = []; 202 foreach ( (array) $module->actions() as $a ) { 203 if ( ! is_array( $a ) || empty( $a['label'] ) || empty( $a['url'] ) ) continue; 204 $out[] = $a; 205 } 206 return $out; 207 } 208 209 private static function action_buttons( array $actions ): void { 210 foreach ( $actions as $a ) { 211 printf( 212 '<a class="button %s" href="%s">%s%s</a>', 213 ! empty( $a['primary'] ) ? 'button-primary' : '', 214 esc_url( $a['url'] ), 215 esc_html( $a['label'] ), 216 ! empty( $a['count'] ) ? ' <span class="pwc-button-count">' . (int) $a['count'] . '</span>' : '' 217 ); 218 } 219 } 180 220 181 221 private function render_module( Module $module ): void { … … 187 227 'licence' => __( 'Licence', 'patchwork-order-split' ), 188 228 ]; 229 // A free-only plugin (no Freemius, nothing in Pro) has no licence to show. 230 if ( ! $module->freemius() && ! $module->pro_features() ) unset( $tabs['licence'] ); 189 231 if ( ! isset( $tabs[ $tab ] ) ) $tab = 'settings'; 190 232 ?> 191 233 <div class="wrap pwc-wrap"> 234 <?php self::back_link(); ?> 192 235 <?php self::page_header( $module->name(), $module->description(), $module ); ?> 236 <?php if ( $actions = self::actions( $module ) ) : ?> 237 <p class="pwc-actions pwc-page-actions"><?php self::action_buttons( $actions ); ?></p> 238 <?php endif; ?> 193 239 194 240 <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -
patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-module.php
r3783125 r3794384 53 53 public function links(): array { return []; } 54 54 55 /** 56 * Shortcuts to this module's main screens, shown as buttons on its overview card 57 * and at the top of its page (added in core 0.5.0): 58 * [ [ 'label' => 'Reorder', 'url' => '…', 'primary' => true, 'count' => 3 ], … ] 59 * 'count' (optional) shows as a small number on the button. 60 */ 61 public function actions(): array { return []; } 62 55 63 // ─── Settings ───────────────────────────────────────────────────────────── 56 64 … … 82 90 * 'notice' => '', // short status line, optional 83 91 * 'tone' => 'ok|warning', 92 * 'action' => [ 'label' => '', 'url' => '' ], // optional: the fix for the notice (core 0.5.0) 84 93 * ] 85 94 */ … … 99 108 */ 100 109 public function order_panel( \WC_Order $order ): string { return ''; } 110 111 /** 112 * Buttons for the Patchwork bar at the top of the order's items box (core 0.6.0). Every 113 * module's buttons share one bar, so Patchwork tools are always in the same place: 114 * [ [ 'label' => '', 'id' => 'html-id', 'url' => '', 'primary' => bool, 'title' => '', 115 * 'data' => [ 'key' => 'value' ], 'preview' => bool ], … ] 116 * Without 'url' it's a <button type="button"> and the module's own script handles the 117 * click (bind on document, as the bar is added after page load). 'preview' => true also 118 * shows it in the orders list preview, with data-order-id set. 119 */ 120 public function order_bar( \WC_Order $order ): array { return []; } 121 122 /** 123 * HTML under one product line on the order screen (core 0.6.0): what this module knows 124 * about that item, e.g. stock per location, what's on order. '' for nothing. 125 */ 126 public function order_item_info( \WC_Order_Item_Product $item, \WC_Order $order ): string { return ''; } 127 128 /** This module's lines in the Patchwork part of the orders list preview (core 0.6.0). '' for nothing. */ 129 public function order_preview( \WC_Order $order ): string { return ''; } 101 130 102 131 // ─── Licensing (Freemius) ───────────────────────────────────────────────── -
patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-orders.php
r3783125 r3794384 5 5 6 6 /** 7 * Shared WooCommerce order UI: 8 * - ONE "Patchwork" column on the orders list; each module adds badges to it. 9 * - ONE "Patchwork" box on the order screen; each module adds a section. 10 * Works with HPOS and legacy order storage. Rendered per row / per order, so 11 * it only touches the orders on screen. 7 * Shared WooCommerce order UI, so every Patchwork plugin shows up in the same places: 8 * - the orders list: ONE "Patchwork" column (each module adds badges); 9 * - the order screen: ONE Patchwork bar at the top of the items box (buttons and a summary), 10 * ONE "Patchwork" box at the side (details, one section per module), and a line of 11 * Patchwork info under each product; 12 * - the orders list preview (eye icon): ONE Patchwork section with the same badges, 13 * details and buttons. 14 * Works with HPOS and legacy order storage, and only touches the orders on screen. 12 15 */ 13 16 final class Orders { … … 27 30 add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_column' ], 10, 2 ); 28 31 29 // Order screen box32 // Order screen: side box, bar, item lines 30 33 add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ], 30 ); 34 add_action( 'admin_footer', [ $this, 'render_bar' ] ); 35 add_action( 'woocommerce_after_order_itemmeta', [ $this, 'render_item_info' ], 20, 3 ); 36 37 // Orders list preview 38 add_filter( 'woocommerce_admin_order_preview_get_order_details', [ $this, 'preview_details' ], 20, 2 ); 39 add_action( 'woocommerce_admin_order_preview_end', [ $this, 'preview_template' ] ); 40 } 41 42 /** Modules that define a method (an older module, built for an older core, may not). */ 43 private function modules_with( string $method ): array { 44 return array_filter( $this->core->modules(), static fn( $m ) => method_exists( $m, $method ) ); 31 45 } 32 46 … … 54 68 $order = $order instanceof \WC_Order ? $order : wc_get_order( $order ); 55 69 if ( ! $order ) return; 56 70 echo $this->badges_html( $order ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in badge() 71 } 72 73 /** Every module's badges for an order. */ 74 private function badges_html( \WC_Order $order ): string { 75 $out = ''; 57 76 foreach ( $this->core->modules_supporting( 'orders_column' ) as $module ) { 58 77 foreach ( $module->order_badges( $order ) as $badge ) { 59 echo self::badge( $badge ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in badge()78 $out .= self::badge( $badge ); 60 79 } 61 80 } 81 return $out; 62 82 } 63 83 … … 69 89 $label = esc_html( $badge['label'] ?? '' ); 70 90 $title = esc_attr( $badge['title'] ?? '' ); 91 $style = self::badge_style( (string) ( $badge['color'] ?? '' ) ); 71 92 return ! empty( $badge['url'] ) 72 ? sprintf( '<a href="%s" class="pwc-badge pwc-badge-%s" title="%s">%s</a>', esc_url( $badge['url'] ), $tone, $title, $label ) 73 : sprintf( '<span class="pwc-badge pwc-badge-%s" title="%s">%s</span>', $tone, $title, $label ); 74 } 75 76 // ─── Order box ──────────────────────────────────────────────────────────── 93 ? sprintf( '<a href="%s" class="pwc-badge pwc-badge-%s" title="%s"%s>%s</a>', esc_url( $badge['url'] ), $tone, $title, $style, $label ) 94 : sprintf( '<span class="pwc-badge pwc-badge-%s" title="%s"%s>%s</span>', $tone, $title, $style, $label ); 95 } 96 97 /** 98 * Optional custom colour for a badge ('color' => '#2563eb'): that background, 99 * with dark or white text, whichever reads better on it. '' when no valid colour. 100 */ 101 public static function badge_style( string $color ): string { 102 $hex = sanitize_hex_color( $color ); 103 if ( ! $hex ) return ''; 104 $full = strlen( $hex ) === 4 ? '#' . $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3] : $hex; 105 [ $r, $g, $b ] = array_map( 'hexdec', str_split( substr( $full, 1 ), 2 ) ); 106 $text = ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) > 150 ? '#1f2937' : '#ffffff'; 107 return sprintf( ' style="background:%1$s;border-color:%1$s;color:%2$s"', esc_attr( $full ), esc_attr( $text ) ); 108 } 109 110 /** 111 * What module HTML on the order screen may contain: post HTML plus form fields, so a 112 * section can hold a control (e.g. Locations' location picker), saved with the order. 113 */ 114 public static function allowed_html(): array { 115 $attrs = [ 'name' => true, 'value' => true, 'type' => true, 'id' => true, 'class' => true, 'style' => true, 'title' => true, 116 'disabled' => true, 'readonly' => true, 'checked' => true, 'selected' => true, 'min' => true, 'max' => true, 'step' => true, 117 'placeholder' => true, 'for' => true, 'aria-label' => true, 'aria-hidden' => true, 'data-*' => true, 'tabindex' => true ]; 118 return array_merge( wp_kses_allowed_html( 'post' ), [ 119 'input' => $attrs, 'select' => $attrs, 'option' => $attrs, 'label' => $attrs, 'button' => $attrs, 'textarea' => $attrs + [ 'rows' => true ], 120 'span' => $attrs, 'div' => $attrs, 'a' => $attrs + [ 'href' => true, 'target' => true, 'rel' => true ], 121 ] ); 122 } 123 124 // ─── Order screen ───────────────────────────────────────────────────────── 77 125 78 126 public function add_meta_box(): void { … … 85 133 wc_get_page_screen_id( 'shop-order' ), 86 134 'side', 87 ' default'135 'high' 88 136 ); 89 137 } … … 101 149 if ( $html === '' ) continue; 102 150 $sections++; 103 echo '<div class="pwc-panel-section ">';151 echo '<div class="pwc-panel-section pwc-panel-' . esc_attr( $module->id() ) . '">'; 104 152 echo '<h4>' . esc_html( preg_replace( '/^Patchwork\s+/i', '', $module->name() ) ) . '</h4>'; 105 echo wp_kses _post( $html);153 echo wp_kses( $html, self::allowed_html() ); 106 154 echo '</div>'; 107 155 } … … 111 159 } 112 160 } 161 162 /** The order being edited on this admin screen, or null. */ 163 private static function current_order(): ?\WC_Order { 164 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; 165 if ( ! $screen || ! function_exists( 'wc_get_page_screen_id' ) || $screen->id !== wc_get_page_screen_id( 'shop-order' ) ) return null; 166 global $theorder, $post; 167 if ( $theorder instanceof \WC_Order ) return $theorder; 168 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading which order is open 169 $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : ( $post instanceof \WP_Post ? (int) $post->ID : 0 ); 170 $order = $id ? wc_get_order( $id ) : null; 171 return $order instanceof \WC_Order ? $order : null; 172 } 173 174 /** Every module's bar buttons for an order. */ 175 private function bar_buttons( \WC_Order $order, bool $preview = false ): array { 176 $out = []; 177 foreach ( $this->modules_with( 'order_bar' ) as $module ) { 178 foreach ( (array) $module->order_bar( $order ) as $b ) { 179 if ( ! is_array( $b ) || empty( $b['label'] ) ) continue; 180 if ( $preview && empty( $b['preview'] ) ) continue; 181 $out[] = $b; 182 } 183 } 184 return $out; 185 } 186 187 private static function button_html( array $b, int $order_id = 0 ): string { 188 $data = ''; 189 foreach ( (array) ( $b['data'] ?? [] ) as $k => $v ) { 190 $data .= sprintf( ' data-%s="%s"', esc_attr( sanitize_key( (string) $k ) ), esc_attr( (string) $v ) ); 191 } 192 if ( $order_id ) $data .= sprintf( ' data-order-id="%d"', $order_id ); 193 $class = 'button pwc-bar-button' . ( ! empty( $b['primary'] ) ? ' button-primary' : '' ); 194 $id = ! empty( $b['id'] ) ? sprintf( ' id="%s"', esc_attr( $b['id'] ) ) : ''; 195 $title = ! empty( $b['title'] ) ? sprintf( ' title="%s"', esc_attr( $b['title'] ) ) : ''; 196 return ! empty( $b['url'] ) 197 ? sprintf( '<a class="%s" href="%s"%s%s%s>%s</a>', esc_attr( $class ), esc_url( $b['url'] ), $id, $title, $data, esc_html( $b['label'] ) ) 198 : sprintf( '<button type="button" class="%s"%s%s%s>%s</button>', esc_attr( $class ), $id, $title, $data, esc_html( $b['label'] ) ); 199 } 200 201 /** 202 * The Patchwork bar: printed as a template in the footer and placed at the top of the 203 * items box by a few lines of script. WooCommerce rebuilds the items box when items 204 * change, so the script puts the bar back whenever it goes missing. 205 */ 206 public function render_bar(): void { 207 $order = self::current_order(); 208 if ( ! $order ) return; 209 $buttons = $this->bar_buttons( $order ); 210 $badges = $this->badges_html( $order ); 211 if ( ! $buttons && $badges === '' ) return; 212 ?> 213 <template id="pwc-order-bar-tpl"> 214 <div id="pwc-order-bar" class="pwc-order-bar"> 215 <div class="pwc-order-bar-info"> 216 <span class="pwc-order-bar-logo" aria-hidden="true" style="background-image:url('<?php echo esc_attr( Admin::icon_svg( '#fff' ) ); ?>')"></span> 217 <div> 218 <div class="pwc-order-bar-title">Patchwork</div> 219 <?php if ( $badges !== '' ) : ?><div class="pwc-order-bar-badges"><?php echo $badges; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in badge() ?></div><?php endif; ?> 220 </div> 221 </div> 222 <?php if ( $buttons ) : ?> 223 <div class="pwc-order-bar-buttons"><?php foreach ( $buttons as $b ) echo self::button_html( $b ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in button_html() ?></div> 224 <?php endif; ?> 225 </div> 226 </template> 227 <script> 228 ( function () { 229 var tpl = document.getElementById( 'pwc-order-bar-tpl' ); 230 var box = document.querySelector( '#woocommerce-order-items .inside' ); 231 if ( ! tpl || ! box ) return; 232 var place = function () { 233 if ( box.querySelector( '#pwc-order-bar' ) ) return; 234 box.insertBefore( tpl.content.cloneNode( true ), box.firstChild ); 235 document.dispatchEvent( new CustomEvent( 'patchwork:order-bar' ) ); 236 }; 237 place(); 238 new MutationObserver( place ).observe( box, { childList: true } ); 239 } )(); 240 </script> 241 <?php 242 } 243 244 /** 245 * A line of Patchwork info under each product on the order screen (also after 246 * WooCommerce reloads the items, as it renders them with the same hook). 247 */ 248 public function render_item_info( $item_id, $item, $product ): void { 249 if ( ! is_admin() || ! $item instanceof \WC_Order_Item_Product ) return; 250 $order = $item->get_order(); 251 if ( ! $order instanceof \WC_Order ) return; 252 $parts = []; 253 foreach ( $this->modules_with( 'order_item_info' ) as $module ) { 254 $html = (string) $module->order_item_info( $item, $order ); 255 if ( $html !== '' ) $parts[] = '<div class="pwc-item-info-part pwc-item-info-' . esc_attr( $module->id() ) . '">' . wp_kses( $html, self::allowed_html() ) . '</div>'; 256 } 257 if ( $parts ) echo '<div class="pwc-item-info">' . implode( '', $parts ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- each part passed through wp_kses 258 } 259 260 // ─── Orders list preview ────────────────────────────────────────────────── 261 262 /** The Patchwork part of the preview: badges, each module's lines, and its preview buttons. */ 263 public function preview_html( \WC_Order $order ): string { 264 $badges = $this->badges_html( $order ); 265 $sections = ''; 266 foreach ( $this->modules_with( 'order_preview' ) as $module ) { 267 $html = (string) $module->order_preview( $order ); 268 if ( $html === '' ) continue; 269 $sections .= '<div class="pwc-preview-section"><strong>' . esc_html( preg_replace( '/^Patchwork\s+/i', '', $module->name() ) ) . '</strong> ' . wp_kses( $html, self::allowed_html() ) . '</div>'; 270 } 271 $buttons = ''; 272 foreach ( $this->bar_buttons( $order, true ) as $b ) { 273 $b['id'] = ''; 274 $buttons .= self::button_html( $b, $order->get_id() ); 275 } 276 if ( $badges === '' && $sections === '' && $buttons === '' ) return ''; 277 return '<div id="pwc-preview" class="pwc-preview"><h2>Patchwork</h2>' 278 . ( $badges !== '' ? '<div class="pwc-preview-badges">' . $badges . '</div>' : '' ) 279 . $sections 280 . ( $buttons !== '' ? '<div class="pwc-preview-buttons">' . $buttons . '</div>' : '' ) 281 . '</div>'; 282 } 283 284 public function preview_details( $details, $order ) { 285 if ( is_array( $details ) && $order instanceof \WC_Order ) { 286 $details['patchwork_html'] = $this->preview_html( $order ); 287 } 288 return $details; 289 } 290 291 public function preview_template(): void { 292 echo '<# if ( data.patchwork_html ) { #>{{{ data.patchwork_html }}}<# } #>'; 293 } 113 294 } -
patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-support.php
r3783125 r3794384 105 105 106 106 $modules = $this->core->modules(); 107 if ( $form['plugin'] !== 'other'&& ! isset( $modules[ $form['plugin'] ] ) ) return $fail( __( 'Choose which plugin this is about.', 'patchwork-order-split' ) );107 if ( ! in_array( $form['plugin'], [ 'other', 'custom' ], true ) && ! isset( $modules[ $form['plugin'] ] ) ) return $fail( __( 'Choose which plugin this is about.', 'patchwork-order-split' ) ); 108 108 if ( $form['subject'] === '' ) return $fail( __( 'Add a short subject.', 'patchwork-order-split' ) ); 109 109 if ( mb_strlen( $form['message'] ) < 10 ) return $fail( __( 'Describe the issue in a little more detail.', 'patchwork-order-split' ) ); … … 117 117 if ( is_wp_error( $files ) ) return $fail( $files->get_error_message() ); 118 118 119 $plugin_name = $form['plugin'] === 'other' ? __( 'Something else', 'patchwork-order-split' ) : $modules[ $form['plugin'] ]->name();119 $plugin_name = $form['plugin'] === 'other' ? __( 'Something else', 'patchwork-order-split' ) : ( $form['plugin'] === 'custom' ? __( 'Custom work', 'patchwork-order-split' ) : $modules[ $form['plugin'] ]->name() ); 120 120 $user = wp_get_current_user(); 121 121 … … 231 231 ?> 232 232 <div class="wrap pwc-wrap"> 233 <?php Admin::back_link(); ?> 233 234 <?php Admin::page_header( __( 'Get support', 'patchwork-order-split' ), __( 'Tell us what\'s going on and we\'ll get back to you by email.', 'patchwork-order-split' ) ); ?> 234 235 … … 251 252 <option value="<?php echo esc_attr( $id ); ?>" <?php selected( $pre, $id ); ?>><?php echo esc_html( $module->name() ); ?></option> 252 253 <?php endforeach; ?> 254 <option value="custom" <?php selected( $pre, 'custom' ); ?>><?php esc_html_e( 'Custom work (a feature or an integration)', 'patchwork-order-split' ); ?></option> 253 255 <option value="other" <?php selected( $pre, 'other' ); ?>><?php esc_html_e( 'Something else', 'patchwork-order-split' ); ?></option> 254 256 </select> … … 300 302 </div> 301 303 </form> 304 305 <div class="pwc-card pwc-custom-work"> 306 <h2 class="pwc-card-title"><?php esc_html_e( 'Need something the plugins don\'t do?', 'patchwork-order-split' ); ?></h2> 307 <p><?php esc_html_e( 'We also build custom features and integrations for WordPress and WooCommerce: for example, sending your orders straight into a wholesaler\'s ordering system (if your supplier allows it), syncing stock with another system, or a workflow that fits how your business runs.', 'patchwork-order-split' ); ?></p> 308 <p><a href="<?php echo esc_url( self::url( [ 'plugin' => 'custom' ] ) ); ?>"><?php esc_html_e( 'Tell us what you need →', 'patchwork-order-split' ); ?></a></p> 309 </div> 302 310 </div> 303 311 <?php -
patchwork-order-split/tags/1.1.2/patchwork-order-split.php
r3783125 r3794384 3 3 /** 4 4 * Plugin Name: Patchwork Split – Split & Merge Orders for WooCommerce 5 * Plugin URI: https://patchwork.guru/plugins/ 5 6 * Description: Split WooCommerce orders into child orders, split part of a line's quantity, and record items you are unable to fulfil – with correct totals, taxes and stock. 6 * Version: 1.1. 17 * Version: 1.1.2 7 8 * Author: Patchwork 9 * Author URI: https://patchwork.guru/ 8 10 * License: GPL-2.0-or-later 9 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 19 21 } 20 22 // ─── Constants ─────────────────────────────────────────────────────────────── 21 define( 'PATCHWORK_SPLIT_VERSION', '1.1. 1' );23 define( 'PATCHWORK_SPLIT_VERSION', '1.1.2' ); 22 24 define( 'PATCHWORK_SPLIT_FILE', __FILE__ ); 23 25 define( 'PATCHWORK_SPLIT_DIR', plugin_dir_path( __FILE__ ) ); -
patchwork-order-split/tags/1.1.2/readme.txt
r3783125 r3794384 1 1 === Patchwork Split – Split & Merge Orders for WooCommerce === 2 2 Contributors: patchworkstudio 3 Tags: woocommerce, split order, backorder, partial shipment, order management3 Tags: woocommerce, split order, merge orders, backorder, partial shipment 4 4 Requires at least: 6.2 5 5 Tested up to: 7.1 … … 7 7 WC requires at least: 7.0 8 8 WC tested up to: 11.2 9 Stable tag: 1.1. 19 Stable tag: 1.1.2 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 53 53 Hooks such as `patchwork_split_validate_batch`, `patchwork_split_before_child_save`, `patchwork_split_completed` and `patchwork_split_order_badges`, plus a small JavaScript extension API, let other plugins add to the manager. 54 54 55 = More from Patchwork = 56 57 Patchwork builds practical WordPress plugins and custom fixes. 58 59 * [Patchwork Split – features and Pro](https://patchwork.guru/plugins/patchwork-split/) 60 * [Guide: how to split a WooCommerce order](https://patchwork.guru/guides/how-to-split-a-woocommerce-order/) 61 * [Guide: how to merge WooCommerce orders](https://patchwork.guru/guides/how-to-merge-woocommerce-orders/) 62 * [Need something custom for your store or site?](https://patchwork.guru/services/) 63 * [Support](https://patchwork.guru/support/) 64 55 65 == Installation == 56 66 … … 58 68 2. Activate it. WooCommerce must be active. 59 69 3. Optional: adjust reasons, shipping, backorders and emails under **Patchwork → Split**. 60 4. Open any order and click ** Open manager**above the order items.70 4. Open any order and click **Split order** in the Patchwork bar above the order items. 61 71 62 72 == Frequently Asked Questions == 73 74 = How do I split a WooCommerce order into two shipments? = 75 76 Open the order and click **Split order** in the Patchwork bar above the order items. Mark the items (or part of an item's quantity) that should ship later as **Split**, then apply. They move into one new order, and the original keeps the rest, ready to ship now. 77 78 = Does splitting an order change the tax or the price? = 79 80 No. Prices, discounts and taxes are carried over exactly as the customer was charged — never recalculated at today's rates — and the original order plus the new one always add up to the original total. 81 82 = Can I merge two WooCommerce orders? = 83 84 Yes, with Pro. Open orders from the same customer can be combined so they ship together. Both orders must be in the same payment state (both paid or both unpaid), so payments and stock stay straight. 85 86 = How does it handle backorders? = 87 88 Orders with backordered items get a "⚠ on backorder" badge and a notice on the order. One click opens the manager with the backordered units already selected, so the rest can ship now — or dismiss it to ship everything together. Pro can split backorders automatically when an order is paid. 63 89 64 90 = Who can use the manager? = … … 109 135 == Changelog == 110 136 137 = 1.1.2 = 138 * Order screen: Split's buttons now sit in one shared Patchwork bar above the order items, with this order's badges. Every Patchwork plugin uses the same bar, the same Patchwork box and the same line under each product, so nothing is scattered. 139 * Orders list preview (the eye icon) has a matching Patchwork section with the badges and a Split order button. 140 * With Patchwork Inventory: when stock ordered for an order arrives, the "on backorder" badge and notice clear by themselves. 141 * Patchwork dashboard: a link back to all Patchwork plugins, shortcut buttons on each plugin's card, and a "custom work" option on the Support page. 142 * Guide links on Split's page and in this readme. 143 * Fix: after an update, browsers now always load the new scripts and styles (no more stale copies). 144 111 145 = 1.1.1 = 112 146 * Security: order actions now check that you can edit that specific order (respects plugins that limit which orders a user can edit). -
patchwork-order-split/trunk/assets/js/manager.js
r3783125 r3794384 64 64 // ── Edit screen: toolbar above the items table ───────────────────────────── 65 65 66 function openFromEditScreen(e) { 67 e.preventDefault(); 68 const $btn = $(this).prop('disabled', true); 69 loadItems(CFG.order_id) 70 .then(openModal) 71 .catch(err => alert((err && err.message) || i18n.err_load)) 72 .finally(() => $btn.prop('disabled', false)); 73 } 74 66 75 function initEditScreen() { 76 // Patchwork Core 0.6+ puts "Split order" in the shared Patchwork bar (the bar can be 77 // re-added when WooCommerce reloads the items, so the click is bound on document). 78 if (document.getElementById('pwc-order-bar-tpl')) { 79 $(document).on('click', '#pwc-order-bar #pwsplit-open-btn', openFromEditScreen); 80 return; 81 } 67 82 const $anchor = $('#woocommerce-order-items .inside').first(); 68 83 if (!$anchor.length || $('#pwsplit-toolbar').length) return; … … 86 101 </div>`); 87 102 88 $('#pwsplit-open-btn').on('click', function (e) { 89 e.preventDefault(); 90 const $btn = $(this).prop('disabled', true); 91 loadItems(CFG.order_id) 92 .then(openModal) 93 .catch(err => alert((err && err.message) || i18n.err_load)) 94 .finally(() => $btn.prop('disabled', false)); 95 }); 103 $('#pwsplit-open-btn').on('click', openFromEditScreen); 96 104 } 97 105 … … 144 152 if (target === 'wc-modal-view-order') setTimeout(injectPreviewButton, 50); 145 153 }); 154 // Patchwork Core 0.6+: "Split order" is in the preview's Patchwork section. 155 $(document).on('click', '#pwc-preview [data-pwsplit-open="manager"]', function () { 156 const orderId = parseInt($(this).data('order-id'), 10); 157 if (!orderId) return; 158 const $btn = $(this).prop('disabled', true); 159 loadItems(orderId) 160 .then(data => { 161 $('#wc-backbone-modal-dialog .modal-close').first().trigger('click'); 162 openModal(data); 163 }) 164 .catch(err => { 165 alert((err && err.message) || i18n.err_load); 166 $btn.prop('disabled', false); 167 }); 168 }); 146 169 } 147 170 148 171 function injectPreviewButton() { 149 172 const $dialog = $('#wc-backbone-modal-dialog'); 150 if (!$dialog.length || $dialog.find('#pwsplit-preview-btn ').length) return;173 if (!$dialog.length || $dialog.find('#pwsplit-preview-btn, #pwc-preview').length) return; 151 174 152 175 // Order ID from the modal's Edit link (HPOS: &id=, legacy: post=) -
patchwork-order-split/trunk/includes/class-admin.php
r3783125 r3794384 23 23 if ( ! $order_id && ! $on_list ) return; 24 24 25 wp_enqueue_style( 'patchwork-split-admin', PATCHWORK_SPLIT_URL . 'assets/css/admin.css', [], PATCHWORK_SPLIT_VERSION );26 wp_enqueue_script( 'patchwork-split-manager', PATCHWORK_SPLIT_URL . 'assets/js/manager.js', [ 'jquery', 'wp-api-fetch' ], PATCHWORK_SPLIT_VERSION , true );25 wp_enqueue_style( 'patchwork-split-admin', PATCHWORK_SPLIT_URL . 'assets/css/admin.css', [], PATCHWORK_SPLIT_VERSION . '.' . filemtime( PATCHWORK_SPLIT_DIR . 'assets/css/admin.css' ) ); 26 wp_enqueue_script( 'patchwork-split-manager', PATCHWORK_SPLIT_URL . 'assets/js/manager.js', [ 'jquery', 'wp-api-fetch' ], PATCHWORK_SPLIT_VERSION . '.' . filemtime( PATCHWORK_SPLIT_DIR . 'assets/js/manager.js' ), true ); 27 27 28 28 wp_localize_script( 'patchwork-split-manager', 'PatchworkSplitConfig', [ -
patchwork-order-split/trunk/includes/class-backorders.php
r3783125 r3794384 32 32 add_action( 'woocommerce_reduce_order_stock', [ $this, 'record' ] ); 33 33 add_action( 'patchwork_split_completed', [ $this, 'after_split' ], 10, 3 ); 34 // Patchwork Inventory: stock ordered for this order has arrived, so fewer units are on backorder. 35 add_action( 'patchwork_inventory_stock_in', [ __CLASS__, 'stock_arrived' ], 10, 3 ); 34 36 35 37 add_filter( 'patchwork_split_order_badges', [ $this, 'badge' ], 5, 2 ); … … 143 145 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound, WordPress.WP.I18n.TextDomainMismatch 144 146 return (string) apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), null ); 147 } 148 149 /** 150 * Stock has arrived for this order (Patchwork Inventory): take the units off the recorded 151 * backorder for items of that product, so the badge and the notice stop asking to split. 152 * $line_id is the product that holds the stock (a variation, or a parent sharing its stock). 153 */ 154 public static function stock_arrived( $order, $line_id, $qty ): void { 155 if ( ! $order instanceof \WC_Order ) return; 156 $recorded = Splitter::meta_list( $order, self::QTY_META ); 157 if ( ! $recorded ) return; 158 $left = (float) $qty; 159 foreach ( $order->get_items( 'line_item' ) as $item_id => $item ) { 160 if ( $left <= 0 || empty( $recorded[ $item_id ] ) ) continue; 161 $p = $item instanceof \WC_Order_Item_Product ? $item->get_product() : null; 162 if ( ! $p || (int) $p->get_stock_managed_by_id() !== (int) $line_id ) continue; 163 $take = (int) min( (float) $recorded[ $item_id ], floor( $left ) ); 164 $recorded[ $item_id ] = (int) $recorded[ $item_id ] - $take; 165 $left -= $take; 166 if ( $recorded[ $item_id ] <= 0 ) unset( $recorded[ $item_id ] ); 167 } 168 if ( $recorded ) { 169 $order->update_meta_data( self::QTY_META, $recorded ); 170 } else { 171 // Nothing left on backorder: same as after a split, the order needs nothing more. 172 $order->delete_meta_data( self::QTY_META ); 173 $order->update_meta_data( self::DONE_META, time() ); 174 } 175 $order->save(); 145 176 } 146 177 -
patchwork-order-split/trunk/includes/class-module.php
r3783125 r3794384 172 172 'tone' => 'warning', 173 173 ]; 174 } 175 176 /** Guides, shown at the foot of Split's page. */ 177 public function links(): array { 178 return [ 179 [ 'label' => __( 'Guide: how to split an order', 'patchwork-order-split' ), 'url' => 'https://patchwork.guru/guides/how-to-split-a-woocommerce-order/' ], 180 [ 'label' => __( 'Guide: merging orders', 'patchwork-order-split' ), 'url' => 'https://patchwork.guru/guides/how-to-merge-woocommerce-orders/' ], 181 ]; 182 } 183 184 // ─── Patchwork bar (core 0.6.0) ───────────────────────────────────────── 185 186 /** 187 * Split's buttons in the shared Patchwork bar on the order screen (and in the orders list 188 * preview). Pro adds Merge through the patchwork_split_order_bar filter. 189 */ 190 public function order_bar( \WC_Order $order ): array { 191 if ( ! Settings::current_user_can_split() || ! Settings::current_user_can_edit_order( $order ) ) return []; 192 $buttons = [ [ 193 'id' => 'pwsplit-open-btn', 194 'label' => __( 'Split order', 'patchwork-order-split' ), 195 'title' => __( 'Split into a new order, split part of a quantity, or mark items you can\'t fulfil', 'patchwork-order-split' ), 196 'primary' => true, 197 'data' => [ 'pwsplit-open' => 'manager' ], 198 'preview' => true, 199 ] ]; 200 return (array) apply_filters( 'patchwork_split_order_bar', $buttons, $order ); 174 201 } 175 202 -
patchwork-order-split/trunk/lib/patchwork-core/assets/core.css
r3783125 r3794384 47 47 } 48 48 img.pwc-module-icon { padding: 6px; box-sizing: border-box; } 49 .pwc-actions { margin: 14px 0 0; display: flex; gap: 8px; }49 .pwc-actions { margin: 14px 0 0; display: flex; flex-wrap: wrap; gap: 8px; } 50 50 .pwc-catalog-card { background: #fafafa; } 51 51 .pwc-pill { … … 112 112 .pwc-support-privacy { font-size: 12px; margin: 4px 0 14px; } 113 113 .pwc-support .pwc-sysinfo th, .pwc-support .pwc-sysinfo td { padding: 5px 10px; line-height: 1.4; font-size: 12px; } 114 115 /* 0.5.0: back link, shortcuts, notice actions, custom work */ 116 .pwc-back { margin: 14px 0 -6px; font-size: 13px; } 117 .pwc-back a { text-decoration: none; } 118 .pwc-page-actions { margin: 4px 0 12px; } 119 .pwc-button-count { display: inline-block; min-width: 18px; margin-left: 4px; padding: 0 5px; border-radius: 9px; background: rgba(0,0,0,.12); font-size: 11px; line-height: 18px; text-align: center; font-weight: 600; } 120 .button-primary .pwc-button-count { background: rgba(255,255,255,.25); } 121 .pwc-notice { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; justify-content: space-between; } 122 .pwc-notice-action { font-weight: 600; white-space: nowrap; color: inherit; } 123 .pwc-custom-work { max-width: 900px; margin-top: 20px; } 124 125 /* 0.6.0: order screen bar, item info, preview */ 126 .pwc-order-bar { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; justify-content: space-between; margin: 0 0 1px; padding: 10px 12px; background: var(--pwc-accent-soft, #ecfdf5); border-bottom: 1px solid #dcdcde; } 127 .pwc-order-bar-info { display: flex; gap: 10px; align-items: center; min-width: 0; } 128 .pwc-order-bar-logo { flex: none; width: 30px; height: 30px; border-radius: 7px; background-color: var(--pwc-accent, #16a34a); background-repeat: no-repeat; background-position: center; background-size: 18px; } 129 .pwc-order-bar-title { font-weight: 600; font-size: 13px; line-height: 1.3; } 130 .pwc-order-bar-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; } 131 .pwc-order-bar-buttons { display: flex; flex-wrap: wrap; gap: 6px; } 132 .pwc-item-info { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; font-size: 12px; color: #50575e; } 133 .pwc-item-info-part { display: flex; flex-wrap: wrap; gap: 2px 10px; align-items: center; } 134 .pwc-item-info .button { min-height: 24px; line-height: 22px; padding: 0 8px; font-size: 12px; } 135 .pwc-item-info strong { color: #1d2327; } 136 .pwc-preview { margin: 16px 0 0; padding: 14px 1.5em 6px; border-top: 1px solid #dcdcde; } 137 .pwc-preview h2 { margin: 0 0 8px; font-size: 14px; } 138 .pwc-preview-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; } 139 .pwc-preview-section { margin: 4px 0; } 140 .pwc-preview-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } -
patchwork-order-split/trunk/lib/patchwork-core/bootstrap.php
r3783125 r3794384 52 52 } 53 53 54 patchwork_core_register( '0. 4.0', __DIR__ );54 patchwork_core_register( '0.6.0', __DIR__ ); -
patchwork-order-split/trunk/lib/patchwork-core/src/class-admin.php
r3783125 r3794384 93 93 if ( ! $is_patchwork && ! $is_orders ) return; 94 94 95 wp_enqueue_style( 'patchwork-core', PATCHWORK_CORE_URL . 'assets/core.css', [], PATCHWORK_CORE_VERSION );95 wp_enqueue_style( 'patchwork-core', PATCHWORK_CORE_URL . 'assets/core.css', [], PATCHWORK_CORE_VERSION . '.' . filemtime( PATCHWORK_CORE_DIR . 'assets/core.css' ) ); 96 96 } 97 97 … … 135 135 136 136 <?php if ( ! empty( $card['notice'] ) ) : ?> 137 <p class="pwc-notice pwc-tone-<?php echo esc_attr( $card['tone'] ?? 'ok' ); ?>"><?php echo esc_html( $card['notice'] ); ?></p> 138 <?php endif; ?> 139 140 <?php if ( $can ) : ?> 137 <div class="pwc-notice pwc-tone-<?php echo esc_attr( $card['tone'] ?? 'ok' ); ?>"> 138 <span><?php echo esc_html( $card['notice'] ); ?></span> 139 <?php if ( $can && ! empty( $card['action']['url'] ) && ! empty( $card['action']['label'] ) ) : ?> 140 <a class="pwc-notice-action" href="<?php echo esc_url( $card['action']['url'] ); ?>"><?php echo esc_html( $card['action']['label'] ); ?> →</a> 141 <?php endif; ?> 142 </div> 143 <?php endif; ?> 144 145 <?php if ( $can ) : 146 $actions = self::actions( $module ); 147 ?> 141 148 <p class="pwc-actions"> 142 <a class="button button-primary" href="<?php echo esc_url( $module->admin_url( 'settings' ) ); ?>"><?php esc_html_e( 'Settings', 'patchwork-order-split' ); ?></a> 149 <?php self::action_buttons( $actions ); ?> 150 <a class="button <?php echo $actions ? '' : 'button-primary'; ?>" href="<?php echo esc_url( $module->admin_url( 'settings' ) ); ?>"><?php esc_html_e( 'Settings', 'patchwork-order-split' ); ?></a> 143 151 <a class="button" href="<?php echo esc_url( $module->admin_url( 'activity' ) ); ?>"><?php esc_html_e( 'Activity', 'patchwork-order-split' ); ?></a> 144 152 </p> … … 178 186 179 187 // ─── Module page ────────────────────────────────────────────────────────── 188 189 /** "← All Patchwork plugins": back to the overview. For module pages and a module's own screens. */ 190 public static function back_link(): void { 191 printf( 192 '<p class="pwc-back"><a href="%s">← %s</a></p>', 193 esc_url( add_query_arg( 'page', self::SLUG, admin_url( 'admin.php' ) ) ), 194 esc_html__( 'All Patchwork plugins', 'patchwork-order-split' ) 195 ); 196 } 197 198 /** A module's shortcuts, cleaned (an older or broken module returning junk shows nothing). */ 199 private static function actions( Module $module ): array { 200 if ( ! method_exists( $module, 'actions' ) ) return []; 201 $out = []; 202 foreach ( (array) $module->actions() as $a ) { 203 if ( ! is_array( $a ) || empty( $a['label'] ) || empty( $a['url'] ) ) continue; 204 $out[] = $a; 205 } 206 return $out; 207 } 208 209 private static function action_buttons( array $actions ): void { 210 foreach ( $actions as $a ) { 211 printf( 212 '<a class="button %s" href="%s">%s%s</a>', 213 ! empty( $a['primary'] ) ? 'button-primary' : '', 214 esc_url( $a['url'] ), 215 esc_html( $a['label'] ), 216 ! empty( $a['count'] ) ? ' <span class="pwc-button-count">' . (int) $a['count'] . '</span>' : '' 217 ); 218 } 219 } 180 220 181 221 private function render_module( Module $module ): void { … … 187 227 'licence' => __( 'Licence', 'patchwork-order-split' ), 188 228 ]; 229 // A free-only plugin (no Freemius, nothing in Pro) has no licence to show. 230 if ( ! $module->freemius() && ! $module->pro_features() ) unset( $tabs['licence'] ); 189 231 if ( ! isset( $tabs[ $tab ] ) ) $tab = 'settings'; 190 232 ?> 191 233 <div class="wrap pwc-wrap"> 234 <?php self::back_link(); ?> 192 235 <?php self::page_header( $module->name(), $module->description(), $module ); ?> 236 <?php if ( $actions = self::actions( $module ) ) : ?> 237 <p class="pwc-actions pwc-page-actions"><?php self::action_buttons( $actions ); ?></p> 238 <?php endif; ?> 193 239 194 240 <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -
patchwork-order-split/trunk/lib/patchwork-core/src/class-module.php
r3783125 r3794384 53 53 public function links(): array { return []; } 54 54 55 /** 56 * Shortcuts to this module's main screens, shown as buttons on its overview card 57 * and at the top of its page (added in core 0.5.0): 58 * [ [ 'label' => 'Reorder', 'url' => '…', 'primary' => true, 'count' => 3 ], … ] 59 * 'count' (optional) shows as a small number on the button. 60 */ 61 public function actions(): array { return []; } 62 55 63 // ─── Settings ───────────────────────────────────────────────────────────── 56 64 … … 82 90 * 'notice' => '', // short status line, optional 83 91 * 'tone' => 'ok|warning', 92 * 'action' => [ 'label' => '', 'url' => '' ], // optional: the fix for the notice (core 0.5.0) 84 93 * ] 85 94 */ … … 99 108 */ 100 109 public function order_panel( \WC_Order $order ): string { return ''; } 110 111 /** 112 * Buttons for the Patchwork bar at the top of the order's items box (core 0.6.0). Every 113 * module's buttons share one bar, so Patchwork tools are always in the same place: 114 * [ [ 'label' => '', 'id' => 'html-id', 'url' => '', 'primary' => bool, 'title' => '', 115 * 'data' => [ 'key' => 'value' ], 'preview' => bool ], … ] 116 * Without 'url' it's a <button type="button"> and the module's own script handles the 117 * click (bind on document, as the bar is added after page load). 'preview' => true also 118 * shows it in the orders list preview, with data-order-id set. 119 */ 120 public function order_bar( \WC_Order $order ): array { return []; } 121 122 /** 123 * HTML under one product line on the order screen (core 0.6.0): what this module knows 124 * about that item, e.g. stock per location, what's on order. '' for nothing. 125 */ 126 public function order_item_info( \WC_Order_Item_Product $item, \WC_Order $order ): string { return ''; } 127 128 /** This module's lines in the Patchwork part of the orders list preview (core 0.6.0). '' for nothing. */ 129 public function order_preview( \WC_Order $order ): string { return ''; } 101 130 102 131 // ─── Licensing (Freemius) ───────────────────────────────────────────────── -
patchwork-order-split/trunk/lib/patchwork-core/src/class-orders.php
r3783125 r3794384 5 5 6 6 /** 7 * Shared WooCommerce order UI: 8 * - ONE "Patchwork" column on the orders list; each module adds badges to it. 9 * - ONE "Patchwork" box on the order screen; each module adds a section. 10 * Works with HPOS and legacy order storage. Rendered per row / per order, so 11 * it only touches the orders on screen. 7 * Shared WooCommerce order UI, so every Patchwork plugin shows up in the same places: 8 * - the orders list: ONE "Patchwork" column (each module adds badges); 9 * - the order screen: ONE Patchwork bar at the top of the items box (buttons and a summary), 10 * ONE "Patchwork" box at the side (details, one section per module), and a line of 11 * Patchwork info under each product; 12 * - the orders list preview (eye icon): ONE Patchwork section with the same badges, 13 * details and buttons. 14 * Works with HPOS and legacy order storage, and only touches the orders on screen. 12 15 */ 13 16 final class Orders { … … 27 30 add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_column' ], 10, 2 ); 28 31 29 // Order screen box32 // Order screen: side box, bar, item lines 30 33 add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ], 30 ); 34 add_action( 'admin_footer', [ $this, 'render_bar' ] ); 35 add_action( 'woocommerce_after_order_itemmeta', [ $this, 'render_item_info' ], 20, 3 ); 36 37 // Orders list preview 38 add_filter( 'woocommerce_admin_order_preview_get_order_details', [ $this, 'preview_details' ], 20, 2 ); 39 add_action( 'woocommerce_admin_order_preview_end', [ $this, 'preview_template' ] ); 40 } 41 42 /** Modules that define a method (an older module, built for an older core, may not). */ 43 private function modules_with( string $method ): array { 44 return array_filter( $this->core->modules(), static fn( $m ) => method_exists( $m, $method ) ); 31 45 } 32 46 … … 54 68 $order = $order instanceof \WC_Order ? $order : wc_get_order( $order ); 55 69 if ( ! $order ) return; 56 70 echo $this->badges_html( $order ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in badge() 71 } 72 73 /** Every module's badges for an order. */ 74 private function badges_html( \WC_Order $order ): string { 75 $out = ''; 57 76 foreach ( $this->core->modules_supporting( 'orders_column' ) as $module ) { 58 77 foreach ( $module->order_badges( $order ) as $badge ) { 59 echo self::badge( $badge ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in badge()78 $out .= self::badge( $badge ); 60 79 } 61 80 } 81 return $out; 62 82 } 63 83 … … 69 89 $label = esc_html( $badge['label'] ?? '' ); 70 90 $title = esc_attr( $badge['title'] ?? '' ); 91 $style = self::badge_style( (string) ( $badge['color'] ?? '' ) ); 71 92 return ! empty( $badge['url'] ) 72 ? sprintf( '<a href="%s" class="pwc-badge pwc-badge-%s" title="%s">%s</a>', esc_url( $badge['url'] ), $tone, $title, $label ) 73 : sprintf( '<span class="pwc-badge pwc-badge-%s" title="%s">%s</span>', $tone, $title, $label ); 74 } 75 76 // ─── Order box ──────────────────────────────────────────────────────────── 93 ? sprintf( '<a href="%s" class="pwc-badge pwc-badge-%s" title="%s"%s>%s</a>', esc_url( $badge['url'] ), $tone, $title, $style, $label ) 94 : sprintf( '<span class="pwc-badge pwc-badge-%s" title="%s"%s>%s</span>', $tone, $title, $style, $label ); 95 } 96 97 /** 98 * Optional custom colour for a badge ('color' => '#2563eb'): that background, 99 * with dark or white text, whichever reads better on it. '' when no valid colour. 100 */ 101 public static function badge_style( string $color ): string { 102 $hex = sanitize_hex_color( $color ); 103 if ( ! $hex ) return ''; 104 $full = strlen( $hex ) === 4 ? '#' . $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3] : $hex; 105 [ $r, $g, $b ] = array_map( 'hexdec', str_split( substr( $full, 1 ), 2 ) ); 106 $text = ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) > 150 ? '#1f2937' : '#ffffff'; 107 return sprintf( ' style="background:%1$s;border-color:%1$s;color:%2$s"', esc_attr( $full ), esc_attr( $text ) ); 108 } 109 110 /** 111 * What module HTML on the order screen may contain: post HTML plus form fields, so a 112 * section can hold a control (e.g. Locations' location picker), saved with the order. 113 */ 114 public static function allowed_html(): array { 115 $attrs = [ 'name' => true, 'value' => true, 'type' => true, 'id' => true, 'class' => true, 'style' => true, 'title' => true, 116 'disabled' => true, 'readonly' => true, 'checked' => true, 'selected' => true, 'min' => true, 'max' => true, 'step' => true, 117 'placeholder' => true, 'for' => true, 'aria-label' => true, 'aria-hidden' => true, 'data-*' => true, 'tabindex' => true ]; 118 return array_merge( wp_kses_allowed_html( 'post' ), [ 119 'input' => $attrs, 'select' => $attrs, 'option' => $attrs, 'label' => $attrs, 'button' => $attrs, 'textarea' => $attrs + [ 'rows' => true ], 120 'span' => $attrs, 'div' => $attrs, 'a' => $attrs + [ 'href' => true, 'target' => true, 'rel' => true ], 121 ] ); 122 } 123 124 // ─── Order screen ───────────────────────────────────────────────────────── 77 125 78 126 public function add_meta_box(): void { … … 85 133 wc_get_page_screen_id( 'shop-order' ), 86 134 'side', 87 ' default'135 'high' 88 136 ); 89 137 } … … 101 149 if ( $html === '' ) continue; 102 150 $sections++; 103 echo '<div class="pwc-panel-section ">';151 echo '<div class="pwc-panel-section pwc-panel-' . esc_attr( $module->id() ) . '">'; 104 152 echo '<h4>' . esc_html( preg_replace( '/^Patchwork\s+/i', '', $module->name() ) ) . '</h4>'; 105 echo wp_kses _post( $html);153 echo wp_kses( $html, self::allowed_html() ); 106 154 echo '</div>'; 107 155 } … … 111 159 } 112 160 } 161 162 /** The order being edited on this admin screen, or null. */ 163 private static function current_order(): ?\WC_Order { 164 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; 165 if ( ! $screen || ! function_exists( 'wc_get_page_screen_id' ) || $screen->id !== wc_get_page_screen_id( 'shop-order' ) ) return null; 166 global $theorder, $post; 167 if ( $theorder instanceof \WC_Order ) return $theorder; 168 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading which order is open 169 $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : ( $post instanceof \WP_Post ? (int) $post->ID : 0 ); 170 $order = $id ? wc_get_order( $id ) : null; 171 return $order instanceof \WC_Order ? $order : null; 172 } 173 174 /** Every module's bar buttons for an order. */ 175 private function bar_buttons( \WC_Order $order, bool $preview = false ): array { 176 $out = []; 177 foreach ( $this->modules_with( 'order_bar' ) as $module ) { 178 foreach ( (array) $module->order_bar( $order ) as $b ) { 179 if ( ! is_array( $b ) || empty( $b['label'] ) ) continue; 180 if ( $preview && empty( $b['preview'] ) ) continue; 181 $out[] = $b; 182 } 183 } 184 return $out; 185 } 186 187 private static function button_html( array $b, int $order_id = 0 ): string { 188 $data = ''; 189 foreach ( (array) ( $b['data'] ?? [] ) as $k => $v ) { 190 $data .= sprintf( ' data-%s="%s"', esc_attr( sanitize_key( (string) $k ) ), esc_attr( (string) $v ) ); 191 } 192 if ( $order_id ) $data .= sprintf( ' data-order-id="%d"', $order_id ); 193 $class = 'button pwc-bar-button' . ( ! empty( $b['primary'] ) ? ' button-primary' : '' ); 194 $id = ! empty( $b['id'] ) ? sprintf( ' id="%s"', esc_attr( $b['id'] ) ) : ''; 195 $title = ! empty( $b['title'] ) ? sprintf( ' title="%s"', esc_attr( $b['title'] ) ) : ''; 196 return ! empty( $b['url'] ) 197 ? sprintf( '<a class="%s" href="%s"%s%s%s>%s</a>', esc_attr( $class ), esc_url( $b['url'] ), $id, $title, $data, esc_html( $b['label'] ) ) 198 : sprintf( '<button type="button" class="%s"%s%s%s>%s</button>', esc_attr( $class ), $id, $title, $data, esc_html( $b['label'] ) ); 199 } 200 201 /** 202 * The Patchwork bar: printed as a template in the footer and placed at the top of the 203 * items box by a few lines of script. WooCommerce rebuilds the items box when items 204 * change, so the script puts the bar back whenever it goes missing. 205 */ 206 public function render_bar(): void { 207 $order = self::current_order(); 208 if ( ! $order ) return; 209 $buttons = $this->bar_buttons( $order ); 210 $badges = $this->badges_html( $order ); 211 if ( ! $buttons && $badges === '' ) return; 212 ?> 213 <template id="pwc-order-bar-tpl"> 214 <div id="pwc-order-bar" class="pwc-order-bar"> 215 <div class="pwc-order-bar-info"> 216 <span class="pwc-order-bar-logo" aria-hidden="true" style="background-image:url('<?php echo esc_attr( Admin::icon_svg( '#fff' ) ); ?>')"></span> 217 <div> 218 <div class="pwc-order-bar-title">Patchwork</div> 219 <?php if ( $badges !== '' ) : ?><div class="pwc-order-bar-badges"><?php echo $badges; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in badge() ?></div><?php endif; ?> 220 </div> 221 </div> 222 <?php if ( $buttons ) : ?> 223 <div class="pwc-order-bar-buttons"><?php foreach ( $buttons as $b ) echo self::button_html( $b ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in button_html() ?></div> 224 <?php endif; ?> 225 </div> 226 </template> 227 <script> 228 ( function () { 229 var tpl = document.getElementById( 'pwc-order-bar-tpl' ); 230 var box = document.querySelector( '#woocommerce-order-items .inside' ); 231 if ( ! tpl || ! box ) return; 232 var place = function () { 233 if ( box.querySelector( '#pwc-order-bar' ) ) return; 234 box.insertBefore( tpl.content.cloneNode( true ), box.firstChild ); 235 document.dispatchEvent( new CustomEvent( 'patchwork:order-bar' ) ); 236 }; 237 place(); 238 new MutationObserver( place ).observe( box, { childList: true } ); 239 } )(); 240 </script> 241 <?php 242 } 243 244 /** 245 * A line of Patchwork info under each product on the order screen (also after 246 * WooCommerce reloads the items, as it renders them with the same hook). 247 */ 248 public function render_item_info( $item_id, $item, $product ): void { 249 if ( ! is_admin() || ! $item instanceof \WC_Order_Item_Product ) return; 250 $order = $item->get_order(); 251 if ( ! $order instanceof \WC_Order ) return; 252 $parts = []; 253 foreach ( $this->modules_with( 'order_item_info' ) as $module ) { 254 $html = (string) $module->order_item_info( $item, $order ); 255 if ( $html !== '' ) $parts[] = '<div class="pwc-item-info-part pwc-item-info-' . esc_attr( $module->id() ) . '">' . wp_kses( $html, self::allowed_html() ) . '</div>'; 256 } 257 if ( $parts ) echo '<div class="pwc-item-info">' . implode( '', $parts ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- each part passed through wp_kses 258 } 259 260 // ─── Orders list preview ────────────────────────────────────────────────── 261 262 /** The Patchwork part of the preview: badges, each module's lines, and its preview buttons. */ 263 public function preview_html( \WC_Order $order ): string { 264 $badges = $this->badges_html( $order ); 265 $sections = ''; 266 foreach ( $this->modules_with( 'order_preview' ) as $module ) { 267 $html = (string) $module->order_preview( $order ); 268 if ( $html === '' ) continue; 269 $sections .= '<div class="pwc-preview-section"><strong>' . esc_html( preg_replace( '/^Patchwork\s+/i', '', $module->name() ) ) . '</strong> ' . wp_kses( $html, self::allowed_html() ) . '</div>'; 270 } 271 $buttons = ''; 272 foreach ( $this->bar_buttons( $order, true ) as $b ) { 273 $b['id'] = ''; 274 $buttons .= self::button_html( $b, $order->get_id() ); 275 } 276 if ( $badges === '' && $sections === '' && $buttons === '' ) return ''; 277 return '<div id="pwc-preview" class="pwc-preview"><h2>Patchwork</h2>' 278 . ( $badges !== '' ? '<div class="pwc-preview-badges">' . $badges . '</div>' : '' ) 279 . $sections 280 . ( $buttons !== '' ? '<div class="pwc-preview-buttons">' . $buttons . '</div>' : '' ) 281 . '</div>'; 282 } 283 284 public function preview_details( $details, $order ) { 285 if ( is_array( $details ) && $order instanceof \WC_Order ) { 286 $details['patchwork_html'] = $this->preview_html( $order ); 287 } 288 return $details; 289 } 290 291 public function preview_template(): void { 292 echo '<# if ( data.patchwork_html ) { #>{{{ data.patchwork_html }}}<# } #>'; 293 } 113 294 } -
patchwork-order-split/trunk/lib/patchwork-core/src/class-support.php
r3783125 r3794384 105 105 106 106 $modules = $this->core->modules(); 107 if ( $form['plugin'] !== 'other'&& ! isset( $modules[ $form['plugin'] ] ) ) return $fail( __( 'Choose which plugin this is about.', 'patchwork-order-split' ) );107 if ( ! in_array( $form['plugin'], [ 'other', 'custom' ], true ) && ! isset( $modules[ $form['plugin'] ] ) ) return $fail( __( 'Choose which plugin this is about.', 'patchwork-order-split' ) ); 108 108 if ( $form['subject'] === '' ) return $fail( __( 'Add a short subject.', 'patchwork-order-split' ) ); 109 109 if ( mb_strlen( $form['message'] ) < 10 ) return $fail( __( 'Describe the issue in a little more detail.', 'patchwork-order-split' ) ); … … 117 117 if ( is_wp_error( $files ) ) return $fail( $files->get_error_message() ); 118 118 119 $plugin_name = $form['plugin'] === 'other' ? __( 'Something else', 'patchwork-order-split' ) : $modules[ $form['plugin'] ]->name();119 $plugin_name = $form['plugin'] === 'other' ? __( 'Something else', 'patchwork-order-split' ) : ( $form['plugin'] === 'custom' ? __( 'Custom work', 'patchwork-order-split' ) : $modules[ $form['plugin'] ]->name() ); 120 120 $user = wp_get_current_user(); 121 121 … … 231 231 ?> 232 232 <div class="wrap pwc-wrap"> 233 <?php Admin::back_link(); ?> 233 234 <?php Admin::page_header( __( 'Get support', 'patchwork-order-split' ), __( 'Tell us what\'s going on and we\'ll get back to you by email.', 'patchwork-order-split' ) ); ?> 234 235 … … 251 252 <option value="<?php echo esc_attr( $id ); ?>" <?php selected( $pre, $id ); ?>><?php echo esc_html( $module->name() ); ?></option> 252 253 <?php endforeach; ?> 254 <option value="custom" <?php selected( $pre, 'custom' ); ?>><?php esc_html_e( 'Custom work (a feature or an integration)', 'patchwork-order-split' ); ?></option> 253 255 <option value="other" <?php selected( $pre, 'other' ); ?>><?php esc_html_e( 'Something else', 'patchwork-order-split' ); ?></option> 254 256 </select> … … 300 302 </div> 301 303 </form> 304 305 <div class="pwc-card pwc-custom-work"> 306 <h2 class="pwc-card-title"><?php esc_html_e( 'Need something the plugins don\'t do?', 'patchwork-order-split' ); ?></h2> 307 <p><?php esc_html_e( 'We also build custom features and integrations for WordPress and WooCommerce: for example, sending your orders straight into a wholesaler\'s ordering system (if your supplier allows it), syncing stock with another system, or a workflow that fits how your business runs.', 'patchwork-order-split' ); ?></p> 308 <p><a href="<?php echo esc_url( self::url( [ 'plugin' => 'custom' ] ) ); ?>"><?php esc_html_e( 'Tell us what you need →', 'patchwork-order-split' ); ?></a></p> 309 </div> 302 310 </div> 303 311 <?php -
patchwork-order-split/trunk/patchwork-order-split.php
r3783125 r3794384 3 3 /** 4 4 * Plugin Name: Patchwork Split – Split & Merge Orders for WooCommerce 5 * Plugin URI: https://patchwork.guru/plugins/ 5 6 * Description: Split WooCommerce orders into child orders, split part of a line's quantity, and record items you are unable to fulfil – with correct totals, taxes and stock. 6 * Version: 1.1. 17 * Version: 1.1.2 7 8 * Author: Patchwork 9 * Author URI: https://patchwork.guru/ 8 10 * License: GPL-2.0-or-later 9 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 19 21 } 20 22 // ─── Constants ─────────────────────────────────────────────────────────────── 21 define( 'PATCHWORK_SPLIT_VERSION', '1.1. 1' );23 define( 'PATCHWORK_SPLIT_VERSION', '1.1.2' ); 22 24 define( 'PATCHWORK_SPLIT_FILE', __FILE__ ); 23 25 define( 'PATCHWORK_SPLIT_DIR', plugin_dir_path( __FILE__ ) ); -
patchwork-order-split/trunk/readme.txt
r3783125 r3794384 1 1 === Patchwork Split – Split & Merge Orders for WooCommerce === 2 2 Contributors: patchworkstudio 3 Tags: woocommerce, split order, backorder, partial shipment, order management3 Tags: woocommerce, split order, merge orders, backorder, partial shipment 4 4 Requires at least: 6.2 5 5 Tested up to: 7.1 … … 7 7 WC requires at least: 7.0 8 8 WC tested up to: 11.2 9 Stable tag: 1.1. 19 Stable tag: 1.1.2 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 53 53 Hooks such as `patchwork_split_validate_batch`, `patchwork_split_before_child_save`, `patchwork_split_completed` and `patchwork_split_order_badges`, plus a small JavaScript extension API, let other plugins add to the manager. 54 54 55 = More from Patchwork = 56 57 Patchwork builds practical WordPress plugins and custom fixes. 58 59 * [Patchwork Split – features and Pro](https://patchwork.guru/plugins/patchwork-split/) 60 * [Guide: how to split a WooCommerce order](https://patchwork.guru/guides/how-to-split-a-woocommerce-order/) 61 * [Guide: how to merge WooCommerce orders](https://patchwork.guru/guides/how-to-merge-woocommerce-orders/) 62 * [Need something custom for your store or site?](https://patchwork.guru/services/) 63 * [Support](https://patchwork.guru/support/) 64 55 65 == Installation == 56 66 … … 58 68 2. Activate it. WooCommerce must be active. 59 69 3. Optional: adjust reasons, shipping, backorders and emails under **Patchwork → Split**. 60 4. Open any order and click ** Open manager**above the order items.70 4. Open any order and click **Split order** in the Patchwork bar above the order items. 61 71 62 72 == Frequently Asked Questions == 73 74 = How do I split a WooCommerce order into two shipments? = 75 76 Open the order and click **Split order** in the Patchwork bar above the order items. Mark the items (or part of an item's quantity) that should ship later as **Split**, then apply. They move into one new order, and the original keeps the rest, ready to ship now. 77 78 = Does splitting an order change the tax or the price? = 79 80 No. Prices, discounts and taxes are carried over exactly as the customer was charged — never recalculated at today's rates — and the original order plus the new one always add up to the original total. 81 82 = Can I merge two WooCommerce orders? = 83 84 Yes, with Pro. Open orders from the same customer can be combined so they ship together. Both orders must be in the same payment state (both paid or both unpaid), so payments and stock stay straight. 85 86 = How does it handle backorders? = 87 88 Orders with backordered items get a "⚠ on backorder" badge and a notice on the order. One click opens the manager with the backordered units already selected, so the rest can ship now — or dismiss it to ship everything together. Pro can split backorders automatically when an order is paid. 63 89 64 90 = Who can use the manager? = … … 109 135 == Changelog == 110 136 137 = 1.1.2 = 138 * Order screen: Split's buttons now sit in one shared Patchwork bar above the order items, with this order's badges. Every Patchwork plugin uses the same bar, the same Patchwork box and the same line under each product, so nothing is scattered. 139 * Orders list preview (the eye icon) has a matching Patchwork section with the badges and a Split order button. 140 * With Patchwork Inventory: when stock ordered for an order arrives, the "on backorder" badge and notice clear by themselves. 141 * Patchwork dashboard: a link back to all Patchwork plugins, shortcut buttons on each plugin's card, and a "custom work" option on the Support page. 142 * Guide links on Split's page and in this readme. 143 * Fix: after an update, browsers now always load the new scripts and styles (no more stale copies). 144 111 145 = 1.1.1 = 112 146 * Security: order actions now check that you can edit that specific order (respects plugins that limit which orders a user can edit).
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)