wallets-api / client /src /pages /WalletsView.tsx
z1amez's picture
v.1
2dddd1f
import { useState, useMemo } from 'react';
import { useWallets, useTransactions, useRates, useCreateTransaction, useExchanges, useCreateExchange } from '../hooks/queries';
import { Wallet as WalletIcon, DollarSign, Coins, Bitcoin, MessageCircle, ShieldCheck, Zap, CreditCard, PlusCircle, RefreshCw, MinusCircle } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { AmountInput } from '../components/ui/AmountInput';
import { Select } from '../components/ui/select';
import { Label } from '../components/ui/label';
export default function WalletsView() {
const getWalletStyling = (name: string) => {
const n = name.toLowerCase();
if (n.includes('dollar')) return { icon: DollarSign, color: 'text-emerald-400', bg: 'bg-emerald-500/20', border: 'border-emerald-500/30' };
if (n.includes('dinnar') || n.includes('dinar')) return { icon: Coins, color: 'text-indigo-400', bg: 'bg-indigo-500/20', border: 'border-indigo-500/30' };
if (n.includes('crypto')) return { icon: Bitcoin, color: 'text-orange-400', bg: 'bg-orange-500/20', border: 'border-orange-500/30' };
if (n.includes('wechat')) return { icon: MessageCircle, color: 'text-green-400', bg: 'bg-green-500/20', border: 'border-green-500/30' };
if (n.includes('alipay')) return { icon: ShieldCheck, color: 'text-blue-400', bg: 'bg-blue-500/20', border: 'border-blue-500/30' };
if (n.includes('fib')) return { icon: Zap, color: 'text-yellow-400', bg: 'bg-yellow-500/20', border: 'border-yellow-500/30' };
if (n.includes('fastpay')) return { icon: Zap, color: 'text-red-400', bg: 'bg-red-500/20', border: 'border-red-500/30' };
if (n.includes('super qi')) return { icon: CreditCard, color: 'text-blue-400', bg: 'bg-blue-500/20', border: 'border-blue-500/30' };
if (n.includes('kj wallets')) return { icon: WalletIcon, color: 'text-purple-400', bg: 'bg-purple-500/20', border: 'border-purple-500/30' };
return { icon: WalletIcon, color: 'text-indigo-400', bg: 'bg-indigo-500/20', border: 'border-indigo-500/30' };
};
const getAllowedDestinations = (sourceName: string) => {
const name = sourceName.toLowerCase();
if (name.includes('usd') && !name.includes('usdt')) return ['USDT', 'Cash Dinar', 'Alipay', 'WeChat'];
if (name.includes('kj wallets')) return ['Cash USD', 'FIB'];
if (name.includes('dinar')) return ['FIB', 'FastPay', 'Super Qi', 'Cash USD'];
if (name.includes('usdt')) return ['Cash USD'];
if (name.includes('fib')) return ['FastPay', 'Super Qi', 'Cash Dinar'];
if (name.includes('fastpay')) return ['FIB', 'Super Qi', 'Cash Dinar'];
if (name.includes('qi')) return ['FastPay', 'FIB', 'Cash Dinar'];
if (name.includes('wechat')) return ['Alipay', 'Cash USD'];
if (name.includes('alipay')) return ['WeChat', 'Cash USD'];
return [];
};
const { data: rawWallets = [] } = useWallets();
const wallets = useMemo(() => {
const order = ['cash usd', 'cash dinar', 'super qi', 'alipay', 'wechat', 'fib', 'fastpay', 'usdt', 'kj wallets'];
return [...rawWallets].sort((a: any, b: any) => {
const idxA = order.indexOf(a.name.toLowerCase());
const idxB = order.indexOf(b.name.toLowerCase());
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB);
});
}, [rawWallets]);
const { data: transactions = [] } = useTransactions();
const { data: exchanges = [] } = useExchanges();
const { data: rates } = useRates();
const { mutateAsync: createTransaction } = useCreateTransaction();
const { mutateAsync: createExchange } = useCreateExchange();
const [actionState, setActionState] = useState<{type: 'transfer' | 'income' | 'expense' | 'exchange', walletId?: number} | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Form States
const [amount, setAmount] = useState('');
const [toAmount, setToAmount] = useState('');
const [toWallet, setToWallet] = useState('');
const [note, setNote] = useState('');
const [category, setCategory] = useState('other');
const exchangeRates = rates || { USD: 1, IQD: 1539.5, RMB: 6.86 };
// Calculate Balances dynamically factoring in currency differences
const getBalance = (w: any) => {
const txBal = transactions.reduce((acc: number, tx: any) => {
let effectiveAmount = tx.amount;
if (tx.currency !== w.currency) {
const txRate = exchangeRates[tx.currency] || 1;
const walletRate = exchangeRates[w.currency] || 1;
effectiveAmount = (tx.amount / txRate) * walletRate;
}
if (tx.type === 'income' && tx.wallet_id === w.id) return acc + effectiveAmount;
if (tx.type === 'expense' && tx.wallet_id === w.id) return acc - effectiveAmount;
if (tx.type === 'transfer') {
if (tx.wallet_id === w.id) return acc - effectiveAmount;
if (tx.to_wallet_id === w.id) return acc + effectiveAmount;
}
return acc;
}, 0);
const exBal = exchanges.reduce((acc: number, ex: any) => {
let bal = 0;
if (ex.from_wallet_id === w.id) bal -= ex.from_amount;
if (ex.to_wallet_id === w.id) bal += ex.to_amount;
return acc + bal;
}, 0);
return txBal + exBal;
};
const activeWallet = actionState?.walletId ? wallets.find((w: any) => w.id === actionState.walletId) : null;
const currentBalance = activeWallet ? getBalance(activeWallet) : 0;
const enteredAmount = parseFloat(amount) || 0;
const isInsufficient = (actionState?.type === 'expense' || actionState?.type === 'exchange') && enteredAmount > currentBalance;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isInsufficient) return;
setIsSubmitting(true);
try {
if (actionState?.type === 'income' || actionState?.type === 'expense') {
const targetWallet = wallets.find((w: any) => w.id === actionState.walletId);
if (!targetWallet) return;
let finalCategory = category;
if (actionState.type === 'income' && category === 'other') {
// If it was the default 'other' but from a previous expense, and we are in income now,
// we might want to ensure it's handled correctly if not changed.
// But actually, the state is shared. So we just use 'category' state.
}
await createTransaction({
type: actionState.type,
amount: parseFloat(amount),
currency: targetWallet.currency,
wallet_id: targetWallet.id,
category: finalCategory,
note: note || (actionState.type === 'income' ? 'Added Income' : 'Recorded Expense'),
date: new Date().toISOString()
} as any);
} else if (actionState?.type === 'exchange') {
const sourceW = wallets.find((w: any) => w.id === actionState.walletId);
const destW = wallets.find((w: any) => w.id.toString() === toWallet);
if (!sourceW || !destW) return;
const fAmt = parseFloat(amount);
const tAmt = parseFloat(toAmount);
await createExchange({
from_amount: fAmt,
from_currency: sourceW.currency,
from_wallet_id: sourceW.id,
to_amount: tAmt,
to_currency: destW.currency,
to_wallet_id: destW.id,
rate: tAmt / fAmt,
note: note || `Exchanged to ${destW.name}`,
date: new Date().toISOString()
} as any);
}
setActionState(null);
setAmount(''); setToAmount(''); setNote(''); setToWallet(''); setCategory('other');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="p-4 md:p-8 h-full flex flex-col">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 md:mb-8 shrink-0">
<h1 className="text-xl md:text-2xl font-bold flex items-center gap-3">
<WalletIcon className="w-5 h-5 md:w-6 md:h-6 text-indigo-400" /> Wallets
</h1>
<Button onClick={() => setActionState(null)} className="opacity-0 cursor-default pointer-events-none w-full sm:w-auto">
Placeholder
</Button>
</div>
{actionState && (
<div className={`glass-panel p-4 md:p-6 mb-6 md:mb-8 border ${
actionState.type === 'income' ? 'border-emerald-500/30' :
actionState.type === 'expense' ? 'border-rose-500/30' :
'border-blue-500/30'
}`}>
<div className="flex justify-between items-center mb-4">
<h2 className={`text-lg font-semibold ${
actionState.type === 'income' ? 'text-emerald-400' :
actionState.type === 'expense' ? 'text-rose-400' :
'text-blue-400'
}`}>
{actionState.type === 'income' ? `Add Salary / Income to ${wallets.find((w: any) => w.id === actionState.walletId)?.name}` :
actionState.type === 'expense' ? `Add Expense from ${wallets.find((w: any) => w.id === actionState.walletId)?.name}` :
`Exchange from ${wallets.find((w: any) => w.id === actionState.walletId)?.name}`}
</h2>
<Button variant="ghost" size="sm" onClick={() => setActionState(null)}>Cancel</Button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{actionState.type === 'exchange' && (() => {
const sourceName = wallets.find((w: any) => w.id === actionState.walletId)?.name || '';
const allowedNames = getAllowedDestinations(sourceName);
const availableDestinations = wallets.filter((w: any) => w.id !== actionState.walletId && allowedNames.includes(w.name));
return (
<div>
<Label>To Wallet</Label>
<Select required value={toWallet} onChange={e => setToWallet(e.target.value)} className="mt-1">
<option value="">Select Destination</option>
{availableDestinations.map((w: any) => (
<option key={w.id} value={w.id}>{w.name} ({w.currency})</option>
))}
</Select>
{availableDestinations.length === 0 && (
<p className="text-xs text-red-400 mt-2">No allowed conversions for this wallet type.</p>
)}
</div>
);
})()}
<div>
<Label>{
actionState.type === 'exchange' ? 'Amount to Exchange' :
actionState.type === 'income' ? 'Income Amount' :
'Expense Amount'
}</Label>
<div className="relative mt-1">
<AmountInput required value={amount} onChange={e => setAmount(e.target.value)} className="pr-12" />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-neutral-500 font-medium text-sm">
{wallets.find((w: any) => w.id === actionState.walletId)?.currency}
</div>
</div>
</div>
{actionState.type === 'exchange' && (
<div>
<Label>Exact Amount Received</Label>
<div className="relative mt-1">
<AmountInput required value={toAmount} onChange={e => setToAmount(e.target.value)} className="pr-12" />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-neutral-500 font-medium text-sm">
{wallets.find((w: any) => w.id.toString() === toWallet)?.currency}
</div>
</div>
</div>
)}
{actionState.type === 'expense' && (
<div>
<Label>Category</Label>
<Select
value={category}
onChange={e => setCategory(e.target.value)}
className="mt-1"
>
<option value="food">Food</option>
<option value="market">Market</option>
<option value="transport">Transport</option>
<option value="cafe">Cafe</option>
<option value="barber">Barber</option>
<option value="mobile-balance">Mobile Balance</option>
<option value="electricity">Electricity</option>
<option value="health">Health</option>
<option value="gift">Gift</option>
<option value="other">Other</option>
</Select>
</div>
)}
{actionState.type === 'income' && (
<div>
<Label>Category</Label>
<Select
value={category}
onChange={e => setCategory(e.target.value)}
className="mt-1"
>
<option value="salary">Salary</option>
<option value="gift">Gift</option>
<option value="other">Other</option>
</Select>
</div>
)}
<div className={actionState.type === 'exchange' ? "col-span-1 md:col-span-2" : ""}>
<Label>Note (Optional)</Label>
<Input type="text" value={note} onChange={e => setNote(e.target.value)} placeholder={
actionState.type === 'income' ? 'e.g. Salary' :
actionState.type === 'expense' ? 'e.g. Food, Transport' :
''
} className="mt-1" />
</div>
</div>
{isInsufficient && (
<div className="bg-rose-500/10 border border-rose-500/20 text-rose-400 p-3 rounded-xl text-sm mb-4">
cantbe expense balnce than your balance
</div>
)}
<div className="flex justify-end pt-2 md:pt-4">
<Button
type="submit"
disabled={isSubmitting || isInsufficient}
className={`w-full md:w-auto ${
isInsufficient ? 'bg-neutral-800 text-neutral-500 cursor-not-allowed' :
actionState.type === 'income' ? 'bg-emerald-600 hover:bg-emerald-500' :
actionState.type === 'expense' ? 'bg-rose-600 hover:bg-rose-500' :
'bg-blue-600 hover:bg-blue-500'
} text-white`}
>
{actionState.type === 'income' ? 'Add Funds' :
actionState.type === 'expense' ? 'Record Expense' :
'Execute Exchange'}
</Button>
</div>
</form>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 overflow-y-auto pb-24 md:pb-0 pr-2">
{wallets.map((w: any) => {
const balance = getBalance(w);
const { icon: Icon, color, bg, border } = getWalletStyling(w.name);
return (
<div key={w.id} className={`glass-panel p-4 md:p-6 border border-white/5 hover:border-indigo-500/30 transition-all flex flex-col justify-between group`}>
<div>
<div className={`w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center mb-3 md:mb-4 border ${bg} ${color} ${border}`}>
<Icon className="w-5 h-5 md:w-6 md:h-6" />
</div>
<h3 className="text-base md:text-lg font-bold">{w.name}</h3>
<p className="text-xs md:text-sm text-neutral-400 capitalize">{w.type}</p>
</div>
<div className="mt-6 md:mt-8 pt-4 border-t border-white/10">
<p className="text-xs md:text-sm text-neutral-500 mb-1">Current Balance</p>
<p className="text-xl md:text-2xl font-bold tracking-tight mb-4">
{balance.toLocaleString(undefined, { maximumFractionDigits: 2 })} <span className="text-sm md:text-lg text-neutral-500">{w.currency}</span>
</p>
<div className="grid grid-cols-3 gap-1 md:gap-2 mt-auto">
<button onClick={() => { setActionState({ type: 'income', walletId: w.id }); setCategory('salary'); }} className="flex items-center justify-center gap-1 md:gap-1.5 py-2 rounded shadow-sm bg-white/5 hover:bg-emerald-500/20 text-emerald-400 hover:text-emerald-300 font-medium text-[10px] md:text-sm transition-colors border border-emerald-500/10 hover:border-emerald-500/30">
<PlusCircle className="w-3 h-3 md:w-4 md:h-4" /> Add
</button>
<button onClick={() => { setActionState({ type: 'expense', walletId: w.id }); setCategory('other'); }} className="flex items-center justify-center gap-1 md:gap-1.5 py-2 rounded shadow-sm bg-white/5 hover:bg-rose-500/20 text-rose-400 hover:text-rose-300 font-medium text-[10px] md:text-sm transition-colors border border-rose-500/10 hover:border-rose-500/30">
<MinusCircle className="w-3 h-3 md:w-4 md:h-4" /> Expense
</button>
<button onClick={() => { setActionState({ type: 'exchange', walletId: w.id }); setCategory('other'); }} className="flex items-center justify-center gap-1 md:gap-1.5 py-2 rounded shadow-sm bg-white/5 hover:bg-blue-500/20 text-blue-400 hover:text-blue-300 font-medium text-[10px] md:text-sm transition-colors border border-blue-500/10 hover:border-blue-500/30">
<RefreshCw className="w-3 h-3 md:w-3.5 md:h-3.5" /> Exch
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}