317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useService } from '../../../core/di/ServiceContainer';
|
|
import { IWalletService } from '../services/interfaces/IWalletService';
|
|
import {
|
|
UserWallet,
|
|
WalletTransaction,
|
|
TransactionType,
|
|
TransactionStatus,
|
|
WalletBalanceSummary
|
|
} from '../types/wallet.types';
|
|
|
|
interface WalletBalanceProps {
|
|
userId?: string;
|
|
className?: string;
|
|
showTransactions?: boolean;
|
|
}
|
|
|
|
export const WalletBalance: React.FC<WalletBalanceProps> = ({
|
|
userId = 'current-user',
|
|
className = '',
|
|
showTransactions = true
|
|
}) => {
|
|
const walletService = useService<IWalletService>('wallet-service');
|
|
const [wallet, setWallet] = useState<UserWallet | null>(null);
|
|
const [transactions, setTransactions] = useState<WalletTransaction[]>([]);
|
|
const [balanceSummary, setBalanceSummary] = useState<WalletBalanceSummary | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showAllTransactions, setShowAllTransactions] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (walletService) {
|
|
loadWalletData();
|
|
}
|
|
}, [userId, walletService]);
|
|
|
|
const loadWalletData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
if (!walletService) {
|
|
throw new Error('Wallet service is not available');
|
|
}
|
|
|
|
// Load or create wallet
|
|
let userWallet = await walletService.getWalletByUserId(userId);
|
|
if (!userWallet) {
|
|
userWallet = await walletService.createWallet({
|
|
userId,
|
|
currency: 'IDR'
|
|
});
|
|
}
|
|
|
|
// Load balance summary
|
|
const summary = await walletService.getWalletBalance(userId);
|
|
|
|
// Load recent transactions
|
|
const recentTransactions = await walletService.getTransactionHistory(
|
|
userId,
|
|
1, // page
|
|
showAllTransactions ? 50 : 10 // limit
|
|
);
|
|
|
|
setWallet(userWallet);
|
|
setBalanceSummary(summary);
|
|
setTransactions(recentTransactions.transactions);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load wallet data');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR'
|
|
}).format(amount);
|
|
};
|
|
|
|
const getTransactionTypeLabel = (type: TransactionType) => {
|
|
const labels = {
|
|
[TransactionType.CREDIT]: 'Kredit',
|
|
[TransactionType.DEBIT]: 'Debit'
|
|
};
|
|
return labels[type] || type;
|
|
};
|
|
|
|
const getTransactionIcon = (type: TransactionType) => {
|
|
const icons = {
|
|
[TransactionType.CREDIT]: '💰',
|
|
[TransactionType.DEBIT]: '💸'
|
|
};
|
|
return icons[type] || '💳';
|
|
};
|
|
|
|
const getStatusBadge = (status: TransactionStatus) => {
|
|
const statusConfig = {
|
|
[TransactionStatus.PENDING]: { label: 'Menunggu', className: 'bg-yellow-100 text-yellow-800' },
|
|
[TransactionStatus.COMPLETED]: { label: 'Selesai', className: 'bg-green-100 text-green-800' },
|
|
[TransactionStatus.FAILED]: { label: 'Gagal', className: 'bg-red-100 text-red-800' },
|
|
[TransactionStatus.CANCELLED]: { label: 'Dibatalkan', className: 'bg-gray-100 text-gray-800' }
|
|
};
|
|
|
|
const config = statusConfig[status];
|
|
return (
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.className}`}>
|
|
{config.label}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const isPositiveTransaction = (type: TransactionType) => {
|
|
return type === TransactionType.CREDIT;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
|
<div className="animate-pulse">
|
|
<div className="h-6 bg-gray-200 rounded mb-4"></div>
|
|
<div className="h-32 bg-gray-200 rounded mb-6"></div>
|
|
{showTransactions && (
|
|
<div className="space-y-3">
|
|
{[1, 2, 3].map(i => (
|
|
<div key={i} className="h-16 bg-gray-200 rounded"></div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
|
<div className="text-center text-red-600">
|
|
<p className="text-lg font-semibold mb-2">Error</p>
|
|
<p>{error}</p>
|
|
<button
|
|
onClick={loadWalletData}
|
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
>
|
|
Coba Lagi
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-2xl font-bold text-gray-900">Wallet Saya</h2>
|
|
{!wallet?.isActive && (
|
|
<span className="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
|
|
🔒 Dibekukan
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Balance Card */}
|
|
{wallet && balanceSummary && (
|
|
<div className="bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl p-6 text-white mb-6">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div>
|
|
<p className="text-purple-100 text-sm mb-1">Saldo Tersedia</p>
|
|
<p className="text-3xl font-bold">{formatCurrency(balanceSummary.availableBalance)}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-purple-100 text-sm">Wallet ID</p>
|
|
<p className="text-sm font-mono">{wallet.id.substring(0, 8)}...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-purple-400">
|
|
<div>
|
|
<p className="text-purple-100 text-sm">Total Balance</p>
|
|
<p className="text-lg font-semibold">{formatCurrency(balanceSummary.totalBalance)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-purple-100 text-sm">Pending</p>
|
|
<p className="text-lg font-semibold">{formatCurrency(balanceSummary.pendingCredits)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Quick Stats */}
|
|
{balanceSummary && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div className="bg-green-50 rounded-lg p-4 text-center">
|
|
<div className="text-2xl mb-1">📈</div>
|
|
<p className="text-sm text-gray-600">Kredit Pending</p>
|
|
<p className="font-semibold text-green-600">
|
|
{formatCurrency(balanceSummary.pendingCredits)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-red-50 rounded-lg p-4 text-center">
|
|
<div className="text-2xl mb-1">📉</div>
|
|
<p className="text-sm text-gray-600">Debit Pending</p>
|
|
<p className="font-semibold text-red-600">
|
|
{formatCurrency(balanceSummary.pendingDebits)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 rounded-lg p-4 text-center">
|
|
<div className="text-2xl mb-1">🔄</div>
|
|
<p className="text-sm text-gray-600">Total Transaksi</p>
|
|
<p className="font-semibold text-blue-600">
|
|
{transactions.length}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-yellow-50 rounded-lg p-4 text-center">
|
|
<div className="text-2xl mb-1">⏳</div>
|
|
<p className="text-sm text-gray-600">Mata Uang</p>
|
|
<p className="font-semibold text-yellow-600">
|
|
{balanceSummary.currency}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transactions */}
|
|
{showTransactions && (
|
|
<div>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-semibold">Riwayat Transaksi</h3>
|
|
{transactions.length > 10 && (
|
|
<button
|
|
onClick={() => setShowAllTransactions(!showAllTransactions)}
|
|
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
|
>
|
|
{showAllTransactions ? 'Tampilkan Sedikit' : 'Lihat Semua'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{transactions.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<div className="text-4xl mb-2">💳</div>
|
|
<p>Belum ada transaksi</p>
|
|
<p className="text-sm">Transaksi akan muncul di sini</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{transactions.map((transaction) => (
|
|
<div
|
|
key={transaction.id}
|
|
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50"
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
<div className="text-2xl">
|
|
{getTransactionIcon(transaction.type)}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-900">
|
|
{getTransactionTypeLabel(transaction.type)}
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
{transaction.description}
|
|
</p>
|
|
<p className="text-xs text-gray-400">
|
|
{transaction.createdAt.toLocaleString('id-ID')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<p className={`font-semibold ${
|
|
isPositiveTransaction(transaction.type)
|
|
? 'text-green-600'
|
|
: 'text-red-600'
|
|
}`}>
|
|
{isPositiveTransaction(transaction.type) ? '+' : '-'}
|
|
{formatCurrency(Math.abs(transaction.amount))}
|
|
</p>
|
|
<div className="mt-1">
|
|
{getStatusBadge(transaction.status)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="mt-6 flex space-x-3">
|
|
<button
|
|
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 font-medium"
|
|
onClick={() => {/* Handle top up */}}
|
|
>
|
|
💰 Top Up
|
|
</button>
|
|
<button
|
|
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 font-medium"
|
|
onClick={() => {/* Handle withdraw */}}
|
|
>
|
|
💸 Withdraw
|
|
</button>
|
|
<button
|
|
className="flex-1 bg-purple-600 text-white py-2 px-4 rounded-lg hover:bg-purple-700 font-medium"
|
|
onClick={() => {/* Handle transfer */}}
|
|
>
|
|
🔄 Transfer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |