Bladeren bron

feat: add phone number selection to WooCommerce store connection #65

- Create PhoneNumberSelector component for phone number selection UI
- Add phone number field to WooCommerceConnect form with validation
- Update oauth-woocommerce Edge Function to handle phoneNumberId
- Validate phone number availability before store creation
- Assign phone number to store after successful connection
- Show free vs premium phone numbers with pricing
- Filter phone numbers by user's country
- Display error when no phone numbers available
Claude 5 maanden geleden
bovenliggende
commit
c80e26872b

+ 197 - 0
shopcall.ai-main/src/components/PhoneNumberSelector.tsx

@@ -0,0 +1,197 @@
+import { useState, useEffect } from "react";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { PhoneCall, AlertCircle, Loader2, DollarSign } from "lucide-react";
+import { API_URL } from "@/lib/config";
+
+interface PhoneNumber {
+  id: string;
+  phone_number: string;
+  country_code: string;
+  country_name: string;
+  location: string;
+  phone_type: string;
+  price: number;
+  is_available: boolean;
+}
+
+interface PhoneNumberSelectorProps {
+  value: string;
+  onChange: (phoneNumberId: string) => void;
+  disabled?: boolean;
+  error?: string;
+}
+
+export function PhoneNumberSelector({ value, onChange, disabled, error }: PhoneNumberSelectorProps) {
+  const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [fetchError, setFetchError] = useState<string>("");
+  const [userCountry, setUserCountry] = useState<string | null>(null);
+
+  useEffect(() => {
+    fetchPhoneNumbers();
+  }, []);
+
+  const fetchPhoneNumbers = async () => {
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        setFetchError('Authentication required. Please log in again.');
+        setLoading(false);
+        return;
+      }
+
+      const session = JSON.parse(sessionData);
+      const response = await fetch(`${API_URL}/api/phone-numbers`, {
+        headers: {
+          'Authorization': `Bearer ${session.session.access_token}`,
+          'Content-Type': 'application/json'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to fetch phone numbers');
+      }
+
+      const data = await response.json();
+      if (data.success) {
+        setPhoneNumbers(data.phone_numbers || []);
+        setUserCountry(data.user_country);
+
+        // Check if there are no available phone numbers
+        const availableNumbers = data.phone_numbers.filter((pn: PhoneNumber) => pn.is_available);
+        if (availableNumbers.length === 0) {
+          setFetchError(
+            data.user_country
+              ? `No available phone numbers for your country (${data.user_country}). Please contact support.`
+              : 'No available phone numbers. Please contact support.'
+          );
+        }
+      }
+    } catch (err) {
+      console.error('Error fetching phone numbers:', err);
+      setFetchError('Failed to load phone numbers. Please try again.');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const formatPhoneNumber = (pn: PhoneNumber) => {
+    const price = pn.price > 0 ? ` ($${pn.price}/mo)` : ' (Free)';
+    return `${pn.phone_number} - ${pn.location}${price}`;
+  };
+
+  const selectedPhone = phoneNumbers.find(pn => pn.id === value);
+
+  if (loading) {
+    return (
+      <div className="space-y-2">
+        <Label className="text-white">Phone Number</Label>
+        <div className="flex items-center gap-2 p-3 bg-slate-700 border border-slate-600 rounded-md">
+          <Loader2 className="w-4 h-4 text-slate-400 animate-spin" />
+          <span className="text-slate-400">Loading available phone numbers...</span>
+        </div>
+      </div>
+    );
+  }
+
+  if (fetchError) {
+    return (
+      <div className="space-y-2">
+        <Label className="text-white">Phone Number</Label>
+        <Alert className="bg-red-500/10 border-red-500/50">
+          <AlertCircle className="h-4 w-4 text-red-500" />
+          <AlertDescription className="text-red-500">
+            {fetchError}
+          </AlertDescription>
+        </Alert>
+      </div>
+    );
+  }
+
+  const availableNumbers = phoneNumbers.filter(pn => pn.is_available);
+
+  if (availableNumbers.length === 0) {
+    return (
+      <div className="space-y-2">
+        <Label className="text-white">Phone Number</Label>
+        <Alert className="bg-red-500/10 border-red-500/50">
+          <AlertCircle className="h-4 w-4 text-red-500" />
+          <AlertDescription className="text-red-500">
+            No available phone numbers for your country{userCountry ? ` (${userCountry})` : ''}. Please contact support to add more phone numbers.
+          </AlertDescription>
+        </Alert>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-2">
+      <Label htmlFor="phoneNumber" className="text-white">
+        Phone Number <span className="text-red-500">*</span>
+      </Label>
+      <Select value={value} onValueChange={onChange} disabled={disabled}>
+        <SelectTrigger
+          id="phoneNumber"
+          className={`bg-slate-700 border-slate-600 text-white ${error ? 'border-red-500' : ''}`}
+        >
+          <SelectValue placeholder="Select a phone number for this store">
+            {selectedPhone ? (
+              <div className="flex items-center gap-2">
+                <PhoneCall className="w-4 h-4 text-cyan-500" />
+                <span className="font-mono">{selectedPhone.phone_number}</span>
+                <span className="text-slate-400">- {selectedPhone.location}</span>
+                {selectedPhone.price > 0 ? (
+                  <span className="text-purple-400 flex items-center gap-1">
+                    <DollarSign className="w-3 h-3" />
+                    {selectedPhone.price}/mo
+                  </span>
+                ) : (
+                  <span className="text-green-400">(Free)</span>
+                )}
+              </div>
+            ) : null}
+          </SelectValue>
+        </SelectTrigger>
+        <SelectContent className="bg-slate-800 border-slate-700">
+          {availableNumbers.map((pn) => (
+            <SelectItem
+              key={pn.id}
+              value={pn.id}
+              className="text-white hover:bg-slate-700 focus:bg-slate-700"
+            >
+              <div className="flex items-center gap-2">
+                <PhoneCall className="w-4 h-4 text-cyan-500" />
+                <span className="font-mono">{pn.phone_number}</span>
+                <span className="text-slate-400">- {pn.location}</span>
+                {pn.price > 0 ? (
+                  <span className="text-purple-400 flex items-center gap-1">
+                    <DollarSign className="w-3 h-3" />
+                    {pn.price}/mo
+                  </span>
+                ) : (
+                  <span className="text-green-400">(Free)</span>
+                )}
+              </div>
+            </SelectItem>
+          ))}
+        </SelectContent>
+      </Select>
+      {error && (
+        <p className="text-sm text-red-500">{error}</p>
+      )}
+      <p className="text-sm text-slate-400">
+        Select a phone number for AI-powered calls to this store's customers
+        {userCountry && ` (showing ${userCountry} numbers)`}
+      </p>
+      {selectedPhone && selectedPhone.price === 0 && (
+        <Alert className="bg-green-500/10 border-green-500/50">
+          <AlertDescription className="text-green-500 text-sm">
+            This is a free phone number with no monthly fees.
+          </AlertDescription>
+        </Alert>
+      )}
+    </div>
+  );
+}

+ 19 - 1
shopcall.ai-main/src/components/WooCommerceConnect.tsx

@@ -6,6 +6,7 @@ import { Label } from "@/components/ui/label";
 import { Alert, AlertDescription } from "@/components/ui/alert";
 import { Loader2, ShoppingBag, ExternalLink, CheckCircle2, AlertCircle, Key } from "lucide-react";
 import { API_URL } from "@/lib/config";
+import { PhoneNumberSelector } from "./PhoneNumberSelector";
 
 interface WooCommerceConnectProps {
   onClose?: () => void;
@@ -15,13 +16,16 @@ 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("");
 
   const handleManualConnect = async () => {
     setError("");
+    setPhoneNumberError("");
     setSuccess(false);
 
     // Validate inputs
@@ -40,6 +44,12 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
       return;
     }
 
+    if (!phoneNumberId) {
+      setPhoneNumberError("Please select a phone number for this store");
+      setError("Please select a phone number before connecting");
+      return;
+    }
+
     // Normalize URL
     let normalizedUrl = storeUrl.trim();
 
@@ -88,7 +98,8 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
           body: JSON.stringify({
             storeUrl: normalizedUrl,
             consumerKey: consumerKey.trim(),
-            consumerSecret: consumerSecret.trim()
+            consumerSecret: consumerSecret.trim(),
+            phoneNumberId: phoneNumberId
           })
         }
       );
@@ -207,6 +218,13 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
                 disabled={isConnecting}
               />
             </div>
+
+            <PhoneNumberSelector
+              value={phoneNumberId}
+              onChange={setPhoneNumberId}
+              disabled={isConnecting}
+              error={phoneNumberError}
+            />
           </div>
 
           <Button

+ 49 - 1
supabase/functions/oauth-woocommerce/index.ts

@@ -133,7 +133,7 @@ serve(wrapHandler('oauth-woocommerce', async (req) => {
 
     // Handle manual API key connection
     if (action === 'connect_manual') {
-      const { storeUrl: storeUrlParam, consumerKey, consumerSecret } = await req.json()
+      const { storeUrl: storeUrlParam, consumerKey, consumerSecret, phoneNumberId } = await req.json()
 
       if (!storeUrlParam || !consumerKey || !consumerSecret) {
         return new Response(
@@ -142,6 +142,13 @@ serve(wrapHandler('oauth-woocommerce', async (req) => {
         )
       }
 
+      if (!phoneNumberId) {
+        return new Response(
+          JSON.stringify({ error: 'phoneNumberId is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
       // Validate store URL
       const validation = validateStoreUrl(storeUrlParam)
       if (!validation.valid) {
@@ -197,6 +204,24 @@ serve(wrapHandler('oauth-woocommerce', async (req) => {
       // Create Supabase admin client
       const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
 
+      // Verify phone number is available and belongs to user's country
+      const { data: phoneNumber, error: phoneError } = await supabaseAdmin
+        .from('phone_numbers')
+        .select('*')
+        .eq('id', phoneNumberId)
+        .eq('is_available', true)
+        .eq('is_active', true)
+        .is('assigned_to_store_id', null)
+        .single()
+
+      if (phoneError || !phoneNumber) {
+        console.error('[WooCommerce] Phone number validation failed:', phoneError)
+        return new Response(
+          JSON.stringify({ error: 'Selected phone number is not available' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
       // Store credentials in database
       const { data: insertedStore, error: insertError } = await supabaseAdmin
         .from('stores')
@@ -207,6 +232,7 @@ serve(wrapHandler('oauth-woocommerce', async (req) => {
           store_url: validation.normalized,
           api_key: consumerKey,
           api_secret: consumerSecret,
+          phone_number_id: phoneNumberId,
           scopes: ['read'],
           data_access_permissions: {
             allow_product_access: true
@@ -230,6 +256,28 @@ serve(wrapHandler('oauth-woocommerce', async (req) => {
         )
       }
 
+      // Assign phone number to store
+      const { error: phoneUpdateError } = await supabaseAdmin
+        .from('phone_numbers')
+        .update({
+          assigned_to_store_id: insertedStore.id,
+          assigned_to_user_id: user.id,
+          is_available: false,
+          assigned_at: new Date().toISOString(),
+          updated_at: new Date().toISOString()
+        })
+        .eq('id', phoneNumberId)
+
+      if (phoneUpdateError) {
+        console.error('[WooCommerce] Error assigning phone number:', phoneUpdateError)
+        // Delete the store since phone assignment failed
+        await supabaseAdmin.from('stores').delete().eq('id', insertedStore.id)
+        return new Response(
+          JSON.stringify({ error: 'Failed to assign phone number to store' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
       console.log(`[WooCommerce] Store connected successfully (manual): ${storeName}`)
 
       // Trigger auto-sync in background