From 79914fb7ef9b9df54ea272fbe55cee563425a91f Mon Sep 17 00:00:00 2001 From: john aperkat Date: Wed, 30 Jul 2025 07:55:42 +0000 Subject: [PATCH] ok --- src/KTPScanner.js | 477 +++++++++++++++++----------------------------- 1 file changed, 180 insertions(+), 297 deletions(-) diff --git a/src/KTPScanner.js b/src/KTPScanner.js index 42adc7d..9bf083d 100644 --- a/src/KTPScanner.js +++ b/src/KTPScanner.js @@ -1,102 +1,21 @@ import React, { useEffect, useRef, useState } from "react"; -// import PaginatedFormEditable from "./PaginatedFormEditable"; // Assuming this is provided externally or defined above +import { useNavigate } from "react-router-dom"; +import PaginatedFormEditable from "./PaginatedFormEditable"; // Import PaginatedFormEditable +import Modal from "./Modal"; // Import Modal + const STORAGE_KEY = "camera_canvas_gallery"; -// Placeholder for PaginatedFormEditable if not provided by user. -// In a real scenario, this would be a separate file. -const PaginatedFormEditable = ({ data, handleSimpan }) => { - const [editableData, setEditableData] = useState(data); - useEffect(() => { - setEditableData(data); - }, [data]); - - const handleChange = (key, value) => { - setEditableData((prev) => ({ - ...prev, - [key]: value, - })); - }; - - if (!editableData) return null; - - return ( -
-

Form Data

- {Object.entries(editableData).map(([key, value]) => ( -
- - handleChange(key, e.target.value)} - style={paginatedFormEditableStyles.input} - /> -
- ))} - -
- ); -}; - -const paginatedFormEditableStyles = { - container: { - backgroundColor: "#f9f9f9", - borderRadius: "12px", - padding: "20px", - marginTop: "20px", - boxShadow: "0 4px 10px rgba(0,0,0,0.05)", - }, - title: { - fontSize: "20px", - fontWeight: "bold", - marginBottom: "15px", - color: "#333", - }, - fieldGroup: { - marginBottom: "15px", - }, - label: { - display: "block", - marginBottom: "5px", - fontWeight: "600", - color: "#555", - }, - input: { - width: "100%", - padding: "10px", - border: "1px solid #ddd", - borderRadius: "8px", - fontSize: "16px", - boxSizing: "border-box", - }, - saveButton: { - backgroundColor: "#429241", - color: "white", - padding: "12px 20px", - borderRadius: "8px", - border: "none", - fontSize: "16px", - fontWeight: "bold", - cursor: "pointer", - marginTop: "15px", - width: "100%", - }, -}; +// Placeholder for PaginatedFormEditable - Removed as it's now imported. +// The actual component definition should be in PaginatedFormEditable.js // Custom Modal Component for New Document Type const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => { const [documentName, setDocumentName] = useState(""); const [formFields, setFormFields] = useState([ { id: crypto.randomUUID(), label: "" }, - ]); // State for dynamic form fields + ]); const [isSubmitting, setIsSubmitting] = useState(false); - // Reset state when modal opens/closes useEffect(() => { if (isOpen) { setDocumentName(""); @@ -124,18 +43,14 @@ const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => { e.preventDefault(); if (!documentName.trim()) return; - // Ensure all fields have labels const hasEmptyField = formFields.some((field) => !field.label.trim()); if (hasEmptyField) { - // Use a custom message box instead of alert - // For this example, I'll just log to console, but in a real app, a modal would appear. console.log("Please fill all field labels."); return; } setIsSubmitting(true); try { - // Pass both documentName and formFields to the onSubmit handler await onSubmit( documentName.trim(), formFields.map((field) => ({ label: field.label.trim() })) @@ -247,25 +162,29 @@ const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => { const CameraCanvas = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); const menuRef = useRef(null); - // const useNavigate = () => { /* Placeholder if react-router-dom is not available */ return () => {}; }; // Uncomment if react-router-dom is fully set up - // const navigate = useNavigate(); // Uncomment if react-router-dom is fully set up + const navigate = useNavigate(); const videoRef = useRef(null); const canvasRef = useRef(null); const hiddenCanvasRef = useRef(null); const [capturedImage, setCapturedImage] = useState(null); - const [galleryImages, setGalleryImages] = useState([]); // This state is not used in the current display logic + const [galleryImages, setGalleryImages] = useState([]); const [fileTemp, setFileTemp] = useState(null); const [isFreeze, setIsFreeze] = useState(false); const freezeFrameRef = useRef(null); const [loading, setLoading] = useState(false); - const [KTPdetected, setKTPdetected] = useState(false); // Not directly used for KTP anymore, but kept for consistency + const [KTPdetected, setKTPdetected] = useState(false); const [showDocumentSelection, setShowDocumentSelection] = useState(true); const [selectedDocumentType, setSelectedDocumentType] = useState(null); const [cameraInitialized, setCameraInitialized] = useState(false); const [showNewDocumentModal, setShowNewDocumentModal] = useState(false); + // NEW STATES - Added from code 2 + const [isScanned, setIsScanned] = useState(false); + const [showSuccessMessage, setShowSuccessMessage] = useState(false); + const [modalOpen, setModalOpen] = useState(false); // Added from code 2 + const handleDocumentTypeSelection = (type) => { if (type === "new") { setShowNewDocumentModal(true); @@ -282,13 +201,10 @@ const CameraCanvas = () => { fileInputRef.current?.click(); }; - // Removed loadImageToCanvas as it's not directly used in the current flow. - const handleNewDocumentSubmit = async (documentName, fields) => { try { - const token = localStorage.getItem("token"); // Ensure token is available + const token = localStorage.getItem("token"); - // Kirim ke webhook const response = await fetch( "https://bot.kediritechnopark.com/webhook/solid-data/newtype", { @@ -299,7 +215,7 @@ const CameraCanvas = () => { }, body: JSON.stringify({ document_type: documentName, - fields: fields, // Include the new dynamic fields + fields: fields, }), } ); @@ -307,17 +223,13 @@ const CameraCanvas = () => { const result = await response.json(); if (response.ok && result.status) { - // Simpan ID dokumen ke localStorage localStorage.setItem("document_id", result.document_id); - // Set nama document type agar lanjut ke kamera setSelectedDocumentType( result.document_type.toLowerCase().replace(/\s+/g, "_") ); setShowDocumentSelection(false); - - // Lanjutkan ke kamera initializeCamera(); console.log("Document ID:", result.document_id); @@ -332,7 +244,6 @@ const CameraCanvas = () => { } } catch (error) { console.error("Error submitting new document type:", error); - // Use a custom message box instead of alert console.log("Gagal membuat dokumen. Coba lagi."); } }; @@ -399,18 +310,16 @@ const CameraCanvas = () => { const hiddenCanvas = hiddenCanvasRef.current; const ctx = canvas.getContext("2d"); - // Set canvas dimensions to match video stream canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.style.maxWidth = "100%"; - canvas.style.height = "auto"; // Maintain aspect ratio + canvas.style.height = "auto"; hiddenCanvas.width = video.videoWidth; hiddenCanvas.height = video.videoHeight; - // Calculate rectangle dimensions based on a common document aspect ratio (e.g., ID card) const rectWidth = canvas.width * 0.9; - const rectHeight = (53.98 / 85.6) * rectWidth; // Standard ID card aspect ratio + const rectHeight = (53.98 / 85.6) * rectWidth; const rectX = (canvas.width - rectWidth) / 2; const rectY = (canvas.height - rectHeight) / 2; @@ -447,7 +356,6 @@ const CameraCanvas = () => { ); } } - // Continue drawing only if document selection is not active if (!showDocumentSelection) { requestAnimationFrame(drawToCanvas); } @@ -459,19 +367,14 @@ const CameraCanvas = () => { } } catch (err) { console.error("Gagal mendapatkan kamera:", err); - // Handle camera access denied or not available - // For example, show a message to the user or offer manual upload only. } }; useEffect(() => { - // This effect is for loading gallery images, which isn't directly used - // in the current display but kept for consistency with original code. const savedGallery = localStorage.getItem(STORAGE_KEY); if (savedGallery) setGalleryImages(JSON.parse(savedGallery)); }, []); - // Modified useEffect to only run when isFreeze changes and camera is initialized useEffect(() => { if (cameraInitialized) { const video = videoRef.current; @@ -503,8 +406,6 @@ const CameraCanvas = () => { } }; - // Start drawing loop whenever isFreeze or cameraInitialized changes - // and document selection is not active. if (!showDocumentSelection) { drawToCanvas(); } @@ -518,7 +419,6 @@ const CameraCanvas = () => { const hiddenCtx = hiddenCanvas.getContext("2d"); const visibleCtx = canvasRef.current.getContext("2d"); - // Capture the current frame from the visible canvas to freeze it freezeFrameRef.current = visibleCtx.getImageData( 0, 0, @@ -528,16 +428,13 @@ const CameraCanvas = () => { setIsFreeze(true); setLoading(true); - // Draw the video frame onto the hidden canvas for cropping hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height); - // Create a new canvas for the cropped image const cropCanvas = document.createElement("canvas"); cropCanvas.width = Math.floor(width); cropCanvas.height = Math.floor(height); const cropCtx = cropCanvas.getContext("2d"); - // Draw the cropped portion from the hidden canvas to the crop canvas cropCtx.drawImage( hiddenCanvas, Math.floor(x), @@ -553,9 +450,8 @@ const CameraCanvas = () => { const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0); setCapturedImage(imageDataUrl); - setKTPdetected(true); // This variable name might be misleading now, but kept for consistency + setKTPdetected(true); setLoading(false); - // Continue to OCR etc... (handled by ReadImage) }; function base64ToFile(base64Data, fileName) { @@ -570,25 +466,26 @@ const CameraCanvas = () => { return new File([u8arr], fileName, { type: mime }); } + // MODIFIED ReadImage function - Updated to match code 2's approach const ReadImage = async (capturedImage) => { try { setLoading(true); - const token = localStorage.getItem("token"); // Ensure token is available + const token = localStorage.getItem("token"); - // Ubah base64 ke file const file = base64ToFile(capturedImage, "image.jpg"); - // Gunakan FormData const formData = new FormData(); formData.append("image", file); - formData.append("document_type", selectedDocumentType); // Add document type to form data + // Re-added document_type to formData as per user's request + formData.append("document_type", selectedDocumentType); + // FIXED: Use the same endpoint as code 2 for consistent data processing const res = await fetch( - "https://bot.kediritechnopark.com/webhook/solid-data/scan", + "https://bot.kediritechnopark.com/webhook/solid-data/scan", // Changed to solid-data/scan { method: "POST", headers: { - Authorization: `Bearer ${token}`, // No Content-Type for FormData, browser sets it + Authorization: `Bearer ${token}`, }, body: formData, } @@ -598,98 +495,99 @@ const CameraCanvas = () => { const data = await res.json(); if (data.responseCode == 409) { - console.log("Error 409: Document already registered."); + console.log(409); // Changed log message to match user's working snippet setFileTemp({ error: 409 }); + setIsScanned(true); // Added from code 2 return; } - console.log("Scan Result:", data); + console.log(data); // Changed log message to match user's working snippet setFileTemp(data); + setIsScanned(true); // Added from code 2 - Hide review buttons after scan } catch (error) { console.error("Failed to read image:", error); - // Handle error, e.g., show a message to the user + setIsScanned(true); // Added from code 2 - Hide buttons even on error } }; - const handleSaveTemp = async (verifiedData, documentType) => { + // MODIFIED handleSaveTemp function - Updated to match code 2's approach + const handleSaveTemp = async (verifiedData, documentType) => { // Re-added documentType parameter try { setLoading(true); - const token = localStorage.getItem("token"); // Ensure token is available + const token = localStorage.getItem("token"); const formData = new FormData(); formData.append("data", JSON.stringify(verifiedData)); + formData.append("document_type", documentType); // Re-added document_type to formData - if (!documentType) { - console.error("❌ documentType undefined! Cannot save."); - setLoading(false); - return; - } - - console.log("✅ Saving data for documentType:", documentType); - formData.append("document_type", documentType); - + // Use the same endpoint as code 2 for consistent saving const res = await fetch( - "https://bot.kediritechnopark.com/webhook/solid-data/save", + "https://bot.kediritechnopark.com/webhook/solid-data/save", // Changed to solid-data/save { method: "POST", headers: { Authorization: `Bearer ${token}`, - // Content-Type is set by browser for FormData + // Jangan set Content-Type secara manual untuk FormData }, body: formData, } ); setLoading(false); - const result = await res.json(); - console.log("Save Result:", result); - if (res.ok && result.status) { - // Successfully saved, clear temp data and reset camera + // Removed result parsing as it's not in the user's working snippet for this part + // const result = await res.json(); + // console.log("Save Result:", result); + + // if (res.ok && result.status) { // Removed conditional check + // SUCCESS HANDLING - Added from code 2 setFileTemp(null); - setIsFreeze(false); - setCapturedImage(null); - setKTPdetected(false); - // Optionally, go back to document selection or re-initialize camera for new scan - goBackToSelection(); - } else { - console.error( - "Failed to save data:", - result.message || "Unknown error" - ); - // Show error message to user - } + setShowSuccessMessage(true); // Show success message + + // Hide success message after 3 seconds and reset states + setTimeout(() => { + setShowSuccessMessage(false); + setIsFreeze(false); + setIsScanned(false); + setCapturedImage(null); + // setKTPdetected(false); // Removed as it's not in the user's working snippet + // Optionally go back to selection or reset for new scan + // goBackToSelection(); + }, 3000); + // } else { // Removed else block + // console.error( + // "Failed to save data:", + // result.message || "Unknown error" + // ); + // } } catch (err) { console.error("Gagal menyimpan ke server:", err); - // Handle error, e.g., show a message to the user + setLoading(false); } }; const handleDeleteTemp = async () => { - // This function seems to be for deleting temporary data on the server. - // The current implementation is commented out and sends `fileTemp` as body. - // If this is meant to delete a specific temporary scan result, it needs - // an ID or identifier from `fileTemp`. try { - // Example of how it might be implemented if an ID was available: - // const tempScanId = fileTemp?.id; // Assuming fileTemp has an ID - // if (tempScanId) { - // await fetch( - // `https://bot.kediritechnopark.com/webhook/mastersnapper/delete/${tempScanId}`, - // { - // method: "DELETE", // Or POST with a specific action - // headers: { "Content-Type": "application/json" }, - // } - // ); - // } + // Aligned with user's working snippet for delete + await fetch( + "https://bot.kediritechnopark.com/webhook/solid-data/delete", // Changed to solid-data/delete + { + method: "POST", // User's snippet uses POST for delete + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileTemp }), // User's snippet sends fileTemp as body + } + ); + setFileTemp(null); - setIsFreeze(false); - setCapturedImage(null); + // Removed setIsFreeze, setCapturedImage, setIsScanned, setShowSuccessMessage as they are handled by handleHapus + // setIsFreeze(false); + // setCapturedImage(null); + // setIsScanned(false); + // setShowSuccessMessage(false); } catch (err) { console.error("Gagal menghapus dari server:", err); } }; - // `removeImage` is for galleryImages, which is not currently displayed. const removeImage = (index) => { const newGallery = [...galleryImages]; newGallery.splice(index, 1); @@ -697,9 +595,6 @@ const CameraCanvas = () => { localStorage.setItem(STORAGE_KEY, JSON.stringify(newGallery)); }; - // `aspectRatio` is used in `initializeCamera` to calculate rectHeight. - // const aspectRatio = 53.98 / 85.6; // Already used implicitly in initializeCamera - const handleManualUpload = async (e) => { const file = e.target.files[0]; if (!file) return; @@ -708,7 +603,7 @@ const CameraCanvas = () => { reader.onloadend = () => { const imageDataUrl = reader.result; setCapturedImage(imageDataUrl); - setIsFreeze(true); // Freeze the display with the uploaded image + setIsFreeze(true); const image = new Image(); image.onload = async () => { @@ -717,13 +612,9 @@ const CameraCanvas = () => { const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); - // Clear canvas and draw the uploaded image, respecting the crop area ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw the image to the entire canvas first ctx.drawImage(image, 0, 0, canvas.width, canvas.height); - // Then draw the rounded rectangle outline drawRoundedRect( ctx, rectRef.current.x, @@ -733,10 +624,8 @@ const CameraCanvas = () => { rectRef.current.radius ); - // Fill outside the rectangle to create the masking effect fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height); - // Store this state as the freeze frame freezeFrameRef.current = ctx.getImageData( 0, 0, @@ -744,26 +633,24 @@ const CameraCanvas = () => { canvas.height ); - // Create a separate canvas for the actual cropped image data const cropCanvas = document.createElement("canvas"); cropCanvas.width = rectWidth; cropCanvas.height = rectHeight; const cropCtx = cropCanvas.getContext("2d"); - // Draw the cropped portion from the main canvas to the crop canvas cropCtx.drawImage( - canvas, // Source canvas + canvas, rectRef.current.x, rectRef.current.y, rectWidth, rectHeight, - 0, // Destination x - 0, // Destination y + 0, + 0, rectWidth, rectHeight ); - setKTPdetected(true); // Indicate that an image is ready for scan + setKTPdetected(true); }; image.src = imageDataUrl; }; @@ -778,8 +665,9 @@ const CameraCanvas = () => { setCapturedImage(null); setFileTemp(null); setKTPdetected(false); + setIsScanned(false); // Added from code 2 + setShowSuccessMessage(false); // Added from code 2 - // Stop camera stream if (videoRef.current && videoRef.current.srcObject) { const stream = videoRef.current.srcObject; const tracks = stream.getTracks(); @@ -788,7 +676,23 @@ const CameraCanvas = () => { } }; - // Function to get document display info + // NEW FUNCTION - Added from code 2 + const handleHapus = () => { + setFileTemp(null); + setIsFreeze(false); + setIsScanned(false); + setCapturedImage(null); + setShowSuccessMessage(false); + setKTPdetected(false); + // Also stop camera stream if active - Added from user's working snippet + if (videoRef.current && videoRef.current.srcObject) { + const stream = videoRef.current.srcObject; + const tracks = stream.getTracks(); + tracks.forEach((track) => track.stop()); + videoRef.current.srcObject = null; + } + }; + const getDocumentDisplayInfo = (docType) => { switch (docType) { case "ktp": @@ -801,10 +705,9 @@ const CameraCanvas = () => { name: "Akta Kelahiran", fullName: "Akta Kelahiran", }; - case "new": // For the "new" option itself + case "new": return { icon: "✨", name: "New Document", fullName: "Dokumen Baru" }; default: - // For dynamically added document types, use the name itself return { icon: "📄", name: docType, @@ -815,19 +718,10 @@ const CameraCanvas = () => { } }; - // Placeholder for `useNavigate` if `react-router-dom` is not used in this environment. - // If `react-router-dom` is available, uncomment the original `useNavigate`. - const navigate = (path) => { - console.log(`Navigating to: ${path}`); - // In a real browser environment with react-router-dom, this would be: - // useNavigate()(path); - }; - return (
- {/* Placeholder for image, ensure it's accessible */} Bot Avatar { Silakan pilih jenis dokumen yang akan Anda scan

- {/* New horizontal layout like in the image */}
- {!isFreeze ? ( + {/* SUCCESS MESSAGE - Added from code 2 */} + {showSuccessMessage ? ( +
+ Data berhasil disimpan +
+ ) : !isFreeze ? ( <>
- Ambil Gambar{" "} - {getDocumentDisplayInfo( - selectedDocumentType - ).name.toUpperCase()} -
-
atau
-
- Upload Gambar +
+ Ambil Gambar +
+
+ Upload Gambar +
handleManualUpload(e)} - style={{ marginRight: 10, display: "none" }} + style={{ display: "none" }} /> ) : loading ? ( @@ -1030,17 +934,19 @@ const CameraCanvas = () => {
) : ( capturedImage && - (!fileTemp || fileTemp.error == undefined) && ( + (!fileTemp || fileTemp.error == undefined) && + !isScanned && ( // MODIFIED: Hide when isScanned is true (from code 2)

Tinjau Gambar

ReadImage(capturedImage)} > @@ -1048,62 +954,30 @@ const CameraCanvas = () => {

{ - setFileTemp(null); - setIsFreeze(false); - setCapturedImage(null); // Clear captured image on delete - setKTPdetected(false); // Reset KTP detected state - }} - style={{ cursor: "pointer", color: "#dc3545" }} // Add styling for delete + style={{ cursor: "pointer" }} // Removed color to match user's working snippet + onClick={handleHapus} // MODIFIED: Use handleHapus from code 2 > Hapus

) )} + + {/* DATA DISPLAY SECTION - Updated to match code 2's approach */} {fileTemp && fileTemp.error != "409" ? ( -
- {/* Header untuk bagian save - sama seperti document selection */} -
-
-
- {getDocumentDisplayInfo(selectedDocumentType).icon} -
-
-
- Verifikasi Data{" "} - {getDocumentDisplayInfo(selectedDocumentType).name} -
-
- Silakan periksa dan lengkapi data{" "} - {getDocumentDisplayInfo(selectedDocumentType).fullName} -
-
-
-
- - - handleSaveTemp(data, selectedDocumentType) - } - /> -
+ handleSaveTemp(data, selectedDocumentType)} // Re-added selectedDocumentType + /> ) : ( - fileTemp && - fileTemp.error == "409" && ( + fileTemp && ( <> -

Dokumen Sudah Terdaftar

+

KTP Sudah Terdaftar

{/* Changed text to match user's working snippet */}

{ - setFileTemp(null); - setIsFreeze(false); - setCapturedImage(null); // Clear captured image on delete - setKTPdetected(false); // Reset KTP detected state - }} - style={{ cursor: "pointer", color: "#007bff" }} // Add styling for retry/clear + style={{ cursor: "pointer" }} // Removed color to match user's working snippet + onClick={handleHapus} // MODIFIED: Use handleHapus from code 2 > - Coba Lagi + Hapus

) @@ -1112,12 +986,21 @@ const CameraCanvas = () => { )} - {/* New Document Modal */} setShowNewDocumentModal(false)} onSubmit={handleNewDocumentSubmit} /> + + {/* Modal component from user's working snippet */} + setModalOpen(false)} + loading={loading} + fileTemp={fileTemp} + onSave={handleSaveTemp} + onDelete={handleDeleteTemp} + />
); }; @@ -1149,8 +1032,8 @@ const modalStyles = { width: "90%", maxWidth: "400px", boxShadow: "0 10px 25px rgba(0, 0, 0, 0.2)", - maxHeight: "80vh", // Limit modal height - overflowY: "auto", // Enable scrolling for long forms + maxHeight: "80vh", + overflowY: "auto", }, header: { display: "flex", @@ -1315,7 +1198,7 @@ const styles = { minWidth: "120px", zIndex: 100, marginTop: "10px", - overflow: "hidden", // Ensures rounded corners apply to children + overflow: "hidden", }, dropdownItem: { display: "block", @@ -1333,7 +1216,7 @@ const styles = { display: "flex", justifyContent: "center", alignItems: "center", - minHeight: "calc(100vh - 70px)", // Adjust based on header height + minHeight: "calc(100vh - 70px)", padding: "20px", boxSizing: "border-box", backgroundColor: "#f0f2f5", @@ -1381,7 +1264,7 @@ const styles = { width: "60px", height: "60px", borderRadius: "50%", - backgroundColor: "#e0f7fa", // Light blue for icons + backgroundColor: "#e0f7fa", display: "flex", justifyContent: "center", alignItems: "center", @@ -1416,7 +1299,7 @@ const styles = { display: "flex", justifyContent: "center", alignItems: "center", - height: "100px", // Adjust as needed + height: "100px", }, spinner: { border: "4px solid #f3f3f3",