This commit is contained in:
john aperkat
2025-07-30 07:55:42 +00:00
parent afe9b24f56
commit 79914fb7ef

View File

@@ -1,102 +1,21 @@
import React, { useEffect, useRef, useState } from "react"; 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"; 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(() => { // Placeholder for PaginatedFormEditable - Removed as it's now imported.
setEditableData(data); // The actual component definition should be in PaginatedFormEditable.js
}, [data]);
const handleChange = (key, value) => {
setEditableData((prev) => ({
...prev,
[key]: value,
}));
};
if (!editableData) return null;
return (
<div style={paginatedFormEditableStyles.container}>
<h3 style={paginatedFormEditableStyles.title}>Form Data</h3>
{Object.entries(editableData).map(([key, value]) => (
<div key={key} style={paginatedFormEditableStyles.fieldGroup}>
<label style={paginatedFormEditableStyles.label}>{key}:</label>
<input
type="text"
value={value || ""}
onChange={(e) => handleChange(key, e.target.value)}
style={paginatedFormEditableStyles.input}
/>
</div>
))}
<button
onClick={() => handleSimpan(editableData)}
style={paginatedFormEditableStyles.saveButton}
>
Simpan Data
</button>
</div>
);
};
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%",
},
};
// Custom Modal Component for New Document Type // Custom Modal Component for New Document Type
const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => { const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => {
const [documentName, setDocumentName] = useState(""); const [documentName, setDocumentName] = useState("");
const [formFields, setFormFields] = useState([ const [formFields, setFormFields] = useState([
{ id: crypto.randomUUID(), label: "" }, { id: crypto.randomUUID(), label: "" },
]); // State for dynamic form fields ]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Reset state when modal opens/closes
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setDocumentName(""); setDocumentName("");
@@ -124,18 +43,14 @@ const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => {
e.preventDefault(); e.preventDefault();
if (!documentName.trim()) return; if (!documentName.trim()) return;
// Ensure all fields have labels
const hasEmptyField = formFields.some((field) => !field.label.trim()); const hasEmptyField = formFields.some((field) => !field.label.trim());
if (hasEmptyField) { 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."); console.log("Please fill all field labels.");
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// Pass both documentName and formFields to the onSubmit handler
await onSubmit( await onSubmit(
documentName.trim(), documentName.trim(),
formFields.map((field) => ({ label: field.label.trim() })) formFields.map((field) => ({ label: field.label.trim() }))
@@ -247,25 +162,29 @@ const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => {
const CameraCanvas = () => { const CameraCanvas = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef(null); 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();
// const navigate = useNavigate(); // Uncomment if react-router-dom is fully set up
const videoRef = useRef(null); const videoRef = useRef(null);
const canvasRef = useRef(null); const canvasRef = useRef(null);
const hiddenCanvasRef = useRef(null); const hiddenCanvasRef = useRef(null);
const [capturedImage, setCapturedImage] = useState(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 [fileTemp, setFileTemp] = useState(null);
const [isFreeze, setIsFreeze] = useState(false); const [isFreeze, setIsFreeze] = useState(false);
const freezeFrameRef = useRef(null); const freezeFrameRef = useRef(null);
const [loading, setLoading] = useState(false); 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 [showDocumentSelection, setShowDocumentSelection] = useState(true);
const [selectedDocumentType, setSelectedDocumentType] = useState(null); const [selectedDocumentType, setSelectedDocumentType] = useState(null);
const [cameraInitialized, setCameraInitialized] = useState(false); const [cameraInitialized, setCameraInitialized] = useState(false);
const [showNewDocumentModal, setShowNewDocumentModal] = 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) => { const handleDocumentTypeSelection = (type) => {
if (type === "new") { if (type === "new") {
setShowNewDocumentModal(true); setShowNewDocumentModal(true);
@@ -282,13 +201,10 @@ const CameraCanvas = () => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
// Removed loadImageToCanvas as it's not directly used in the current flow.
const handleNewDocumentSubmit = async (documentName, fields) => { const handleNewDocumentSubmit = async (documentName, fields) => {
try { try {
const token = localStorage.getItem("token"); // Ensure token is available const token = localStorage.getItem("token");
// Kirim ke webhook
const response = await fetch( const response = await fetch(
"https://bot.kediritechnopark.com/webhook/solid-data/newtype", "https://bot.kediritechnopark.com/webhook/solid-data/newtype",
{ {
@@ -299,7 +215,7 @@ const CameraCanvas = () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
document_type: documentName, document_type: documentName,
fields: fields, // Include the new dynamic fields fields: fields,
}), }),
} }
); );
@@ -307,17 +223,13 @@ const CameraCanvas = () => {
const result = await response.json(); const result = await response.json();
if (response.ok && result.status) { if (response.ok && result.status) {
// Simpan ID dokumen ke localStorage
localStorage.setItem("document_id", result.document_id); localStorage.setItem("document_id", result.document_id);
// Set nama document type agar lanjut ke kamera
setSelectedDocumentType( setSelectedDocumentType(
result.document_type.toLowerCase().replace(/\s+/g, "_") result.document_type.toLowerCase().replace(/\s+/g, "_")
); );
setShowDocumentSelection(false); setShowDocumentSelection(false);
// Lanjutkan ke kamera
initializeCamera(); initializeCamera();
console.log("Document ID:", result.document_id); console.log("Document ID:", result.document_id);
@@ -332,7 +244,6 @@ const CameraCanvas = () => {
} }
} catch (error) { } catch (error) {
console.error("Error submitting new document type:", error); console.error("Error submitting new document type:", error);
// Use a custom message box instead of alert
console.log("Gagal membuat dokumen. Coba lagi."); console.log("Gagal membuat dokumen. Coba lagi.");
} }
}; };
@@ -399,18 +310,16 @@ const CameraCanvas = () => {
const hiddenCanvas = hiddenCanvasRef.current; const hiddenCanvas = hiddenCanvasRef.current;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
// Set canvas dimensions to match video stream
canvas.width = video.videoWidth; canvas.width = video.videoWidth;
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
canvas.style.maxWidth = "100%"; canvas.style.maxWidth = "100%";
canvas.style.height = "auto"; // Maintain aspect ratio canvas.style.height = "auto";
hiddenCanvas.width = video.videoWidth; hiddenCanvas.width = video.videoWidth;
hiddenCanvas.height = video.videoHeight; 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 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 rectX = (canvas.width - rectWidth) / 2;
const rectY = (canvas.height - rectHeight) / 2; const rectY = (canvas.height - rectHeight) / 2;
@@ -447,7 +356,6 @@ const CameraCanvas = () => {
); );
} }
} }
// Continue drawing only if document selection is not active
if (!showDocumentSelection) { if (!showDocumentSelection) {
requestAnimationFrame(drawToCanvas); requestAnimationFrame(drawToCanvas);
} }
@@ -459,19 +367,14 @@ const CameraCanvas = () => {
} }
} catch (err) { } catch (err) {
console.error("Gagal mendapatkan kamera:", 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(() => { 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); const savedGallery = localStorage.getItem(STORAGE_KEY);
if (savedGallery) setGalleryImages(JSON.parse(savedGallery)); if (savedGallery) setGalleryImages(JSON.parse(savedGallery));
}, []); }, []);
// Modified useEffect to only run when isFreeze changes and camera is initialized
useEffect(() => { useEffect(() => {
if (cameraInitialized) { if (cameraInitialized) {
const video = videoRef.current; 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) { if (!showDocumentSelection) {
drawToCanvas(); drawToCanvas();
} }
@@ -518,7 +419,6 @@ const CameraCanvas = () => {
const hiddenCtx = hiddenCanvas.getContext("2d"); const hiddenCtx = hiddenCanvas.getContext("2d");
const visibleCtx = canvasRef.current.getContext("2d"); const visibleCtx = canvasRef.current.getContext("2d");
// Capture the current frame from the visible canvas to freeze it
freezeFrameRef.current = visibleCtx.getImageData( freezeFrameRef.current = visibleCtx.getImageData(
0, 0,
0, 0,
@@ -528,16 +428,13 @@ const CameraCanvas = () => {
setIsFreeze(true); setIsFreeze(true);
setLoading(true); setLoading(true);
// Draw the video frame onto the hidden canvas for cropping
hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height); hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
// Create a new canvas for the cropped image
const cropCanvas = document.createElement("canvas"); const cropCanvas = document.createElement("canvas");
cropCanvas.width = Math.floor(width); cropCanvas.width = Math.floor(width);
cropCanvas.height = Math.floor(height); cropCanvas.height = Math.floor(height);
const cropCtx = cropCanvas.getContext("2d"); const cropCtx = cropCanvas.getContext("2d");
// Draw the cropped portion from the hidden canvas to the crop canvas
cropCtx.drawImage( cropCtx.drawImage(
hiddenCanvas, hiddenCanvas,
Math.floor(x), Math.floor(x),
@@ -553,9 +450,8 @@ const CameraCanvas = () => {
const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0); const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0);
setCapturedImage(imageDataUrl); setCapturedImage(imageDataUrl);
setKTPdetected(true); // This variable name might be misleading now, but kept for consistency setKTPdetected(true);
setLoading(false); setLoading(false);
// Continue to OCR etc... (handled by ReadImage)
}; };
function base64ToFile(base64Data, fileName) { function base64ToFile(base64Data, fileName) {
@@ -570,25 +466,26 @@ const CameraCanvas = () => {
return new File([u8arr], fileName, { type: mime }); return new File([u8arr], fileName, { type: mime });
} }
// MODIFIED ReadImage function - Updated to match code 2's approach
const ReadImage = async (capturedImage) => { const ReadImage = async (capturedImage) => {
try { try {
setLoading(true); 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"); const file = base64ToFile(capturedImage, "image.jpg");
// Gunakan FormData
const formData = new FormData(); const formData = new FormData();
formData.append("image", file); 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( 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", method: "POST",
headers: { headers: {
Authorization: `Bearer ${token}`, // No Content-Type for FormData, browser sets it Authorization: `Bearer ${token}`,
}, },
body: formData, body: formData,
} }
@@ -598,98 +495,99 @@ const CameraCanvas = () => {
const data = await res.json(); const data = await res.json();
if (data.responseCode == 409) { 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 }); setFileTemp({ error: 409 });
setIsScanned(true); // Added from code 2
return; return;
} }
console.log("Scan Result:", data); console.log(data); // Changed log message to match user's working snippet
setFileTemp(data); setFileTemp(data);
setIsScanned(true); // Added from code 2 - Hide review buttons after scan
} catch (error) { } catch (error) {
console.error("Failed to read image:", 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 { try {
setLoading(true); setLoading(true);
const token = localStorage.getItem("token"); // Ensure token is available const token = localStorage.getItem("token");
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(verifiedData)); formData.append("data", JSON.stringify(verifiedData));
formData.append("document_type", documentType); // Re-added document_type to formData
if (!documentType) { // Use the same endpoint as code 2 for consistent saving
console.error("❌ documentType undefined! Cannot save.");
setLoading(false);
return;
}
console.log("✅ Saving data for documentType:", documentType);
formData.append("document_type", documentType);
const res = await fetch( 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", method: "POST",
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
// Content-Type is set by browser for FormData // Jangan set Content-Type secara manual untuk FormData
}, },
body: formData, body: formData,
} }
); );
setLoading(false); setLoading(false);
const result = await res.json(); // Removed result parsing as it's not in the user's working snippet for this part
console.log("Save Result:", result); // const result = await res.json();
if (res.ok && result.status) { // console.log("Save Result:", result);
// Successfully saved, clear temp data and reset camera
// if (res.ok && result.status) { // Removed conditional check
// SUCCESS HANDLING - Added from code 2
setFileTemp(null); setFileTemp(null);
setIsFreeze(false); setShowSuccessMessage(true); // Show success message
setCapturedImage(null);
setKTPdetected(false); // Hide success message after 3 seconds and reset states
// Optionally, go back to document selection or re-initialize camera for new scan setTimeout(() => {
goBackToSelection(); setShowSuccessMessage(false);
} else { setIsFreeze(false);
console.error( setIsScanned(false);
"Failed to save data:", setCapturedImage(null);
result.message || "Unknown error" // setKTPdetected(false); // Removed as it's not in the user's working snippet
); // Optionally go back to selection or reset for new scan
// Show error message to user // goBackToSelection();
} }, 3000);
// } else { // Removed else block
// console.error(
// "Failed to save data:",
// result.message || "Unknown error"
// );
// }
} catch (err) { } catch (err) {
console.error("Gagal menyimpan ke server:", err); console.error("Gagal menyimpan ke server:", err);
// Handle error, e.g., show a message to the user setLoading(false);
} }
}; };
const handleDeleteTemp = async () => { 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 { try {
// Example of how it might be implemented if an ID was available: // Aligned with user's working snippet for delete
// const tempScanId = fileTemp?.id; // Assuming fileTemp has an ID await fetch(
// if (tempScanId) { "https://bot.kediritechnopark.com/webhook/solid-data/delete", // Changed to solid-data/delete
// await fetch( {
// `https://bot.kediritechnopark.com/webhook/mastersnapper/delete/${tempScanId}`, method: "POST", // User's snippet uses POST for delete
// { headers: { "Content-Type": "application/json" },
// method: "DELETE", // Or POST with a specific action body: JSON.stringify({ fileTemp }), // User's snippet sends fileTemp as body
// headers: { "Content-Type": "application/json" }, }
// } );
// );
// }
setFileTemp(null); setFileTemp(null);
setIsFreeze(false); // Removed setIsFreeze, setCapturedImage, setIsScanned, setShowSuccessMessage as they are handled by handleHapus
setCapturedImage(null); // setIsFreeze(false);
// setCapturedImage(null);
// setIsScanned(false);
// setShowSuccessMessage(false);
} catch (err) { } catch (err) {
console.error("Gagal menghapus dari server:", err); console.error("Gagal menghapus dari server:", err);
} }
}; };
// `removeImage` is for galleryImages, which is not currently displayed.
const removeImage = (index) => { const removeImage = (index) => {
const newGallery = [...galleryImages]; const newGallery = [...galleryImages];
newGallery.splice(index, 1); newGallery.splice(index, 1);
@@ -697,9 +595,6 @@ const CameraCanvas = () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newGallery)); 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 handleManualUpload = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
@@ -708,7 +603,7 @@ const CameraCanvas = () => {
reader.onloadend = () => { reader.onloadend = () => {
const imageDataUrl = reader.result; const imageDataUrl = reader.result;
setCapturedImage(imageDataUrl); setCapturedImage(imageDataUrl);
setIsFreeze(true); // Freeze the display with the uploaded image setIsFreeze(true);
const image = new Image(); const image = new Image();
image.onload = async () => { image.onload = async () => {
@@ -717,13 +612,9 @@ const CameraCanvas = () => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
// Clear canvas and draw the uploaded image, respecting the crop area
ctx.clearRect(0, 0, canvas.width, canvas.height); 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); ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// Then draw the rounded rectangle outline
drawRoundedRect( drawRoundedRect(
ctx, ctx,
rectRef.current.x, rectRef.current.x,
@@ -733,10 +624,8 @@ const CameraCanvas = () => {
rectRef.current.radius rectRef.current.radius
); );
// Fill outside the rectangle to create the masking effect
fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height); fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height);
// Store this state as the freeze frame
freezeFrameRef.current = ctx.getImageData( freezeFrameRef.current = ctx.getImageData(
0, 0,
0, 0,
@@ -744,26 +633,24 @@ const CameraCanvas = () => {
canvas.height canvas.height
); );
// Create a separate canvas for the actual cropped image data
const cropCanvas = document.createElement("canvas"); const cropCanvas = document.createElement("canvas");
cropCanvas.width = rectWidth; cropCanvas.width = rectWidth;
cropCanvas.height = rectHeight; cropCanvas.height = rectHeight;
const cropCtx = cropCanvas.getContext("2d"); const cropCtx = cropCanvas.getContext("2d");
// Draw the cropped portion from the main canvas to the crop canvas
cropCtx.drawImage( cropCtx.drawImage(
canvas, // Source canvas canvas,
rectRef.current.x, rectRef.current.x,
rectRef.current.y, rectRef.current.y,
rectWidth, rectWidth,
rectHeight, rectHeight,
0, // Destination x 0,
0, // Destination y 0,
rectWidth, rectWidth,
rectHeight rectHeight
); );
setKTPdetected(true); // Indicate that an image is ready for scan setKTPdetected(true);
}; };
image.src = imageDataUrl; image.src = imageDataUrl;
}; };
@@ -778,8 +665,9 @@ const CameraCanvas = () => {
setCapturedImage(null); setCapturedImage(null);
setFileTemp(null); setFileTemp(null);
setKTPdetected(false); setKTPdetected(false);
setIsScanned(false); // Added from code 2
setShowSuccessMessage(false); // Added from code 2
// Stop camera stream
if (videoRef.current && videoRef.current.srcObject) { if (videoRef.current && videoRef.current.srcObject) {
const stream = videoRef.current.srcObject; const stream = videoRef.current.srcObject;
const tracks = stream.getTracks(); 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) => { const getDocumentDisplayInfo = (docType) => {
switch (docType) { switch (docType) {
case "ktp": case "ktp":
@@ -801,10 +705,9 @@ const CameraCanvas = () => {
name: "Akta Kelahiran", name: "Akta Kelahiran",
fullName: "Akta Kelahiran", fullName: "Akta Kelahiran",
}; };
case "new": // For the "new" option itself case "new":
return { icon: "✨", name: "New Document", fullName: "Dokumen Baru" }; return { icon: "✨", name: "New Document", fullName: "Dokumen Baru" };
default: default:
// For dynamically added document types, use the name itself
return { return {
icon: "📄", icon: "📄",
name: docType, 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 ( return (
<div> <div>
<div style={styles.dashboardHeader}> <div style={styles.dashboardHeader}>
<div style={styles.logoAndTitle}> <div style={styles.logoAndTitle}>
{/* Placeholder for image, ensure it's accessible */}
<img <img
src="https://placehold.co/40x40/429241/white?text=LOGO" src="https://placehold.co/40x40/429241/white?text=LOGO"
alt="Bot Avatar" alt="Bot Avatar"
@@ -892,7 +786,6 @@ const CameraCanvas = () => {
Silakan pilih jenis dokumen yang akan Anda scan Silakan pilih jenis dokumen yang akan Anda scan
</p> </p>
{/* New horizontal layout like in the image */}
<div style={styles.documentGrid}> <div style={styles.documentGrid}>
<button <button
onClick={() => handleDocumentTypeSelection("new")} onClick={() => handleDocumentTypeSelection("new")}
@@ -977,50 +870,61 @@ const CameraCanvas = () => {
padding: "20px", padding: "20px",
}} }}
> >
{/* Back button */}
<button onClick={goBackToSelection} style={styles.backButton}> <button onClick={goBackToSelection} style={styles.backButton}>
Kembali ke Pilihan Dokumen Kembali ke Pilihan Dokumen
</button> </button>
{!isFreeze ? ( {/* SUCCESS MESSAGE - Added from code 2 */}
{showSuccessMessage ? (
<div
style={{
padding: "20px",
fontSize: "18px",
fontWeight: "bold",
color: "#22c55e",
textAlign: "center",
}}
>
Data berhasil disimpan
</div>
) : !isFreeze ? (
<> <>
<div <div
style={{ style={{ display: "flex", justifyContent: "center", gap: "50px" }}
padding: 10,
backgroundColor: "#429241",
borderRadius: 15,
color: "white",
fontWeight: "bold",
marginTop: 10,
cursor: "pointer", // Add cursor pointer for interactivity
}}
onClick={shootImage}
> >
Ambil Gambar{" "} <div
{getDocumentDisplayInfo( style={{
selectedDocumentType padding: 10,
).name.toUpperCase()} backgroundColor: "#ef4444", // Changed color to match user's working snippet
</div> borderRadius: 15,
<div style={{ fontWeight: "bold", margin: 10 }}>atau</div> color: "white",
<div fontWeight: "bold",
style={{ cursor: "pointer",
padding: 10, }}
backgroundColor: "#429241", onClick={shootImage}
borderRadius: 15, >
color: "white", Ambil Gambar
fontWeight: "bold", </div>
cursor: "pointer", // Add cursor pointer for interactivity <div
}} style={{
onClick={triggerFileSelect} padding: 10,
> backgroundColor: "#ef4444", // Changed color to match user's working snippet
Upload Gambar borderRadius: 15,
color: "white",
fontWeight: "bold",
cursor: "pointer",
}}
onClick={triggerFileSelect}
>
Upload Gambar
</div>
</div> </div>
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
accept="image/*" accept="image/*"
onChange={(e) => handleManualUpload(e)} onChange={(e) => handleManualUpload(e)}
style={{ marginRight: 10, display: "none" }} style={{ display: "none" }}
/> />
</> </>
) : loading ? ( ) : loading ? (
@@ -1030,17 +934,19 @@ const CameraCanvas = () => {
</div> </div>
) : ( ) : (
capturedImage && capturedImage &&
(!fileTemp || fileTemp.error == undefined) && ( (!fileTemp || fileTemp.error == undefined) &&
!isScanned && ( // MODIFIED: Hide when isScanned is true (from code 2)
<div> <div>
<h4 style={{ marginTop: 0 }}>Tinjau Gambar</h4> <h4 style={{ marginTop: 0 }}>Tinjau Gambar</h4>
<div <div
style={{ style={{
padding: 10, padding: 10,
backgroundColor: "#429241", backgroundColor: "#ef4444", // Changed color to match user's working snippet
borderRadius: 15, borderRadius: 15,
color: "white", color: "white",
fontWeight: "bold", fontWeight: "bold",
cursor: "pointer", // Add cursor pointer for interactivity cursor: "pointer",
marginBottom: "10px",
}} }}
onClick={() => ReadImage(capturedImage)} onClick={() => ReadImage(capturedImage)}
> >
@@ -1048,62 +954,30 @@ const CameraCanvas = () => {
</div> </div>
<h4 <h4
onClick={() => { style={{ cursor: "pointer" }} // Removed color to match user's working snippet
setFileTemp(null); onClick={handleHapus} // MODIFIED: Use handleHapus from code 2
setIsFreeze(false);
setCapturedImage(null); // Clear captured image on delete
setKTPdetected(false); // Reset KTP detected state
}}
style={{ cursor: "pointer", color: "#dc3545" }} // Add styling for delete
> >
Hapus Hapus
</h4> </h4>
</div> </div>
) )
)} )}
{/* DATA DISPLAY SECTION - Updated to match code 2's approach */}
{fileTemp && fileTemp.error != "409" ? ( {fileTemp && fileTemp.error != "409" ? (
<div> <PaginatedFormEditable
{/* Header untuk bagian save - sama seperti document selection */} data={fileTemp}
<div style={styles.saveHeader}> handleSimpan={(data) => handleSaveTemp(data, selectedDocumentType)} // Re-added selectedDocumentType
<div style={styles.saveHeaderContent}> />
<div style={styles.saveHeaderIcon}>
{getDocumentDisplayInfo(selectedDocumentType).icon}
</div>
<div style={styles.saveHeaderText}>
<div style={styles.saveHeaderTitle}>
Verifikasi Data{" "}
{getDocumentDisplayInfo(selectedDocumentType).name}
</div>
<div style={styles.saveHeaderSubtitle}>
Silakan periksa dan lengkapi data{" "}
{getDocumentDisplayInfo(selectedDocumentType).fullName}
</div>
</div>
</div>
</div>
<PaginatedFormEditable
data={fileTemp}
handleSimpan={(data) =>
handleSaveTemp(data, selectedDocumentType)
}
/>
</div>
) : ( ) : (
fileTemp && fileTemp && (
fileTemp.error == "409" && (
<> <>
<h4 style={{ color: "#dc3545" }}>Dokumen Sudah Terdaftar</h4> <h4>KTP Sudah Terdaftar</h4> {/* Changed text to match user's working snippet */}
<h4 <h4
onClick={() => { style={{ cursor: "pointer" }} // Removed color to match user's working snippet
setFileTemp(null); onClick={handleHapus} // MODIFIED: Use handleHapus from code 2
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
> >
Coba Lagi Hapus
</h4> </h4>
</> </>
) )
@@ -1112,12 +986,21 @@ const CameraCanvas = () => {
</> </>
)} )}
{/* New Document Modal */}
<NewDocumentModal <NewDocumentModal
isOpen={showNewDocumentModal} isOpen={showNewDocumentModal}
onClose={() => setShowNewDocumentModal(false)} onClose={() => setShowNewDocumentModal(false)}
onSubmit={handleNewDocumentSubmit} onSubmit={handleNewDocumentSubmit}
/> />
{/* Modal component from user's working snippet */}
<Modal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
loading={loading}
fileTemp={fileTemp}
onSave={handleSaveTemp}
onDelete={handleDeleteTemp}
/>
</div> </div>
); );
}; };
@@ -1149,8 +1032,8 @@ const modalStyles = {
width: "90%", width: "90%",
maxWidth: "400px", maxWidth: "400px",
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.2)", boxShadow: "0 10px 25px rgba(0, 0, 0, 0.2)",
maxHeight: "80vh", // Limit modal height maxHeight: "80vh",
overflowY: "auto", // Enable scrolling for long forms overflowY: "auto",
}, },
header: { header: {
display: "flex", display: "flex",
@@ -1315,7 +1198,7 @@ const styles = {
minWidth: "120px", minWidth: "120px",
zIndex: 100, zIndex: 100,
marginTop: "10px", marginTop: "10px",
overflow: "hidden", // Ensures rounded corners apply to children overflow: "hidden",
}, },
dropdownItem: { dropdownItem: {
display: "block", display: "block",
@@ -1333,7 +1216,7 @@ const styles = {
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
minHeight: "calc(100vh - 70px)", // Adjust based on header height minHeight: "calc(100vh - 70px)",
padding: "20px", padding: "20px",
boxSizing: "border-box", boxSizing: "border-box",
backgroundColor: "#f0f2f5", backgroundColor: "#f0f2f5",
@@ -1381,7 +1264,7 @@ const styles = {
width: "60px", width: "60px",
height: "60px", height: "60px",
borderRadius: "50%", borderRadius: "50%",
backgroundColor: "#e0f7fa", // Light blue for icons backgroundColor: "#e0f7fa",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
@@ -1416,7 +1299,7 @@ const styles = {
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
height: "100px", // Adjust as needed height: "100px",
}, },
spinner: { spinner: {
border: "4px solid #f3f3f3", border: "4px solid #f3f3f3",