complete api integration
This commit is contained in:
parent
085417ab7e
commit
b7b4b4d3c5
10 changed files with 1267 additions and 437 deletions
2
.env
2
.env
|
|
@ -1 +1 @@
|
|||
VITE_SERVER_URL=https://backend.planpostai.com/api/v2
|
||||
VITE_SERVER_URL=https://backendv2.planpostai.com/api/v2
|
||||
276
src/components/PaymentModal.tsx
Normal file
276
src/components/PaymentModal.tsx
Normal 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;
|
||||
|
|
@ -10,7 +10,7 @@ const DashboardLayout: React.FC = () => {
|
|||
<SidebarInset>
|
||||
<div className="flex-1">
|
||||
{/* 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" />
|
||||
</header>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
DollarSign,
|
||||
TrendingUp,
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
CheckCircle,
|
||||
X,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
XAxis,
|
||||
|
|
@ -22,89 +23,104 @@ import {
|
|||
Area,
|
||||
AreaChart,
|
||||
} 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 [timeframe, setTimeframe] = useState<"7d" | "30d" | "90d" | "1y">(
|
||||
"30d"
|
||||
);
|
||||
const [showPayoutModal, setShowPayoutModal] = useState(false);
|
||||
const [payoutAmount, setPayoutAmount] = useState("890");
|
||||
const [payoutAmount, setPayoutAmount] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const [withdrawalNote, setWithdrawalNote] = useState("");
|
||||
|
||||
const earningsData = [
|
||||
{ date: "Sep 1", amount: 120 },
|
||||
{ date: "Sep 5", amount: 180 },
|
||||
{ date: "Sep 10", amount: 240 },
|
||||
{ date: "Sep 15", amount: 210 },
|
||||
{ date: "Sep 20", amount: 290 },
|
||||
{ date: "Sep 25", amount: 350 },
|
||||
{ date: "Sep 30", amount: 450 },
|
||||
];
|
||||
const {
|
||||
data: affiliateData,
|
||||
fetchEarnings,
|
||||
isLoading: earningsLoading,
|
||||
} = useAffiliateStore();
|
||||
const {
|
||||
withdrawals,
|
||||
fetchWithdrawals,
|
||||
requestWithdrawal,
|
||||
isLoading: withdrawalsLoading,
|
||||
} = useWithdrawalStore();
|
||||
|
||||
const transactionData = [
|
||||
{
|
||||
type: "Commission",
|
||||
amount: "$120",
|
||||
date: "Sep 28, 2025",
|
||||
status: "Completed",
|
||||
ref: "REF-4521",
|
||||
},
|
||||
{
|
||||
type: "Bonus",
|
||||
amount: "$50",
|
||||
date: "Sep 25, 2025",
|
||||
status: "Completed",
|
||||
ref: "BONUS-892",
|
||||
},
|
||||
{
|
||||
type: "Commission",
|
||||
amount: "$85",
|
||||
date: "Sep 22, 2025",
|
||||
status: "Completed",
|
||||
ref: "REF-4489",
|
||||
},
|
||||
{
|
||||
type: "Commission",
|
||||
amount: "$95",
|
||||
date: "Sep 18, 2025",
|
||||
status: "Pending",
|
||||
ref: "REF-4455",
|
||||
},
|
||||
{
|
||||
type: "Commission",
|
||||
amount: "$100",
|
||||
date: "Sep 15, 2025",
|
||||
status: "Completed",
|
||||
ref: "REF-4421",
|
||||
},
|
||||
];
|
||||
useEffect(() => {
|
||||
const userStr = localStorage.getItem("aff-user");
|
||||
if (userStr) {
|
||||
const userData = JSON.parse(userStr);
|
||||
if (userData.id) {
|
||||
fetchEarnings(userData.id);
|
||||
fetchWithdrawals();
|
||||
}
|
||||
}
|
||||
}, [fetchEarnings, fetchWithdrawals]);
|
||||
|
||||
// Get summary data
|
||||
const totalEarnings = affiliateData?.summary.total_earning || 0;
|
||||
const totalWithdraw = affiliateData?.summary.total_withdraw || 0;
|
||||
// Convert negative pending amount to positive
|
||||
const pendingAmount = Math.abs(affiliateData?.summary.pending_amount || 0);
|
||||
const availableBalance = Math.max(totalEarnings - totalWithdraw, 0);
|
||||
|
||||
// Calculate monthly earnings (last transaction)
|
||||
const lastTransaction = affiliateData?.history[0];
|
||||
const monthlyEarnings = lastTransaction?.amount || 0;
|
||||
|
||||
// Transform affiliate history to chart data
|
||||
const earningsData =
|
||||
affiliateData?.history
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((item) => ({
|
||||
date: new Date(item.created_at).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}),
|
||||
amount: item.amount,
|
||||
})) || [];
|
||||
|
||||
// Calculate stats
|
||||
const totalTransactions = affiliateData?.total || 0;
|
||||
const avgCommission =
|
||||
totalTransactions > 0 ? totalEarnings / totalTransactions : 0;
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: "Total Earnings",
|
||||
value: "$12,450",
|
||||
change: "+12.5%",
|
||||
trend: "up",
|
||||
value: `৳${totalEarnings.toFixed(2)}`,
|
||||
change:
|
||||
totalWithdraw > 0
|
||||
? `৳${totalWithdraw.toFixed(2)} withdrawn`
|
||||
: "No withdrawals yet",
|
||||
trend: "up" as const,
|
||||
icon: DollarSign,
|
||||
color: "from-green-500 to-emerald-600",
|
||||
bgColor: "bg-green-50",
|
||||
textColor: "text-green-600",
|
||||
},
|
||||
{
|
||||
label: "This Month",
|
||||
value: "$2,340",
|
||||
change: "+8.2%",
|
||||
trend: "up",
|
||||
label: "Last Transaction",
|
||||
value: `৳${monthlyEarnings.toFixed(2)}`,
|
||||
change: lastTransaction
|
||||
? new Date(lastTransaction.created_at).toLocaleDateString()
|
||||
: "No transactions",
|
||||
trend: "up" as const,
|
||||
icon: TrendingUp,
|
||||
color: "from-blue-500 to-indigo-600",
|
||||
bgColor: "bg-blue-50",
|
||||
textColor: "text-blue-600",
|
||||
},
|
||||
{
|
||||
label: "Pending Payout",
|
||||
value: "$450",
|
||||
change: "Oct 15",
|
||||
trend: "neutral",
|
||||
label: "Pending Amount",
|
||||
value: `৳${Math.max(pendingAmount, 0).toFixed(2)}`,
|
||||
change: "To be processed",
|
||||
trend: "neutral" as const,
|
||||
icon: Clock,
|
||||
color: "from-orange-500 to-amber-600",
|
||||
bgColor: "bg-orange-50",
|
||||
|
|
@ -112,9 +128,9 @@ const Earnings: React.FC = () => {
|
|||
},
|
||||
{
|
||||
label: "Available Balance",
|
||||
value: "$890",
|
||||
change: "Ready now",
|
||||
trend: "neutral",
|
||||
value: `৳${availableBalance.toFixed(2)}`,
|
||||
change: availableBalance >= 50 ? "Ready now" : `Minimum $50 required`,
|
||||
trend: "neutral" as const,
|
||||
icon: Wallet,
|
||||
color: "from-purple-500 to-pink-600",
|
||||
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);
|
||||
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);
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-50 p-4 sm:p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
|
|
@ -152,8 +245,12 @@ const Earnings: React.FC = () => {
|
|||
<span className="hidden sm:inline">Export</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowPayoutModal(true)}
|
||||
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"
|
||||
onClick={() => {
|
||||
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" />
|
||||
<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" />
|
||||
)}
|
||||
<span className="text-xs sm:text-sm font-semibold">
|
||||
{stat.change}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -200,9 +294,7 @@ const Earnings: React.FC = () => {
|
|||
<div className="text-xs sm:text-sm text-gray-600">
|
||||
{stat.label}
|
||||
</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>
|
||||
|
|
@ -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 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">
|
||||
<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 className="text-indigo-100 text-xs sm:text-sm mb-1">
|
||||
Next Scheduled Payout
|
||||
Total Withdrawn
|
||||
</div>
|
||||
<div className="text-xl sm:text-3xl font-bold text-white mb-2">
|
||||
October 15, 2025
|
||||
৳{totalWithdraw.toFixed(2)}
|
||||
</div>
|
||||
<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 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">
|
||||
Estimated Amount
|
||||
Available Balance
|
||||
</div>
|
||||
<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 className="text-black text-xs mt-1">+ pending approvals</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -266,65 +362,131 @@ const Earnings: React.FC = () => {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<AreaChart data={earningsData}>
|
||||
<defs>
|
||||
<linearGradient id="colorAmount" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#9ca3af"
|
||||
style={{ fontSize: "10px" }}
|
||||
/>
|
||||
<YAxis stroke="#9ca3af" style={{ fontSize: "10px" }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="amount"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorAmount)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
{earningsData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<AreaChart data={earningsData}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorAmount"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#9ca3af"
|
||||
style={{ fontSize: "10px" }}
|
||||
/>
|
||||
<YAxis stroke="#9ca3af" style={{ fontSize: "10px" }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
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>
|
||||
|
||||
{/* Payment Methods */}
|
||||
{/* Payment Methods */}
|
||||
<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">
|
||||
Payment Method
|
||||
Payment Methods
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<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">
|
||||
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
PayPal
|
||||
{/* Bank Details */}
|
||||
{affiliateData?.account_info?.bank_details && (
|
||||
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
|
||||
<div className="flex items-start gap-3">
|
||||
<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">
|
||||
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-600 truncate">
|
||||
john@example.com
|
||||
<div className="flex-1 min-w-0">
|
||||
<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>
|
||||
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600 flex-shrink-0" />
|
||||
</div>
|
||||
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600 flex-shrink-0" />
|
||||
</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
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Bkash Number */}
|
||||
{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 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
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
142
|
||||
{totalTransactions}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
|
|
@ -345,15 +507,15 @@ const Earnings: React.FC = () => {
|
|||
Avg. Commission
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
$87.68
|
||||
৳{avgCommission.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs sm:text-sm text-gray-600">
|
||||
Success Rate
|
||||
Withdrawals
|
||||
</span>
|
||||
<span className="font-semibold text-green-600 text-sm sm:text-base">
|
||||
98.2%
|
||||
<span className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
{withdrawals?.total || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -365,80 +527,86 @@ const Earnings: React.FC = () => {
|
|||
<div className="bg-white rounded-xl shadow-sm border 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">
|
||||
Recent Transactions
|
||||
Withdrawal History
|
||||
</h2>
|
||||
<p className="text-xs sm:text-sm text-gray-600 mt-1">
|
||||
Your latest earnings and payouts
|
||||
Your withdrawal requests and status
|
||||
</p>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{transactionData.map((transaction, idx) => (
|
||||
<div
|
||||
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={`w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
transaction.type === "Bonus"
|
||||
? "bg-purple-100"
|
||||
: "bg-green-100"
|
||||
}`}
|
||||
>
|
||||
<DollarSign
|
||||
className={`w-5 h-5 sm:w-6 sm:h-6 ${
|
||||
transaction.type === "Bonus"
|
||||
? "text-purple-600"
|
||||
: "text-green-600"
|
||||
{withdrawals?.data && withdrawals.data.length > 0 ? (
|
||||
withdrawals.data.map((transaction, idx) => (
|
||||
<div
|
||||
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={`w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
transaction.status === "paid"
|
||||
? "bg-green-100"
|
||||
: transaction.status === "cancelled"
|
||||
? "bg-red-100"
|
||||
: "bg-yellow-100"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
{transaction.type}
|
||||
>
|
||||
<DollarSign
|
||||
className={`w-5 h-5 sm:w-6 sm:h-6 ${
|
||||
transaction.status === "paid"
|
||||
? "text-green-600"
|
||||
: transaction.status === "cancelled"
|
||||
? "text-red-600"
|
||||
: "text-yellow-600"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-600">
|
||||
{transaction.date}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{transaction.ref}
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
Withdrawal Request
|
||||
</div>
|
||||
<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 className="text-right flex-shrink-0">
|
||||
<div className="text-lg sm:text-2xl font-bold text-gray-900">
|
||||
{transaction.amount}
|
||||
</div>
|
||||
<span
|
||||
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 === "Completed"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700"
|
||||
}`}
|
||||
>
|
||||
{transaction.status === "Completed" ? (
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
) : (
|
||||
<Clock className="w-3 h-3" />
|
||||
)}
|
||||
<span className="hidden sm:inline">
|
||||
{transaction.status}
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className="text-lg sm:text-2xl font-bold text-gray-900">
|
||||
৳{transaction.amount.toFixed(2)}
|
||||
</div>
|
||||
<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(
|
||||
transaction.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(transaction.status)}
|
||||
<span className="hidden sm:inline capitalize">
|
||||
{transaction.status}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No withdrawal history yet
|
||||
</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>
|
||||
|
||||
<PaymentMethodModal
|
||||
isOpen={showPaymentModal}
|
||||
onClose={() => setShowPaymentModal(false)}
|
||||
/>
|
||||
|
||||
{/* Payout Request Modal */}
|
||||
{showPayoutModal && (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
{/* Modal Body */}
|
||||
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
{/* Available Balance */}
|
||||
|
|
@ -474,7 +643,7 @@ const Earnings: React.FC = () => {
|
|||
Available Balance
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl font-bold text-green-900">
|
||||
$890.00
|
||||
৳{availableBalance.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-green-600 mt-1">
|
||||
Ready for withdrawal
|
||||
|
|
@ -493,18 +662,36 @@ const Earnings: React.FC = () => {
|
|||
value={payoutAmount}
|
||||
onChange={(e) => setPayoutAmount(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
Max
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -513,22 +700,55 @@ const Earnings: React.FC = () => {
|
|||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Payment Method
|
||||
</label>
|
||||
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
PayPal
|
||||
{affiliateData?.account_info?.bank_details && (
|
||||
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50 mb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-600 truncate">
|
||||
john@example.com
|
||||
<div className="flex-1 min-w-0">
|
||||
<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>
|
||||
<CheckCircle className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<CheckCircle className="w-5 h-5 text-indigo-600" />
|
||||
</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>
|
||||
|
||||
{/* Processing Info */}
|
||||
|
|
@ -548,19 +768,19 @@ const Earnings: React.FC = () => {
|
|||
<div className="flex justify-between text-xs sm:text-sm">
|
||||
<span className="text-gray-600">Withdrawal Amount</span>
|
||||
<span className="font-semibold text-gray-900">
|
||||
${payoutAmount}
|
||||
৳{Number(payoutAmount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs sm:text-sm">
|
||||
<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 className="border-t border-gray-200 pt-2 flex justify-between">
|
||||
<span className="font-semibold text-gray-900 text-sm sm:text-base">
|
||||
You'll Receive
|
||||
</span>
|
||||
<span className="text-lg sm:text-xl font-bold text-indigo-600">
|
||||
${payoutAmount}
|
||||
৳{Number(payoutAmount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -579,7 +799,10 @@ const Earnings: React.FC = () => {
|
|||
disabled={
|
||||
isProcessing ||
|
||||
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"
|
||||
>
|
||||
|
|
@ -589,7 +812,7 @@ const Earnings: React.FC = () => {
|
|||
<span className="hidden sm:inline">Processing...</span>
|
||||
</>
|
||||
) : (
|
||||
<>Confirm</>
|
||||
<>Confirm Withdrawal</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -82,12 +82,23 @@ const Overview = () => {
|
|||
// Calculate metrics from affiliate data
|
||||
const totalEarnings = affiliateData?.summary.total_earning || 0;
|
||||
const totalWithdraw = affiliateData?.summary.total_withdraw || 0;
|
||||
const pendingAmount = affiliateData?.summary.pending_amount || 0;
|
||||
const totalReferrals = affiliateData?.total || 0;
|
||||
// Convert negative pending amount to positive
|
||||
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 =
|
||||
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;
|
||||
|
||||
|
|
@ -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">
|
||||
<MetricsCard
|
||||
title="Total Earnings"
|
||||
value={`$${totalEarnings.toFixed(2)}`}
|
||||
value={`৳${totalEarnings.toFixed(2)}`}
|
||||
icon={DollarSign}
|
||||
bgColor="bg-gradient-to-br from-blue-500 to-blue-600"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Referrals"
|
||||
value={totalReferrals}
|
||||
subtitle={`${affiliateData?.history.length || 0} transactions`}
|
||||
value={uniqueReferredUsers}
|
||||
subtitle={`${totalTransactions} transactions`}
|
||||
icon={Users}
|
||||
bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Withdrawn"
|
||||
value={`$${totalWithdraw.toFixed(2)}`}
|
||||
value={`৳${totalWithdraw.toFixed(2)}`}
|
||||
icon={TrendingUp}
|
||||
bgColor="bg-gradient-to-br from-green-500 to-green-600"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Pending Commission"
|
||||
value={`$${Math.max(pendingAmount, 0).toFixed(2)}`}
|
||||
value={`৳${Math.max(pendingAmount, 0).toFixed(2)}`}
|
||||
icon={Clock}
|
||||
bgColor="bg-gradient-to-br from-orange-500 to-orange-600"
|
||||
/>
|
||||
|
|
@ -192,9 +203,11 @@ const Overview = () => {
|
|||
Average Commission
|
||||
</h3>
|
||||
<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 className="text-xs sm:text-sm text-gray-500 mt-2">Per referral</p>
|
||||
</div>
|
||||
|
||||
<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
|
||||
</h3>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
${(totalEarnings - totalWithdraw).toFixed(2)}
|
||||
৳{availableBalance.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm text-green-600 mt-2">
|
||||
Ready to withdraw
|
||||
{availableBalance >= 50
|
||||
? "Ready to withdraw"
|
||||
: "Minimum ৳50 required"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -214,7 +229,7 @@ const Overview = () => {
|
|||
Latest Transaction
|
||||
</h3>
|
||||
<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 className="text-xs sm:text-sm text-gray-500 mt-2">
|
||||
{affiliateData?.history[0]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Users,
|
||||
DollarSign,
|
||||
|
|
@ -13,75 +13,30 @@ import {
|
|||
Clock,
|
||||
MoreVertical,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Referral {
|
||||
name: string;
|
||||
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",
|
||||
},
|
||||
];
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { useAffiliateStore } from "@/stores/affiliateStore";
|
||||
import { useReferredUsersStore } from "@/stores/referredUserStore";
|
||||
|
||||
const Referrals: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
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 = () => {
|
||||
navigator.clipboard.writeText(referralLink);
|
||||
|
|
@ -89,42 +44,65 @@ const Referrals: React.FC = () => {
|
|||
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 = [
|
||||
{
|
||||
label: "Total Referrals",
|
||||
value: "5",
|
||||
value: totalReferrals.toString(),
|
||||
icon: Users,
|
||||
color: "bg-blue-500",
|
||||
change: "+12% from last month",
|
||||
change: `${referredUsers?.data.length || 0} users`,
|
||||
},
|
||||
{
|
||||
label: "Active Referrals",
|
||||
value: "3",
|
||||
value: activeReferrals.toString(),
|
||||
icon: CheckCircle,
|
||||
color: "bg-green-500",
|
||||
change: "60% conversion rate",
|
||||
change:
|
||||
totalReferrals > 0
|
||||
? `${Math.round(
|
||||
(activeReferrals / totalReferrals) * 100
|
||||
)}% conversion rate`
|
||||
: "0% conversion rate",
|
||||
},
|
||||
{
|
||||
label: "Total Earnings",
|
||||
value: "$2,615",
|
||||
value: `৳${totalEarnings.toFixed(2)}`,
|
||||
icon: DollarSign,
|
||||
color: "bg-purple-500",
|
||||
change: "+$450 this month",
|
||||
change: `From referrals`,
|
||||
},
|
||||
{
|
||||
label: "Avg. Per Referral",
|
||||
value: "$523",
|
||||
value: `৳${avgPerReferral.toFixed(2)}`,
|
||||
icon: TrendingUp,
|
||||
color: "bg-orange-500",
|
||||
change: "+18% growth",
|
||||
change: "Average commission",
|
||||
},
|
||||
];
|
||||
|
||||
const filteredReferrals = referrals.filter(
|
||||
(r) =>
|
||||
r.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
r.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
const filteredReferrals =
|
||||
referredUsers?.data.filter(
|
||||
(r) =>
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
|
|
@ -178,7 +156,7 @@ const Referrals: React.FC = () => {
|
|||
Share this link to earn commissions
|
||||
</p>
|
||||
<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}
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -235,68 +213,69 @@ const Referrals: React.FC = () => {
|
|||
|
||||
{/* Mobile Card View */}
|
||||
<div className="block lg:hidden">
|
||||
{filteredReferrals.map((r, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
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.name.charAt(0)}
|
||||
{filteredReferrals.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No referrals found
|
||||
</div>
|
||||
) : (
|
||||
filteredReferrals.map((r, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
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 className="font-semibold text-gray-900 text-sm">
|
||||
{r.name}
|
||||
<div className="text-xs text-gray-500 mb-1">Status</div>
|
||||
<div className="text-gray-700">
|
||||
{r.has_used_referral_discount
|
||||
? "Used Discount"
|
||||
: "Not Used"}
|
||||
</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>
|
||||
|
||||
{/* Desktop Table View */}
|
||||
|
|
@ -311,13 +290,7 @@ const Referrals: React.FC = () => {
|
|||
Signup Date
|
||||
</th>
|
||||
<th className="text-left p-4 text-sm font-semibold text-gray-700">
|
||||
Country
|
||||
</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
|
||||
Email
|
||||
</th>
|
||||
<th className="text-left p-4 text-sm font-semibold text-gray-700">
|
||||
Status
|
||||
|
|
@ -326,70 +299,70 @@ const Referrals: React.FC = () => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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.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>
|
||||
{filteredReferrals.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-8 text-center text-gray-500">
|
||||
No referrals found
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -399,7 +372,7 @@ const Referrals: React.FC = () => {
|
|||
<div className="text-xs sm:text-sm text-gray-600">
|
||||
Showing{" "}
|
||||
<span className="font-semibold">{filteredReferrals.length}</span>{" "}
|
||||
of <span className="font-semibold">{referrals.length}</span>{" "}
|
||||
of <span className="font-semibold">{totalReferrals}</span>{" "}
|
||||
referrals
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ interface AffiliateData {
|
|||
success: boolean;
|
||||
affiliate: Affiliate;
|
||||
summary: AffiliateSummary;
|
||||
account_info?: {
|
||||
bank_details?: string;
|
||||
bkash_number?: string;
|
||||
};
|
||||
history: EarningHistory[];
|
||||
total: number;
|
||||
page: number;
|
||||
|
|
|
|||
85
src/stores/paymentStore.ts
Normal file
85
src/stores/paymentStore.ts
Normal 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 });
|
||||
},
|
||||
}));
|
||||
96
src/stores/referredUserStore.ts
Normal file
96
src/stores/referredUserStore.ts
Normal 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
158
src/stores/withdrawStore.ts
Normal 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 });
|
||||
},
|
||||
}));
|
||||
Loading…
Add table
Reference in a new issue