PluginProbe
Live Chat & AI Chatbot – onWebChat / 3.9.0
Live Chat & AI Chatbot – onWebChat v3.9.0
3.9.2 3.9.3 3.9.1 3.9.0 3.8.4 3.8.2 3.8.1 3.8.0 3.7.2 3.7.1 3.7.0 3.6.0 3.5.5 trunk 1.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 48 releases
onwebchat / admin / tabs / woocommerce.php

woocommerce.php in Live Chat & AI Chatbot – onWebChat 3.9.0, at admin/tabs/woocommerce.php

887 lines 40.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Tab - Product Sync Settings
4 */
5
6 if (!defined('ABSPATH')) {
7 exit;
8 }
9
10 function onwebchat_woocommerce_tab() {
11
12 // Handle form submissions
13 onwebchat_handle_woocommerce_actions();
14
15 // Get current settings
16 $sync_enabled = get_option('onwebchat_wc_sync_enabled', false);
17 $sync_mode = get_option('onwebchat_wc_sync_mode', 'short_plus_full');
18 $secret = get_option('onwebchat_wc_sync_secret', '');
19 $order_lookup_enabled = get_option('onwebchat_wc_order_lookup_enabled', false);
20
21 // Get sync status
22 $sync_status = array(
23 'last_sync' => get_option('onwebchat_wc_last_bulk_sync', 0),
24 'in_progress' => get_option('onwebchat_wc_bulk_in_progress', false),
25 'done' => get_option('onwebchat_wc_bulk_done', 0),
26 'total' => get_option('onwebchat_wc_bulk_total', 0),
27 'last_sync_start' => get_option('onwebchat_wc_last_sync_start', 0),
28 );
29
30 // Calculate cooldown status
31 $cooldown_period = 5 * 60; // 5 minutes // 5min // 5 * 60 //also in the file woocommerce-sync.php
32 $time_since_last_sync = time() - $sync_status['last_sync_start'];
33 $is_in_cooldown = ($time_since_last_sync < $cooldown_period) && $sync_status['last_sync_start'] > 0;
34 $cooldown_remaining = $is_in_cooldown ? ceil(($cooldown_period - $time_since_last_sync) / 60) : 0;
35
36 ?>
37
38 <?php if (isset($_GET['wc_saved']) && $_GET['wc_saved'] == '1'): ?>
39 <div class="notice notice-success is-dismissible">
40 <p><strong>WooCommerce settings saved successfully!</strong></p>
41 </div>
42 <?php endif; ?>
43
44 <?php
45 // Check for authentication errors
46 $auth_error = get_transient('onwebchat_wc_auth_error');
47 if ($auth_error):
48 ?>
49 <div class="notice notice-error is-dismissible">
50 <p><strong>⚠️ WooCommerce Sync Authentication Error:</strong></p>
51 <p><?php echo esc_html($auth_error); ?></p>
52 <p>This usually happens when:</p>
53 <ul style="list-style: disc; margin-left: 20px;">
54 <li>You're trying to connect the plugin to a different onWebChat account</li>
55 </ul>
56 <p><strong>Solution:</strong> Please enter your onWebChat account credentials again in the "Connect WooCommerce Sync" section below and click "Connect WooCommerce Sync" button to reconnect.</p>
57 </div>
58 <?php endif; ?>
59
60 <h2>WooCommerce Product Sync</h2>
61 <p>
62 Let your AI chatbot understand your WooCommerce products.
63 When enabled, products are automatically synced and kept up to date, allowing the AI to answer product-related questions accurately.
64 </p>
65
66 <?php if (empty($secret)): ?>
67 <!-- Authentication required section -->
68 <div class="notice notice-warning" style="margin: 15px 0; padding: 15px;">
69 <h3 style="margin-top: 0;">🔐 Connect WooCommerce Sync</h3>
70 <p>
71 To enable WooCommerce sync, please enter your onWebChat account credentials below.
72 </p>
73 <p style="color: #666; font-size: 13px;">
74 <!-- <strong>💡 Tip:</strong> If you originally connected using email/password in the <a href="?page=onwebchat_settings&tab=general">General tab</a>,
75 try unlinking and re-linking your account there. WooCommerce sync will be connected automatically! -->
76 </p>
77
78 <table class="form-table" style="margin-bottom: 0;">
79 <tr>
80 <th scope="row"><label for="onwebchat_wc_email">onWebChat Email (username)</label></th>
81 <td>
82 <input type="email"
83 id="onwebchat_wc_email"
84 class="regular-text"
85 placeholder="Your registered email"
86 value="<?php echo esc_attr(get_option('onwebchat_plugin_option_user', '')); ?>">
87 </td>
88 </tr>
89 <tr>
90 <th scope="row"><label for="onwebchat_wc_password">onWebChat Password</label></th>
91 <td>
92 <input type="password"
93 id="onwebchat_wc_password"
94 class="regular-text"
95 placeholder="Your account password">
96 </td>
97 </tr>
98 </table>
99
100 <p style="margin-top: 15px;">
101 <button type="button" id="onwebchat_wc_connect" class="button button-primary">
102 Connect WooCommerce Sync
103 </button>
104 <span id="onwebchat_wc_connect_status" style="margin-left: 10px;"></span>
105 </p>
106 </div>
107
108 <script type="text/javascript">
109 jQuery(document).ready(function($) {
110 $('#onwebchat_wc_connect').on('click', function() {
111 var email = $('#onwebchat_wc_email').val();
112 var password = $('#onwebchat_wc_password').val();
113
114 if (!email || !password) {
115 alert('Please enter both email and password.');
116 return;
117 }
118
119 var $btn = $(this);
120 var $status = $('#onwebchat_wc_connect_status');
121
122 $btn.prop('disabled', true).text('Connecting...');
123 $status.html('');
124
125 $.ajax({
126 url: ajaxurl,
127 type: 'POST',
128 data: {
129 action: 'onwebchat_wc_connect',
130 email: email,
131 password: password,
132 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
133 },
134 success: function(response) {
135 if (response.success) {
136 $status.html('<span style="color: green;">✓ Connected successfully!</span>');
137 setTimeout(function() {
138 location.reload();
139 }, 1000);
140 } else {
141 // Render the server-supplied error as TEXT (never HTML) so a malicious/MITM'd
142 // API response can't inject markup into the admin page.
143 $status.empty().append(
144 $('<span/>').css('color', 'red').text('' + response.data)
145 );
146 $btn.prop('disabled', false).text('Connect WooCommerce Sync');
147 }
148 },
149 error: function() {
150 $status.html('<span style="color: red;">✗ Connection error. Please try again.</span>');
151 $btn.prop('disabled', false).text('Connect WooCommerce Sync');
152 }
153 });
154 });
155 });
156 </script>
157
158 <?php else: ?>
159 <!-- Connected - show settings -->
160
161 <form action="admin.php?page=onwebchat_settings&tab=woocommerce" method="post">
162 <input type="hidden" name="action" value="save_wc_sync">
163 <?php wp_nonce_field('onwebchat_wc_sync_nonce'); ?>
164
165 <table class="form-table">
166 <tr>
167 <th scope="row" style="line-height: 1 !important;">
168 <label for="onwebchat_wc_sync_enabled">Enable Product Sync</label>
169 </th>
170 <td>
171 <label for="onwebchat_wc_sync_enabled">
172 <input type="checkbox"
173 id="onwebchat_wc_sync_enabled"
174 name="onwebchat_wc_sync_enabled"
175 value="1"
176 <?php checked($sync_enabled, true); ?>>
177 Automatically sync products to onWebChat for AI training
178 </label>
179 <p id="onwebchat_wc_sync_status" style="margin: 8px 0 0 0; font-size: 13px; min-height: 20px; visibility: hidden; opacity: 0;">
180 </p>
181 </td>
182 </tr>
183
184 <tr>
185 <th scope="row">
186 <label for="onwebchat_wc_sync_mode">Description Mode</label>
187 </th>
188 <td>
189 <select id="onwebchat_wc_sync_mode" name="onwebchat_wc_sync_mode" style="width: 350px;">
190 <option value="short_plus_full" <?php selected($sync_mode, 'short_plus_full'); ?>>
191 Short + full description (default)
192 </option>
193 <option value="short_only" <?php selected($sync_mode, 'short_only'); ?>>
194 Short description only
195 </option>
196 <option value="short_fallback_full" <?php selected($sync_mode, 'short_fallback_full'); ?>>
197 Short description (fallback to full)
198 </option>
199 </select>
200 <p class="description">
201 Choose how product descriptions are synced for AI training.
202 The default sends both texts, so the AI bot also learns the
203 detailed description (intended use, compatibility, specs).
204 </p>
205 </td>
206 </tr>
207
208 <?php if (!empty($secret)): ?>
209 <tr>
210 <th scope="row">Connection Status</th>
211 <td>
212 <span style="color: green; font-weight: bold; vertical-align: middle;"> Connected to onWebChat</span>
213 <button type="button"
214 id="onwebchat_regenerate_secret"
215 class="button button-small"
216 style="margin-left: 10px; vertical-align: middle;">
217 Disconnect
218 </button>
219 <p class="description">
220 WooCommerce sync is securely connected. Click "Disconnect" to re-authenticate.
221 </p>
222 </td>
223 </tr>
224 <?php endif; ?>
225 </table>
226 </form>
227
228 <hr>
229
230 <h2>AI Order Status Lookup</h2>
231 <p>
232 Let your AI chatbot answer "where is my order?" with live data. The store verifies identity using the
233 order number and email (signed-in users only need the order number) before sharing details.
234 </p>
235
236 <table class="form-table">
237 <tr>
238 <th scope="row" style="line-height: 1 !important;">
239 <label for="onwebchat_wc_order_lookup_enabled">Enable Order Status Lookup</label>
240 </th>
241 <td>
242 <label for="onwebchat_wc_order_lookup_enabled">
243 <input type="checkbox"
244 id="onwebchat_wc_order_lookup_enabled"
245 name="onwebchat_wc_order_lookup_enabled"
246 value="1"
247 <?php checked($order_lookup_enabled, true); ?>>
248 Allow the AI chatbot to look up live order status (with identity verification)
249 </label>
250 <p id="onwebchat_wc_order_lookup_status" style="margin: 8px 0 0 0; font-size: 13px; min-height: 20px; visibility: hidden; opacity: 0;">
251 </p>
252 </td>
253 </tr>
254 </table>
255
256 <script type="text/javascript">
257 jQuery(document).ready(function($) {
258 $('#onwebchat_wc_order_lookup_enabled').on('change', function() {
259 var $checkbox = $(this);
260 var $status = $('#onwebchat_wc_order_lookup_status');
261 var isEnabled = $checkbox.is(':checked');
262
263 $checkbox.prop('disabled', true);
264
265 $.ajax({
266 url: ajaxurl,
267 type: 'POST',
268 data: {
269 action: 'onwebchat_wc_save_order_lookup',
270 order_lookup_enabled: isEnabled ? '1' : '0',
271 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
272 },
273 success: function(response) {
274 if (response.success) {
275 var autohide = true;
276 if (response.data.warning) {
277 // Local testing mode: saved locally, but the onWebChat server was not notified.
278 $status.empty().append(
279 $('<span/>').css('color', '#996800').text('' + (response.data.message || 'Saved locally; onWebChat server not notified.'))
280 );
281 autohide = false;
282 } else if (response.data.enabled) {
283 $status.html('<span style="color: green;">�
284 AI order status lookup enabled</span>');
285 } else {
286 $status.html('<span style="color: #d63638;"> AI order status lookup disabled</span>');
287 }
288 $status.css('visibility', 'visible').css('opacity', 1);
289 if (autohide) {
290 setTimeout(function() {
291 $status.animate({opacity: 0}, 300, function() {
292 $status.css('visibility', 'hidden');
293 });
294 }, 1700);
295 }
296 } else {
297 // Revert checkbox on error
298 $checkbox.prop('checked', !isEnabled);
299 alert('Error: ' + (response.data || 'Failed to save setting'));
300 }
301 $checkbox.prop('disabled', false);
302 },
303 error: function() {
304 $checkbox.prop('checked', !isEnabled);
305 alert('An error occurred. Please try again.');
306 $checkbox.prop('disabled', false);
307 }
308 });
309 });
310 });
311 </script>
312
313 <hr>
314
315 <h2>Bulk Sync Status</h2>
316
317 <?php
318 // Large-catalogue category picker. Stores above the threshold can choose
319 // which categories to sync (staying within the hard cap); the choice is
320 // persisted as the ongoing auto-sync scope. Below the threshold the whole
321 // catalogue is synced as before and no picker is shown.
322 if (class_exists('OnWebChat_WooCommerce_Sync')) {
323 $owc_threshold = OnWebChat_WooCommerce_Sync::CATEGORY_SELECT_THRESHOLD;
324 $owc_max = OnWebChat_WooCommerce_Sync::MAX_SYNC_PRODUCTS;
325 } else {
326 $owc_threshold = 2000;
327 $owc_max = 15000;
328 }
329
330 $owc_counts = wp_count_posts('product');
331 $owc_published_count = (is_object($owc_counts) && isset($owc_counts->publish)) ? (int) $owc_counts->publish : 0;
332 $owc_needs_selection = $owc_published_count > $owc_threshold;
333 $owc_over_max = $owc_published_count > $owc_max;
334
335 // Saved sync scope (product_cat term IDs) to pre-tick in the picker.
336 $owc_saved_scope = array();
337 $owc_scope_raw = (string) get_option('onwebchat_wc_sync_categories', '');
338 if ($owc_scope_raw !== '') {
339 foreach (explode(',', $owc_scope_raw) as $owc_sid) {
340 $owc_sid = (int) trim($owc_sid);
341 if ($owc_sid > 0) {
342 $owc_saved_scope[] = $owc_sid;
343 }
344 }
345 }
346
347 $owc_categories = $owc_needs_selection ? get_terms(array(
348 'taxonomy' => 'product_cat',
349 'hide_empty' => false,
350 'orderby' => 'name',
351 'order' => 'ASC',
352 )) : array();
353 if (is_wp_error($owc_categories)) {
354 $owc_categories = array();
355 }
356
357 // Build the category tree. The picker only offers top-level categories, and
358 // selecting one covers its whole subtree, so each row shows a distinct product
359 // count spanning the category and all of its descendants.
360 $owc_children = array(); // parent_id => array of term objects
361 foreach ($owc_categories as $owc_term) {
362 $owc_children[(int) $owc_term->parent][] = $owc_term;
363 }
364
365 // Distinct published products in a category and its whole subtree. Counting
366 // distinct posts (WP_Query found_posts with include_children) avoids the
367 // over-count you get from summing per-term counts when a product is assigned
368 // to both a parent and a child category.
369 if (!function_exists('owc_subtree_product_count')) {
370 function owc_subtree_product_count($term_id) {
371 $query = new WP_Query(array(
372 'post_type' => 'product',
373 'post_status' => 'publish',
374 'posts_per_page' => 1,
375 'fields' => 'ids',
376 'no_found_rows' => false,
377 'tax_query' => array(array(
378 'taxonomy' => 'product_cat',
379 'field' => 'term_id',
380 'terms' => (int) $term_id,
381 'include_children' => true,
382 )),
383 ));
384 return (int) $query->found_posts;
385 }
386 }
387
388 // Renderable top-level categories (parent 0) that actually cover products.
389 $owc_top_rows = array();
390 $owc_top_level = isset($owc_children[0]) ? $owc_children[0] : array();
391 foreach ($owc_top_level as $owc_term) {
392 $owc_count = owc_subtree_product_count((int) $owc_term->term_id);
393 if ($owc_count > 0) {
394 $owc_top_rows[] = array('term' => $owc_term, 'count' => $owc_count);
395 }
396 }
397 ?>
398
399 <?php if ($owc_needs_selection && !empty($owc_top_rows)): ?>
400 <div id="owc-category-picker" style="background: #f9f9f9; padding: 20px; border-radius: 8px; max-width: 800px; margin-top: 15px;">
401 <p style="margin: 0 0 10px 0;"><strong>Choose what to sync</strong></p>
402 <p id="owc-category-intro" style="margin: 0 0 12px 0; color: #555;">
403 <?php if ($owc_over_max): ?>
404 <?php printf(
405 'Your store has %s products. onWebChat can sync up to %s, so please tick the categories you want to sync. Selecting a category includes all of its subcategories.',
406 esc_html(number_format_i18n($owc_published_count)),
407 esc_html(number_format_i18n($owc_max))
408 ); ?>
409 <?php else: ?>
410 <?php printf(
411 'Your store has %s products. Leave every category unticked to sync the whole catalogue, including products without a category, or tick specific categories to sync only those. Selecting a category includes all of its subcategories.',
412 esc_html(number_format_i18n($owc_published_count))
413 ); ?>
414 <?php endif; ?>
415 </p>
416 <p style="margin: 0 0 10px 0;">
417 <input type="text" id="owc-category-search" placeholder="Search categories..." class="regular-text" style="max-width: 360px;">
418 </p>
419 <p style="margin: 0 0 8px 0;">
420 <label><input type="checkbox" id="owc-category-all"> <strong>Select all</strong></label>
421 <span id="owc-category-selected" style="margin-left: 12px; color: #666;"></span>
422 </p>
423 <div id="owc-category-list" style="max-height: 280px; overflow: auto; border: 1px solid #ddd; border-radius: 4px; background: #fff; padding: 8px 12px;">
424 <?php foreach ($owc_top_rows as $owc_row): $owc_term = $owc_row['term']; ?>
425 <label class="owc-category-row" data-name="<?php echo esc_attr(strtolower($owc_term->name)); ?>" style="display: block; padding: 3px 0;">
426 <input type="checkbox" class="owc-category-cb" value="<?php echo (int) $owc_term->term_id; ?>" <?php echo in_array((int) $owc_term->term_id, $owc_saved_scope, true) ? 'checked' : ''; ?>>
427 <?php echo esc_html($owc_term->name); ?>
428 <span style="color: #999;">(<?php echo (int) $owc_row['count']; ?>)</span>
429 </label>
430 <?php endforeach; ?>
431 </div>
432 </div>
433 <?php endif; ?>
434
435 <div id="onwebchat_sync_status_display" style="background: #f9f9f9; padding: 20px; border-radius: 8px; max-width: 800px; margin-top: 15px;">
436 <?php if ($sync_status['in_progress']): ?>
437 <p style="margin: 0 0 10px 0;">
438 <strong>⏳ Sync in progress:</strong>
439 <?php echo esc_html($sync_status['done']); ?> / <?php echo esc_html($sync_status['total']); ?> products synced
440 </p>
441 <div style="background: #fff; border: 1px solid #ddd; border-radius: 4px; height: 20px; overflow: hidden;">
442 <div style="background: #2271b1; height: 100%; width: <?php echo $sync_status['total'] > 0 ? ($sync_status['done'] / $sync_status['total'] * 100) : 0; ?>%; transition: width 0.3s;"></div>
443 </div>
444 <?php elseif ($sync_status['last_sync'] > 0): ?>
445 <p id="onwebchat_last_sync_info" style="margin: 0;">
446 <strong>✓ Last bulk sync:</strong>
447 <?php echo esc_html(human_time_diff($sync_status['last_sync'], current_time('timestamp')) . ' ago'); ?>
448 </p>
449 <p id="onwebchat_total_synced_info" style="margin: 10px 0 0 0; color: #666;">
450 Total products synced: <?php echo esc_html($sync_status['total']); ?>
451 </p>
452 <?php else: ?>
453 <p style="margin: 0; color: #666;">
454 No bulk sync has been performed yet.
455 </p>
456 <?php endif; ?>
457
458 <?php if (!$is_in_cooldown): ?>
459 <p style="margin-top: 15px;">
460 <button type="button"
461 id="onwebchat_sync_now"
462 class="button button-primary"
463 <?php echo $sync_status['in_progress'] ? 'disabled' : ''; ?>>
464 <?php echo $sync_status['in_progress'] ? 'Sync in Progress...' : 'Sync All Products Now'; ?>
465 </button>
466 </p>
467 <?php endif; ?>
468
469 <p class="description onwebchat-sync-description" style="margin-top: 10px;">
470 <?php if ($is_in_cooldown): ?>
471 <?php if ($sync_enabled): ?>
472 Bulk sync was completed recently. Your products are already up to date, and any new or updated products will be synced automatically.
473 <?php else: ?>
474 Bulk sync was completed recently. Your products are already up to date. Please enable "Automatically sync products to onWebChat for AI training" above to keep all your products synced automatically.
475 <?php endif; ?>
476 <?php else: ?>
477 Click to sync all existing products. This process runs in the background and may take several minutes depending on your product count.
478 <?php endif; ?>
479 </p>
480
481 <?php
482 // Show reset button if sync appears stuck (done >= total OR in progress for more than 3 minutes)
483 $stuck_sync = $sync_status['in_progress'] && (
484 $sync_status['done'] >= $sync_status['total'] ||
485 (time() - $sync_status['last_sync_start'] > 180)
486 );
487 if ($stuck_sync):
488 ?>
489 <p style="margin-top: 10px;">
490 <button type="button"
491 id="onwebchat_manual_process_batch"
492 class="button button-secondary"
493 style="margin-right: 10px;">
494 Process Batch Manually
495 </button>
496 <button type="button"
497 id="onwebchat_reset_sync_status"
498 class="button">
499 Mark Sync as Complete
500 </button>
501 <span class="description" style="margin-left: 10px;">
502 If sync appears stuck, try processing manually or reset status.
503 </span>
504 </p>
505 <?php endif; ?>
506 </div>
507
508 <script type="text/javascript">
509 jQuery(document).ready(function($) {
510 // Cooldown timer - reload page when cooldown expires
511 <?php if ($is_in_cooldown): ?>
512 var cooldownSeconds = <?php echo ($cooldown_period - $time_since_last_sync); ?>;
513 setTimeout(function() {
514 location.reload();
515 }, cooldownSeconds * 1000);
516 <?php endif; ?>
517
518 // ---- Large-catalogue category picker ----
519 var owcOverMax = <?php echo (!empty($owc_over_max)) ? 'true' : 'false'; ?>;
520 var owcHasPicker = $('#owc-category-picker').length > 0;
521
522 function owcSelectedCategoryIds() {
523 var ids = [];
524 $('.owc-category-cb:checked').each(function() {
525 ids.push($(this).val());
526 });
527 return ids;
528 }
529
530 function owcUpdateSyncButtonLabel() {
531 var $btn = $('#onwebchat_sync_now');
532 if (!$btn.length || $btn.prop('disabled')) {
533 return;
534 }
535 var label = (owcOverMax || owcSelectedCategoryIds().length > 0)
536 ? 'Sync selected categories'
537 : 'Sync All Products Now';
538 $btn.text(label);
539 }
540
541 function owcUpdateSelected() {
542 var selected = owcSelectedCategoryIds().length;
543 var total = $('.owc-category-cb').length;
544 $('#owc-category-selected').text(selected > 0 ? (selected + ' of ' + total + ' categories selected') : '');
545 $('#owc-category-all').prop('checked', total > 0 && selected === total);
546 owcUpdateSyncButtonLabel();
547 }
548
549 if (owcHasPicker) {
550 $('.owc-category-cb').on('change', owcUpdateSelected);
551
552 // Select all toggles only the rows currently visible (respects search).
553 $('#owc-category-all').on('change', function() {
554 var checked = $(this).is(':checked');
555 $('.owc-category-row:visible').find('.owc-category-cb').prop('checked', checked);
556 owcUpdateSelected();
557 });
558
559 $('#owc-category-search').on('input', function() {
560 var q = $(this).val().toLowerCase();
561 $('.owc-category-row').each(function() {
562 var name = String($(this).data('name'));
563 $(this).toggle(name.indexOf(q) !== -1);
564 });
565 });
566
567 // Reflect the saved scope (pre-ticked categories) and set the label.
568 owcUpdateSelected();
569 }
570
571 // Sync enabled checkbox - save immediately via AJAX
572 $('#onwebchat_wc_sync_enabled').on('change', function() {
573 var $checkbox = $(this);
574 var $status = $('#onwebchat_wc_sync_status');
575 var $syncDescription = $('.onwebchat-sync-description');
576 var isEnabled = $checkbox.is(':checked');
577
578 // Disable checkbox while saving
579 $checkbox.prop('disabled', true);
580
581 $.ajax({
582 url: ajaxurl,
583 type: 'POST',
584 data: {
585 action: 'onwebchat_wc_save_sync_enabled',
586 sync_enabled: isEnabled ? '1' : '0',
587 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
588 },
589 success: function(response) {
590 if (response.success) {
591 if (response.data.enabled) {
592 $status.html('<span style="color: green;">�
593 WooCommerce product sync enabled</span>');
594 // Update the sync description text if in cooldown
595 if ($syncDescription.length) {
596 var currentText = $syncDescription.text().trim();
597 if (currentText.includes('Bulk sync was completed recently')) {
598 $syncDescription.text('Bulk sync was completed recently. Your products are already up to date, and any new or updated products will be synced automatically.');
599 }
600 }
601 } else {
602 $status.html('<span style="color: #d63638;"> WooCommerce product sync disabled</span>');
603 // Update the sync description text if in cooldown
604 if ($syncDescription.length) {
605 var currentText = $syncDescription.text().trim();
606 if (currentText.includes('Bulk sync was completed recently')) {
607 $syncDescription.text('Bulk sync was completed recently. Your products are already up to date. Please enable "Automatically sync products to onWebChat for AI training" above to keep all your products synced automatically.');
608 }
609 }
610 }
611 // Ensure status is visible and fade in smoothly
612 $status.css('visibility', 'visible').css('opacity', 1);
613 // Hide it after 2.5 seconds with fade out
614 setTimeout(function() {
615 $status.animate({opacity: 0}, 300, function() {
616 $status.css('visibility', 'hidden');
617 });
618 }, 1700);
619 } else {
620 // Revert checkbox on error
621 $checkbox.prop('checked', !isEnabled);
622 alert('Error: ' + (response.data || 'Failed to save setting'));
623 }
624 $checkbox.prop('disabled', false);
625 },
626 error: function() {
627 // Revert checkbox on error
628 $checkbox.prop('checked', !isEnabled);
629 alert('An error occurred. Please try again.');
630 $checkbox.prop('disabled', false);
631 }
632 });
633 });
634
635 // Sync now button
636 $('#onwebchat_sync_now').on('click', function() {
637 if ($(this).prop('disabled')) {
638 return;
639 }
640
641 // Determine the sync scope from the category picker (if shown).
642 var owcSelected = owcHasPicker ? owcSelectedCategoryIds() : [];
643
644 // Above the hard cap a category selection is required.
645 if (owcHasPicker && owcOverMax && owcSelected.length === 0) {
646 alert('Your store has too many products to sync all at once. Please tick at least one category to sync.');
647 return;
648 }
649
650 var confirmMsg = (owcSelected.length > 0)
651 ? 'Sync products in the selected categories with onWebChat to train your AI chatbot?'
652 : 'Sync all published products with onWebChat to train your AI chatbot?';
653 if (!confirm(confirmMsg)) {
654 return;
655 }
656
657 var $btn = $(this);
658 var nonce = '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>';
659 $btn.prop('disabled', true).text('Syncing products...');
660
661 // Hide the "Last sync" info while syncing
662 $('#onwebchat_last_sync_info').hide();
663 $('#onwebchat_total_synced_info').hide();
664
665 // Show initial syncing message with progress bar
666 $('#onwebchat_sync_status_display').html(
667 '<p style="margin: 0 0 10px 0;"><strong>⏳ Sync in progress:</strong> <span id="sync_progress_text">Starting…</span></p>' +
668 '<div style="background: #fff; border: 1px solid #ddd; border-radius: 4px; height: 20px; overflow: hidden;">' +
669 '<div id="sync_progress_bar" style="background: #2271b1; height: 100%; width: 0%; transition: width 0.3s;"></div>' +
670 '</div>' +
671 '<p style="margin: 10px 0 0 0; color: #666;">Please keep this page open until the sync completes.</p>'
672 );
673
674 function owcSyncFail(msg) {
675 alert('Error: ' + msg);
676 $btn.prop('disabled', false);
677 owcUpdateSyncButtonLabel();
678 $('#onwebchat_sync_status_display').html('');
679 }
680
681 function owcSyncProgress(done, total) {
682 var pct = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 0;
683 $('#sync_progress_text').text(done + ' / ' + total + ' products synced');
684 $('#sync_progress_bar').css('width', pct + '%');
685 }
686
687 // Drive the sync one page per request until the run reports complete.
688 // A single long request would outrun the server timeout on large
689 // catalogues and report a false failure; this keeps every request
690 // short and only reports success once the whole run is done.
691 function owcRunBatch(total, attempt) {
692 $.ajax({
693 url: ajaxurl,
694 type: 'POST',
695 timeout: 120000,
696 data: { action: 'onwebchat_wc_sync_batch', nonce: nonce },
697 success: function(resp) {
698 if (!resp || !resp.success) { owcSyncFail((resp && resp.data) || 'Sync failed'); return; }
699 var d = resp.data || {};
700 total = d.total || total;
701 owcSyncProgress(d.done || 0, total);
702
703 if (d.in_progress) {
704 owcRunBatch(total, 0);
705 } else {
706 var s = d.stats || {};
707 alert('Sync completed!\n\n' +
708 'Created: ' + (s.created || 0) + '\n' +
709 'Updated: ' + (s.updated || 0) + '\n' +
710 'Unchanged: ' + (s.skipped || 0) + '\n' +
711 'Errors: ' + (s.errors || 0));
712 location.reload();
713 }
714 },
715 error: function() {
716 // One page timing out is recoverable (progress is saved
717 // server-side): retry a few times before giving up.
718 if (attempt < 3) {
719 setTimeout(function() { owcRunBatch(total, attempt + 1); }, 3000);
720 } else {
721 owcSyncFail('A network error occurred. Some products may not have synced; please try again.');
722 }
723 }
724 });
725 }
726
727 // Kick off the run, then let the browser drive the batches.
728 $.ajax({
729 url: ajaxurl,
730 type: 'POST',
731 timeout: 120000,
732 data: {
733 action: 'onwebchat_wc_sync_start',
734 categories: owcSelected.join(','),
735 nonce: nonce
736 },
737 success: function(response) {
738 if (!response || !response.success) { owcSyncFail((response && response.data) || 'Could not start sync'); return; }
739 var total = response.data.total || 0;
740 owcSyncProgress(0, total);
741 if (total === 0) {
742 alert('No products found to sync.');
743 $btn.prop('disabled', false);
744 owcUpdateSyncButtonLabel();
745 $('#onwebchat_sync_status_display').html('');
746 return;
747 }
748 owcRunBatch(total, 0);
749 },
750 error: function() {
751 owcSyncFail('Could not start sync. Please try again.');
752 }
753 });
754 });
755
756 // Manual process batch button (for debugging stuck syncs)
757 $('#onwebchat_manual_process_batch').on('click', function() {
758 var $btn = $(this);
759 $btn.prop('disabled', true).text('Processing...');
760
761 $.ajax({
762 url: ajaxurl,
763 type: 'POST',
764 data: {
765 action: 'onwebchat_wc_manual_process_batch',
766 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
767 },
768 success: function(response) {
769 if (response.success) {
770 alert('Batch processed. Check console/logs for details.');
771 location.reload();
772 } else {
773 alert('Error: ' + response.data);
774 $btn.prop('disabled', false).text('Process Batch Manually');
775 }
776 },
777 error: function() {
778 alert('An error occurred. Please try again.');
779 $btn.prop('disabled', false).text('Process Batch Manually');
780 }
781 });
782 });
783
784 // Reset sync status button
785 $('#onwebchat_reset_sync_status').on('click', function() {
786 if (!confirm('Mark sync as complete? This will reset the progress indicator.')) {
787 return;
788 }
789
790 var $btn = $(this);
791 $btn.prop('disabled', true).text('Resetting...');
792
793 $.ajax({
794 url: ajaxurl,
795 type: 'POST',
796 data: {
797 action: 'onwebchat_wc_reset_sync_status',
798 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
799 },
800 success: function(response) {
801 if (response.success) {
802 location.reload();
803 } else {
804 alert('Error: ' + response.data);
805 $btn.prop('disabled', false).text('Mark Sync as Complete');
806 }
807 },
808 error: function() {
809 alert('An error occurred. Please try again.');
810 $btn.prop('disabled', false).text('Mark Sync as Complete');
811 }
812 });
813 });
814
815 // Regenerate secret button (now "Disconnect" button)
816 $('#onwebchat_regenerate_secret').on('click', function() {
817 if (!confirm('This will disconnect WooCommerce sync. You will need to re-authenticate with your credentials. Continue?')) {
818 return;
819 }
820
821 var $btn = $(this);
822 $btn.prop('disabled', true).text('Disconnecting...');
823
824 $.ajax({
825 url: ajaxurl,
826 type: 'POST',
827 data: {
828 action: 'onwebchat_wc_regenerate_secret',
829 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
830 },
831 success: function(response) {
832 if (response.success) {
833 alert('WooCommerce sync disconnected. Please re-authenticate to continue syncing.');
834 location.reload();
835 } else {
836 alert('Error: ' + response.data);
837 $btn.prop('disabled', false).text('Disconnect');
838 }
839 },
840 error: function() {
841 alert('An error occurred. Please try again.');
842 $btn.prop('disabled', false).text('Disconnect');
843 }
844 });
845 });
846 });
847 </script>
848
849 <?php endif; // End if secret exists ?>
850
851 <?php
852 }
853
854 /**
855 * Handle form submissions for WooCommerce tab
856 */
857 function onwebchat_handle_woocommerce_actions() {
858
859 if (isset($_POST["action"]) && $_POST["action"] == "save_wc_sync") {
860
861 if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'onwebchat_wc_sync_nonce')) {
862 wp_die('Sorry, your nonce did not verify.');
863 }
864
865 if (!current_user_can('manage_options')) {
866 wp_die('Insufficient permissions.');
867 }
868
869 // Save settings
870 $sync_enabled = isset($_POST["onwebchat_wc_sync_enabled"]) ? true : false;
871 $sync_mode = isset($_POST["onwebchat_wc_sync_mode"]) ? sanitize_text_field($_POST["onwebchat_wc_sync_mode"]) : 'short_plus_full';
872 $allowed_sync_modes = array('short_only', 'short_fallback_full', 'short_plus_full');
873 if (!in_array($sync_mode, $allowed_sync_modes, true)) {
874 $sync_mode = 'short_plus_full';
875 }
876
877 update_option('onwebchat_wc_sync_enabled', $sync_enabled);
878 update_option('onwebchat_wc_sync_mode', $sync_mode);
879
880 // Secret is now obtained via authenticated connection, not auto-generated
881
882 wp_redirect(admin_url('admin.php?page=onwebchat_settings&tab=woocommerce&wc_saved=1'));
883 exit;
884 }
885 }
886
887