feat: implement automated double-entry accounting for investments and add transaction details modal

This commit is contained in:
sazzadulalambd
2026-04-26 14:56:12 +06:00
parent ae94ce0427
commit 7457b997ef
16 changed files with 8809 additions and 201 deletions

View File

@@ -8,7 +8,7 @@ import type { Investor } from '@/data/mockData';
import {
ArrowLeft, Wallet, TrendingUp, Banknote, Calendar, Phone, Mail, MapPin, Edit, Trash2, Plus, X, Bike,
User, FileText, CreditCard, DollarSign, Clock, ChevronDown, ExternalLink, Download, Upload,
AlertTriangle, Shield, Star, CheckCircle, XCircle, Search, Filter
AlertTriangle, Shield, Star, CheckCircle, XCircle, Search, Filter, BookOpen, ArrowRight, Printer
} from 'lucide-react';
const statusColors: Record<string, string> = {
@@ -54,6 +54,10 @@ export default function InvestorDetailPage() {
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
const [selectedBikeId, setSelectedBikeId] = useState('');
const [showCreateInvestmentModal, setShowCreateInvestmentModal] = useState(false);
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
const [showJournalModal, setShowJournalModal] = useState(false);
const [selectedInvoice, setSelectedInvoice] = useState<any>(null);
const [investorJournals, setInvestorJournals] = useState<any[]>([]);
const [showBankModal, setShowBankModal] = useState(false);
const [showMobileBankingModal, setShowMobileBankingModal] = useState(false);
const [showTaxModal, setShowTaxModal] = useState(false);
@@ -101,8 +105,40 @@ export default function InvestorDetailPage() {
};
const handleCreateInvestment = () => {
const invId = `ip${Date.now()}`;
const transactionId = `INV/${new Date().getFullYear()}/${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`;
const invId = `INV-${Date.now()}`;
const year = new Date().getFullYear();
const transactionRef = newInvestment.transactionReference || `INV/${year}/${String(investor.investments.length + 1).padStart(4, '0')}`;
const getDebitAccount = (method: string) => {
switch (method) {
case 'bank': return { code: '1200', name: 'Bank - City Bank' };
case 'cash': return { code: '1100', name: 'Cash in Hand' };
case 'mobile': return { code: '1300', name: 'bKash Business' };
case 'cheque': return { code: '1410', name: 'Cheque Receivable' };
default: return { code: '1200', name: 'Bank - City Bank' };
}
};
const debitAccount = getDebitAccount(newInvestment.paymentMethod);
const journalEntry = {
entryId: `JE-${Date.now()}`,
date: newInvestment.startDate,
reference: transactionRef,
description: `${investor.name} - ${newInvestment.planName}`,
entries: [
{ accountCode: debitAccount.code, accountName: debitAccount.name, debit: newInvestment.totalInvestment, credit: 0 },
{ accountCode: '2200', accountName: 'Investor Liabilities', debit: 0, credit: newInvestment.totalInvestment },
],
isAuto: true,
sourceType: 'investor_funding',
createdAt: new Date().toISOString(),
type: 'investment',
amount: newInvestment.totalInvestment,
paymentMethod: newInvestment.paymentMethod
};
setInvestorJournals([journalEntry, ...investorJournals]);
console.log('Creating Investment:', {
id: invId,
@@ -110,23 +146,26 @@ export default function InvestorDetailPage() {
...newInvestment,
actualEarnings: 0,
status: 'active' as const,
transactionId: transactionId,
transactionId: transactionRef,
createdAt: new Date().toISOString()
});
console.log('Accounting Entry:', {
entryId: `AC-${Date.now()}`,
type: 'investment_created',
investorId: investor.id,
investmentId: invId,
amount: newInvestment.totalInvestment,
debitAccount: 'Investment Asset - Investor',
creditAccount: newInvestment.paymentMethod === 'bank' ? 'Bank Account' : 'Cash Account',
transactionRef: transactionId,
createdAt: new Date().toISOString()
});
alert(`Investment created successfully!\n\nInvestment ID: ${invId}\nTransaction Ref: ${transactionId}\nAmount: ৳${newInvestment.totalInvestment.toLocaleString()}\n\nAccounting entries have been logged.`);
alert(`Investment created successfully!
Investor: ${investor.name}
Investment ID: ${invId}
Transaction Ref: ${transactionRef}
Amount: ${newInvestment.totalInvestment.toLocaleString()}
Accounting Entry Created (Auto-Journal):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Date: ${newInvestment.startDate}
Ref: ${transactionRef}
Description: ${investor.name} - ${newInvestment.planName}
Debit (Dr): ${debitAccount.name}${newInvestment.totalInvestment.toLocaleString()}
Credit (Cr): Investor Liability ৳${newInvestment.totalInvestment.toLocaleString()}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
setShowCreateInvestmentModal(false);
setNewInvestment({
planName: '',
@@ -745,7 +784,59 @@ export default function InvestorDetailPage() {
</div>
<div>
<p className="text-sm font-medium text-slate-700">{tx.description}</p>
<p className="text-xs text-slate-400">{tx.createdAt} {tx.referenceNumber && `• Ref: ${tx.referenceNumber}`}</p>
<div className="flex items-center gap-2">
<p className="text-xs text-slate-400">{tx.createdAt}</p>
{tx.referenceNumber && (
<button
onClick={() => {
const getAccountingInfo = (type: string, amount: number) => {
if (type === 'investment') {
return {
debitAccount: 'Bank - City Bank (1200)',
creditAccount: 'Investor Liabilities (2200)',
amount: amount
};
} else if (type === 'earning' || type === 'bike_earning' || type === 'investment_return') {
return {
debitAccount: 'Investor Liabilities (2200)',
creditAccount: 'Rental Income (4200)',
amount: amount
};
} else if (type === 'withdrawal') {
return {
debitAccount: 'Investor Liabilities (2200)',
creditAccount: 'Bank - City Bank (1200)',
amount: amount
};
}
return {
debitAccount: 'N/A',
creditAccount: 'N/A',
amount: amount
};
};
const accounting = getAccountingInfo(tx.type, tx.amount);
setSelectedInvoice({
reference: tx.referenceNumber,
date: tx.createdAt,
type: tx.type,
amount: tx.amount,
description: tx.description,
status: tx.status,
investorName: investor.name,
investorId: investor.id,
debitAccount: accounting.debitAccount,
creditAccount: accounting.creditAccount
});
setShowInvoiceModal(true);
}}
className="text-xs text-investor hover:underline flex items-center gap-1"
>
Ref: {tx.referenceNumber}
<BookOpen className="w-3 h-3" />
</button>
)}
</div>
</div>
</div>
<div className="text-right">
@@ -1034,14 +1125,52 @@ export default function InvestorDetailPage() {
/>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-800 mb-2">Accounting Entries to be Created:</h4>
<div className="text-sm text-blue-700 space-y-1">
<p> Debit: Investment Asset - Investor ({newInvestment.totalInvestment ? `${newInvestment.totalInvestment.toLocaleString()}` : '৳0'})</p>
<p> Credit: {newInvestment.paymentMethod === 'bank' ? 'Bank Account' : newInvestment.paymentMethod === 'mobile' ? 'Mobile Wallet' : newInvestment.paymentMethod === 'cash' ? 'Cash Account' : 'Cheque Receivable'} ({newInvestment.totalInvestment ? `${newInvestment.totalInvestment.toLocaleString()}` : '৳0'})</p>
<p> Transaction ID will be auto-generated</p>
</div>
</div>
{(() => {
const getDebitAccountDisplay = (method: string) => {
switch (method) {
case 'bank': return 'Bank - City Bank (1200)';
case 'cash': return 'Cash in Hand (1100)';
case 'mobile': return 'bKash Business (1300)';
case 'cheque': return 'Cheque Receivable (1410)';
default: return 'Bank - City Bank (1200)';
}
};
const debitDisplay = getDebitAccountDisplay(newInvestment.paymentMethod);
return (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-green-600" />
<h4 className="font-semibold text-green-800">Auto-Journal Entry</h4>
</div>
<div className="text-sm text-green-700 space-y-2">
<p className="text-xs text-green-600 uppercase font-medium mb-2">Double EntryAccounting</p>
<div className="flex items-center justify-between bg-white rounded p-2 border border-green-100">
<div>
<p className="font-medium">Debit (Dr)</p>
<p className="text-xs text-green-600">{debitDisplay}</p>
</div>
<p className="font-bold text-green-700">{newInvestment.totalInvestment.toLocaleString()}</p>
</div>
<div className="flex items-center justify-center py-1">
<div className="w-6 h-6 rounded-full bg-green-200 flex items-center justify-center">
<span className="text-green-600 text-xs"></span>
</div>
</div>
<div className="flex items-center justify-between bg-white rounded p-2 border border-green-100">
<div>
<p className="font-medium">Credit (Cr)</p>
<p className="text-xs text-green-600">Investor Liabilities (2200)</p>
</div>
<p className="font-bold text-green-700">{newInvestment.totalInvestment.toLocaleString()}</p>
</div>
<div className="mt-3 pt-2 border-t border-green-200">
<p className="text-xs text-green-600">Transaction Ref: Auto Generate</p>
</div>
</div>
</div>
);
})()}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
@@ -1212,6 +1341,202 @@ export default function InvestorDetailPage() {
</div>
</div>
)}
{showJournalModal && (
<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-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Journal Records</h2>
<button onClick={() => setShowJournalModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 overflow-y-auto flex-1">
{investorJournals.length > 0 ? (
<div className="space-y-3">
{investorJournals.map((journal, idx) => (
<div key={idx} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-slate-500">{journal.reference}</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700">Auto</span>
</div>
<span className="text-xs text-slate-500">{journal.date}</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between bg-white p-2 rounded border border-slate-200">
<div>
<p className="text-sm font-medium">Debit (Dr)</p>
<p className="text-xs text-slate-500">Bank - City Bank (1200)</p>
</div>
<p className="font-bold text-green-600">{journal.amount.toLocaleString()}</p>
</div>
<div className="flex items-center justify-center py-0.5">
<ArrowRight className="w-4 h-4 text-slate-400 rotate-90" />
</div>
<div className="flex items-center justify-between bg-white p-2 rounded border border-slate-200">
<div>
<p className="text-sm font-medium">Credit (Cr)</p>
<p className="text-xs text-slate-500">Investor Liabilities (2200)</p>
</div>
<p className="font-bold text-red-600">{journal.amount.toLocaleString()}</p>
</div>
</div>
<div className="mt-2 pt-2 border-t border-slate-200 text-xs text-slate-500">
{journal.description}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-slate-400">
<BookOpen className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No journal records yet</p>
<p className="text-sm">Create an investment to see auto-journal entries</p>
</div>
)}
</div>
</div>
</div>
)}
{showInvoiceModal && selectedInvoice && (
<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-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Invoice</h2>
<button onClick={() => setShowInvoiceModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-6" id="invoice-content">
<div className="text-center border-b border-slate-200 pb-4 mb-4">
<h1 className="text-xl font-extrabold text-investor">JAIBEN Mobility Ltd</h1>
<p className="text-xs text-slate-500">EV Rental & Investment Company</p>
</div>
<div className="flex justify-between mb-4">
<div>
<p className="text-xs text-slate-500">Invoice No</p>
<p className="text-sm font-medium">{selectedInvoice.reference}</p>
</div>
<div className="text-right">
<p className="text-xs text-slate-500">Date</p>
<p className="text-sm font-medium">{selectedInvoice.date}</p>
</div>
</div>
<div className="mb-4">
<p className="text-xs text-slate-500">Investor</p>
<p className="text-sm font-medium">{selectedInvoice.investorName}</p>
<p className="text-xs text-slate-400">{selectedInvoice.investorId}</p>
</div>
<table className="w-full mb-4">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 text-xs text-slate-500">Description</th>
<th className="text-right py-2 text-xs text-slate-500">Amount</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-slate-100">
<td className="py-3 text-sm">{selectedInvoice.description}</td>
<td className="py-3 text-sm text-right font-medium">{selectedInvoice.amount.toLocaleString()}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td className="py-2 text-sm font-medium">Total</td>
<td className="py-2 text-sm font-bold text-right">{selectedInvoice.amount.toLocaleString()}</td>
</tr>
</tfoot>
</table>
<div className="flex justify-between mb-2">
<span className="text-xs text-slate-500">Status</span>
<span className={`text-xs px-2 py-1 rounded-full ${
selectedInvoice.status === 'completed' ? 'bg-green-100 text-green-700' :
selectedInvoice.status === 'pending' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-700'
}`}>
{selectedInvoice.status}
</span>
</div>
<div className="mt-4 pt-4 border-t border-slate-200">
<p className="text-xs text-slate-500 mb-2">Double Entry Accounting (Journal)</p>
<div className="bg-slate-50 rounded-lg p-3 border border-slate-200">
<div className="flex items-center justify-between mb-2">
<div>
<p className="text-xs font-medium">Debit (Dr)</p>
<p className="text-xs text-slate-500">{selectedInvoice.debitAccount || 'N/A'}</p>
</div>
<p className="text-sm font-bold">{selectedInvoice.amount.toLocaleString()}</p>
</div>
<div className="flex items-center justify-center py-1">
<ArrowRight className="w-4 h-4 text-slate-400 rotate-90" />
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium">Credit (Cr)</p>
<p className="text-xs text-slate-500">{selectedInvoice.creditAccount || 'N/A'}</p>
</div>
<p className="text-sm font-bold">{selectedInvoice.amount.toLocaleString()}</p>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-slate-200 text-center">
<p className="text-xs text-slate-400">Thank you for your investment!</p>
<p className="text-xs text-slate-400">Generated on {new Date().toLocaleDateString()}</p>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-between">
<button
onClick={() => window.print()}
className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark flex items-center gap-2"
>
<Printer className="w-4 h-4" /> Print
</button>
<button
onClick={() => {
import('jspdf').then(jsPDF => {
const doc = new jsPDF.default();
doc.setFontSize(18);
doc.setTextColor(6, 95, 70);
doc.text('JAIBEN Mobility Ltd', 20, 20);
doc.setFontSize(12);
doc.setTextColor(0);
doc.text('Invoice: ' + selectedInvoice.reference, 20, 30);
doc.text('Date: ' + selectedInvoice.date, 20, 38);
doc.text('Investor: ' + selectedInvoice.investorName, 20, 46);
doc.text('Description: ' + selectedInvoice.description, 20, 54);
doc.text('Amount: ৳' + selectedInvoice.amount.toLocaleString(), 20, 64);
doc.text('Status: ' + selectedInvoice.status, 20, 72);
if (selectedInvoice.debitAccount) {
doc.setFontSize(10);
doc.setTextColor(100);
doc.text('Double Entry Accounting:', 20, 85);
doc.text('Dr: ' + selectedInvoice.debitAccount + ' ৳' + selectedInvoice.amount.toLocaleString(), 20, 93);
doc.text('Cr: ' + selectedInvoice.creditAccount + ' ৳' + selectedInvoice.amount.toLocaleString(), 20, 101);
}
doc.setFontSize(9);
doc.setTextColor(150);
doc.text('Thank you for your investment!', 20, 115);
doc.save(`invoice-${selectedInvoice.reference}.pdf`);
});
}}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
>
<Download className="w-4 h-4" /> PDF
</button>
<button onClick={() => setShowInvoiceModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Close</button>
</div>
</div>
</div>
)}
</div>
);
}