From 83f9bd135c3eee978ccd7bf9b3957fc39492f580 Mon Sep 17 00:00:00 2001 From: Rizqika Date: Tue, 12 Nov 2024 12:15:10 +0700 Subject: [PATCH] Implement API on OCR KTP --- src/screens/Biometric/OcrKtp/Verify.jsx | 504 +++++++++++------- .../Home/Applications/Applications.jsx | 2 +- 2 files changed, 320 insertions(+), 186 deletions(-) diff --git a/src/screens/Biometric/OcrKtp/Verify.jsx b/src/screens/Biometric/OcrKtp/Verify.jsx index 8baa9ed..06fa901 100644 --- a/src/screens/Biometric/OcrKtp/Verify.jsx +++ b/src/screens/Biometric/OcrKtp/Verify.jsx @@ -1,24 +1,38 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChevronDown, faChevronLeft, faImage, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faImage, faTimes, faChevronDown, faChevronLeft, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; +import { Link } from 'react-router-dom'; +import Select from 'react-select' +import { DummyKtp } from '../../../assets/images'; + +const CustomLabel = ({ overRide, children, ...props }) => { + return ; +}; const Verify = () => { const BASE_URL = process.env.REACT_APP_BASE_URL; const API_KEY = process.env.REACT_APP_API_KEY; - const fileTypes = ["image/jpeg", "image/png"]; // Use MIME types for better file validation + const fileTypes = ["image/jpeg", "image/png"]; const fileInputRef = useRef(null); - const [isSelectOpen, setIsSelectOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [selectedImageName, setSelectedImageName] = useState(''); const [file, setFile] = useState(null); const [applicationId, setApplicationId] = useState(''); const [isLoading, setIsLoading] = useState(false); const [applicationIds, setApplicationIds] = useState([]); + const [imageUrl, setImageUrl] = useState(''); const [imageError, setImageError] = useState(''); const [data, setData] = useState(null); const [showResult, setShowResult] = useState(false); + const [inputValueApplication, setInputValueApplication] = useState(''); + + // Validation state + const [validationErrors, setValidationErrors] = useState({ + applicationId: '', + file: '' + }); // Fetch Application IDs useEffect(() => { @@ -51,47 +65,91 @@ const Verify = () => { }; fetchApplicationIds(); + + const checkMobile = () => { + setIsMobile(window.innerWidth <= 768); // Example: 768px as the threshold for mobile devices + }; + + // Check on initial load + checkMobile(); + + // Add resize listener to adjust `isMobile` state on window resize + window.addEventListener('resize', checkMobile); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener('resize', checkMobile); + }; }, []); - const handleFocus = () => setIsSelectOpen(true); - const handleBlur = () => setIsSelectOpen(false); - - // Handle file upload - const handleImageUpload = (e) => { - const file = e.target.files[0]; - if (!file) return; - - const fileType = file.type; // Use MIME type instead of file extension - if (!fileTypes.includes(fileType)) { - setImageError('Image format is not supported'); - setFile(null); - return; - } - - if (file.size > 2 * 1024 * 1024) { // 2MB check - setImageError('File size exceeds 2MB'); - setFile(null); - return; - } - - setSelectedImageName(file.name); - setFile(file); - setImageError(''); + const handleInputChangeApplication = (inputValue) => { + setInputValueApplication(inputValue); + }; + + const handleApplicationChange = (selectedOption) => { + setApplicationId(selectedOption ? selectedOption.value : ''); + }; + + const applicationOptions = applicationIds.map(app => ({ + value: app.id, // This is what will be sent when an option is selected + label: app.name // This is what will be displayed in the dropdown + })); + + // Handle file upload + const handleFileDrop = (files) => { + if (files && files[0]) { + handleImageUpload(files[0]); + } else { + console.error('No valid files dropped'); + } + }; + + const handleImageUpload = (file) => { + setFile(file); + setSelectedImageName(file.name); + + // Validate file type + if (!fileTypes.includes(file.type)) { + setImageError('Invalid file type. Only JPG, JPEG, and PNG are allowed.'); + } else if (file.size > 2 * 1024 * 1024) { // Max 2MB + setImageError('File size exceeds 2MB.'); + } else { + setImageError(''); + } }; - // Cancel file upload const handleImageCancel = () => { - setSelectedImageName(''); setFile(null); + setSelectedImageName(''); setImageError(''); fileInputRef.current.value = ''; }; + // Validate form inputs before submitting + const validateForm = () => { + const errors = { + applicationId: '', + file: '' + }; + + if (!applicationId) { + errors.applicationId = 'Please select an Application ID.'; + } + + if (!file) { + errors.file = 'Please upload an image file.'; + } else if (imageError) { + errors.file = imageError; + } + + setValidationErrors(errors); + return Object.values(errors).every(error => error === ''); + }; + // Submit form and trigger OCR API const handleCheckClick = async () => { - if (!file || !applicationId) { - setErrorMessage('Please select an application and upload a file.'); - return; + if (!validateForm()) { + return; // Form is not valid } setIsLoading(true); @@ -99,8 +157,6 @@ const Verify = () => { formData.append('application_id', applicationId); formData.append('file', file); - console.log(`id: ${applicationId}, file: ${file}`) - try { const response = await fetch(`${BASE_URL}/ocr-ktp`, { method: 'POST', @@ -116,36 +172,13 @@ const Verify = () => { } const result = await response.json(); - - console.log('Full response:', result); // Log full response to inspect the structure + + // Log the full result to verify structure + console.log('OCR API Response:', result); if (result.status_code === 201) { - console.log('Success'); - // Correct the path to access the data-ktp object const responseData = result.details.data?.['data-ktp'] || {}; - // Log each field to inspect the data - console.log('NIK:', responseData.nik); - console.log('District:', responseData.kecamatan); - console.log('Name:', responseData.nama); - console.log('City:', responseData.kabkot); - console.log('Date of Birth:', responseData.tanggal_lahir); - console.log('State:', responseData.provinsi); - console.log('Gender:', responseData.jenis_kelamin); - console.log('Religion:', responseData.agama); - console.log('Blood Type:', responseData.golongan_darah); - console.log('Marital Status:', responseData.status_perkawinan); - console.log('Address:', responseData.alamat); - console.log('Occupation:', responseData.pekerjaan); - console.log('RT/RW:', `${responseData.rt}/${responseData.rw}`); - console.log('Nationality:', responseData.kewarganegaraan); - console.log('Image URL:', result.details.image_url); - console.log('Dark:', responseData.dark); - console.log('Blur:', responseData.blur); - console.log('Grayscale:', responseData.grayscale); - console.log('Flashlight:', responseData.flashlight); - - // Map the response data to a new object with default values if the property doesn't exist const data = { nik: responseData.nik || 'N/A', district: responseData.kecamatan || 'N/A', @@ -161,17 +194,22 @@ const Verify = () => { occupation: responseData.pekerjaan || 'N/A', rtRw: `${responseData.rt || 'N/A'}/${responseData.rw || 'N/A'}`, nationality: responseData.kewarganegaraan || 'N/A', - imageUrl: result.details.image_url || '', - dark: responseData.dark || 'N/A', - blur: responseData.blur || 'N/A', - grayscale: responseData.grayscale || 'N/A', - flashlight: responseData.flashlight || 'N/A', + imageUrl: result.details.data?.image_url || '', // Properly access image_url }; + console.log('Image URL from OCR:', result.details.data?.image_url); // Log the image URL correctly + setData(data); setShowResult(true); setErrorMessage(''); setSelectedImageName(''); + + // Fetch image if image URL exists in the result + if (result.details.data?.image_url) { + const imageFileName = result.details.data.image_url.split('/').pop(); // Get the image filename + console.log('Image file name:', imageFileName); // Debug the file name + await fetchImage(imageFileName); // Call the fetchImage function to fetch the image + } } else { setErrorMessage('OCR processing failed'); } @@ -182,15 +220,53 @@ const Verify = () => { } }; + + + // The fetchImage function you already have in your code + const fetchImage = async (imageFileName) => { + setIsLoading(true); + try { + const response = await fetch(`${BASE_URL}/preview/image/${imageFileName}`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'x-api-key': API_KEY, + } + }); + + if (!response.ok) { + const errorDetails = await response.json(); + console.error('Image fetch error details:', errorDetails); + setErrorMessage('Failed to fetch image, please try again.'); + return; + } + + // Get the image blob + const imageBlob = await response.blob(); + const imageData = URL.createObjectURL(imageBlob); // Create object URL from the blob + + // Debugging: Make sure imageData is correct + console.log('Fetched image URL:', imageData); + + setImageUrl(imageData); // Update imageUrl state with the fetched image data + } catch (error) { + console.error('Error fetching image:', error); + setErrorMessage(error.message); + } finally { + setIsLoading(false); + } + }; + return (
+ {/* Inject keyframes for the spinner */} {isLoading && (
@@ -223,27 +299,20 @@ const Verify = () => {
- - + handleImageUpload(e.target.files[0])} /> + {selectedImageName && (

File: {selectedImageName}

@@ -276,7 +367,7 @@ const Verify = () => {
)} - {imageError &&

{imageError}

} + {validationErrors.file &&

{validationErrors.file}

}
@@ -285,7 +376,7 @@ const Verify = () => {
- {showResult && data && ( + {showResult && data && (
-

OCR Result

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NIK{data.nik}
District{data.district}
Name{data.name}
City{data.city}
Date of Birth{data.dob}
State{data.state}
Gender{data.gender}
Religion{data.religion}
Blood Type{data.bloodType}
Marital Status{data.maritalStatus}
Address{data.address}
Occupation{data.occupation}
RT/RW{data.rtRw}
Nationality{data.nationality}
ImageKTP
+

OCR KTP Result

+
+ {/* Gambar di kolom pertama */} +
+ KTP Image +
+ + {/* Tabel di kolom kedua */} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NIK{data.nik}
District{data.district}
Name{data.name}
City{data.city}
Date of Birth{data.dob}
State{data.state}
Gender{data.gender}
Religion{data.religion}
Blood Type{data.bloodType}
Marital Status{data.maritalStatus}
Address{data.address}
Occupation{data.occupation}
RT/RW{data.rtRw}
Nationality{data.nationality}
+
+
+
)}
); }; +export default Verify; + const styles = { selectWrapper: { position: 'relative', @@ -385,13 +490,6 @@ const styles = { borderRadius: '4px', border: '1px solid #ccc', }, - chevronIcon: { - position: 'absolute', - top: '50%', - right: '10px', - transform: 'translateY(-50%)', - color: '#0542cc', - }, remainingQuota: { display: 'flex', alignItems: 'center', @@ -404,8 +502,47 @@ const styles = { fontSize: '16px', fontWeight: '300', }, + customLabel: { + fontSize: '18px', + fontWeight: '600', + color: '#1f2d3d', + }, uploadWrapper: { - position: 'relative', + marginTop: '1rem', + }, + uploadArea: { + backgroundColor: '#e6f2ff', + height: '58svh', + cursor: 'pointer', + marginTop: '1rem', + paddingTop: '22px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + border: '1px solid #ced4da', + borderRadius: '0.25rem', + textAlign: 'center', + }, + uploadIcon: { + fontSize: '40px', + color: '#0542cc', + marginBottom: '10px', + }, + uploadText: { + color: '#1f2d3d', + fontWeight: '400', + fontSize: '16px', + }, + browseLink: { + color: '#0542cc', + textDecoration: 'none', + fontWeight: 'bold', + }, + uploadError: { + color: 'red', + fontSize: '12px', + marginTop: '10px', }, errorContainer: { marginTop: '10px', @@ -418,28 +555,27 @@ const styles = { color: '#721c24', fontSize: '14px', margin: '0', - }, - loadingOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, + },loadingOverlay: { + position: 'fixed', // Gunakan fixed untuk overlay penuh layar + top: 0, + left: 0, + right: 0, + bottom: 0, // Pastikan menutupi seluruh layar display: 'flex', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', // Posisikan spinner di tengah secara horizontal + alignItems: 'center', // Posisikan spinner di tengah secara vertikal + backgroundColor: 'rgba(0, 0, 0, 0.5)', // Semitransparan di background color: 'white', fontSize: '24px', - zIndex: 1000, + zIndex: 1000, // Pastikan overlay muncul di atas konten lainnya }, spinner: { - border: '4px solid #f3f3f3', - borderTop: '4px solid #3498db', - borderRadius: '50%', - width: '50px', - height: '50px', - animation: 'spin 2s linear infinite', + border: '4px solid #f3f3f3', // Border gray untuk spinner + borderTop: '4px solid #3498db', // Border biru untuk spinner + borderRadius: '50%', // Membuat spinner bulat + width: '50px', // Ukuran spinner + height: '50px', // Ukuran spinner + animation: 'spin 2s linear infinite', // Menambahkan animasi spin }, loadingText: { marginLeft: '20px', @@ -455,5 +591,3 @@ const styles = { textAlign: 'left', }, }; - -export default Verify; diff --git a/src/screens/Home/Applications/Applications.jsx b/src/screens/Home/Applications/Applications.jsx index 4bae33e..123cc7d 100644 --- a/src/screens/Home/Applications/Applications.jsx +++ b/src/screens/Home/Applications/Applications.jsx @@ -135,7 +135,7 @@ const Applications = () => { />
- +