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,7 +479,13 @@ 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">
|
||||||
|
{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">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" /> Invoice Locked (OCR Synced)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`flex flex-wrap gap-2 ${ocrComplete ? 'filter blur-[1.5px] opacity-50 pointer-events-none' : ''}`}>
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<>
|
<>
|
||||||
<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">
|
<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">
|
||||||
@@ -547,6 +559,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -617,7 +630,15 @@ 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">
|
||||||
|
{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-orange-800 mb-3 flex items-center gap-2">
|
<h3 className="font-semibold text-orange-800 mb-3 flex items-center gap-2">
|
||||||
<History className="w-5 h-5" /> Issue History
|
<History className="w-5 h-5" /> Issue History
|
||||||
</h3>
|
</h3>
|
||||||
@@ -646,7 +667,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
<ArrowRight className="w-4 h-4 text-orange-400" />
|
<ArrowRight className="w-4 h-4 text-orange-400" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -724,14 +745,175 @@ export default function MaintenanceDetailPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<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">
|
||||||
|
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
|
||||||
|
</h3>
|
||||||
|
{editMode && (
|
||||||
|
<div className="flex gap-2 mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="newNoteInput"
|
||||||
|
placeholder="Add a note..."
|
||||||
|
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const input = e.currentTarget as HTMLInputElement;
|
||||||
|
if (input.value.trim()) {
|
||||||
|
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.getElementById('newNoteInput') as HTMLInputElement;
|
||||||
|
if (input?.value.trim()) {
|
||||||
|
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(editMode ? editForm.notes : record.notes)?.map((note, idx) => (
|
||||||
|
<div key={idx} className="text-sm text-slate-600 p-2 bg-white rounded-lg">
|
||||||
|
• {note}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(editMode ? editForm.notes : record.notes)?.length === 0 && (
|
||||||
|
<p className="text-sm text-slate-400">No notes yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
||||||
|
|
||||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
{/* 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">
|
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||||
<DollarSign className="w-5 h-5" /> Cost Details
|
<DollarSign className="w-5 h-5" /> Cost Details
|
||||||
</h3>
|
</h3>
|
||||||
@@ -783,6 +965,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
|
<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">
|
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
|
||||||
@@ -807,7 +990,15 @@ 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">
|
||||||
|
{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">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
||||||
<Wrench className="w-5 h-5" /> Parts Used
|
<Wrench className="w-5 h-5" /> Parts Used
|
||||||
@@ -938,57 +1129,11 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
|
||||||
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
|
|
||||||
</h3>
|
|
||||||
{editMode && (
|
|
||||||
<div className="flex gap-2 mb-3">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="newNoteInput"
|
|
||||||
placeholder="Add a note..."
|
|
||||||
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
|
||||||
onKeyPress={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const input = e.currentTarget as HTMLInputElement;
|
|
||||||
if (input.value.trim()) {
|
|
||||||
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const input = document.getElementById('newNoteInput') as HTMLInputElement;
|
|
||||||
if (input?.value.trim()) {
|
|
||||||
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{(editMode ? editForm.notes : record.notes)?.map((note, idx) => (
|
|
||||||
<div key={idx} className="text-sm text-slate-600 p-2 bg-white rounded-lg">
|
|
||||||
• {note}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{(editMode ? editForm.notes : record.notes)?.length === 0 && (
|
|
||||||
<p className="text-sm text-slate-400">No notes yet</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user