diff --git a/customer-spa/src/pages/Account/AffiliateDashboard.tsx b/customer-spa/src/pages/Account/AffiliateDashboard.tsx new file mode 100644 index 0000000..9a72878 --- /dev/null +++ b/customer-spa/src/pages/Account/AffiliateDashboard.tsx @@ -0,0 +1,643 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '@/lib/api/client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Copy, CheckCircle, Activity, DollarSign, ChevronRight, Clock, Info, Wallet, CreditCard } from 'lucide-react'; +import { toast } from 'sonner'; +import { formatPrice, getCurrencySettings } from '@/lib/currency'; + +// Affiliate types +interface AffiliateProfile { + status: 'active' | 'pending' | 'approved' | 'rejected'; + referral_code: string; + commission_rate: number; + custom_commission_rate: number | null; + global_commission_rate: number; +} + +interface AffiliateReferral { + id: number; + status: 'pending' | 'approved' | 'rejected'; + commission_amount: string; + created_at: string; + order_id: number; + currency: string; + approved_at?: string; + cancelled_reason?: string; +} + +interface AffiliatePayout { + id: number; + amount: string; + currency: string; + method: string; + status: string; + notes: string; + created_at: string; + completed_at: string; +} + +interface PaymentDetails { + payment_method: string; + payment_details: { + bank_name?: string; + account_number?: string; + account_holder?: string; + swift_code?: string; + bank_address?: string; + email?: string; + name?: string; + notes?: string; + }; +} + +interface AffiliateSettings { + woonoow_affiliate_payment_methods?: string[]; +} + +// Payment method labels +const PAYMENT_METHOD_LABELS: Record = { + bank_transfer: 'Bank Transfer', + paypal: 'PayPal', + wise: 'Wise', + skrill: 'Skrill', + payoneer: 'Payoneer', + custom: 'Custom (Other)', +}; + +// Format amount using site's currency settings +function formatAmount(amount: number | string, currency?: string): string { + const amountNum = typeof amount === 'string' ? parseFloat(amount) : amount; + const settings = getCurrencySettings(); + + const decimals = currency === 'IDR' ? 0 : settings.decimals; + + const formatted = formatNumberWithSeparators(amountNum, decimals, settings.thousandSeparator, settings.decimalSeparator); + return `${settings.symbol}${formatted}`; +} + +function formatNumberWithSeparators( + value: number, + decimals: number, + thousandSeparator: string, + decimalSeparator: string +): string { + const rounded = value.toFixed(decimals); + const [integerPart, decimalPart] = rounded.split('.'); + const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator); + + if (decimals > 0 && decimalPart) { + return `${formattedInteger}${decimalSeparator}${decimalPart}`; + } + return formattedInteger; +} + +export default function AffiliateDashboard() { + const queryClient = useQueryClient(); + const [copied, setCopied] = useState(false); + const [showPaymentForm, setShowPaymentForm] = useState(false); + const [selectedMethod, setSelectedMethod] = useState(''); + const [paymentFormData, setPaymentFormData] = useState>({}); + + // Fetch affiliate settings for available payment methods + const { data: settings } = useQuery({ + queryKey: ['affiliate-settings'], + queryFn: async () => { + try { + return await api.get('/modules/affiliate/settings'); + } catch { + return { woonoow_affiliate_payment_methods: ['bank_transfer'] }; + } + }, + }); + + const availableMethods = settings?.woonoow_affiliate_payment_methods || ['bank_transfer']; + + // Fetch dashboard info + const { data: profile, isLoading } = useQuery({ + queryKey: ['affiliate-profile'], + queryFn: async () => { + try { + const response = await api.get('/account/affiliate'); + return response; + } catch (err: any) { + if (err.status === 404) return null; + throw err; + } + }, + retry: false + }); + + // Fetch referrals + const { data: referrals, isLoading: isLoadingReferrals } = useQuery({ + queryKey: ['affiliate-referrals'], + queryFn: async () => { + return await api.get('/account/affiliate/referrals'); + }, + enabled: !!profile && profile.status === 'active' + }); + + // Fetch payout history + const { data: payouts = [], isLoading: isLoadingPayouts } = useQuery({ + queryKey: ['affiliate-payouts'], + queryFn: async () => { + return await api.get('/account/affiliate/payouts'); + }, + enabled: !!profile && profile.status === 'active' + }); + + // Fetch payment details + const { data: savedPaymentDetails } = useQuery({ + queryKey: ['affiliate-payment-details'], + queryFn: async () => { + return await api.get('/account/affiliate/payment-details'); + }, + enabled: !!profile && profile.status === 'active' + }); + + // Initialize form when data loads OR when edit mode is opened + const initFormFromSaved = React.useCallback(() => { + if (savedPaymentDetails?.payment_method) { + setSelectedMethod(savedPaymentDetails.payment_method); + setPaymentFormData(savedPaymentDetails.payment_details || {}); + } + }, [savedPaymentDetails]); + + // Initialize when saved data changes and form is open + React.useEffect(() => { + if (showPaymentForm && savedPaymentDetails) { + initFormFromSaved(); + } + }, [showPaymentForm, savedPaymentDetails, initFormFromSaved]); + + const updatePaymentMutation = useMutation({ + mutationFn: async (data: { payment_method: string; payment_details: Record }) => { + return await api.post('/account/affiliate/payment-details', data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['affiliate-payment-details'] }); + toast.success('Payment details saved successfully!'); + setShowPaymentForm(false); + }, + onError: () => { + toast.error('Failed to save payment details.'); + } + }); + + const applyMutation = useMutation({ + mutationFn: async () => { + return await api.post('/account/affiliate/apply'); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['affiliate-profile'] }); + toast.success('Successfully applied for the affiliate program!'); + }, + onError: () => { + toast.error('Failed to apply. Please try again later.'); + } + }); + + if (isLoading) { + return
Loading...
; + } + + if (!profile) { + return ( +
+

Affiliate Program

+
+

Join our Affiliate Program

+

+ Earn commissions by referring customers to our store! You'll get a unique referral link that tracks any purchases made by your referrals. +

+ +
+
+ ); + } + + if (profile.status === 'pending') { + return ( +
+

Affiliate Program

+
+

Application Pending

+

+ Your application is currently being reviewed. We will notify you once it's approved. +

+
+
+ ); + } + + const woonoowConfig = (window as any).woonoowCustomer || {}; + const basePath = woonoowConfig.basePath || ''; + const shopPath = basePath ? `${basePath}/shop` : '/shop'; + const referralLink = `${window.location.origin}${shopPath}?ref=${profile.referral_code}`; + + const handleCopy = () => { + navigator.clipboard.writeText(referralLink); + setCopied(true); + toast.success('Referral link copied to clipboard!'); + setTimeout(() => setCopied(false), 2000); + }; + + const approvedReferrals = (referrals || []).filter((r: any) => r.status === 'approved'); + const pendingReferrals = (referrals || []).filter((r: any) => r.status === 'pending'); + + const totalEarnings = approvedReferrals.reduce((sum: number, r: any) => sum + parseFloat(r.commission_amount), 0); + const pendingEarnings = pendingReferrals.reduce((sum: number, r: any) => sum + parseFloat(r.commission_amount), 0); + + const handleSavePayment = () => { + if (!selectedMethod) { + toast.error('Please select a payment method'); + return; + } + updatePaymentMutation.mutate({ + payment_method: selectedMethod, + payment_details: paymentFormData + }); + }; + + const renderPaymentFields = () => { + switch (selectedMethod) { + case 'bank_transfer': + return ( +
+
+ + setPaymentFormData({ ...paymentFormData, bank_name: e.target.value })} + placeholder="e.g., Bank Central Asia" + /> +
+
+ + setPaymentFormData({ ...paymentFormData, account_holder: e.target.value })} + placeholder="Full name as on account" + /> +
+
+ + setPaymentFormData({ ...paymentFormData, account_number: e.target.value })} + placeholder="Account number" + /> +
+
+ + setPaymentFormData({ ...paymentFormData, swift_code: e.target.value })} + placeholder="e.g., CENAIDJA (for Indonesian banks)" + /> +
+
+ + setPaymentFormData({ ...paymentFormData, bank_address: e.target.value })} + placeholder="Bank branch address" + /> +
+
+ ); + + case 'paypal': + case 'wise': + case 'skrill': + case 'payoneer': + return ( +
+
+ + setPaymentFormData({ ...paymentFormData, name: e.target.value })} + placeholder="Your name" + /> +
+
+ + setPaymentFormData({ ...paymentFormData, email: e.target.value })} + placeholder="your@email.com" + /> +
+
+ ); + + case 'custom': + return ( +
+ +