feat: implement automated double-entry accounting for investments and add transaction details modal
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user