Explorar el Código

feat: add manual sync status polling and statistics display #22

- Fixed infinite loading state when sync button is pressed
- Added polling mechanism to check sync status every 3 seconds
- Implemented sync statistics display showing synced items by type
- Updated trigger-sync to capture and store sync stats in alt_data
- Added toast notifications when sync completes with item counts
- Statistics show products, orders, and customers synced
- Properly handle sync completion and error states

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude hace 5 meses
padre
commit
1b7cc6f7ee

+ 90 - 3
shopcall.ai-main/src/components/IntegrationsContent.tsx

@@ -39,6 +39,7 @@ export function IntegrationsContent() {
   const [loading, setLoading] = useState(true);
   const [showConnectDialog, setShowConnectDialog] = useState(false);
   const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
+  const [syncingStores, setSyncingStores] = useState<Set<string>>(new Set());
   const { toast } = useToast();
 
   const fetchStores = async () => {
@@ -62,7 +63,46 @@ export function IntegrationsContent() {
 
       const data = await response.json();
       if (data.success) {
-        setConnectedShops(data.stores || []);
+        const stores = data.stores || [];
+        setConnectedShops(stores);
+
+        // Update syncingStores - remove stores that finished syncing
+        setSyncingStores(prev => {
+          const newSyncing = new Set(prev);
+          stores.forEach((store: ConnectedStore) => {
+            if (prev.has(store.id) && store.sync_status !== 'syncing') {
+              newSyncing.delete(store.id);
+
+              // Show completion notification
+              if (store.sync_status === 'completed') {
+                const stats = store.alt_data?.last_sync_stats;
+                let description = `${store.store_name || 'Store'} data synchronized successfully.`;
+
+                if (stats) {
+                  const items = [];
+                  if (stats.products?.synced) items.push(`${stats.products.synced} products`);
+                  if (stats.orders?.synced) items.push(`${stats.orders.synced} orders`);
+                  if (stats.customers?.synced) items.push(`${stats.customers.synced} customers`);
+                  if (items.length > 0) {
+                    description = `Synced: ${items.join(', ')}`;
+                  }
+                }
+
+                toast({
+                  title: "Sync Complete",
+                  description,
+                });
+              } else if (store.sync_status === 'error') {
+                toast({
+                  title: "Sync Failed",
+                  description: store.sync_error || "An error occurred during synchronization.",
+                  variant: "destructive",
+                });
+              }
+            }
+          });
+          return newSyncing;
+        });
       }
     } catch (error) {
       console.error('Error fetching stores:', error);
@@ -138,6 +178,17 @@ export function IntegrationsContent() {
     fetchStores();
   }, []);
 
+  // Poll for sync status updates
+  useEffect(() => {
+    if (syncingStores.size === 0) return;
+
+    const pollInterval = setInterval(() => {
+      fetchStores();
+    }, 3000); // Poll every 3 seconds
+
+    return () => clearInterval(pollInterval);
+  }, [syncingStores]);
+
   const platforms = [
     {
       id: "shopify",
@@ -242,13 +293,16 @@ export function IntegrationsContent() {
         throw new Error(data.error || 'Failed to trigger sync');
       }
 
+      // Add store to syncing set to start polling
+      setSyncingStores(prev => new Set(prev).add(storeId));
+
       toast({
         title: "Sync Started",
         description: `Data synchronization started for ${storeName}`,
       });
 
-      // Refresh stores list to show updated sync status
-      setTimeout(() => fetchStores(), 1000);
+      // Refresh stores list immediately to show syncing status
+      setTimeout(() => fetchStores(), 500);
     } catch (error) {
       console.error('Error triggering sync:', error);
       toast({
@@ -279,6 +333,38 @@ export function IntegrationsContent() {
     }
   };
 
+  const getSyncStats = (shop: ConnectedStore) => {
+    const stats = shop.alt_data?.last_sync_stats;
+    if (!stats || shop.sync_status !== 'completed') return null;
+
+    const items = [];
+    if (stats.products?.synced !== undefined) {
+      items.push({ label: 'Products', count: stats.products.synced, errors: stats.products.errors || 0 });
+    }
+    if (stats.orders?.synced !== undefined) {
+      items.push({ label: 'Orders', count: stats.orders.synced, errors: stats.orders.errors || 0 });
+    }
+    if (stats.customers?.synced !== undefined) {
+      items.push({ label: 'Customers', count: stats.customers.synced, errors: stats.customers.errors || 0 });
+    }
+
+    if (items.length === 0) return null;
+
+    return (
+      <div className="flex flex-wrap gap-2 mt-2">
+        {items.map((item) => (
+          <div key={item.label} className="text-xs bg-slate-700 px-2 py-1 rounded">
+            <span className="text-slate-400">{item.label}:</span>{' '}
+            <span className="text-green-400 font-semibold">{item.count}</span>
+            {item.errors > 0 && (
+              <span className="text-red-400 ml-1">({item.errors} errors)</span>
+            )}
+          </div>
+        ))}
+      </div>
+    );
+  };
+
   return (
     <div className="flex-1 space-y-6 p-8 bg-slate-900">
       <div className="flex items-center justify-between">
@@ -514,6 +600,7 @@ export function IntegrationsContent() {
                                 {new Date(shop.sync_completed_at).toLocaleString()}
                               </div>
                             )}
+                            {getSyncStats(shop)}
                           </div>
                         </td>
                         <td className="p-4">

+ 30 - 1
supabase/functions/trigger-sync/index.ts

@@ -119,11 +119,40 @@ serve(async (req) => {
 
         if (response.ok) {
           console.log(`[TriggerSync] Sync completed successfully for ${store_id}`)
+
+          // Extract sync statistics from result
+          let syncStats = null
+          if (result.stats) {
+            // WooCommerce and ShopRenter format
+            syncStats = result.stats
+          } else if (result.results) {
+            // Shopify format
+            syncStats = {
+              products: result.results.products || { synced: 0, errors: 0 },
+              orders: result.results.orders || { synced: 0, errors: 0 },
+              customers: result.results.customers || { synced: 0, errors: 0 }
+            }
+          }
+
+          // Get current alt_data to preserve existing fields
+          const { data: currentStore } = await supabaseAdmin
+            .from('stores')
+            .select('alt_data')
+            .eq('id', store_id)
+            .single()
+
+          const altData = currentStore?.alt_data || {}
+
           await supabaseAdmin
             .from('stores')
             .update({
               sync_status: 'completed',
-              sync_completed_at: new Date().toISOString()
+              sync_completed_at: new Date().toISOString(),
+              alt_data: {
+                ...altData,
+                last_sync_at: new Date().toISOString(),
+                last_sync_stats: syncStats
+              }
             })
             .eq('id', store_id)
         } else {