feat: implement automated battery rental invoicing and journal entry tracking with print support
This commit is contained in:
@@ -6,7 +6,7 @@ import Link from 'next/link';
|
|||||||
import {
|
import {
|
||||||
ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle,
|
ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle,
|
||||||
Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin,
|
Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin,
|
||||||
Phone, MessageCircle, Play, Check, X, FileText, Download, Battery
|
Phone, MessageCircle, Play, Check, X, FileText, Download, Battery, Printer
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit,
|
canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit,
|
||||||
@@ -86,9 +86,35 @@ interface BatteryRentalHistory {
|
|||||||
assignedAt: string;
|
assignedAt: string;
|
||||||
returnedAt?: string;
|
returnedAt?: string;
|
||||||
monthlyRent: number;
|
monthlyRent: number;
|
||||||
|
deposit: number;
|
||||||
|
depositMethod: 'cash' | 'bank' | 'bkash' | 'nagad';
|
||||||
|
invoiceId: string;
|
||||||
|
invoiceGeneratedAt: string;
|
||||||
status: 'active' | 'returned';
|
status: 'active' | 'returned';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Invoice {
|
||||||
|
id: string;
|
||||||
|
type: 'ev_rental' | 'battery_rental';
|
||||||
|
relatedId: string; // rental id or battery history id
|
||||||
|
amount: number;
|
||||||
|
deposit?: number;
|
||||||
|
generatedAt: string;
|
||||||
|
status: 'paid' | 'unpaid';
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JournalEntry {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
description: string;
|
||||||
|
debit: string;
|
||||||
|
credit: string;
|
||||||
|
amount: number;
|
||||||
|
referenceId: string;
|
||||||
|
type: 'battery_deposit' | 'battery_rent' | 'ev_deposit' | 'ev_rent';
|
||||||
|
}
|
||||||
|
|
||||||
interface LockEvent {
|
interface LockEvent {
|
||||||
id: string;
|
id: string;
|
||||||
action: 'locked' | 'unlocked';
|
action: 'locked' | 'unlocked';
|
||||||
@@ -162,8 +188,8 @@ const mockRentals: Rental[] = [
|
|||||||
{ id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' },
|
{ id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' },
|
||||||
],
|
],
|
||||||
batteryHistory: [
|
batteryHistory: [
|
||||||
{ id: 'BAT-RENT-001', batteryId: 'BAT-DH-001', batteryName: 'Galaxy 72V 45Ah', assignedAt: '2024-01-16', monthlyRent: 1500, status: 'active' },
|
{ id: 'BAT-RENT-001', batteryId: 'BAT-DH-001', batteryName: 'Galaxy 72V 45Ah', assignedAt: '2024-01-16', monthlyRent: 1500, deposit: 3000, depositMethod: 'bkash' as const, invoiceId: 'INV-BAT-001', invoiceGeneratedAt: '2024-01-16', status: 'active' as const },
|
||||||
{ id: 'BAT-RENT-002', batteryId: 'BAT-DH-002', batteryName: 'Titan 72V 50Ah', assignedAt: '2024-02-20', returnedAt: '2024-03-15', monthlyRent: 1800, status: 'returned' },
|
{ id: 'BAT-RENT-002', batteryId: 'BAT-DH-002', batteryName: 'Titan 72V 50Ah', assignedAt: '2024-02-20', returnedAt: '2024-03-15', monthlyRent: 1800, deposit: 3500, depositMethod: 'cash' as const, invoiceId: 'INV-BAT-002', invoiceGeneratedAt: '2024-02-20', status: 'returned' as const },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,6 +380,21 @@ export default function RentalDetailPage() {
|
|||||||
const [uploadDocName, setUploadDocName] = useState('');
|
const [uploadDocName, setUploadDocName] = useState('');
|
||||||
const [showAddBatteryModal, setShowAddBatteryModal] = useState(false);
|
const [showAddBatteryModal, setShowAddBatteryModal] = useState(false);
|
||||||
const [selectedBatteryId, setSelectedBatteryId] = useState('');
|
const [selectedBatteryId, setSelectedBatteryId] = useState('');
|
||||||
|
const [batteryDeposit, setBatteryDeposit] = useState(0);
|
||||||
|
const [batteryDepositMethod, setBatteryDepositMethod] = useState<'cash' | 'bank' | 'bkash' | 'nagad'>('cash');
|
||||||
|
const [showBatteryInvoicePreview, setShowBatteryInvoicePreview] = useState<Invoice | null>(null);
|
||||||
|
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
|
||||||
|
const [downloadSuccess, setDownloadSuccess] = useState(false);
|
||||||
|
const [invoices, setInvoices] = useState<Invoice[]>([
|
||||||
|
{ id: 'INV-EV-001', type: 'ev_rental', relatedId: 'RNT-001', amount: 3000, deposit: 3000, generatedAt: '2024-01-15', status: 'paid', description: 'EV Rental Deposit — Jamal Uddin (AIMA Lightning)' },
|
||||||
|
{ id: 'INV-BAT-001', type: 'battery_rental', relatedId: 'BAT-RENT-001', amount: 1500, deposit: 3000, generatedAt: '2024-01-16', status: 'paid', description: 'Battery Rental — Galaxy 72V 45Ah (Deposit + 1st Month)' },
|
||||||
|
{ id: 'INV-BAT-002', type: 'battery_rental', relatedId: 'BAT-RENT-002', amount: 1800, deposit: 3500, generatedAt: '2024-02-20', status: 'paid', description: 'Battery Rental — Titan 72V 50Ah (Deposit + 1st Month)' },
|
||||||
|
]);
|
||||||
|
const [journalEntries, setJournalEntries] = useState<JournalEntry[]>([
|
||||||
|
{ id: 'JRN-001', date: '2024-01-15', description: 'EV Rental Deposit received from Jamal Uddin', debit: 'Cash / Bank', credit: 'Rental Deposit Liability', amount: 3000, referenceId: 'INV-EV-001', type: 'ev_deposit' },
|
||||||
|
{ id: 'JRN-002', date: '2024-01-16', description: 'Battery Deposit received — Galaxy 72V 45Ah', debit: 'Cash / Bank', credit: 'Battery Deposit Liability', amount: 3000, referenceId: 'INV-BAT-001', type: 'battery_deposit' },
|
||||||
|
{ id: 'JRN-003', date: '2024-02-20', description: 'Battery Deposit received — Titan 72V 50Ah', debit: 'Cash / Bank', credit: 'Battery Deposit Liability', amount: 3500, referenceId: 'INV-BAT-002', type: 'battery_deposit' },
|
||||||
|
]);
|
||||||
|
|
||||||
const [acceptPermission, setAcceptPermission] = useState(false);
|
const [acceptPermission, setAcceptPermission] = useState(false);
|
||||||
const [rejectPermission, setRejectPermission] = useState(false);
|
const [rejectPermission, setRejectPermission] = useState(false);
|
||||||
@@ -511,15 +552,60 @@ export default function RentalDetailPage() {
|
|||||||
const battery = mockBatteries.find(b => b.id === selectedBatteryId);
|
const battery = mockBatteries.find(b => b.id === selectedBatteryId);
|
||||||
if (!battery) return;
|
if (!battery) return;
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const newInvId = `INV-BAT-${Date.now().toString().slice(-6)}`;
|
||||||
|
const newBatHistId = `BAT-RENT-${Date.now()}`;
|
||||||
|
|
||||||
|
// 1. Create battery rental history entry
|
||||||
const newBatteryHistory: BatteryRentalHistory = {
|
const newBatteryHistory: BatteryRentalHistory = {
|
||||||
id: `BAT-RENT-${Date.now()}`,
|
id: newBatHistId,
|
||||||
batteryId: battery.id,
|
batteryId: battery.id,
|
||||||
batteryName: `${battery.brand} ${battery.model}`,
|
batteryName: `${battery.brand} ${battery.model}`,
|
||||||
assignedAt: new Date().toISOString().split('T')[0],
|
assignedAt: today,
|
||||||
monthlyRent: battery.monthlyRent,
|
monthlyRent: battery.monthlyRent,
|
||||||
|
deposit: batteryDeposit,
|
||||||
|
depositMethod: batteryDepositMethod,
|
||||||
|
invoiceId: newInvId,
|
||||||
|
invoiceGeneratedAt: today,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 2. Generate invoice
|
||||||
|
const newInvoice: Invoice = {
|
||||||
|
id: newInvId,
|
||||||
|
type: 'battery_rental',
|
||||||
|
relatedId: newBatHistId,
|
||||||
|
amount: battery.monthlyRent,
|
||||||
|
deposit: batteryDeposit,
|
||||||
|
generatedAt: today,
|
||||||
|
status: 'paid',
|
||||||
|
description: `Battery Rental — ${battery.brand} ${battery.model} (Deposit ৳${batteryDeposit} + 1st Month ৳${battery.monthlyRent})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Auto journal entries
|
||||||
|
const journalDeposit: JournalEntry = {
|
||||||
|
id: `JRN-${Date.now()}-DEP`,
|
||||||
|
date: today,
|
||||||
|
description: `Battery Deposit received — ${battery.brand} ${battery.model}`,
|
||||||
|
debit: 'Cash / Bank',
|
||||||
|
credit: 'Battery Deposit Liability',
|
||||||
|
amount: batteryDeposit,
|
||||||
|
referenceId: newInvId,
|
||||||
|
type: 'battery_deposit',
|
||||||
|
};
|
||||||
|
const journalRent: JournalEntry = {
|
||||||
|
id: `JRN-${Date.now()}-RENT`,
|
||||||
|
date: today,
|
||||||
|
description: `1st Month Battery Rent — ${battery.brand} ${battery.model}`,
|
||||||
|
debit: 'Cash / Bank',
|
||||||
|
credit: 'Battery Rent Revenue',
|
||||||
|
amount: battery.monthlyRent,
|
||||||
|
referenceId: newInvId,
|
||||||
|
type: 'battery_rent',
|
||||||
|
};
|
||||||
|
|
||||||
|
setInvoices(prev => [...prev, newInvoice]);
|
||||||
|
setJournalEntries(prev => [...prev, journalDeposit, journalRent]);
|
||||||
setRental(prev => prev ? {
|
setRental(prev => prev ? {
|
||||||
...prev,
|
...prev,
|
||||||
batteryId: battery.id,
|
batteryId: battery.id,
|
||||||
@@ -527,8 +613,12 @@ export default function RentalDetailPage() {
|
|||||||
batteryRent: (prev.batteryRent || 0) + battery.monthlyRent,
|
batteryRent: (prev.batteryRent || 0) + battery.monthlyRent,
|
||||||
batteryHistory: [...(prev.batteryHistory || []), newBatteryHistory],
|
batteryHistory: [...(prev.batteryHistory || []), newBatteryHistory],
|
||||||
} : null);
|
} : null);
|
||||||
|
|
||||||
|
setShowBatteryInvoicePreview(newInvoice);
|
||||||
setShowAddBatteryModal(false);
|
setShowAddBatteryModal(false);
|
||||||
setSelectedBatteryId('');
|
setSelectedBatteryId('');
|
||||||
|
setBatteryDeposit(0);
|
||||||
|
setBatteryDepositMethod('cash');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddNote = () => {
|
const handleAddNote = () => {
|
||||||
@@ -559,6 +649,25 @@ export default function RentalDetailPage() {
|
|||||||
setShowSmsModal(false);
|
setShowSmsModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePrintInvoiceDirect = (inv: Invoice) => {
|
||||||
|
setShowBatteryInvoicePreview(inv);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSimulatedPDFDownload = (invId: string) => {
|
||||||
|
setIsDownloadingPDF(true);
|
||||||
|
setDownloadSuccess(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsDownloadingPDF(false);
|
||||||
|
setDownloadSuccess(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDownloadSuccess(false);
|
||||||
|
}, 2000);
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
const statusBadge = getStatusBadge(rental.status);
|
const statusBadge = getStatusBadge(rental.status);
|
||||||
const typeBadge = getTypeBadge(rental.type);
|
const typeBadge = getTypeBadge(rental.type);
|
||||||
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus);
|
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus);
|
||||||
@@ -579,6 +688,47 @@ export default function RentalDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
||||||
|
{/* Global `@media print` style sheet overrides */}
|
||||||
|
<style dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
@media print {
|
||||||
|
/* Hide the screen scrollbar and all default content layout wrapper */
|
||||||
|
body, html {
|
||||||
|
background-color: white !important;
|
||||||
|
color: black !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide standard layout wrapper tags */
|
||||||
|
body * {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show exclusively our paper canvas sheet */
|
||||||
|
#printable-invoice-modal, #printable-invoice-modal * {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#printable-invoice-modal {
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: none !important;
|
||||||
|
background: white !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suppress all interactive controls from printouts */
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}} />
|
||||||
<Link href="/admin/rentals" className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4">
|
<Link href="/admin/rentals" className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4">
|
||||||
<ArrowLeft className="w-4 h-4" /> Back to Rentals
|
<ArrowLeft className="w-4 h-4" /> Back to Rentals
|
||||||
</Link>
|
</Link>
|
||||||
@@ -766,9 +916,9 @@ export default function RentalDetailPage() {
|
|||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm font-medium text-emerald-700">Total (Bike + Battery)</span>
|
<span className="text-sm font-medium text-emerald-700">Total (Bike + Battery)</span>
|
||||||
<span className="text-sm font-bold text-emerald-700">
|
<span className="text-sm font-bold text-emerald-700">
|
||||||
৳{rental.subscriptionType === 'daily'
|
৳{rental.subscriptionType === 'daily'
|
||||||
? rental.dailyRate + Math.round(rental.batteryRent / 30)
|
? rental.dailyRate + Math.round(rental.batteryRent / 30)
|
||||||
: rental.subscriptionType === 'weekly'
|
: rental.subscriptionType === 'weekly'
|
||||||
? rental.weeklyRate + Math.round(rental.batteryRent / 4)
|
? rental.weeklyRate + Math.round(rental.batteryRent / 4)
|
||||||
: rental.monthlyRate + rental.batteryRent}/
|
: rental.monthlyRate + rental.batteryRent}/
|
||||||
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
|
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
|
||||||
@@ -798,9 +948,9 @@ export default function RentalDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Battery Rental History */}
|
{/* Battery Rental History */}
|
||||||
<div className="bg-white p-4 rounded-xl border border-slate-200">
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-amber-50">
|
||||||
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
|
<h3 className="font-semibold text-amber-800 flex items-center gap-2">
|
||||||
<Battery className="w-5 h-5 text-amber-500" /> Battery Rental History
|
<Battery className="w-5 h-5 text-amber-500" /> Battery Rental History
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={() => setShowAddBatteryModal(true)} className="px-3 py-1.5 bg-amber-600 text-white rounded-lg text-xs font-medium hover:bg-amber-700 flex items-center gap-1">
|
<button onClick={() => setShowAddBatteryModal(true)} className="px-3 py-1.5 bg-amber-600 text-white rounded-lg text-xs font-medium hover:bg-amber-700 flex items-center gap-1">
|
||||||
@@ -812,102 +962,189 @@ export default function RentalDetailPage() {
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-slate-50">
|
<thead className="bg-slate-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery ID</th>
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery Name</th>
|
|
||||||
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned</th>
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned</th>
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Deposit</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Invoice</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
|
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
|
||||||
|
<th className="px-3 py-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100">
|
<tbody className="divide-y divide-slate-100">
|
||||||
{rental.batteryHistory.map(bat => (
|
{rental.batteryHistory.map(bat => (
|
||||||
<tr key={bat.id} className="hover:bg-slate-50">
|
<tr key={bat.id} className="hover:bg-slate-50">
|
||||||
<td className="px-3 py-2 text-sm text-slate-700 font-mono">{bat.batteryId}</td>
|
<td className="px-3 py-2">
|
||||||
<td className="px-3 py-2 text-sm text-slate-700">{bat.batteryName}</td>
|
<p className="text-sm font-medium text-slate-800">{bat.batteryName}</p>
|
||||||
|
<p className="text-xs text-slate-400 font-mono">{bat.batteryId}</p>
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2 text-sm text-slate-600">{bat.assignedAt}</td>
|
<td className="px-3 py-2 text-sm text-slate-600">{bat.assignedAt}</td>
|
||||||
<td className="px-3 py-2 text-sm text-slate-600">{bat.returnedAt || '-'}</td>
|
<td className="px-3 py-2 text-sm text-slate-600">{bat.returnedAt || <span className="text-slate-300">—</span>}</td>
|
||||||
<td className="px-3 py-2 text-sm font-medium text-green-600">৳{bat.monthlyRent}</td>
|
<td className="px-3 py-2">
|
||||||
|
<p className="text-sm font-medium text-purple-700">৳{bat.deposit?.toLocaleString() ?? '—'}</p>
|
||||||
|
<p className="text-xs text-slate-400 capitalize">{bat.depositMethod}</p>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm font-semibold text-emerald-600">৳{bat.monthlyRent}/mo</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const inv = invoices.find(i => i.id === bat.invoiceId);
|
||||||
|
if (inv) setShowBatteryInvoicePreview(inv);
|
||||||
|
}}
|
||||||
|
className="text-xs text-blue-600 hover:underline font-mono flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3" />{bat.invoiceId}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${bat.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'}`}>
|
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${bat.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'}`}>
|
||||||
{bat.status === 'active' ? 'Active' : 'Returned'}
|
{bat.status === 'active' ? 'Active' : 'Returned'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
{bat.status === 'active' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
setRental(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
batteryHistory: prev.batteryHistory?.map(b =>
|
||||||
|
b.id === bat.id ? { ...b, status: 'returned' as const, returnedAt: today } : b
|
||||||
|
),
|
||||||
|
} : null);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs hover:bg-slate-200"
|
||||||
|
>
|
||||||
|
Return
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-slate-500">No battery assigned yet.</p>
|
<div className="px-4 py-6 text-center">
|
||||||
|
<Battery className="w-8 h-8 text-slate-200 mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-slate-400">No battery assigned yet.</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{rental.batteryHistory?.some(b => b.status === 'active') && (
|
{rental.batteryHistory?.some(b => b.status === 'active') && (
|
||||||
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
<div className="m-3 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-center justify-between">
|
||||||
<p className="text-sm text-amber-700">
|
<p className="text-sm text-amber-700">
|
||||||
<span className="font-medium">Active Battery Rent: </span>
|
<span className="font-semibold">Active Battery Rent: </span>
|
||||||
৳{rental.batteryHistory.filter(b => b.status === 'active').reduce((sum, b) => sum + b.monthlyRent, 0)}/month
|
৳{rental.batteryHistory.filter(b => b.status === 'active').reduce((sum, b) => sum + b.monthlyRent, 0).toLocaleString()}/month
|
||||||
</p>
|
</p>
|
||||||
|
<span className="text-xs text-amber-500">{rental.batteryHistory.filter(b => b.status === 'active').length} active</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Battery Modal */}
|
{/* Add Battery Modal */}
|
||||||
{showAddBatteryModal && (
|
{showAddBatteryModal && (
|
||||||
<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 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg">
|
||||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
<div className="p-5 border-b border-slate-100 flex justify-between items-center bg-amber-50 rounded-t-2xl">
|
||||||
<h3 className="font-semibold text-slate-800">Add Battery to Rental</h3>
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={() => setShowAddBatteryModal(false)} className="text-slate-400 hover:text-slate-600">
|
<Battery className="w-5 h-5 text-amber-600" />
|
||||||
|
<h3 className="font-bold text-amber-900">Add Battery to Rental</h3>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => { setShowAddBatteryModal(false); setSelectedBatteryId(''); setBatteryDeposit(0); }} className="text-slate-400 hover:text-slate-600">
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-5 space-y-4">
|
||||||
|
{/* Battery Select */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm text-slate-600 mb-1 block">Select Battery</label>
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Select Battery</label>
|
||||||
<select
|
<select
|
||||||
value={selectedBatteryId}
|
value={selectedBatteryId}
|
||||||
onChange={(e) => setSelectedBatteryId(e.target.value)}
|
onChange={(e) => setSelectedBatteryId(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Select Battery...</option>
|
<option value="">Choose a battery...</option>
|
||||||
{mockBatteries.map(bat => (
|
{mockBatteries.map(bat => (
|
||||||
<option key={bat.id} value={bat.id}>{bat.brand} {bat.model} - SOC: {bat.soc}% - Rent: ৳{bat.monthlyRent}/month</option>
|
<option key={bat.id} value={bat.id}>{bat.brand} {bat.model} — SOC: {bat.soc}% — ৳{bat.monthlyRent}/month</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Deposit */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Deposit Amount (৳)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={batteryDeposit}
|
||||||
|
onChange={(e) => setBatteryDeposit(parseInt(e.target.value) || 0)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
placeholder="e.g. 3000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Payment Method</label>
|
||||||
|
<select
|
||||||
|
value={batteryDepositMethod}
|
||||||
|
onChange={(e) => setBatteryDepositMethod(e.target.value as 'cash' | 'bank' | 'bkash' | 'nagad')}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="cash">Cash</option>
|
||||||
|
<option value="bank">Bank Transfer</option>
|
||||||
|
<option value="bkash">bKash</option>
|
||||||
|
<option value="nagad">Nagad</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rate preview */}
|
||||||
{selectedBatteryId && (() => {
|
{selectedBatteryId && (() => {
|
||||||
const batteryMonthlyRent = mockBatteries.find(b => b.id === selectedBatteryId)?.monthlyRent || 0;
|
const bat = mockBatteries.find(b => b.id === selectedBatteryId);
|
||||||
|
if (!bat) return null;
|
||||||
|
const rate = rental.subscriptionType === 'daily'
|
||||||
|
? Math.round(bat.monthlyRent / 30)
|
||||||
|
: rental.subscriptionType === 'weekly'
|
||||||
|
? Math.round(bat.monthlyRent / 4)
|
||||||
|
: bat.monthlyRent;
|
||||||
return (
|
return (
|
||||||
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg space-y-2">
|
<div className="p-4 bg-amber-50 border border-amber-200 rounded-xl space-y-3">
|
||||||
<p className="text-sm text-amber-700">
|
<div className="flex items-center justify-between">
|
||||||
Battery Monthly Rent: <span className="font-bold">৳{batteryMonthlyRent}/month</span>
|
<span className="text-sm text-amber-700 font-medium">Monthly Rent</span>
|
||||||
</p>
|
<span className="text-sm font-bold text-amber-800">৳{bat.monthlyRent.toLocaleString()}</span>
|
||||||
<div className="text-xs text-amber-600 pt-2 border-t border-amber-100">
|
|
||||||
<p className="font-medium mb-1">Calculated Rate by Subscription:</p>
|
|
||||||
<p>• Daily: ৳{Math.round(batteryMonthlyRent / 30)}/day (৳{batteryMonthlyRent}/30)</p>
|
|
||||||
<p>• Weekly: ৳{Math.round(batteryMonthlyRent / 4)}/week (৳{batteryMonthlyRent}/4)</p>
|
|
||||||
<p>• Monthly (30 days): ৳{batteryMonthlyRent}/month</p>
|
|
||||||
<p>• Monthly (31 days): ৳{batteryMonthlyRent}/month</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2 mt-2 border-t border-amber-100">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-medium text-amber-700">Your Current Subscription: {rental.subscriptionType}</p>
|
<span className="text-sm text-amber-700 font-medium">Deposit</span>
|
||||||
<p className="text-sm font-bold text-amber-800">
|
<span className="text-sm font-bold text-purple-700">৳{batteryDeposit.toLocaleString()}</span>
|
||||||
You will pay: ৳{rental.subscriptionType === 'daily'
|
</div>
|
||||||
? Math.round(batteryMonthlyRent / 30)
|
<div className="border-t border-amber-200 pt-2 flex items-center justify-between">
|
||||||
: rental.subscriptionType === 'weekly'
|
<span className="text-sm text-amber-800 font-semibold">Total Due Today</span>
|
||||||
? Math.round(batteryMonthlyRent / 4)
|
<span className="text-base font-extrabold text-amber-900">৳{(batteryDeposit + rate).toLocaleString()}</span>
|
||||||
: batteryMonthlyRent}/{rental.subscriptionType}
|
</div>
|
||||||
</p>
|
<div className="bg-white rounded-lg p-2 border border-amber-100">
|
||||||
|
<p className="text-xs text-amber-600 font-medium mb-1">Rate by Subscription ({rental.subscriptionType})</p>
|
||||||
|
<p className="text-sm font-bold text-amber-800">৳{rate}/{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-amber-500 bg-amber-50 p-2 rounded border border-amber-100">
|
||||||
|
<p className="font-semibold mb-0.5">📋 Invoice will be auto-generated</p>
|
||||||
|
<p>• Debit: Cash/Bank (৳{(batteryDeposit + rate).toLocaleString()})</p>
|
||||||
|
<p>• Credit: Battery Deposit Liability (৳{batteryDeposit.toLocaleString()}) + Battery Rent Revenue (৳{rate.toLocaleString()})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<button onClick={() => setShowAddBatteryModal(false)} className="flex-1 py-2 px-4 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
<button onClick={() => { setShowAddBatteryModal(false); setSelectedBatteryId(''); setBatteryDeposit(0); }} className="flex-1 py-2.5 px-4 border border-slate-200 text-slate-600 rounded-xl text-sm hover:bg-slate-50">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleAddBattery} disabled={!selectedBatteryId} className="flex-1 py-2 px-4 bg-amber-600 text-white rounded-lg text-sm hover:bg-amber-700 disabled:opacity-50">
|
<button
|
||||||
Add Battery
|
onClick={handleAddBattery}
|
||||||
|
disabled={!selectedBatteryId}
|
||||||
|
className="flex-1 py-2.5 px-4 bg-amber-600 text-white rounded-xl text-sm font-semibold hover:bg-amber-700 disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" /> Assign & Generate Invoice
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -915,6 +1152,243 @@ export default function RentalDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Unified Printable Invoice Viewer Modal */}
|
||||||
|
{showBatteryInvoicePreview && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 overflow-y-auto no-print">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden border border-slate-100 flex flex-col my-8 transform transition-all animate-fadeIn">
|
||||||
|
|
||||||
|
{/* Modal Title bar (no-print) */}
|
||||||
|
<div className={`p-5 border-b border-slate-100 flex justify-between items-center text-white ${showBatteryInvoicePreview.type === 'battery_rental'
|
||||||
|
? 'bg-gradient-to-r from-amber-500 to-amber-600'
|
||||||
|
: 'bg-gradient-to-r from-blue-600 to-indigo-600'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{showBatteryInvoicePreview.type === 'battery_rental' ? (
|
||||||
|
<Battery className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<Bike className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-lg">Invoice Details</h3>
|
||||||
|
<p className="text-xs opacity-90">Manage, print and download client invoice</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xs font-bold px-3 py-1 rounded-full bg-white/20 border border-white/30 tracking-wide uppercase">
|
||||||
|
{showBatteryInvoicePreview.status}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => setShowBatteryInvoicePreview(null)} className="p-1.5 text-white/80 hover:text-white transition bg-white/10 hover:bg-white/20 rounded-full">
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Physical Invoice Paper (Rendered to PDF/Printer) */}
|
||||||
|
<div className="p-8 space-y-6 overflow-y-auto max-h-[60vh] bg-slate-50/50" id="printable-invoice-modal">
|
||||||
|
|
||||||
|
{/* Invoice Banner & Branding */}
|
||||||
|
<div className="flex justify-between items-start border-b border-slate-200 pb-6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-slate-800 font-extrabold text-xl tracking-tight">
|
||||||
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center text-white ${showBatteryInvoicePreview.type === 'battery_rental' ? 'bg-amber-500' : 'bg-blue-600'
|
||||||
|
}`}>
|
||||||
|
⚡
|
||||||
|
</div>
|
||||||
|
<span>JAIBEN <span className="text-xs font-light text-slate-400">by JML Group</span></span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-2 font-medium">Gulshan Hub, House 12, Road 5</p>
|
||||||
|
<p className="text-xs text-slate-500">Gulshan-1, Dhaka 1212, Bangladesh</p>
|
||||||
|
<p className="text-xs text-slate-400">billing@jaiben.com | +880 9612-JAIBEN</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<h2 className="text-2xl font-black text-slate-800 uppercase tracking-widest">INVOICE</h2>
|
||||||
|
<div className="text-sm font-bold text-slate-700 font-mono mt-1">{showBatteryInvoicePreview.id}</div>
|
||||||
|
|
||||||
|
<div className="mt-3 text-xs text-slate-500 space-y-0.5">
|
||||||
|
<p><span className="font-semibold text-slate-700">Date:</span> {showBatteryInvoicePreview.generatedAt}</p>
|
||||||
|
<p><span className="font-semibold text-slate-700">Due Date:</span> {showBatteryInvoicePreview.generatedAt}</p>
|
||||||
|
<p><span className="font-semibold text-slate-700">Status:</span> <span className="text-emerald-600 font-bold uppercase">{showBatteryInvoicePreview.status}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Client & Billing Info */}
|
||||||
|
<div className="grid grid-cols-2 gap-6 bg-white p-4 rounded-xl border border-slate-100">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Billed To</p>
|
||||||
|
<p className="text-sm font-bold text-slate-800">{rental.userName}</p>
|
||||||
|
<p className="text-xs text-slate-600 mt-1">{rental.userPhone}</p>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5">Hub: {rental.hubName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Details</p>
|
||||||
|
<p className="text-xs text-slate-700"><span className="font-semibold text-slate-600">Rental Type:</span> <span className="capitalize">{rental.type}</span></p>
|
||||||
|
{showBatteryInvoicePreview.type === 'battery_rental' ? (
|
||||||
|
<p className="text-xs text-slate-700 mt-1"><span className="font-semibold text-slate-600">Product:</span> Battery Rent ({rental.batteryName || 'Galaxy 72V 45Ah'})</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-slate-700 mt-1"><span className="font-semibold text-slate-600">Product:</span> EV Ride Rent ({rental.bikeModel})</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-slate-700 mt-1"><span className="font-semibold text-slate-600">Contract ID:</span> <span className="font-mono">{rental.id}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoice Items Table */}
|
||||||
|
<div className="overflow-hidden border border-slate-200 rounded-xl bg-white">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-50 border-b border-slate-200">
|
||||||
|
<th className="p-3 text-xs font-bold text-slate-600 uppercase">Item Description</th>
|
||||||
|
<th className="p-3 text-xs font-bold text-slate-600 uppercase text-center">Qty</th>
|
||||||
|
<th className="p-3 text-xs font-bold text-slate-600 uppercase text-right">Price</th>
|
||||||
|
<th className="p-3 text-xs font-bold text-slate-600 uppercase text-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{/* Deposit Line Item */}
|
||||||
|
{(showBatteryInvoicePreview.deposit ?? 0) > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="p-3">
|
||||||
|
<p className="text-sm font-semibold text-slate-800">
|
||||||
|
{showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Security Deposit' : 'EV Rental Security Deposit'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-400">Refundable security deposit held for hardware safety</p>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-sm text-slate-600 text-center font-semibold">1</td>
|
||||||
|
<td className="p-3 text-sm text-slate-700 text-right font-medium">৳{showBatteryInvoicePreview.deposit?.toLocaleString()}</td>
|
||||||
|
<td className="p-3 text-sm text-slate-800 text-right font-bold">৳{showBatteryInvoicePreview.deposit?.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{/* Rent Line Item */}
|
||||||
|
{showBatteryInvoicePreview.amount > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="p-3">
|
||||||
|
<p className="text-sm font-semibold text-slate-800">
|
||||||
|
{showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Subscription Service Fee' : 'EV Rent Service Advance'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-400">Initial cycle subscription fee ({rental.subscriptionType} billing rate)</p>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-sm text-slate-600 text-center font-semibold">1</td>
|
||||||
|
<td className="p-3 text-sm text-slate-700 text-right font-medium">৳{showBatteryInvoicePreview.amount.toLocaleString()}</td>
|
||||||
|
<td className="p-3 text-sm text-slate-800 text-right font-bold">৳{showBatteryInvoicePreview.amount.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary and Payment Breakdown */}
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start gap-4 pt-2">
|
||||||
|
<div className="text-xs text-slate-500 max-w-sm">
|
||||||
|
<p className="font-bold text-slate-700 mb-1">Terms & Conditions</p>
|
||||||
|
<p className="leading-relaxed">This invoice confirms receipt of payment. Security deposits are refundable upon lease completion and inspection approval of the hardware. Rent fees are non-refundable.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-64 bg-slate-50 border border-slate-200 rounded-xl p-4 space-y-2">
|
||||||
|
<div className="flex justify-between text-sm text-slate-600">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span className="font-semibold">৳{((showBatteryInvoicePreview.deposit ?? 0) + showBatteryInvoicePreview.amount).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm text-slate-600">
|
||||||
|
<span>Tax/VAT (0%):</span>
|
||||||
|
<span>৳0</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-slate-200 pt-2 flex justify-between text-base font-bold text-slate-800">
|
||||||
|
<span>Total Paid:</span>
|
||||||
|
<span className="text-emerald-700">৳{((showBatteryInvoicePreview.deposit ?? 0) + showBatteryInvoicePreview.amount).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs text-slate-500 font-medium">
|
||||||
|
<span>Total Due:</span>
|
||||||
|
<span>৳0 (Fully Settled)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto Journal Entry / Accounting Ledger (no-print) */}
|
||||||
|
<div className="bg-emerald-50/50 border border-emerald-100 rounded-xl p-4 space-y-3 no-print">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<p className="text-xs font-bold text-emerald-800 uppercase tracking-wide flex items-center gap-1.5">
|
||||||
|
System Ledger (Auto Journal Entry)
|
||||||
|
</p>
|
||||||
|
<span className="text-[10px] bg-emerald-100 text-emerald-800 font-semibold px-2 py-0.5 rounded-full">Balanced</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5 divide-y divide-emerald-100/50 text-xs">
|
||||||
|
<div className="flex justify-between pb-1.5 font-bold text-emerald-900">
|
||||||
|
<span>Debit Account</span>
|
||||||
|
<span>Amount</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-1.5 text-emerald-800">
|
||||||
|
<span className="flex items-center gap-1">🟢 Dr: Cash / Bank Account</span>
|
||||||
|
<span className="font-semibold">৳{((showBatteryInvoicePreview.deposit ?? 0) + showBatteryInvoicePreview.amount).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between pt-1.5 pb-1 font-bold text-slate-800">
|
||||||
|
<span>Credit Account(s)</span>
|
||||||
|
<span>Amount</span>
|
||||||
|
</div>
|
||||||
|
{(showBatteryInvoicePreview.deposit ?? 0) > 0 && (
|
||||||
|
<div className="flex justify-between py-1 text-slate-600">
|
||||||
|
<span>⚪ Cr: {showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Security Deposit Liability' : 'EV Rental Security Deposit Liability'}</span>
|
||||||
|
<span>৳{showBatteryInvoicePreview.deposit?.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showBatteryInvoicePreview.amount > 0 && (
|
||||||
|
<div className="flex justify-between py-1 text-slate-600">
|
||||||
|
<span>⚪ Cr: {showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Rental Revenue Account' : 'EV Rental Revenue Account'}</span>
|
||||||
|
<span>৳{showBatteryInvoicePreview.amount.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions footer (no-print) */}
|
||||||
|
<div className="p-5 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row gap-2 justify-end no-print">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBatteryInvoicePreview(null)}
|
||||||
|
className="py-2.5 px-4 border border-slate-200 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-100 transition order-2 sm:order-1"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleSimulatedPDFDownload(showBatteryInvoicePreview.id)}
|
||||||
|
disabled={isDownloadingPDF || downloadSuccess}
|
||||||
|
className="py-2.5 px-4 bg-indigo-600 text-white rounded-xl text-sm font-semibold hover:bg-indigo-700 flex items-center justify-center gap-2 transition disabled:opacity-75 order-1 sm:order-2"
|
||||||
|
>
|
||||||
|
{isDownloadingPDF ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
Generating PDF...
|
||||||
|
</>
|
||||||
|
) : downloadSuccess ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 text-white" />
|
||||||
|
Downloaded!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
Download PDF
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.print()}
|
||||||
|
className="py-2.5 px-5 bg-emerald-600 text-white rounded-xl text-sm font-semibold hover:bg-emerald-700 flex items-center justify-center gap-2 transition order-1 sm:order-3"
|
||||||
|
>
|
||||||
|
<Printer className="w-4 h-4" />
|
||||||
|
Print Invoice
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Initial Condition Images */}
|
{/* Initial Condition Images */}
|
||||||
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
|
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
|
||||||
{rental.initialImages && (
|
{rental.initialImages && (
|
||||||
@@ -1123,38 +1597,127 @@ export default function RentalDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-4 rounded-xl border border-slate-200">
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-blue-50">
|
||||||
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
|
<h3 className="font-semibold text-blue-900 flex items-center gap-2">
|
||||||
<FileText className="w-5 h-5 text-blue-500" /> Rental Documents
|
<FileText className="w-5 h-5 text-blue-500" /> Rental Documents & Invoices
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={() => setShowUploadModal(true)} className="px-3 py-1.5 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 flex items-center gap-2">
|
<button onClick={() => setShowUploadModal(true)} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-medium hover:bg-blue-700 flex items-center gap-1">
|
||||||
<Upload className="w-4 h-4" /> Upload Document
|
<Upload className="w-3 h-3" /> Upload
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
{mockDocuments.map(doc => (
|
{/* EV Rental Invoices */}
|
||||||
<div key={doc.id} className="p-3 bg-slate-50 rounded-lg flex items-center justify-between">
|
<div className="px-4 pt-4 pb-2">
|
||||||
<div className="flex items-center gap-3">
|
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1"><Bike className="w-3.5 h-3.5" /> EV Rental Invoices</p>
|
||||||
<FileText className="w-5 h-5 text-slate-400" />
|
<div className="space-y-1.5">
|
||||||
<div>
|
{invoices.filter(inv => inv.type === 'ev_rental').length === 0 && (
|
||||||
<p className="text-sm font-medium text-slate-700">{doc.name}</p>
|
<p className="text-xs text-slate-400 italic py-1">No EV rental invoices.</p>
|
||||||
<p className="text-xs text-slate-500">{doc.uploadedAt}</p>
|
)}
|
||||||
|
{invoices.filter(inv => inv.type === 'ev_rental').map(inv => (
|
||||||
|
<div key={inv.id} className="flex items-center justify-between p-2.5 bg-blue-50 border border-blue-100 rounded-lg transition hover:shadow-sm">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
|
<FileText className="w-4 h-4 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-slate-800 font-mono">{inv.id}</p>
|
||||||
|
<p className="text-xs text-slate-500">{inv.description}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
|
<span className="text-xs text-slate-400">{inv.generatedAt}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBatteryInvoicePreview(inv)}
|
||||||
|
className="text-xs text-emerald-600 hover:text-emerald-700 hover:underline font-semibold flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
view invoice
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-bold text-blue-700">৳{(inv.amount + (inv.deposit ?? 0)).toLocaleString()}</span>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${inv.status === 'paid' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>{inv.status}</span>
|
||||||
|
<button onClick={() => setShowBatteryInvoicePreview(inv)} className="p-1.5 text-blue-600 hover:bg-blue-100 rounded-lg" title="View Invoice">
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handlePrintInvoiceDirect(inv)} className="p-1.5 text-slate-600 hover:bg-slate-100 rounded-lg" title="Print Invoice">
|
||||||
|
<Printer className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => {
|
))}
|
||||||
const link = document.createElement('a');
|
</div>
|
||||||
link.href = '#';
|
</div>
|
||||||
link.download = doc.name;
|
|
||||||
alert(`Downloading: ${doc.name}`);
|
{/* Battery Rental Invoices */}
|
||||||
}} className="px-3 py-1.5 text-blue-600 hover:bg-blue-50 rounded-lg text-sm flex items-center gap-1">
|
<div className="px-4 pt-3 pb-4">
|
||||||
<Download className="w-4 h-4" /> Download
|
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1"><Battery className="w-3.5 h-3.5" /> Battery Rental Invoices</p>
|
||||||
</button>
|
<div className="space-y-1.5">
|
||||||
<button onClick={() => alert(`Viewing: ${doc.name}`)} className="px-3 py-1.5 text-slate-600 hover:bg-slate-100 rounded-lg text-sm flex items-center gap-1">
|
{invoices.filter(inv => inv.type === 'battery_rental').length === 0 && (
|
||||||
<FileText className="w-4 h-4" /> View
|
<p className="text-xs text-slate-400 italic py-1">No battery rental invoices.</p>
|
||||||
</button>
|
)}
|
||||||
</div>
|
{invoices.filter(inv => inv.type === 'battery_rental').map(inv => (
|
||||||
))}
|
<div key={inv.id} className="flex items-center justify-between p-2.5 bg-amber-50 border border-amber-100 rounded-lg transition hover:shadow-sm">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center">
|
||||||
|
<Battery className="w-4 h-4 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-slate-800 font-mono">{inv.id}</p>
|
||||||
|
<p className="text-xs text-slate-500">{inv.description}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
|
<span className="text-xs text-slate-400">{inv.generatedAt}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBatteryInvoicePreview(inv)}
|
||||||
|
className="text-xs text-emerald-600 hover:text-emerald-700 hover:underline font-semibold flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
view invoice
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-bold text-amber-700">৳{((inv.deposit ?? 0) + inv.amount).toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-slate-400">Dep: ৳{(inv.deposit ?? 0).toLocaleString()} + Rent: ৳{inv.amount.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${inv.status === 'paid' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>{inv.status}</span>
|
||||||
|
<button onClick={() => setShowBatteryInvoicePreview(inv)} className="p-1.5 text-amber-600 hover:bg-amber-100 rounded-lg" title="View Invoice">
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handlePrintInvoiceDirect(inv)} className="p-1.5 text-slate-600 hover:bg-slate-100 rounded-lg" title="Print Invoice">
|
||||||
|
<Printer className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* General Docs */}
|
||||||
|
<div className="px-4 pb-4 border-t border-slate-100 pt-3">
|
||||||
|
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1"><FileText className="w-3.5 h-3.5" /> Other Documents</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{documents.map(doc => (
|
||||||
|
<div key={doc.id} className="flex items-center justify-between p-2.5 bg-slate-50 border border-slate-100 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<FileText className="w-4 h-4 text-slate-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-700">{doc.name}</p>
|
||||||
|
<p className="text-xs text-slate-400">{doc.uploadedAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button onClick={() => alert(`Downloading: ${doc.name}`)} className="px-2.5 py-1.5 text-blue-600 hover:bg-blue-50 rounded-lg text-xs flex items-center gap-1">
|
||||||
|
<Download className="w-3.5 h-3.5" /> Download
|
||||||
|
</button>
|
||||||
|
<button onClick={() => alert(`Viewing: ${doc.name}`)} className="px-2.5 py-1.5 text-slate-600 hover:bg-slate-100 rounded-lg text-xs flex items-center gap-1">
|
||||||
|
<FileText className="w-3.5 h-3.5" /> View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user