LMS-BGN/src/features/payroll-reward-system/components/WalletBalance.tsx

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>
);
};