get_option('onwebchat_wc_last_bulk_sync', 0),
'in_progress' => get_option('onwebchat_wc_bulk_in_progress', false),
'done' => get_option('onwebchat_wc_bulk_done', 0),
'total' => get_option('onwebchat_wc_bulk_total', 0),
'last_sync_start' => get_option('onwebchat_wc_last_sync_start', 0),
);
// What the AI training data currently covers (point 4 of the scope model):
// the saved scope IS what was synced, because a sync only ever adds to it.
global $onwebchat_wc_sync;
$owc_scope_summary = (isset($onwebchat_wc_sync) && is_object($onwebchat_wc_sync) && method_exists($onwebchat_wc_sync, 'get_scope_summary'))
? $onwebchat_wc_sync->get_scope_summary()
: null;
// Calculate cooldown status
// Short on purpose: it only guards against double-clicking the sync button.
// Now that the selection is cumulative, syncing again straight after a run is
// a normal thing to do (you just ticked another subcategory), so a long block
// gets in the way. The button stays VISIBLE and disabled with a live
// countdown, instead of disappearing as it used to.
// Also in includes/woocommerce-sync.php.
$cooldown_period = 30; // seconds
$time_since_last_sync = time() - $sync_status['last_sync_start'];
$is_in_cooldown = ($time_since_last_sync < $cooldown_period) && $sync_status['last_sync_start'] > 0;
$cooldown_remaining = $is_in_cooldown ? max(1, $cooldown_period - $time_since_last_sync) : 0;
?>
WooCommerce settings saved successfully!
β οΈ WooCommerce Sync Authentication Error:
This usually happens when:
You're trying to connect the plugin to a different onWebChat account
Solution: Please enter your onWebChat account credentials again in the "Connect WooCommerce Sync" section below and click "Connect WooCommerce Sync" button to reconnect.
Product Sync for the AI Chatbot
Let your AI chatbot understand your WooCommerce products.
When enabled, products are automatically synced and kept up to date, allowing the AI to answer product-related questions accurately.
π Connect WooCommerce Sync
To enable WooCommerce sync, please enter your onWebChat account credentials below.
AI Order Status Lookup
Let your AI chatbot answer "where is my order?" with live data. The store verifies identity using the
order number and email (signed-in users only need the order number) before sharing details.
Bulk Sync Status
publish)) ? (int) $owc_counts->publish : 0;
$owc_needs_selection = $owc_published_count > $owc_threshold;
$owc_over_max = $owc_published_count > $owc_max;
// Saved sync scope (product_cat term IDs) to pre-tick in the picker.
$owc_saved_scope = array();
$owc_scope_raw = (string) get_option('onwebchat_wc_sync_categories', '');
if ($owc_scope_raw !== '') {
foreach (explode(',', $owc_scope_raw) as $owc_sid) {
$owc_sid = (int) trim($owc_sid);
if ($owc_sid > 0) {
$owc_saved_scope[] = $owc_sid;
}
}
}
$owc_categories = $owc_needs_selection ? get_terms(array(
'taxonomy' => 'product_cat',
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
)) : array();
if (is_wp_error($owc_categories)) {
$owc_categories = array();
}
// Build the whole category tree for the picker. Every category is offered
// (subcategories indented under their parent, searchable by name or path),
// and selecting one covers its whole subtree, so each row shows a distinct
// product count spanning the category and all of its descendants.
$owc_term_by_id = array(); // term_id => term object
foreach ($owc_categories as $owc_term) {
$owc_term_by_id[(int) $owc_term->term_id] = $owc_term;
}
$owc_children = array(); // parent_id => array of term objects (name order, from get_terms)
$owc_parent_of = array(); // term_id => parent term_id (0 for roots and orphans)
foreach ($owc_categories as $owc_term) {
$owc_pid = (int) $owc_term->parent;
if (!isset($owc_term_by_id[$owc_pid])) {
$owc_pid = 0; // orphan (parent missing): treat as a root
}
$owc_parent_of[(int) $owc_term->term_id] = $owc_pid;
$owc_children[$owc_pid][] = $owc_term;
}
// Distinct published products per category subtree, computed in ONE pass:
// read every (product, category) pair of the published products and, for
// each product, credit the category and each of its ancestors once. Counting
// per product avoids the over-count you get from summing per-term counts when
// a product sits in both a parent and a child category, and one query keeps
// the page fast even with more than a thousand categories (a WP_Query per
// category would not).
$owc_counts = array(); // term_id => distinct published products in the subtree
if (!empty($owc_categories)) {
global $wpdb;
$owc_pairs = $wpdb->get_results($wpdb->prepare(
"SELECT tr.object_id, tt.term_id
FROM {$wpdb->term_relationships} tr
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id
WHERE tt.taxonomy = %s AND p.post_type = %s AND p.post_status = %s
ORDER BY tr.object_id",
'product_cat', 'product', 'publish'
), ARRAY_N);
if (!is_array($owc_pairs)) {
$owc_pairs = array();
}
$owc_current_product = 0;
$owc_credited = array(); // categories already credited for the current product
foreach ($owc_pairs as $owc_pair) {
$owc_product_id = (int) $owc_pair[0];
if ($owc_product_id !== $owc_current_product) {
$owc_current_product = $owc_product_id;
$owc_credited = array();
}
// Walk up to the root; stop at a category already credited for this
// product (its ancestors were credited on that walk too).
$owc_walk = (int) $owc_pair[1];
$owc_hops = 0;
while ($owc_walk > 0 && isset($owc_parent_of[$owc_walk]) && !isset($owc_credited[$owc_walk]) && $owc_hops++ < 100) {
$owc_credited[$owc_walk] = true;
$owc_counts[$owc_walk] = isset($owc_counts[$owc_walk]) ? $owc_counts[$owc_walk] + 1 : 1;
$owc_walk = $owc_parent_of[$owc_walk];
}
}
unset($owc_pairs, $owc_credited);
}
// Rows in tree order (depth first, siblings by name), skipping categories
// whose subtree holds no published product. Anything not reached from the
// roots (e.g. a parent loop) is appended as a root so nothing is hidden.
$owc_rows = array(); // each: term, name, count, level, parents (path prefix)
$owc_visited = array();
for ($owc_pass = 0; $owc_pass < 2; $owc_pass++) {
if ($owc_pass === 0) {
$owc_roots = isset($owc_children[0]) ? $owc_children[0] : array();
} else {
$owc_roots = array();
foreach ($owc_categories as $owc_term) {
if (!isset($owc_visited[(int) $owc_term->term_id])) {
$owc_roots[] = $owc_term;
}
}
}
$owc_stack = array();
foreach (array_reverse($owc_roots) as $owc_term) {
$owc_stack[] = array($owc_term, 0, '');
}
while (!empty($owc_stack)) {
list($owc_term, $owc_level, $owc_parents) = array_pop($owc_stack);
$owc_id = (int) $owc_term->term_id;
if (isset($owc_visited[$owc_id])) {
continue;
}
$owc_visited[$owc_id] = true;
$owc_count = isset($owc_counts[$owc_id]) ? $owc_counts[$owc_id] : 0;
if ($owc_count === 0) {
continue; // empty subtree (descendants are counted into their ancestors)
}
$owc_name = html_entity_decode((string) $owc_term->name, ENT_QUOTES, 'UTF-8');
$owc_rows[] = array(
'term' => $owc_term,
'name' => $owc_name,
'count' => $owc_count,
'level' => $owc_level,
'parents' => $owc_parents,
);
if (!empty($owc_children[$owc_id])) {
foreach (array_reverse($owc_children[$owc_id]) as $owc_child) {
$owc_stack[] = array($owc_child, $owc_level + 1, $owc_parents . $owc_name . ' > ');
}
}
}
}
unset($owc_visited, $owc_stack, $owc_term_by_id, $owc_children);
?>
Choose what to sync
β³ Sync in progress:
/ products synced
0): ?>
β Last bulk sync:
Total products synced:
No bulk sync has been performed yet.
In your AI training data:
no products yet
A sync has just finished. The button is available again in a moment, so you can sync another selection right away.
A sync has just finished. Please enable "Automatically sync products to onWebChat for AI training" above to keep your products synced automatically.
Click to sync all existing products. This process runs in the background and may take several minutes depending on your product count.
= total OR in progress for more than 3 minutes)
$stuck_sync = $sync_status['in_progress'] && (
$sync_status['done'] >= $sync_status['total'] ||
(time() - $sync_status['last_sync_start'] > 180)
);
if ($stuck_sync):
?>
If sync appears stuck, try processing manually or reset status.