first commit
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-28 08:55:05 +07:00
commit 32a36cceff
444 files changed
+67186

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import React from 'react';
import { OcrDocument, OcrDocumentStatus } from '../../types';
interface OcrDocumentsTableProps {
documents: OcrDocument[];
onReview: (doc: OcrDocument) => void;
}
const statusConfig: { [key in OcrDocumentStatus]: { text: string; color: string; icon: string } } =
{
Processing: {
text: 'Memproses',
color: 'bg-blue-100 text-blue-700',
icon: 'fa-spin fa-spinner',
},
'Needs Review': {
text: 'Perlu Tinjauan',
color: 'bg-yellow-100 text-yellow-700',
icon: 'fa-user-check',
},
Completed: { text: 'Selesai', color: 'bg-green-100 text-green-700', icon: 'fa-check-circle' },
Failed: { text: 'Gagal', color: 'bg-red-100 text-red-700', icon: 'fa-times-circle' },
};
const OcrDocumentsTable: React.FC<OcrDocumentsTableProps> = ({ documents, onReview }) => {
const sortedDocs = [...documents].sort((a, b) => b.uploadedAt.getTime() - a.uploadedAt.getTime());
return (
<div className="bg-white p-6 rounded-xl shadow-sm">
<h2 className="text-xl font-bold mb-4">Dokumen Terproses</h2>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-gray-100 text-left text-gray-600">
<tr>
<th className="p-3 font-semibold">Nama File</th>
<th className="p-3 font-semibold">Waktu Unggah</th>
<th className="p-3 font-semibold">Jenis Dokumen</th>
<th className="p-3 font-semibold">Akurasi</th>
<th className="p-3 font-semibold">Status</th>
<th className="p-3 font-semibold text-right">Aksi</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{sortedDocs.map((doc) => {
const config = statusConfig[doc.status];
const accuracyColor =
doc.accuracy > 95
? 'text-green-600'
: doc.accuracy > 80
? 'text-yellow-600'
: 'text-red-600';
return (
<tr key={doc.id} className="hover:bg-gray-50">
<td className="p-3 font-semibold text-gray-800 truncate" title={doc.fileName}>
{doc.fileName}
</td>
<td className="p-3 text-gray-600">{doc.uploadedAt.toLocaleString('id-ID')}</td>
<td className="p-3 text-gray-600">{doc.documentType}</td>
<td className={`p-3 font-bold ${accuracyColor}`}>
{doc.accuracy > 0 ? `${doc.accuracy}%` : '-'}
</td>
<td className="p-3">
<span
className={`inline-flex items-center space-x-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${config.color}`}
>
<i className={`fa-solid ${config.icon}`}></i>
<span>{config.text}</span>
</span>
</td>
<td className="p-3 text-right">
{(doc.status === 'Needs Review' || doc.status === 'Completed') && (
<button
onClick={() => onReview(doc)}
className="text-blue-600 hover:text-blue-800 font-semibold"
>
<i className="fa-solid fa-search mr-1"></i> Tinjau
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
export default OcrDocumentsTable;
+102
View File
@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import { OcrDocument } from '../../types';
interface OcrReviewModalProps {
isOpen: boolean;
onClose: () => void;
document: OcrDocument;
onSave: (updatedDocument: OcrDocument) => void;
}
const OcrReviewModal: React.FC<OcrReviewModalProps> = ({ isOpen, onClose, document, onSave }) => {
const [extractedText, setExtractedText] = useState(document.extractedText);
const handleSave = () => {
onSave({ ...document, extractedText, status: 'Completed' });
onClose();
};
if (!isOpen) return null;
const accuracyColor =
document.accuracy > 95
? 'text-green-600'
: document.accuracy > 80
? 'text-yellow-600'
: 'text-red-600';
return (
<div
className="fixed inset-0 bg-black bg-opacity-60 flex justify-center items-center z-50 p-4"
onClick={onClose}
>
<div
className="bg-white rounded-xl shadow-2xl w-full max-w-5xl h-[90vh] flex flex-col"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 border-b">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-gray-800">Tinjau Dokumen OCR</h2>
<p className="text-sm text-gray-500">{document.fileName}</p>
</div>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
&times;
</button>
</div>
</div>
<div className="flex-grow p-4 grid grid-cols-1 md:grid-cols-2 gap-4 overflow-hidden">
{/* Image Viewer */}
<div className="bg-gray-100 rounded-lg flex items-center justify-center overflow-auto p-2">
<img
src={document.imageUrl}
alt="Document Scan"
className="max-w-full max-h-full object-contain"
/>
</div>
{/* Extracted Text Editor */}
<div className="flex flex-col">
<div className="flex justify-between items-center mb-2">
<h3 className="font-semibold">Teks Hasil Ekstraksi</h3>
<div className="text-sm">
Akurasi:{' '}
<span className={`font-bold ${accuracyColor}`}>
{document.accuracy.toFixed(1)}%
</span>
</div>
</div>
<textarea
value={extractedText}
onChange={(e) => setExtractedText(e.target.value)}
className="w-full flex-grow p-3 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 font-mono text-sm"
placeholder="Teks yang diekstrak akan muncul di sini..."
/>
</div>
</div>
<div className="bg-gray-100 px-6 py-3 rounded-b-xl flex justify-end space-x-3">
<button
onClick={onClose}
className="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 font-medium"
>
Tutup
</button>
<button
onClick={handleSave}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 font-medium shadow-sm"
>
<i className="fa-solid fa-check mr-2"></i> Validasi & Simpan
</button>
</div>
</div>
</div>
);
};
export default OcrReviewModal;
+66
View File
@@ -0,0 +1,66 @@
import React, { useMemo } from 'react';
import { OcrDocument } from '../../types';
interface OcrStatsProps {
documents: OcrDocument[];
}
const StatCard: React.FC<{ label: string; value: string; icon: string; color: string }> = ({
label,
value,
icon,
color,
}) => (
<div className="bg-white p-6 rounded-lg shadow-sm flex items-center space-x-4">
<div className={`text-3xl p-4 rounded-full bg-opacity-10 ${color}`}>
<i className={`fa-solid ${icon}`}></i>
</div>
<div>
<p className="text-gray-600 font-medium">{label}</p>
<p className="text-3xl font-bold text-gray-800">{value}</p>
</div>
</div>
);
const OcrStats: React.FC<OcrStatsProps> = ({ documents }) => {
const stats = useMemo(() => {
const today = new Date();
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const processedToday = documents.filter((d) => d.uploadedAt >= startOfToday).length;
const needsReview = documents.filter((d) => d.status === 'Needs Review').length;
const completedDocs = documents.filter(
(d) => d.status === 'Completed' || d.status === 'Needs Review'
);
const totalAccuracy = completedDocs.reduce((sum, doc) => sum + doc.accuracy, 0);
const avgAccuracy = completedDocs.length > 0 ? totalAccuracy / completedDocs.length : 0;
return { processedToday, needsReview, avgAccuracy };
}, [documents]);
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<StatCard
label="Diproses Hari Ini"
value={stats.processedToday.toString()}
icon="fa-calendar-day"
color="text-blue-500 bg-blue-500"
/>
<StatCard
label="Akurasi Rata-rata"
value={`${stats.avgAccuracy.toFixed(1)}%`}
icon="fa-crosshairs"
color="text-green-500 bg-green-500"
/>
<StatCard
label="Perlu Tinjauan"
value={stats.needsReview.toString()}
icon="fa-user-check"
color="text-yellow-500 bg-yellow-500"
/>
</div>
);
};
export default OcrStats;
+27
View File
@@ -0,0 +1,27 @@
import React from 'react';
const OcrUpload: React.FC = () => {
return (
<div className="bg-white p-6 rounded-xl shadow-sm h-full">
<h2 className="text-xl font-bold mb-4">Unggah Dokumen Baru</h2>
<div className="mt-4 p-8 border-2 border-dashed border-gray-300 rounded-lg text-center">
<i className="fa-solid fa-cloud-upload-alt text-5xl text-gray-400"></i>
<p className="mt-4 text-sm text-gray-600">
Seret & lepas file di sini, atau klik untuk memilih file.
</p>
<p className="text-xs text-gray-500 mt-1">Mendukung: PDF, JPG, PNG</p>
<button className="mt-6 w-full bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition duration-300 shadow-sm font-semibold">
Pilih File
</button>
</div>
<div className="mt-4 text-xs text-gray-500">
<p>
<strong>Catatan:</strong> Ini adalah mockup. Mengklik tombol tidak akan membuka dialog
file.
</p>
</div>
</div>
);
};
export default OcrUpload;