Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 155
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Woo_Sync_Woo_Admin_Integration
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 9
1122
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 instance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 init_hooks
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 append_orders_column
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 render_orders_column_content
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 add_meta_boxes
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 render_woo_order_page_contact_box
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
56
 get_contact_id_from_order
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 render_metabox_styles
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 *
6 * WooSync: Woo Admin class
7 *  Collects CRM additions to the WooCommerce backend UI
8 */
9namespace Automattic\JetpackCRM;
10
11// block direct access
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14/**
15 * WooSync Woo Admin class
16 */
17class Woo_Sync_Woo_Admin_Integration {
18
19    /**
20     * The single instance of the class.
21     */
22    protected static $_instance = null;
23
24    /**
25     * Setup WooSync
26     * Note: This will effectively fire after core settings and modules loaded
27     * ... effectively on tail end of `init`
28     */
29    public function __construct() {
30        // Initialise Hooks
31        $this->init_hooks();
32    }
33
34    /**
35     * Main Class Instance.
36     *
37     * Ensures only one instance of Woo_Sync_Woo_Admin_Integration is loaded or can be loaded.
38     *
39     * @since 2.0
40     * @static
41     * @return Woo_Sync_Woo_Admin_Integration main instance
42     */
43    public static function instance() {
44        if ( self::$_instance === null ) {
45            self::$_instance = new self();
46        }
47        return self::$_instance;
48    }
49
50    /**
51     * Initialise Hooks
52     */
53    private function init_hooks() {
54        global $zbs;
55
56        // Abort if Woo isn't active.
57        if ( ! $zbs->woocommerce_is_active() ) {
58            return;
59        }
60
61        // Hook into Woo orders listview.
62        if ( jpcrm_woosync_is_hpos_enabled() ) {
63            // These hooks are available as of Woo 7.3.0 and are required for HPOS.
64            add_filter( 'woocommerce_shop_order_list_table_columns', array( $this, 'append_orders_column' ) );
65            add_action( 'woocommerce_shop_order_list_table_custom_column', array( $this, 'render_orders_column_content' ), 20, 2 );
66        } else {
67            add_filter( 'manage_edit-shop_order_columns', array( $this, 'append_orders_column' ) );
68            add_action( 'manage_shop_order_posts_custom_column', array( $this, 'render_orders_column_content' ), 20, 2 );
69        }
70
71        // Add CRM contact to Woo order page.
72        add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
73    }
74
75    /**
76     * Add CRM contact column to Woo orders listview.
77     *
78     * @param array $columns Listview columns.
79     */
80    public function append_orders_column( $columns ) {
81
82        $rebuilt_columns = array();
83
84        // Inserting columns to a specific location
85        foreach ( $columns as $key => $column ) {
86
87            $rebuilt_columns[ $key ] = $column;
88
89            if ( $key === 'order_status' ) {
90                // Inserting after "Status" column
91                $rebuilt_columns['jpcrm_contact'] = __( 'CRM Contact', 'zero-bs-crm' );
92            }
93        }
94
95        return $rebuilt_columns;
96    }
97
98    /**
99     * HTML rendering of our custom orders view column content
100     *
101     * @param string $column Column slug.
102     * @param int    $order_or_post_id Order post ID.
103     */
104    public function render_orders_column_content( $column, $order_or_post_id ) {
105
106        global $zbs;
107        switch ( $column ) {
108
109            case 'jpcrm_contact':
110                if ( jpcrm_woosync_is_hpos_enabled() ) {
111                    $order = $order_or_post_id;
112                } else {
113                    $order = wc_get_order( $order_or_post_id );
114                }
115
116                // Use the helper method to find contact by email or WordPress user ID.
117                $contact_id = $this->get_contact_id_from_order( $order );
118
119                if ( $contact_id > 0 ) {
120                    $url   = jpcrm_esc_link( 'view', $contact_id, 'zerobs_customer' );
121                    $label = __( 'View Contact', 'zero-bs-crm' );
122                    $class = 'primary';
123                } else {
124                    $url   = admin_url( 'admin.php?page=' . $zbs->modules->woosync->slugs['hub'] );
125                    $label = __( 'Add Contact', 'zero-bs-crm' );
126                    $class = 'secondary';
127                }
128                ?>
129                <div class="zbs-actions">
130                    <a class="button button-<?php echo esc_attr( $class ); ?>" href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( $label ); ?></a>
131                </div>
132                <?php
133                break;
134        }
135    }
136
137    /**
138     * Add CRM meta boxes to Woo pages
139     */
140    public function add_meta_boxes() {
141
142        // Gather Woo screens where we'll want to add metaboxes.
143        $screens_to_use = array();
144        $woo_screens    = array( 'shop_order', 'shop_subscription' );
145        foreach ( $woo_screens as $woo_screen ) {
146            $potential_screen = wc_get_page_screen_id( $woo_screen );
147            if ( ! empty( $potential_screen ) ) {
148                $screens_to_use[] = $potential_screen;
149            }
150        }
151
152        // Currently if Woo is active we should at least have the orders page, but that could change.
153        if ( ! empty( $screens_to_use ) ) {
154            add_meta_box(
155                'zbs_crm_contact',
156                __( 'CRM Contact', 'zero-bs-crm' ),
157                array( $this, 'render_woo_order_page_contact_box' ),
158                $screens_to_use,
159                'side',
160                'core'
161            );
162        }
163    }
164
165    /**
166     * Renders HTML for contact metabox on Woo pages
167     *
168     * @param WC_Order|\WP_POST $order_or_post Order or post.
169     */
170    public function render_woo_order_page_contact_box( $order_or_post ) {
171        // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
172
173        global $zbs;
174        if ( jpcrm_woosync_is_hpos_enabled() ) {
175            $order = $order_or_post;
176        } else {
177            $order = wc_get_order( $order_or_post->ID );
178        }
179
180        $this->render_metabox_styles();
181
182        // the customer information pane
183        $billing_email = $order->get_billing_email();
184
185        // No billing email found, so render message and return.
186        if ( empty( $billing_email ) ) {
187            ?>
188            <div class="jpcrm-contact-metabox">
189                <div class="no-crm-contact">
190                    <p style="margin-top:20px;">
191                    <?php esc_html_e( 'Once you save your order to a customer with a billing email, the CRM contact card will display here.', 'zero-bs-crm' ); ?>
192                    </p>
193                </div>
194            </div>
195            <?php
196            return;
197        }
198
199        // Use the helper method to find contact by email or WordPress user ID.
200        $contact_id = $this->get_contact_id_from_order( $order );
201
202        // Can't find a contact, show "Add Contact" button.
203        if ( $contact_id <= 0 ) {
204            ?>
205            <div class="jpcrm-contact-metabox">
206                <div class="no-crm-contact">
207                    <p style="margin-top:20px;">
208                    <?php esc_html_e( 'No CRM contact found for this order.', 'zero-bs-crm' ); ?>
209                    </p>
210                    <div class="panel-edit-contact">
211                        <a class="view-contact-link button button-secondary" href="<?php echo esc_url( admin_url( 'admin.php?page=' . $zbs->modules->woosync->slugs['hub'] ) ); ?>"><?php echo esc_html__( 'Add Contact', 'zero-bs-crm' ); ?></a>
212                    </div>
213                </div>
214            </div>
215            <?php
216            return;
217        }
218
219        // retrieve contact
220        $crm_contact               = $zbs->DAL->contacts->getContact( $contact_id );
221        $contact_name              = $zbs->DAL->contacts->getContactFullNameEtc( $contact_id, $crm_contact, array( false, false ) );
222        $contact_transaction_count = $zbs->DAL->specific_obj_type_count_for_assignee( $contact_id, ZBS_TYPE_TRANSACTION, ZBS_TYPE_CONTACT ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
223
224        // Use contact email from CRM if available, otherwise fallback to billing email.
225        $display_email = ! empty( $crm_contact['email'] ) ? $crm_contact['email'] : $billing_email;
226
227        // check avatar mode
228        $avatar      = '';
229        $avatar_mode = zeroBSCRM_getSetting( 'avatarmode' );
230        if ( $avatar_mode !== 3 ) {
231            $avatar = zeroBS_customerAvatarHTML( $contact_id, $crm_contact, 100, 'ui small image centered' );
232        }
233        ?>
234
235        <div class="jpcrm-contact-metabox">
236            <?php echo $avatar; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */ ?>
237            <div id="panel-name"><span class="jpcrm-name"><?php echo esc_html( $contact_name ); ?></span></div>
238            <div id="panel-status" class="ui label status <?php echo esc_attr( strtolower( $crm_contact['status'] ) ); ?>">
239                <?php echo esc_html( $crm_contact['status'] ); ?>
240            </div>
241
242            <div>
243                <?php
244                echo esc_html( zeroBSCRM_prettifyLongInts( $contact_transaction_count ) . ' ' . ( $contact_transaction_count > 1 ? __( 'Transactions', 'zero-bs-crm' ) : __( 'Transaction', 'zero-bs-crm' ) ) );
245                ?>
246            </div>
247
248            <div class="panel-left-info cust-email">
249                <i class="ui icon envelope outline"></i> <span class="panel-customer-email"><?php echo esc_html( $display_email ); ?></span>
250            </div>
251
252            <div class="panel-edit-contact">
253                <a class="view-contact-link button button-primary" href="<?php echo esc_url( jpcrm_esc_link( 'view', $contact_id, 'zerobs_customer' ) ); ?>"><?php echo esc_html__( 'View Contact', 'zero-bs-crm' ); ?></a>
254            </div>
255        </div>
256        <?php
257        // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
258    }
259
260    /**
261     * Get CRM contact ID from WooCommerce order.
262     *
263     * This method attempts to identify the CRM contact associated with a WooCommerce order.
264     *
265     * Priority order:
266     * 1. If order has a customer (WordPress user), use that to find CRM contact
267     * 2. If order is from a guest (no WordPress user), use billing email
268     *
269     * This ensures that logged-in customers are always identified by their account,
270     * even if they use a different billing email on the order.
271     *
272     * @param \WC_Order $order WooCommerce order object.
273     * @return int CRM contact ID, or 0 if not found.
274     */
275    public function get_contact_id_from_order( $order ) {
276        // First, check if the order has a customer (WordPress user).
277        // If so, use their account to find the CRM contact.
278        $customer_id = $order->get_customer_id();
279        if ( $customer_id > 0 ) {
280            // Try to find CRM contact linked to this WordPress user.
281            $contact_id = zeroBS_getCustomerIDFromWPID( $customer_id );
282            if ( $contact_id > 0 ) {
283                return $contact_id;
284            }
285
286            // If no CRM contact is linked to WP user, try the WP user's email.
287            $wp_user = get_user_by( 'id', $customer_id );
288            if ( $wp_user && ! empty( $wp_user->user_email ) ) {
289                $contact_id = zeroBS_getCustomerIDWithEmail( $wp_user->user_email );
290                if ( $contact_id > 0 ) {
291                    return $contact_id;
292                }
293            }
294        }
295
296        // Fallback for guest orders: use billing email.
297        $billing_email = $order->get_billing_email();
298        if ( ! empty( $billing_email ) ) {
299            $contact_id = zeroBS_getCustomerIDWithEmail( $billing_email );
300            if ( $contact_id > 0 ) {
301                return $contact_id;
302            }
303        }
304
305        return 0;
306    }
307
308    /**
309     * Renders metabox styles.
310     *
311     * It'd be better to move this to its own file, but putting it here for now.
312     */
313    public function render_metabox_styles() {
314        ?>
315        <style>
316        .jpcrm-contact-metabox{
317            margin: 20px 0;
318            text-align:center;
319        }
320        .zbs-custom-avatar{
321            border-radius: 50% !important;
322            max-width:80px;
323            text-align:center;
324            padding:10px;
325        }
326        .view-contact-link{
327            margin-top:10px;
328        }
329        .cust-email{
330            padding-bottom:10px;
331            padding-top:10px;
332            color: black;
333            font-weight:700;
334        }
335        .jpcrm-name{
336            font-weight:900;
337        }
338        .status{
339            margin-left: 0;
340            padding: 0.3em 0.78571429em;
341            display: inline-block;
342            border-radius: 5px;
343            margin-top: 3px;
344            margin-bottom: 3px;
345            font-size: 12px !important;
346            font-weight: 500;
347            background-color: #ccc;
348        }
349        .customer{
350            background-color: #21BA45 !important;
351            border-color: #21BA45 !important;
352            color: #FFFFFF !important;
353        }
354        </style>
355        <?php
356    }
357}