feat: add payment tracking and manual payment submission to investment details and configure standalone deployment mode
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,3 +50,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
**/docs
|
**/docs
|
||||||
**/.docs
|
**/.docs
|
||||||
|
|
||||||
|
**/deploy.zip
|
||||||
|
**/deploy
|
||||||
20
deploy.sh
Executable file
20
deploy.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
echo "Building project..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Create a deployment folder
|
||||||
|
echo "Preparing deployment files..."
|
||||||
|
mkdir -p deploy
|
||||||
|
cp -r .next deploy/
|
||||||
|
cp -r public deploy/
|
||||||
|
cp server.js deploy/
|
||||||
|
cp package.json deploy/
|
||||||
|
cp next.config.ts deploy/
|
||||||
|
|
||||||
|
# Optional: Zip the files
|
||||||
|
echo "Zipping deployment files..."
|
||||||
|
cd deploy && zip -r ../deploy.zip . && cd ..
|
||||||
|
|
||||||
|
echo "Done! Upload 'deploy.zip' to your cPanel directory and follow the guide."
|
||||||
@@ -8,6 +8,7 @@ const withPWA = withPWAInit({
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
|||||||
19
server.js
Normal file
19
server.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const { createServer } = require('http')
|
||||||
|
const { parse } = require('url')
|
||||||
|
const next = require('next')
|
||||||
|
|
||||||
|
const dev = process.env.NODE_ENV !== 'production'
|
||||||
|
const app = next({ dev })
|
||||||
|
const handle = app.getRequestHandler()
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000
|
||||||
|
|
||||||
|
app.prepare().then(() => {
|
||||||
|
createServer((req, res) => {
|
||||||
|
const parsedUrl = parse(req.url, true)
|
||||||
|
handle(req, res, parsedUrl)
|
||||||
|
}).listen(port, (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log(`> Ready on http://localhost:${port}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -13,6 +13,16 @@ import { investors } from '@/data/mockData';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import InvestorNotification from '@/components/InvestorNotification';
|
import InvestorNotification from '@/components/InvestorNotification';
|
||||||
|
|
||||||
|
interface PaymentRecord {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
amount: number;
|
||||||
|
installmentNo: number | null;
|
||||||
|
type: 'full' | 'partial' | 'installment';
|
||||||
|
method: string;
|
||||||
|
status: 'completed' | 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
export default function InvestorInvestmentDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function InvestorInvestmentDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const resolvedParams = use(params);
|
const resolvedParams = use(params);
|
||||||
const { id: investmentId } = resolvedParams;
|
const { id: investmentId } = resolvedParams;
|
||||||
@@ -23,7 +33,12 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState('overview');
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||||
const [selectedInstallment, setSelectedInstallment] = useState<'full' | '2' | '3'>('3');
|
const [paymentAmount, setPaymentAmount] = useState('');
|
||||||
|
|
||||||
|
const paymentHistory: PaymentRecord[] = [
|
||||||
|
{ id: 'pay1', date: '2024-01-15', amount: 400000, installmentNo: 1, type: 'installment', method: 'Bank Transfer', status: 'completed' },
|
||||||
|
{ id: 'pay2', date: '2024-02-15', amount: 150000, installmentNo: null, type: 'partial', method: 'bKash', status: 'completed' },
|
||||||
|
];
|
||||||
|
|
||||||
if (!investment) {
|
if (!investment) {
|
||||||
return (
|
return (
|
||||||
@@ -42,6 +57,9 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPaid = paymentHistory.reduce((sum, p) => p.status === 'completed' ? sum + p.amount : sum, 0);
|
||||||
|
const dueAmount = investment.totalInvestment - totalPaid;
|
||||||
|
|
||||||
const planConfig: Record<string, { badge: string }> = {
|
const planConfig: Record<string, { badge: string }> = {
|
||||||
silver: { badge: 'bg-slate-200 text-slate-700' },
|
silver: { badge: 'bg-slate-200 text-slate-700' },
|
||||||
gold: { badge: 'bg-amber-100 text-amber-700' },
|
gold: { badge: 'bg-amber-100 text-amber-700' },
|
||||||
@@ -70,16 +88,23 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
{ id: 'tx8', date: '2024-05-08', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
|
{ id: 'tx8', date: '2024-05-08', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const dueAmount = investment.totalInvestment * 0.33;
|
|
||||||
const paidAmount = investment.totalInvestment * 0.67;
|
|
||||||
|
|
||||||
const handlePaymentSubmit = () => {
|
const handlePaymentSubmit = () => {
|
||||||
toast.success(`Payment of ৳${(dueAmount / (selectedInstallment === '2' ? 2 : 3)).toLocaleString()} initiated successfully!`);
|
const amount = parseFloat(paymentAmount);
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
toast.error('Please enter a valid amount');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount > dueAmount) {
|
||||||
|
toast.error('Amount exceeds due amount');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(`Payment of ৳${amount.toLocaleString()} submitted successfully!`);
|
||||||
setShowPaymentModal(false);
|
setShowPaymentModal(false);
|
||||||
|
setPaymentAmount('');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen lg:pt-6 pt-0 ">
|
<div className="min-h-screen pb-20 lg:pb-0">
|
||||||
<InvestorNotification isMobile />
|
<InvestorNotification isMobile />
|
||||||
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-20 lg:mb-0">
|
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-20 lg:mb-0">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -152,6 +177,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
{[
|
{[
|
||||||
{ key: 'overview', label: 'Overview', icon: FileText, count: null },
|
{ key: 'overview', label: 'Overview', icon: FileText, count: null },
|
||||||
{ key: 'bikes', label: 'Bikes', icon: Bike, count: demoBikes.length },
|
{ key: 'bikes', label: 'Bikes', icon: Bike, count: demoBikes.length },
|
||||||
|
{ key: 'payments', label: 'Payments', icon: Wallet, count: null },
|
||||||
{ key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
|
{ key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
|
||||||
{ key: 'statement', label: 'Statement', icon: Receipt, count: null },
|
{ key: 'statement', label: 'Statement', icon: Receipt, count: null },
|
||||||
|
|
||||||
@@ -346,6 +372,62 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'payments' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
|
<p className="text-sm text-slate-500">{paymentHistory.length} payments made</p>
|
||||||
|
<button onClick={() => setShowPaymentModal(true)} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2">
|
||||||
|
<DollarSign className="w-4 h-4" /> Make Payment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="hidden lg:table w-full text-left">
|
||||||
|
<thead className="bg-slate-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Date</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Type</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Installment</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Method</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase text-right">Amount</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{paymentHistory.map((payment) => (
|
||||||
|
<tr key={payment.id} className="hover:bg-slate-50 transition-colors">
|
||||||
|
<td className="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">{payment.date}</td>
|
||||||
|
<td className="px-4 py-3"><span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span></td>
|
||||||
|
<td className="px-4 py-3 text-sm text-slate-600">{payment.installmentNo ? `#${payment.installmentNo}` : '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-slate-600">{payment.method}</td>
|
||||||
|
<td className="px-4 py-3 text-sm font-bold text-green-600 text-right">৳{payment.amount.toLocaleString()}</td>
|
||||||
|
<td className="px-4 py-3"><span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:hidden space-y-3">
|
||||||
|
{paymentHistory.map((payment) => (
|
||||||
|
<div key={payment.id} className="p-4 bg-white rounded-xl border border-slate-200">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-800">৳{payment.amount.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-slate-400">{payment.date} • {payment.method}</p>
|
||||||
|
</div>
|
||||||
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span>
|
||||||
|
{payment.installmentNo && <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-blue-100 text-blue-600">Inst #{payment.installmentNo}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,55 +436,44 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl">
|
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl">
|
||||||
<div className="flex items-center justify-between p-5 border-b border-slate-100">
|
<div className="flex items-center justify-between p-5 border-b border-slate-100">
|
||||||
<h3 className="text-lg font-bold text-slate-800">Pay Due Amount</h3>
|
<h3 className="text-lg font-bold text-slate-800">Make Payment</h3>
|
||||||
<button onClick={() => setShowPaymentModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
|
<button onClick={() => setShowPaymentModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
|
||||||
<X className="w-5 h-5 text-slate-400" />
|
<X className="w-5 h-5 text-slate-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 space-y-4">
|
<div className="p-5 space-y-4">
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-center">
|
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||||
<p className="text-sm text-amber-700 mb-1">Total Due</p>
|
<div className="flex justify-between mb-2">
|
||||||
<p className="text-3xl font-bold text-amber-600">৳{dueAmount.toLocaleString()}</p>
|
<span className="text-sm text-slate-500">Total Investment</span>
|
||||||
|
<span className="text-sm font-semibold text-slate-800">৳{investment.totalInvestment.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-sm text-slate-500">Already Paid</span>
|
||||||
|
<span className="text-sm font-semibold text-green-600">৳{totalPaid.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between pt-2 border-t border-slate-200">
|
||||||
|
<span className="text-sm font-semibold text-slate-800">Due Amount</span>
|
||||||
|
<span className="text-sm font-bold text-amber-600">৳{dueAmount.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-700 mb-3">Installment List</p>
|
<label className="text-sm font-semibold text-slate-700 mb-2 block">Enter Amount</label>
|
||||||
<div className="space-y-2">
|
<div className="relative">
|
||||||
<div className="p-3 bg-green-50 border border-green-200 rounded-lg flex items-center justify-between">
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400">৳</span>
|
||||||
<div className="flex items-center gap-2">
|
<input type="number" value={paymentAmount} onChange={(e) => setPaymentAmount(e.target.value)}
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
placeholder="Enter amount"
|
||||||
<div>
|
className="w-full pl-8 pr-4 py-3 border border-slate-200 rounded-xl text-lg font-semibold focus:outline-none focus:border-investor" />
|
||||||
<p className="text-sm font-semibold text-slate-800">Installment 1</p>
|
</div>
|
||||||
<p className="text-xs text-slate-500">Paid on Jan 15, 2024</p>
|
<div className="flex gap-2 mt-2">
|
||||||
</div>
|
<button onClick={() => setPaymentAmount(dueAmount.toString())} className="px-3 py-1 text-xs bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">Full Due</button>
|
||||||
</div>
|
<button onClick={() => setPaymentAmount((dueAmount / 2).toString())} className="px-3 py-1 text-xs bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">Half</button>
|
||||||
<span className="text-sm font-bold text-green-600">৳{(investment.totalInvestment * 0.4).toLocaleString()}</span>
|
<button onClick={() => setPaymentAmount((dueAmount / 4).toString())} className="px-3 py-1 text-xs bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">25%</button>
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-amber-50 border border-amber-300 rounded-lg flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className="w-4 h-4 text-amber-600" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-semibold text-slate-800">Installment 2</p>
|
|
||||||
<p className="text-xs text-amber-600">Due: Jun 15, 2024</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-bold text-amber-600">৳{(investment.totalInvestment * 0.3).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-amber-50 border border-amber-300 rounded-lg flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className="w-4 h-4 text-amber-600" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-semibold text-slate-800">Installment 3</p>
|
|
||||||
<p className="text-xs text-amber-600">Due: Jul 15, 2024</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-bold text-amber-600">৳{(investment.totalInvestment * 0.3).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<p className="text-sm font-semibold text-slate-700 mb-2">Select Payment</p>
|
<label className="text-sm font-semibold text-slate-700 mb-2 block">Payment Method</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
|
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
|
||||||
<Building2 className="w-5 h-5 mx-auto text-slate-600 mb-1" />
|
<Building2 className="w-5 h-5 mx-auto text-slate-600 mb-1" />
|
||||||
@@ -416,7 +487,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={handlePaymentSubmit} className="w-full py-3 bg-investor text-white rounded-xl font-bold hover:bg-investor-dark transition-colors">
|
<button onClick={handlePaymentSubmit} className="w-full py-3 bg-investor text-white rounded-xl font-bold hover:bg-investor-dark transition-colors">
|
||||||
Pay Now ৳{(investment.totalInvestment * 0.3).toLocaleString()}
|
Pay ৳{paymentAmount ? parseFloat(paymentAmount).toLocaleString() : '0'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user