feat(customer): add affiliate dashboard entry in account area
This commit is contained in:
643
customer-spa/src/pages/Account/AffiliateDashboard.tsx
Normal file
643
customer-spa/src/pages/Account/AffiliateDashboard.tsx
Normal file
@@ -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<string, string> = {
|
||||
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<Record<string, string>>({});
|
||||
|
||||
// Fetch affiliate settings for available payment methods
|
||||
const { data: settings } = useQuery<AffiliateSettings>({
|
||||
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<AffiliateProfile | null>({
|
||||
queryKey: ['affiliate-profile'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await api.get<AffiliateProfile>('/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<AffiliateReferral[]>({
|
||||
queryKey: ['affiliate-referrals'],
|
||||
queryFn: async () => {
|
||||
return await api.get<AffiliateReferral[]>('/account/affiliate/referrals');
|
||||
},
|
||||
enabled: !!profile && profile.status === 'active'
|
||||
});
|
||||
|
||||
// Fetch payout history
|
||||
const { data: payouts = [], isLoading: isLoadingPayouts } = useQuery<AffiliatePayout[]>({
|
||||
queryKey: ['affiliate-payouts'],
|
||||
queryFn: async () => {
|
||||
return await api.get<AffiliatePayout[]>('/account/affiliate/payouts');
|
||||
},
|
||||
enabled: !!profile && profile.status === 'active'
|
||||
});
|
||||
|
||||
// Fetch payment details
|
||||
const { data: savedPaymentDetails } = useQuery<PaymentDetails>({
|
||||
queryKey: ['affiliate-payment-details'],
|
||||
queryFn: async () => {
|
||||
return await api.get<PaymentDetails>('/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<string, string> }) => {
|
||||
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 <div className="text-center py-8">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<h2 className="text-2xl font-bold">Affiliate Program</h2>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-100 mb-2">Join our Affiliate Program</h3>
|
||||
<p className="text-blue-800 dark:text-blue-200 mb-4">
|
||||
Earn commissions by referring customers to our store! You'll get a unique referral link that tracks any purchases made by your referrals.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => applyMutation.mutate()}
|
||||
disabled={applyMutation.isPending}
|
||||
>
|
||||
{applyMutation.isPending ? 'Applying...' : 'Apply Now'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.status === 'pending') {
|
||||
return (
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<h2 className="text-2xl font-bold">Affiliate Program</h2>
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800/50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">Application Pending</h3>
|
||||
<p className="text-yellow-800 dark:text-yellow-200">
|
||||
Your application is currently being reviewed. We will notify you once it's approved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Bank Name</label>
|
||||
<Input
|
||||
value={paymentFormData.bank_name || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, bank_name: e.target.value })}
|
||||
placeholder="e.g., Bank Central Asia"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Account Holder Name</label>
|
||||
<Input
|
||||
value={paymentFormData.account_holder || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, account_holder: e.target.value })}
|
||||
placeholder="Full name as on account"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Account Number</label>
|
||||
<Input
|
||||
value={paymentFormData.account_number || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, account_number: e.target.value })}
|
||||
placeholder="Account number"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">SWIFT / IBAN Code</label>
|
||||
<Input
|
||||
value={paymentFormData.swift_code || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, swift_code: e.target.value })}
|
||||
placeholder="e.g., CENAIDJA (for Indonesian banks)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Bank Address (Optional)</label>
|
||||
<Input
|
||||
value={paymentFormData.bank_address || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, bank_address: e.target.value })}
|
||||
placeholder="Bank branch address"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'paypal':
|
||||
case 'wise':
|
||||
case 'skrill':
|
||||
case 'payoneer':
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Account Name</label>
|
||||
<Input
|
||||
value={paymentFormData.name || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, name: e.target.value })}
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">{PAYMENT_METHOD_LABELS[selectedMethod]} Email</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={paymentFormData.email || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, email: e.target.value })}
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'custom':
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Payment Instructions</label>
|
||||
<textarea
|
||||
className="w-full px-3 py-2 border rounded-lg bg-background text-sm min-h-[100px]"
|
||||
value={paymentFormData.notes || ''}
|
||||
onChange={(e) => setPaymentFormData({ ...paymentFormData, notes: e.target.value })}
|
||||
placeholder="Describe how you would like to receive payment..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentPaymentDisplay = () => {
|
||||
if (!savedPaymentDetails?.payment_method || !savedPaymentDetails?.payment_details) {
|
||||
return 'Not configured';
|
||||
}
|
||||
|
||||
const method = savedPaymentDetails.payment_method;
|
||||
const details = savedPaymentDetails.payment_details;
|
||||
|
||||
switch (method) {
|
||||
case 'bank_transfer':
|
||||
return `${details.bank_name || 'Bank'} - ${details.account_number || '****'}`;
|
||||
case 'paypal':
|
||||
case 'wise':
|
||||
case 'skrill':
|
||||
case 'payoneer':
|
||||
return details.email || 'Not set';
|
||||
case 'custom':
|
||||
return 'Custom payment';
|
||||
default:
|
||||
return 'Not configured';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Affiliate Dashboard</h2>
|
||||
<p className="text-gray-500 mt-1">Manage your referrals and view your earnings.</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards - Improved styling */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white p-5 rounded-lg border shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<DollarSign className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide">Total Earnings</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatAmount(totalEarnings)}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg border shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Activity className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide">Pending</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-yellow-600">{formatAmount(pendingEarnings)}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg border shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide">Commission Rate</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900">{profile.commission_rate}%</p>
|
||||
{profile.custom_commission_rate ? (
|
||||
<p className="text-xs text-green-600">Custom rate</p>
|
||||
) : (
|
||||
<p className="text-xs text-gray-400">Default (global: {profile.global_commission_rate}%)</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Referral Link */}
|
||||
<div className="bg-white p-6 rounded-lg border">
|
||||
<h3 className="text-lg font-semibold mb-4">Your Referral Link</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={referralLink}
|
||||
readOnly
|
||||
className="bg-gray-50 font-mono text-sm"
|
||||
/>
|
||||
<Button variant="outline" onClick={handleCopy} className="shrink-0 w-32">
|
||||
{copied ? (
|
||||
<><CheckCircle className="w-4 h-4 mr-2" /> Copied</>
|
||||
) : (
|
||||
<><Copy className="w-4 h-4 mr-2" /> Copy Link</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Share this link with your audience. When they make a purchase, you'll earn a {profile.commission_rate}% commission.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Referrals */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Recent Referrals</h3>
|
||||
|
||||
{isLoadingReferrals ? (
|
||||
<div className="text-center py-8 text-gray-500">Loading referrals...</div>
|
||||
) : !referrals || referrals.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 border rounded-lg bg-gray-50">
|
||||
No referrals yet. Share your link to start earning!
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{referrals.map((ref: any) => {
|
||||
const createdDate = new Date(ref.created_at);
|
||||
const approvedDate = ref.approved_at ? new Date(ref.approved_at) : null;
|
||||
|
||||
return (
|
||||
<div key={ref.id} className="bg-white p-4 rounded-lg border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">Order #{ref.order_id}</span>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
ref.status === 'approved'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: ref.status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{ref.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{createdDate.toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
{ref.status === 'approved' && approvedDate && (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Approved {approvedDate.toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{formatAmount(ref.commission_amount, ref.currency)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ref.status === 'rejected' && ref.cancelled_reason && (
|
||||
<div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 rounded-md">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-red-800 dark:text-red-200">
|
||||
Reason: {ref.cancelled_reason}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payout History */}
|
||||
{payouts.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Payout History</h3>
|
||||
<div className="space-y-3">
|
||||
{payouts.map((payout) => {
|
||||
const payoutDate = new Date(payout.created_at);
|
||||
return (
|
||||
<div key={payout.id} className="bg-white p-4 rounded-lg border">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
payout.status === 'completed'
|
||||
? 'bg-green-100 text-green-600'
|
||||
: 'bg-yellow-100 text-yellow-600'
|
||||
}`}>
|
||||
<Wallet className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{formatAmount(payout.amount, payout.currency)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 capitalize">
|
||||
{payout.method.replace('_', ' ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
payout.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{payout.status}
|
||||
</span>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{payoutDate.toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{payout.notes && (
|
||||
<div className="mt-3 pt-3 border-t text-xs text-gray-500">
|
||||
{payout.notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Details Section */}
|
||||
<div className="bg-white p-6 rounded-lg border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
|
||||
<CreditCard className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Payment Details</h3>
|
||||
<p className="text-xs text-gray-500">{getCurrentPaymentDisplay()}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!showPaymentForm && (
|
||||
<Button variant="outline" size="sm" onClick={() => setShowPaymentForm(true)}>
|
||||
{savedPaymentDetails?.payment_method ? 'Edit' : 'Add'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPaymentForm ? (
|
||||
<div className="space-y-4">
|
||||
{/* Payment Method Selection */}
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Payment Method</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableMethods.map((method) => (
|
||||
<button
|
||||
key={method}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedMethod(method);
|
||||
setPaymentFormData({});
|
||||
}}
|
||||
className={`px-4 py-2 rounded-lg border text-sm transition-colors ${
|
||||
selectedMethod === method
|
||||
? 'bg-purple-100 border-purple-500 text-purple-700'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{PAYMENT_METHOD_LABELS[method] || method}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conditional Fields */}
|
||||
{selectedMethod && renderPaymentFields()}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => setShowPaymentForm(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSavePayment}
|
||||
disabled={updatePaymentMutation.isPending || !selectedMethod}
|
||||
>
|
||||
{updatePaymentMutation.isPending ? 'Saving...' : 'Save Payment Details'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">
|
||||
Configure how you want to receive your affiliate payouts.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export function AccountLayout({ children }: AccountLayoutProps) {
|
||||
{ id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin },
|
||||
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
|
||||
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
|
||||
{ id: 'affiliate', label: 'Affiliate', path: '/my-account/affiliate', icon: User },
|
||||
];
|
||||
|
||||
// Filter out wishlist if module disabled or settings disabled, licenses if licensing disabled
|
||||
@@ -63,6 +64,7 @@ export function AccountLayout({ children }: AccountLayoutProps) {
|
||||
if (item.id === 'wishlist') return isEnabled('wishlist') && wishlistEnabled;
|
||||
if (item.id === 'licenses') return isEnabled('licensing');
|
||||
if (item.id === 'subscriptions') return isEnabled('subscription');
|
||||
if (item.id === 'affiliate') return isEnabled('affiliate');
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import Licenses from './Licenses';
|
||||
import LicenseConnect from './LicenseConnect';
|
||||
import Subscriptions from './Subscriptions';
|
||||
import SubscriptionDetail from './SubscriptionDetail';
|
||||
import AffiliateDashboard from './AffiliateDashboard';
|
||||
|
||||
export default function Account() {
|
||||
const user = (window as any).woonoowCustomer?.user;
|
||||
@@ -42,6 +43,7 @@ export default function Account() {
|
||||
<Route path="licenses" element={<Licenses />} />
|
||||
<Route path="subscriptions" element={<Subscriptions />} />
|
||||
<Route path="subscriptions/:id" element={<SubscriptionDetail />} />
|
||||
<Route path="affiliate" element={<AffiliateDashboard />} />
|
||||
<Route path="account-details" element={<AccountDetails />} />
|
||||
<Route path="*" element={<Navigate to="/my-account" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -489,6 +489,7 @@ class AccountController {
|
||||
'total' => html_entity_decode(strip_tags(wc_price($order->get_total()))),
|
||||
'currency' => $order->get_currency(),
|
||||
'payment_method_title' => $payment_title,
|
||||
'items_count' => $order->get_item_count(),
|
||||
];
|
||||
|
||||
if ($detailed) {
|
||||
|
||||
Reference in New Issue
Block a user