feat: add withdrawal management tab and request processing functionality to accounting page

This commit is contained in:
sazzadulalambd
2026-05-17 20:13:33 +06:00
parent 8f445857a9
commit 89300a457e

View File

@@ -6,7 +6,8 @@ import {
DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building, DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building,
ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List, ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List,
Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet, Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet,
Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike,
Clock, Check, CheckCircle
} from 'lucide-react'; } from 'lucide-react';
export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense'; export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense';
@@ -71,6 +72,20 @@ export interface AccountingTransaction {
createdBy: string; createdBy: string;
} }
export interface WithdrawRequest {
id: string;
investorId: string;
investorName: string;
phone: string;
amount: number;
requestDate: string;
status: 'pending' | 'approved' | 'completed' | 'rejected';
bankName: string;
accountNo: string;
processedDate?: string;
paymentMethod?: string;
}
const defaultAccounts: ChartOfAccount[] = [ const defaultAccounts: ChartOfAccount[] = [
{ id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 }, { id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 },
{ id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 }, { id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 },
@@ -223,7 +238,7 @@ function generateAutoJournalEntries(type: TransactionType, amount: number, descr
} }
export default function AccountingPage() { export default function AccountingPage() {
const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts'>('dashboard'); const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts' | 'withdraw'>('dashboard');
const [transactions, setTransactions] = useState(mockTransactions); const [transactions, setTransactions] = useState(mockTransactions);
const [accounts] = useState(defaultAccounts); const [accounts] = useState(defaultAccounts);
const [journalEntries, setJournalEntries] = useState(mockJournalEntries); const [journalEntries, setJournalEntries] = useState(mockJournalEntries);
@@ -234,6 +249,14 @@ export default function AccountingPage() {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [editingTransaction, setEditingTransaction] = useState<AccountingTransaction | null>(null); const [editingTransaction, setEditingTransaction] = useState<AccountingTransaction | null>(null);
const [viewingTransaction, setViewingTransaction] = useState<AccountingTransaction | null>(null); const [viewingTransaction, setViewingTransaction] = useState<AccountingTransaction | null>(null);
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [payNowModal, setPayNowModal] = useState<WithdrawRequest | null>(null);
const [paymentForm, setPaymentForm] = useState({ method: 'bank', reference: '', notes: '', date: new Date().toISOString().split('T')[0] });
const [withdrawRequests, setWithdrawRequests] = useState<WithdrawRequest[]>([
{ id: 'WDR-001', investorId: 'INV-001', investorName: 'Mohammad Islam', phone: '01987654321', amount: 15000, requestDate: '2024-03-20', status: 'pending', bankName: 'City Bank', accountNo: '1234567890' },
{ id: 'WDR-002', investorId: 'INV-002', investorName: 'Rahima Begum', phone: '01876543210', amount: 25000, requestDate: '2024-03-18', status: 'approved', bankName: 'DBBL', accountNo: '9876543210', processedDate: '2024-03-19', paymentMethod: 'bank' },
{ id: 'WDR-003', investorId: 'INV-003', investorName: 'Ahmed Hassan', phone: '01765432109', amount: 8000, requestDate: '2024-03-15', status: 'completed', bankName: 'bKash', accountNo: '01765432109', processedDate: '2024-03-16', paymentMethod: 'mobile' },
]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10; const itemsPerPage = 10;
@@ -417,7 +440,8 @@ export default function AccountingPage() {
{ id: 'transactions', label: 'Transactions', icon: Receipt }, { id: 'transactions', label: 'Transactions', icon: Receipt },
{ id: 'journal', label: 'Journal', icon: BookOpen }, { id: 'journal', label: 'Journal', icon: BookOpen },
{ id: 'ledger', label: 'Ledger', icon: List }, { id: 'ledger', label: 'Ledger', icon: List },
{ id: 'accounts', label: 'Chart of Accounts', icon: Calculator }, { id: 'accounts', label: 'Accounts', icon: Calculator },
{ id: 'withdraw', label: 'Withdraw', icon: ArrowDownLeft },
]; ];
return ( return (
@@ -453,6 +477,11 @@ export default function AccountingPage() {
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
{tab.label} {tab.label}
{tab.id === 'withdraw' && withdrawRequests.filter(w => w.status === 'pending').length > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-xs font-medium bg-orange-100 text-orange-700 rounded-full">
{withdrawRequests.filter(w => w.status === 'pending').length}
</span>
)}
</button> </button>
); );
})} })}
@@ -547,6 +576,289 @@ export default function AccountingPage() {
{activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />} {activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />}
{activeTab === 'accounts' && <AccountsView accounts={accounts} />} {activeTab === 'accounts' && <AccountsView accounts={accounts} />}
{activeTab === 'withdraw' && (
<div className="space-y-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-slate-800">Withdraw Management</h2>
<p className="text-sm text-slate-500">Process investor withdrawal requests</p>
</div>
<button
onClick={() => setShowWithdrawModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
<Plus className="w-5 h-5" />
<span>New Withdraw Request</span>
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'pending').length}</p>
<p className="text-sm text-slate-500">Pending</p>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Check className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'approved').length}</p>
<p className="text-sm text-slate-500">Approved</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'completed').length}</p>
<p className="text-sm text-slate-500">Completed</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<TrendingDown className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.reduce((sum, w) => sum + w.amount, 0).toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Amount</p>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th className="px-4 py-3 text-left font-semibold text-slate-600">ID</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Investor</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Phone</th>
<th className="px-4 py-3 text-right font-semibold text-slate-600">Amount</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Bank/Method</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Request Date</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Status</th>
<th className="px-4 py-3 text-center font-semibold text-slate-600">Actions</th>
</tr>
</thead>
<tbody>
{withdrawRequests.map((req) => (
<tr key={req.id} className="border-b border-slate-50 hover:bg-slate-50">
<td className="px-4 py-3 font-medium text-slate-800">{req.id}</td>
<td className="px-4 py-3">
<div>
<p className="font-medium text-slate-800">{req.investorName}</p>
<p className="text-xs text-slate-500">{req.investorId}</p>
</div>
</td>
<td className="px-4 py-3 text-slate-600">{req.phone}</td>
<td className="px-4 py-3 text-right font-bold text-slate-800">{req.amount.toLocaleString()}</td>
<td className="px-4 py-3">
<p className="text-slate-600">{req.bankName}</p>
<p className="text-xs text-slate-400">{req.accountNo}</p>
</td>
<td className="px-4 py-3 text-slate-600">{req.requestDate}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${
req.status === 'pending' ? 'bg-orange-100 text-orange-700' :
req.status === 'approved' ? 'bg-blue-100 text-blue-700' :
req.status === 'completed' ? 'bg-green-100 text-green-700' :
'bg-red-100 text-red-700'
}`}>
{req.status === 'pending' && <Clock className="w-3 h-3" />}
{req.status === 'approved' && <Check className="w-3 h-3" />}
{req.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{req.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-1">
{req.status === 'pending' && (
<>
<button
onClick={() => setWithdrawRequests(prev => prev.map(w => w.id === req.id ? { ...w, status: 'approved', processedDate: new Date().toISOString().split('T')[0] } : w))}
className="p-1.5 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200"
title="Approve"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={() => setWithdrawRequests(prev => prev.map(w => w.id === req.id ? { ...w, status: 'rejected' } : w))}
className="p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200"
title="Reject"
>
<X className="w-4 h-4" />
</button>
</>
)}
{req.status === 'approved' && (
<button
onClick={() => { setPayNowModal(req); setPaymentForm({ method: 'bank', reference: `PAY-${req.id}`, notes: '', date: new Date().toISOString().split('T')[0] }); }}
className="px-3 py-1.5 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 flex items-center gap-1"
>
<DollarSign className="w-3 h-3" /> Pay Now
</button>
)}
{req.status === 'completed' && (
<div className="flex items-center gap-1">
<span className="text-xs text-green-600 font-medium">Paid</span>
<button onClick={() => window.print()} className="p-1 text-slate-400 hover:text-blue-600" title="Print Invoice">
<Printer className="w-4 h-4" />
</button>
</div>
)}
{req.status === 'rejected' && (
<span className="text-xs text-red-600 font-medium">Rejected</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{showWithdrawModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">New Withdraw Request</h3>
<button onClick={() => setShowWithdrawModal(false)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor ID</label>
<input type="text" placeholder="INV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone</label>
<input type="tel" placeholder="01XXXXXXXXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor Name</label>
<input type="text" placeholder="Enter name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Amount ()</label>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Name</label>
<input type="text" placeholder="Bank name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Number</label>
<input type="text" placeholder="Account number" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Submit Request</button>
</div>
</div>
</div>
)}
</div>
)}
{payNowModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Process Payment</h3>
<button onClick={() => setPayNowModal(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Paying to</p>
<p className="font-medium text-slate-800">{payNowModal.investorName}</p>
<p className="text-sm text-slate-600">{payNowModal.bankName} - {payNowModal.accountNo}</p>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Amount</p>
<p className="text-xl font-bold text-blue-600">{payNowModal.amount.toLocaleString()}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Date</label>
<input type="date" value={paymentForm.date} onChange={(e) => setPaymentForm(p => ({ ...p, date: e.target.value }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select value={paymentForm.method} onChange={(e) => setPaymentForm(p => ({ ...p, method: e.target.value }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Reference No.</label>
<input type="text" value={paymentForm.reference} onChange={(e) => setPaymentForm(p => ({ ...p, reference: e.target.value }))} placeholder="e.g. TRX-123456" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Notes (Optional)</label>
<textarea value={paymentForm.notes} onChange={(e) => setPaymentForm(p => ({ ...p, notes: e.target.value }))} placeholder="Add any notes..." className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} />
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setPayNowModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={() => {
const newTransaction: AccountingTransaction = {
id: `TXN-${Date.now()}`,
date: paymentForm.date,
type: 'investor_withdraw',
amount: payNowModal.amount,
description: `Investor Withdrawal - ${payNowModal.investorName}`,
beneficiary: payNowModal.investorName,
beneficiaryPhone: payNowModal.phone,
paymentMethod: paymentForm.method as 'cash' | 'bank' | 'mobile',
reference: paymentForm.reference,
notes: paymentForm.notes,
createdAt: new Date().toISOString(),
createdBy: 'Admin'
};
setTransactions(prev => [newTransaction, ...prev]);
setWithdrawRequests(prev => prev.map(w => w.id === payNowModal.id ? { ...w, status: 'completed' as const, processedDate: paymentForm.date, paymentMethod: paymentForm.method } : w));
setPayNowModal(null);
}} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Banknote className="w-4 h-4" /> Complete Payment
</button>
</div>
</div>
</div>
)}
<TransactionModal <TransactionModal
isOpen={showModal} isOpen={showModal}
onClose={() => { setShowModal(false); setEditingTransaction(null); }} onClose={() => { setShowModal(false); setEditingTransaction(null); }}