Преглед изворни кода

feat: Add shop disable/enable feature #41

Implemented functionality to enable/disable shops with the following changes:

Frontend:
- Added UI toggle in IntegrationsContent to enable/disable background sync
- Added "Background Sync" column with Switch component in stores table
- Added handleToggleStoreEnabled function to manage enable/disable state
- Updated fetchStores to include store_sync_config data
- Disabled toggle during active sync operations

Backend:
- Added PUT /api/stores/:id/enable endpoint to update sync status
- Modified GET /api/stores to support include_sync_config query parameter
- Added store ownership verification before updates

Scheduled Sync:
- Updated shoprenter-scheduled-sync to check store_sync_config.enabled
- Updated to filter stores based on enabled status and next_sync_at
- Added logic to update store_sync_config timestamps after sync
- Now skips disabled stores during scheduled background sync

When a shop is disabled:
- Background sync is paused (scheduled sync skips the shop)
- Manual sync is still available via the sync button
- User receives clear feedback about the change

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude пре 5 месеци
родитељ
комит
3fb1f676e5

+ 70 - 1
shopcall.ai-main/src/components/IntegrationsContent.tsx

@@ -32,6 +32,13 @@ interface ConnectedStore {
     connectedAt?: string;
     [key: string]: any;
   };
+  store_sync_config?: {
+    enabled: boolean;
+    sync_frequency: string;
+    sync_products: boolean;
+    sync_orders: boolean;
+    sync_customers: boolean;
+  }[];
 }
 
 export function IntegrationsContent() {
@@ -50,7 +57,7 @@ export function IntegrationsContent() {
       }
 
       const session = JSON.parse(sessionData);
-      const response = await fetch(`${API_URL}/api/stores`, {
+      const response = await fetch(`${API_URL}/api/stores?include_sync_config=true`, {
         headers: {
           'Authorization': `Bearer ${session.session.access_token}`,
           'Content-Type': 'application/json'
@@ -318,6 +325,50 @@ export function IntegrationsContent() {
     }
   };
 
+  const handleToggleStoreEnabled = async (storeId: string, storeName: string, currentEnabled: boolean) => {
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        throw new Error('No session data found');
+      }
+
+      const session = JSON.parse(sessionData);
+      const newEnabled = !currentEnabled;
+
+      const response = await fetch(`${API_URL}/api/stores/${storeId}/enable`, {
+        method: 'PUT',
+        headers: {
+          'Authorization': `Bearer ${session.session.access_token}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ enabled: newEnabled })
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        throw new Error(data.error || 'Failed to update store status');
+      }
+
+      toast({
+        title: newEnabled ? "Shop Enabled" : "Shop Disabled",
+        description: newEnabled
+          ? `${storeName} is now enabled. Background sync will resume.`
+          : `${storeName} is now disabled. Background sync is paused (manual sync still available).`,
+      });
+
+      // Refresh stores list
+      fetchStores();
+    } catch (error) {
+      console.error('Error toggling store enabled status:', error);
+      toast({
+        title: "Update Failed",
+        description: "Failed to update shop status. Please try again.",
+        variant: "destructive",
+      });
+    }
+  };
+
   const getSyncStatusBadge = (shop: ConnectedStore) => {
     const status = shop.sync_status || 'idle';
 
@@ -542,6 +593,7 @@ export function IntegrationsContent() {
                       <th className="p-4 text-slate-300 font-medium">Phone Number</th>
                       <th className="p-4 text-slate-300 font-medium">Platform</th>
                       <th className="p-4 text-slate-300 font-medium">Sync Status</th>
+                      <th className="p-4 text-slate-300 font-medium">Background Sync</th>
                       <th className="p-4 text-slate-300 font-medium">Created</th>
                       <th className="p-4 text-slate-300 font-medium">Status</th>
                       <th className="p-4 text-slate-300 font-medium">Actions</th>
@@ -608,6 +660,23 @@ export function IntegrationsContent() {
                             {getSyncStats(shop)}
                           </div>
                         </td>
+                        <td className="p-4">
+                          <div className="flex items-center gap-2">
+                            <Switch
+                              checked={shop.store_sync_config?.[0]?.enabled ?? true}
+                              onCheckedChange={() => handleToggleStoreEnabled(
+                                shop.id,
+                                shop.store_name || 'this store',
+                                shop.store_sync_config?.[0]?.enabled ?? true
+                              )}
+                              disabled={shop.sync_status === 'syncing'}
+                              title={shop.sync_status === 'syncing' ? 'Cannot change while syncing' : 'Toggle background sync'}
+                            />
+                            <span className="text-xs text-slate-400">
+                              {shop.store_sync_config?.[0]?.enabled ?? true ? 'Enabled' : 'Disabled'}
+                            </span>
+                          </div>
+                        </td>
                         <td className="p-4">
                           <div className="text-sm text-slate-400">
                             {shop.created_at ? new Date(shop.created_at).toLocaleDateString() : '-'}

+ 92 - 2
supabase/functions/api/index.ts

@@ -50,12 +50,25 @@ serve(async (req) => {
 
     // GET /api/stores - List all stores for the user
     if (path === 'stores' && req.method === 'GET') {
-      const { data: stores, error } = await supabase
+      const includeSyncConfig = url.searchParams.get('include_sync_config') === 'true'
+
+      let query = supabase
         .from('stores')
-        .select('*')
+        .select(includeSyncConfig ? `
+          *,
+          store_sync_config (
+            enabled,
+            sync_frequency,
+            sync_products,
+            sync_orders,
+            sync_customers
+          )
+        ` : '*')
         .eq('user_id', user.id)
         .order('created_at', { ascending: false })
 
+      const { data: stores, error } = await query
+
       if (error) {
         console.error('Error fetching stores:', error)
         return new Response(
@@ -262,6 +275,83 @@ serve(async (req) => {
       )
     }
 
+    // PUT /api/stores/:id/enable - Enable or disable background sync for a store
+    if (path.match(/^stores\/[^\/]+\/enable$/) && req.method === 'PUT') {
+      const storeId = path.split('/')[1]
+      const { enabled } = await req.json()
+
+      if (typeof enabled !== 'boolean') {
+        return new Response(
+          JSON.stringify({ error: 'enabled must be a boolean value' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Update or insert store_sync_config
+      const { data: existingConfig } = await supabase
+        .from('store_sync_config')
+        .select('id')
+        .eq('store_id', storeId)
+        .single()
+
+      if (existingConfig) {
+        // Update existing config
+        const { error: updateError } = await supabase
+          .from('store_sync_config')
+          .update({ enabled, updated_at: new Date().toISOString() })
+          .eq('store_id', storeId)
+
+        if (updateError) {
+          console.error('Error updating store sync config:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update sync configuration' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+      } else {
+        // Create new config
+        const { error: insertError } = await supabase
+          .from('store_sync_config')
+          .insert({
+            store_id: storeId,
+            enabled,
+            sync_frequency: 'hourly'
+          })
+
+        if (insertError) {
+          console.error('Error creating store sync config:', insertError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to create sync configuration' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: `Background sync ${enabled ? 'enabled' : 'disabled'} successfully`,
+          enabled
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     // PUT /api/stores/:id/ai-config - Update AI configuration for a store
     if (path.match(/^stores\/[^\/]+\/ai-config$/) && req.method === 'PUT') {
       const storeId = path.split('/')[1]

+ 64 - 9
supabase/functions/shoprenter-scheduled-sync/index.ts

@@ -47,10 +47,25 @@ serve(async (req) => {
     const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
     const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
 
-    // Get all active ShopRenter stores that need syncing
+    // Get all ShopRenter stores with sync config
     const { data: stores, error: storesError } = await supabaseAdmin
       .from('stores')
-      .select('id, user_id, store_name, store_url, alt_data')
+      .select(`
+        id,
+        user_id,
+        store_name,
+        store_url,
+        alt_data,
+        store_sync_config (
+          enabled,
+          sync_frequency,
+          sync_products,
+          sync_orders,
+          sync_customers,
+          last_sync_at,
+          next_sync_at
+        )
+      `)
       .eq('platform_name', 'shoprenter')
 
     if (storesError) {
@@ -61,25 +76,44 @@ serve(async (req) => {
       )
     }
 
-    if (!stores || stores.length === 0) {
-      console.log('[ShopRenter Scheduled Sync] No ShopRenter stores found')
+    // Filter stores that have sync enabled and are due for sync
+    const now = new Date()
+    const storesToSync = stores?.filter(store => {
+      const config = store.store_sync_config?.[0]
+      if (!config || !config.enabled) {
+        return false
+      }
+
+      // Check if sync is due
+      if (config.next_sync_at) {
+        const nextSync = new Date(config.next_sync_at)
+        return nextSync <= now
+      }
+
+      // If no next_sync_at, sync is due
+      return true
+    }) || []
+
+    if (storesToSync.length === 0) {
+      console.log('[ShopRenter Scheduled Sync] No ShopRenter stores due for sync')
       return new Response(
         JSON.stringify({
           success: true,
-          message: 'No stores to sync',
+          message: 'No stores due for sync',
           stores_processed: 0
         }),
         { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       )
     }
 
-    console.log(`[ShopRenter Scheduled Sync] Found ${stores.length} stores to sync`)
+    console.log(`[ShopRenter Scheduled Sync] Found ${storesToSync.length} stores due for sync`)
 
     const syncResults = []
 
     // Sync each store
-    for (const store of stores) {
+    for (const store of storesToSync) {
       const storeId = store.id
+      const config = store.store_sync_config?.[0]
       console.log(`[ShopRenter Scheduled Sync] Starting sync for store ${storeId} (${store.store_name})`)
 
       // Detect country code from store URL for phone formatting
@@ -307,6 +341,27 @@ serve(async (req) => {
           .update({ alt_data: updatedAltData })
           .eq('id', storeId)
 
+        // Update store_sync_config timestamps
+        if (config) {
+          await supabaseAdmin
+            .from('store_sync_config')
+            .update({
+              last_sync_at: new Date().toISOString(),
+              // next_sync_at will be auto-calculated by trigger
+            })
+            .eq('store_id', storeId)
+        } else {
+          // Create sync config if it doesn't exist
+          await supabaseAdmin
+            .from('store_sync_config')
+            .insert({
+              store_id: storeId,
+              enabled: true,
+              sync_frequency: 'hourly',
+              last_sync_at: new Date().toISOString()
+            })
+        }
+
         syncStats.completed_at = new Date().toISOString()
         console.log(`[ShopRenter Scheduled Sync] Store ${storeId} sync completed with status: ${syncStats.status}`)
 
@@ -327,7 +382,7 @@ serve(async (req) => {
         .insert({
           sync_type: 'scheduled',
           platform: 'shoprenter',
-          stores_processed: stores.length,
+          stores_processed: storesToSync.length,
           results: syncResults,
           started_at: syncResults[0]?.started_at || new Date().toISOString(),
           completed_at: new Date().toISOString()
@@ -348,7 +403,7 @@ serve(async (req) => {
         success: true,
         message: 'Scheduled sync completed',
         summary: {
-          stores_processed: stores.length,
+          stores_processed: storesToSync.length,
           success: successCount,
           partial: partialCount,
           failed: failedCount