feat: implement maintenance invoice creation flow with dynamic pricing, quantity editing, and PDF breakdown reporting

This commit is contained in:
sazzadulalambd
2026-05-17 00:45:04 +06:00
parent 0274e9a90b
commit fb1eff4931

View File

@@ -287,6 +287,8 @@ export default function MaintenanceDetailPage() {
const [showAddServiceCostModal, setShowAddServiceCostModal] = useState(false); const [showAddServiceCostModal, setShowAddServiceCostModal] = useState(false);
const [partSearch, setPartSearch] = useState(''); const [partSearch, setPartSearch] = useState('');
const [invoiceData, setInvoiceData] = useState({ tips: 0, discount: 0 }); const [invoiceData, setInvoiceData] = useState({ tips: 0, discount: 0 });
const [invoiceCreated, setInvoiceCreated] = useState(false);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<'online' | 'offline' | 'later' | null>(null);
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false); const [showPaymentSuccess, setShowPaymentSuccess] = useState(false);
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({}); const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
const [newNoteText, setNewNoteText] = useState(''); const [newNoteText, setNewNoteText] = useState('');
@@ -351,7 +353,11 @@ export default function MaintenanceDetailPage() {
if (!record) return; if (!record) return;
import('jspdf').then(jsPDF => { import('jspdf').then(jsPDF => {
const doc = new jsPDF.default(); const doc = new jsPDF.default();
const cost = record.actualCost || record.estimatedCost; const partsTotal = record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0;
const serviceCost = record.serviceCost || 0;
const subtotal = partsTotal + serviceCost;
const total = subtotal + invoiceData.tips - invoiceData.discount;
const cost = total || record.estimatedCost;
const qrData = `INV-${record.id}|${record.bikePlate}|${record.type}|${cost}|${new Date().toISOString().split('T')[0]}`; const qrData = `INV-${record.id}|${record.bikePlate}|${record.type}|${cost}|${new Date().toISOString().split('T')[0]}`;
doc.setFontSize(20); doc.setFontSize(20);
@@ -402,11 +408,22 @@ export default function MaintenanceDetailPage() {
doc.setFontSize(11); doc.setFontSize(11);
doc.text('Cost Breakdown', 20, 175); doc.text('Cost Breakdown', 20, 175);
doc.setFontSize(10); doc.setFontSize(10);
doc.text(`Estimated Cost: ৳${record.estimatedCost}`, 20, 181); doc.text(`Parts Total: ৳${partsTotal.toLocaleString()}`, 20, 181);
if (record.actualCost) doc.text(`Actual Cost: ৳${record.actualCost}`, 20, 187); doc.text(`Service Cost (Labor): ৳${serviceCost.toLocaleString()}`, 20, 187);
if (record.partsUsed && record.partsUsed.length > 0) { doc.text(`Subtotal: ৳${subtotal.toLocaleString()}`, 20, 193);
doc.text(`Parts: ${record.partsUsed.join(', ')}`, 20, 193); if (invoiceData.tips > 0) {
doc.setTextColor(22, 163, 74);
doc.text(`Tips: +৳${invoiceData.tips.toLocaleString()}`, 20, 199);
doc.setTextColor(100);
} }
if (invoiceData.discount > 0) {
doc.setTextColor(220, 38, 38);
doc.text(`Discount: -৳${invoiceData.discount.toLocaleString()}`, 20, 205);
doc.setTextColor(100);
}
doc.setFontSize(12);
doc.setTextColor(0);
doc.text(`Total: ৳${total.toLocaleString()}`, 20, 215);
doc.setFontSize(14); doc.setFontSize(14);
doc.setTextColor(6, 95, 70); doc.setTextColor(6, 95, 70);
@@ -470,20 +487,47 @@ export default function MaintenanceDetailPage() {
</> </>
) : ( ) : (
<> <>
<button onClick={() => setEditMode(true)} 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"> {!invoiceCreated && (
<Edit className="w-4 h-4" /> Edit <button onClick={() => setEditMode(true)} 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">
</button> <Edit className="w-4 h-4" /> Edit
<button onClick={() => setShowAddNoteModal(true)} 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"> </button>
<MessageSquare className="w-4 h-4" /> Note )}
</button> {!invoiceCreated && (
{record.status !== 'completed' && ( <button onClick={() => setShowAddNoteModal(true)} 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">
<MessageSquare className="w-4 h-4" /> Note
</button>
)}
{record.status !== 'completed' && !invoiceCreated && (
<button <button
onClick={() => setShowCompleteModal(true)} onClick={() => setShowCompleteModal(true)}
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2" className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
> >
<Check className="w-4 h-4" /> Complete <FileText className="w-4 h-4" /> Create Invoice
</button> </button>
)} )}
{record.status !== 'completed' && invoiceCreated && (
<>
<button
onClick={handleGenerateInvoice}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
>
<Printer className="w-4 h-4" /> Print Invoice
</button>
<button
onClick={() => setShowPaymentModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
>
<DollarSign className="w-4 h-4" /> Proceed to Payment
</button>
<button
onClick={() => setShowCompleteModal(true)}
className="px-3 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50"
title="Edit Invoice"
>
<Edit className="w-4 h-4" />
</button>
</>
)}
{record.status === 'completed' && record.paymentStatus !== 'paid' && ( {record.status === 'completed' && record.paymentStatus !== 'paid' && (
<button <button
onClick={() => setShowPaymentModal(true)} onClick={() => setShowPaymentModal(true)}
@@ -494,10 +538,10 @@ export default function MaintenanceDetailPage() {
)} )}
{record.paymentStatus === 'paid' && ( {record.paymentStatus === 'paid' && (
<button <button
onClick={() => setShowInvoiceModal(true)} onClick={handleGenerateInvoice}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2" className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
> >
<Printer className="w-4 h-4" /> Invoice <Printer className="w-4 h-4" /> Print Invoice
</button> </button>
)} )}
</> </>
@@ -762,11 +806,37 @@ export default function MaintenanceDetailPage() {
{record.partsUsed?.map((part) => ( {record.partsUsed?.map((part) => (
<tr key={part.id} className="bg-white border-b border-orange-100"> <tr key={part.id} className="bg-white border-b border-orange-100">
<td className="px-3 py-2 text-slate-700">{part.partName}</td> <td className="px-3 py-2 text-slate-700">{part.partName}</td>
<td className="px-3 py-2 text-center text-slate-600">{part.quantity}</td> <td className="px-3 py-2 text-center">
<input
type="number"
min="1"
value={part.quantity}
onChange={(e) => {
const newQty = Math.max(1, parseInt(e.target.value) || 1);
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.map(p =>
p.id === part.id
? { ...p, quantity: newQty, totalPrice: newQty * p.unitPrice }
: p
)
} : null);
}}
className="w-16 px-2 py-1 border border-orange-200 rounded text-center text-sm"
/>
</td>
<td className="px-3 py-2 text-right text-slate-600">{part.unitPrice.toLocaleString()}</td> <td className="px-3 py-2 text-right text-slate-600">{part.unitPrice.toLocaleString()}</td>
<td className="px-3 py-2 text-right font-medium text-orange-700">{part.totalPrice.toLocaleString()}</td> <td className="px-3 py-2 text-right font-medium text-orange-700">{part.totalPrice.toLocaleString()}</td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
<button className="text-red-400 hover:text-red-600 p-1"> <button
onClick={() => {
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.filter(p => p.id !== part.id)
} : null);
}}
className="text-red-400 hover:text-red-600 p-1"
>
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</td> </td>
@@ -846,7 +916,7 @@ export default function MaintenanceDetailPage() {
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg"> <div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
<div className="p-4 border-b border-slate-100 bg-gradient-to-r from-green-50 to-emerald-50"> <div className="p-4 border-b border-slate-100 bg-gradient-to-r from-green-50 to-emerald-50">
<h3 className="font-semibold text-slate-800 flex items-center gap-2"> <h3 className="font-semibold text-slate-800 flex items-center gap-2">
<FileText className="w-5 h-5 text-green-600" /> Maintenance Invoice <FileText className="w-5 h-5 text-green-600" /> {invoiceCreated ? 'Edit Invoice' : 'Create Invoice'}
</h3> </h3>
<p className="text-xs text-slate-500">{record.id} {record.bikeModel}</p> <p className="text-xs text-slate-500">{record.id} {record.bikeModel}</p>
</div> </div>
@@ -923,10 +993,10 @@ export default function MaintenanceDetailPage() {
<div className="p-4 border-t border-slate-100 flex justify-end gap-2"> <div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setShowCompleteModal(false); setInvoiceData({ tips: 0, discount: 0 }); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button> <button onClick={() => { setShowCompleteModal(false); setInvoiceData({ tips: 0, discount: 0 }); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button <button
onClick={() => { setShowCompleteModal(false); setShowPaymentModal(true); }} onClick={() => { setInvoiceCreated(true); setShowCompleteModal(false); }}
className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2" className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2"
> >
<FileText className="w-4 h-4" /> Proceed to Payment <FileText className="w-4 h-4" /> {invoiceCreated ? 'Update Invoice' : 'Create Invoice'}
</button> </button>
</div> </div>
</div> </div>
@@ -970,22 +1040,26 @@ export default function MaintenanceDetailPage() {
<Wallet className="w-5 h-5 text-green-600" /> <Wallet className="w-5 h-5 text-green-600" />
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="font-medium text-slate-800">Cash</p> <p className="font-medium text-slate-800">Offline (Cash)</p>
<p className="text-xs text-slate-500">Debit Cash (1100) Credit Maintenance (5400)</p> <p className="text-xs text-slate-500">Pay with cash</p>
</div> </div>
</div> </div>
</button> </button>
<button <button
onClick={() => handlePayment('biker')} onClick={() => {
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-colors" if (!record) return;
setRecord(prev => prev ? { ...prev, paymentStatus: 'pending' } : null);
setShowPaymentModal(false);
}}
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-orange-500 hover:bg-orange-50 transition-colors"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center"> <div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<User className="w-5 h-5 text-purple-600" /> <Clock className="w-5 h-5 text-orange-600" />
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="font-medium text-slate-800">Biker Wallet</p> <p className="font-medium text-slate-800">Pay Later</p>
<p className="text-xs text-slate-500">Deduct from rider wallet</p> <p className="text-xs text-slate-500">Mark as pending payment</p>
</div> </div>
</div> </div>
</button> </button>
@@ -1219,7 +1293,7 @@ export default function MaintenanceDetailPage() {
</div> </div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2"> <div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowAddServiceCostModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button> <button onClick={() => setShowAddServiceCostModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button <button
onClick={() => { onClick={() => {
const cost = parseFloat(serviceCostInput) || 0; const cost = parseFloat(serviceCostInput) || 0;
setRecord(prev => prev ? { ...prev, serviceCost: cost } : null); setRecord(prev => prev ? { ...prev, serviceCost: cost } : null);