PluginProbe
AI Chatbot for WooCommerce & Live Chat – onWebChat / 3.10.0
AI Chatbot for WooCommerce & Live Chat – onWebChat v3.10.0
3.10.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 All 49 releases
onwebchat / admin / tabs / woocommerce.php

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

1,289 lines 59.6 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 // What the AI training data currently covers (point 4 of the scope model):
31 // the saved scope IS what was synced, because a sync only ever adds to it.
32 global $onwebchat_wc_sync;
33 $owc_scope_summary = (isset($onwebchat_wc_sync) && is_object($onwebchat_wc_sync) && method_exists($onwebchat_wc_sync, 'get_scope_summary'))
34 ? $onwebchat_wc_sync->get_scope_summary()
35 : null;
36
37 // Calculate cooldown status
38 // Short on purpose: it only guards against double-clicking the sync button.
39 // Now that the selection is cumulative, syncing again straight after a run is
40 // a normal thing to do (you just ticked another subcategory), so a long block
41 // gets in the way. The button stays VISIBLE and disabled with a live
42 // countdown, instead of disappearing as it used to.
43 // Also in includes/woocommerce-sync.php.
44 $cooldown_period = 30; // seconds
45 $time_since_last_sync = time() - $sync_status['last_sync_start'];
46 $is_in_cooldown = ($time_since_last_sync < $cooldown_period) && $sync_status['last_sync_start'] > 0;
47 $cooldown_remaining = $is_in_cooldown ? max(1, $cooldown_period - $time_since_last_sync) : 0;
48
49 ?>
50
51 <?php if (isset($_GET['wc_saved']) && $_GET['wc_saved'] == '1'): ?>
52 <div class="notice notice-success is-dismissible">
53 <p><strong>WooCommerce settings saved successfully!</strong></p>
54 </div>
55 <?php endif; ?>
56
57 <?php
58 // Check for authentication errors
59 $auth_error = get_transient('onwebchat_wc_auth_error');
60 if ($auth_error):
61 ?>
62 <div class="notice notice-error is-dismissible">
63 <p><strong>⚠️ WooCommerce Sync Authentication Error:</strong></p>
64 <p><?php echo esc_html($auth_error); ?></p>
65 <p>This usually happens when:</p>
66 <ul style="list-style: disc; margin-left: 20px;">
67 <li>You're trying to connect the plugin to a different onWebChat account</li>
68 </ul>
69 <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>
70 </div>
71 <?php endif; ?>
72
73 <h2>Product Sync for the AI Chatbot</h2>
74 <p>
75 Let your AI chatbot understand your WooCommerce products.
76 When enabled, products are automatically synced and kept up to date, allowing the AI to answer product-related questions accurately.
77 </p>
78
79 <?php if (empty($secret)): ?>
80 <!-- Authentication required section -->
81 <div class="notice notice-warning" style="margin: 15px 0; padding: 15px;">
82 <h3 style="margin-top: 0;">🔐 Connect WooCommerce Sync</h3>
83 <p>
84 To enable WooCommerce sync, please enter your onWebChat account credentials below.
85 </p>
86 <p style="color: #666; font-size: 13px;">
87 <!-- <strong>💡 Tip:</strong> If you originally connected using email/password in the <a href="?page=onwebchat_settings&tab=general">General tab</a>,
88 try unlinking and re-linking your account there. WooCommerce sync will be connected automatically! -->
89 </p>
90
91 <table class="form-table" style="margin-bottom: 0;">
92 <tr>
93 <th scope="row"><label for="onwebchat_wc_email">onWebChat Email (username)</label></th>
94 <td>
95 <input type="email"
96 id="onwebchat_wc_email"
97 class="regular-text"
98 placeholder="Your registered email"
99 value="<?php echo esc_attr(get_option('onwebchat_plugin_option_user', '')); ?>">
100 </td>
101 </tr>
102 <tr>
103 <th scope="row"><label for="onwebchat_wc_password">onWebChat Password</label></th>
104 <td>
105 <input type="password"
106 id="onwebchat_wc_password"
107 class="regular-text"
108 placeholder="Your account password">
109 </td>
110 </tr>
111 </table>
112
113 <p style="margin-top: 15px;">
114 <button type="button" id="onwebchat_wc_connect" class="button button-primary">
115 Connect WooCommerce Sync
116 </button>
117 <span id="onwebchat_wc_connect_status" style="margin-left: 10px;"></span>
118 </p>
119 </div>
120
121 <script type="text/javascript">
122 jQuery(document).ready(function($) {
123 $('#onwebchat_wc_connect').on('click', function() {
124 var email = $('#onwebchat_wc_email').val();
125 var password = $('#onwebchat_wc_password').val();
126
127 if (!email || !password) {
128 alert('Please enter both email and password.');
129 return;
130 }
131
132 var $btn = $(this);
133 var $status = $('#onwebchat_wc_connect_status');
134
135 $btn.prop('disabled', true).text('Connecting...');
136 $status.html('');
137
138 $.ajax({
139 url: ajaxurl,
140 type: 'POST',
141 data: {
142 action: 'onwebchat_wc_connect',
143 email: email,
144 password: password,
145 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
146 },
147 success: function(response) {
148 if (response.success) {
149 $status.html('<span style="color: green;">✓ Connected successfully!</span>');
150 setTimeout(function() {
151 location.reload();
152 }, 1000);
153 } else {
154 // Render the server-supplied error as TEXT (never HTML) so a malicious/MITM'd
155 // API response can't inject markup into the admin page.
156 $status.empty().append(
157 $('<span/>').css('color', 'red').text('' + response.data)
158 );
159 $btn.prop('disabled', false).text('Connect WooCommerce Sync');
160 }
161 },
162 error: function() {
163 $status.html('<span style="color: red;">✗ Connection error. Please try again.</span>');
164 $btn.prop('disabled', false).text('Connect WooCommerce Sync');
165 }
166 });
167 });
168 });
169 </script>
170
171 <?php else: ?>
172 <!-- Connected - show settings -->
173
174 <form action="admin.php?page=onwebchat_settings&tab=woocommerce" method="post">
175 <input type="hidden" name="action" value="save_wc_sync">
176 <?php wp_nonce_field('onwebchat_wc_sync_nonce'); ?>
177
178 <table class="form-table">
179 <tr>
180 <th scope="row" style="line-height: 1 !important;">
181 <label for="onwebchat_wc_sync_enabled">Enable Product Sync</label>
182 </th>
183 <td>
184 <label for="onwebchat_wc_sync_enabled">
185 <input type="checkbox"
186 id="onwebchat_wc_sync_enabled"
187 name="onwebchat_wc_sync_enabled"
188 value="1"
189 <?php checked($sync_enabled, true); ?>>
190 Automatically sync products to onWebChat for AI training
191 </label>
192 <p id="onwebchat_wc_sync_status" style="margin: 8px 0 0 0; font-size: 13px; min-height: 20px; visibility: hidden; opacity: 0;">
193 </p>
194 </td>
195 </tr>
196
197 <tr>
198 <th scope="row">
199 <label for="onwebchat_wc_sync_mode">Description Mode</label>
200 </th>
201 <td>
202 <select id="onwebchat_wc_sync_mode" name="onwebchat_wc_sync_mode" style="width: 350px;">
203 <option value="short_plus_full" <?php selected($sync_mode, 'short_plus_full'); ?>>
204 Short + full description (default)
205 </option>
206 <option value="short_only" <?php selected($sync_mode, 'short_only'); ?>>
207 Short description only
208 </option>
209 <option value="short_fallback_full" <?php selected($sync_mode, 'short_fallback_full'); ?>>
210 Short description (fallback to full)
211 </option>
212 </select>
213 <p class="description">
214 Choose how product descriptions are synced for AI training.
215 The default sends both texts, so the AI bot also learns the
216 detailed description (intended use, compatibility, specs).
217 </p>
218 </td>
219 </tr>
220
221 <?php if (!empty($secret)): ?>
222 <tr>
223 <th scope="row">Connection Status</th>
224 <td>
225 <span style="color: green; font-weight: bold; vertical-align: middle;"> Connected to onWebChat</span>
226 <button type="button"
227 id="onwebchat_regenerate_secret"
228 class="button button-small"
229 style="margin-left: 10px; vertical-align: middle;">
230 Disconnect
231 </button>
232 <p class="description">
233 WooCommerce sync is securely connected. Click "Disconnect" to re-authenticate.
234 </p>
235 </td>
236 </tr>
237 <?php endif; ?>
238 </table>
239 </form>
240
241 <hr>
242
243 <h2>AI Order Status Lookup</h2>
244 <p>
245 Let your AI chatbot answer "where is my order?" with live data. The store verifies identity using the
246 order number and email (signed-in users only need the order number) before sharing details.
247 </p>
248
249 <table class="form-table">
250 <tr>
251 <th scope="row" style="line-height: 1 !important;">
252 <label for="onwebchat_wc_order_lookup_enabled">Enable Order Status Lookup</label>
253 </th>
254 <td>
255 <label for="onwebchat_wc_order_lookup_enabled">
256 <input type="checkbox"
257 id="onwebchat_wc_order_lookup_enabled"
258 name="onwebchat_wc_order_lookup_enabled"
259 value="1"
260 <?php checked($order_lookup_enabled, true); ?>>
261 Allow the AI chatbot to look up live order status (with identity verification)
262 </label>
263 <p id="onwebchat_wc_order_lookup_status" style="margin: 8px 0 0 0; font-size: 13px; min-height: 20px; visibility: hidden; opacity: 0;">
264 </p>
265 </td>
266 </tr>
267 </table>
268
269 <script type="text/javascript">
270 jQuery(document).ready(function($) {
271 $('#onwebchat_wc_order_lookup_enabled').on('change', function() {
272 var $checkbox = $(this);
273 var $status = $('#onwebchat_wc_order_lookup_status');
274 var isEnabled = $checkbox.is(':checked');
275
276 $checkbox.prop('disabled', true);
277
278 $.ajax({
279 url: ajaxurl,
280 type: 'POST',
281 data: {
282 action: 'onwebchat_wc_save_order_lookup',
283 order_lookup_enabled: isEnabled ? '1' : '0',
284 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
285 },
286 success: function(response) {
287 if (response.success) {
288 var autohide = true;
289 if (response.data.warning) {
290 // Local testing mode: saved locally, but the onWebChat server was not notified.
291 $status.empty().append(
292 $('<span/>').css('color', '#996800').text('' + (response.data.message || 'Saved locally; onWebChat server not notified.'))
293 );
294 autohide = false;
295 } else if (response.data.enabled) {
296 $status.html('<span style="color: green;">�
297 AI order status lookup enabled</span>');
298 } else {
299 $status.html('<span style="color: #d63638;"> AI order status lookup disabled</span>');
300 }
301 $status.css('visibility', 'visible').css('opacity', 1);
302 if (autohide) {
303 setTimeout(function() {
304 $status.animate({opacity: 0}, 300, function() {
305 $status.css('visibility', 'hidden');
306 });
307 }, 1700);
308 }
309 } else {
310 // Revert checkbox on error
311 $checkbox.prop('checked', !isEnabled);
312 alert('Error: ' + (response.data || 'Failed to save setting'));
313 }
314 $checkbox.prop('disabled', false);
315 },
316 error: function() {
317 $checkbox.prop('checked', !isEnabled);
318 alert('An error occurred. Please try again.');
319 $checkbox.prop('disabled', false);
320 }
321 });
322 });
323 });
324 </script>
325
326 <hr>
327
328 <h2>Bulk Sync Status</h2>
329
330 <?php
331 // Large-catalogue category picker. Stores above the threshold can choose
332 // which categories to sync (staying within the hard cap); the choice is
333 // persisted as the ongoing auto-sync scope. Below the threshold the whole
334 // catalogue is synced as before and no picker is shown.
335 if (class_exists('OnWebChat_WooCommerce_Sync')) {
336 $owc_threshold = OnWebChat_WooCommerce_Sync::CATEGORY_SELECT_THRESHOLD;
337 $owc_max = OnWebChat_WooCommerce_Sync::MAX_SYNC_PRODUCTS;
338 } else {
339 $owc_threshold = 2000;
340 $owc_max = 15000;
341 }
342
343 $owc_counts = wp_count_posts('product');
344 $owc_published_count = (is_object($owc_counts) && isset($owc_counts->publish)) ? (int) $owc_counts->publish : 0;
345 $owc_needs_selection = $owc_published_count > $owc_threshold;
346 $owc_over_max = $owc_published_count > $owc_max;
347
348 // Saved sync scope (product_cat term IDs) to pre-tick in the picker.
349 $owc_saved_scope = array();
350 $owc_scope_raw = (string) get_option('onwebchat_wc_sync_categories', '');
351 if ($owc_scope_raw !== '') {
352 foreach (explode(',', $owc_scope_raw) as $owc_sid) {
353 $owc_sid = (int) trim($owc_sid);
354 if ($owc_sid > 0) {
355 $owc_saved_scope[] = $owc_sid;
356 }
357 }
358 }
359
360 $owc_categories = $owc_needs_selection ? get_terms(array(
361 'taxonomy' => 'product_cat',
362 'hide_empty' => false,
363 'orderby' => 'name',
364 'order' => 'ASC',
365 )) : array();
366 if (is_wp_error($owc_categories)) {
367 $owc_categories = array();
368 }
369
370 // Build the whole category tree for the picker. Every category is offered
371 // (subcategories indented under their parent, searchable by name or path),
372 // and selecting one covers its whole subtree, so each row shows a distinct
373 // product count spanning the category and all of its descendants.
374 $owc_term_by_id = array(); // term_id => term object
375 foreach ($owc_categories as $owc_term) {
376 $owc_term_by_id[(int) $owc_term->term_id] = $owc_term;
377 }
378
379 $owc_children = array(); // parent_id => array of term objects (name order, from get_terms)
380 $owc_parent_of = array(); // term_id => parent term_id (0 for roots and orphans)
381 foreach ($owc_categories as $owc_term) {
382 $owc_pid = (int) $owc_term->parent;
383 if (!isset($owc_term_by_id[$owc_pid])) {
384 $owc_pid = 0; // orphan (parent missing): treat as a root
385 }
386 $owc_parent_of[(int) $owc_term->term_id] = $owc_pid;
387 $owc_children[$owc_pid][] = $owc_term;
388 }
389
390 // Distinct published products per category subtree, computed in ONE pass:
391 // read every (product, category) pair of the published products and, for
392 // each product, credit the category and each of its ancestors once. Counting
393 // per product avoids the over-count you get from summing per-term counts when
394 // a product sits in both a parent and a child category, and one query keeps
395 // the page fast even with more than a thousand categories (a WP_Query per
396 // category would not).
397 $owc_counts = array(); // term_id => distinct published products in the subtree
398 if (!empty($owc_categories)) {
399 global $wpdb;
400 $owc_pairs = $wpdb->get_results($wpdb->prepare(
401 "SELECT tr.object_id, tt.term_id
402 FROM {$wpdb->term_relationships} tr
403 INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
404 INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id
405 WHERE tt.taxonomy = %s AND p.post_type = %s AND p.post_status = %s
406 ORDER BY tr.object_id",
407 'product_cat', 'product', 'publish'
408 ), ARRAY_N);
409 if (!is_array($owc_pairs)) {
410 $owc_pairs = array();
411 }
412
413 $owc_current_product = 0;
414 $owc_credited = array(); // categories already credited for the current product
415 foreach ($owc_pairs as $owc_pair) {
416 $owc_product_id = (int) $owc_pair[0];
417 if ($owc_product_id !== $owc_current_product) {
418 $owc_current_product = $owc_product_id;
419 $owc_credited = array();
420 }
421 // Walk up to the root; stop at a category already credited for this
422 // product (its ancestors were credited on that walk too).
423 $owc_walk = (int) $owc_pair[1];
424 $owc_hops = 0;
425 while ($owc_walk > 0 && isset($owc_parent_of[$owc_walk]) && !isset($owc_credited[$owc_walk]) && $owc_hops++ < 100) {
426 $owc_credited[$owc_walk] = true;
427 $owc_counts[$owc_walk] = isset($owc_counts[$owc_walk]) ? $owc_counts[$owc_walk] + 1 : 1;
428 $owc_walk = $owc_parent_of[$owc_walk];
429 }
430 }
431 unset($owc_pairs, $owc_credited);
432 }
433
434 // Rows in tree order (depth first, siblings by name), skipping categories
435 // whose subtree holds no published product. Anything not reached from the
436 // roots (e.g. a parent loop) is appended as a root so nothing is hidden.
437 $owc_rows = array(); // each: term, name, count, level, parents (path prefix)
438 $owc_visited = array();
439 for ($owc_pass = 0; $owc_pass < 2; $owc_pass++) {
440 if ($owc_pass === 0) {
441 $owc_roots = isset($owc_children[0]) ? $owc_children[0] : array();
442 } else {
443 $owc_roots = array();
444 foreach ($owc_categories as $owc_term) {
445 if (!isset($owc_visited[(int) $owc_term->term_id])) {
446 $owc_roots[] = $owc_term;
447 }
448 }
449 }
450 $owc_stack = array();
451 foreach (array_reverse($owc_roots) as $owc_term) {
452 $owc_stack[] = array($owc_term, 0, '');
453 }
454 while (!empty($owc_stack)) {
455 list($owc_term, $owc_level, $owc_parents) = array_pop($owc_stack);
456 $owc_id = (int) $owc_term->term_id;
457 if (isset($owc_visited[$owc_id])) {
458 continue;
459 }
460 $owc_visited[$owc_id] = true;
461 $owc_count = isset($owc_counts[$owc_id]) ? $owc_counts[$owc_id] : 0;
462 if ($owc_count === 0) {
463 continue; // empty subtree (descendants are counted into their ancestors)
464 }
465 $owc_name = html_entity_decode((string) $owc_term->name, ENT_QUOTES, 'UTF-8');
466 $owc_rows[] = array(
467 'term' => $owc_term,
468 'name' => $owc_name,
469 'count' => $owc_count,
470 'level' => $owc_level,
471 'parents' => $owc_parents,
472 );
473 if (!empty($owc_children[$owc_id])) {
474 foreach (array_reverse($owc_children[$owc_id]) as $owc_child) {
475 $owc_stack[] = array($owc_child, $owc_level + 1, $owc_parents . $owc_name . ' > ');
476 }
477 }
478 }
479 }
480 unset($owc_visited, $owc_stack, $owc_term_by_id, $owc_children);
481 ?>
482
483 <?php if ($owc_needs_selection && !empty($owc_rows)): ?>
484 <style>
485 #owc-category-list .owc-category-row { display: block; padding-top: 3px; padding-bottom: 3px; }
486 #owc-category-list .owc-cat-path { display: none; color: #999; }
487 #owc-category-list.owc-filtering .owc-cat-path { display: inline; }
488 #owc-category-list .owc-category-implied { color: #8c8f94; }
489 </style>
490 <div id="owc-category-picker" style="background: #f9f9f9; padding: 20px; border-radius: 8px; max-width: 800px; margin-top: 15px;">
491 <p style="margin: 0 0 10px 0;"><strong>Choose what to sync</strong></p>
492 <p id="owc-category-intro" style="margin: 0 0 12px 0; color: #555;">
493 <?php if ($owc_over_max): ?>
494 <?php printf(
495 '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. Type in the search box to find any category or subcategory.',
496 esc_html(number_format_i18n($owc_published_count)),
497 esc_html(number_format_i18n($owc_max))
498 ); ?>
499 <?php else: ?>
500 <?php printf(
501 '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. Type in the search box to find any category or subcategory.',
502 esc_html(number_format_i18n($owc_published_count))
503 ); ?>
504 <?php endif; ?>
505 </p>
506 <p style="margin: 0 0 10px 0;">
507 <input type="text" id="owc-category-search" placeholder="Search categories and subcategories..." class="regular-text" style="max-width: 360px;">
508 </p>
509 <p style="margin: 0 0 8px 0;">
510 <label><input type="checkbox" id="owc-category-all"> <strong>Select all</strong></label>
511 <span id="owc-category-selected" style="margin-left: 12px; color: #666;"></span>
512 </p>
513 <div id="owc-category-list" style="max-height: 280px; overflow: auto; border: 1px solid #ddd; border-radius: 4px; background: #fff; padding: 8px 12px;">
514 <?php foreach ($owc_rows as $owc_row): $owc_term = $owc_row['term']; $owc_indent = (int) $owc_row['level'] * 18; ?>
515 <label class="owc-category-row" data-path="<?php echo esc_attr($owc_row['parents'] . $owc_row['name']); ?>" data-parent="<?php echo (int) $owc_parent_of[(int) $owc_term->term_id]; ?>" data-indent="<?php echo $owc_indent; ?>" style="padding-left: <?php echo $owc_indent; ?>px;">
516 <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' : ''; ?>>
517 <?php if ($owc_row['parents'] !== ''): ?><span class="owc-cat-path"><?php echo esc_html($owc_row['parents']); ?></span><?php endif; ?><?php echo esc_html($owc_row['name']); ?>
518 <span style="color: #999;">(<?php echo (int) $owc_row['count']; ?>)</span>
519 </label>
520 <?php endforeach; ?>
521 </div>
522 <p id="owc-scope-actions" style="margin: 12px 0 0 0;">
523 <span id="owc-scope-removed-note" style="display: none; color: #b32d2e; margin-right: 10px;"></span>
524 <button type="button" class="button button-secondary" id="owc-scope-remove" style="display: none;">
525 Remove unticked categories from the AI training data
526 </button>
527 <span id="owc-scope-remove-progress" style="display: none; margin-left: 10px; color: #666;"></span>
528 </p>
529 </div>
530 <?php endif; ?>
531
532 <div id="onwebchat_sync_status_display" style="background: #f9f9f9; padding: 20px; border-radius: 8px; max-width: 800px; margin-top: 15px;">
533 <?php if ($sync_status['in_progress']): ?>
534 <p style="margin: 0 0 10px 0;">
535 <strong>⏳ Sync in progress:</strong>
536 <?php echo esc_html($sync_status['done']); ?> / <?php echo esc_html($sync_status['total']); ?> products synced
537 </p>
538 <div style="background: #fff; border: 1px solid #ddd; border-radius: 4px; height: 20px; overflow: hidden;">
539 <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>
540 </div>
541 <?php elseif ($sync_status['last_sync'] > 0): ?>
542 <p id="onwebchat_last_sync_info" style="margin: 0;">
543 <strong>✓ Last bulk sync:</strong>
544 <?php echo esc_html(human_time_diff($sync_status['last_sync'], current_time('timestamp')) . ' ago'); ?>
545 </p>
546 <p id="onwebchat_total_synced_info" style="margin: 10px 0 0 0; color: #666;">
547 Total products synced: <?php echo esc_html($sync_status['total']); ?>
548 </p>
549 <?php else: ?>
550 <p style="margin: 0; color: #666;">
551 No bulk sync has been performed yet.
552 </p>
553 <?php endif; ?>
554
555 <?php if ($owc_scope_summary && !empty($owc_scope_summary['known'])): ?>
556 <p id="onwebchat_scope_summary" style="margin: 10px 0 0 0; color: #666;">
557 <strong>In your AI training data:</strong>
558 <?php if ($owc_scope_summary['whole_catalogue']): ?>
559 <?php printf(
560 'your whole catalogue (%s products)',
561 esc_html(number_format_i18n($owc_scope_summary['products']))
562 ); ?>
563 <?php elseif (!empty($owc_scope_summary['nothing'])): ?>
564 no products yet
565 <?php else: ?>
566 <?php printf(
567 '%s %s, %s products',
568 esc_html(number_format_i18n($owc_scope_summary['categories'])),
569 esc_html($owc_scope_summary['categories'] === 1 ? 'category' : 'categories'),
570 esc_html(number_format_i18n($owc_scope_summary['products']))
571 ); ?>
572 <?php endif; ?>
573 </p>
574 <?php endif; ?>
575
576 <p style="margin-top: 15px;">
577 <button type="button"
578 id="onwebchat_sync_now"
579 class="button button-primary"
580 data-cooldown="<?php echo (int) $cooldown_remaining; ?>"
581 <?php echo ($sync_status['in_progress'] || $is_in_cooldown) ? 'disabled' : ''; ?>>
582 <?php
583 if ($sync_status['in_progress']) {
584 echo 'Sync in Progress...';
585 } elseif ($is_in_cooldown) {
586 printf('Just synced, wait %ds', (int) $cooldown_remaining);
587 } else {
588 echo 'Sync All Products Now';
589 }
590 ?>
591 </button>
592 </p>
593
594 <p class="description onwebchat-sync-description" style="margin-top: 10px;">
595 <?php if ($is_in_cooldown): ?>
596 <?php if ($sync_enabled): ?>
597 A sync has just finished. The button is available again in a moment, so you can sync another selection right away.
598 <?php else: ?>
599 A sync has just finished. Please enable "Automatically sync products to onWebChat for AI training" above to keep your products synced automatically.
600 <?php endif; ?>
601 <?php else: ?>
602 Click to sync all existing products. This process runs in the background and may take several minutes depending on your product count.
603 <?php endif; ?>
604 </p>
605
606 <?php
607 // Show reset button if sync appears stuck (done >= total OR in progress for more than 3 minutes)
608 $stuck_sync = $sync_status['in_progress'] && (
609 $sync_status['done'] >= $sync_status['total'] ||
610 (time() - $sync_status['last_sync_start'] > 180)
611 );
612 if ($stuck_sync):
613 ?>
614 <p style="margin-top: 10px;">
615 <button type="button"
616 id="onwebchat_manual_process_batch"
617 class="button button-secondary"
618 style="margin-right: 10px;">
619 Process Batch Manually
620 </button>
621 <button type="button"
622 id="onwebchat_reset_sync_status"
623 class="button">
624 Mark Sync as Complete
625 </button>
626 <span class="description" style="margin-left: 10px;">
627 If sync appears stuck, try processing manually or reset status.
628 </span>
629 </p>
630 <?php endif; ?>
631 </div>
632
633 <script type="text/javascript">
634 jQuery(document).ready(function($) {
635 // Cooldown: count down on the button itself and enable it when it runs
636 // out. The button used to be removed from the page entirely and only came
637 // back on a reload, which read as "the sync button is gone".
638 var $syncBtnCooldown = $('#onwebchat_sync_now');
639 var owcCooldownLeft = parseInt($syncBtnCooldown.attr('data-cooldown'), 10) || 0;
640 if (owcCooldownLeft > 0) {
641 var owcCooldownTimer = setInterval(function() {
642 owcCooldownLeft--;
643 if (owcCooldownLeft > 0) {
644 $syncBtnCooldown.text('Just synced, wait ' + owcCooldownLeft + 's');
645 return;
646 }
647 clearInterval(owcCooldownTimer);
648 $syncBtnCooldown.prop('disabled', false);
649 $('.onwebchat-sync-description').text('Click to sync all existing products. This process runs in the background and may take several minutes depending on your product count.');
650 // Restores "Sync All Products Now" / "Sync N new categories".
651 if (typeof owcUpdateSyncButtonLabel === 'function') {
652 owcUpdateSyncButtonLabel();
653 } else {
654 $syncBtnCooldown.text('Sync All Products Now');
655 }
656 }, 1000);
657 }
658
659 // ---- Large-catalogue category picker ----
660 var owcOverMax = <?php echo (!empty($owc_over_max)) ? 'true' : 'false'; ?>;
661 var owcHasPicker = $('#owc-category-picker').length > 0;
662 // The categories already in the AI training data. A sync only ever ADDS to
663 // this, so it is also the list a removal is measured against.
664 var owcSavedScope = <?php echo wp_json_encode(array_map('strval', $owc_saved_scope)); ?>;
665 // Nothing in the training data yet: every ticked category is a first sync,
666 // so the button should not talk about what is "new" or a "re-sync".
667 var owcScopeNone = <?php echo (!empty($owc_scope_summary['nothing'])) ? 'true' : 'false'; ?>;
668 var owcCatParent = {}; // category id => parent id ('0' for top-level categories)
669 var owcCatExplicit = {}; // category id => true when ticked by the merchant
670 var owcFiltering = false;
671
672 // Lower-case and strip accents, so "camasi" also finds a category with diacritics.
673 function owcFold(str) {
674 str = String(str || '').toLowerCase();
675 if (str.normalize) {
676 str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
677 }
678 return str;
679 }
680
681 // Only explicit ticks are sent: a ticked category already covers its subtree.
682 function owcSelectedCategoryIds() {
683 var ids = [];
684 $('.owc-category-cb').each(function() {
685 if (owcCatExplicit[String(this.value)]) {
686 ids.push(String(this.value));
687 }
688 });
689 return ids;
690 }
691
692 function owcHasTickedAncestor(id) {
693 var parent = owcCatParent[id], hops = 0;
694 while (parent && parent !== '0' && hops++ < 100) {
695 if (owcCatExplicit[parent]) {
696 return true;
697 }
698 parent = owcCatParent[parent];
699 }
700 return false;
701 }
702
703 // Descendants of a ticked category show as ticked and disabled (implied)
704 // and are handed back when the parent is unticked.
705 function owcApplyTicks() {
706 $('.owc-category-cb').each(function() {
707 var id = String(this.value);
708 var implied = owcHasTickedAncestor(id);
709 this.disabled = implied;
710 this.checked = implied || !!owcCatExplicit[id];
711 $(this).closest('.owc-category-row').toggleClass('owc-category-implied', implied);
712 });
713 }
714
715 // Is this category already in the training data, i.e. covered by the saved
716 // scope itself or by one of its ancestors? Mirrors scope_covers() in PHP.
717 function owcCoveredBy(list, id) {
718 id = String(id);
719 if (list.indexOf(id) !== -1) {
720 return true;
721 }
722 var parent = owcCatParent[id], hops = 0;
723 while (parent && parent !== '0' && hops++ < 100) {
724 if (list.indexOf(parent) !== -1) {
725 return true;
726 }
727 parent = owcCatParent[parent];
728 }
729 return false;
730 }
731
732 // Ticked categories that are not in the training data yet: the only ones a
733 // sync has to push.
734 function owcAddedCategories() {
735 if (!owcSavedScope.length) {
736 return []; // whole catalogue already covered
737 }
738 return owcSelectedCategoryIds().filter(function(id) {
739 return !owcCoveredBy(owcSavedScope, id);
740 });
741 }
742
743 // Synced categories the merchant just unticked. Nothing happens to them
744 // until the removal button below is used.
745 function owcRemovedCategories() {
746 var selected = owcSelectedCategoryIds();
747 return owcSavedScope.filter(function(id) {
748 return !owcCoveredBy(selected, id);
749 });
750 }
751
752 function owcUpdateSyncButtonLabel() {
753 var $btn = $('#onwebchat_sync_now');
754 if (!$btn.length || $btn.prop('disabled')) {
755 return;
756 }
757 var selected = owcSelectedCategoryIds().length;
758 var added = owcAddedCategories().length;
759 var label;
760 if (!selected && !owcOverMax) {
761 label = 'Sync All Products Now';
762 } else if (owcScopeNone) {
763 label = 'Sync selected ' + (selected === 1 ? 'category' : 'categories');
764 } else if (added > 0) {
765 label = 'Sync ' + added + (added === 1 ? ' new category' : ' new categories');
766 } else {
767 label = 'Re-sync selected categories';
768 }
769 $btn.text(label);
770 }
771
772 // Show the removal offer only while synced categories are unticked.
773 function owcUpdateRemoveControls() {
774 var $btn = $('#owc-scope-remove');
775 if (!$btn.length) {
776 return;
777 }
778 var removed = owcRemovedCategories().length;
779 var $note = $('#owc-scope-removed-note');
780 if (removed > 0) {
781 $note.text('Unticked categories keep their products in your AI training data until you remove them.').show();
782 $btn.text('Remove unticked categories from the AI training data (' + removed + ')').show();
783 } else {
784 $note.hide();
785 $btn.hide();
786 }
787 }
788
789 function owcUpdateSelected() {
790 var selected = owcSelectedCategoryIds().length;
791 var $all = $('.owc-category-cb');
792 var checked = $all.filter(':checked').length;
793 var text = '';
794 if (selected > 0) {
795 text = selected + (selected === 1 ? ' category' : ' categories') + ' selected';
796 if (checked > selected) {
797 text += ' (' + checked + ' including subcategories)';
798 }
799 }
800 $('#owc-category-selected').text(text);
801 $('#owc-category-all').prop('checked', $all.length > 0 && checked === $all.length);
802 owcUpdateSyncButtonLabel();
803 owcUpdateRemoveControls();
804 }
805
806 if (owcHasPicker) {
807 $('.owc-category-row').each(function() {
808 var $row = $(this);
809 var cb = $row.find('.owc-category-cb')[0];
810 if (!cb) {
811 return;
812 }
813 var id = String(cb.value);
814 owcCatParent[id] = String($row.attr('data-parent') || '0');
815 if (cb.checked) {
816 owcCatExplicit[id] = true; // saved sync scope
817 }
818 $row.data('fold', owcFold($row.attr('data-path')));
819 });
820
821 $('.owc-category-cb').on('change', function() {
822 if (this.checked) {
823 owcCatExplicit[String(this.value)] = true;
824 } else {
825 delete owcCatExplicit[String(this.value)];
826 }
827 owcApplyTicks();
828 owcUpdateSelected();
829 });
830
831 // Select all: without a search it ticks every top-level category (the
832 // whole tree); while searching it toggles only the rows currently shown.
833 $('#owc-category-all').on('change', function() {
834 var checked = this.checked;
835 if (owcFiltering) {
836 $('.owc-category-row:visible .owc-category-cb').each(function() {
837 if (checked) {
838 owcCatExplicit[String(this.value)] = true;
839 } else {
840 delete owcCatExplicit[String(this.value)];
841 }
842 });
843 } else {
844 owcCatExplicit = {};
845 if (checked) {
846 $.each(owcCatParent, function(id, parent) {
847 if (parent === '0') {
848 owcCatExplicit[id] = true;
849 }
850 });
851 }
852 }
853 owcApplyTicks();
854 owcUpdateSelected();
855 });
856
857 // Search the whole tree by name or path ("shoes" also lists "Men > Shoes").
858 // Matches are shown flat with their full path while a search is active.
859 $('#owc-category-search').on('input', function() {
860 var q = owcFold($(this).val().trim());
861 owcFiltering = q !== '';
862 $('#owc-category-list').toggleClass('owc-filtering', owcFiltering);
863 $('.owc-category-row').each(function() {
864 var $row = $(this);
865 var hit = !owcFiltering || String($row.data('fold')).indexOf(q) !== -1;
866 $row.toggle(hit);
867 $row.css('padding-left', owcFiltering ? '0' : (parseInt($row.attr('data-indent'), 10) || 0) + 'px');
868 });
869 });
870
871 // Reflect the saved scope (pre-ticked categories) and set the label.
872 owcApplyTicks();
873 owcUpdateSelected();
874 }
875
876 // Sync enabled checkbox - save immediately via AJAX
877 $('#onwebchat_wc_sync_enabled').on('change', function() {
878 var $checkbox = $(this);
879 var $status = $('#onwebchat_wc_sync_status');
880 var $syncDescription = $('.onwebchat-sync-description');
881 var isEnabled = $checkbox.is(':checked');
882
883 // Disable checkbox while saving
884 $checkbox.prop('disabled', true);
885
886 $.ajax({
887 url: ajaxurl,
888 type: 'POST',
889 data: {
890 action: 'onwebchat_wc_save_sync_enabled',
891 sync_enabled: isEnabled ? '1' : '0',
892 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
893 },
894 success: function(response) {
895 if (response.success) {
896 if (response.data.enabled) {
897 $status.html('<span style="color: green;">�
898 WooCommerce product sync enabled</span>');
899 // Update the sync description text if in cooldown
900 if ($syncDescription.length) {
901 var currentText = $syncDescription.text().trim();
902 if (currentText.includes('Bulk sync was completed recently')) {
903 $syncDescription.text('Bulk sync was completed recently. Your products are already up to date, and any new or updated products will be synced automatically.');
904 }
905 }
906 } else {
907 $status.html('<span style="color: #d63638;"> WooCommerce product sync disabled</span>');
908 // Update the sync description text if in cooldown
909 if ($syncDescription.length) {
910 var currentText = $syncDescription.text().trim();
911 if (currentText.includes('Bulk sync was completed recently')) {
912 $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.');
913 }
914 }
915 }
916 // Ensure status is visible and fade in smoothly
917 $status.css('visibility', 'visible').css('opacity', 1);
918 // Hide it after 2.5 seconds with fade out
919 setTimeout(function() {
920 $status.animate({opacity: 0}, 300, function() {
921 $status.css('visibility', 'hidden');
922 });
923 }, 1700);
924 } else {
925 // Revert checkbox on error
926 $checkbox.prop('checked', !isEnabled);
927 alert('Error: ' + (response.data || 'Failed to save setting'));
928 }
929 $checkbox.prop('disabled', false);
930 },
931 error: function() {
932 // Revert checkbox on error
933 $checkbox.prop('checked', !isEnabled);
934 alert('An error occurred. Please try again.');
935 $checkbox.prop('disabled', false);
936 }
937 });
938 });
939
940 // Sync now button
941 $('#onwebchat_sync_now').on('click', function() {
942 if ($(this).prop('disabled')) {
943 return;
944 }
945
946 // Determine the sync scope from the category picker (if shown).
947 var owcSelected = owcHasPicker ? owcSelectedCategoryIds() : [];
948
949 // Above the hard cap a category selection is required.
950 if (owcHasPicker && owcOverMax && owcSelected.length === 0) {
951 alert('Your store has too many products to sync all at once. Please tick at least one category to sync.');
952 return;
953 }
954
955 var confirmMsg = (owcSelected.length > 0)
956 ? 'Sync products in the selected categories with onWebChat to train your AI chatbot?'
957 : 'Sync all published products with onWebChat to train your AI chatbot?';
958 if (!confirm(confirmMsg)) {
959 return;
960 }
961
962 var $btn = $(this);
963 var nonce = '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>';
964 $btn.prop('disabled', true).text('Syncing products...');
965
966 // Hide the "Last sync" info while syncing
967 $('#onwebchat_last_sync_info').hide();
968 $('#onwebchat_total_synced_info').hide();
969
970 // Show initial syncing message with progress bar
971 $('#onwebchat_sync_status_display').html(
972 '<p style="margin: 0 0 10px 0;"><strong>⏳ Sync in progress:</strong> <span id="sync_progress_text">Starting…</span></p>' +
973 '<div style="background: #fff; border: 1px solid #ddd; border-radius: 4px; height: 20px; overflow: hidden;">' +
974 '<div id="sync_progress_bar" style="background: #2271b1; height: 100%; width: 0%; transition: width 0.3s;"></div>' +
975 '</div>' +
976 '<p style="margin: 10px 0 0 0; color: #666;">Please keep this page open until the sync completes.</p>'
977 );
978
979 function owcSyncFail(msg) {
980 alert('Error: ' + msg);
981 $btn.prop('disabled', false);
982 owcUpdateSyncButtonLabel();
983 $('#onwebchat_sync_status_display').html('');
984 }
985
986 function owcSyncProgress(done, total) {
987 var pct = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 0;
988 $('#sync_progress_text').text(done + ' / ' + total + ' products synced');
989 $('#sync_progress_bar').css('width', pct + '%');
990 }
991
992 // Drive the sync one page per request until the run reports complete.
993 // A single long request would outrun the server timeout on large
994 // catalogues and report a false failure; this keeps every request
995 // short and only reports success once the whole run is done.
996 function owcRunBatch(total, attempt) {
997 $.ajax({
998 url: ajaxurl,
999 type: 'POST',
1000 // MUST stay above the 180s the PHP side allows for its own
1001 // /product/batch call (see send_product_batch): on a first
1002 // sync of long descriptions onWebChat summarizes every
1003 // oversized product with a model call, so a page can take
1004 // minutes. A browser timeout shorter than that aborts a
1005 // request that is still running server-side, and the retry
1006 // below then re-enters sync_next_page() concurrently, which
1007 // double-counts the progress counters and can end the run
1008 // early with products left unsynced.
1009 timeout: 240000,
1010 data: { action: 'onwebchat_wc_sync_batch', nonce: nonce },
1011 success: function(resp) {
1012 if (!resp || !resp.success) { owcSyncFail((resp && resp.data) || 'Sync failed'); return; }
1013 var d = resp.data || {};
1014 total = d.total || total;
1015 owcSyncProgress(d.done || 0, total);
1016
1017 if (d.in_progress) {
1018 owcRunBatch(total, 0);
1019 } else {
1020 var s = d.stats || {};
1021 alert('Sync completed!\n\n' +
1022 'Created: ' + (s.created || 0) + '\n' +
1023 'Updated: ' + (s.updated || 0) + '\n' +
1024 'Unchanged: ' + (s.skipped || 0) + '\n' +
1025 'Errors: ' + (s.errors || 0));
1026 location.reload();
1027 }
1028 },
1029 error: function() {
1030 // One page timing out is recoverable (progress is saved
1031 // server-side): retry a few times before giving up.
1032 if (attempt < 3) {
1033 setTimeout(function() { owcRunBatch(total, attempt + 1); }, 3000);
1034 } else {
1035 owcSyncFail('A network error occurred. Some products may not have synced; please try again.');
1036 }
1037 }
1038 });
1039 }
1040
1041 // Kick off the run, then let the browser drive the batches.
1042 $.ajax({
1043 url: ajaxurl,
1044 type: 'POST',
1045 // Also processes the first page inline, so it needs the same
1046 // headroom as owcRunBatch above.
1047 timeout: 240000,
1048 data: {
1049 action: 'onwebchat_wc_sync_start',
1050 categories: owcSelected.join(','),
1051 nonce: nonce
1052 },
1053 success: function(response) {
1054 if (!response || !response.success) { owcSyncFail((response && response.data) || 'Could not start sync'); return; }
1055 var total = response.data.total || 0;
1056 owcSyncProgress(0, total);
1057 // Starting a sync switches automatic sync on server-side; show it.
1058 if (response.data.auto_sync_enabled) {
1059 $('#onwebchat_wc_sync_enabled').prop('checked', true);
1060 }
1061 if (total === 0) {
1062 alert('No products found to sync.');
1063 $btn.prop('disabled', false);
1064 owcUpdateSyncButtonLabel();
1065 $('#onwebchat_sync_status_display').html('');
1066 return;
1067 }
1068 owcRunBatch(total, 0);
1069 },
1070 error: function() {
1071 owcSyncFail('Could not start sync. Please try again.');
1072 }
1073 });
1074 });
1075
1076 // Explicit removal of unticked categories (point 3 of the scope model).
1077 // Unticking a category never deletes anything on its own; this button is
1078 // the only way products leave the AI training data from here, and it says
1079 // exactly how many are affected before doing it.
1080 $('#owc-scope-remove').on('click', function() {
1081 var $btn = $(this);
1082 var $progress = $('#owc-scope-remove-progress');
1083 var nonce = '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>';
1084 var keep = owcSelectedCategoryIds().join(',');
1085
1086 $btn.prop('disabled', true);
1087 $progress.text('Checking...').show();
1088
1089 function removeFail(message) {
1090 $btn.prop('disabled', false);
1091 $progress.hide().text('');
1092 alert(message);
1093 }
1094
1095 function removeNextPage(total, done) {
1096 $.ajax({
1097 url: ajaxurl,
1098 type: 'POST',
1099 timeout: 240000,
1100 data: { action: 'onwebchat_wc_scope_remove_batch', nonce: nonce },
1101 success: function(resp) {
1102 if (!resp || !resp.success) { removeFail((resp && resp.data) || 'Could not remove the products.'); return; }
1103 var d = resp.data || {};
1104 done = d.done || done;
1105 total = d.total || total;
1106 $progress.text('Removed ' + done + ' / ' + total + '...');
1107 if (d.complete) {
1108 $progress.text('Done. Reloading...');
1109 location.reload();
1110 return;
1111 }
1112 removeNextPage(total, done);
1113 },
1114 error: function() {
1115 removeFail('A network error occurred while removing the products. Some may still be in your AI training data; please try again.');
1116 }
1117 });
1118 }
1119
1120 // Ask the server how much this affects, confirm, then run.
1121 $.ajax({
1122 url: ajaxurl,
1123 type: 'POST',
1124 timeout: 240000,
1125 data: { action: 'onwebchat_wc_scope_remove_start', categories: keep, nonce: nonce },
1126 success: function(resp) {
1127 if (!resp || !resp.success) { removeFail((resp && resp.data) || 'Could not start the removal.'); return; }
1128 var d = resp.data || {};
1129 var total = d.total || 0;
1130 var message = 'Remove ' + total + (total === 1 ? ' product' : ' products') +
1131 ' of ' + d.categories + (d.categories === 1 ? ' category' : ' categories') +
1132 ' from your AI training data?\n\nYour chatbot will stop answering questions about them.' +
1133 ' You can sync them again at any time.';
1134 if (d.disables_sync) {
1135 message += '\n\nNothing is left ticked, so automatic product sync will also be switched off.';
1136 }
1137
1138 if (total === 0 || d.complete) {
1139 location.reload();
1140 return;
1141 }
1142
1143 if (!window.confirm(message)) {
1144 $btn.prop('disabled', false);
1145 $progress.hide().text('');
1146 return;
1147 }
1148
1149 $progress.text('Removed 0 / ' + total + '...');
1150 removeNextPage(total, 0);
1151 },
1152 error: function() {
1153 removeFail('Could not start the removal. Please try again.');
1154 }
1155 });
1156 });
1157
1158 // Manual process batch button (for debugging stuck syncs)
1159 $('#onwebchat_manual_process_batch').on('click', function() {
1160 var $btn = $(this);
1161 $btn.prop('disabled', true).text('Processing...');
1162
1163 $.ajax({
1164 url: ajaxurl,
1165 type: 'POST',
1166 data: {
1167 action: 'onwebchat_wc_manual_process_batch',
1168 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
1169 },
1170 success: function(response) {
1171 if (response.success) {
1172 alert('Batch processed. Check console/logs for details.');
1173 location.reload();
1174 } else {
1175 alert('Error: ' + response.data);
1176 $btn.prop('disabled', false).text('Process Batch Manually');
1177 }
1178 },
1179 error: function() {
1180 alert('An error occurred. Please try again.');
1181 $btn.prop('disabled', false).text('Process Batch Manually');
1182 }
1183 });
1184 });
1185
1186 // Reset sync status button
1187 $('#onwebchat_reset_sync_status').on('click', function() {
1188 if (!confirm('Mark sync as complete? This will reset the progress indicator.')) {
1189 return;
1190 }
1191
1192 var $btn = $(this);
1193 $btn.prop('disabled', true).text('Resetting...');
1194
1195 $.ajax({
1196 url: ajaxurl,
1197 type: 'POST',
1198 data: {
1199 action: 'onwebchat_wc_reset_sync_status',
1200 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
1201 },
1202 success: function(response) {
1203 if (response.success) {
1204 location.reload();
1205 } else {
1206 alert('Error: ' + response.data);
1207 $btn.prop('disabled', false).text('Mark Sync as Complete');
1208 }
1209 },
1210 error: function() {
1211 alert('An error occurred. Please try again.');
1212 $btn.prop('disabled', false).text('Mark Sync as Complete');
1213 }
1214 });
1215 });
1216
1217 // Regenerate secret button (now "Disconnect" button)
1218 $('#onwebchat_regenerate_secret').on('click', function() {
1219 if (!confirm('This will disconnect WooCommerce sync. You will need to re-authenticate with your credentials. Continue?')) {
1220 return;
1221 }
1222
1223 var $btn = $(this);
1224 $btn.prop('disabled', true).text('Disconnecting...');
1225
1226 $.ajax({
1227 url: ajaxurl,
1228 type: 'POST',
1229 data: {
1230 action: 'onwebchat_wc_regenerate_secret',
1231 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
1232 },
1233 success: function(response) {
1234 if (response.success) {
1235 alert('WooCommerce sync disconnected. Please re-authenticate to continue syncing.');
1236 location.reload();
1237 } else {
1238 alert('Error: ' + response.data);
1239 $btn.prop('disabled', false).text('Disconnect');
1240 }
1241 },
1242 error: function() {
1243 alert('An error occurred. Please try again.');
1244 $btn.prop('disabled', false).text('Disconnect');
1245 }
1246 });
1247 });
1248 });
1249 </script>
1250
1251 <?php endif; // End if secret exists ?>
1252
1253 <?php
1254 }
1255
1256 /**
1257 * Handle form submissions for WooCommerce tab
1258 */
1259 function onwebchat_handle_woocommerce_actions() {
1260
1261 if (isset($_POST["action"]) && $_POST["action"] == "save_wc_sync") {
1262
1263 if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'onwebchat_wc_sync_nonce')) {
1264 wp_die('Sorry, your nonce did not verify.');
1265 }
1266
1267 if (!current_user_can('manage_options')) {
1268 wp_die('Insufficient permissions.');
1269 }
1270
1271 // Save settings
1272 $sync_enabled = isset($_POST["onwebchat_wc_sync_enabled"]) ? true : false;
1273 $sync_mode = isset($_POST["onwebchat_wc_sync_mode"]) ? sanitize_text_field($_POST["onwebchat_wc_sync_mode"]) : 'short_plus_full';
1274 $allowed_sync_modes = array('short_only', 'short_fallback_full', 'short_plus_full');
1275 if (!in_array($sync_mode, $allowed_sync_modes, true)) {
1276 $sync_mode = 'short_plus_full';
1277 }
1278
1279 update_option('onwebchat_wc_sync_enabled', $sync_enabled);
1280 update_option('onwebchat_wc_sync_mode', $sync_mode);
1281
1282 // Secret is now obtained via authenticated connection, not auto-generated
1283
1284 wp_redirect(admin_url('admin.php?page=onwebchat_settings&tab=woocommerce&wc_saved=1'));
1285 exit;
1286 }
1287 }
1288
1289