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 [partSearch, setPartSearch] = useState('');
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 [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
const [newNoteText, setNewNoteText] = useState('');
@@ -351,7 +353,11 @@ export default function MaintenanceDetailPage() {
if (!record) return;
import('jspdf').then(jsPDF => {
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]}`;
doc.setFontSize(20);
@@ -402,11 +408,22 @@ export default function MaintenanceDetailPage() {
doc.setFontSize(11);
doc.text('Cost Breakdown', 20, 175);
doc.setFontSize(10);
doc.text(`Estimated Cost: ৳${record.estimatedCost}`, 20, 181);
if (record.actualCost) doc.text(`Actual Cost: ৳${record.actualCost}`, 20, 187);
if (record.partsUsed && record.partsUsed.length > 0) {
doc.text(`Parts: ${record.partsUsed.join(', ')}`, 20, 193);
doc.text(`Parts Total: ৳${partsTotal.toLocaleString()}`, 20, 181);
doc.text(`Service Cost (Labor): ৳${serviceCost.toLocaleString()}`, 20, 187);
doc.text(`Subtotal: ৳${subtotal.toLocaleString()}`, 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.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">
<Edit className="w-4 h-4" /> Edit
</button>
<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 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">
<Edit className="w-4 h-4" /> Edit
</button>
)}
{!invoiceCreated && (
<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
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"
>
<Check className="w-4 h-4" /> Complete
<FileText className="w-4 h-4" /> Create Invoice
</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' && (
<button
onClick={() => setShowPaymentModal(true)}
@@ -494,10 +538,10 @@ export default function MaintenanceDetailPage() {
)}
{record.paymentStatus === 'paid' && (
<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"
>
<Printer className="w-4 h-4" /> Invoice
<Printer className="w-4 h-4" /> Print Invoice
</button>
)}
</>
@@ -762,11 +806,37 @@ export default function MaintenanceDetailPage() {
{record.partsUsed?.map((part) => (
<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-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 font-medium text-orange-700">{part.totalPrice.toLocaleString()}</td>
<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" />
</button>
</td>
@@ -846,7 +916,7 @@ export default function MaintenanceDetailPage() {
<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">
<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>
<p className="text-xs text-slate-500">{record.id} {record.bikeModel}</p>
</div>
@@ -865,7 +935,7 @@ export default function MaintenanceDetailPage() {
<span className="font-medium text-blue-600">{(record.serviceCost || 0).toLocaleString()}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-slate-500 mb-1 block">Add Tips ()</label>
@@ -890,7 +960,7 @@ export default function MaintenanceDetailPage() {
/>
</div>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
<div className="flex justify-between items-center mb-2">
<span className="text-slate-600">Subtotal:</span>
@@ -915,7 +985,7 @@ export default function MaintenanceDetailPage() {
</div>
</div>
</div>
<p className="text-xs text-slate-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> After payment, this maintenance will be marked as completed
</p>
@@ -923,10 +993,10 @@ export default function MaintenanceDetailPage() {
<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); setShowPaymentModal(true); }}
onClick={() => { setInvoiceCreated(true); setShowCompleteModal(false); }}
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>
</div>
</div>
@@ -970,22 +1040,26 @@ export default function MaintenanceDetailPage() {
<Wallet className="w-5 h-5 text-green-600" />
</div>
<div className="text-left">
<p className="font-medium text-slate-800">Cash</p>
<p className="text-xs text-slate-500">Debit Cash (1100) Credit Maintenance (5400)</p>
<p className="font-medium text-slate-800">Offline (Cash)</p>
<p className="text-xs text-slate-500">Pay with cash</p>
</div>
</div>
</button>
<button
onClick={() => handlePayment('biker')}
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-colors"
onClick={() => {
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="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<User className="w-5 h-5 text-purple-600" />
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-600" />
</div>
<div className="text-left">
<p className="font-medium text-slate-800">Biker Wallet</p>
<p className="text-xs text-slate-500">Deduct from rider wallet</p>
<p className="font-medium text-slate-800">Pay Later</p>
<p className="text-xs text-slate-500">Mark as pending payment</p>
</div>
</div>
</button>
@@ -1219,7 +1293,7 @@ export default function MaintenanceDetailPage() {
</div>
<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
<button
onClick={() => {
const cost = parseFloat(serviceCostInput) || 0;
setRecord(prev => prev ? { ...prev, serviceCost: cost } : null);
@@ -1244,7 +1318,7 @@ export default function MaintenanceDetailPage() {
</div>
<h3 className="text-2xl font-bold text-green-600 mb-2">Payment Successful!</h3>
<p className="text-slate-600 mb-4">Maintenance has been completed</p>
<div className="bg-slate-50 p-4 rounded-lg text-left mb-6">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Maintenance ID</span>
@@ -1263,7 +1337,7 @@ export default function MaintenanceDetailPage() {
<span className="text-lg font-bold text-green-600">{((record?.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0) + (record?.serviceCost || 0) + invoiceData.tips - invoiceData.discount).toLocaleString()}</span>
</div>
</div>
<button
onClick={() => { setShowPaymentSuccess(false); setInvoiceData({ tips: 0, discount: 0 }); }}
className="px-6 py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700"