feat: add AI OCR processing state and conditional UI locking for maintenance records
This commit is contained in:
@@ -282,6 +282,12 @@ export default function MaintenanceDetailPage() {
|
|||||||
const [showCompleteModal, setShowCompleteModal] = useState(false);
|
const [showCompleteModal, setShowCompleteModal] = useState(false);
|
||||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||||
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
||||||
|
|
||||||
|
// AI OCR Simulation States
|
||||||
|
const [isOcrProcessing, setIsOcrProcessing] = useState(false);
|
||||||
|
const [ocrComplete, setOcrComplete] = useState(false);
|
||||||
|
const [ocrFileName, setOcrFileName] = useState('');
|
||||||
|
const [ocrStep, setOcrStep] = useState('');
|
||||||
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
||||||
const [showAddPartModal, setShowAddPartModal] = useState(false);
|
const [showAddPartModal, setShowAddPartModal] = useState(false);
|
||||||
const [partSearch, setPartSearch] = useState('');
|
const [partSearch, setPartSearch] = useState('');
|
||||||
@@ -473,77 +479,84 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-slate-500 mt-1">{typeLabels[record.type]} • {record.date}</p>
|
<p className="text-slate-500 mt-1">{typeLabels[record.type]} • {record.date}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap items-center gap-2 relative">
|
||||||
{editMode ? (
|
{ocrComplete && (
|
||||||
<>
|
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 text-xs font-bold rounded-lg mr-2 animate-fadeIn">
|
||||||
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" /> Invoice Locked (OCR Synced)
|
||||||
<Save className="w-4 h-4" /> Save
|
</div>
|
||||||
</button>
|
)}
|
||||||
<button onClick={() => setEditMode(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
<div className={`flex flex-wrap gap-2 ${ocrComplete ? 'filter blur-[1.5px] opacity-50 pointer-events-none' : ''}`}>
|
||||||
Cancel
|
{editMode ? (
|
||||||
</button>
|
<>
|
||||||
</>
|
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
||||||
) : (
|
<Save className="w-4 h-4" /> Save
|
||||||
<>
|
|
||||||
{!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>
|
</button>
|
||||||
)}
|
<button onClick={() => setEditMode(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||||
{!invoiceCreated && (
|
Cancel
|
||||||
<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>
|
</button>
|
||||||
)}
|
</>
|
||||||
{record.status !== 'completed' && !invoiceCreated && (
|
) : (
|
||||||
<button
|
<>
|
||||||
onClick={() => setShowCompleteModal(true)}
|
{!invoiceCreated && (
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
|
<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
|
||||||
<FileText className="w-4 h-4" /> Create Invoice
|
</button>
|
||||||
</button>
|
)}
|
||||||
)}
|
{!invoiceCreated && (
|
||||||
{record.status !== 'completed' && 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"
|
||||||
|
>
|
||||||
|
<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)}
|
||||||
|
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" /> Payment
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{record.paymentStatus === 'paid' && (
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerateInvoice}
|
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" /> Print Invoice
|
<Printer className="w-4 h-4" /> Print Invoice
|
||||||
</button>
|
</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"
|
)}
|
||||||
>
|
</div>
|
||||||
<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)}
|
|
||||||
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" /> Payment
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{record.paymentStatus === 'paid' && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -617,36 +630,44 @@ export default function MaintenanceDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
|
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100 relative overflow-hidden transition-all duration-300">
|
||||||
<h3 className="font-semibold text-orange-800 mb-3 flex items-center gap-2">
|
{ocrComplete && (
|
||||||
<History className="w-5 h-5" /> Issue History
|
<div className="absolute inset-0 bg-slate-900/5 backdrop-blur-[1px] flex items-center justify-center z-10 p-2">
|
||||||
</h3>
|
<span className="px-3 py-1 bg-slate-900 text-white border border-slate-700 text-[10px] font-bold rounded-lg uppercase tracking-wider flex items-center gap-1.5 shadow-md">
|
||||||
<div className="space-y-2">
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-400" /> Populated via AI OCR
|
||||||
{record.batteryId && (
|
</span>
|
||||||
<Link
|
</div>
|
||||||
href={`/admin/maintenance/history/battery/${record.batteryId}?from=${record.id}`}
|
)}
|
||||||
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
<div className={ocrComplete ? 'filter blur-[1.5px] opacity-60 pointer-events-none' : ''}>
|
||||||
>
|
<h3 className="font-semibold text-orange-800 mb-3 flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<History className="w-5 h-5" /> Issue History
|
||||||
<Battery className="w-4 h-4 text-green-600" />
|
</h3>
|
||||||
<span className="text-sm text-slate-700">Battery History</span>
|
<div className="space-y-2">
|
||||||
</div>
|
{record.batteryId && (
|
||||||
<ArrowRight className="w-4 h-4 text-orange-400" />
|
<Link
|
||||||
</Link>
|
href={`/admin/maintenance/history/battery/${record.batteryId}?from=${record.id}`}
|
||||||
)}
|
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
||||||
{record.bikeId && (
|
>
|
||||||
<Link
|
<div className="flex items-center gap-2">
|
||||||
href={`/admin/maintenance/history/bike/${record.bikeId}?from=${record.id}`}
|
<Battery className="w-4 h-4 text-green-600" />
|
||||||
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
<span className="text-sm text-slate-700">Battery History</span>
|
||||||
>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<ArrowRight className="w-4 h-4 text-orange-400" />
|
||||||
<Bike className="w-4 h-4 text-purple-600" />
|
</Link>
|
||||||
<span className="text-sm text-slate-700">Fleet History</span>
|
)}
|
||||||
</div>
|
{record.bikeId && (
|
||||||
<ArrowRight className="w-4 h-4 text-orange-400" />
|
<Link
|
||||||
</Link>
|
href={`/admin/maintenance/history/bike/${record.bikeId}?from=${record.id}`}
|
||||||
)}
|
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bike className="w-4 h-4 text-purple-600" />
|
||||||
|
<span className="text-sm text-slate-700">Fleet History</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="w-4 h-4 text-orange-400" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -724,224 +745,6 @@ export default function MaintenanceDetailPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
|
|
||||||
|
|
||||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
|
||||||
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
|
||||||
<DollarSign className="w-5 h-5" /> Cost Details
|
|
||||||
</h3>
|
|
||||||
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="font-semibold text-purple-800 flex items-center gap-2">
|
|
||||||
<DollarSign className="w-5 h-5" /> Cost Breakdown
|
|
||||||
</h3>
|
|
||||||
{editMode && (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={editForm.estimatedCost || 0}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
|
|
||||||
className="px-2 py-1 border border-purple-200 rounded text-sm w-24"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between items-center text-sm">
|
|
||||||
<span className="text-slate-600">Estimated Cost:</span>
|
|
||||||
<span className="font-medium text-purple-700">৳{record.estimatedCost.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-purple-100 pt-2">
|
|
||||||
<div className="flex justify-between items-center text-sm">
|
|
||||||
<span className="text-slate-600 flex items-center gap-1">
|
|
||||||
<Package className="w-4 h-4" /> Parts Total:
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-orange-600">৳{record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString() || 0}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center text-sm mt-1">
|
|
||||||
<span className="text-slate-600 flex items-center gap-1">
|
|
||||||
<Wrench className="w-4 h-4" /> Service Cost (Labor):
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-blue-600">৳{(record.serviceCost || 0).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t-2 border-purple-200 pt-2 mt-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="font-semibold text-purple-800">Actual Total Cost:</span>
|
|
||||||
<span className="text-xl font-bold text-purple-800">
|
|
||||||
৳{((record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0) || 0) + (record.serviceCost || 0)).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
|
|
||||||
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
|
|
||||||
<User className="w-5 h-5" /> Assigned To
|
|
||||||
</h3>
|
|
||||||
{editMode ? (
|
|
||||||
<select
|
|
||||||
value={editForm.assignedTo || ''}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
<option value="">Select Service Center</option>
|
|
||||||
<option value="Service Center A">Service Center A</option>
|
|
||||||
<option value="Service Center B">Service Center B</option>
|
|
||||||
<option value="Authorized Service Center">Authorized Service Center</option>
|
|
||||||
<option value="Gulshan Hub">Gulshan Hub</option>
|
|
||||||
<option value="Banani Hub">Banani Hub</option>
|
|
||||||
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
|
||||||
<Wrench className="w-5 h-5" /> Parts Used
|
|
||||||
</h3>
|
|
||||||
{!editMode && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAddPartModal(true)}
|
|
||||||
className="px-3 py-1 bg-orange-600 text-white text-xs rounded-lg hover:bg-orange-700 flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Plus className="w-3 h-3" /> Add Part
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{record.partsUsed && record.partsUsed.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="hidden lg:block overflow-x-auto">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-orange-100">
|
|
||||||
<tr>
|
|
||||||
<th className="px-3 py-2 text-left text-orange-800 font-medium">Part</th>
|
|
||||||
<th className="px-3 py-2 text-center text-orange-800 font-medium">Qty</th>
|
|
||||||
<th className="px-3 py-2 text-right text-orange-800 font-medium">Unit Price</th>
|
|
||||||
<th className="px-3 py-2 text-right text-orange-800 font-medium">Total</th>
|
|
||||||
<th className="px-3 py-2 text-center text-orange-800 font-medium">Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{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">
|
|
||||||
<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
|
|
||||||
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>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
{record.partsUsed.map((part) => (
|
|
||||||
<div key={part.id} className="bg-white rounded-lg border border-orange-200 p-3">
|
|
||||||
<div className="flex items-start justify-between mb-2">
|
|
||||||
<span className="font-medium text-slate-800">{part.partName}</span>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-sm mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-slate-500">Qty:</span>
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-slate-600">৳{part.unitPrice.toLocaleString()}/unit</span>
|
|
||||||
</div>
|
|
||||||
<div className="pt-2 border-t border-orange-100 flex justify-between">
|
|
||||||
<span className="text-xs text-slate-500">Total</span>
|
|
||||||
<span className="font-bold text-orange-700">৳{part.totalPrice.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-orange-400">No parts added</p>
|
|
||||||
)}
|
|
||||||
{record.partsUsed && record.partsUsed.length > 0 && (
|
|
||||||
<div className="bg-orange-50 rounded-lg p-3 flex justify-between items-center">
|
|
||||||
<span className="font-semibold text-orange-800">Parts Total:</span>
|
|
||||||
<span className="text-lg font-bold text-orange-700">
|
|
||||||
৳{record.partsUsed.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||||
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
||||||
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
|
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
|
||||||
@@ -988,6 +791,348 @@ export default function MaintenanceDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
|
||||||
|
|
||||||
|
{/* Manual Invoice OCR Card */}
|
||||||
|
<div className="bg-indigo-50/70 p-4 rounded-xl border border-indigo-100 space-y-3 relative overflow-hidden transition-all duration-300">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-semibold text-indigo-800 text-sm flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-indigo-600" /> Manual Invoice OCR Upload
|
||||||
|
</h3>
|
||||||
|
{ocrComplete && (
|
||||||
|
<span className="px-2.5 py-0.5 bg-emerald-100 border border-emerald-200 text-emerald-800 text-[10px] font-bold rounded-full uppercase tracking-wider animate-pulse">
|
||||||
|
OCR Synced
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isOcrProcessing && !ocrComplete && (
|
||||||
|
<div className="border border-dashed border-indigo-300 hover:border-indigo-500 rounded-xl p-5 text-center cursor-pointer transition-all bg-white relative group hover:bg-indigo-50/50">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*,.pdf"
|
||||||
|
className="absolute inset-0 opacity-0 cursor-pointer z-10"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setOcrFileName(file.name);
|
||||||
|
setIsOcrProcessing(true);
|
||||||
|
|
||||||
|
setOcrStep('1/3: Parsing document layout & digital signature...');
|
||||||
|
setTimeout(() => {
|
||||||
|
setOcrStep('2/3: Running AI layout analysis & line item extraction...');
|
||||||
|
setTimeout(() => {
|
||||||
|
setOcrStep('3/3: Auto-populating ledger items and actual costs...');
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOcrProcessing(false);
|
||||||
|
setOcrComplete(true);
|
||||||
|
// Populate parts dynamically with mock OCR data!
|
||||||
|
setRecord(prev => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
partsUsed: [
|
||||||
|
{ id: 'PU-OCR-1', partId: 'PRT-001', partName: 'Front fender (OCR Extracted)', quantity: 1, unitPrice: 1500, totalPrice: 1500, addedAt: new Date().toISOString().split('T')[0] },
|
||||||
|
{ id: 'PU-OCR-2', partId: 'PRT-003', partName: 'Mounting brackets (OCR Extracted)', quantity: 2, unitPrice: 800, totalPrice: 1600, addedAt: new Date().toISOString().split('T')[0] },
|
||||||
|
{ id: 'PU-OCR-3', partId: 'PRT-004', partName: 'Brake pads (OCR Extracted)', quantity: 2, unitPrice: 600, totalPrice: 1200, addedAt: new Date().toISOString().split('T')[0] },
|
||||||
|
],
|
||||||
|
serviceCost: 1800,
|
||||||
|
actualCost: 6100, // 1500 + 1600 + 1200 + 1800
|
||||||
|
notes: [...prev.notes, `OCR Scan Success: Extracted items from manual invoice "${file.name}".`]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}, 1200);
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-2">
|
||||||
|
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-600 group-hover:scale-110 transition-transform">
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-slate-700">Upload manual invoice receipt</p>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5">No file chosen</p>
|
||||||
|
<p className="text-[10px] text-slate-400 mt-2 font-medium">Supports PDF, PNG, JPG (e.g. MNT-001-invoice.pdf)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOcrProcessing && (
|
||||||
|
<div className="p-4 bg-white border border-indigo-100 rounded-xl space-y-3">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-indigo-800 font-semibold animate-pulse">Running JAIBEN AI OCR Engine...</span>
|
||||||
|
<span className="text-indigo-600 font-bold">{ocrStep.split(':')[0]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden border border-slate-200">
|
||||||
|
<div className="bg-gradient-to-r from-indigo-500 to-indigo-700 h-full w-2/3 animate-pulse rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-slate-500 italic">{ocrStep}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ocrComplete && (
|
||||||
|
<div className="p-4 bg-emerald-50 border border-emerald-100 rounded-xl space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-emerald-800 text-xs font-bold">
|
||||||
|
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||||
|
<span>OCR PARSING COMPLETE</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-600 space-y-1.5 bg-white p-3 rounded-lg border border-emerald-100">
|
||||||
|
<p>📄 Document: <strong className="text-slate-800 font-semibold">{ocrFileName}</strong></p>
|
||||||
|
<p>🔧 Extracted Parts: <strong className="text-slate-800 font-semibold">3 items</strong></p>
|
||||||
|
<p>⚡ Labor Service Costs: <strong className="text-slate-800 font-semibold">৳1,800</strong></p>
|
||||||
|
<p>💰 Auto Total Synced: <strong className="text-slate-800 font-semibold">৳6,100</strong></p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setOcrComplete(false);
|
||||||
|
setOcrFileName('');
|
||||||
|
const found = mockMaintenance.find(r => r.id === id);
|
||||||
|
if (found) {
|
||||||
|
setRecord(found);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full py-1.5 mt-2 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg transition-colors border border-slate-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
Reset OCR Invoice Attachment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100 relative overflow-hidden transition-all duration-300">
|
||||||
|
{ocrComplete && (
|
||||||
|
<div className="absolute inset-0 bg-slate-900/5 backdrop-blur-[1px] flex items-center justify-center z-10 p-2">
|
||||||
|
<span className="px-3 py-1 bg-slate-900 text-white border border-slate-700 text-[10px] font-bold rounded-lg uppercase tracking-wider flex items-center gap-1.5 shadow-md">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-400" /> Populated via AI OCR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={ocrComplete ? 'filter blur-[1.5px] opacity-60 pointer-events-none' : ''}>
|
||||||
|
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||||
|
<DollarSign className="w-5 h-5" /> Cost Details
|
||||||
|
</h3>
|
||||||
|
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-semibold text-purple-800 flex items-center gap-2">
|
||||||
|
<DollarSign className="w-5 h-5" /> Cost Breakdown
|
||||||
|
</h3>
|
||||||
|
{editMode && (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editForm.estimatedCost || 0}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
|
||||||
|
className="px-2 py-1 border border-purple-200 rounded text-sm w-24"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span className="text-slate-600">Estimated Cost:</span>
|
||||||
|
<span className="font-medium text-purple-700">৳{record.estimatedCost.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-purple-100 pt-2">
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span className="text-slate-600 flex items-center gap-1">
|
||||||
|
<Package className="w-4 h-4" /> Parts Total:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-orange-600">৳{record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString() || 0}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center text-sm mt-1">
|
||||||
|
<span className="text-slate-600 flex items-center gap-1">
|
||||||
|
<Wrench className="w-4 h-4" /> Service Cost (Labor):
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-blue-600">৳{(record.serviceCost || 0).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t-2 border-purple-200 pt-2 mt-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-semibold text-purple-800">Actual Total Cost:</span>
|
||||||
|
<span className="text-xl font-bold text-purple-800">
|
||||||
|
৳{((record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0) || 0) + (record.serviceCost || 0)).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
|
||||||
|
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5" /> Assigned To
|
||||||
|
</h3>
|
||||||
|
{editMode ? (
|
||||||
|
<select
|
||||||
|
value={editForm.assignedTo || ''}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="">Select Service Center</option>
|
||||||
|
<option value="Service Center A">Service Center A</option>
|
||||||
|
<option value="Service Center B">Service Center B</option>
|
||||||
|
<option value="Authorized Service Center">Authorized Service Center</option>
|
||||||
|
<option value="Gulshan Hub">Gulshan Hub</option>
|
||||||
|
<option value="Banani Hub">Banani Hub</option>
|
||||||
|
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100 relative overflow-hidden transition-all duration-300">
|
||||||
|
{ocrComplete && (
|
||||||
|
<div className="absolute inset-0 bg-slate-900/5 backdrop-blur-[1px] flex items-center justify-center z-10 p-2">
|
||||||
|
<span className="px-3 py-1 bg-slate-900 text-white border border-slate-700 text-[10px] font-bold rounded-lg uppercase tracking-wider flex items-center gap-1.5 shadow-md">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-400" /> Populated via AI OCR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={ocrComplete ? 'filter blur-[1.5px] opacity-60 pointer-events-none' : ''}>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
||||||
|
<Wrench className="w-5 h-5" /> Parts Used
|
||||||
|
</h3>
|
||||||
|
{!editMode && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddPartModal(true)}
|
||||||
|
className="px-3 py-1 bg-orange-600 text-white text-xs rounded-lg hover:bg-orange-700 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" /> Add Part
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{record.partsUsed && record.partsUsed.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="hidden lg:block overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-orange-100">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left text-orange-800 font-medium">Part</th>
|
||||||
|
<th className="px-3 py-2 text-center text-orange-800 font-medium">Qty</th>
|
||||||
|
<th className="px-3 py-2 text-right text-orange-800 font-medium">Unit Price</th>
|
||||||
|
<th className="px-3 py-2 text-right text-orange-800 font-medium">Total</th>
|
||||||
|
<th className="px-3 py-2 text-center text-orange-800 font-medium">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{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">
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{record.partsUsed.map((part) => (
|
||||||
|
<div key={part.id} className="bg-white rounded-lg border border-orange-200 p-3">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<span className="font-medium text-slate-800">{part.partName}</span>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-500">Qty:</span>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-600">৳{part.unitPrice.toLocaleString()}/unit</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-orange-100 flex justify-between">
|
||||||
|
<span className="text-xs text-slate-500">Total</span>
|
||||||
|
<span className="font-bold text-orange-700">৳{part.totalPrice.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-orange-400">No parts added</p>
|
||||||
|
)}
|
||||||
|
{record.partsUsed && record.partsUsed.length > 0 && (
|
||||||
|
<div className="bg-orange-50 rounded-lg p-3 flex justify-between items-center">
|
||||||
|
<span className="font-semibold text-orange-800">Parts Total:</span>
|
||||||
|
<span className="text-lg font-bold text-orange-700">
|
||||||
|
৳{record.partsUsed.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user