소스 검색

fix: WooCommerce integration bugs - country filter and storeId error #102

- Fix 'storeId is not defined' error in woocommerce-sync by correcting
  variable reference from storeId to store_id in sync config query
- Add cascading Country/City/Phone number selection to WooCommerceConnect
  component matching the ShopRenter integration implementation
- Auto-detect user's country from browser settings
- Filter phone numbers by selected country and city

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 4 달 전
부모
커밋
f044791b7b
2개의 변경된 파일343개의 추가작업 그리고 14개의 파일을 삭제
  1. 342 13
      shopcall.ai-main/src/components/WooCommerceConnect.tsx
  2. 1 1
      supabase/functions/woocommerce-sync/index.ts

+ 342 - 13
shopcall.ai-main/src/components/WooCommerceConnect.tsx

@@ -1,12 +1,28 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Loader2, ShoppingBag, ExternalLink, CheckCircle2, AlertCircle, Key } from "lucide-react";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Loader2, ShoppingBag, ExternalLink, CheckCircle2, AlertCircle, Key, Phone } from "lucide-react";
 import { API_URL } from "@/lib/config";
-import { PhoneNumberSelector } from "./PhoneNumberSelector";
+
+interface PhoneNumber {
+  id: string;
+  phone_number: string;
+  country_code: string;
+  country_name: string;
+  location: string;
+  price: number;
+  currency: string;
+  is_available: boolean;
+}
+
+interface Country {
+  country_code: string;
+  country_name: string;
+}
 
 interface WooCommerceConnectProps {
   onClose?: () => void;
@@ -16,13 +32,208 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
   const [storeUrl, setStoreUrl] = useState("");
   const [consumerKey, setConsumerKey] = useState("");
   const [consumerSecret, setConsumerSecret] = useState("");
-  const [phoneNumberId, setPhoneNumberId] = useState("");
   const [isConnecting, setIsConnecting] = useState(false);
   const [error, setError] = useState("");
   const [phoneNumberError, setPhoneNumberError] = useState("");
   const [success, setSuccess] = useState(false);
   const [successMessage, setSuccessMessage] = useState("");
 
+  // Phone number selection state with cascading selectors
+  const [countries, setCountries] = useState<Country[]>([]);
+  const [cities, setCities] = useState<string[]>([]);
+  const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>([]);
+  const [selectedCountry, setSelectedCountry] = useState<string>('');
+  const [selectedCity, setSelectedCity] = useState<string>('');
+  const [selectedPhoneNumber, setSelectedPhoneNumber] = useState<string>('');
+  const [loadingCountries, setLoadingCountries] = useState(false);
+  const [loadingCities, setLoadingCities] = useState(false);
+  const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
+
+  // Detect browser/user country using various methods
+  const detectBrowserCountry = (): string | null => {
+    try {
+      // Method 1: Try to get from browser's navigator.language
+      const browserLang = navigator.language || (navigator as any).userLanguage;
+      if (browserLang) {
+        const parts = browserLang.split('-');
+        if (parts.length >= 2) {
+          return parts[1].toUpperCase();
+        }
+      }
+
+      // Method 2: Try Intl.DateTimeFormat
+      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+      const timezoneCountryMap: { [key: string]: string } = {
+        'Europe/Budapest': 'HU',
+        'Europe/London': 'GB',
+        'America/New_York': 'US',
+        'America/Los_Angeles': 'US',
+        'America/Chicago': 'US',
+        'Europe/Berlin': 'DE',
+        'Europe/Paris': 'FR',
+        'Europe/Rome': 'IT',
+        'Europe/Madrid': 'ES',
+        'Europe/Vienna': 'AT',
+        'Europe/Brussels': 'BE',
+        'Europe/Amsterdam': 'NL',
+        'Europe/Warsaw': 'PL',
+        'America/Toronto': 'CA',
+        'America/Vancouver': 'CA'
+      };
+
+      if (timezone && timezoneCountryMap[timezone]) {
+        return timezoneCountryMap[timezone];
+      }
+    } catch (err) {
+      console.error('Error detecting browser country:', err);
+    }
+
+    return null;
+  };
+
+  // Fetch available countries on mount
+  useEffect(() => {
+    const fetchCountries = async () => {
+      setLoadingCountries(true);
+      try {
+        const sessionData = localStorage.getItem('session_data');
+        if (!sessionData) return;
+
+        const session = JSON.parse(sessionData);
+        const response = await fetch(`${API_URL}/api/phone-numbers?group_by=countries`, {
+          method: 'GET',
+          headers: {
+            'Authorization': `Bearer ${session.session.access_token}`,
+            'Content-Type': 'application/json'
+          }
+        });
+
+        if (response.ok) {
+          const data = await response.json();
+          setCountries(data.countries || []);
+
+          // Determine which country to auto-select
+          let countryToSelect = data.user_country;
+
+          // If user doesn't have a country set, try browser detection
+          if (!countryToSelect) {
+            countryToSelect = detectBrowserCountry();
+            console.log('[WooCommerce] Browser detected country:', countryToSelect);
+          }
+
+          if (countryToSelect) {
+            // Check if the country is in the available countries list
+            const hasCountry = (data.countries || []).some(
+              (c: Country) => c.country_code === countryToSelect
+            );
+            if (hasCountry) {
+              setSelectedCountry(countryToSelect);
+            }
+          }
+        } else {
+          console.error('Failed to fetch countries:', response.status);
+        }
+      } catch (err) {
+        console.error('Error fetching countries:', err);
+      } finally {
+        setLoadingCountries(false);
+      }
+    };
+
+    fetchCountries();
+  }, []);
+
+  // Fetch cities when country is selected
+  useEffect(() => {
+    const fetchCities = async () => {
+      if (!selectedCountry) {
+        setCities([]);
+        setSelectedCity('');
+        setPhoneNumbers([]);
+        setSelectedPhoneNumber('');
+        return;
+      }
+
+      setLoadingCities(true);
+      setCities([]);
+      setSelectedCity('');
+      setPhoneNumbers([]);
+      setSelectedPhoneNumber('');
+
+      try {
+        const sessionData = localStorage.getItem('session_data');
+        if (!sessionData) return;
+
+        const session = JSON.parse(sessionData);
+        const response = await fetch(`${API_URL}/api/phone-numbers?group_by=cities&country=${selectedCountry}`, {
+          method: 'GET',
+          headers: {
+            'Authorization': `Bearer ${session.session.access_token}`,
+            'Content-Type': 'application/json'
+          }
+        });
+
+        if (response.ok) {
+          const data = await response.json();
+          setCities(data.cities || []);
+        } else {
+          console.error('Failed to fetch cities:', response.status);
+        }
+      } catch (err) {
+        console.error('Error fetching cities:', err);
+      } finally {
+        setLoadingCities(false);
+      }
+    };
+
+    fetchCities();
+  }, [selectedCountry]);
+
+  // Fetch phone numbers when city is selected
+  useEffect(() => {
+    const fetchPhoneNumbers = async () => {
+      if (!selectedCountry || !selectedCity) {
+        setPhoneNumbers([]);
+        setSelectedPhoneNumber('');
+        return;
+      }
+
+      setLoadingPhoneNumbers(true);
+      setPhoneNumbers([]);
+      setSelectedPhoneNumber('');
+
+      try {
+        const sessionData = localStorage.getItem('session_data');
+        if (!sessionData) return;
+
+        const session = JSON.parse(sessionData);
+        const response = await fetch(
+          `${API_URL}/api/phone-numbers?available=true&country=${selectedCountry}&city=${encodeURIComponent(selectedCity)}`,
+          {
+            method: 'GET',
+            headers: {
+              'Authorization': `Bearer ${session.session.access_token}`,
+              'Content-Type': 'application/json'
+            }
+          }
+        );
+
+        if (response.ok) {
+          const data = await response.json();
+          setPhoneNumbers(data.phone_numbers || []);
+        } else {
+          console.error('Failed to fetch phone numbers:', response.status);
+        }
+      } catch (err) {
+        console.error('Error fetching phone numbers:', err);
+      } finally {
+        setLoadingPhoneNumbers(false);
+      }
+    };
+
+    fetchPhoneNumbers();
+  }, [selectedCountry, selectedCity]);
+
   const handleManualConnect = async () => {
     setError("");
     setPhoneNumberError("");
@@ -44,9 +255,9 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
       return;
     }
 
-    if (!phoneNumberId) {
+    if (!selectedPhoneNumber) {
       setPhoneNumberError("Please select a phone number for this store");
-      setError("Please select a phone number before connecting");
+      setError("Please select a country, city, and phone number before connecting");
       return;
     }
 
@@ -99,7 +310,7 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
             storeUrl: normalizedUrl,
             consumerKey: consumerKey.trim(),
             consumerSecret: consumerSecret.trim(),
-            phoneNumberId: phoneNumberId
+            phoneNumberId: selectedPhoneNumber
           })
         }
       );
@@ -219,12 +430,130 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
               />
             </div>
 
-            <PhoneNumberSelector
-              value={phoneNumberId}
-              onChange={setPhoneNumberId}
-              disabled={isConnecting}
-              error={phoneNumberError}
-            />
+            {/* Phone Number Selection - Cascading: Country → City → Phone */}
+            <div className="space-y-4 border border-slate-600 rounded-lg p-4 bg-slate-700/30">
+              <h4 className="text-white font-medium flex items-center gap-2">
+                <Phone className="w-4 h-4 text-cyan-500" />
+                Select Phone Number
+              </h4>
+
+              {/* Country Selector */}
+              <div className="space-y-2">
+                <Label className="text-slate-300">
+                  Country <span className="text-red-400">*</span>
+                </Label>
+                {loadingCountries ? (
+                  <div className="flex items-center justify-center py-3">
+                    <Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
+                    <span className="ml-2 text-sm text-slate-400">Loading countries...</span>
+                  </div>
+                ) : countries.length > 0 ? (
+                  <Select value={selectedCountry} onValueChange={setSelectedCountry} disabled={isConnecting}>
+                    <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedCountry ? 'border-red-500/50' : ''}`}>
+                      <SelectValue placeholder="Select a country" />
+                    </SelectTrigger>
+                    <SelectContent className="bg-slate-700 border-slate-600">
+                      {countries.map((country) => (
+                        <SelectItem key={country.country_code} value={country.country_code} className="text-white hover:bg-slate-600">
+                          {country.country_name}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                ) : (
+                  <div className="bg-red-500/10 border border-red-500/30 rounded-md p-3">
+                    <p className="text-sm text-red-400">
+                      No countries with available phone numbers.
+                    </p>
+                  </div>
+                )}
+              </div>
+
+              {/* City Selector - shown after country selection */}
+              {selectedCountry && (
+                <div className="space-y-2">
+                  <Label className="text-slate-300">
+                    City <span className="text-red-400">*</span>
+                  </Label>
+                  {loadingCities ? (
+                    <div className="flex items-center justify-center py-3">
+                      <Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
+                      <span className="ml-2 text-sm text-slate-400">Loading cities...</span>
+                    </div>
+                  ) : cities.length > 0 ? (
+                    <Select value={selectedCity} onValueChange={setSelectedCity} disabled={isConnecting}>
+                      <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedCity ? 'border-red-500/50' : ''}`}>
+                        <SelectValue placeholder="Select a city" />
+                      </SelectTrigger>
+                      <SelectContent className="bg-slate-700 border-slate-600">
+                        {cities.map((city) => (
+                          <SelectItem key={city} value={city} className="text-white hover:bg-slate-600">
+                            {city}
+                          </SelectItem>
+                        ))}
+                      </SelectContent>
+                    </Select>
+                  ) : (
+                    <div className="bg-yellow-500/10 border border-yellow-500/30 rounded-md p-3">
+                      <p className="text-sm text-yellow-400">
+                        No cities with available phone numbers in this country.
+                      </p>
+                    </div>
+                  )}
+                </div>
+              )}
+
+              {/* Phone Number Selector - shown after city selection */}
+              {selectedCity && (
+                <div className="space-y-2">
+                  <Label className="text-slate-300">
+                    Phone Number <span className="text-red-400">*</span>
+                  </Label>
+                  {loadingPhoneNumbers ? (
+                    <div className="flex items-center justify-center py-3">
+                      <Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
+                      <span className="ml-2 text-sm text-slate-400">Loading phone numbers...</span>
+                    </div>
+                  ) : phoneNumbers.length > 0 ? (
+                    <>
+                      <Select value={selectedPhoneNumber} onValueChange={setSelectedPhoneNumber} disabled={isConnecting}>
+                        <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedPhoneNumber ? 'border-red-500/50' : ''} ${phoneNumberError ? 'border-red-500' : ''}`}>
+                          <SelectValue placeholder="Select a phone number" />
+                        </SelectTrigger>
+                        <SelectContent className="bg-slate-700 border-slate-600">
+                          {phoneNumbers.map((phone) => (
+                            <SelectItem key={phone.id} value={phone.id} className="text-white hover:bg-slate-600">
+                              <div className="flex items-center justify-between w-full">
+                                <span>{phone.phone_number}</span>
+                                <span className="ml-2 text-sm text-slate-400">
+                                  {phone.price > 0 ? `$${phone.price}/mo` : 'Free'}
+                                </span>
+                              </div>
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                      {phoneNumberError && (
+                        <p className="text-xs text-red-400">{phoneNumberError}</p>
+                      )}
+                    </>
+                  ) : (
+                    <div className="bg-red-500/10 border border-red-500/30 rounded-md p-3">
+                      <p className="text-sm text-red-400">
+                        No phone numbers available in this city.
+                      </p>
+                    </div>
+                  )}
+                </div>
+              )}
+
+              {/* Show message when not all selections made */}
+              {!selectedPhoneNumber && countries.length > 0 && (
+                <p className="text-xs text-slate-400">
+                  Please select a country, city, and phone number to continue.
+                </p>
+              )}
+            </div>
           </div>
 
           <Button

+ 1 - 1
supabase/functions/woocommerce-sync/index.ts

@@ -936,7 +936,7 @@ serve(wrapHandler('woocommerce-sync', async (req) => {
       const { data: syncConfig } = await supabaseAdmin
         .from('store_sync_config')
         .select('products_access_policy, orders_access_policy, customers_access_policy')
-        .eq('store_id', storeId)
+        .eq('store_id', store_id)
         .single()
 
       // Only sync if policy is 'sync' (not 'api_only' or 'not_allowed')