feat: add AI OCR processing state and conditional UI locking for maintenance records

This commit is contained in:
sazzadulalambd
2026-05-20 13:26:25 +06:00
parent 3933141140
commit bb561e493b

View File

@@ -282,6 +282,12 @@ export default function MaintenanceDetailPage() {
const [showCompleteModal, setShowCompleteModal] = useState(false);
const [showPaymentModal, setShowPaymentModal] = 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 [showAddPartModal, setShowAddPartModal] = useState(false);
const [partSearch, setPartSearch] = useState('');
@@ -473,7 +479,13 @@ export default function MaintenanceDetailPage() {
</div>
<p className="text-slate-500 mt-1">{typeLabels[record.type]} {record.date}</p>
</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 ? (
<>
<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 className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
@@ -617,7 +630,15 @@ export default function MaintenanceDetailPage() {
)}
</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">
<History className="w-5 h-5" /> Issue History
</h3>
@@ -646,7 +667,7 @@ export default function MaintenanceDetailPage() {
<ArrowRight className="w-4 h-4 text-orange-400" />
</Link>
)}
</div>
</div>
</div>
@@ -724,14 +745,175 @@ export default function MaintenanceDetailPage() {
</>
)}
</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 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">
<DollarSign className="w-5 h-5" /> Cost Details
</h3>
@@ -783,6 +965,7 @@ export default function MaintenanceDetailPage() {
</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">
@@ -807,7 +990,15 @@ export default function MaintenanceDetailPage() {
)}
</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">
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
<Wrench className="w-5 h-5" /> Parts Used
@@ -938,57 +1129,11 @@ export default function MaintenanceDetailPage() {
</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>