Browse Source

feat: product exclusion refactoring with category support #111

- Added excluded_categories table for category-level exclusions
- Added 'excluded' column to all *_products_cache tables
- Normalized shoprenter_products_cache (extracted fields from JSONB)
- Created shopify_products_cache table (was missing)
- Migrated data from store_data_exclusions to cache tables
- Dropped store_data_exclusions table
- Created helper functions and views for computed exclusion status
- Updated API endpoints to use new structure
- Added category exclusion endpoints (GET /categories, PUT /categories/:id/exclude)
- Products can now be excluded individually OR by category
- Multiple category support per product
- Responses include exclusion_reason (individual vs. category)
Claude 4 months ago
parent
commit
be39f4d440

+ 44 - 44
shopcall.ai-main/src/components/APIKeysContent.tsx

@@ -1,4 +1,5 @@
 import { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
@@ -21,6 +22,7 @@ interface APIKey {
 }
 
 export function APIKeysContent() {
+  const { t } = useTranslation();
   const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
   const [loading, setLoading] = useState(true);
   const [showCreateDialog, setShowCreateDialog] = useState(false);
@@ -55,8 +57,8 @@ export function APIKeysContent() {
     } catch (error) {
       console.error('Error fetching API keys:', error);
       toast({
-        title: "Error",
-        description: "Failed to load API keys. Please try again.",
+        title: t('apiKeys.toast.errorLoadTitle'),
+        description: t('apiKeys.toast.errorLoadDescription'),
         variant: "destructive"
       });
     } finally {
@@ -71,8 +73,8 @@ export function APIKeysContent() {
   const handleCreateKey = async () => {
     if (!newKeyName.trim()) {
       toast({
-        title: "Error",
-        description: "Please enter a key name.",
+        title: t('apiKeys.toast.errorNameRequired'),
+        description: t('apiKeys.toast.errorNameRequiredDescription'),
         variant: "destructive"
       });
       return;
@@ -114,14 +116,14 @@ export function APIKeysContent() {
       fetchAPIKeys();
 
       toast({
-        title: "Success",
-        description: "API key created successfully!",
+        title: t('apiKeys.toast.successCreatedTitle'),
+        description: t('apiKeys.toast.successCreatedDescription'),
       });
     } catch (error) {
       console.error('Error creating API key:', error);
       toast({
-        title: "Error",
-        description: error instanceof Error ? error.message : "Failed to create API key. Please try again.",
+        title: t('apiKeys.toast.errorCreateTitle'),
+        description: error instanceof Error ? error.message : t('apiKeys.toast.errorCreateDescription'),
         variant: "destructive"
       });
     } finally {
@@ -130,7 +132,7 @@ export function APIKeysContent() {
   };
 
   const handleRevokeKey = async (keyId: string, keyName: string) => {
-    if (!confirm(`Are you sure you want to revoke the API key "${keyName}"? This action cannot be undone.`)) {
+    if (!confirm(t('apiKeys.revokeConfirm', { keyName }))) {
       return;
     }
 
@@ -155,8 +157,8 @@ export function APIKeysContent() {
       }
 
       toast({
-        title: "Success",
-        description: "API key revoked successfully.",
+        title: t('apiKeys.toast.successRevokedTitle'),
+        description: t('apiKeys.toast.successRevokedDescription'),
       });
 
       // Refresh the list
@@ -164,8 +166,8 @@ export function APIKeysContent() {
     } catch (error) {
       console.error('Error revoking API key:', error);
       toast({
-        title: "Error",
-        description: "Failed to revoke API key. Please try again.",
+        title: t('apiKeys.toast.errorRevokeTitle'),
+        description: t('apiKeys.toast.errorRevokeDescription'),
         variant: "destructive"
       });
     }
@@ -174,13 +176,13 @@ export function APIKeysContent() {
   const copyToClipboard = (text: string) => {
     navigator.clipboard.writeText(text);
     toast({
-      title: "Copied!",
-      description: "API key copied to clipboard.",
+      title: t('apiKeys.toast.copiedTitle'),
+      description: t('apiKeys.toast.copiedDescription'),
     });
   };
 
   const formatDate = (dateString: string | null) => {
-    if (!dateString) return 'Never';
+    if (!dateString) return t('apiKeys.metadata.never');
     return new Date(dateString).toLocaleString();
   };
 
@@ -201,22 +203,21 @@ export function APIKeysContent() {
     <div className="p-6 space-y-6">
       <div className="flex items-center justify-between">
         <div>
-          <h1 className="text-3xl font-bold">API Keys</h1>
+          <h1 className="text-3xl font-bold">{t('apiKeys.title')}</h1>
           <p className="text-muted-foreground mt-2">
-            Manage your API keys for accessing webshop data
+            {t('apiKeys.subtitle')}
           </p>
         </div>
         <Button onClick={() => setShowCreateDialog(true)} disabled={apiKeys.length >= 10}>
           <Plus className="h-4 w-4 mr-2" />
-          Create API Key
+          {t('apiKeys.createButton')}
         </Button>
       </div>
 
       <Alert>
         <AlertCircle className="h-4 w-4" />
         <AlertDescription>
-          <strong>Security Notice:</strong> API keys provide access to your webshop data via the Webshop Data API.
-          Keep them secure and never share them publicly. You can create up to 10 API keys per account.
+          <strong>{t('apiKeys.securityNotice.title')}</strong> {t('apiKeys.securityNotice.description')}
         </AlertDescription>
       </Alert>
 
@@ -225,13 +226,13 @@ export function APIKeysContent() {
           <Card>
             <CardContent className="flex flex-col items-center justify-center py-12">
               <Key className="h-12 w-12 text-muted-foreground mb-4" />
-              <p className="text-lg font-medium">No API keys yet</p>
+              <p className="text-lg font-medium">{t('apiKeys.empty.title')}</p>
               <p className="text-sm text-muted-foreground mb-4">
-                Create your first API key to start accessing webshop data
+                {t('apiKeys.empty.description')}
               </p>
               <Button onClick={() => setShowCreateDialog(true)}>
                 <Plus className="h-4 w-4 mr-2" />
-                Create API Key
+                {t('apiKeys.empty.createButton')}
               </Button>
             </CardContent>
           </Card>
@@ -245,20 +246,20 @@ export function APIKeysContent() {
                       <Key className="h-5 w-5" />
                       {key.key_name}
                       {!key.is_active && (
-                        <Badge variant="destructive">Revoked</Badge>
+                        <Badge variant="destructive">{t('apiKeys.status.revoked')}</Badge>
                       )}
                       {key.is_active && isExpired(key.expires_at) && (
-                        <Badge variant="destructive">Expired</Badge>
+                        <Badge variant="destructive">{t('apiKeys.status.expired')}</Badge>
                       )}
                       {key.is_active && !isExpired(key.expires_at) && (
-                        <Badge variant="default">Active</Badge>
+                        <Badge variant="default">{t('apiKeys.status.active')}</Badge>
                       )}
                     </CardTitle>
                     <CardDescription className="mt-2">
                       <div className="space-y-1">
-                        <div>Created: {formatDate(key.created_at)}</div>
-                        <div>Last used: {formatDate(key.last_used_at)}</div>
-                        <div>Expires: {key.expires_at ? formatDate(key.expires_at) : 'Never'}</div>
+                        <div>{t('apiKeys.metadata.created')}: {formatDate(key.created_at)}</div>
+                        <div>{t('apiKeys.metadata.lastUsed')}: {formatDate(key.last_used_at)}</div>
+                        <div>{t('apiKeys.metadata.expires')}: {key.expires_at ? formatDate(key.expires_at) : t('apiKeys.metadata.never')}</div>
                       </div>
                     </CardDescription>
                   </div>
@@ -270,7 +271,7 @@ export function APIKeysContent() {
                         onClick={() => handleRevokeKey(key.id, key.key_name)}
                       >
                         <Trash2 className="h-4 w-4 mr-2" />
-                        Revoke
+                        {t('apiKeys.actions.revoke')}
                       </Button>
                     )}
                   </div>
@@ -300,23 +301,23 @@ export function APIKeysContent() {
       <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>Create New API Key</DialogTitle>
+            <DialogTitle>{t('apiKeys.createDialog.title')}</DialogTitle>
             <DialogDescription>
-              Give your API key a descriptive name and set an expiration period.
+              {t('apiKeys.createDialog.description')}
             </DialogDescription>
           </DialogHeader>
           <div className="space-y-4 py-4">
             <div className="space-y-2">
-              <Label htmlFor="keyName">Key Name</Label>
+              <Label htmlFor="keyName">{t('apiKeys.createDialog.keyNameLabel')}</Label>
               <Input
                 id="keyName"
-                placeholder="e.g., Production API Key"
+                placeholder={t('apiKeys.createDialog.keyNamePlaceholder')}
                 value={newKeyName}
                 onChange={(e) => setNewKeyName(e.target.value)}
               />
             </div>
             <div className="space-y-2">
-              <Label htmlFor="expireDays">Expires In (days)</Label>
+              <Label htmlFor="expireDays">{t('apiKeys.createDialog.expireDaysLabel')}</Label>
               <Input
                 id="expireDays"
                 type="number"
@@ -326,17 +327,17 @@ export function APIKeysContent() {
                 onChange={(e) => setNewKeyExpireDays(e.target.value)}
               />
               <p className="text-sm text-muted-foreground">
-                Default: 365 days (1 year)
+                {t('apiKeys.createDialog.expireDaysHint')}
               </p>
             </div>
           </div>
           <DialogFooter>
             <Button variant="outline" onClick={() => setShowCreateDialog(false)}>
-              Cancel
+              {t('apiKeys.createDialog.cancelButton')}
             </Button>
             <Button onClick={handleCreateKey} disabled={creating}>
               {creating && <RefreshCw className="h-4 w-4 mr-2 animate-spin" />}
-              Create Key
+              {creating ? t('apiKeys.createDialog.creating') : t('apiKeys.createDialog.createButton')}
             </Button>
           </DialogFooter>
         </DialogContent>
@@ -346,20 +347,19 @@ export function APIKeysContent() {
       <Dialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>API Key Created!</DialogTitle>
+            <DialogTitle>{t('apiKeys.createdDialog.title')}</DialogTitle>
             <DialogDescription>
               <Alert className="mt-4" variant="destructive">
                 <AlertCircle className="h-4 w-4" />
                 <AlertDescription>
-                  <strong>Important:</strong> This is the only time you'll see the full API key.
-                  Copy it now and store it securely.
+                  <strong>{t('apiKeys.createdDialog.warning.title')}</strong> {t('apiKeys.createdDialog.warning.description')}
                 </AlertDescription>
               </Alert>
             </DialogDescription>
           </DialogHeader>
           <div className="space-y-4 py-4">
             <div className="space-y-2">
-              <Label>Your API Key</Label>
+              <Label>{t('apiKeys.createdDialog.keyLabel')}</Label>
               <div className="flex items-center gap-2 font-mono text-sm bg-muted p-3 rounded break-all">
                 <code className="flex-1">{createdKey}</code>
                 <Button
@@ -377,7 +377,7 @@ export function APIKeysContent() {
               setShowKeyDialog(false);
               setCreatedKey(null);
             }}>
-              I've Saved My Key
+              {t('apiKeys.createdDialog.confirmButton')}
             </Button>
           </DialogFooter>
         </DialogContent>

+ 30 - 28
shopcall.ai-main/src/components/ManageStoreDataContent.tsx

@@ -23,6 +23,7 @@ import {
 import { Loader2, Search, Package, ChevronLeft, ChevronRight } from "lucide-react";
 import { API_URL } from "@/lib/config";
 import { useToast } from "@/hooks/use-toast";
+import { useTranslation } from "react-i18next";
 
 interface StoreData {
   id: string;
@@ -53,6 +54,7 @@ interface DataResponse {
 export function ManageStoreDataContent() {
   const [searchParams] = useSearchParams();
   const { toast } = useToast();
+  const { t } = useTranslation();
 
   const [stores, setStores] = useState<StoreData[]>([]);
   const [selectedStore, setSelectedStore] = useState<StoreData | null>(null);
@@ -134,8 +136,8 @@ export function ManageStoreDataContent() {
       } catch (error) {
         console.error('Error fetching stores:', error);
         toast({
-          title: "Error",
-          description: "Failed to load stores. Please try again.",
+          title: t('manageStoreData.toast.errorLoadStoresTitle'),
+          description: t('manageStoreData.toast.errorLoadStoresDescription'),
           variant: "destructive"
         });
       } finally {
@@ -202,8 +204,8 @@ export function ManageStoreDataContent() {
     } catch (error) {
       console.error('Error fetching data:', error);
       toast({
-        title: "Error",
-        description: `Failed to load ${activeTab}. Please try again.`,
+        title: t('manageStoreData.toast.errorLoadDataTitle'),
+        description: t('manageStoreData.toast.errorLoadDataDescription', { type: activeTab }),
         variant: "destructive"
       });
     } finally {
@@ -265,14 +267,14 @@ export function ManageStoreDataContent() {
       }
 
       toast({
-        title: "Success",
-        description: `Item ${!currentEnabled ? 'enabled' : 'disabled'} successfully.`,
+        title: t('manageStoreData.toast.successToggleTitle'),
+        description: !currentEnabled ? t('manageStoreData.toast.successToggleEnabled') : t('manageStoreData.toast.successToggleDisabled'),
       });
     } catch (error) {
       console.error('Error toggling item:', error);
       toast({
-        title: "Error",
-        description: "Failed to update item. Please try again.",
+        title: t('manageStoreData.toast.errorToggleTitle'),
+        description: t('manageStoreData.toast.errorToggleDescription'),
         variant: "destructive"
       });
     }
@@ -307,8 +309,8 @@ export function ManageStoreDataContent() {
       }
 
       toast({
-        title: "Success",
-        description: `${selectedItems.size} items ${enable ? 'enabled' : 'disabled'} successfully.`,
+        title: t('manageStoreData.toast.successBulkTitle'),
+        description: enable ? t('manageStoreData.toast.successBulkEnabled', { count: selectedItems.size }) : t('manageStoreData.toast.successBulkDisabled', { count: selectedItems.size }),
       });
 
       setSelectedItems(new Set());
@@ -317,8 +319,8 @@ export function ManageStoreDataContent() {
     } catch (error) {
       console.error('Error with bulk action:', error);
       toast({
-        title: "Error",
-        description: "Failed to update items. Please try again.",
+        title: t('manageStoreData.toast.errorBulkTitle'),
+        description: t('manageStoreData.toast.errorBulkDescription'),
         variant: "destructive"
       });
     }
@@ -351,16 +353,16 @@ export function ManageStoreDataContent() {
       }
 
       toast({
-        title: "Success",
-        description: `All ${activeTab} enabled successfully.`,
+        title: t('manageStoreData.toast.successEnableAllTitle'),
+        description: t('manageStoreData.toast.successEnableAllDescription', { type: activeTab }),
       });
 
       fetchData();
     } catch (error) {
       console.error('Error enabling all:', error);
       toast({
-        title: "Error",
-        description: "Failed to enable all items. Please try again.",
+        title: t('manageStoreData.toast.errorEnableAllTitle'),
+        description: t('manageStoreData.toast.errorEnableAllDescription'),
         variant: "destructive"
       });
     }
@@ -393,16 +395,16 @@ export function ManageStoreDataContent() {
       }
 
       toast({
-        title: "Success",
-        description: `All ${activeTab} disabled successfully.`,
+        title: t('manageStoreData.toast.successDisableAllTitle'),
+        description: t('manageStoreData.toast.successDisableAllDescription', { type: activeTab }),
       });
 
       fetchData();
     } catch (error) {
       console.error('Error disabling all:', error);
       toast({
-        title: "Error",
-        description: "Failed to disable all items. Please try again.",
+        title: t('manageStoreData.toast.errorDisableAllTitle'),
+        description: t('manageStoreData.toast.errorDisableAllDescription'),
         variant: "destructive"
       });
     }
@@ -465,15 +467,15 @@ export function ManageStoreDataContent() {
         <div className="flex items-center justify-center min-h-[60vh]">
           <div className="text-center">
             <Package className="w-16 h-16 text-slate-600 mx-auto mb-4" />
-            <h3 className="text-lg font-semibold text-white mb-2">No Stores Connected</h3>
+            <h3 className="text-lg font-semibold text-white mb-2">{t('manageStoreData.noStores.title')}</h3>
             <p className="text-slate-400 mb-6">
-              Please connect a store first before managing data.
+              {t('manageStoreData.noStores.description')}
             </p>
             <Button
               className="bg-cyan-500 hover:bg-cyan-600 text-white"
               onClick={() => window.location.href = '/webshops'}
             >
-              Go to Webshops
+              {t('manageStoreData.noStores.button')}
             </Button>
           </div>
         </div>
@@ -487,8 +489,8 @@ export function ManageStoreDataContent() {
     <div className="flex-1 space-y-6 p-8 bg-slate-900">
       <div className="flex items-center justify-between">
         <div>
-          <h2 className="text-3xl font-bold tracking-tight text-white">Manage Store Data</h2>
-          <p className="text-slate-400">Control which data is included in AI context</p>
+          <h2 className="text-3xl font-bold tracking-tight text-white">{t('manageStoreData.title')}</h2>
+          <p className="text-slate-400">{t('manageStoreData.subtitle')}</p>
         </div>
         {stores.length > 1 && (
           <Select value={selectedStore?.id} onValueChange={(value) => {
@@ -502,7 +504,7 @@ export function ManageStoreDataContent() {
               {stores.map((store) => (
                 <SelectItem key={store.id} value={store.id}>
                   <div className="flex items-center gap-2">
-                    <span className="text-white">{store.store_name || 'Unnamed Store'}</span>
+                    <span className="text-white">{store.store_name || t('manageStoreData.storeSelector.unnamedStore')}</span>
                     <span className="text-slate-400 text-sm capitalize">({store.platform_name})</span>
                   </div>
                 </SelectItem>
@@ -514,7 +516,7 @@ export function ManageStoreDataContent() {
 
       <Card className="bg-slate-800 border-slate-700">
         <CardHeader>
-          <CardTitle className="text-white">Data Management</CardTitle>
+          <CardTitle className="text-white">{t('manageStoreData.tabs.dataManagement')}</CardTitle>
         </CardHeader>
         <CardContent>
           <Tabs value={activeTab} onValueChange={(value) => {
@@ -526,7 +528,7 @@ export function ManageStoreDataContent() {
             <TabsList className="bg-slate-700 mb-6">
               <TabsTrigger value="products" className="data-[state=active]:bg-slate-600 data-[state=active]:text-white">
                 <Package className="w-4 h-4 mr-2" />
-                Products ({enabledCount}/{totalCount})
+                {t('manageStoreData.tabs.products')} ({enabledCount}/{totalCount})
               </TabsTrigger>
             </TabsList>
 

+ 218 - 1
shopcall.ai-main/src/i18n/locales/en.json

@@ -704,7 +704,79 @@
     },
     "dataAccessSettings": {
       "title": "Data Access Permissions",
-      "description": "Control which data types can be accessed via API for {{storeName}}"
+      "description": "Control which data types can be accessed via API for {{storeName}}",
+      "securityLevel": {
+        "maximumPrivacy": "Maximum Privacy",
+        "highPrivacy": "High Privacy",
+        "mediumPrivacy": "Medium Privacy",
+        "balanced": "Balanced",
+        "fullSync": "Full Sync"
+      },
+      "gdprNotice": {
+        "title": "GDPR Compliance:",
+        "description": "Control how data is accessed and stored. \"API Only\" mode ensures no personal data is cached locally. \"Sync & Cache\" provides faster access but stores data in our database."
+      },
+      "gdprProtection": {
+        "title": "GDPR Protection Active:",
+        "customerData": "customer data",
+        "orderData": "order data",
+        "and": "and",
+        "description": "is accessed directly from your store without local caching, ensuring maximum privacy compliance."
+      },
+      "productData": {
+        "title": "Product Data",
+        "badgePublic": "Public Data",
+        "badgeRequired": "Required",
+        "description": "Product information (names, prices, descriptions, stock levels). This setting is managed by the system and cannot be changed."
+      },
+      "customerData": {
+        "title": "Customer Data",
+        "badgePII": "Personal Data (PII)",
+        "description": "Customer information (names, emails, addresses, purchase history)"
+      },
+      "orderData": {
+        "title": "Order Data",
+        "badgePII": "Personal Data (PII)",
+        "description": "Order information (order details, amounts, customer info, shipping addresses)"
+      },
+      "accessModes": {
+        "syncCache": {
+          "title": "Sync & Cache",
+          "badge": "Fastest",
+          "description": "Store data locally in database and Qdrant vector search for instant access"
+        },
+        "apiOnly": {
+          "title": "API Access Only",
+          "badge": "GDPR Friendly",
+          "description": "Fetch data directly from your store's API on demand (no local storage)"
+        },
+        "noAccess": {
+          "title": "No Access",
+          "badge": "Blocked",
+          "description": "Completely block access to this data type"
+        }
+      },
+      "actions": {
+        "cancel": "Cancel",
+        "saveChanges": "Save Changes",
+        "saving": "Saving..."
+      },
+      "alerts": {
+        "maximumPrivacy": {
+          "title": "Maximum Privacy:",
+          "description": "All data access is blocked. API tools will not be able to access any store data."
+        },
+        "apiOnlyMode": {
+          "title": "API-Only Mode Active:",
+          "description": "Selected data types will be fetched directly from your store on demand. This is slower but ensures no personal data is cached locally, making it GDPR-friendly."
+        }
+      },
+      "toast": {
+        "successTitle": "Access Policies Updated",
+        "successDescription": "Data access policies have been successfully updated.",
+        "errorTitle": "Update Failed",
+        "errorDescription": "Failed to update access policies. Please try again."
+      }
     },
     "oauth": {
       "woocommerceConnected": "WooCommerce Connected!",
@@ -1015,5 +1087,150 @@
       "about": "About Us",
       "privacy": "Privacy Policy"
     }
+  },
+  "apiKeys": {
+    "title": "API Keys",
+    "subtitle": "Manage your API keys for accessing webshop data",
+    "createButton": "Create API Key",
+    "loading": "Loading API keys...",
+    "securityNotice": {
+      "title": "Security Notice:",
+      "description": "API keys provide access to your webshop data via the Webshop Data API. Keep them secure and never share them publicly. You can create up to 10 API keys per account."
+    },
+    "empty": {
+      "title": "No API keys yet",
+      "description": "Create your first API key to start accessing webshop data",
+      "createButton": "Create API Key"
+    },
+    "status": {
+      "active": "Active",
+      "revoked": "Revoked",
+      "expired": "Expired"
+    },
+    "metadata": {
+      "created": "Created",
+      "lastUsed": "Last used",
+      "expires": "Expires",
+      "never": "Never"
+    },
+    "actions": {
+      "revoke": "Revoke",
+      "copy": "Copy"
+    },
+    "createDialog": {
+      "title": "Create New API Key",
+      "description": "Give your API key a descriptive name and set an expiration period.",
+      "keyNameLabel": "Key Name",
+      "keyNamePlaceholder": "e.g., Production API Key",
+      "expireDaysLabel": "Expires In (days)",
+      "expireDaysHint": "Default: 365 days (1 year)",
+      "cancelButton": "Cancel",
+      "createButton": "Create Key",
+      "creating": "Creating..."
+    },
+    "createdDialog": {
+      "title": "API Key Created!",
+      "warning": {
+        "title": "Important:",
+        "description": "This is the only time you'll see the full API key. Copy it now and store it securely."
+      },
+      "keyLabel": "Your API Key",
+      "confirmButton": "I've Saved My Key"
+    },
+    "revokeConfirm": "Are you sure you want to revoke the API key \"{{keyName}}\"? This action cannot be undone.",
+    "toast": {
+      "errorLoadTitle": "Error",
+      "errorLoadDescription": "Failed to load API keys. Please try again.",
+      "errorNameRequired": "Error",
+      "errorNameRequiredDescription": "Please enter a key name.",
+      "successCreatedTitle": "Success",
+      "successCreatedDescription": "API key created successfully!",
+      "errorCreateTitle": "Error",
+      "errorCreateDescription": "Failed to create API key. Please try again.",
+      "successRevokedTitle": "Success",
+      "successRevokedDescription": "API key revoked successfully.",
+      "errorRevokeTitle": "Error",
+      "errorRevokeDescription": "Failed to revoke API key. Please try again.",
+      "copiedTitle": "Copied!",
+      "copiedDescription": "API key copied to clipboard."
+    }
+  },
+  "manageStoreData": {
+    "title": "Manage Store Data",
+    "subtitle": "Control which data is included in AI context",
+    "loading": "Loading stores...",
+    "noStores": {
+      "title": "No Stores Connected",
+      "description": "Please connect a store first before managing data.",
+      "button": "Go to Webshops"
+    },
+    "storeSelector": {
+      "unnamedStore": "Unnamed Store"
+    },
+    "tabs": {
+      "products": "Products",
+      "dataManagement": "Data Management"
+    },
+    "search": {
+      "placeholder": "Search {{type}}...",
+      "products": "products"
+    },
+    "filters": {
+      "all": "All Items",
+      "enabled": "Enabled Only",
+      "disabled": "Disabled Only"
+    },
+    "bulkActions": {
+      "selectedCount": "{{count}} items selected",
+      "enableSelected": "Enable Selected",
+      "disableSelected": "Disable Selected",
+      "enableAll": "Enable All",
+      "disableAll": "Disable All"
+    },
+    "table": {
+      "name": "Name",
+      "sku": "SKU",
+      "price": "Price",
+      "enabled": "Enabled",
+      "noProducts": "No products found"
+    },
+    "pagination": {
+      "itemsPerPage": "Items per page:",
+      "page": "Page",
+      "of": "of",
+      "total": "{{count}} total"
+    },
+    "confirmDialog": {
+      "enableAllTitle": "Enable All {{type}}",
+      "enableAllDescription": "Are you sure you want to enable all {{type}} for AI context?",
+      "disableAllTitle": "Disable All {{type}}",
+      "disableAllDescription": "Are you sure you want to disable all {{type}} from AI context?",
+      "cancel": "Cancel",
+      "confirm": "Confirm"
+    },
+    "toast": {
+      "errorLoadStoresTitle": "Error",
+      "errorLoadStoresDescription": "Failed to load stores. Please try again.",
+      "errorLoadDataTitle": "Error",
+      "errorLoadDataDescription": "Failed to load {{type}}. Please try again.",
+      "successToggleTitle": "Success",
+      "successToggleEnabled": "Item enabled successfully.",
+      "successToggleDisabled": "Item disabled successfully.",
+      "errorToggleTitle": "Error",
+      "errorToggleDescription": "Failed to update item. Please try again.",
+      "successBulkTitle": "Success",
+      "successBulkEnabled": "{{count}} items enabled successfully.",
+      "successBulkDisabled": "{{count}} items disabled successfully.",
+      "errorBulkTitle": "Error",
+      "errorBulkDescription": "Failed to update items. Please try again.",
+      "successEnableAllTitle": "Success",
+      "successEnableAllDescription": "All {{type}} enabled successfully.",
+      "errorEnableAllTitle": "Error",
+      "errorEnableAllDescription": "Failed to enable all items. Please try again.",
+      "successDisableAllTitle": "Success",
+      "successDisableAllDescription": "All {{type}} disabled successfully.",
+      "errorDisableAllTitle": "Error",
+      "errorDisableAllDescription": "Failed to disable all items. Please try again."
+    }
   }
 }

+ 218 - 1
shopcall.ai-main/src/i18n/locales/hu.json

@@ -694,7 +694,79 @@
     },
     "dataAccessSettings": {
       "title": "Adathozzáférési Engedélyek",
-      "description": "API-n keresztül elérhető adattípusok kezelése: {{storeName}}"
+      "description": "API-n keresztül elérhető adattípusok kezelése: {{storeName}}",
+      "securityLevel": {
+        "maximumPrivacy": "Maximális Adatvédelem",
+        "highPrivacy": "Magas Adatvédelem",
+        "mediumPrivacy": "Közepes Adatvédelem",
+        "balanced": "Kiegyensúlyozott",
+        "fullSync": "Teljes Szinkronizálás"
+      },
+      "gdprNotice": {
+        "title": "GDPR Megfelelés:",
+        "description": "Szabályozza, hogyan történik az adatok elérése és tárolása. Az \"Csak API\" mód biztosítja, hogy személyes adatok ne kerüljenek helyi tárolásra. A \"Szinkronizálás és Gyorsítótár\" gyorsabb hozzáférést biztosít, de az adatbázisunkban tárolja az adatokat."
+      },
+      "gdprProtection": {
+        "title": "GDPR Védelem Aktív:",
+        "customerData": "ügyfél adatok",
+        "orderData": "rendelési adatok",
+        "and": "és",
+        "description": "közvetlenül az áruházából kerül lekérésre helyi gyorsítótárazás nélkül, biztosítva a maximális adatvédelmi megfelelést."
+      },
+      "productData": {
+        "title": "Termék Adatok",
+        "badgePublic": "Nyilvános Adat",
+        "badgeRequired": "Kötelező",
+        "description": "Termékinformációk (nevek, árak, leírások, készletszintek). Ez a beállítás a rendszer által kezelt és nem módosítható."
+      },
+      "customerData": {
+        "title": "Ügyfél Adatok",
+        "badgePII": "Személyes Adat (PII)",
+        "description": "Ügyfél információk (nevek, email címek, címek, vásárlási előzmények)"
+      },
+      "orderData": {
+        "title": "Rendelés Adatok",
+        "badgePII": "Személyes Adat (PII)",
+        "description": "Rendelés információk (rendelési részletek, összegek, ügyféladatok, szállítási címek)"
+      },
+      "accessModes": {
+        "syncCache": {
+          "title": "Szinkronizálás és Gyorsítótár",
+          "badge": "Leggyorsabb",
+          "description": "Adatok helyi tárolása adatbázisban és Qdrant vektorkereséssel azonnali hozzáféréshez"
+        },
+        "apiOnly": {
+          "title": "Csak API Hozzáférés",
+          "badge": "GDPR Barát",
+          "description": "Adatok közvetlen lekérése az áruház API-ján keresztül igény szerint (nincs helyi tárolás)"
+        },
+        "noAccess": {
+          "title": "Nincs Hozzáférés",
+          "badge": "Letiltva",
+          "description": "Hozzáférés teljes letiltása ehhez az adattípushoz"
+        }
+      },
+      "actions": {
+        "cancel": "Mégse",
+        "saveChanges": "Változtatások Mentése",
+        "saving": "Mentés..."
+      },
+      "alerts": {
+        "maximumPrivacy": {
+          "title": "Maximális Adatvédelem:",
+          "description": "Minden adathozzáférés letiltva. Az API eszközök nem fognak tudni hozzáférni semmilyen áruházi adathoz."
+        },
+        "apiOnlyMode": {
+          "title": "Csak API Mód Aktív:",
+          "description": "A kiválasztott adattípusok közvetlenül az áruházából kerülnek lekérésre igény szerint. Ez lassabb, de biztosítja, hogy személyes adatok ne kerüljenek helyi gyorsítótárazásra, GDPR-baráttá téve azt."
+        }
+      },
+      "toast": {
+        "successTitle": "Hozzáférési Szabályzatok Frissítve",
+        "successDescription": "Az adathozzáférési szabályzatok sikeresen frissítve.",
+        "errorTitle": "Frissítés Sikertelen",
+        "errorDescription": "Hozzáférési szabályzatok frissítése sikertelen. Kérjük, próbálja újra."
+      }
     },
     "oauth": {
       "woocommerceConnected": "WooCommerce Csatlakoztatva!",
@@ -1005,5 +1077,150 @@
       "about": "Rólunk",
       "privacy": "Adatvédelmi irányelvek"
     }
+  },
+  "apiKeys": {
+    "title": "API Kulcsok",
+    "subtitle": "API kulcsok kezelése webáruház adatokhoz való hozzáféréshez",
+    "createButton": "API Kulcs Létrehozása",
+    "loading": "API kulcsok betöltése...",
+    "securityNotice": {
+      "title": "Biztonsági Figyelmeztetés:",
+      "description": "Az API kulcsok hozzáférést biztosítanak webáruháza adataihoz a Webáruház Adat API-n keresztül. Tartsa őket biztonságban, és soha ne ossza meg őket nyilvánosan. Fiókonként maximum 10 API kulcsot hozhat létre."
+    },
+    "empty": {
+      "title": "Még nincsenek API kulcsok",
+      "description": "Hozza létre első API kulcsát a webáruház adatok elérésének megkezdéséhez",
+      "createButton": "API Kulcs Létrehozása"
+    },
+    "status": {
+      "active": "Aktív",
+      "revoked": "Visszavonva",
+      "expired": "Lejárt"
+    },
+    "metadata": {
+      "created": "Létrehozva",
+      "lastUsed": "Utoljára használva",
+      "expires": "Lejár",
+      "never": "Soha"
+    },
+    "actions": {
+      "revoke": "Visszavonás",
+      "copy": "Másolás"
+    },
+    "createDialog": {
+      "title": "Új API Kulcs Létrehozása",
+      "description": "Adjon leíró nevet API kulcsának és állítsa be a lejárati időt.",
+      "keyNameLabel": "Kulcs Neve",
+      "keyNamePlaceholder": "pl. Éles API Kulcs",
+      "expireDaysLabel": "Lejárat (napokban)",
+      "expireDaysHint": "Alapértelmezett: 365 nap (1 év)",
+      "cancelButton": "Mégse",
+      "createButton": "Kulcs Létrehozása",
+      "creating": "Létrehozás..."
+    },
+    "createdDialog": {
+      "title": "API Kulcs Létrehozva!",
+      "warning": {
+        "title": "Fontos:",
+        "description": "Ez az egyetlen alkalom, amikor láthatja a teljes API kulcsot. Másolja ki most és tárolja biztonságosan."
+      },
+      "keyLabel": "Az Ön API Kulcsa",
+      "confirmButton": "Elmentettem a Kulcsot"
+    },
+    "revokeConfirm": "Biztosan vissza szeretné vonni a(z) \"{{keyName}}\" API kulcsot? Ez a művelet nem vonható vissza.",
+    "toast": {
+      "errorLoadTitle": "Hiba",
+      "errorLoadDescription": "API kulcsok betöltése sikertelen. Kérjük, próbálja újra.",
+      "errorNameRequired": "Hiba",
+      "errorNameRequiredDescription": "Kérjük, adjon meg egy kulcsnevet.",
+      "successCreatedTitle": "Sikeres",
+      "successCreatedDescription": "API kulcs sikeresen létrehozva!",
+      "errorCreateTitle": "Hiba",
+      "errorCreateDescription": "API kulcs létrehozása sikertelen. Kérjük, próbálja újra.",
+      "successRevokedTitle": "Sikeres",
+      "successRevokedDescription": "API kulcs sikeresen visszavonva.",
+      "errorRevokeTitle": "Hiba",
+      "errorRevokeDescription": "API kulcs visszavonása sikertelen. Kérjük, próbálja újra.",
+      "copiedTitle": "Másolva!",
+      "copiedDescription": "API kulcs vágólapra másolva."
+    }
+  },
+  "manageStoreData": {
+    "title": "Áruház Adatok Kezelése",
+    "subtitle": "Határozza meg, mely adatok szerepeljenek az AI kontextusban",
+    "loading": "Áruházak betöltése...",
+    "noStores": {
+      "title": "Nincsenek Csatlakoztatott Áruházak",
+      "description": "Kérjük, először csatlakoztasson egy áruházat az adatok kezelése előtt.",
+      "button": "Ugrás a Webáruházakhoz"
+    },
+    "storeSelector": {
+      "unnamedStore": "Névtelen Áruház"
+    },
+    "tabs": {
+      "products": "Termékek",
+      "dataManagement": "Adatkezelés"
+    },
+    "search": {
+      "placeholder": "{{type}} keresése...",
+      "products": "termékek"
+    },
+    "filters": {
+      "all": "Minden Elem",
+      "enabled": "Csak Engedélyezettek",
+      "disabled": "Csak Letiltottak"
+    },
+    "bulkActions": {
+      "selectedCount": "{{count}} elem kiválasztva",
+      "enableSelected": "Kiválasztottak Engedélyezése",
+      "disableSelected": "Kiválasztottak Letiltása",
+      "enableAll": "Összes Engedélyezése",
+      "disableAll": "Összes Letiltása"
+    },
+    "table": {
+      "name": "Név",
+      "sku": "Cikkszám",
+      "price": "Ár",
+      "enabled": "Engedélyezve",
+      "noProducts": "Nem találhatók termékek"
+    },
+    "pagination": {
+      "itemsPerPage": "Elemek oldalanként:",
+      "page": "Oldal",
+      "of": "/",
+      "total": "összesen {{count}}"
+    },
+    "confirmDialog": {
+      "enableAllTitle": "Összes {{type}} Engedélyezése",
+      "enableAllDescription": "Biztosan engedélyezni szeretné az összes {{type}} elemet az AI kontextusban?",
+      "disableAllTitle": "Összes {{type}} Letiltása",
+      "disableAllDescription": "Biztosan le szeretné tiltani az összes {{type}} elemet az AI kontextusból?",
+      "cancel": "Mégse",
+      "confirm": "Megerősítés"
+    },
+    "toast": {
+      "errorLoadStoresTitle": "Hiba",
+      "errorLoadStoresDescription": "Áruházak betöltése sikertelen. Kérjük, próbálja újra.",
+      "errorLoadDataTitle": "Hiba",
+      "errorLoadDataDescription": "{{type}} betöltése sikertelen. Kérjük, próbálja újra.",
+      "successToggleTitle": "Sikeres",
+      "successToggleEnabled": "Elem sikeresen engedélyezve.",
+      "successToggleDisabled": "Elem sikeresen letiltva.",
+      "errorToggleTitle": "Hiba",
+      "errorToggleDescription": "Elem frissítése sikertelen. Kérjük, próbálja újra.",
+      "successBulkTitle": "Sikeres",
+      "successBulkEnabled": "{{count}} elem sikeresen engedélyezve.",
+      "successBulkDisabled": "{{count}} elem sikeresen letiltva.",
+      "errorBulkTitle": "Hiba",
+      "errorBulkDescription": "Elemek frissítése sikertelen. Kérjük, próbálja újra.",
+      "successEnableAllTitle": "Sikeres",
+      "successEnableAllDescription": "Összes {{type}} sikeresen engedélyezve.",
+      "errorEnableAllTitle": "Hiba",
+      "errorEnableAllDescription": "Összes elem engedélyezése sikertelen. Kérjük, próbálja újra.",
+      "successDisableAllTitle": "Sikeres",
+      "successDisableAllDescription": "Összes {{type}} sikeresen letiltva.",
+      "errorDisableAllTitle": "Hiba",
+      "errorDisableAllDescription": "Összes elem letiltása sikertelen. Kérjük, próbálja újra."
+    }
   }
 }

+ 251 - 245
supabase/functions/api/index.ts

@@ -1090,14 +1090,140 @@ serve(async (req) => {
       }
     }
 
-    // GET /api/store-data/products|orders|customers - Get store data by type
-    if (path.match(/^store-data\/(products|orders|customers)$/) && req.method === 'GET') {
-      const dataType = path.split('/')[1] // products, orders, or customers
+    // =========================================================================
+    // NEW ENDPOINT: GET /api/store-data/categories - Get categories for a store
+    // =========================================================================
+    if (path === 'store-data/categories' && req.method === 'GET') {
+      const storeId = url.searchParams.get('store_id')
+
+      if (!storeId) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      try {
+        // Use the helper function to get categories
+        const { data: categories, error: categoriesError } = await supabase
+          .rpc('get_store_categories', {
+            p_store_id: storeId,
+            p_platform: store.platform_name
+          })
+
+        if (categoriesError) {
+          console.error('Error fetching categories:', categoriesError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch categories' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            categories: categories || []
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error fetching categories:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch categories' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // NEW ENDPOINT: PUT /api/store-data/categories/:categoryId/exclude
+    // =========================================================================
+    if (path.match(/^store-data\/categories\/[^\/]+\/exclude$/) && req.method === 'PUT') {
+      const categoryId = path.split('/')[2]
+      const { store_id, exclude, category_name } = await req.json()
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        // Use the helper function to toggle category exclusion
+        const { error: toggleError } = await supabase.rpc('toggle_category_exclusion', {
+          p_store_id: store_id,
+          p_category_id: categoryId,
+          p_category_name: category_name || categoryId,
+          p_platform: store.platform_name,
+          p_exclude: exclude
+        })
+
+        if (toggleError) {
+          console.error('Error toggling category exclusion:', toggleError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to toggle category exclusion' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Category ${exclude ? 'excluded' : 'included'} successfully`
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error toggling category exclusion:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to toggle category exclusion' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // UPDATED: GET /api/store-data/products - Get products with exclusion info
+    // =========================================================================
+    if (path.match(/^store-data\/(products)$/) && req.method === 'GET') {
+      const dataType = path.split('/')[1]
       const storeId = url.searchParams.get('store_id')
       const page = parseInt(url.searchParams.get('page') || '1')
       const limit = parseInt(url.searchParams.get('limit') || '25')
       const search = url.searchParams.get('search')
       const enabledFilter = url.searchParams.get('enabled')
+      const categoryFilter = url.searchParams.get('category')
 
       if (!storeId) {
         return new Response(
@@ -1122,69 +1248,44 @@ serve(async (req) => {
       }
 
       try {
-        // Map dataType to singular form for table lookup
-        const singularType = dataType === 'products' ? 'product' : dataType === 'orders' ? 'order' : 'customer'
         const platform = store.platform_name
 
-        // Orders and customers are fetched in real-time (GDPR compliance)
-        // Products are fetched from cache
-        let cacheItems: any[] = []
-        let idColumn = ''
-
-        if (dataType === 'products') {
-          // Fetch products from cache
-          const tableName = platform === 'woocommerce'
-            ? 'woocommerce_products_cache'
-            : platform === 'shopify'
-            ? 'shopify_products_cache'
-            : 'shoprenter_products_cache'
-
-          idColumn = platform === 'woocommerce'
-            ? 'wc_product_id'
-            : platform === 'shopify'
-            ? 'shopify_product_id'
-            : 'shoprenter_product_id'
+        // Use the view with computed exclusion status
+        const viewName = platform === 'woocommerce'
+          ? 'woocommerce_products_with_exclusion'
+          : platform === 'shopify'
+          ? 'shopify_products_with_exclusion'
+          : 'shoprenter_products_with_exclusion'
 
-          // Build cache query
-          let cacheQuery = supabase
-            .from(tableName)
-            .select('*')
-            .eq('store_id', storeId)
-            .order('created_at', { ascending: false })
+        // Build query
+        let query = supabase
+          .from(viewName)
+          .select('*')
+          .eq('store_id', storeId)
+          .order('created_at', { ascending: false })
 
-          // Apply search filter
-          if (search) {
-            if (platform === 'shoprenter') {
-              cacheQuery = cacheQuery.or(`product_data->>name.ilike.%${search}%,product_data->>sku.ilike.%${search}%`)
-            } else {
-              cacheQuery = cacheQuery.or(`name.ilike.%${search}%,sku.ilike.%${search}%`)
-            }
-          }
+        // Apply search filter
+        if (search) {
+          query = query.or(`name.ilike.%${search}%,sku.ilike.%${search}%`)
+        }
 
-          const { data, error: cacheError } = await cacheQuery
+        // Apply category filter
+        if (categoryFilter) {
+          // Filter products that have this category
+          query = query.contains('categories', [{ id: categoryFilter }])
+        }
 
-          if (cacheError) {
-            console.error(`Error fetching products from cache:`, cacheError)
-            return new Response(
-              JSON.stringify({ error: 'Failed to fetch products' }),
-              { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-            )
-          }
+        const { data: products, error: productsError } = await query
 
-          cacheItems = data || []
-        } else {
-          // For orders and customers, return message that real-time access is not available via this endpoint
+        if (productsError) {
+          console.error('Error fetching products:', productsError)
           return new Response(
-            JSON.stringify({
-              error: `${dataType} data is no longer cached for GDPR compliance`,
-              hint: `Access ${dataType} in real-time via the shop-data-api Edge Function or through your platform's admin panel`,
-              success: false
-            }),
-            { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+            JSON.stringify({ error: 'Failed to fetch products' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
           )
         }
 
-        if (!cacheItems || cacheItems.length === 0) {
+        if (!products || products.length === 0) {
           return new Response(
             JSON.stringify({
               success: true,
@@ -1197,47 +1298,19 @@ serve(async (req) => {
           )
         }
 
-        // Get all exclusions for this store and type
-        const { data: exclusions } = await supabase
-          .from('store_data_exclusions')
-          .select('data_id, is_enabled')
-          .eq('store_id', storeId)
-          .eq('data_type', singularType)
-
-        // Create exclusion map
-        const exclusionMap = new Map()
-        if (exclusions) {
-          exclusions.forEach(e => {
-            exclusionMap.set(e.data_id, e.is_enabled)
-          })
-        }
-
-        // Transform cache items to result format
-        let results: any[] = []
-        for (const item of cacheItems) {
-          const itemId = item[idColumn]
-          const isEnabled = exclusionMap.has(itemId) ? exclusionMap.get(itemId) : true
-
-          let resultItem: any = {
-            id: itemId,
-            enabled_in_context: isEnabled
-          }
-
-          // Add product-specific fields
-          if (platform === 'shoprenter') {
-            resultItem.name = item.product_data?.name || ''
-            resultItem.sku = item.product_data?.sku || ''
-            resultItem.price = item.product_data?.price || '0'
-            resultItem.currency = item.product_data?.currency || 'HUF'
-          } else {
-            resultItem.name = item.name || item.title || ''
-            resultItem.sku = item.sku || ''
-            resultItem.price = item.price || '0'
-            resultItem.currency = item.currency || 'USD'
-          }
-
-          results.push(resultItem)
-        }
+        // Transform to response format
+        let results = products.map(p => ({
+          id: p[platform === 'woocommerce' ? 'wc_product_id' : platform === 'shopify' ? 'shopify_product_id' : 'shoprenter_product_id'],
+          name: p.name || '',
+          sku: p.sku || '',
+          price: p.price || '0',
+          currency: p.currency || 'USD',
+          categories: p.categories || [],
+          enabled_in_context: !p.effectively_excluded, // Inverted logic: enabled = NOT excluded
+          excluded_by_individual: p.excluded,
+          excluded_by_category: p.exclusion_reason === 'category',
+          exclusion_reason: p.exclusion_reason
+        }))
 
         // Apply enabled filter if specified
         if (enabledFilter !== null) {
@@ -1265,32 +1338,21 @@ serve(async (req) => {
           { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
         )
       } catch (error) {
-        console.error(`Error fetching ${dataType}:`, error)
+        console.error('Error fetching products:', error)
         return new Response(
-          JSON.stringify({ error: `Failed to fetch ${dataType}` }),
+          JSON.stringify({ error: 'Failed to fetch products' }),
           { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
         )
       }
     }
 
-    // PUT /api/store-data/:type/:id/toggle - Toggle individual item (products only)
-    if (path.match(/^store-data\/(products|orders|customers)\/[^\/]+\/toggle$/) && req.method === 'PUT') {
-      const pathParts = path.split('/')
-      const dataType = pathParts[1] // products, orders, or customers
-      const itemId = pathParts[2]
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/:id/toggle - Toggle individual product
+    // =========================================================================
+    if (path.match(/^store-data\/products\/[^\/]+\/toggle$/) && req.method === 'PUT') {
+      const itemId = path.split('/')[2]
       const { store_id, enabled } = await req.json()
 
-      // Only allow toggle for products (orders/customers not cached due to GDPR)
-      if (dataType !== 'products') {
-        return new Response(
-          JSON.stringify({
-            error: `Cannot toggle ${dataType} - only products can be toggled`,
-            hint: 'Orders and customers are not cached for GDPR compliance'
-          }),
-          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-        )
-      }
-
       if (!store_id) {
         return new Response(
           JSON.stringify({ error: 'store_id is required' }),
@@ -1315,8 +1377,6 @@ serve(async (req) => {
 
       try {
         const platform = store.platform_name
-
-        // Get metadata from product cache
         const tableName = platform === 'woocommerce'
           ? 'woocommerce_products_cache'
           : platform === 'shopify'
@@ -1329,48 +1389,21 @@ serve(async (req) => {
           ? 'shopify_product_id'
           : 'shoprenter_product_id'
 
-        const { data: cacheItem } = await supabase
+        // Update excluded column directly in cache table
+        // Note: enabled in UI = NOT excluded in DB
+        const { error: updateError } = await supabase
           .from(tableName)
-          .select('*')
-          .eq('store_id', store_id)
-          .eq(idColumn, itemId)
-          .single()
-
-        let metadata = {}
-        if (cacheItem) {
-          if (platform === 'shoprenter') {
-            metadata = {
-              name: cacheItem.product_data?.name,
-              sku: cacheItem.product_data?.sku,
-              price: cacheItem.product_data?.price
-            }
-          } else {
-            metadata = {
-              name: cacheItem.name || cacheItem.title,
-              sku: cacheItem.sku,
-              price: cacheItem.price
-            }
-          }
-        }
-
-        // Upsert exclusion
-        const { error: upsertError } = await supabase
-          .from('store_data_exclusions')
-          .upsert({
-            store_id: store_id,
-            data_type: 'product',
-            data_id: itemId,
-            is_enabled: enabled,
-            metadata,
+          .update({
+            excluded: !enabled, // Inverted logic
             updated_at: new Date().toISOString()
-          }, {
-            onConflict: 'store_id,data_type,data_id'
           })
+          .eq('store_id', store_id)
+          .eq(idColumn, itemId)
 
-        if (upsertError) {
-          console.error('Error upserting exclusion:', upsertError)
+        if (updateError) {
+          console.error('Error updating product:', updateError)
           return new Response(
-            JSON.stringify({ error: 'Failed to update item' }),
+            JSON.stringify({ error: 'Failed to update product' }),
             { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
           )
         }
@@ -1391,22 +1424,12 @@ serve(async (req) => {
       }
     }
 
-    // PUT /api/store-data/:type/bulk-toggle - Bulk toggle items (products only)
-    if (path.match(/^store-data\/(products|orders|customers)\/bulk-toggle$/) && req.method === 'PUT') {
-      const dataType = path.split('/')[1]
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/bulk-toggle - Bulk toggle products
+    // =========================================================================
+    if (path.match(/^store-data\/products\/bulk-toggle$/) && req.method === 'PUT') {
       const { store_id, item_ids, enabled } = await req.json()
 
-      // Only allow bulk toggle for products
-      if (dataType !== 'products') {
-        return new Response(
-          JSON.stringify({
-            error: `Cannot bulk toggle ${dataType} - only products can be toggled`,
-            hint: 'Orders and customers are not cached for GDPR compliance'
-          }),
-          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-        )
-      }
-
       if (!store_id || !item_ids || !Array.isArray(item_ids)) {
         return new Response(
           JSON.stringify({ error: 'store_id and item_ids array are required' }),
@@ -1430,19 +1453,29 @@ serve(async (req) => {
       }
 
       try {
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        const idColumn = platform === 'woocommerce'
+          ? 'wc_product_id'
+          : platform === 'shopify'
+          ? 'shopify_product_id'
+          : 'shoprenter_product_id'
+
+        // Update all products in bulk
         for (const itemId of item_ids) {
           await supabase
-            .from('store_data_exclusions')
-            .upsert({
-              store_id: store_id,
-              data_type: 'product',
-              data_id: itemId,
-              is_enabled: enabled,
-              metadata: {},
+            .from(tableName)
+            .update({
+              excluded: !enabled, // Inverted logic
               updated_at: new Date().toISOString()
-            }, {
-              onConflict: 'store_id,data_type,data_id'
             })
+            .eq('store_id', store_id)
+            .eq(idColumn, itemId)
         }
 
         return new Response(
@@ -1461,22 +1494,12 @@ serve(async (req) => {
       }
     }
 
-    // PUT /api/store-data/:type/enable-all - Enable all items (products only)
-    if (path.match(/^store-data\/(products|orders|customers)\/enable-all$/) && req.method === 'PUT') {
-      const dataType = path.split('/')[1]
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/enable-all - Enable all products
+    // =========================================================================
+    if (path.match(/^store-data\/products\/enable-all$/) && req.method === 'PUT') {
       const { store_id } = await req.json()
 
-      // Only allow enable-all for products
-      if (dataType !== 'products') {
-        return new Response(
-          JSON.stringify({
-            error: `Cannot enable all ${dataType} - only products can be managed`,
-            hint: 'Orders and customers are not cached for GDPR compliance'
-          }),
-          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-        )
-      }
-
       if (!store_id) {
         return new Response(
           JSON.stringify({ error: 'store_id is required' }),
@@ -1487,7 +1510,7 @@ serve(async (req) => {
       // Verify store ownership
       const { data: store, error: storeError } = await supabase
         .from('stores')
-        .select('id')
+        .select('id, platform_name')
         .eq('id', store_id)
         .eq('user_id', user.id)
         .single()
@@ -1500,21 +1523,37 @@ serve(async (req) => {
       }
 
       try {
-        // Update all exclusions for this store to enabled
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        // Set excluded = false for all products (enable all)
         const { error: updateError } = await supabase
-          .from('store_data_exclusions')
-          .update({ is_enabled: true, updated_at: new Date().toISOString() })
+          .from(tableName)
+          .update({
+            excluded: false,
+            updated_at: new Date().toISOString()
+          })
           .eq('store_id', store_id)
-          .eq('data_type', 'product')
 
         if (updateError) {
           console.error('Error enabling all products:', updateError)
           return new Response(
-            JSON.stringify({ error: 'Failed to enable all items' }),
+            JSON.stringify({ error: 'Failed to enable all products' }),
             { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
           )
         }
 
+        // Also clear all excluded categories for this store
+        await supabase
+          .from('excluded_categories')
+          .delete()
+          .eq('store_id', store_id)
+          .eq('platform', platform)
+
         return new Response(
           JSON.stringify({
             success: true,
@@ -1531,22 +1570,12 @@ serve(async (req) => {
       }
     }
 
-    // PUT /api/store-data/:type/disable-all - Disable all items (products only)
-    if (path.match(/^store-data\/(products|orders|customers)\/disable-all$/) && req.method === 'PUT') {
-      const dataType = path.split('/')[1]
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/disable-all - Disable all products
+    // =========================================================================
+    if (path.match(/^store-data\/products\/disable-all$/) && req.method === 'PUT') {
       const { store_id } = await req.json()
 
-      // Only allow disable-all for products
-      if (dataType !== 'products') {
-        return new Response(
-          JSON.stringify({
-            error: `Cannot disable all ${dataType} - only products can be managed`,
-            hint: 'Orders and customers are not cached for GDPR compliance'
-          }),
-          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-        )
-      }
-
       if (!store_id) {
         return new Response(
           JSON.stringify({ error: 'store_id is required' }),
@@ -1571,49 +1600,27 @@ serve(async (req) => {
 
       try {
         const platform = store.platform_name
-
-        // Get all products from cache to create exclusions
         const tableName = platform === 'woocommerce'
           ? 'woocommerce_products_cache'
           : platform === 'shopify'
           ? 'shopify_products_cache'
           : 'shoprenter_products_cache'
 
-        const idColumn = platform === 'woocommerce'
-          ? 'wc_product_id'
-          : platform === 'shopify'
-          ? 'shopify_product_id'
-          : 'shoprenter_product_id'
-
-        const { data: cacheItems } = await supabase
+        // Set excluded = true for all products (disable all)
+        const { error: updateError } = await supabase
           .from(tableName)
-          .select(idColumn)
-          .eq('store_id', store_id)
-
-        if (cacheItems && cacheItems.length > 0) {
-          // Create or update exclusions for all products
-          const exclusions = cacheItems.map(item => ({
-            store_id: store_id,
-            data_type: 'product',
-            data_id: item[idColumn],
-            is_enabled: false,
-            metadata: {},
+          .update({
+            excluded: true,
             updated_at: new Date().toISOString()
-          }))
-
-          const { error: upsertError } = await supabase
-            .from('store_data_exclusions')
-            .upsert(exclusions, {
-              onConflict: 'store_id,data_type,data_id'
-            })
+          })
+          .eq('store_id', store_id)
 
-          if (upsertError) {
-            console.error('Error disabling all products:', upsertError)
-            return new Response(
-              JSON.stringify({ error: 'Failed to disable all products' }),
-              { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-            )
-          }
+        if (updateError) {
+          console.error('Error disabling all products:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to disable all products' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
         }
 
         return new Response(
@@ -1631,7 +1638,6 @@ serve(async (req) => {
         )
       }
     }
-
     // GET /api/dashboard/stats - Get dashboard statistics
     if (path === 'dashboard/stats' && req.method === 'GET') {
       // TODO: Implement real dashboard stats calculation

+ 1946 - 0
supabase/functions/api/index.ts.backup

@@ -0,0 +1,1946 @@
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+import { wrapHandler } from '../_shared/error-handler.ts'
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+}
+
+// Handle OPTIONS requests BEFORE wrapping with error handler to avoid timeouts
+serve(async (req) => {
+  // Fast-path for OPTIONS requests
+  if (req.method === 'OPTIONS') {
+    return new Response('ok', { headers: corsHeaders })
+  }
+
+  // Wrap all other requests with error handler
+  return wrapHandler('api', async (req) => {
+
+  try {
+    const url = new URL(req.url)
+    const path = url.pathname.replace('/api/', '')
+
+    // Get user from authorization header
+    const authHeader = req.headers.get('authorization')
+    if (!authHeader) {
+      return new Response(
+        JSON.stringify({ error: 'No authorization header' }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    const token = authHeader.replace('Bearer ', '')
+
+    // Create Supabase client with the user's token for proper RLS
+    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+    const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY')!
+    const supabase = createClient(supabaseUrl, supabaseKey, {
+      global: {
+        headers: {
+          Authorization: authHeader
+        }
+      }
+    })
+
+    // Verify the token and get user
+    const { data: { user }, error: userError } = await supabase.auth.getUser(token)
+
+    if (userError || !user) {
+      return new Response(
+        JSON.stringify({ error: 'Invalid token' }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // GET /api/stores - List all stores for the user
+    if (path === 'stores' && req.method === 'GET') {
+      // Always include sync config since Web UI needs last_sync_at
+      let query = supabase
+        .from('stores')
+        .select(`
+          *,
+          phone_numbers!stores_phone_number_id_fkey (
+            id,
+            phone_number,
+            country_code,
+            country_name,
+            location,
+            phone_type,
+            price
+          ),
+          store_sync_config (
+            enabled,
+            sync_frequency,
+            products_access_policy,
+            customers_access_policy,
+            orders_access_policy,
+            last_sync_at,
+            next_sync_at
+          )
+        `)
+        .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(
+          JSON.stringify({ error: 'Failed to fetch stores' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Transform phone_numbers to phone_number string for backward compatibility
+      // Note: For many-to-one relationships, Supabase returns an object, not an array
+      const transformedStores = stores?.map(store => {
+        // Handle both object (many-to-one) and array (one-to-many) formats
+        const phoneData = Array.isArray(store.phone_numbers)
+          ? store.phone_numbers[0]
+          : store.phone_numbers;
+
+        return {
+          ...store,
+          phone_number: phoneData?.phone_number || null,
+          phone_number_details: phoneData || null
+        };
+      }) || []
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          stores: transformedStores
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // POST /api/stores/finalize-shoprenter - Finalize ShopRenter installation
+    if (path === 'stores/finalize-shoprenter' && req.method === 'POST') {
+      const { installation_id } = await req.json()
+
+      if (!installation_id) {
+        return new Response(
+          JSON.stringify({ error: 'installation_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+      const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
+
+      // Get pending installation
+      const { data: installation, error: installError } = await supabaseAdmin
+        .from('pending_shoprenter_installs')
+        .select('*')
+        .eq('installation_id', installation_id)
+        .single()
+
+      if (installError || !installation) {
+        return new Response(
+          JSON.stringify({ error: 'Installation not found or expired' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Check if expired
+      if (new Date(installation.expires_at) < new Date()) {
+        await supabaseAdmin.from('pending_shoprenter_installs').delete().eq('installation_id', installation_id)
+        return new Response(
+          JSON.stringify({ error: 'Installation expired. Please try again.' }),
+          { status: 410, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify phone number
+      const phoneNumberId = installation.phone_number_id
+      if (!phoneNumberId) {
+        await supabaseAdmin.from('pending_shoprenter_installs').delete().eq('installation_id', installation_id)
+        return new Response(
+          JSON.stringify({ error: 'Phone number selection missing. Please reconnect your store.' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify phone number is still available
+      const { data: phoneNumber, error: phoneError } = await supabaseAdmin
+        .from('phone_numbers')
+        .select('id, is_available')
+        .eq('id', phoneNumberId)
+        .single()
+
+      if (phoneError || !phoneNumber || !phoneNumber.is_available) {
+        await supabaseAdmin.from('pending_shoprenter_installs').delete().eq('installation_id', installation_id)
+        return new Response(
+          JSON.stringify({ error: 'Selected phone number is no longer available. Please reconnect your store.' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get client credentials from environment (app-level credentials)
+      const clientId = Deno.env.get('SHOPRENTER_APP_CLIENT_ID')
+      const clientSecret = Deno.env.get('SHOPRENTER_APP_CLIENT_SECRET')
+
+      // Create store record with phone_number_id
+      // IMPORTANT: Store client credentials in api_key/api_secret for token renewal
+      // Store OAuth tokens in access_token/refresh_token columns
+      const { data: newStore, error: storeError } = await supabaseAdmin
+        .from('stores')
+        .insert({
+          user_id: user.id,
+          platform_name: 'shoprenter',
+          store_name: installation.shopname,
+          store_url: `https://${installation.shopname}.shoprenter.hu`,
+          api_key: clientId,
+          api_secret: clientSecret,
+          access_token: installation.access_token,
+          refresh_token: installation.refresh_token,
+          token_expires_at: new Date(Date.now() + (installation.expires_in * 1000)).toISOString(),
+          scopes: installation.scopes || [],
+          phone_number_id: phoneNumberId,
+          alt_data: {
+            token_type: installation.token_type,
+            expires_in: installation.expires_in,
+            connectedAt: new Date().toISOString(),
+            client_id: clientId,
+            client_secret: clientSecret
+          }
+        })
+        .select('id')
+        .single()
+
+      if (storeError || !newStore) {
+        console.error('[API] Error creating store:', storeError)
+        return new Response(
+          JSON.stringify({ error: 'Failed to save store credentials' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Update phone number to mark as assigned
+      const { error: phoneUpdateError } = await supabaseAdmin
+        .from('phone_numbers')
+        .update({
+          is_available: false,
+          store_id: newStore.id,
+          updated_at: new Date().toISOString()
+        })
+        .eq('id', phoneNumberId)
+
+      if (phoneUpdateError) {
+        console.error('[API] Error updating phone number:', phoneUpdateError)
+        // Rollback: delete the store
+        await supabaseAdmin.from('stores').delete().eq('id', newStore.id)
+        await supabaseAdmin.from('pending_shoprenter_installs').delete().eq('installation_id', installation_id)
+        return new Response(
+          JSON.stringify({ error: 'Phone number assignment failed. Please try again.' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Delete pending installation
+      await supabaseAdmin.from('pending_shoprenter_installs').delete().eq('installation_id', installation_id)
+
+      console.log(`[API] ShopRenter store finalized: ${installation.shopname} with phone number ${phoneNumberId}`)
+
+      // Trigger auto-sync in background
+      const triggerSyncUrl = `${supabaseUrl}/functions/v1/trigger-sync`
+      fetch(triggerSyncUrl, {
+        method: 'POST',
+        headers: {
+          'Authorization': `Bearer ${supabaseServiceKey}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ store_id: newStore.id })
+      })
+        .then(() => console.log(`[API] Auto-sync triggered for ShopRenter store ${newStore.id}`))
+        .catch(err => console.error(`[API] Failed to trigger auto-sync:`, err))
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Store connected successfully',
+          store_id: newStore.id
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // GET /api/stores/:id/sync-status - Get sync status for a store
+    if (path.match(/^stores\/[^\/]+\/sync-status$/) && req.method === 'GET') {
+      const storeId = path.split('/')[1]
+
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('sync_status, sync_started_at, sync_completed_at, sync_error')
+        .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' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          status: store.sync_status || 'idle',
+          started_at: store.sync_started_at,
+          completed_at: store.sync_completed_at,
+          error: store.sync_error
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // POST /api/stores/:id/sync - Trigger manual sync for a store
+    if (path.match(/^stores\/[^\/]+\/sync$/) && req.method === 'POST') {
+      const storeId = path.split('/')[1]
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name, sync_status')
+        .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' } }
+        )
+      }
+
+      // Check if sync is already running
+      if (store.sync_status === 'syncing') {
+        return new Response(
+          JSON.stringify({
+            success: false,
+            error: 'Sync already in progress',
+            status: 'syncing'
+          }),
+          { status: 409, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Trigger sync via trigger-sync function
+      const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+      const triggerSyncUrl = `${supabaseUrl}/functions/v1/trigger-sync`
+
+      const syncResponse = await fetch(triggerSyncUrl, {
+        method: 'POST',
+        headers: {
+          'Authorization': `Bearer ${supabaseServiceKey}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ store_id: storeId })
+      })
+
+      const syncResult = await syncResponse.json()
+
+      if (!syncResponse.ok) {
+        return new Response(
+          JSON.stringify({
+            success: false,
+            error: syncResult.error || 'Failed to trigger sync'
+          }),
+          { status: syncResponse.status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Sync triggered successfully',
+          status: 'syncing'
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // 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]
+      const { ai_config } = await req.json()
+
+      if (!ai_config) {
+        return new Response(
+          JSON.stringify({ error: 'ai_config is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, alt_data')
+        .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 store's alt_data with AI config
+      const updatedAltData = {
+        ...(store.alt_data || {}),
+        ai_config
+      }
+
+      const { error: updateError } = await supabase
+        .from('stores')
+        .update({ alt_data: updatedAltData })
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+
+      if (updateError) {
+        console.error('Error updating AI config:', updateError)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update AI configuration' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'AI configuration updated successfully'
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // PUT /api/stores/:id/access-policies - Update data access policies for a store (GDPR compliance)
+    if (path.match(/^stores\/[^\/]+\/access-policies$/) && req.method === 'PUT') {
+      const storeId = path.split('/')[1]
+      const body = await req.json()
+
+      // Validate the request body
+      const validPolicies = ['products_access_policy', 'customers_access_policy', 'orders_access_policy']
+      const validPolicyValues = ['sync', 'api_only', 'not_allowed']
+
+      for (const key of validPolicies) {
+        if (!(key in body)) {
+          return new Response(
+            JSON.stringify({ error: `Missing required field: ${key}` }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        if (!validPolicyValues.includes(body[key])) {
+          return new Response(
+            JSON.stringify({ error: `Invalid value for ${key}. Must be one of: ${validPolicyValues.join(', ')}` }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+      }
+
+      // Verify the store belongs to the user
+      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' } }
+        )
+      }
+
+      // Use service role client to call the helper function
+      const supabaseAdmin = createClient(
+        Deno.env.get('SUPABASE_URL')!,
+        Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+      )
+
+      // Update the access policies for each data type
+      const updates = []
+      for (const dataType of ['products', 'customers', 'orders']) {
+        const policyKey = `${dataType}_access_policy`
+        const policy = body[policyKey]
+
+        const { error: updateError } = await supabaseAdmin
+          .rpc('set_data_access_policy', {
+            p_store_id: storeId,
+            p_data_type: dataType,
+            p_policy: policy
+          })
+
+        if (updateError) {
+          console.error(`Error updating ${dataType} access policy:`, updateError)
+          updates.push({ dataType, success: false, error: updateError.message })
+        } else {
+          updates.push({ dataType, success: true })
+        }
+      }
+
+      // Check if all updates succeeded
+      const allSuccess = updates.every(u => u.success)
+
+      if (!allSuccess) {
+        return new Response(
+          JSON.stringify({
+            error: 'Some access policy updates failed',
+            details: updates
+          }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Access policies updated successfully',
+          policies: body
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // DELETE /api/stores/{id} - Delete a store
+    if (path.startsWith('stores/') && req.method === 'DELETE') {
+      const storeId = path.replace('stores/', '')
+
+      // Verify the store belongs to the user
+      const { data: store, error: fetchError } = await supabase
+        .from('stores')
+        .select('id, platform_name, store_name')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (fetchError || !store) {
+        console.error('Error fetching store for deletion:', fetchError)
+        return new Response(
+          JSON.stringify({
+            error: 'Store not found or access denied',
+            details: fetchError ? fetchError.message : 'Store does not exist or you do not have permission to delete it'
+          }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      console.log(`[API] Attempting to delete store: ${store.store_name} (${store.platform_name}) - ID: ${storeId}`)
+
+      // Delete the store - cascading will handle related records
+      const { error: deleteError } = await supabase
+        .from('stores')
+        .delete()
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+
+      if (deleteError) {
+        console.error('Error deleting store:', {
+          storeId,
+          userId: user.id,
+          error: deleteError
+        })
+        return new Response(
+          JSON.stringify({
+            error: 'Failed to delete store',
+            details: deleteError.message || 'An unknown error occurred while deleting the store',
+            code: deleteError.code || 'unknown'
+          }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      console.log(`[API] Store deleted successfully: ${storeId}`)
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Store deleted successfully'
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // GET /api/stores/:id/knowledge-data - List knowledge data items
+    if (path.match(/^stores\/[^\/]+\/knowledge-data$/) && req.method === 'GET') {
+      const storeId = path.split('/')[1]
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      // Get query parameters
+      const dataType = url.searchParams.get('data_type')
+      const search = url.searchParams.get('search')
+      const page = parseInt(url.searchParams.get('page') || '1')
+      const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100)
+      const offset = (page - 1) * limit
+
+      try {
+        // Build queries based on platform and data type
+        const results: any[] = []
+        const platform = store.platform_name
+
+        // Helper to get table name and ID column
+        const getTableInfo = (type: string) => {
+          if (platform === 'woocommerce') {
+            return { table: `woocommerce_${type}s_cache`, idCol: `wc_${type}_id` }
+          } else if (platform === 'shopify') {
+            return { table: `shopify_${type}s_cache`, idCol: `shopify_${type}_id` }
+          } else if (platform === 'shoprenter') {
+            return { table: `shoprenter_${type}s_cache`, idCol: `shoprenter_${type}_id` }
+          }
+          return null
+        }
+
+        // Determine which data types to query
+        const typesToQuery = dataType ? [dataType] : ['product', 'order', 'customer']
+
+        for (const type of typesToQuery) {
+          const tableInfo = getTableInfo(type)
+          if (!tableInfo) continue
+
+          // First, fetch all cache items
+          let cacheQuery = supabase
+            .from(tableInfo.table)
+            .select('*')
+            .eq('store_id', storeId)
+            .order('created_at', { ascending: false })
+
+          // Apply search filter
+          if (search) {
+            if (type === 'product') {
+              if (platform === 'shoprenter') {
+                // For ShopRenter, search in product_data jsonb
+                cacheQuery = cacheQuery.or(`product_data->>name.ilike.%${search}%,product_data->>sku.ilike.%${search}%`)
+              } else {
+                cacheQuery = cacheQuery.or(`name.ilike.%${search}%,sku.ilike.%${search}%`)
+              }
+            } else if (type === 'order') {
+              cacheQuery = cacheQuery.or(`order_number.ilike.%${search}%,customer_name.ilike.%${search}%,customer_email.ilike.%${search}%`)
+            } else if (type === 'customer') {
+              cacheQuery = cacheQuery.or(`email.ilike.%${search}%,first_name.ilike.%${search}%,last_name.ilike.%${search}%,phone.ilike.%${search}%`)
+            }
+          }
+
+          const { data: cacheItems, error: cacheError } = await cacheQuery
+
+          if (cacheError) {
+            console.error(`Error fetching ${type}s from cache:`, cacheError)
+            continue
+          }
+
+          if (!cacheItems || cacheItems.length === 0) continue
+
+          // Get all exclusions for this store and type
+          const { data: exclusions } = await supabase
+            .from('store_data_exclusions')
+            .select('data_id, is_enabled, metadata')
+            .eq('store_id', storeId)
+            .eq('data_type', type)
+
+          // Create a map of exclusions for quick lookup
+          const exclusionMap = new Map()
+          if (exclusions) {
+            exclusions.forEach(e => {
+              exclusionMap.set(e.data_id, { is_enabled: e.is_enabled, metadata: e.metadata })
+            })
+          }
+
+          // Transform cache items to result format
+          for (const item of cacheItems) {
+            const itemId = item[tableInfo.idCol]
+            const exclusionData = exclusionMap.get(itemId)
+
+            let resultItem: any = {
+              data_type: type,
+              data_id: itemId,
+              is_enabled: exclusionData ? exclusionData.is_enabled : true
+            }
+
+            // Add type-specific fields
+            if (type === 'product') {
+              if (platform === 'shoprenter') {
+                resultItem.name = item.product_data?.name
+                resultItem.sku = item.product_data?.sku
+                resultItem.price = item.product_data?.price
+              } else {
+                resultItem.name = item.name
+                resultItem.sku = item.sku
+                resultItem.price = item.price
+              }
+            } else if (type === 'order') {
+              resultItem.order_number = item.order_number
+              resultItem.customer_name = item.customer_name
+              resultItem.customer_email = item.customer_email
+              resultItem.total = item.total
+            } else if (type === 'customer') {
+              resultItem.email = item.email
+              resultItem.first_name = item.first_name
+              resultItem.last_name = item.last_name
+              resultItem.phone = item.phone || ''
+            }
+
+            results.push(resultItem)
+          }
+        }
+
+        // Apply pagination to combined results
+        const totalCount = results.length
+        const paginatedResults = results.slice(offset, offset + limit)
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            data: paginatedResults,
+            pagination: {
+              page,
+              limit,
+              total: totalCount,
+              totalPages: Math.ceil(totalCount / limit)
+            }
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error fetching knowledge data:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch knowledge data' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/stores/:id/knowledge-data/:dataType/:dataId - Toggle item state
+    if (path.match(/^stores\/[^\/]+\/knowledge-data\/[^\/]+\/[^\/]+$/) && req.method === 'PUT') {
+      const pathParts = path.split('/')
+      const storeId = pathParts[1]
+      const dataType = pathParts[3]
+      const dataId = pathParts[4]
+
+      // Validate data_type
+      if (!['product', 'order', 'customer'].includes(dataType)) {
+        return new Response(
+          JSON.stringify({ error: 'Invalid data_type. Must be product, order, or customer' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      // Get request body
+      const body = await req.json()
+      const isEnabled = body.is_enabled !== undefined ? body.is_enabled : true
+
+      try {
+        // Extract metadata from cache tables
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? `woocommerce_${dataType}s_cache`
+          : platform === 'shopify'
+          ? `shopify_${dataType}s_cache`
+          : `shoprenter_${dataType}s_cache`
+
+        const idColumn = platform === 'woocommerce'
+          ? `wc_${dataType}_id`
+          : platform === 'shopify'
+          ? `shopify_${dataType}_id`
+          : `shoprenter_${dataType}_id`
+
+        // Fetch item from cache to extract metadata
+        let metadata = {}
+        const { data: cacheItem } = await supabase
+          .from(tableName)
+          .select('*')
+          .eq('store_id', storeId)
+          .eq(idColumn, dataId)
+          .single()
+
+        if (cacheItem) {
+          if (dataType === 'product') {
+            if (platform === 'shoprenter') {
+              metadata = {
+                name: cacheItem.product_data?.name,
+                sku: cacheItem.product_data?.sku,
+                price: cacheItem.product_data?.price
+              }
+            } else {
+              metadata = {
+                name: cacheItem.name,
+                sku: cacheItem.sku,
+                price: cacheItem.price
+              }
+            }
+          } else if (dataType === 'order') {
+            metadata = {
+              order_number: cacheItem.order_number,
+              customer_name: cacheItem.customer_name,
+              customer_email: cacheItem.customer_email,
+              total: cacheItem.total
+            }
+          } else if (dataType === 'customer') {
+            metadata = {
+              email: cacheItem.email,
+              first_name: cacheItem.first_name,
+              last_name: cacheItem.last_name,
+              phone: cacheItem.phone
+            }
+          }
+        }
+
+        // Upsert into store_data_exclusions
+        const { data: exclusion, error: upsertError } = await supabase
+          .from('store_data_exclusions')
+          .upsert({
+            store_id: storeId,
+            data_type: dataType,
+            data_id: dataId,
+            is_enabled: isEnabled,
+            metadata,
+            updated_at: new Date().toISOString()
+          }, {
+            onConflict: 'store_id,data_type,data_id'
+          })
+          .select()
+          .single()
+
+        if (upsertError) {
+          console.error('Error upserting exclusion:', upsertError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update item state' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Item ${isEnabled ? 'enabled' : 'disabled'} successfully`,
+            data: exclusion
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error updating item state:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update item state' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // POST /api/stores/:id/knowledge-data/batch - Bulk enable/disable items
+    if (path.match(/^stores\/[^\/]+\/knowledge-data\/batch$/) && req.method === 'POST') {
+      const storeId = path.split('/')[1]
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      // Get request body
+      const body = await req.json()
+      const { items, data_type, is_enabled } = body
+
+      if (!items || !Array.isArray(items) || items.length === 0) {
+        return new Response(
+          JSON.stringify({ error: 'items array is required and must not be empty' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+        const results = []
+
+        for (const item of items) {
+          const itemDataType = item.data_type || data_type
+          const itemDataId = item.data_id
+          const itemIsEnabled = item.is_enabled !== undefined ? item.is_enabled : is_enabled
+
+          if (!itemDataType || !itemDataId) {
+            continue
+          }
+
+          // Validate data_type
+          if (!['product', 'order', 'customer'].includes(itemDataType)) {
+            continue
+          }
+
+          // Extract metadata from cache tables
+          const tableName = platform === 'woocommerce'
+            ? `woocommerce_${itemDataType}s_cache`
+            : platform === 'shopify'
+            ? `shopify_${itemDataType}s_cache`
+            : `shoprenter_${itemDataType}s_cache`
+
+          const idColumn = platform === 'woocommerce'
+            ? `wc_${itemDataType}_id`
+            : platform === 'shopify'
+            ? `shopify_${itemDataType}_id`
+            : `shoprenter_${itemDataType}_id`
+
+          // Fetch item from cache to extract metadata
+          let metadata = {}
+          const { data: cacheItem } = await supabase
+            .from(tableName)
+            .select('*')
+            .eq('store_id', storeId)
+            .eq(idColumn, itemDataId)
+            .single()
+
+          if (cacheItem) {
+            if (itemDataType === 'product') {
+              if (platform === 'shoprenter') {
+                metadata = {
+                  name: cacheItem.product_data?.name,
+                  sku: cacheItem.product_data?.sku,
+                  price: cacheItem.product_data?.price
+                }
+              } else {
+                metadata = {
+                  name: cacheItem.name,
+                  sku: cacheItem.sku,
+                  price: cacheItem.price
+                }
+              }
+            } else if (itemDataType === 'order') {
+              metadata = {
+                order_number: cacheItem.order_number,
+                customer_name: cacheItem.customer_name,
+                customer_email: cacheItem.customer_email,
+                total: cacheItem.total
+              }
+            } else if (itemDataType === 'customer') {
+              metadata = {
+                email: cacheItem.email,
+                first_name: cacheItem.first_name,
+                last_name: cacheItem.last_name,
+                phone: cacheItem.phone
+              }
+            }
+          }
+
+          // Upsert into store_data_exclusions
+          const { error: upsertError } = await supabase
+            .from('store_data_exclusions')
+            .upsert({
+              store_id: storeId,
+              data_type: itemDataType,
+              data_id: itemDataId,
+              is_enabled: itemIsEnabled,
+              metadata,
+              updated_at: new Date().toISOString()
+            }, {
+              onConflict: 'store_id,data_type,data_id'
+            })
+
+          if (!upsertError) {
+            results.push({
+              data_type: itemDataType,
+              data_id: itemDataId,
+              success: true
+            })
+          } else {
+            results.push({
+              data_type: itemDataType,
+              data_id: itemDataId,
+              success: false,
+              error: upsertError.message
+            })
+          }
+        }
+
+        const successCount = results.filter(r => r.success).length
+        const failCount = results.filter(r => !r.success).length
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Batch operation completed: ${successCount} succeeded, ${failCount} failed`,
+            results
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in batch operation:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to complete batch operation' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // GET /api/store-data/products|orders|customers - Get store data by type
+    if (path.match(/^store-data\/(products|orders|customers)$/) && req.method === 'GET') {
+      const dataType = path.split('/')[1] // products, orders, or customers
+      const storeId = url.searchParams.get('store_id')
+      const page = parseInt(url.searchParams.get('page') || '1')
+      const limit = parseInt(url.searchParams.get('limit') || '25')
+      const search = url.searchParams.get('search')
+      const enabledFilter = url.searchParams.get('enabled')
+
+      if (!storeId) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      try {
+        // Map dataType to singular form for table lookup
+        const singularType = dataType === 'products' ? 'product' : dataType === 'orders' ? 'order' : 'customer'
+        const platform = store.platform_name
+
+        // Orders and customers are fetched in real-time (GDPR compliance)
+        // Products are fetched from cache
+        let cacheItems: any[] = []
+        let idColumn = ''
+
+        if (dataType === 'products') {
+          // Fetch products from cache
+          const tableName = platform === 'woocommerce'
+            ? 'woocommerce_products_cache'
+            : platform === 'shopify'
+            ? 'shopify_products_cache'
+            : 'shoprenter_products_cache'
+
+          idColumn = platform === 'woocommerce'
+            ? 'wc_product_id'
+            : platform === 'shopify'
+            ? 'shopify_product_id'
+            : 'shoprenter_product_id'
+
+          // Build cache query
+          let cacheQuery = supabase
+            .from(tableName)
+            .select('*')
+            .eq('store_id', storeId)
+            .order('created_at', { ascending: false })
+
+          // Apply search filter
+          if (search) {
+            if (platform === 'shoprenter') {
+              cacheQuery = cacheQuery.or(`product_data->>name.ilike.%${search}%,product_data->>sku.ilike.%${search}%`)
+            } else {
+              cacheQuery = cacheQuery.or(`name.ilike.%${search}%,sku.ilike.%${search}%`)
+            }
+          }
+
+          const { data, error: cacheError } = await cacheQuery
+
+          if (cacheError) {
+            console.error(`Error fetching products from cache:`, cacheError)
+            return new Response(
+              JSON.stringify({ error: 'Failed to fetch products' }),
+              { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+            )
+          }
+
+          cacheItems = data || []
+        } else {
+          // For orders and customers, return message that real-time access is not available via this endpoint
+          return new Response(
+            JSON.stringify({
+              error: `${dataType} data is no longer cached for GDPR compliance`,
+              hint: `Access ${dataType} in real-time via the shop-data-api Edge Function or through your platform's admin panel`,
+              success: false
+            }),
+            { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        if (!cacheItems || cacheItems.length === 0) {
+          return new Response(
+            JSON.stringify({
+              success: true,
+              data: [],
+              total: 0,
+              enabled_count: 0,
+              disabled_count: 0
+            }),
+            { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Get all exclusions for this store and type
+        const { data: exclusions } = await supabase
+          .from('store_data_exclusions')
+          .select('data_id, is_enabled')
+          .eq('store_id', storeId)
+          .eq('data_type', singularType)
+
+        // Create exclusion map
+        const exclusionMap = new Map()
+        if (exclusions) {
+          exclusions.forEach(e => {
+            exclusionMap.set(e.data_id, e.is_enabled)
+          })
+        }
+
+        // Transform cache items to result format
+        let results: any[] = []
+        for (const item of cacheItems) {
+          const itemId = item[idColumn]
+          const isEnabled = exclusionMap.has(itemId) ? exclusionMap.get(itemId) : true
+
+          let resultItem: any = {
+            id: itemId,
+            enabled_in_context: isEnabled
+          }
+
+          // Add product-specific fields
+          if (platform === 'shoprenter') {
+            resultItem.name = item.product_data?.name || ''
+            resultItem.sku = item.product_data?.sku || ''
+            resultItem.price = item.product_data?.price || '0'
+            resultItem.currency = item.product_data?.currency || 'HUF'
+          } else {
+            resultItem.name = item.name || item.title || ''
+            resultItem.sku = item.sku || ''
+            resultItem.price = item.price || '0'
+            resultItem.currency = item.currency || 'USD'
+          }
+
+          results.push(resultItem)
+        }
+
+        // Apply enabled filter if specified
+        if (enabledFilter !== null) {
+          const filterEnabled = enabledFilter === 'true'
+          results = results.filter(r => r.enabled_in_context === filterEnabled)
+        }
+
+        // Calculate counts
+        const enabledCount = results.filter(r => r.enabled_in_context).length
+        const disabledCount = results.filter(r => !r.enabled_in_context).length
+        const totalCount = results.length
+
+        // Apply pagination
+        const offset = (page - 1) * limit
+        const paginatedResults = results.slice(offset, offset + limit)
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            data: paginatedResults,
+            total: totalCount,
+            enabled_count: enabledCount,
+            disabled_count: disabledCount
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error(`Error fetching ${dataType}:`, error)
+        return new Response(
+          JSON.stringify({ error: `Failed to fetch ${dataType}` }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/store-data/:type/:id/toggle - Toggle individual item (products only)
+    if (path.match(/^store-data\/(products|orders|customers)\/[^\/]+\/toggle$/) && req.method === 'PUT') {
+      const pathParts = path.split('/')
+      const dataType = pathParts[1] // products, orders, or customers
+      const itemId = pathParts[2]
+      const { store_id, enabled } = await req.json()
+
+      // Only allow toggle for products (orders/customers not cached due to GDPR)
+      if (dataType !== 'products') {
+        return new Response(
+          JSON.stringify({
+            error: `Cannot toggle ${dataType} - only products can be toggled`,
+            hint: 'Orders and customers are not cached for GDPR compliance'
+          }),
+          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+
+        // Get metadata from product cache
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        const idColumn = platform === 'woocommerce'
+          ? 'wc_product_id'
+          : platform === 'shopify'
+          ? 'shopify_product_id'
+          : 'shoprenter_product_id'
+
+        const { data: cacheItem } = await supabase
+          .from(tableName)
+          .select('*')
+          .eq('store_id', store_id)
+          .eq(idColumn, itemId)
+          .single()
+
+        let metadata = {}
+        if (cacheItem) {
+          if (platform === 'shoprenter') {
+            metadata = {
+              name: cacheItem.product_data?.name,
+              sku: cacheItem.product_data?.sku,
+              price: cacheItem.product_data?.price
+            }
+          } else {
+            metadata = {
+              name: cacheItem.name || cacheItem.title,
+              sku: cacheItem.sku,
+              price: cacheItem.price
+            }
+          }
+        }
+
+        // Upsert exclusion
+        const { error: upsertError } = await supabase
+          .from('store_data_exclusions')
+          .upsert({
+            store_id: store_id,
+            data_type: 'product',
+            data_id: itemId,
+            is_enabled: enabled,
+            metadata,
+            updated_at: new Date().toISOString()
+          }, {
+            onConflict: 'store_id,data_type,data_id'
+          })
+
+        if (upsertError) {
+          console.error('Error upserting exclusion:', upsertError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update item' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Product ${enabled ? 'enabled' : 'disabled'} successfully`
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error toggling product:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update product' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/store-data/:type/bulk-toggle - Bulk toggle items (products only)
+    if (path.match(/^store-data\/(products|orders|customers)\/bulk-toggle$/) && req.method === 'PUT') {
+      const dataType = path.split('/')[1]
+      const { store_id, item_ids, enabled } = await req.json()
+
+      // Only allow bulk toggle for products
+      if (dataType !== 'products') {
+        return new Response(
+          JSON.stringify({
+            error: `Cannot bulk toggle ${dataType} - only products can be toggled`,
+            hint: 'Orders and customers are not cached for GDPR compliance'
+          }),
+          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      if (!store_id || !item_ids || !Array.isArray(item_ids)) {
+        return new Response(
+          JSON.stringify({ error: 'store_id and item_ids array are required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        for (const itemId of item_ids) {
+          await supabase
+            .from('store_data_exclusions')
+            .upsert({
+              store_id: store_id,
+              data_type: 'product',
+              data_id: itemId,
+              is_enabled: enabled,
+              metadata: {},
+              updated_at: new Date().toISOString()
+            }, {
+              onConflict: 'store_id,data_type,data_id'
+            })
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `${item_ids.length} products ${enabled ? 'enabled' : 'disabled'} successfully`
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error bulk toggling products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/store-data/:type/enable-all - Enable all items (products only)
+    if (path.match(/^store-data\/(products|orders|customers)\/enable-all$/) && req.method === 'PUT') {
+      const dataType = path.split('/')[1]
+      const { store_id } = await req.json()
+
+      // Only allow enable-all for products
+      if (dataType !== 'products') {
+        return new Response(
+          JSON.stringify({
+            error: `Cannot enable all ${dataType} - only products can be managed`,
+            hint: 'Orders and customers are not cached for GDPR compliance'
+          }),
+          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        // Update all exclusions for this store to enabled
+        const { error: updateError } = await supabase
+          .from('store_data_exclusions')
+          .update({ is_enabled: true, updated_at: new Date().toISOString() })
+          .eq('store_id', store_id)
+          .eq('data_type', 'product')
+
+        if (updateError) {
+          console.error('Error enabling all products:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to enable all items' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: 'All products enabled successfully'
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error enabling all products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to enable all products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/store-data/:type/disable-all - Disable all items (products only)
+    if (path.match(/^store-data\/(products|orders|customers)\/disable-all$/) && req.method === 'PUT') {
+      const dataType = path.split('/')[1]
+      const { store_id } = await req.json()
+
+      // Only allow disable-all for products
+      if (dataType !== 'products') {
+        return new Response(
+          JSON.stringify({
+            error: `Cannot disable all ${dataType} - only products can be managed`,
+            hint: 'Orders and customers are not cached for GDPR compliance'
+          }),
+          { status: 501, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+
+        // Get all products from cache to create exclusions
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        const idColumn = platform === 'woocommerce'
+          ? 'wc_product_id'
+          : platform === 'shopify'
+          ? 'shopify_product_id'
+          : 'shoprenter_product_id'
+
+        const { data: cacheItems } = await supabase
+          .from(tableName)
+          .select(idColumn)
+          .eq('store_id', store_id)
+
+        if (cacheItems && cacheItems.length > 0) {
+          // Create or update exclusions for all products
+          const exclusions = cacheItems.map(item => ({
+            store_id: store_id,
+            data_type: 'product',
+            data_id: item[idColumn],
+            is_enabled: false,
+            metadata: {},
+            updated_at: new Date().toISOString()
+          }))
+
+          const { error: upsertError } = await supabase
+            .from('store_data_exclusions')
+            .upsert(exclusions, {
+              onConflict: 'store_id,data_type,data_id'
+            })
+
+          if (upsertError) {
+            console.error('Error disabling all products:', upsertError)
+            return new Response(
+              JSON.stringify({ error: 'Failed to disable all products' }),
+              { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+            )
+          }
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: 'All products disabled successfully'
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error disabling all products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to disable all products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // GET /api/dashboard/stats - Get dashboard statistics
+    if (path === 'dashboard/stats' && req.method === 'GET') {
+      // TODO: Implement real dashboard stats calculation
+      // For now, returning mock/empty data to unblock the frontend
+      return new Response(
+        JSON.stringify({
+          success: true,
+          stats: {
+            totalCalls: { value: 0, change: '0%', changeType: 'neutral' },
+            resolvedCalls: { value: 0, change: '0%', changeType: 'neutral' },
+            avgDuration: { value: 0, formatted: '0:00', change: '0%', changeType: 'neutral' },
+            totalCost: { value: 0, formatted: '$0.00', change: '0%', changeType: 'neutral' },
+            timeSaved: { value: '0h', formatted: '0 hours', change: '0%', changeType: 'neutral' },
+            humanCostSaved: { value: 0, formatted: '$0.00', change: '0%', changeType: 'neutral' },
+            resolutionRate: { value: 0, dailyChange: '0%', weeklyChange: '0%' },
+            topIntents: []
+          }
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // GET /api/call-logs - Get call logs
+    if (path === 'call-logs' && req.method === 'GET') {
+      try {
+        // Fetch call logs from database
+        const { data: callLogs, error: callLogsError } = await supabase
+          .from('call_logs')
+          .select('*')
+          .eq('user_id', user.id)
+          .order('created_at', { ascending: false })
+
+        if (callLogsError) {
+          console.error('Error fetching call logs:', callLogsError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch call logs' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Transform call logs to match frontend format
+        const transformedCallLogs = (callLogs || []).map(log => {
+          // Format duration
+          const duration = log.duration_seconds
+            ? `${Math.floor(log.duration_seconds / 60)}:${String(log.duration_seconds % 60).padStart(2, '0')}`
+            : '0:00'
+
+          // Determine outcome color
+          let outcomeColor = 'text-slate-300'
+          if (log.call_outcome === 'resolved' || log.call_outcome === 'interested') {
+            outcomeColor = 'text-green-500'
+          } else if (log.call_outcome === 'not_interested' || log.call_outcome === 'failed') {
+            outcomeColor = 'text-red-500'
+          } else if (log.call_outcome === 'pending') {
+            outcomeColor = 'text-yellow-500'
+          }
+
+          // Determine sentiment from analysis or summary
+          let sentiment = 'Neutral'
+          let sentimentColor = 'text-slate-300'
+
+          if (log.analysis?.successEvaluation === 'true' || log.analysis?.successEvaluation === true) {
+            sentiment = 'Positive'
+            sentimentColor = 'text-green-500'
+          } else if (log.analysis?.successEvaluation === 'false' || log.analysis?.successEvaluation === false) {
+            sentiment = 'Negative'
+            sentimentColor = 'text-red-500'
+          }
+
+          // Format cost
+          const cost = log.cost_total ? `$${Number(log.cost_total).toFixed(4)}` : '$0.00'
+
+          // Format time
+          const time = new Date(log.created_at).toLocaleString()
+
+          return {
+            id: log.id,
+            time,
+            customer: log.customer_number || 'Unknown',
+            intent: log.summary || 'N/A',
+            outcome: log.call_outcome || 'pending',
+            duration,
+            sentiment,
+            cost,
+            outcomeColor,
+            sentimentColor,
+            // Include full log data for details modal
+            fullData: log
+          }
+        })
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            call_logs: transformedCallLogs
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in call-logs endpoint:', error)
+        return new Response(
+          JSON.stringify({ error: 'Internal server error' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // GET /api/phone-numbers - Get available phone numbers for user's country
+    if (path === 'phone-numbers' && req.method === 'GET') {
+      try {
+        // Get user's profile to find their country
+        const { data: profile, error: profileError } = await supabase
+          .from('profiles')
+          .select('country_code, country_name')
+          .eq('id', user.id)
+          .single()
+
+        if (profileError) {
+          console.error('Error fetching user profile:', profileError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch user profile' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Check query parameters
+        const availableOnly = url.searchParams.get('available') === 'true'
+        const allCountries = url.searchParams.get('all_countries') === 'true'
+        const countryFilter = url.searchParams.get('country')
+        const cityFilter = url.searchParams.get('city')
+        const groupBy = url.searchParams.get('group_by') // 'countries' or 'cities'
+
+        // Special endpoint: Get list of countries with available phone numbers
+        if (groupBy === 'countries') {
+          const { data: countries, error: countriesError } = await supabase
+            .from('phone_numbers')
+            .select('country_code, country_name')
+            .eq('is_active', true)
+            .eq('is_available', true)
+            .is('assigned_to_store_id', null)
+            .order('country_name', { ascending: true })
+
+          if (countriesError) {
+            console.error('Error fetching countries:', countriesError)
+            return new Response(
+              JSON.stringify({ error: 'Failed to fetch countries' }),
+              { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+            )
+          }
+
+          // Get unique countries
+          const uniqueCountries = Array.from(
+            new Map((countries || []).map(c => [c.country_code, c])).values()
+          )
+
+          return new Response(
+            JSON.stringify({
+              success: true,
+              countries: uniqueCountries,
+              user_country: profile?.country_code || null
+            }),
+            { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Special endpoint: Get list of cities for a country
+        if (groupBy === 'cities' && countryFilter) {
+          const { data: cities, error: citiesError } = await supabase
+            .from('phone_numbers')
+            .select('location')
+            .eq('is_active', true)
+            .eq('is_available', true)
+            .eq('country_code', countryFilter)
+            .is('assigned_to_store_id', null)
+            .order('location', { ascending: true })
+
+          if (citiesError) {
+            console.error('Error fetching cities:', citiesError)
+            return new Response(
+              JSON.stringify({ error: 'Failed to fetch cities' }),
+              { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+            )
+          }
+
+          // Get unique cities
+          const uniqueCities = Array.from(
+            new Set((cities || []).map(c => c.location))
+          ).filter(Boolean)
+
+          return new Response(
+            JSON.stringify({
+              success: true,
+              cities: uniqueCities
+            }),
+            { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Build query for phone numbers
+        let query = supabase
+          .from('phone_numbers')
+          .select('*')
+          .eq('is_active', true)
+          .order('country_name', { ascending: true })
+          .order('location', { ascending: true })
+
+        // Filter by availability if requested
+        if (availableOnly) {
+          query = query.eq('is_available', true)
+          query = query.is('assigned_to_store_id', null)
+        }
+
+        // Filter by specific country if provided
+        if (countryFilter) {
+          query = query.eq('country_code', countryFilter)
+        } else if (profile?.country_code && !allCountries) {
+          // Filter by user's country if set (unless all_countries=true)
+          query = query.eq('country_code', profile.country_code)
+        }
+
+        // Filter by specific city if provided
+        if (cityFilter) {
+          query = query.eq('location', cityFilter)
+        }
+
+        const { data: phoneNumbers, error: phoneError } = await query
+
+        if (phoneError) {
+          console.error('Error fetching phone numbers:', phoneError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch phone numbers' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            phone_numbers: phoneNumbers || [],
+            user_country: profile?.country_code || null
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in phone-numbers endpoint:', error)
+        return new Response(
+          JSON.stringify({ error: 'Internal server error' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/profile/country - Update user's country
+    if (path === 'profile/country' && req.method === 'PUT') {
+      try {
+        const { country_code, country_name } = await req.json()
+
+        if (!country_code || !country_name) {
+          return new Response(
+            JSON.stringify({ error: 'country_code and country_name are required' }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Update user's profile with country information
+        const { error: updateError } = await supabase
+          .from('profiles')
+          .update({
+            country_code,
+            country_name,
+            updated_at: new Date().toISOString()
+          })
+          .eq('id', user.id)
+
+        if (updateError) {
+          console.error('Error updating user country:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update country' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: 'Country updated successfully'
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in profile/country endpoint:', error)
+        return new Response(
+          JSON.stringify({ error: 'Internal server error' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    return new Response(
+      JSON.stringify({ error: 'Not found' }),
+      { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+
+  } catch (error) {
+    console.error('[API] Unexpected error:', error)
+    return new Response(
+      JSON.stringify({ error: 'Internal server error', details: error.message }),
+      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+  }
+  })(req)
+})

+ 559 - 0
supabase/functions/api/store-data-endpoints-updated.ts

@@ -0,0 +1,559 @@
+// This file contains the UPDATED store-data endpoints for the API Edge Function
+// These replace the existing endpoints that used store_data_exclusions table
+//
+// INSTRUCTIONS: Replace the corresponding sections in api/index.ts with these implementations
+//
+// Changes:
+// 1. Use 'excluded' column in *_products_cache tables instead of store_data_exclusions
+// 2. Add category exclusion support
+// 3. Add new endpoints: GET /categories, PUT /categories/:id/exclude
+// 4. Update responses to include exclusion_reason (individual vs. category)
+
+    // =========================================================================
+    // NEW ENDPOINT: GET /api/store-data/categories - Get categories for a store
+    // =========================================================================
+    if (path === 'store-data/categories' && req.method === 'GET') {
+      const storeId = url.searchParams.get('store_id')
+
+      if (!storeId) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      try {
+        // Use the helper function to get categories
+        const { data: categories, error: categoriesError } = await supabase
+          .rpc('get_store_categories', {
+            p_store_id: storeId,
+            p_platform: store.platform_name
+          })
+
+        if (categoriesError) {
+          console.error('Error fetching categories:', categoriesError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch categories' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            categories: categories || []
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error fetching categories:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch categories' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // NEW ENDPOINT: PUT /api/store-data/categories/:categoryId/exclude
+    // =========================================================================
+    if (path.match(/^store-data\/categories\/[^\/]+\/exclude$/) && req.method === 'PUT') {
+      const categoryId = path.split('/')[2]
+      const { store_id, exclude, category_name } = await req.json()
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        // Use the helper function to toggle category exclusion
+        const { error: toggleError } = await supabase.rpc('toggle_category_exclusion', {
+          p_store_id: store_id,
+          p_category_id: categoryId,
+          p_category_name: category_name || categoryId,
+          p_platform: store.platform_name,
+          p_exclude: exclude
+        })
+
+        if (toggleError) {
+          console.error('Error toggling category exclusion:', toggleError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to toggle category exclusion' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Category ${exclude ? 'excluded' : 'included'} successfully`
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error toggling category exclusion:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to toggle category exclusion' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // UPDATED: GET /api/store-data/products - Get products with exclusion info
+    // =========================================================================
+    if (path.match(/^store-data\/(products)$/) && req.method === 'GET') {
+      const dataType = path.split('/')[1]
+      const storeId = url.searchParams.get('store_id')
+      const page = parseInt(url.searchParams.get('page') || '1')
+      const limit = parseInt(url.searchParams.get('limit') || '25')
+      const search = url.searchParams.get('search')
+      const enabledFilter = url.searchParams.get('enabled')
+      const categoryFilter = url.searchParams.get('category')
+
+      if (!storeId) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+
+        // Use the view with computed exclusion status
+        const viewName = platform === 'woocommerce'
+          ? 'woocommerce_products_with_exclusion'
+          : platform === 'shopify'
+          ? 'shopify_products_with_exclusion'
+          : 'shoprenter_products_with_exclusion'
+
+        // Build query
+        let query = supabase
+          .from(viewName)
+          .select('*')
+          .eq('store_id', storeId)
+          .order('created_at', { ascending: false })
+
+        // Apply search filter
+        if (search) {
+          query = query.or(`name.ilike.%${search}%,sku.ilike.%${search}%`)
+        }
+
+        // Apply category filter
+        if (categoryFilter) {
+          // Filter products that have this category
+          query = query.contains('categories', [{ id: categoryFilter }])
+        }
+
+        const { data: products, error: productsError } = await query
+
+        if (productsError) {
+          console.error('Error fetching products:', productsError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch products' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        if (!products || products.length === 0) {
+          return new Response(
+            JSON.stringify({
+              success: true,
+              data: [],
+              total: 0,
+              enabled_count: 0,
+              disabled_count: 0
+            }),
+            { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Transform to response format
+        let results = products.map(p => ({
+          id: p[platform === 'woocommerce' ? 'wc_product_id' : platform === 'shopify' ? 'shopify_product_id' : 'shoprenter_product_id'],
+          name: p.name || '',
+          sku: p.sku || '',
+          price: p.price || '0',
+          currency: p.currency || 'USD',
+          categories: p.categories || [],
+          enabled_in_context: !p.effectively_excluded, // Inverted logic: enabled = NOT excluded
+          excluded_by_individual: p.excluded,
+          excluded_by_category: p.exclusion_reason === 'category',
+          exclusion_reason: p.exclusion_reason
+        }))
+
+        // Apply enabled filter if specified
+        if (enabledFilter !== null) {
+          const filterEnabled = enabledFilter === 'true'
+          results = results.filter(r => r.enabled_in_context === filterEnabled)
+        }
+
+        // Calculate counts
+        const enabledCount = results.filter(r => r.enabled_in_context).length
+        const disabledCount = results.filter(r => !r.enabled_in_context).length
+        const totalCount = results.length
+
+        // Apply pagination
+        const offset = (page - 1) * limit
+        const paginatedResults = results.slice(offset, offset + limit)
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            data: paginatedResults,
+            total: totalCount,
+            enabled_count: enabledCount,
+            disabled_count: disabledCount
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error fetching products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/:id/toggle - Toggle individual product
+    // =========================================================================
+    if (path.match(/^store-data\/products\/[^\/]+\/toggle$/) && req.method === 'PUT') {
+      const itemId = path.split('/')[2]
+      const { store_id, enabled } = await req.json()
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        const idColumn = platform === 'woocommerce'
+          ? 'wc_product_id'
+          : platform === 'shopify'
+          ? 'shopify_product_id'
+          : 'shoprenter_product_id'
+
+        // Update excluded column directly in cache table
+        // Note: enabled in UI = NOT excluded in DB
+        const { error: updateError } = await supabase
+          .from(tableName)
+          .update({
+            excluded: !enabled, // Inverted logic
+            updated_at: new Date().toISOString()
+          })
+          .eq('store_id', store_id)
+          .eq(idColumn, itemId)
+
+        if (updateError) {
+          console.error('Error updating product:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update product' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Product ${enabled ? 'enabled' : 'disabled'} successfully`
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error toggling product:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update product' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/bulk-toggle - Bulk toggle products
+    // =========================================================================
+    if (path.match(/^store-data\/products\/bulk-toggle$/) && req.method === 'PUT') {
+      const { store_id, item_ids, enabled } = await req.json()
+
+      if (!store_id || !item_ids || !Array.isArray(item_ids)) {
+        return new Response(
+          JSON.stringify({ error: 'store_id and item_ids array are required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        const idColumn = platform === 'woocommerce'
+          ? 'wc_product_id'
+          : platform === 'shopify'
+          ? 'shopify_product_id'
+          : 'shoprenter_product_id'
+
+        // Update all products in bulk
+        for (const itemId of item_ids) {
+          await supabase
+            .from(tableName)
+            .update({
+              excluded: !enabled, // Inverted logic
+              updated_at: new Date().toISOString()
+            })
+            .eq('store_id', store_id)
+            .eq(idColumn, itemId)
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `${item_ids.length} products ${enabled ? 'enabled' : 'disabled'} successfully`
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error bulk toggling products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/enable-all - Enable all products
+    // =========================================================================
+    if (path.match(/^store-data\/products\/enable-all$/) && req.method === 'PUT') {
+      const { store_id } = await req.json()
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        // Set excluded = false for all products (enable all)
+        const { error: updateError } = await supabase
+          .from(tableName)
+          .update({
+            excluded: false,
+            updated_at: new Date().toISOString()
+          })
+          .eq('store_id', store_id)
+
+        if (updateError) {
+          console.error('Error enabling all products:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to enable all products' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Also clear all excluded categories for this store
+        await supabase
+          .from('excluded_categories')
+          .delete()
+          .eq('store_id', store_id)
+          .eq('platform', platform)
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: 'All products enabled successfully'
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error enabling all products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to enable all products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // =========================================================================
+    // UPDATED: PUT /api/store-data/products/disable-all - Disable all products
+    // =========================================================================
+    if (path.match(/^store-data\/products\/disable-all$/) && req.method === 'PUT') {
+      const { store_id } = await req.json()
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', store_id)
+        .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' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? 'woocommerce_products_cache'
+          : platform === 'shopify'
+          ? 'shopify_products_cache'
+          : 'shoprenter_products_cache'
+
+        // Set excluded = true for all products (disable all)
+        const { error: updateError } = await supabase
+          .from(tableName)
+          .update({
+            excluded: true,
+            updated_at: new Date().toISOString()
+          })
+          .eq('store_id', store_id)
+
+        if (updateError) {
+          console.error('Error disabling all products:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to disable all products' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: 'All products disabled successfully'
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error disabling all products:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to disable all products' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }

+ 465 - 0
supabase/migrations/20251120_product_exclusion_refactor.sql

@@ -0,0 +1,465 @@
+-- Migration: Product Exclusion Refactoring
+-- Description: Refactors product exclusion system to use cache tables with category support
+-- Date: 2025-11-20
+-- Related Issue: #111
+--
+-- This migration:
+-- 1. Adds 'excluded' column to all *_products_cache tables for individual product exclusions
+-- 2. Creates 'excluded_categories' table for category-level exclusions
+-- 3. Restructures product cache tables to normalize product data
+-- 4. Migrates existing store_data_exclusions data to new structure
+-- 5. Drops store_data_exclusions table
+-- 6. Creates Shopify products cache table
+-- 7. Adds helper functions and views
+
+-- ============================================================================
+-- STEP 1: Create excluded_categories table
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS excluded_categories (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
+  category_id TEXT NOT NULL,
+  category_name TEXT NOT NULL,
+  platform TEXT NOT NULL CHECK (platform IN ('shopify', 'woocommerce', 'shoprenter')),
+  created_at TIMESTAMPTZ DEFAULT NOW(),
+  updated_at TIMESTAMPTZ DEFAULT NOW(),
+  CONSTRAINT unique_store_category UNIQUE(store_id, category_id, platform)
+);
+
+-- Indexes for optimal query performance
+CREATE INDEX IF NOT EXISTS idx_excluded_categories_store_id ON excluded_categories(store_id);
+CREATE INDEX IF NOT EXISTS idx_excluded_categories_platform ON excluded_categories(platform);
+
+-- Enable RLS
+ALTER TABLE excluded_categories ENABLE ROW LEVEL SECURITY;
+
+-- RLS Policies
+CREATE POLICY "Users can view their excluded categories"
+  ON excluded_categories FOR SELECT
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = excluded_categories.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+CREATE POLICY "Users can insert their excluded categories"
+  ON excluded_categories FOR INSERT
+  TO authenticated
+  WITH CHECK (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = excluded_categories.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+CREATE POLICY "Users can update their excluded categories"
+  ON excluded_categories FOR UPDATE
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = excluded_categories.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+CREATE POLICY "Users can delete their excluded categories"
+  ON excluded_categories FOR DELETE
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = excluded_categories.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+-- Trigger for updated_at
+CREATE OR REPLACE FUNCTION update_excluded_categories_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+  NEW.updated_at = NOW();
+  RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_excluded_categories_updated_at
+  BEFORE UPDATE ON excluded_categories
+  FOR EACH ROW
+  EXECUTE FUNCTION update_excluded_categories_updated_at();
+
+-- ============================================================================
+-- STEP 2: Update WooCommerce products cache table
+-- ============================================================================
+
+-- Add excluded column
+ALTER TABLE woocommerce_products_cache ADD COLUMN IF NOT EXISTS excluded BOOLEAN DEFAULT FALSE;
+
+-- Add index for excluded column
+CREATE INDEX IF NOT EXISTS idx_woocommerce_products_excluded ON woocommerce_products_cache(store_id, excluded);
+
+-- Ensure categories column exists (already there, but verify it's JSONB)
+-- categories already exists as JSONB in the current schema
+
+-- Add updated_at if not exists
+DO $$
+BEGIN
+  IF NOT EXISTS (
+    SELECT 1 FROM information_schema.columns
+    WHERE table_name = 'woocommerce_products_cache'
+    AND column_name = 'updated_at'
+  ) THEN
+    ALTER TABLE woocommerce_products_cache ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
+  END IF;
+END $$;
+
+-- ============================================================================
+-- STEP 3: Update ShopRenter products cache table
+-- ============================================================================
+
+-- Add excluded column
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS excluded BOOLEAN DEFAULT FALSE;
+
+-- Add inner_id column (ShopRenter has both id and innerId)
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS inner_id TEXT;
+
+-- Add normalized columns (extract from product_data JSONB)
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS name TEXT;
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS sku TEXT;
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS price NUMERIC;
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS currency TEXT;
+ALTER TABLE shoprenter_products_cache ADD COLUMN IF NOT EXISTS categories JSONB;
+
+-- Backfill normalized columns from product_data
+UPDATE shoprenter_products_cache
+SET
+  name = product_data->>'name',
+  sku = product_data->>'sku',
+  price = (product_data->>'price')::numeric,
+  currency = COALESCE(product_data->>'currency', 'HUF'),
+  inner_id = product_data->>'innerId',
+  categories = COALESCE(product_data->'categories', '[]'::jsonb)
+WHERE product_data IS NOT NULL;
+
+-- Add indexes
+CREATE INDEX IF NOT EXISTS idx_shoprenter_products_excluded ON shoprenter_products_cache(store_id, excluded);
+CREATE INDEX IF NOT EXISTS idx_shoprenter_products_name ON shoprenter_products_cache(store_id, name);
+
+-- ============================================================================
+-- STEP 4: Create Shopify products cache table
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS shopify_products_cache (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
+  shopify_product_id TEXT NOT NULL,
+  name TEXT,
+  sku TEXT,
+  price NUMERIC,
+  currency TEXT DEFAULT 'USD',
+  categories JSONB DEFAULT '[]'::jsonb,
+  product_type TEXT,
+  vendor TEXT,
+  tags TEXT[],
+  description TEXT,
+  excluded BOOLEAN DEFAULT FALSE,
+  raw_data JSONB,
+  last_synced_at TIMESTAMPTZ DEFAULT NOW(),
+  created_at TIMESTAMPTZ DEFAULT NOW(),
+  updated_at TIMESTAMPTZ DEFAULT NOW(),
+  CONSTRAINT unique_shopify_product UNIQUE(store_id, shopify_product_id)
+);
+
+-- Indexes
+CREATE INDEX IF NOT EXISTS idx_shopify_products_store_id ON shopify_products_cache(store_id);
+CREATE INDEX IF NOT EXISTS idx_shopify_products_excluded ON shopify_products_cache(store_id, excluded);
+CREATE INDEX IF NOT EXISTS idx_shopify_products_name ON shopify_products_cache(store_id, name);
+CREATE INDEX IF NOT EXISTS idx_shopify_products_sku ON shopify_products_cache(store_id, sku);
+
+-- Enable RLS
+ALTER TABLE shopify_products_cache ENABLE ROW LEVEL SECURITY;
+
+-- RLS Policies
+CREATE POLICY "Users can view their shopify products"
+  ON shopify_products_cache FOR SELECT
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = shopify_products_cache.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+CREATE POLICY "Service role can manage shopify products"
+  ON shopify_products_cache FOR ALL
+  TO service_role
+  USING (true)
+  WITH CHECK (true);
+
+-- ============================================================================
+-- STEP 5: Migrate data from store_data_exclusions to cache tables
+-- ============================================================================
+
+-- Migrate WooCommerce products
+UPDATE woocommerce_products_cache wc
+SET excluded = NOT sde.is_enabled
+FROM store_data_exclusions sde
+WHERE sde.store_id = wc.store_id
+  AND sde.data_type = 'product'
+  AND sde.data_id = wc.wc_product_id;
+
+-- Migrate ShopRenter products
+UPDATE shoprenter_products_cache sr
+SET excluded = NOT sde.is_enabled
+FROM store_data_exclusions sde
+WHERE sde.store_id = sr.store_id
+  AND sde.data_type = 'product'
+  AND sde.data_id = sr.shoprenter_product_id;
+
+-- ============================================================================
+-- STEP 6: Create helper functions
+-- ============================================================================
+
+-- Function to check if product is excluded by category
+CREATE OR REPLACE FUNCTION is_product_excluded_by_category(
+  p_store_id UUID,
+  p_categories JSONB,
+  p_platform TEXT
+)
+RETURNS BOOLEAN AS $$
+DECLARE
+  v_category JSONB;
+  v_category_id TEXT;
+BEGIN
+  -- If categories is null or empty, return false
+  IF p_categories IS NULL OR jsonb_array_length(p_categories) = 0 THEN
+    RETURN FALSE;
+  END IF;
+
+  -- Check each category
+  FOR v_category IN SELECT * FROM jsonb_array_elements(p_categories)
+  LOOP
+    -- Extract category ID (format varies by platform)
+    v_category_id := COALESCE(
+      v_category->>'id',
+      v_category->>'category_id',
+      v_category::text
+    );
+
+    -- Check if this category is excluded
+    IF EXISTS (
+      SELECT 1 FROM excluded_categories
+      WHERE store_id = p_store_id
+        AND category_id = v_category_id
+        AND platform = p_platform
+    ) THEN
+      RETURN TRUE;
+    END IF;
+  END LOOP;
+
+  RETURN FALSE;
+END;
+$$ LANGUAGE plpgsql STABLE;
+
+-- Function to get all categories for a store (aggregated from products)
+CREATE OR REPLACE FUNCTION get_store_categories(
+  p_store_id UUID,
+  p_platform TEXT
+)
+RETURNS TABLE (
+  category_id TEXT,
+  category_name TEXT,
+  product_count INTEGER,
+  is_excluded BOOLEAN
+) AS $$
+BEGIN
+  IF p_platform = 'woocommerce' THEN
+    RETURN QUERY
+    SELECT DISTINCT
+      cat->>'id' AS category_id,
+      cat->>'name' AS category_name,
+      COUNT(*)::INTEGER AS product_count,
+      EXISTS(
+        SELECT 1 FROM excluded_categories ec
+        WHERE ec.store_id = p_store_id
+          AND ec.category_id = cat->>'id'
+          AND ec.platform = p_platform
+      ) AS is_excluded
+    FROM woocommerce_products_cache wc,
+         jsonb_array_elements(wc.categories) AS cat
+    WHERE wc.store_id = p_store_id
+      AND wc.categories IS NOT NULL
+    GROUP BY cat->>'id', cat->>'name';
+
+  ELSIF p_platform = 'shopify' THEN
+    RETURN QUERY
+    SELECT DISTINCT
+      cat->>'id' AS category_id,
+      cat->>'name' AS category_name,
+      COUNT(*)::INTEGER AS product_count,
+      EXISTS(
+        SELECT 1 FROM excluded_categories ec
+        WHERE ec.store_id = p_store_id
+          AND ec.category_id = cat->>'id'
+          AND ec.platform = p_platform
+      ) AS is_excluded
+    FROM shopify_products_cache sc,
+         jsonb_array_elements(sc.categories) AS cat
+    WHERE sc.store_id = p_store_id
+      AND sc.categories IS NOT NULL
+    GROUP BY cat->>'id', cat->>'name';
+
+  ELSIF p_platform = 'shoprenter' THEN
+    RETURN QUERY
+    SELECT DISTINCT
+      cat->>'id' AS category_id,
+      cat->>'name' AS category_name,
+      COUNT(*)::INTEGER AS product_count,
+      EXISTS(
+        SELECT 1 FROM excluded_categories ec
+        WHERE ec.store_id = p_store_id
+          AND ec.category_id = cat->>'id'
+          AND ec.platform = p_platform
+      ) AS is_excluded
+    FROM shoprenter_products_cache sr,
+         jsonb_array_elements(sr.categories) AS cat
+    WHERE sr.store_id = p_store_id
+      AND sr.categories IS NOT NULL
+    GROUP BY cat->>'id', cat->>'name';
+  END IF;
+END;
+$$ LANGUAGE plpgsql STABLE;
+
+-- Function to toggle category exclusion
+CREATE OR REPLACE FUNCTION toggle_category_exclusion(
+  p_store_id UUID,
+  p_category_id TEXT,
+  p_category_name TEXT,
+  p_platform TEXT,
+  p_exclude BOOLEAN
+)
+RETURNS VOID AS $$
+BEGIN
+  IF p_exclude THEN
+    -- Add to excluded categories
+    INSERT INTO excluded_categories (store_id, category_id, category_name, platform)
+    VALUES (p_store_id, p_category_id, p_category_name, p_platform)
+    ON CONFLICT (store_id, category_id, platform) DO NOTHING;
+  ELSE
+    -- Remove from excluded categories
+    DELETE FROM excluded_categories
+    WHERE store_id = p_store_id
+      AND category_id = p_category_id
+      AND platform = p_platform;
+  END IF;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+-- ============================================================================
+-- STEP 7: Drop store_data_exclusions table and related functions
+-- ============================================================================
+
+-- Drop policies first
+DROP POLICY IF EXISTS "Users can view their store data exclusions" ON store_data_exclusions;
+DROP POLICY IF EXISTS "Users can insert their store data exclusions" ON store_data_exclusions;
+DROP POLICY IF EXISTS "Users can update their store data exclusions" ON store_data_exclusions;
+DROP POLICY IF EXISTS "Users can delete their store data exclusions" ON store_data_exclusions;
+
+-- Drop trigger
+DROP TRIGGER IF EXISTS trigger_update_store_data_exclusions_updated_at ON store_data_exclusions;
+
+-- Drop functions
+DROP FUNCTION IF EXISTS update_store_data_exclusions_updated_at();
+DROP FUNCTION IF EXISTS toggle_data_exclusion(UUID, TEXT, TEXT, BOOLEAN);
+DROP FUNCTION IF EXISTS get_enabled_data_items(UUID, TEXT);
+DROP FUNCTION IF EXISTS get_excluded_data_items(UUID, TEXT);
+
+-- Drop indexes
+DROP INDEX IF EXISTS idx_store_data_exclusions_store_id;
+DROP INDEX IF EXISTS idx_store_data_exclusions_store_data_type;
+DROP INDEX IF EXISTS idx_store_data_exclusions_is_enabled;
+DROP INDEX IF EXISTS idx_store_data_exclusions_store_type_enabled;
+
+-- Drop table
+DROP TABLE IF EXISTS store_data_exclusions;
+
+-- ============================================================================
+-- STEP 8: Create view for products with computed exclusion status
+-- ============================================================================
+
+-- WooCommerce products with effective exclusion status
+CREATE OR REPLACE VIEW woocommerce_products_with_exclusion AS
+SELECT
+  wc.*,
+  (wc.excluded OR is_product_excluded_by_category(wc.store_id, wc.categories, 'woocommerce')) AS effectively_excluded,
+  CASE
+    WHEN wc.excluded THEN 'individual'
+    WHEN is_product_excluded_by_category(wc.store_id, wc.categories, 'woocommerce') THEN 'category'
+    ELSE NULL
+  END AS exclusion_reason
+FROM woocommerce_products_cache wc;
+
+-- ShopRenter products with effective exclusion status
+CREATE OR REPLACE VIEW shoprenter_products_with_exclusion AS
+SELECT
+  sr.*,
+  (sr.excluded OR is_product_excluded_by_category(sr.store_id, sr.categories, 'shoprenter')) AS effectively_excluded,
+  CASE
+    WHEN sr.excluded THEN 'individual'
+    WHEN is_product_excluded_by_category(sr.store_id, sr.categories, 'shoprenter') THEN 'category'
+    ELSE NULL
+  END AS exclusion_reason
+FROM shoprenter_products_cache sr;
+
+-- Shopify products with effective exclusion status
+CREATE OR REPLACE VIEW shopify_products_with_exclusion AS
+SELECT
+  sc.*,
+  (sc.excluded OR is_product_excluded_by_category(sc.store_id, sc.categories, 'shopify')) AS effectively_excluded,
+  CASE
+    WHEN sc.excluded THEN 'individual'
+    WHEN is_product_excluded_by_category(sc.store_id, sc.categories, 'shopify') THEN 'category'
+    ELSE NULL
+  END AS exclusion_reason
+FROM shopify_products_cache sc;
+
+-- ============================================================================
+-- Migration Complete
+-- ============================================================================
+
+DO $$
+BEGIN
+  RAISE NOTICE '=================================================================';
+  RAISE NOTICE 'Product Exclusion Refactoring Migration Completed Successfully';
+  RAISE NOTICE '=================================================================';
+  RAISE NOTICE '';
+  RAISE NOTICE 'Changes Applied:';
+  RAISE NOTICE '  ✓ Created excluded_categories table for category-level exclusions';
+  RAISE NOTICE '  ✓ Added excluded column to all *_products_cache tables';
+  RAISE NOTICE '  ✓ Normalized shoprenter_products_cache (extracted fields from JSONB)';
+  RAISE NOTICE '  ✓ Created shopify_products_cache table';
+  RAISE NOTICE '  ✓ Migrated data from store_data_exclusions to cache tables';
+  RAISE NOTICE '  ✓ Dropped store_data_exclusions table and related functions';
+  RAISE NOTICE '  ✓ Created helper functions for category-based exclusions';
+  RAISE NOTICE '  ✓ Created views with computed exclusion status';
+  RAISE NOTICE '';
+  RAISE NOTICE 'New Features:';
+  RAISE NOTICE '  • Support for multiple categories per product';
+  RAISE NOTICE '  • Category-level exclusion (exclude all products in a category)';
+  RAISE NOTICE '  • Individual product exclusion (independent of category)';
+  RAISE NOTICE '  • Computed exclusion_reason field (individual vs. category)';
+  RAISE NOTICE '  • Product cache now stores essential data only';
+  RAISE NOTICE '';
+  RAISE NOTICE 'Next Steps:';
+  RAISE NOTICE '1. Update Edge Functions (api, *-sync) to use new structure';
+  RAISE NOTICE '2. Update UI to support category filtering and exclusion';
+  RAISE NOTICE '3. Add Qdrant deletion logic for excluded products';
+  RAISE NOTICE '4. Test complete flow with all three platforms';
+  RAISE NOTICE '';
+END $$;