ok
This commit is contained in:
@@ -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);
|
||||||
|
setShowSuccessMessage(true); // Show success message
|
||||||
|
|
||||||
|
// Hide success message after 3 seconds and reset states
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowSuccessMessage(false);
|
||||||
setIsFreeze(false);
|
setIsFreeze(false);
|
||||||
|
setIsScanned(false);
|
||||||
setCapturedImage(null);
|
setCapturedImage(null);
|
||||||
setKTPdetected(false);
|
// setKTPdetected(false); // Removed as it's not in the user's working snippet
|
||||||
// Optionally, go back to document selection or re-initialize camera for new scan
|
// Optionally go back to selection or reset for new scan
|
||||||
goBackToSelection();
|
// goBackToSelection();
|
||||||
} else {
|
}, 3000);
|
||||||
console.error(
|
// } else { // Removed else block
|
||||||
"Failed to save data:",
|
// console.error(
|
||||||
result.message || "Unknown error"
|
// "Failed to save data:",
|
||||||
);
|
// result.message || "Unknown error"
|
||||||
// Show error message to user
|
// );
|
||||||
}
|
// }
|
||||||
} 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
|
||||||
|
style={{ display: "flex", justifyContent: "center", gap: "50px" }}
|
||||||
|
>
|
||||||
<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",
|
||||||
marginTop: 10,
|
cursor: "pointer",
|
||||||
cursor: "pointer", // Add cursor pointer for interactivity
|
|
||||||
}}
|
}}
|
||||||
onClick={shootImage}
|
onClick={shootImage}
|
||||||
>
|
>
|
||||||
Ambil Gambar{" "}
|
Ambil Gambar
|
||||||
{getDocumentDisplayInfo(
|
|
||||||
selectedDocumentType
|
|
||||||
).name.toUpperCase()}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontWeight: "bold", margin: 10 }}>atau</div>
|
|
||||||
<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",
|
||||||
}}
|
}}
|
||||||
onClick={triggerFileSelect}
|
onClick={triggerFileSelect}
|
||||||
>
|
>
|
||||||
Upload Gambar
|
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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{fileTemp && fileTemp.error != "409" ? (
|
|
||||||
<div>
|
|
||||||
{/* Header untuk bagian save - sama seperti document selection */}
|
|
||||||
<div style={styles.saveHeader}>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
{/* DATA DISPLAY SECTION - Updated to match code 2's approach */}
|
||||||
|
{fileTemp && fileTemp.error != "409" ? (
|
||||||
<PaginatedFormEditable
|
<PaginatedFormEditable
|
||||||
data={fileTemp}
|
data={fileTemp}
|
||||||
handleSimpan={(data) =>
|
handleSimpan={(data) => handleSaveTemp(data, selectedDocumentType)} // Re-added selectedDocumentType
|
||||||
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",
|
||||||
|
|||||||
Reference in New Issue
Block a user