Plugin Directory

Changeset 3794384


Ignore:
Timestamp:
11/10/2026 12:30:19 PM (28 hours ago)
Author:
patchworkstudio
Message:

Release 1.1.2

Location:
patchwork-order-split
Files:
24 edited
1 copied

Legend:

Unmodified
Added
Removed
  • patchwork-order-split/tags/1.1.2/assets/js/manager.js

    r3783125 r3794384  
    6464    // ── Edit screen: toolbar above the items table ─────────────────────────────
    6565
     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
    6675    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        }
    6782        const $anchor = $('#woocommerce-order-items .inside').first();
    6883        if (!$anchor.length || $('#pwsplit-toolbar').length) return;
    … …  
    86101            </div>`);
    87102
    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);
    96104    }
    97105
    … …  
    144152            if (target === 'wc-modal-view-order') setTimeout(injectPreviewButton, 50);
    145153        });
     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        });
    146169    }
    147170
    148171    function injectPreviewButton() {
    149172        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;
    151174
    152175        // 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  
    2323        if ( ! $order_id && ! $on_list ) return;
    2424
    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 );
    2727
    2828        wp_localize_script( 'patchwork-split-manager', 'PatchworkSplitConfig', [
  • patchwork-order-split/tags/1.1.2/includes/class-backorders.php

    r3783125 r3794384  
    3232        add_action( 'woocommerce_reduce_order_stock', [ $this, 'record' ] );
    3333        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 );
    3436
    3537        add_filter( 'patchwork_split_order_badges', [ $this, 'badge' ], 5, 2 );
    … …  
    143145        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound, WordPress.WP.I18n.TextDomainMismatch
    144146        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();
    145176    }
    146177
  • patchwork-order-split/tags/1.1.2/includes/class-module.php

    r3783125 r3794384  
    172172            'tone'   => 'warning',
    173173        ];
     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 );
    174201    }
    175202
  • patchwork-order-split/tags/1.1.2/lib/patchwork-core/assets/core.css

    r3783125 r3794384  
    4747}
    4848img.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; }
    5050.pwc-catalog-card { background: #fafafa; }
    5151.pwc-pill {
    … …  
    112112.pwc-support-privacy { font-size: 12px; margin: 4px 0 14px; }
    113113.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  
    5252}
    5353
    54 patchwork_core_register( '0.4.0', __DIR__ );
     54patchwork_core_register( '0.6.0', __DIR__ );
  • patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-admin.php

    r3783125 r3794384  
    9393        if ( ! $is_patchwork && ! $is_orders ) return;
    9494
    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' ) );
    9696    }
    9797
    … …  
    135135
    136136                        <?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                            ?>
    141148                            <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>
    143151                                <a class="button" href="<?php echo esc_url( $module->admin_url( 'activity' ) ); ?>"><?php esc_html_e( 'Activity', 'patchwork-order-split' ); ?></a>
    144152                            </p>
    … …  
    178186
    179187    // ─── 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    }
    180220
    181221    private function render_module( Module $module ): void {
    … …  
    187227            'licence'  => __( 'Licence', 'patchwork-order-split' ),
    188228        ];
     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'] );
    189231        if ( ! isset( $tabs[ $tab ] ) ) $tab = 'settings';
    190232        ?>
    191233        <div class="wrap pwc-wrap">
     234            <?php self::back_link(); ?>
    192235            <?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; ?>
    193239
    194240            <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  • patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-module.php

    r3783125 r3794384  
    5353    public function links(): array { return []; }
    5454
     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
    5563    // ─── Settings ─────────────────────────────────────────────────────────────
    5664
    … …  
    8290     *   'notice'  => '',                // short status line, optional
    8391     *   'tone'    => 'ok|warning',
     92     *   'action'  => [ 'label' => '', 'url' => '' ], // optional: the fix for the notice (core 0.5.0)
    8493     * ]
    8594     */
    … …  
    99108     */
    100109    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 ''; }
    101130
    102131    // ─── Licensing (Freemius) ─────────────────────────────────────────────────
  • patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-orders.php

    r3783125 r3794384  
    55
    66/**
    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.
    1215 */
    1316final class Orders {
    … …  
    2730        add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_column' ], 10, 2 );
    2831
    29         // Order screen box
     32        // Order screen: side box, bar, item lines
    3033        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 ) );
    3145    }
    3246
    … …  
    5468        $order = $order instanceof \WC_Order ? $order : wc_get_order( $order );
    5569        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 = '';
    5776        foreach ( $this->core->modules_supporting( 'orders_column' ) as $module ) {
    5877            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 );
    6079            }
    6180        }
     81        return $out;
    6282    }
    6383
    … …  
    6989        $label = esc_html( $badge['label'] ?? '' );
    7090        $title = esc_attr( $badge['title'] ?? '' );
     91        $style = self::badge_style( (string) ( $badge['color'] ?? '' ) );
    7192        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 ─────────────────────────────────────────────────────────
    77125
    78126    public function add_meta_box(): void {
    … …  
    85133            wc_get_page_screen_id( 'shop-order' ),
    86134            'side',
    87             'default'
     135            'high'
    88136        );
    89137    }
    … …  
    101149            if ( $html === '' ) continue;
    102150            $sections++;
    103             echo '<div class="pwc-panel-section">';
     151            echo '<div class="pwc-panel-section pwc-panel-' . esc_attr( $module->id() ) . '">';
    104152            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() );
    106154            echo '</div>';
    107155        }
    … …  
    111159        }
    112160    }
     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    }
    113294}
  • patchwork-order-split/tags/1.1.2/lib/patchwork-core/src/class-support.php

    r3783125 r3794384  
    105105
    106106        $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' ) );
    108108        if ( $form['subject'] === '' )                return $fail( __( 'Add a short subject.', 'patchwork-order-split' ) );
    109109        if ( mb_strlen( $form['message'] ) < 10 )    return $fail( __( 'Describe the issue in a little more detail.', 'patchwork-order-split' ) );
    … …  
    117117        if ( is_wp_error( $files ) ) return $fail( $files->get_error_message() );
    118118
    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() );
    120120        $user        = wp_get_current_user();
    121121
    … …  
    231231        ?>
    232232        <div class="wrap pwc-wrap">
     233            <?php Admin::back_link(); ?>
    233234            <?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' ) ); ?>
    234235
    … …  
    251252                                        <option value="<?php echo esc_attr( $id ); ?>" <?php selected( $pre, $id ); ?>><?php echo esc_html( $module->name() ); ?></option>
    252253                                    <?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>
    253255                                    <option value="other" <?php selected( $pre, 'other' ); ?>><?php esc_html_e( 'Something else', 'patchwork-order-split' ); ?></option>
    254256                                </select>
    … …  
    300302                </div>
    301303            </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>
    302310        </div>
    303311        <?php
  • patchwork-order-split/tags/1.1.2/patchwork-order-split.php

    r3783125 r3794384  
    33/**
    44 * Plugin Name:          Patchwork Split – Split & Merge Orders for WooCommerce
     5 * Plugin URI:           https://patchwork.guru/plugins/
    56 * 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.1
     7 * Version:              1.1.2
    78 * Author:               Patchwork
     9 * Author URI:           https://patchwork.guru/
    810 * License:              GPL-2.0-or-later
    911 * License URI:          https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1921}
    2022// ─── Constants ───────────────────────────────────────────────────────────────
    21 define( 'PATCHWORK_SPLIT_VERSION', '1.1.1' );
     23define( 'PATCHWORK_SPLIT_VERSION', '1.1.2' );
    2224define( 'PATCHWORK_SPLIT_FILE', __FILE__ );
    2325define( 'PATCHWORK_SPLIT_DIR', plugin_dir_path( __FILE__ ) );
  • patchwork-order-split/tags/1.1.2/readme.txt

    r3783125 r3794384  
    11=== Patchwork Split – Split & Merge Orders for WooCommerce ===
    22Contributors: patchworkstudio
    3 Tags: woocommerce, split order, backorder, partial shipment, order management
     3Tags: woocommerce, split order, merge orders, backorder, partial shipment
    44Requires at least: 6.2
    55Tested up to: 7.1
    … …  
    77WC requires at least: 7.0
    88WC tested up to: 11.2
    9 Stable tag: 1.1.1
     9Stable tag: 1.1.2
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    5353Hooks 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.
    5454
     55= More from Patchwork =
     56
     57Patchwork 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
    5565== Installation ==
    5666
    … …  
    58682. Activate it. WooCommerce must be active.
    59693. Optional: adjust reasons, shipping, backorders and emails under **Patchwork → Split**.
    60 4. Open any order and click **Open manager** above the order items.
     704. Open any order and click **Split order** in the Patchwork bar above the order items.
    6171
    6272== Frequently Asked Questions ==
     73
     74= How do I split a WooCommerce order into two shipments? =
     75
     76Open 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
     80No. 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
     84Yes, 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
     88Orders 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.
    6389
    6490= Who can use the manager? =
    … …  
    109135== Changelog ==
    110136
     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
    111145= 1.1.1 =
    112146* 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  
    6464    // ── Edit screen: toolbar above the items table ─────────────────────────────
    6565
     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
    6675    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        }
    6782        const $anchor = $('#woocommerce-order-items .inside').first();
    6883        if (!$anchor.length || $('#pwsplit-toolbar').length) return;
    … …  
    86101            </div>`);
    87102
    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);
    96104    }
    97105
    … …  
    144152            if (target === 'wc-modal-view-order') setTimeout(injectPreviewButton, 50);
    145153        });
     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        });
    146169    }
    147170
    148171    function injectPreviewButton() {
    149172        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;
    151174
    152175        // Order ID from the modal's Edit link (HPOS: &id=, legacy: post=)
  • patchwork-order-split/trunk/includes/class-admin.php

    r3783125 r3794384  
    2323        if ( ! $order_id && ! $on_list ) return;
    2424
    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 );
    2727
    2828        wp_localize_script( 'patchwork-split-manager', 'PatchworkSplitConfig', [
  • patchwork-order-split/trunk/includes/class-backorders.php

    r3783125 r3794384  
    3232        add_action( 'woocommerce_reduce_order_stock', [ $this, 'record' ] );
    3333        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 );
    3436
    3537        add_filter( 'patchwork_split_order_badges', [ $this, 'badge' ], 5, 2 );
    … …  
    143145        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound, WordPress.WP.I18n.TextDomainMismatch
    144146        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();
    145176    }
    146177
  • patchwork-order-split/trunk/includes/class-module.php

    r3783125 r3794384  
    172172            'tone'   => 'warning',
    173173        ];
     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 );
    174201    }
    175202
  • patchwork-order-split/trunk/lib/patchwork-core/assets/core.css

    r3783125 r3794384  
    4747}
    4848img.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; }
    5050.pwc-catalog-card { background: #fafafa; }
    5151.pwc-pill {
    … …  
    112112.pwc-support-privacy { font-size: 12px; margin: 4px 0 14px; }
    113113.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  
    5252}
    5353
    54 patchwork_core_register( '0.4.0', __DIR__ );
     54patchwork_core_register( '0.6.0', __DIR__ );
  • patchwork-order-split/trunk/lib/patchwork-core/src/class-admin.php

    r3783125 r3794384  
    9393        if ( ! $is_patchwork && ! $is_orders ) return;
    9494
    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' ) );
    9696    }
    9797
    … …  
    135135
    136136                        <?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                            ?>
    141148                            <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>
    143151                                <a class="button" href="<?php echo esc_url( $module->admin_url( 'activity' ) ); ?>"><?php esc_html_e( 'Activity', 'patchwork-order-split' ); ?></a>
    144152                            </p>
    … …  
    178186
    179187    // ─── 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    }
    180220
    181221    private function render_module( Module $module ): void {
    … …  
    187227            'licence'  => __( 'Licence', 'patchwork-order-split' ),
    188228        ];
     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'] );
    189231        if ( ! isset( $tabs[ $tab ] ) ) $tab = 'settings';
    190232        ?>
    191233        <div class="wrap pwc-wrap">
     234            <?php self::back_link(); ?>
    192235            <?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; ?>
    193239
    194240            <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  • patchwork-order-split/trunk/lib/patchwork-core/src/class-module.php

    r3783125 r3794384  
    5353    public function links(): array { return []; }
    5454
     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
    5563    // ─── Settings ─────────────────────────────────────────────────────────────
    5664
    … …  
    8290     *   'notice'  => '',                // short status line, optional
    8391     *   'tone'    => 'ok|warning',
     92     *   'action'  => [ 'label' => '', 'url' => '' ], // optional: the fix for the notice (core 0.5.0)
    8493     * ]
    8594     */
    … …  
    99108     */
    100109    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 ''; }
    101130
    102131    // ─── Licensing (Freemius) ─────────────────────────────────────────────────
  • patchwork-order-split/trunk/lib/patchwork-core/src/class-orders.php

    r3783125 r3794384  
    55
    66/**
    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.
    1215 */
    1316final class Orders {
    … …  
    2730        add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_column' ], 10, 2 );
    2831
    29         // Order screen box
     32        // Order screen: side box, bar, item lines
    3033        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 ) );
    3145    }
    3246
    … …  
    5468        $order = $order instanceof \WC_Order ? $order : wc_get_order( $order );
    5569        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 = '';
    5776        foreach ( $this->core->modules_supporting( 'orders_column' ) as $module ) {
    5877            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 );
    6079            }
    6180        }
     81        return $out;
    6282    }
    6383
    … …  
    6989        $label = esc_html( $badge['label'] ?? '' );
    7090        $title = esc_attr( $badge['title'] ?? '' );
     91        $style = self::badge_style( (string) ( $badge['color'] ?? '' ) );
    7192        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 ─────────────────────────────────────────────────────────
    77125
    78126    public function add_meta_box(): void {
    … …  
    85133            wc_get_page_screen_id( 'shop-order' ),
    86134            'side',
    87             'default'
     135            'high'
    88136        );
    89137    }
    … …  
    101149            if ( $html === '' ) continue;
    102150            $sections++;
    103             echo '<div class="pwc-panel-section">';
     151            echo '<div class="pwc-panel-section pwc-panel-' . esc_attr( $module->id() ) . '">';
    104152            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() );
    106154            echo '</div>';
    107155        }
    … …  
    111159        }
    112160    }
     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    }
    113294}
  • patchwork-order-split/trunk/lib/patchwork-core/src/class-support.php

    r3783125 r3794384  
    105105
    106106        $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' ) );
    108108        if ( $form['subject'] === '' )                return $fail( __( 'Add a short subject.', 'patchwork-order-split' ) );
    109109        if ( mb_strlen( $form['message'] ) < 10 )    return $fail( __( 'Describe the issue in a little more detail.', 'patchwork-order-split' ) );
    … …  
    117117        if ( is_wp_error( $files ) ) return $fail( $files->get_error_message() );
    118118
    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() );
    120120        $user        = wp_get_current_user();
    121121
    … …  
    231231        ?>
    232232        <div class="wrap pwc-wrap">
     233            <?php Admin::back_link(); ?>
    233234            <?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' ) ); ?>
    234235
    … …  
    251252                                        <option value="<?php echo esc_attr( $id ); ?>" <?php selected( $pre, $id ); ?>><?php echo esc_html( $module->name() ); ?></option>
    252253                                    <?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>
    253255                                    <option value="other" <?php selected( $pre, 'other' ); ?>><?php esc_html_e( 'Something else', 'patchwork-order-split' ); ?></option>
    254256                                </select>
    … …  
    300302                </div>
    301303            </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>
    302310        </div>
    303311        <?php
  • patchwork-order-split/trunk/patchwork-order-split.php

    r3783125 r3794384  
    33/**
    44 * Plugin Name:          Patchwork Split – Split & Merge Orders for WooCommerce
     5 * Plugin URI:           https://patchwork.guru/plugins/
    56 * 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.1
     7 * Version:              1.1.2
    78 * Author:               Patchwork
     9 * Author URI:           https://patchwork.guru/
    810 * License:              GPL-2.0-or-later
    911 * License URI:          https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1921}
    2022// ─── Constants ───────────────────────────────────────────────────────────────
    21 define( 'PATCHWORK_SPLIT_VERSION', '1.1.1' );
     23define( 'PATCHWORK_SPLIT_VERSION', '1.1.2' );
    2224define( 'PATCHWORK_SPLIT_FILE', __FILE__ );
    2325define( 'PATCHWORK_SPLIT_DIR', plugin_dir_path( __FILE__ ) );
  • patchwork-order-split/trunk/readme.txt

    r3783125 r3794384  
    11=== Patchwork Split – Split & Merge Orders for WooCommerce ===
    22Contributors: patchworkstudio
    3 Tags: woocommerce, split order, backorder, partial shipment, order management
     3Tags: woocommerce, split order, merge orders, backorder, partial shipment
    44Requires at least: 6.2
    55Tested up to: 7.1
    … …  
    77WC requires at least: 7.0
    88WC tested up to: 11.2
    9 Stable tag: 1.1.1
     9Stable tag: 1.1.2
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    5353Hooks 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.
    5454
     55= More from Patchwork =
     56
     57Patchwork 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
    5565== Installation ==
    5666
    … …  
    58682. Activate it. WooCommerce must be active.
    59693. Optional: adjust reasons, shipping, backorders and emails under **Patchwork → Split**.
    60 4. Open any order and click **Open manager** above the order items.
     704. Open any order and click **Split order** in the Patchwork bar above the order items.
    6171
    6272== Frequently Asked Questions ==
     73
     74= How do I split a WooCommerce order into two shipments? =
     75
     76Open 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
     80No. 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
     84Yes, 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
     88Orders 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.
    6389
    6490= Who can use the manager? =
    … …  
    109135== Changelog ==
    110136
     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
    111145= 1.1.1 =
    112146* 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.