complete api integration

This commit is contained in:
S M Fahim Hossen 2025-10-09 15:08:05 +06:00
parent 085417ab7e
commit b7b4b4d3c5
10 changed files with 1267 additions and 437 deletions

2
.env
View file

@ -1 +1 @@
VITE_SERVER_URL=https://backend.planpostai.com/api/v2 VITE_SERVER_URL=https://backendv2.planpostai.com/api/v2

View file

@ -0,0 +1,276 @@
import React, { useState } from "react";
import { X, CreditCard, Smartphone } from "lucide-react";
import { usePaymentStore } from "@/stores/paymentStore";
import toast from "react-hot-toast";
interface PaymentMethodModalProps {
isOpen: boolean;
onClose: () => void;
}
const PaymentMethodModal: React.FC<PaymentMethodModalProps> = ({
isOpen,
onClose,
}) => {
const [paymentType, setPaymentType] = useState<"bkash" | "bank">("bkash");
const [isProcessing, setIsProcessing] = useState(false);
const { savePaymentInfo } = usePaymentStore();
// Bkash form state
const [bkashNumber, setBkashNumber] = useState("");
// Bank form state
const [bankName, setBankName] = useState("");
const [accountNumber, setAccountNumber] = useState("");
const [branch, setBranch] = useState("");
const [routing, setRouting] = useState("");
const [accountName, setAccountName] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsProcessing(true);
try {
const paymentData: Record<string, string> = {};
if (paymentType === "bkash") {
if (!bkashNumber || bkashNumber.length < 11) {
toast.error("Please enter a valid 11-digit Bkash number");
setIsProcessing(false);
return;
}
paymentData.bkash_number = bkashNumber;
} else {
if (
!bankName ||
!accountNumber ||
!branch ||
!routing ||
!accountName
) {
toast.error("Please fill in all bank details");
setIsProcessing(false);
return;
}
paymentData.bank_details = `Bank Name: ${bankName}, Account No: ${accountNumber}, Branch: ${branch}, Routing: ${routing}, Account Name: ${accountName}`;
}
const result = await savePaymentInfo(paymentData);
if (result.success) {
toast.success("Payment method added successfully!");
onClose();
// Reset form
setBkashNumber("");
setBankName("");
setAccountNumber("");
setBranch("");
setRouting("");
setAccountName("");
} else {
toast.error(result.error || "Failed to add payment method");
}
} catch {
toast.error("An error occurred. Please try again.");
} finally {
setIsProcessing(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-h-[90vh] overflow-y-auto max-w-md w-full">
{/* Modal Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-gray-100">
<div>
<h3 className="text-lg sm:text-xl font-bold text-gray-900">
Add Payment Method
</h3>
<p className="text-xs sm:text-sm text-gray-600">
Choose your preferred payment method
</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-600" />
</button>
</div>
{/* Modal Body */}
<form
onSubmit={handleSubmit}
className="p-4 sm:p-6 space-y-4 sm:space-y-5"
>
{/* Payment Type Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Payment Method
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setPaymentType("bkash")}
className={`p-4 border-2 rounded-lg transition-all ${
paymentType === "bkash"
? "border-pink-500 bg-pink-50"
: "border-gray-200 hover:border-gray-300"
}`}
>
<Smartphone
className={`w-6 h-6 mx-auto mb-2 ${
paymentType === "bkash" ? "text-pink-600" : "text-gray-400"
}`}
/>
<div className="text-sm font-semibold text-gray-900">Bkash</div>
</button>
<button
type="button"
onClick={() => setPaymentType("bank")}
className={`p-4 border-2 rounded-lg transition-all ${
paymentType === "bank"
? "border-indigo-500 bg-indigo-50"
: "border-gray-200 hover:border-gray-300"
}`}
>
<CreditCard
className={`w-6 h-6 mx-auto mb-2 ${
paymentType === "bank" ? "text-indigo-600" : "text-gray-400"
}`}
/>
<div className="text-sm font-semibold text-gray-900">Bank</div>
</button>
</div>
</div>
{/* Bkash Form */}
{paymentType === "bkash" && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Bkash Number
</label>
<input
type="tel"
value={bkashNumber}
onChange={(e) => setBkashNumber(e.target.value)}
placeholder="01XXXXXXXXX"
maxLength={11}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none"
required
/>
<p className="text-xs text-gray-500 mt-1">
Enter your 11-digit Bkash number
</p>
</div>
)}
{/* Bank Form */}
{paymentType === "bank" && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Bank Name
</label>
<input
type="text"
value={bankName}
onChange={(e) => setBankName(e.target.value)}
placeholder="e.g., BRAC Bank"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Account Name
</label>
<input
type="text"
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
placeholder="Full name on account"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Account Number
</label>
<input
type="text"
value={accountNumber}
onChange={(e) => setAccountNumber(e.target.value)}
placeholder="Enter account number"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Branch
</label>
<input
type="text"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="e.g., Gulshan"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Routing Number
</label>
<input
type="text"
value={routing}
onChange={(e) => setRouting(e.target.value)}
placeholder="Routing #"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
</div>
</div>
)}
</form>
{/* Modal Footer */}
<div className="p-4 sm:p-6 border-t border-gray-100 flex gap-3">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors font-semibold text-gray-700"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={isProcessing}
className="flex-1 px-4 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isProcessing ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Saving...
</>
) : (
"Add Payment Method"
)}
</button>
</div>
</div>
</div>
);
};
export default PaymentMethodModal;

View file

@ -10,7 +10,7 @@ const DashboardLayout: React.FC = () => {
<SidebarInset> <SidebarInset>
<div className="flex-1"> <div className="flex-1">
{/* Mobile Header with Sidebar Trigger */} {/* Mobile Header with Sidebar Trigger */}
<header className="sticky top-0 z-10 flex h-[70px] items-center gap-2 px-2"> <header className="sticky top-0 z-10 flex h-[70px] w-[50px] items-center gap-2 px-2">
<SidebarTrigger className="hover:bg-gray-100" /> <SidebarTrigger className="hover:bg-gray-100" />
</header> </header>

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { import {
DollarSign, DollarSign,
TrendingUp, TrendingUp,
@ -12,6 +12,7 @@ import {
CheckCircle, CheckCircle,
X, X,
AlertCircle, AlertCircle,
XCircle,
} from "lucide-react"; } from "lucide-react";
import { import {
XAxis, XAxis,
@ -22,89 +23,104 @@ import {
Area, Area,
AreaChart, AreaChart,
} from "recharts"; } from "recharts";
import { useAffiliateStore } from "@/stores/affiliateStore";
import { useWithdrawalStore } from "@/stores/withdrawStore";
import PaymentMethodModal from "@/components/PaymentModal";
import toast from "react-hot-toast";
const Earnings: React.FC = () => { const Earnings: React.FC = () => {
const [timeframe, setTimeframe] = useState<"7d" | "30d" | "90d" | "1y">( const [timeframe, setTimeframe] = useState<"7d" | "30d" | "90d" | "1y">(
"30d" "30d"
); );
const [showPayoutModal, setShowPayoutModal] = useState(false); const [showPayoutModal, setShowPayoutModal] = useState(false);
const [payoutAmount, setPayoutAmount] = useState("890"); const [payoutAmount, setPayoutAmount] = useState("");
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [withdrawalNote, setWithdrawalNote] = useState("");
const earningsData = [ const {
{ date: "Sep 1", amount: 120 }, data: affiliateData,
{ date: "Sep 5", amount: 180 }, fetchEarnings,
{ date: "Sep 10", amount: 240 }, isLoading: earningsLoading,
{ date: "Sep 15", amount: 210 }, } = useAffiliateStore();
{ date: "Sep 20", amount: 290 }, const {
{ date: "Sep 25", amount: 350 }, withdrawals,
{ date: "Sep 30", amount: 450 }, fetchWithdrawals,
]; requestWithdrawal,
isLoading: withdrawalsLoading,
} = useWithdrawalStore();
const transactionData = [ useEffect(() => {
{ const userStr = localStorage.getItem("aff-user");
type: "Commission", if (userStr) {
amount: "$120", const userData = JSON.parse(userStr);
date: "Sep 28, 2025", if (userData.id) {
status: "Completed", fetchEarnings(userData.id);
ref: "REF-4521", fetchWithdrawals();
}, }
{ }
type: "Bonus", }, [fetchEarnings, fetchWithdrawals]);
amount: "$50",
date: "Sep 25, 2025", // Get summary data
status: "Completed", const totalEarnings = affiliateData?.summary.total_earning || 0;
ref: "BONUS-892", const totalWithdraw = affiliateData?.summary.total_withdraw || 0;
}, // Convert negative pending amount to positive
{ const pendingAmount = Math.abs(affiliateData?.summary.pending_amount || 0);
type: "Commission", const availableBalance = Math.max(totalEarnings - totalWithdraw, 0);
amount: "$85",
date: "Sep 22, 2025", // Calculate monthly earnings (last transaction)
status: "Completed", const lastTransaction = affiliateData?.history[0];
ref: "REF-4489", const monthlyEarnings = lastTransaction?.amount || 0;
},
{ // Transform affiliate history to chart data
type: "Commission", const earningsData =
amount: "$95", affiliateData?.history
date: "Sep 18, 2025", .slice()
status: "Pending", .reverse()
ref: "REF-4455", .map((item) => ({
}, date: new Date(item.created_at).toLocaleDateString("en-US", {
{ month: "short",
type: "Commission", day: "numeric",
amount: "$100", }),
date: "Sep 15, 2025", amount: item.amount,
status: "Completed", })) || [];
ref: "REF-4421",
}, // Calculate stats
]; const totalTransactions = affiliateData?.total || 0;
const avgCommission =
totalTransactions > 0 ? totalEarnings / totalTransactions : 0;
const stats = [ const stats = [
{ {
label: "Total Earnings", label: "Total Earnings",
value: "$12,450", value: `${totalEarnings.toFixed(2)}`,
change: "+12.5%", change:
trend: "up", totalWithdraw > 0
? `${totalWithdraw.toFixed(2)} withdrawn`
: "No withdrawals yet",
trend: "up" as const,
icon: DollarSign, icon: DollarSign,
color: "from-green-500 to-emerald-600", color: "from-green-500 to-emerald-600",
bgColor: "bg-green-50", bgColor: "bg-green-50",
textColor: "text-green-600", textColor: "text-green-600",
}, },
{ {
label: "This Month", label: "Last Transaction",
value: "$2,340", value: `${monthlyEarnings.toFixed(2)}`,
change: "+8.2%", change: lastTransaction
trend: "up", ? new Date(lastTransaction.created_at).toLocaleDateString()
: "No transactions",
trend: "up" as const,
icon: TrendingUp, icon: TrendingUp,
color: "from-blue-500 to-indigo-600", color: "from-blue-500 to-indigo-600",
bgColor: "bg-blue-50", bgColor: "bg-blue-50",
textColor: "text-blue-600", textColor: "text-blue-600",
}, },
{ {
label: "Pending Payout", label: "Pending Amount",
value: "$450", value: `${Math.max(pendingAmount, 0).toFixed(2)}`,
change: "Oct 15", change: "To be processed",
trend: "neutral", trend: "neutral" as const,
icon: Clock, icon: Clock,
color: "from-orange-500 to-amber-600", color: "from-orange-500 to-amber-600",
bgColor: "bg-orange-50", bgColor: "bg-orange-50",
@ -112,9 +128,9 @@ const Earnings: React.FC = () => {
}, },
{ {
label: "Available Balance", label: "Available Balance",
value: "$890", value: `${availableBalance.toFixed(2)}`,
change: "Ready now", change: availableBalance >= 50 ? "Ready now" : `Minimum $50 required`,
trend: "neutral", trend: "neutral" as const,
icon: Wallet, icon: Wallet,
color: "from-purple-500 to-pink-600", color: "from-purple-500 to-pink-600",
bgColor: "bg-purple-50", bgColor: "bg-purple-50",
@ -122,17 +138,94 @@ const Earnings: React.FC = () => {
}, },
]; ];
const handlePayoutRequest = () => { const handlePayoutRequest = async () => {
const amount = Number(payoutAmount);
// Validation
if (amount < 50) {
toast.error("Minimum withdrawal amount is ৳50");
return;
}
if (amount > availableBalance) {
toast.error("Insufficient balance");
return;
}
if (
!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number
) {
toast.error("Please add a payment method first");
return;
}
setIsProcessing(true); setIsProcessing(true);
setTimeout(() => {
try {
const note =
withdrawalNote.trim() || `Withdrawal request for ৳${amount.toFixed(2)}`;
const result = await requestWithdrawal(amount, note);
if (result.success) {
toast.success("Withdrawal request submitted successfully!");
setShowPayoutModal(false);
setPayoutAmount("");
setWithdrawalNote(""); // Clear the note
// Refresh withdrawals and earnings data
fetchWithdrawals();
const userStr = localStorage.getItem("aff-user");
if (userStr) {
const userData = JSON.parse(userStr);
if (userData.id) {
fetchEarnings(userData.id);
}
}
} else {
toast.error(result.error || "Failed to submit withdrawal request");
}
} catch {
toast.error("An error occurred. Please try again.");
} finally {
setIsProcessing(false); setIsProcessing(false);
setShowPayoutModal(false); }
alert(
"Payout request submitted successfully! You'll receive confirmation via email."
);
}, 2000);
}; };
const getStatusIcon = (status: string) => {
switch (status) {
case "paid":
return <CheckCircle className="w-3 h-3" />;
case "cancelled":
return <XCircle className="w-3 h-3" />;
default:
return <Clock className="w-3 h-3" />;
}
};
const getStatusStyle = (status: string) => {
switch (status) {
case "paid":
return "bg-green-100 text-green-700";
case "cancelled":
return "bg-red-100 text-red-700";
default:
return "bg-yellow-100 text-yellow-700";
}
};
const isLoading = earningsLoading || withdrawalsLoading;
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
);
}
return ( return (
<div className="min-h-screen bg-gray-50 p-4 sm:p-6"> <div className="min-h-screen bg-gray-50 p-4 sm:p-6">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
@ -152,8 +245,12 @@ const Earnings: React.FC = () => {
<span className="hidden sm:inline">Export</span> <span className="hidden sm:inline">Export</span>
</button> </button>
<button <button
onClick={() => setShowPayoutModal(true)} onClick={() => {
className="flex-1 sm:flex-initial px-4 sm:px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all shadow-md flex items-center justify-center gap-2 text-sm sm:text-base" setPayoutAmount(availableBalance.toFixed(2));
setShowPayoutModal(true);
}}
disabled={availableBalance < 50}
className="flex-1 sm:flex-initial px-4 sm:px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all shadow-md flex items-center justify-center gap-2 text-sm sm:text-base disabled:opacity-50 disabled:cursor-not-allowed"
> >
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5" /> <CreditCard className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline">Request Payout</span> <span className="hidden sm:inline">Request Payout</span>
@ -188,9 +285,6 @@ const Earnings: React.FC = () => {
) : ( ) : (
<ArrowDownRight className="w-3 h-3 sm:w-4 sm:h-4" /> <ArrowDownRight className="w-3 h-3 sm:w-4 sm:h-4" />
)} )}
<span className="text-xs sm:text-sm font-semibold">
{stat.change}
</span>
</div> </div>
)} )}
</div> </div>
@ -200,9 +294,7 @@ const Earnings: React.FC = () => {
<div className="text-xs sm:text-sm text-gray-600"> <div className="text-xs sm:text-sm text-gray-600">
{stat.label} {stat.label}
</div> </div>
{stat.trend === "neutral" && ( <div className="text-xs text-gray-500 mt-1">{stat.change}</div>
<div className="text-xs text-gray-500 mt-1">{stat.change}</div>
)}
</div> </div>
))} ))}
</div> </div>
@ -212,28 +304,32 @@ const Earnings: React.FC = () => {
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex items-start gap-3 sm:gap-4"> <div className="flex items-start gap-3 sm:gap-4">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-white bg-opacity-20 backdrop-blur-sm rounded-xl flex items-center justify-center flex-shrink-0"> <div className="w-12 h-12 sm:w-14 sm:h-14 bg-white bg-opacity-20 backdrop-blur-sm rounded-xl flex items-center justify-center flex-shrink-0">
<Calendar className="w-6 h-6 sm:w-7 sm:h-7 text-black" /> <Calendar className="w-6 h-6 sm:w-7 sm:h-7 text-white" />
</div> </div>
<div> <div>
<div className="text-indigo-100 text-xs sm:text-sm mb-1"> <div className="text-indigo-100 text-xs sm:text-sm mb-1">
Next Scheduled Payout Total Withdrawn
</div> </div>
<div className="text-xl sm:text-3xl font-bold text-white mb-2"> <div className="text-xl sm:text-3xl font-bold text-white mb-2">
October 15, 2025 {totalWithdraw.toFixed(2)}
</div> </div>
<div className="text-indigo-100 text-xs sm:text-sm"> <div className="text-indigo-100 text-xs sm:text-sm">
Your earnings will be processed automatically {withdrawals?.total || 0} withdrawal requests
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white bg-opacity-20 backdrop-blur-sm rounded-xl p-4 text-center md:text-right"> <div className="bg-white bg-opacity-20 backdrop-blur-sm rounded-xl p-4 text-center md:text-right">
<div className="text-black text-xs sm:text-sm mb-1"> <div className="text-black text-xs sm:text-sm mb-1">
Estimated Amount Available Balance
</div> </div>
<div className="text-3xl sm:text-4xl font-bold text-black"> <div className="text-3xl sm:text-4xl font-bold text-black">
$450 {availableBalance.toFixed(2)}
</div>
<div className="text-black text-xs mt-1">
{availableBalance >= 50
? "Ready to withdraw"
: "Minimum ৳50 required"}
</div> </div>
<div className="text-black text-xs mt-1">+ pending approvals</div>
</div> </div>
</div> </div>
</div> </div>
@ -266,65 +362,131 @@ const Earnings: React.FC = () => {
))} ))}
</div> </div>
</div> </div>
<ResponsiveContainer width="100%" height={250}> {earningsData.length > 0 ? (
<AreaChart data={earningsData}> <ResponsiveContainer width="100%" height={250}>
<defs> <AreaChart data={earningsData}>
<linearGradient id="colorAmount" x1="0" y1="0" x2="0" y2="1"> <defs>
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} /> <linearGradient
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} /> id="colorAmount"
</linearGradient> x1="0"
</defs> y1="0"
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" /> x2="0"
<XAxis y2="1"
dataKey="date" >
stroke="#9ca3af" <stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
style={{ fontSize: "10px" }} <stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
/> </linearGradient>
<YAxis stroke="#9ca3af" style={{ fontSize: "10px" }} /> </defs>
<Tooltip <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
contentStyle={{ <XAxis
backgroundColor: "#fff", dataKey="date"
border: "1px solid #e5e7eb", stroke="#9ca3af"
borderRadius: "8px", style={{ fontSize: "10px" }}
boxShadow: "0 4px 6px rgba(0,0,0,0.1)", />
}} <YAxis stroke="#9ca3af" style={{ fontSize: "10px" }} />
/> <Tooltip
<Area contentStyle={{
type="monotone" backgroundColor: "#fff",
dataKey="amount" border: "1px solid #e5e7eb",
stroke="#6366f1" borderRadius: "8px",
strokeWidth={2} boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
fill="url(#colorAmount)" }}
/> />
</AreaChart> <Area
</ResponsiveContainer> type="monotone"
dataKey="amount"
stroke="#6366f1"
strokeWidth={2}
fill="url(#colorAmount)"
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-gray-500">
No earnings data available
</div>
)}
</div> </div>
{/* Payment Methods */}
{/* Payment Methods */} {/* Payment Methods */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 mb-4"> <h2 className="text-lg sm:text-xl font-semibold text-gray-900 mb-4">
Payment Method Payment Methods
</h2> </h2>
<div className="space-y-3"> <div className="space-y-3">
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50"> {/* Bank Details */}
<div className="flex items-center gap-3 mb-2"> {affiliateData?.account_info?.bank_details && (
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" /> <div className="flex items-start gap-3">
</div> <div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center flex-shrink-0">
<div className="flex-1 min-w-0"> <CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
<div className="font-semibold text-gray-900 text-sm sm:text-base">
PayPal
</div> </div>
<div className="text-xs sm:text-sm text-gray-600 truncate"> <div className="flex-1 min-w-0">
john@example.com <div className="font-semibold text-gray-900 text-sm sm:text-base mb-1">
Bank Account
</div>
<div className="text-xs sm:text-sm text-gray-600">
{affiliateData.account_info.bank_details}
</div>
</div> </div>
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600 flex-shrink-0" />
</div> </div>
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600 flex-shrink-0" />
</div> </div>
</div> )}
<button className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-indigo-600 hover:text-indigo-600 transition-colors font-medium text-sm sm:text-base">
+ Add Payment Method {/* Bkash Number */}
</button> {affiliateData?.account_info?.bkash_number && (
<div className="p-3 sm:p-4 border-2 border-pink-600 rounded-lg bg-pink-50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-pink-600 rounded-lg flex items-center justify-center flex-shrink-0">
<svg
className="w-5 h-5 text-white"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
Bkash
</div>
<div className="text-xs sm:text-sm text-gray-600 truncate">
{affiliateData.account_info.bkash_number}
</div>
</div>
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-pink-600 flex-shrink-0" />
</div>
</div>
)}
{/* Add Payment Method Button - Only show if no payment methods exist */}
{!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number && (
<div className="p-4 border-2 border-dashed border-gray-300 rounded-lg text-center">
<p className="text-gray-500 text-sm mb-3">
No payment method added yet
</p>
<button
onClick={() => setShowPaymentModal(true)}
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-indigo-600 hover:text-indigo-600 transition-colors font-medium text-sm sm:text-base"
>
+ Add Payment Method
</button>
</div>
)}
{/* Add Another Payment Method - Show if at least one exists */}
{(affiliateData?.account_info?.bank_details ||
affiliateData?.account_info?.bkash_number) && (
<button
onClick={() => setShowPaymentModal(true)}
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-indigo-600 hover:text-indigo-600 transition-colors font-medium text-sm sm:text-base"
>
+ Add Another Payment Method
</button>
)}
</div> </div>
<div className="mt-4 sm:mt-6 pt-4 sm:pt-6 border-t border-gray-100"> <div className="mt-4 sm:mt-6 pt-4 sm:pt-6 border-t border-gray-100">
@ -337,7 +499,7 @@ const Earnings: React.FC = () => {
Total Transactions Total Transactions
</span> </span>
<span className="font-semibold text-gray-900 text-sm sm:text-base"> <span className="font-semibold text-gray-900 text-sm sm:text-base">
142 {totalTransactions}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
@ -345,15 +507,15 @@ const Earnings: React.FC = () => {
Avg. Commission Avg. Commission
</span> </span>
<span className="font-semibold text-gray-900 text-sm sm:text-base"> <span className="font-semibold text-gray-900 text-sm sm:text-base">
$87.68 {avgCommission.toFixed(2)}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-xs sm:text-sm text-gray-600"> <span className="text-xs sm:text-sm text-gray-600">
Success Rate Withdrawals
</span> </span>
<span className="font-semibold text-green-600 text-sm sm:text-base"> <span className="font-semibold text-gray-900 text-sm sm:text-base">
98.2% {withdrawals?.total || 0}
</span> </span>
</div> </div>
</div> </div>
@ -365,80 +527,86 @@ const Earnings: React.FC = () => {
<div className="bg-white rounded-xl shadow-sm border border-gray-100"> <div className="bg-white rounded-xl shadow-sm border border-gray-100">
<div className="p-4 sm:p-6 border-b border-gray-100"> <div className="p-4 sm:p-6 border-b border-gray-100">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900"> <h2 className="text-lg sm:text-xl font-semibold text-gray-900">
Recent Transactions Withdrawal History
</h2> </h2>
<p className="text-xs sm:text-sm text-gray-600 mt-1"> <p className="text-xs sm:text-sm text-gray-600 mt-1">
Your latest earnings and payouts Your withdrawal requests and status
</p> </p>
</div> </div>
<div className="divide-y divide-gray-100"> <div className="divide-y divide-gray-100">
{transactionData.map((transaction, idx) => ( {withdrawals?.data && withdrawals.data.length > 0 ? (
<div withdrawals.data.map((transaction, idx) => (
key={idx} <div
className="p-4 sm:p-6 hover:bg-gray-50 transition-colors" key={idx}
> className="p-4 sm:p-6 hover:bg-gray-50 transition-colors"
<div className="flex items-center justify-between gap-4"> >
<div className="flex items-center gap-3 sm:gap-4 min-w-0"> <div className="flex items-center justify-between gap-4">
<div <div className="flex items-center gap-3 sm:gap-4 min-w-0">
className={`w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${ <div
transaction.type === "Bonus" className={`w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
? "bg-purple-100" transaction.status === "paid"
: "bg-green-100" ? "bg-green-100"
}`} : transaction.status === "cancelled"
> ? "bg-red-100"
<DollarSign : "bg-yellow-100"
className={`w-5 h-5 sm:w-6 sm:h-6 ${
transaction.type === "Bonus"
? "text-purple-600"
: "text-green-600"
}`} }`}
/> >
</div> <DollarSign
<div className="min-w-0"> className={`w-5 h-5 sm:w-6 sm:h-6 ${
<div className="font-semibold text-gray-900 text-sm sm:text-base"> transaction.status === "paid"
{transaction.type} ? "text-green-600"
: transaction.status === "cancelled"
? "text-red-600"
: "text-yellow-600"
}`}
/>
</div> </div>
<div className="text-xs sm:text-sm text-gray-600"> <div className="min-w-0">
{transaction.date} <div className="font-semibold text-gray-900 text-sm sm:text-base">
</div> Withdrawal Request
<div className="text-xs text-gray-500 mt-1"> </div>
{transaction.ref} <div className="text-xs sm:text-sm text-gray-600">
{new Date(
transaction.created_at
).toLocaleDateString()}
</div>
<div className="text-xs text-gray-500 mt-1">
{transaction.note}
</div>
</div> </div>
</div> </div>
</div> <div className="text-right flex-shrink-0">
<div className="text-right flex-shrink-0"> <div className="text-lg sm:text-2xl font-bold text-gray-900">
<div className="text-lg sm:text-2xl font-bold text-gray-900"> {transaction.amount.toFixed(2)}
{transaction.amount} </div>
</div> <span
<span className={`inline-flex items-center gap-1 px-2 py-0.5 sm:py-1 rounded-full text-xs font-semibold mt-1 ${getStatusStyle(
className={`inline-flex items-center gap-1 px-2 py-0.5 sm:py-1 rounded-full text-xs font-semibold mt-1 ${ transaction.status
transaction.status === "Completed" )}`}
? "bg-green-100 text-green-700" >
: "bg-yellow-100 text-yellow-700" {getStatusIcon(transaction.status)}
}`} <span className="hidden sm:inline capitalize">
> {transaction.status}
{transaction.status === "Completed" ? ( </span>
<CheckCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
<span className="hidden sm:inline">
{transaction.status}
</span> </span>
</span> </div>
</div> </div>
</div> </div>
))
) : (
<div className="p-8 text-center text-gray-500">
No withdrawal history yet
</div> </div>
))} )}
</div>
<div className="p-4 sm:p-6 border-t border-gray-100 text-center">
<button className="text-indigo-600 hover:text-indigo-700 font-semibold text-xs sm:text-sm">
View All Transactions
</button>
</div> </div>
</div> </div>
</div> </div>
<PaymentMethodModal
isOpen={showPaymentModal}
onClose={() => setShowPaymentModal(false)}
/>
{/* Payout Request Modal */} {/* Payout Request Modal */}
{showPayoutModal && ( {showPayoutModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
@ -466,6 +634,7 @@ const Earnings: React.FC = () => {
</button> </button>
</div> </div>
{/* Modal Body */}
{/* Modal Body */} {/* Modal Body */}
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5"> <div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
{/* Available Balance */} {/* Available Balance */}
@ -474,7 +643,7 @@ const Earnings: React.FC = () => {
Available Balance Available Balance
</div> </div>
<div className="text-2xl sm:text-3xl font-bold text-green-900"> <div className="text-2xl sm:text-3xl font-bold text-green-900">
$890.00 {availableBalance.toFixed(2)}
</div> </div>
<div className="text-xs text-green-600 mt-1"> <div className="text-xs text-green-600 mt-1">
Ready for withdrawal Ready for withdrawal
@ -493,18 +662,36 @@ const Earnings: React.FC = () => {
value={payoutAmount} value={payoutAmount}
onChange={(e) => setPayoutAmount(e.target.value)} onChange={(e) => setPayoutAmount(e.target.value)}
placeholder="0.00" placeholder="0.00"
max="890" max={availableBalance}
className="w-full pl-10 pr-16 sm:pr-20 py-2.5 sm:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none text-base sm:text-lg font-semibold" className="w-full pl-10 pr-16 sm:pr-20 py-2.5 sm:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none text-base sm:text-lg font-semibold"
/> />
<button <button
onClick={() => setPayoutAmount("890")} onClick={() => setPayoutAmount(availableBalance.toFixed(2))}
className="absolute right-2 top-1/2 transform -translate-y-1/2 px-2.5 sm:px-3 py-1 bg-indigo-600 text-white text-xs sm:text-sm rounded-md hover:bg-indigo-700 transition-colors" className="absolute right-2 top-1/2 transform -translate-y-1/2 px-2.5 sm:px-3 py-1 bg-indigo-600 text-white text-xs sm:text-sm rounded-md hover:bg-indigo-700 transition-colors"
> >
Max Max
</button> </button>
</div> </div>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
Minimum withdrawal: $50.00 Minimum withdrawal: 50.00
</p>
</div>
{/* Note Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Note (Optional)
</label>
<textarea
value={withdrawalNote}
onChange={(e) => setWithdrawalNote(e.target.value)}
placeholder="Add a note for this withdrawal..."
rows={3}
maxLength={200}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none text-sm resize-none"
/>
<p className="text-xs text-gray-500 mt-1">
{withdrawalNote.length}/200 characters
</p> </p>
</div> </div>
@ -513,22 +700,55 @@ const Earnings: React.FC = () => {
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Payment Method Payment Method
</label> </label>
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50"> {affiliateData?.account_info?.bank_details && (
<div className="flex items-center gap-3"> <div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50 mb-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center"> <div className="flex items-start gap-3">
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" /> <div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
</div> <CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
PayPal
</div> </div>
<div className="text-xs sm:text-sm text-gray-600 truncate"> <div className="flex-1 min-w-0">
john@example.com <div className="font-semibold text-gray-900 text-sm sm:text-base">
Bank Account
</div>
<div className="text-xs sm:text-sm text-gray-600">
{affiliateData.account_info.bank_details}
</div>
</div> </div>
<CheckCircle className="w-5 h-5 text-indigo-600" />
</div> </div>
<CheckCircle className="w-5 h-5 text-indigo-600" />
</div> </div>
</div> )}
{affiliateData?.account_info?.bkash_number && (
<div className="p-3 sm:p-4 border-2 border-pink-600 rounded-lg bg-pink-50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-pink-600 rounded-lg flex items-center justify-center">
<svg
className="w-5 h-5 text-white"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
Bkash
</div>
<div className="text-xs sm:text-sm text-gray-600 truncate">
{affiliateData.account_info.bkash_number}
</div>
</div>
<CheckCircle className="w-5 h-5 text-pink-600" />
</div>
</div>
)}
{!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number && (
<div className="p-4 border-2 border-dashed border-gray-300 rounded-lg text-center text-gray-500 text-sm">
No payment method added. Please add a payment method
first.
</div>
)}
</div> </div>
{/* Processing Info */} {/* Processing Info */}
@ -548,19 +768,19 @@ const Earnings: React.FC = () => {
<div className="flex justify-between text-xs sm:text-sm"> <div className="flex justify-between text-xs sm:text-sm">
<span className="text-gray-600">Withdrawal Amount</span> <span className="text-gray-600">Withdrawal Amount</span>
<span className="font-semibold text-gray-900"> <span className="font-semibold text-gray-900">
${payoutAmount} {Number(payoutAmount).toFixed(2)}
</span> </span>
</div> </div>
<div className="flex justify-between text-xs sm:text-sm"> <div className="flex justify-between text-xs sm:text-sm">
<span className="text-gray-600">Processing Fee</span> <span className="text-gray-600">Processing Fee</span>
<span className="font-semibold text-gray-900">$0.00</span> <span className="font-semibold text-gray-900">0.00</span>
</div> </div>
<div className="border-t border-gray-200 pt-2 flex justify-between"> <div className="border-t border-gray-200 pt-2 flex justify-between">
<span className="font-semibold text-gray-900 text-sm sm:text-base"> <span className="font-semibold text-gray-900 text-sm sm:text-base">
You'll Receive You'll Receive
</span> </span>
<span className="text-lg sm:text-xl font-bold text-indigo-600"> <span className="text-lg sm:text-xl font-bold text-indigo-600">
${payoutAmount} {Number(payoutAmount).toFixed(2)}
</span> </span>
</div> </div>
</div> </div>
@ -579,7 +799,10 @@ const Earnings: React.FC = () => {
disabled={ disabled={
isProcessing || isProcessing ||
Number(payoutAmount) < 50 || Number(payoutAmount) < 50 ||
Number(payoutAmount) > 890 Number(Number(payoutAmount).toFixed(2)) >
Number(availableBalance.toFixed(2)) ||
(!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number)
} }
className="flex-1 px-3 sm:px-4 py-2.5 sm:py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-sm sm:text-base" className="flex-1 px-3 sm:px-4 py-2.5 sm:py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-sm sm:text-base"
> >
@ -589,7 +812,7 @@ const Earnings: React.FC = () => {
<span className="hidden sm:inline">Processing...</span> <span className="hidden sm:inline">Processing...</span>
</> </>
) : ( ) : (
<>Confirm</> <>Confirm Withdrawal</>
)} )}
</button> </button>
</div> </div>

View file

@ -82,12 +82,23 @@ const Overview = () => {
// Calculate metrics from affiliate data // Calculate metrics from affiliate data
const totalEarnings = affiliateData?.summary.total_earning || 0; const totalEarnings = affiliateData?.summary.total_earning || 0;
const totalWithdraw = affiliateData?.summary.total_withdraw || 0; const totalWithdraw = affiliateData?.summary.total_withdraw || 0;
const pendingAmount = affiliateData?.summary.pending_amount || 0; // Convert negative pending amount to positive
const totalReferrals = affiliateData?.total || 0; const pendingAmount = Math.abs(affiliateData?.summary.pending_amount || 0);
// Calculate average commission // Get unique referred users count
const uniqueReferredUsers = affiliateData?.history
? new Set(affiliateData.history.map((h) => h.referred_user.id)).size
: 0;
// Total transactions count
const totalTransactions = affiliateData?.total || 0;
// Calculate average commission per transaction
const averageCommission = const averageCommission =
totalReferrals > 0 ? totalEarnings / totalReferrals : 0; totalTransactions > 0 ? totalEarnings / totalTransactions : 0;
// Calculate available balance (can't be negative)
const availableBalance = Math.max(totalEarnings - totalWithdraw, 0);
const isLoading = userLoading || earningsLoading; const isLoading = userLoading || earningsLoading;
@ -115,26 +126,26 @@ const Overview = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-6 sm:mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-6 sm:mb-8">
<MetricsCard <MetricsCard
title="Total Earnings" title="Total Earnings"
value={`$${totalEarnings.toFixed(2)}`} value={`${totalEarnings.toFixed(2)}`}
icon={DollarSign} icon={DollarSign}
bgColor="bg-gradient-to-br from-blue-500 to-blue-600" bgColor="bg-gradient-to-br from-blue-500 to-blue-600"
/> />
<MetricsCard <MetricsCard
title="Total Referrals" title="Total Referrals"
value={totalReferrals} value={uniqueReferredUsers}
subtitle={`${affiliateData?.history.length || 0} transactions`} subtitle={`${totalTransactions} transactions`}
icon={Users} icon={Users}
bgColor="bg-gradient-to-br from-purple-500 to-purple-600" bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
/> />
<MetricsCard <MetricsCard
title="Total Withdrawn" title="Total Withdrawn"
value={`$${totalWithdraw.toFixed(2)}`} value={`${totalWithdraw.toFixed(2)}`}
icon={TrendingUp} icon={TrendingUp}
bgColor="bg-gradient-to-br from-green-500 to-green-600" bgColor="bg-gradient-to-br from-green-500 to-green-600"
/> />
<MetricsCard <MetricsCard
title="Pending Commission" title="Pending Commission"
value={`$${Math.max(pendingAmount, 0).toFixed(2)}`} value={`${Math.max(pendingAmount, 0).toFixed(2)}`}
icon={Clock} icon={Clock}
bgColor="bg-gradient-to-br from-orange-500 to-orange-600" bgColor="bg-gradient-to-br from-orange-500 to-orange-600"
/> />
@ -192,9 +203,11 @@ const Overview = () => {
Average Commission Average Commission
</h3> </h3>
<p className="text-2xl sm:text-3xl font-bold text-gray-900"> <p className="text-2xl sm:text-3xl font-bold text-gray-900">
${averageCommission.toFixed(2)} {averageCommission.toFixed(2)}
</p>
<p className="text-xs sm:text-sm text-gray-500 mt-2">
Per transaction
</p> </p>
<p className="text-xs sm:text-sm text-gray-500 mt-2">Per referral</p>
</div> </div>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6">
@ -202,10 +215,12 @@ const Overview = () => {
Available Balance Available Balance
</h3> </h3>
<p className="text-2xl sm:text-3xl font-bold text-gray-900"> <p className="text-2xl sm:text-3xl font-bold text-gray-900">
${(totalEarnings - totalWithdraw).toFixed(2)} {availableBalance.toFixed(2)}
</p> </p>
<p className="text-xs sm:text-sm text-green-600 mt-2"> <p className="text-xs sm:text-sm text-green-600 mt-2">
Ready to withdraw {availableBalance >= 50
? "Ready to withdraw"
: "Minimum ৳50 required"}
</p> </p>
</div> </div>
@ -214,7 +229,7 @@ const Overview = () => {
Latest Transaction Latest Transaction
</h3> </h3>
<p className="text-2xl sm:text-3xl font-bold text-gray-900"> <p className="text-2xl sm:text-3xl font-bold text-gray-900">
${affiliateData?.history[0]?.amount.toFixed(2) || "0.00"} {affiliateData?.history[0]?.amount.toFixed(2) || "0.00"}
</p> </p>
<p className="text-xs sm:text-sm text-gray-500 mt-2"> <p className="text-xs sm:text-sm text-gray-500 mt-2">
{affiliateData?.history[0] {affiliateData?.history[0]

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { import {
Users, Users,
DollarSign, DollarSign,
@ -13,75 +13,30 @@ import {
Clock, Clock,
MoreVertical, MoreVertical,
} from "lucide-react"; } from "lucide-react";
import { useUserStore } from "@/stores/userStore";
interface Referral { import { useAffiliateStore } from "@/stores/affiliateStore";
name: string; import { useReferredUsersStore } from "@/stores/referredUserStore";
email: string;
date: string;
earnings: string;
status: "Active" | "Pending";
signupDate: string;
totalCommission: string;
country: string;
}
const referrals: Referral[] = [
{
name: "John Doe",
email: "john@example.com",
date: "2025-09-20",
earnings: "$50",
status: "Active",
signupDate: "2025-08-15",
totalCommission: "$450",
country: "United States",
},
{
name: "Jane Smith",
email: "jane@example.com",
date: "2025-09-18",
earnings: "$20",
status: "Pending",
signupDate: "2025-09-10",
totalCommission: "$20",
country: "Canada",
},
{
name: "Michael Chen",
email: "michael@example.com",
date: "2025-09-15",
earnings: "$120",
status: "Active",
signupDate: "2025-07-22",
totalCommission: "$890",
country: "Singapore",
},
{
name: "Sarah Johnson",
email: "sarah@example.com",
date: "2025-09-12",
earnings: "$85",
status: "Active",
signupDate: "2025-06-30",
totalCommission: "$1,240",
country: "United Kingdom",
},
{
name: "David Williams",
email: "david@example.com",
date: "2025-09-25",
earnings: "$15",
status: "Pending",
signupDate: "2025-09-20",
totalCommission: "$15",
country: "Australia",
},
];
const Referrals: React.FC = () => { const Referrals: React.FC = () => {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const referralLink = "https://affiliatepro.com/ref/ABC123"; const { user } = useUserStore();
const { data: affiliateData, fetchEarnings } = useAffiliateStore();
const { referredUsers, fetchReferredUsers, isLoading } =
useReferredUsersStore();
useEffect(() => {
const userStr = localStorage.getItem("aff-user");
if (userStr) {
const userData = JSON.parse(userStr);
if (userData.id) {
fetchEarnings(userData.id);
fetchReferredUsers();
}
}
}, [fetchEarnings, fetchReferredUsers]);
const referralLink = `https://dashboard.planpostai.com/sign-up?ref=${user?.referral_code}`;
const handleCopy = () => { const handleCopy = () => {
navigator.clipboard.writeText(referralLink); navigator.clipboard.writeText(referralLink);
@ -89,42 +44,65 @@ const Referrals: React.FC = () => {
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
}; };
// Calculate stats
const totalReferrals = referredUsers?.total || 0;
const activeReferrals =
referredUsers?.data.filter((u) => u.has_used_referral_discount).length || 0;
const totalEarnings = affiliateData?.summary.total_earning || 0;
const avgPerReferral =
totalReferrals > 0 ? totalEarnings / totalReferrals : 0;
const stats = [ const stats = [
{ {
label: "Total Referrals", label: "Total Referrals",
value: "5", value: totalReferrals.toString(),
icon: Users, icon: Users,
color: "bg-blue-500", color: "bg-blue-500",
change: "+12% from last month", change: `${referredUsers?.data.length || 0} users`,
}, },
{ {
label: "Active Referrals", label: "Active Referrals",
value: "3", value: activeReferrals.toString(),
icon: CheckCircle, icon: CheckCircle,
color: "bg-green-500", color: "bg-green-500",
change: "60% conversion rate", change:
totalReferrals > 0
? `${Math.round(
(activeReferrals / totalReferrals) * 100
)}% conversion rate`
: "0% conversion rate",
}, },
{ {
label: "Total Earnings", label: "Total Earnings",
value: "$2,615", value: `${totalEarnings.toFixed(2)}`,
icon: DollarSign, icon: DollarSign,
color: "bg-purple-500", color: "bg-purple-500",
change: "+$450 this month", change: `From referrals`,
}, },
{ {
label: "Avg. Per Referral", label: "Avg. Per Referral",
value: "$523", value: `${avgPerReferral.toFixed(2)}`,
icon: TrendingUp, icon: TrendingUp,
color: "bg-orange-500", color: "bg-orange-500",
change: "+18% growth", change: "Average commission",
}, },
]; ];
const filteredReferrals = referrals.filter( const filteredReferrals =
(r) => referredUsers?.data.filter(
r.name.toLowerCase().includes(searchTerm.toLowerCase()) || (r) =>
r.email.toLowerCase().includes(searchTerm.toLowerCase()) r.first_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
); r.last_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.email.toLowerCase().includes(searchTerm.toLowerCase())
) || [];
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
);
}
return ( return (
<div className="min-h-screen bg-gray-50 p-4"> <div className="min-h-screen bg-gray-50 p-4">
@ -178,7 +156,7 @@ const Referrals: React.FC = () => {
Share this link to earn commissions Share this link to earn commissions
</p> </p>
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1 bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 text-white font-mono text-xs sm:text-sm break-all"> <div className="flex-1 bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 text-black font-mono text-xs sm:text-sm break-all">
{referralLink} {referralLink}
</div> </div>
<button <button
@ -235,68 +213,69 @@ const Referrals: React.FC = () => {
{/* Mobile Card View */} {/* Mobile Card View */}
<div className="block lg:hidden"> <div className="block lg:hidden">
{filteredReferrals.map((r, idx) => ( {filteredReferrals.length === 0 ? (
<div <div className="p-8 text-center text-gray-500">
key={idx} No referrals found
className="p-4 border-b border-gray-100 hover:bg-gray-50 transition-colors" </div>
> ) : (
<div className="flex items-start justify-between mb-3"> filteredReferrals.map((r, idx) => (
<div className="flex items-center gap-3"> <div
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm"> key={idx}
{r.name.charAt(0)} className="p-4 border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm">
{r.first_name.charAt(0)}
</div>
<div>
<div className="font-semibold text-gray-900 text-sm">
{r.first_name} {r.last_name}
</div>
<div className="text-xs text-gray-500">{r.email}</div>
</div>
</div>
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold ${
r.has_used_referral_discount
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.has_used_referral_discount ? (
<>
<CheckCircle className="w-3 h-3" />
Active
</>
) : (
<>
<Clock className="w-3 h-3" />
Pending
</>
)}
</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-xs text-gray-500 mb-1">
Signup Date
</div>
<div className="text-gray-700">
{new Date(r.created_at).toLocaleDateString()}
</div>
</div> </div>
<div> <div>
<div className="font-semibold text-gray-900 text-sm"> <div className="text-xs text-gray-500 mb-1">Status</div>
{r.name} <div className="text-gray-700">
{r.has_used_referral_discount
? "Used Discount"
: "Not Used"}
</div> </div>
<div className="text-xs text-gray-500">{r.email}</div>
</div>
</div>
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold ${
r.status === "Active"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.status === "Active" ? (
<CheckCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
{r.status}
</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-xs text-gray-500 mb-1">
Signup Date
</div>
<div className="text-gray-700">{r.signupDate}</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Country</div>
<div className="text-gray-700">{r.country}</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">
Last Earning
</div>
<div className="font-semibold text-gray-900">
{r.earnings}
</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">
Total Commission
</div>
<div className="font-semibold text-gray-900">
{r.totalCommission}
</div> </div>
</div> </div>
</div> </div>
</div> ))
))} )}
</div> </div>
{/* Desktop Table View */} {/* Desktop Table View */}
@ -311,13 +290,7 @@ const Referrals: React.FC = () => {
Signup Date Signup Date
</th> </th>
<th className="text-left p-4 text-sm font-semibold text-gray-700"> <th className="text-left p-4 text-sm font-semibold text-gray-700">
Country Email
</th>
<th className="text-left p-4 text-sm font-semibold text-gray-700">
Last Earnings
</th>
<th className="text-left p-4 text-sm font-semibold text-gray-700">
Total Commission
</th> </th>
<th className="text-left p-4 text-sm font-semibold text-gray-700"> <th className="text-left p-4 text-sm font-semibold text-gray-700">
Status Status
@ -326,70 +299,70 @@ const Referrals: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredReferrals.map((r, idx) => ( {filteredReferrals.length === 0 ? (
<tr <tr>
key={idx} <td colSpan={5} className="p-8 text-center text-gray-500">
className="border-b border-gray-100 hover:bg-gray-50 transition-colors" No referrals found
>
<td className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold">
{r.name.charAt(0)}
</div>
<div>
<div className="font-semibold text-gray-900">
{r.name}
</div>
<div className="text-sm text-gray-500">{r.email}</div>
</div>
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-2 text-gray-700">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-sm">{r.signupDate}</span>
</div>
</td>
<td className="p-4">
<span className="text-sm text-gray-700">{r.country}</span>
</td>
<td className="p-4">
<div className="flex items-center gap-1">
<DollarSign className="w-4 h-4 text-green-600" />
<span className="font-semibold text-gray-900">
{r.earnings}
</span>
</div>
<div className="text-xs text-gray-500">{r.date}</div>
</td>
<td className="p-4">
<span className="font-semibold text-gray-900">
{r.totalCommission}
</span>
</td>
<td className="p-4">
<span
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold ${
r.status === "Active"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.status === "Active" ? (
<CheckCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
{r.status}
</span>
</td>
<td className="p-4">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<MoreVertical className="w-5 h-5 text-gray-400" />
</button>
</td> </td>
</tr> </tr>
))} ) : (
filteredReferrals.map((r, idx) => (
<tr
key={idx}
className="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<td className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold">
{r.first_name.charAt(0)}
</div>
<div>
<div className="font-semibold text-gray-900">
{r.first_name} {r.last_name}
</div>
</div>
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-2 text-gray-700">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-sm">
{new Date(r.created_at).toLocaleDateString()}
</span>
</div>
</td>
<td className="p-4">
<span className="text-sm text-gray-700">{r.email}</span>
</td>
<td className="p-4">
<span
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold ${
r.has_used_referral_discount
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.has_used_referral_discount ? (
<>
<CheckCircle className="w-3 h-3" />
Active
</>
) : (
<>
<Clock className="w-3 h-3" />
Pending
</>
)}
</span>
</td>
<td className="p-4">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<MoreVertical className="w-5 h-5 text-gray-400" />
</button>
</td>
</tr>
))
)}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -399,7 +372,7 @@ const Referrals: React.FC = () => {
<div className="text-xs sm:text-sm text-gray-600"> <div className="text-xs sm:text-sm text-gray-600">
Showing{" "} Showing{" "}
<span className="font-semibold">{filteredReferrals.length}</span>{" "} <span className="font-semibold">{filteredReferrals.length}</span>{" "}
of <span className="font-semibold">{referrals.length}</span>{" "} of <span className="font-semibold">{totalReferrals}</span>{" "}
referrals referrals
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">

View file

@ -31,6 +31,10 @@ interface AffiliateData {
success: boolean; success: boolean;
affiliate: Affiliate; affiliate: Affiliate;
summary: AffiliateSummary; summary: AffiliateSummary;
account_info?: {
bank_details?: string;
bkash_number?: string;
};
history: EarningHistory[]; history: EarningHistory[];
total: number; total: number;
page: number; page: number;

View file

@ -0,0 +1,85 @@
// store/paymentStore.ts
import { create } from "zustand";
interface PaymentInfo {
bkash_number?: string;
bank_details?: string;
}
interface PaymentState {
paymentInfo: PaymentInfo | null;
isLoading: boolean;
error: string | null;
// Actions
savePaymentInfo: (
data: PaymentInfo
) => Promise<{ success: boolean; error?: string }>;
clearError: () => void;
}
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
export const usePaymentStore = create<PaymentState>((set) => ({
paymentInfo: null,
isLoading: false,
error: null,
savePaymentInfo: async (data: PaymentInfo) => {
set({ isLoading: true, error: null });
try {
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(`${API_BASE_URL}/affiliate/payment-info`, {
method: "POST",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to save payment info");
}
await response.json();
set({
paymentInfo: data,
isLoading: false,
error: null,
});
return { success: true };
} catch (error) {
console.error("Save payment info error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to save payment info",
});
return {
success: false,
error:
error instanceof Error
? error.message
: "Failed to save payment info",
};
}
},
clearError: () => {
set({ error: null });
},
}));

View file

@ -0,0 +1,96 @@
// store/referredUsersStore.ts
import { create } from "zustand";
interface ReferredUser {
id: string;
first_name: string;
last_name: string;
email: string;
created_at: string;
has_used_referral_discount: boolean;
}
interface ReferredUsersData {
success: boolean;
message: string;
data: ReferredUser[];
page: number;
limit: number;
total: number;
}
interface ReferredUsersState {
referredUsers: ReferredUsersData | null;
isLoading: boolean;
error: string | null;
// Actions
fetchReferredUsers: (page?: number, limit?: number) => Promise<void>;
clearData: () => void;
clearError: () => void;
}
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
export const useReferredUsersStore = create<ReferredUsersState>((set) => ({
referredUsers: null,
isLoading: false,
error: null,
fetchReferredUsers: async (page = 1, limit = 10) => {
set({ isLoading: true, error: null });
try {
// Get token from localStorage
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${API_BASE_URL}/affiliate/referred-users?page=${page}&limit=${limit}`,
{
method: "GET",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
},
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to fetch referred users");
}
const data = await response.json();
set({
referredUsers: data,
isLoading: false,
error: null,
});
} catch (error) {
console.error("Fetch referred users error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to fetch referred users",
});
}
},
clearData: () => {
set({
referredUsers: null,
error: null,
});
},
clearError: () => {
set({ error: null });
},
}));

158
src/stores/withdrawStore.ts Normal file
View file

@ -0,0 +1,158 @@
// store/withdrawalStore.ts
import { create } from "zustand";
interface Withdrawal {
id: number;
user: string;
created_at: string;
amount: number;
status: "pending" | "paid" | "cancelled";
note: string;
}
interface WithdrawalData {
success: boolean;
page: number;
limit: number;
total: number;
data: Withdrawal[];
}
interface WithdrawalState {
withdrawals: WithdrawalData | null;
isLoading: boolean;
error: string | null;
// Actions
fetchWithdrawals: (page?: number, limit?: number) => Promise<void>;
requestWithdrawal: (
amount: number,
note: string
) => Promise<{ success: boolean; error?: string }>;
clearData: () => void;
clearError: () => void;
}
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
export const useWithdrawalStore = create<WithdrawalState>((set) => ({
withdrawals: null,
isLoading: false,
error: null,
fetchWithdrawals: async (page = 1, limit = 10) => {
set({ isLoading: true, error: null });
try {
// Get token from localStorage
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${API_BASE_URL}/affiliate/withdraw/my-withdraws?page=${page}&limit=${limit}`,
{
method: "GET",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
},
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to fetch withdrawals");
}
const data = await response.json();
set({
withdrawals: data,
isLoading: false,
error: null,
});
} catch (error) {
console.error("Fetch withdrawals error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to fetch withdrawals",
});
}
},
requestWithdrawal: async (amount: number, note: string) => {
set({ isLoading: true, error: null });
try {
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${API_BASE_URL}/affiliate/withdraw/request`,
{
method: "POST",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount,
note,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to request withdrawal");
}
await response.json();
set({
isLoading: false,
error: null,
});
return { success: true };
} catch (error) {
console.error("Request withdrawal error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to request withdrawal",
});
return {
success: false,
error:
error instanceof Error
? error.message
: "Failed to request withdrawal",
};
}
},
clearData: () => {
set({
withdrawals: null,
error: null,
});
},
clearError: () => {
set({ error: null });
},
}));