diff --git a/src/components/CartDrawer.tsx b/src/components/CartDrawer.tsx index 9818040..2956db0 100644 --- a/src/components/CartDrawer.tsx +++ b/src/components/CartDrawer.tsx @@ -15,6 +15,7 @@ import { } from '@/components/ui/dialog'; import { useCart } from '@/context/CartContext'; import { useToast } from '@/hooks/use-toast'; +import { sendQuoteWebhook } from '@/lib/webhookService'; export function CartDrawer() { const { items, isCartOpen, setIsCartOpen, removeItem, updateQuantity, updateNotes, clearCart } = useCart(); @@ -22,6 +23,7 @@ export function CartDrawer() { const navigate = useNavigate(); const [quoteMessage, setQuoteMessage] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + const [hasSubmitted, setHasSubmitted] = useState(false); const [showQuoteForm, setShowQuoteForm] = useState(false); const [contactInfo, setContactInfo] = useState({ name: '', @@ -55,42 +57,18 @@ export function CartDrawer() { }; const handleSubmitQuote = async () => { - if (!contactInfo.name || !contactInfo.email) { - toast({ - title: 'Missing information', - description: 'Please provide your name and email.', - variant: 'destructive', - }); - return; - } + if (hasSubmitted) return; setIsSubmitting(true); - const quoteRequest = { - items: items.map(item => ({ - productId: item.product.id, - name: item.product.name, - sku: item.product.sku, - quantity: item.quantity, - notes: item.notes, - })), - user: { - email: contactInfo.email, - name: contactInfo.name, - phone: contactInfo.phone, - company: contactInfo.company, - }, - message: quoteMessage, - timestamp: new Date().toISOString(), - }; - - try { - console.log('Quote Request:', quoteRequest); - await new Promise(resolve => setTimeout(resolve, 1000)); + const result = await sendQuoteWebhook(contactInfo, items); + if (result.success) { + setHasSubmitted(true); toast({ - title: 'Quote Request Sent! ✓', - description: 'Our team will contact you within 24 hours with pricing.', + title: 'Quote Request Submitted ✓', + description: + 'Your quote request has been submitted successfully. Our team will review your request and contact you soon.', }); clearCart(); @@ -98,15 +76,16 @@ export function CartDrawer() { setContactInfo({ name: '', email: '', phone: '', company: '' }); setShowQuoteForm(false); setIsCartOpen(false); - } catch { + setHasSubmitted(false); + } else { toast({ title: 'Error sending quote', - description: 'Please try again or contact us directly.', + description: result.error || 'Please try again or contact us directly.', variant: 'destructive', }); - } finally { - setIsSubmitting(false); } + + setIsSubmitting(false); }; return ( diff --git a/src/lib/webhookService.ts b/src/lib/webhookService.ts new file mode 100644 index 0000000..a7b1853 --- /dev/null +++ b/src/lib/webhookService.ts @@ -0,0 +1,144 @@ +import { CartItem } from '@/types'; + +const WEBHOOK_URL = 'https://n8nmac.apphub.co.za/webhook/wfsales_quotes'; +const TIMEOUT_MS = 10_000; + +interface CustomerInfo { + name: string; + email: string; + phone: string; + company: string; +} + +interface WebhookPayload { + source: string; + request_type: string; + quote_request_id: string; + customer: { + name: string; + email: string; + phone: string; + company: string; + }; + cart: { + cart_id: string; + currency: string; + subtotal: number; + items: { + sku: string; + description: string; + quantity: number; + unit: string; + }[]; + }; + metadata: { + website: string; + submitted_at: string; + }; +} + +function generateUUID(): string { + return crypto.randomUUID(); +} + +function isValidEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +export function validateQuoteRequest( + customer: CustomerInfo, + items: CartItem[] +): { valid: boolean; error?: string } { + if (!customer.name.trim()) { + return { valid: false, error: 'Customer name is required.' }; + } + if (!customer.email.trim() || !isValidEmail(customer.email.trim())) { + return { valid: false, error: 'A valid email address is required.' }; + } + if (items.length === 0) { + return { valid: false, error: 'Cart is empty.' }; + } + return { valid: true }; +} + +export function buildPayload( + customer: CustomerInfo, + items: CartItem[] +): WebhookPayload { + return { + source: 'online_shop', + request_type: 'quote_request', + quote_request_id: generateUUID(), + customer: { + name: customer.name.trim(), + email: customer.email.trim(), + phone: customer.phone?.trim() || '', + company: customer.company?.trim() || '', + }, + cart: { + cart_id: generateUUID(), + currency: 'ZAR', + subtotal: 0, + items: items.map((item) => ({ + sku: item.product.sku || '', + description: item.product.name, + quantity: item.quantity || 1, + unit: 'each', + })), + }, + metadata: { + website: 'Q - PORTAL', + submitted_at: new Date().toISOString(), + }, + }; +} + +export async function sendQuoteWebhook( + customer: CustomerInfo, + items: CartItem[] +): Promise<{ success: boolean; error?: string }> { + const validation = validateQuoteRequest(customer, items); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + + const payload = buildPayload(customer, items); + + console.log('📤 Webhook request payload:', JSON.stringify(payload, null, 2)); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS); + + try { + const response = await fetch(WEBHOOK_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }); + + const responseBody = await response.text(); + console.log('📥 Webhook response status:', response.status); + console.log('📥 Webhook response body:', responseBody); + + if (!response.ok) { + return { + success: false, + error: `Webhook returned status ${response.status}. Please try again.`, + }; + } + + return { success: true }; + } catch (err: unknown) { + const message = + err instanceof DOMException && err.name === 'AbortError' + ? 'Request timed out after 10 seconds. Please try again.' + : err instanceof Error + ? err.message + : 'Unknown error'; + console.error('❌ Webhook error:', message); + return { success: false, error: message }; + } finally { + clearTimeout(timeout); + } +}