/** * WordpressSyncCard — WP product sync control for the CP dashboard in embed mode. * * Shown only when: * 1. VITE_WP_EMBED === 'true' (embedded in WP admin) * 2. archerAi.wooActive === true (WooCommerce is installed + active) * 3. The tenant's domain matches archerAi.domain (this is the WP site) * * When WC is missing, renders an info alert prompting WooCommerce install. * When active, renders: * - Product count + last sync timestamp * - "Sync Products" button → wordpressApi.triggerSync → poll status 2s * - Error banner for last failed job (not yet wired — Phase 5+ populates) * * All HTTP to WP + customer-api is already set up by the auto-connect hook * in _authenticated.tsx. This card only reads status + triggers sync jobs. * * (c) 2026 TWWIM UG. All rights reserved. (www.twwim.com) */ import { useEffect, useState } from 'react'; import { AlertTriangle, Loader2, RefreshCcw, ShoppingBag } from 'lucide-react'; import type { WordpressSyncStatusResponse } from '@archer/api-interface'; import { wordpressApi } from '@/infrastructure/http/api/wordpress'; import { getWpDomain, isWooCommerceActive, isWordpressEmbed } from '@/lib/wp-integration'; interface WordpressSyncCardProps { tenantId: string; tenantDomain: string; } const POLL_INTERVAL_MS = 2_000; const POLL_ACTIVE_STATES: ReadonlyArray = [ 'queued', 'running', ]; export function WordpressSyncCard({ tenantId, tenantDomain }: WordpressSyncCardProps) { const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [triggering, setTriggering] = useState(false); // Initial load + polling when a job is active useEffect(() => { if (!shouldRender(tenantDomain)) return; let cancelled = false; let timer: ReturnType | undefined; const poll = async () => { try { const next = await wordpressApi.getStatus(tenantId); if (cancelled) return; setStatus(next); setError(null); if (next.jobStatus && POLL_ACTIVE_STATES.includes(next.jobStatus)) { timer = setTimeout(poll, POLL_INTERVAL_MS); } } catch (err) { if (cancelled) return; setError((err as Error).message); } }; void poll(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [tenantId, tenantDomain]); if (!shouldRender(tenantDomain)) return null; if (!isWooCommerceActive()) { return (

WooCommerce not active

Product sync requires WooCommerce. Install and activate WooCommerce to enable product knowledge sync. The TWWIM widget still works without it.

); } const handleSync = async () => { setTriggering(true); setError(null); try { await wordpressApi.triggerSync(tenantId); // Poll the new job immediately const next = await wordpressApi.getStatus(tenantId); setStatus(next); } catch (err) { setError((err as Error).message); } finally { setTriggering(false); } }; const count = status?.count ?? 0; const lastSyncedAt = status?.lastSyncedAt ? new Date(status.lastSyncedAt).toLocaleString() : '—'; const jobStatus = status?.jobStatus ?? null; const isBusy = triggering || (jobStatus !== null && POLL_ACTIVE_STATES.includes(jobStatus)); return (

WooCommerce product sync

Products synced
{count}
Last sync
{lastSyncedAt}
Status
{jobStatus ?? '—'}
{status?.progress && (
{status.progress.phase} {status.progress.processed} / {status.progress.total}
)} {status?.lastError && (
Last sync failed ({status.lastError.code}): {status.lastError.message}
)} {error && !status?.lastError && (
{error}
)}
); } function shouldRender(tenantDomain: string): boolean { if (!isWordpressEmbed()) return false; return tenantDomain === getWpDomain(); } function progressPercent(processed: number, total: number): number { if (!total || total <= 0) return 0; return Math.min(100, Math.round((processed / total) * 100)); }