PluginProbe
Live Chat & AI Chatbot – onWebChat / trunk
Live Chat & AI Chatbot – onWebChat vtrunk
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 trunk, at admin/tabs/woocommerce.php

1,285 lines 59.3 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>WooCommerce Product Sync</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 if (total === 0) {
1058 alert('No products found to sync.');
1059 $btn.prop('disabled', false);
1060 owcUpdateSyncButtonLabel();
1061 $('#onwebchat_sync_status_display').html('');
1062 return;
1063 }
1064 owcRunBatch(total, 0);
1065 },
1066 error: function() {
1067 owcSyncFail('Could not start sync. Please try again.');
1068 }
1069 });
1070 });
1071
1072 // Explicit removal of unticked categories (point 3 of the scope model).
1073 // Unticking a category never deletes anything on its own; this button is
1074 // the only way products leave the AI training data from here, and it says
1075 // exactly how many are affected before doing it.
1076 $('#owc-scope-remove').on('click', function() {
1077 var $btn = $(this);
1078 var $progress = $('#owc-scope-remove-progress');
1079 var nonce = '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>';
1080 var keep = owcSelectedCategoryIds().join(',');
1081
1082 $btn.prop('disabled', true);
1083 $progress.text('Checking...').show();
1084
1085 function removeFail(message) {
1086 $btn.prop('disabled', false);
1087 $progress.hide().text('');
1088 alert(message);
1089 }
1090
1091 function removeNextPage(total, done) {
1092 $.ajax({
1093 url: ajaxurl,
1094 type: 'POST',
1095 timeout: 240000,
1096 data: { action: 'onwebchat_wc_scope_remove_batch', nonce: nonce },
1097 success: function(resp) {
1098 if (!resp || !resp.success) { removeFail((resp && resp.data) || 'Could not remove the products.'); return; }
1099 var d = resp.data || {};
1100 done = d.done || done;
1101 total = d.total || total;
1102 $progress.text('Removed ' + done + ' / ' + total + '...');
1103 if (d.complete) {
1104 $progress.text('Done. Reloading...');
1105 location.reload();
1106 return;
1107 }
1108 removeNextPage(total, done);
1109 },
1110 error: function() {
1111 removeFail('A network error occurred while removing the products. Some may still be in your AI training data; please try again.');
1112 }
1113 });
1114 }
1115
1116 // Ask the server how much this affects, confirm, then run.
1117 $.ajax({
1118 url: ajaxurl,
1119 type: 'POST',
1120 timeout: 240000,
1121 data: { action: 'onwebchat_wc_scope_remove_start', categories: keep, nonce: nonce },
1122 success: function(resp) {
1123 if (!resp || !resp.success) { removeFail((resp && resp.data) || 'Could not start the removal.'); return; }
1124 var d = resp.data || {};
1125 var total = d.total || 0;
1126 var message = 'Remove ' + total + (total === 1 ? ' product' : ' products') +
1127 ' of ' + d.categories + (d.categories === 1 ? ' category' : ' categories') +
1128 ' from your AI training data?\n\nYour chatbot will stop answering questions about them.' +
1129 ' You can sync them again at any time.';
1130 if (d.disables_sync) {
1131 message += '\n\nNothing is left ticked, so automatic product sync will also be switched off.';
1132 }
1133
1134 if (total === 0 || d.complete) {
1135 location.reload();
1136 return;
1137 }
1138
1139 if (!window.confirm(message)) {
1140 $btn.prop('disabled', false);
1141 $progress.hide().text('');
1142 return;
1143 }
1144
1145 $progress.text('Removed 0 / ' + total + '...');
1146 removeNextPage(total, 0);
1147 },
1148 error: function() {
1149 removeFail('Could not start the removal. Please try again.');
1150 }
1151 });
1152 });
1153
1154 // Manual process batch button (for debugging stuck syncs)
1155 $('#onwebchat_manual_process_batch').on('click', function() {
1156 var $btn = $(this);
1157 $btn.prop('disabled', true).text('Processing...');
1158
1159 $.ajax({
1160 url: ajaxurl,
1161 type: 'POST',
1162 data: {
1163 action: 'onwebchat_wc_manual_process_batch',
1164 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
1165 },
1166 success: function(response) {
1167 if (response.success) {
1168 alert('Batch processed. Check console/logs for details.');
1169 location.reload();
1170 } else {
1171 alert('Error: ' + response.data);
1172 $btn.prop('disabled', false).text('Process Batch Manually');
1173 }
1174 },
1175 error: function() {
1176 alert('An error occurred. Please try again.');
1177 $btn.prop('disabled', false).text('Process Batch Manually');
1178 }
1179 });
1180 });
1181
1182 // Reset sync status button
1183 $('#onwebchat_reset_sync_status').on('click', function() {
1184 if (!confirm('Mark sync as complete? This will reset the progress indicator.')) {
1185 return;
1186 }
1187
1188 var $btn = $(this);
1189 $btn.prop('disabled', true).text('Resetting...');
1190
1191 $.ajax({
1192 url: ajaxurl,
1193 type: 'POST',
1194 data: {
1195 action: 'onwebchat_wc_reset_sync_status',
1196 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
1197 },
1198 success: function(response) {
1199 if (response.success) {
1200 location.reload();
1201 } else {
1202 alert('Error: ' + response.data);
1203 $btn.prop('disabled', false).text('Mark Sync as Complete');
1204 }
1205 },
1206 error: function() {
1207 alert('An error occurred. Please try again.');
1208 $btn.prop('disabled', false).text('Mark Sync as Complete');
1209 }
1210 });
1211 });
1212
1213 // Regenerate secret button (now "Disconnect" button)
1214 $('#onwebchat_regenerate_secret').on('click', function() {
1215 if (!confirm('This will disconnect WooCommerce sync. You will need to re-authenticate with your credentials. Continue?')) {
1216 return;
1217 }
1218
1219 var $btn = $(this);
1220 $btn.prop('disabled', true).text('Disconnecting...');
1221
1222 $.ajax({
1223 url: ajaxurl,
1224 type: 'POST',
1225 data: {
1226 action: 'onwebchat_wc_regenerate_secret',
1227 nonce: '<?php echo wp_create_nonce('onwebchat_wc_sync_nonce'); ?>'
1228 },
1229 success: function(response) {
1230 if (response.success) {
1231 alert('WooCommerce sync disconnected. Please re-authenticate to continue syncing.');
1232 location.reload();
1233 } else {
1234 alert('Error: ' + response.data);
1235 $btn.prop('disabled', false).text('Disconnect');
1236 }
1237 },
1238 error: function() {
1239 alert('An error occurred. Please try again.');
1240 $btn.prop('disabled', false).text('Disconnect');
1241 }
1242 });
1243 });
1244 });
1245 </script>
1246
1247 <?php endif; // End if secret exists ?>
1248
1249 <?php
1250 }
1251
1252 /**
1253 * Handle form submissions for WooCommerce tab
1254 */
1255 function onwebchat_handle_woocommerce_actions() {
1256
1257 if (isset($_POST["action"]) && $_POST["action"] == "save_wc_sync") {
1258
1259 if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'onwebchat_wc_sync_nonce')) {
1260 wp_die('Sorry, your nonce did not verify.');
1261 }
1262
1263 if (!current_user_can('manage_options')) {
1264 wp_die('Insufficient permissions.');
1265 }
1266
1267 // Save settings
1268 $sync_enabled = isset($_POST["onwebchat_wc_sync_enabled"]) ? true : false;
1269 $sync_mode = isset($_POST["onwebchat_wc_sync_mode"]) ? sanitize_text_field($_POST["onwebchat_wc_sync_mode"]) : 'short_plus_full';
1270 $allowed_sync_modes = array('short_only', 'short_fallback_full', 'short_plus_full');
1271 if (!in_array($sync_mode, $allowed_sync_modes, true)) {
1272 $sync_mode = 'short_plus_full';
1273 }
1274
1275 update_option('onwebchat_wc_sync_enabled', $sync_enabled);
1276 update_option('onwebchat_wc_sync_mode', $sync_mode);
1277
1278 // Secret is now obtained via authenticated connection, not auto-generated
1279
1280 wp_redirect(admin_url('admin.php?page=onwebchat_settings&tab=woocommerce&wc_saved=1'));
1281 exit;
1282 }
1283 }
1284
1285