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>
|
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
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