This commit is contained in:
Vassshhh
2025-08-01 16:45:33 +07:00
parent 79914fb7ef
commit e30b1a8de8
3 changed files with 676 additions and 438 deletions

View File

@@ -17,6 +17,69 @@ const FileListComponent = ({
const [successMessage, setSuccessMessage] = useState(""); const [successMessage, setSuccessMessage] = useState("");
const [selectedDocumentType, setSelectedDocumentType] = useState(""); const [selectedDocumentType, setSelectedDocumentType] = useState("");
// Helper function to convert snake_case to Title Case
const formatKeyToLabel = (key) => {
return key
.replace(/_/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
};
// Helper function to check if value is a date string and convert it
const formatValue = (key, value) => {
if (value === null || value === undefined || value === '') {
return null;
}
// Check if the value looks like a date
if (typeof value === 'string' &&
(key.includes('tanggal') || key.includes('lahir') || key.includes('berlaku') || key.includes('pembuatan') || key.includes('created_at'))) {
const date = new Date(value);
if (!isNaN(date.getTime())) {
return date;
}
}
return value;
};
// Dynamic function to process data for Excel export
const processDataForExcel = (data) => {
if (!data || data.length === 0) return [];
return data.map((item) => {
const processedItem = {};
Object.entries(item).forEach(([key, value]) => {
// Skip null, undefined, or empty string values
if (value === null || value === undefined || value === '') {
return;
}
// Skip certain keys that are not needed in export
const excludedKeys = ['id', 'document_type', 'created_at', 'data', 'foto_url'];
if (excludedKeys.includes(key)) {
return;
}
// Format the key as label
const label = formatKeyToLabel(key);
// Format the value
const formattedValue = formatValue(key, value);
processedItem[label] = formattedValue;
});
return processedItem;
});
};
// Dynamic function to get unique document types
const getUniqueDocumentTypes = (data) => {
const types = [...new Set(data.map(item => item.document_type).filter(Boolean))];
return types;
};
useEffect(() => { useEffect(() => {
const fetchFiles = async () => { const fetchFiles = async () => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@@ -47,7 +110,7 @@ const FileListComponent = ({
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const totalToday = fileData.filter((f) => const totalToday = fileData.filter((f) =>
f.created_at.startsWith(today) f.created_at && f.created_at.startsWith(today)
).length; ).length;
setTotalFilesSentToday(totalToday); setTotalFilesSentToday(totalToday);
@@ -55,6 +118,7 @@ const FileListComponent = ({
const currentMonth = now.getMonth(); const currentMonth = now.getMonth();
const currentYear = now.getFullYear(); const currentYear = now.getFullYear();
const totalThisMonth = fileData.filter((f) => { const totalThisMonth = fileData.filter((f) => {
if (!f.created_at) return false;
const d = new Date(f.created_at); const d = new Date(f.created_at);
return ( return (
d.getMonth() === currentMonth && d.getFullYear() === currentYear d.getMonth() === currentMonth && d.getFullYear() === currentYear
@@ -64,7 +128,10 @@ const FileListComponent = ({
setTotalFilesSentOverall(fileData.length); setTotalFilesSentOverall(fileData.length);
const dateObjects = fileData.map((item) => new Date(item.created_at)); const dateObjects = fileData
.filter(item => item.created_at)
.map((item) => new Date(item.created_at));
if (dateObjects.length > 0) { if (dateObjects.length > 0) {
const minDate = new Date(Math.min(...dateObjects)); const minDate = new Date(Math.min(...dateObjects));
const maxDate = new Date(Math.max(...dateObjects)); const maxDate = new Date(Math.max(...dateObjects));
@@ -81,14 +148,16 @@ const FileListComponent = ({
current.setMonth(current.getMonth() + 1); current.setMonth(current.getMonth() + 1);
} }
fileData.forEach((item) => { fileData
const d = new Date(item.created_at); .filter(item => item.created_at)
const monthKey = `${d.getFullYear()}-${String( .forEach((item) => {
d.getMonth() + 1 const d = new Date(item.created_at);
).padStart(2, "0")}`; const monthKey = `${d.getFullYear()}-${String(
if (monthlyDataMap[monthKey] !== undefined) d.getMonth() + 1
monthlyDataMap[monthKey]++; ).padStart(2, "0")}`;
}); if (monthlyDataMap[monthKey] !== undefined)
monthlyDataMap[monthKey]++;
});
const performanceArray = Object.entries(monthlyDataMap).map( const performanceArray = Object.entries(monthlyDataMap).map(
([month, count]) => { ([month, count]) => {
@@ -133,7 +202,7 @@ const FileListComponent = ({
try { try {
const response = await fetch( const response = await fetch(
`https://bot.kediritechnopark.com/webhook/solid-data/merged?nama_lengkap=${encodeURIComponent( `https://bot.kediritechnopark.com/webhook/solid-data/merged?nama_lengkap=${encodeURIComponent(
file.nama_lengkap file.nama_lengkap || ''
)}`, )}`,
{ {
method: "GET", method: "GET",
@@ -155,6 +224,9 @@ const FileListComponent = ({
return; return;
} }
console.log("Data received from merged API:", data[0]); // Debug log
console.log("All keys in data:", Object.keys(data[0])); // Debug log
console.log("Non-null values:", Object.entries(data[0]).filter(([k,v]) => v !== null)); // Debug log
setSelectedFile(data[0]); setSelectedFile(data[0]);
} catch (error) { } catch (error) {
console.error("Gagal mengambil detail:", error.message); console.error("Gagal mengambil detail:", error.message);
@@ -177,38 +249,21 @@ const FileListComponent = ({
phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3"); phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3");
const exportToExcel = (data) => { const exportToExcel = (data) => {
const modifiedData = data.map((item) => ({ const processedData = processDataForExcel(data);
ID: item.id, if (processedData.length === 0) {
Petugas_ID: item.petugas_id, alert("Tidak ada data untuk diekspor.");
Petugas: item.username, return;
NIK: item.nik, }
Nama_Lengkap: item.nama_lengkap,
Tempat_Lahir: item.tempat_lahir,
Tanggal_Lahir: new Date(item.tanggal_lahir),
Jenis_Kelamin: item.jenis_kelamin,
Alamat: item.alamat,
RT: item.rt,
RW: item.rw,
Kel_Desa: item.kel_desa,
Kecamatan: item.kecamatan,
Agama: item.agama,
Status_Perkawinan: item.status_perkawinan,
Pekerjaan: item.pekerjaan,
Kewarganegaraan: item.kewarganegaraan,
No_HP: item.no_hp,
Email: item.email,
Berlaku_Hingga: new Date(item.berlaku_hingga),
Pembuatan: new Date(item.pembuatan),
Kota_Pembuatan: item.kota_pembuatan,
Created_At: new Date(item.created_at),
}));
const worksheet = XLSX.utils.json_to_sheet(modifiedData); const worksheet = XLSX.utils.json_to_sheet(processedData);
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Data"); XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
XLSX.writeFile(workbook, "data-export.xlsx"); XLSX.writeFile(workbook, "data-export.xlsx");
}; };
// Get unique document types for dropdown
const documentTypes = getUniqueDocumentTypes(files);
return ( return (
<div className={styles.fileListSection}> <div className={styles.fileListSection}>
<div className={styles.fileListHeader}> <div className={styles.fileListHeader}>
@@ -220,9 +275,9 @@ const FileListComponent = ({
className={styles.fileCount} className={styles.fileCount}
> >
<option value="">Semua</option> <option value="">Semua</option>
<option value="ktp">KTP</option> {documentTypes.map(type => (
<option value="kk">KK</option> <option key={type} value={type}>{type}</option>
<option value="akta_kelahiran">Akta Kelahiran</option> ))}
</select> </select>
<button <button
onClick={() => { onClick={() => {
@@ -250,14 +305,14 @@ const FileListComponent = ({
<div className={styles.emptyState}> <div className={styles.emptyState}>
<div className={styles.emptyStateTitle}>Belum ada data</div> <div className={styles.emptyStateTitle}>Belum ada data</div>
<p className={styles.emptyStateText}> <p className={styles.emptyStateText}>
Tidak ada data KK yang tersedia saat ini. Tidak ada data yang tersedia saat ini.
</p> </p>
</div> </div>
) : ( ) : (
<table className={styles.fileTable}> <table className={styles.fileTable}>
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>No</th>
<th>NIK</th> <th>NIK</th>
<th>Jenis</th> <th>Jenis</th>
<th className={styles.nameColumn}>Nama Lengkap</th> <th className={styles.nameColumn}>Nama Lengkap</th>
@@ -266,33 +321,32 @@ const FileListComponent = ({
<tbody> <tbody>
{filteredFiles.map((file, index) => ( {filteredFiles.map((file, index) => (
<tr <tr
key={file.id} key={file.id || index}
onClick={() => handleRowClick(file)} onClick={() => handleRowClick(file)}
className={styles.tableRow} className={styles.tableRow}
> >
<td>{index + 1}</td> <td>{index + 1}</td>
<td>{file.nik}</td> <td>{file.nik || '-'}</td>
<td>{file.document_type}</td> <td>{file.document_type || '-'}</td>
<td className={styles.nameColumn}>{file.nama_lengkap}</td> <td className={styles.nameColumn}>{file.nama_lengkap || '-'}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
)} )}
</div> </div>
{/* Modal dan komponen lainnya tetap seperti sebelumnya */}
{/* Modal */}
{selectedFile && ( {selectedFile && (
<div className={styles.modalOverlay} onClick={closeModal}> <div className={styles.modalOverlay} onClick={closeModal}>
{" "}
<div <div
className={styles.modalContent} className={styles.modalContent}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{" "}
{selectedFile.data && ( {selectedFile.data && (
<img <img
src={getImageSrc(selectedFile.data)} src={getImageSrc(selectedFile.data)}
alt={`Foto KTP - ${selectedFile.nik}`} alt={`Foto Document - ${selectedFile.nik || 'Unknown'}`}
style={{ style={{
width: "100%", width: "100%",
maxHeight: "300px", maxHeight: "300px",
@@ -302,7 +356,7 @@ const FileListComponent = ({
boxShadow: "0 2px 6px rgba(0,0,0,0.2)", boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
}} }}
/> />
)}{" "} )}
<h3>🪪 Detail Data Document</h3> <h3>🪪 Detail Data Document</h3>
<div style={{ marginBottom: "1rem" }}> <div style={{ marginBottom: "1rem" }}>
<PDFDownloadLink <PDFDownloadLink
@@ -319,7 +373,7 @@ const FileListComponent = ({
}} }}
/> />
} }
fileName={`KTP_${selectedFile.nik}.pdf`} fileName={`Document_${selectedFile.nik || selectedFile.id || 'unknown'}.pdf`}
style={{ style={{
textDecoration: "none", textDecoration: "none",
padding: "8px 16px", padding: "8px 16px",
@@ -336,46 +390,97 @@ const FileListComponent = ({
</div> </div>
<table className={styles.detailTable}> <table className={styles.detailTable}>
<tbody> <tbody>
{[ {selectedFile && (console.log("selectedFile in modal:", selectedFile), true) &&
["NIK", selectedFile.nik], Object.entries(selectedFile)
["No.Al", selectedFile.no_al], .map(([key, value]) => {
["Nomor Akta Kelahiran", selectedFile.akta_kelahiran_nomor], console.log(`Processing: ${key} = ${value} (type: ${typeof value})`);
["Nama Lengkap", selectedFile.nama_lengkap], return [key, value];
["Anak Ke", selectedFile.anak_ke], })
["Tempat Lahir", selectedFile.tempat_lahir], .filter(([key, value]) => {
["Tanggal Lahir", selectedFile.tanggal_lahir], console.log(`Filtering: ${key} = ${value}`);
["Jenis Kelamin", selectedFile.jenis_kelamin],
["Alamat", selectedFile.alamat], // Exclude specific keys that are not part of the display data
["Ayah", selectedFile.ayah], const excludedKeys = [
["ibu", selectedFile.ibu], "id",
["RT", selectedFile.rt], "document_type",
["RW", selectedFile.rw], "created_at",
["Kelurahan/Desa", selectedFile.kel_desa], "data", // Exclude image data
["Kecamatan", selectedFile.kecamatan], "foto_url", // Exclude image URL
["Agama", selectedFile.agama], ];
["Status Perkawinan", selectedFile.status_perkawinan],
["Pekerjaan", selectedFile.pekerjaan], if (excludedKeys.includes(key)) {
["Kewarganegaraan", selectedFile.kewarganegaraan], console.log(`Excluded key: ${key}`);
["No HP", selectedFile.no_hp], return false;
["Email", selectedFile.email], }
["Berlaku Hingga", selectedFile.berlaku_hingga],
["Tanggal Pembuatan", selectedFile.pembuatan], if (value === null) {
["Kota Pembuatan", selectedFile.kota_pembuatan], console.log(`Null value for key: ${key}`);
] return false;
.filter(([_, value]) => value !== null && value !== "") }
.map(([label, value]) => ( if (value === undefined) {
<tr key={label}> console.log(`Undefined value for key: ${key}`);
<td>{label}</td> return false;
<td>{value}</td> }
</tr> if (typeof value === 'string' && value.trim() === '') {
))} console.log(`Empty string for key: ${key}`);
return false;
}
console.log(`Keeping key: ${key} with value: ${value}`);
return true;
})
.map(([key, value]) => {
console.log(`Rendering field: ${key} = ${value}`);
// Special handling for 'anggota' array
if (key === "anggota" && Array.isArray(value)) {
return (
<tr key={key}>
<td>{formatKeyToLabel(key)}</td>
<td>
{value.map((member, idx) => (
<div key={idx} style={{ marginBottom: "10px", borderBottom: "1px dashed #eee", paddingBottom: "5px" }}>
{Object.entries(member)
.filter(([_, memberValue]) => {
if (memberValue === null || memberValue === undefined) return false;
if (typeof memberValue === 'string' && memberValue.trim() === '') return false;
return true;
})
.map(([memberKey, memberValue]) => (
<div key={memberKey}>
<strong>{formatKeyToLabel(memberKey)}:</strong> {memberValue}
</div>
))}
</div>
))}
</td>
</tr>
);
}
// Format dates for display
let displayValue = value;
if (typeof value === 'string' &&
(key.includes('tanggal') || key.includes('lahir') || key.includes('berlaku') || key.includes('pembuatan') || key.includes('created_at'))) {
const date = new Date(value);
if (!isNaN(date.getTime())) {
displayValue = date.toLocaleDateString('id-ID');
}
}
return (
<tr key={key}>
<td>{formatKeyToLabel(key)}</td>
<td>{displayValue}</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
<button className={styles.closeButton} onClick={closeModal}> <button className={styles.closeButton} onClick={closeModal}>
{" "} Tutup
Tutup{" "} </button>
</button>{" "} </div>
</div>{" "}
</div> </div>
)} )}
</div> </div>

View File

@@ -1,276 +1,276 @@
import React, { useEffect, useState } from "react"; // import React, { useEffect, useState } from "react";
const fieldLabels = { // const fieldLabels = {
nik: "NIK", // nik: "NIK",
fullName: "Nama Lengkap", // fullName: "Nama Lengkap",
birthPlace: "Tempat Lahir", // birthPlace: "Tempat Lahir",
birthDate: "Tanggal Lahir", // birthDate: "Tanggal Lahir",
gender: "Jenis Kelamin", // gender: "Jenis Kelamin",
address: "Alamat", // address: "Alamat",
neighborhoodCode: "RT/RW", // neighborhoodCode: "RT/RW",
village: "Kelurahan/Desa", // village: "Kelurahan/Desa",
subDistrict: "Kecamatan", // subDistrict: "Kecamatan",
religion: "Agama", // religion: "Agama",
maritalStatus: "Status Perkawinan", // maritalStatus: "Status Perkawinan",
occupation: "Pekerjaan", // occupation: "Pekerjaan",
nationality: "Kewarganegaraan", // nationality: "Kewarganegaraan",
validUntil: "Berlaku Hingga", // validUntil: "Berlaku Hingga",
issuedCity: "Kota Terbit", // issuedCity: "Kota Terbit",
issuedDate: "Tanggal Terbit", // issuedDate: "Tanggal Terbit",
phoneNumber: "No. HP", // phoneNumber: "No. HP",
email: "Email", // email: "Email",
}; // };
function Modal({ isOpen, onClose, loading, fileTemp, onSave, onDelete }) { // function Modal({ isOpen, onClose, loading, fileTemp, onSave, onDelete }) {
const [formData, setFormData] = useState({}); // const [formData, setFormData] = useState({});
const [step, setStep] = useState(0); // const [step, setStep] = useState(0);
// ❗Field yang disembunyikan, bisa diisi sesuai kebutuhan // // ❗Field yang disembunyikan, bisa diisi sesuai kebutuhan
const disabledFields = []; // const disabledFields = [];
useEffect(() => { // useEffect(() => {
if (fileTemp) { // if (fileTemp) {
setFormData(Array.isArray(fileTemp) ? fileTemp[0] : fileTemp); // setFormData(Array.isArray(fileTemp) ? fileTemp[0] : fileTemp);
setStep(0); // setStep(0);
} else { // } else {
setFormData({}); // setFormData({});
} // }
}, [fileTemp]); // }, [fileTemp]);
if (!isOpen) return null; // if (!isOpen) return null;
const handleChange = (key, newValue, isDate = false) => { // const handleChange = (key, newValue, isDate = false) => {
setFormData((prev) => ({ // setFormData((prev) => ({
...prev, // ...prev,
[key]: isDate ? { ...prev[key], value: newValue } : newValue, // [key]: isDate ? { ...prev[key], value: newValue } : newValue,
})); // }));
}; // };
const formatDate = (value) => { // const formatDate = (value) => {
if (!value) return ""; // if (!value) return "";
const d = new Date(value); // const d = new Date(value);
return isNaN(d) ? "" : d.toISOString().split("T")[0]; // return isNaN(d) ? "" : d.toISOString().split("T")[0];
}; // };
const renderInput = (key, value) => { // const renderInput = (key, value) => {
if (value && typeof value === "object" && value.type === "dateTime") { // if (value && typeof value === "object" && value.type === "dateTime") {
return ( // return (
<input // <input
type="date" // type="date"
value={formatDate(value.value)} // value={formatDate(value.value)}
onChange={(e) => handleChange(key, e.target.value, true)} // onChange={(e) => handleChange(key, e.target.value, true)}
style={styles.input} // style={styles.input}
/> // />
); // );
} // }
if (key === "address") { // if (key === "address") {
return ( // return (
<textarea // <textarea
rows={2} // rows={2}
value={value || ""} // value={value || ""}
onChange={(e) => handleChange(key, e.target.value)} // onChange={(e) => handleChange(key, e.target.value)}
style={{ ...styles.input, resize: "vertical" }} // style={{ ...styles.input, resize: "vertical" }}
/> // />
); // );
} // }
return ( // return (
<input // <input
type="text" // type="text"
value={value != null ? value : ""} // value={value != null ? value : ""}
onChange={(e) => handleChange(key, e.target.value)} // onChange={(e) => handleChange(key, e.target.value)}
style={styles.input} // style={styles.input}
/> // />
); // );
}; // };
// Langkah-langkah form (per halaman) // // Langkah-langkah form (per halaman)
const rawSteps = [ // const rawSteps = [
["nik", "fullName", "birthPlace", "birthDate"], // ["nik", "fullName", "birthPlace", "birthDate"],
["gender", "address", "neighborhoodCode", "village", "subDistrict"], // ["gender", "address", "neighborhoodCode", "village", "subDistrict"],
["religion", "maritalStatus", "occupation"], // ["religion", "maritalStatus", "occupation"],
[ // [
"nationality", // "nationality",
"validUntil", // "validUntil",
"issuedCity", // "issuedCity",
"issuedDate", // "issuedDate",
"phoneNumber", // "phoneNumber",
"email", // "email",
], // ],
]; // ];
// Filter field yang disable/hide // // Filter field yang disable/hide
const steps = rawSteps.map((fields) => // const steps = rawSteps.map((fields) =>
fields.filter((key) => !disabledFields.includes(key)) // fields.filter((key) => !disabledFields.includes(key))
); // );
// Filter langkah kosong // // Filter langkah kosong
const visibleSteps = steps.filter((step) => step.length > 0); // const visibleSteps = steps.filter((step) => step.length > 0);
return ( // return (
<div style={styles.overlay} onClick={onClose}> // <div style={styles.overlay} onClick={onClose}>
<div style={styles.modal} onClick={(e) => e.stopPropagation()}> // <div style={styles.modal} onClick={(e) => e.stopPropagation()}>
{loading ? ( // {loading ? (
<div style={styles.spinnerContainer}> // <div style={styles.spinnerContainer}>
<div style={styles.spinner} /> // <div style={styles.spinner} />
<style>{spinnerStyle}</style> // <style>{spinnerStyle}</style>
</div> // </div>
) : ( // ) : (
Object.keys(formData).length > 0 && ( // Object.keys(formData).length > 0 && (
<> // <>
<h4> // <h4>
Verifikasi Data (Langkah {step + 1} dari {visibleSteps.length}) // Verifikasi Data (Langkah {step + 1} dari {visibleSteps.length})
</h4> // </h4>
<table style={styles.table}> // <table style={styles.table}>
<tbody> // <tbody>
{visibleSteps[step].map((key) => ( // {visibleSteps[step].map((key) => (
<tr key={key} style={styles.tableRow}> // <tr key={key} style={styles.tableRow}>
<td style={styles.tableLabel}> // <td style={styles.tableLabel}>
{fieldLabels[key] || key} // {fieldLabels[key] || key}
</td> // </td>
<td style={styles.tableInput}> // <td style={styles.tableInput}>
{renderInput(key, formData[key])} // {renderInput(key, formData[key])}
</td> // </td>
</tr> // </tr>
))} // ))}
</tbody> // </tbody>
</table> // </table>
<div // <div
style={{ // style={{
display: "flex", // display: "flex",
justifyContent: "space-between", // justifyContent: "space-between",
marginTop: 10, // marginTop: 10,
}} // }}
> // >
<button // <button
disabled={step === 0} // disabled={step === 0}
onClick={() => setStep((s) => s - 1)} // onClick={() => setStep((s) => s - 1)}
style={{ // style={{
...styles.saveButton, // ...styles.saveButton,
opacity: step === 0 ? 0.5 : 1, // opacity: step === 0 ? 0.5 : 1,
}} // }}
> // >
&lt; Sebelumnya // &lt; Sebelumnya
</button> // </button>
<button // <button
disabled={step === visibleSteps.length - 1} // disabled={step === visibleSteps.length - 1}
onClick={() => setStep((s) => s + 1)} // onClick={() => setStep((s) => s + 1)}
style={{ // style={{
...styles.saveButton, // ...styles.saveButton,
opacity: step === visibleSteps.length - 1 ? 0.5 : 1, // opacity: step === visibleSteps.length - 1 ? 0.5 : 1,
}} // }}
> // >
Selanjutnya &gt; // Selanjutnya &gt;
</button> // </button>
</div> // </div>
<div style={styles.actions}> // <div style={styles.actions}>
<button // <button
onClick={() => onSave(formData)} // onClick={() => onSave(formData)}
style={styles.saveButton} // style={styles.saveButton}
> // >
Simpan ke Galeri // Simpan ke Galeri
</button> // </button>
<button onClick={onDelete} style={styles.deleteButton}> // <button onClick={onDelete} style={styles.deleteButton}>
Hapus // Hapus
</button> // </button>
</div> // </div>
</> // </>
) // )
)} // )}
</div> // </div>
</div> // </div>
); // );
} // }
// Styles dan spinner animation // // Styles dan spinner animation
const styles = { // const styles = {
overlay: { // overlay: {
position: "fixed", // position: "fixed",
inset: 0, // inset: 0,
backgroundColor: "rgba(0,0,0,0.5)", // backgroundColor: "rgba(0,0,0,0.5)",
display: "flex", // display: "flex",
justifyContent: "center", // justifyContent: "center",
alignItems: "center", // alignItems: "center",
zIndex: 1000, // zIndex: 1000,
}, // },
modal: { // modal: {
backgroundColor: "white", // backgroundColor: "white",
borderRadius: 8, // borderRadius: 8,
padding: 20, // padding: 20,
minWidth: 350, // minWidth: 350,
maxWidth: "90vw", // maxWidth: "90vw",
maxHeight: "80vh", // maxHeight: "80vh",
overflowY: "auto", // overflowY: "auto",
boxShadow: "0 2px 10px rgba(0,0,0,0.3)", // boxShadow: "0 2px 10px rgba(0,0,0,0.3)",
}, // },
spinnerContainer: { // spinnerContainer: {
textAlign: "center", // textAlign: "center",
padding: 40, // padding: 40,
}, // },
spinner: { // spinner: {
border: "4px solid #f3f3f3", // border: "4px solid #f3f3f3",
borderTop: "4px solid #3498db", // borderTop: "4px solid #3498db",
borderRadius: "50%", // borderRadius: "50%",
width: 40, // width: 40,
height: 40, // height: 40,
animation: "spin 1s linear infinite", // animation: "spin 1s linear infinite",
margin: "0 auto", // margin: "0 auto",
}, // },
table: { // table: {
width: "100%", // width: "100%",
borderCollapse: "collapse", // borderCollapse: "collapse",
}, // },
tableRow: { // tableRow: {
borderBottom: "1px solid #eee", // borderBottom: "1px solid #eee",
}, // },
tableLabel: { // tableLabel: {
padding: "8px 10px", // padding: "8px 10px",
fontWeight: "bold", // fontWeight: "bold",
width: "30%", // width: "30%",
verticalAlign: "top", // verticalAlign: "top",
textTransform: "capitalize", // textTransform: "capitalize",
}, // },
tableInput: { // tableInput: {
padding: "8px 10px", // padding: "8px 10px",
}, // },
input: { // input: {
padding: 6, // padding: 6,
borderRadius: 4, // borderRadius: 4,
border: "1px solid #ccc", // border: "1px solid #ccc",
width: "100%", // width: "100%",
}, // },
actions: { // actions: {
marginTop: 20, // marginTop: 20,
textAlign: "right", // textAlign: "right",
}, // },
saveButton: { // saveButton: {
marginRight: 10, // marginRight: 10,
backgroundColor: "green", // backgroundColor: "green",
color: "white", // color: "white",
border: "none", // border: "none",
padding: "8px 14px", // padding: "8px 14px",
borderRadius: 4, // borderRadius: 4,
cursor: "pointer", // cursor: "pointer",
}, // },
deleteButton: { // deleteButton: {
backgroundColor: "red", // backgroundColor: "red",
color: "white", // color: "white",
border: "none", // border: "none",
padding: "8px 14px", // padding: "8px 14px",
borderRadius: 4, // borderRadius: 4,
cursor: "pointer", // cursor: "pointer",
}, // },
}; // };
const spinnerStyle = ` // const spinnerStyle = `
@keyframes spin { // @keyframes spin {
0% { transform: rotate(0deg); } // 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } // 100% { transform: rotate(360deg); }
} // }
`; // `;
export default Modal; // export default Modal;

View File

@@ -179,12 +179,76 @@ const CameraCanvas = () => {
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);
const [documentTypes, setDocumentTypes] = useState([]);
const [loadingDocumentTypes, setLoadingDocumentTypes] = useState(true);
const [isEditMode, setIsEditMode] = useState(false); // New state for edit mode
// NEW STATES - Added from code 2 // NEW STATES - Added from code 2
const [isScanned, setIsScanned] = useState(false); const [isScanned, setIsScanned] = useState(false);
const [showSuccessMessage, setShowSuccessMessage] = useState(false); const [showSuccessMessage, setShowSuccessMessage] = useState(false);
const [modalOpen, setModalOpen] = useState(false); // Added from code 2 const [modalOpen, setModalOpen] = useState(false); // Added from code 2
const handleDeleteDocumentType = async (id, documentType) => {
if (window.confirm(`Apakah Anda yakin ingin menghapus dokumen tipe "${documentType}"?`)) {
try {
const token = localStorage.getItem("token");
const response = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/delete-document-type", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ id, document_type: documentType }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log("Delete response:", result);
// Check for 'success' property from the server response
if (result.success) {
setDocumentTypes(prevTypes => prevTypes.filter(doc => doc.id !== id));
alert(`Dokumen tipe "${documentType}" berhasil dihapus.`);
} else {
// Log the full result if success is false to help debug why it's failing
console.error(`Server reported failure for deleting document type "${documentType}":`, result);
alert(`Gagal menghapus dokumen tipe "${documentType}": ${result.message || "Respon tidak menunjukkan keberhasilan."}`);
}
} catch (error) {
console.error("Error deleting document type:", error);
alert(`Terjadi kesalahan saat menghapus dokumen tipe "${documentType}". Detail: ${error.message}`);
} finally {
// Ensure edit mode is exited after a delete attempt
setIsEditMode(false);
}
}
};
useEffect(() => {
const fetchDocumentTypes = async () => {
try {
setLoadingDocumentTypes(true);
const response = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/show");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const activeDocumentTypes = data.filter(doc => doc.document_type !== "INACTIVE");
setDocumentTypes(activeDocumentTypes);
} catch (error) {
console.error("Error fetching document types:", error);
// Optionally handle error display to user
} finally {
setLoadingDocumentTypes(false);
}
};
fetchDocumentTypes();
}, []);
const handleDocumentTypeSelection = (type) => { const handleDocumentTypeSelection = (type) => {
if (type === "new") { if (type === "new") {
setShowNewDocumentModal(true); setShowNewDocumentModal(true);
@@ -205,9 +269,12 @@ const CameraCanvas = () => {
try { try {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
const response = await fetch( // Construct the prompt based on fields
"https://bot.kediritechnopark.com/webhook/solid-data/newtype", const fieldJson = fields.map(field => ` "${field.label.toLowerCase().replace(/\s+/g, '_')}": "string"`).join(",\n");
{ const promptContent = `Ekstrak data ${documentName} dan kembalikan dalam format JSON object tunggal berikut:\n\n{\n${fieldJson}\n}\n\nATURAN PENTING:\n- Kembalikan HANYA object JSON tunggal {...}, BUKAN array [{...}]\n- Gunakan format tanggal sederhana YYYY-MM-DD (jika ada field tanggal)\n- Jangan tambahkan penjelasan atau teks lain\n- Pastikan semua field diisi berdasarkan data yang terdeteksi`;
const [dataResponse, promptResponse] = await Promise.all([
fetch("https://bot.kediritechnopark.com/webhook/solid-data/newtype-data", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -217,35 +284,62 @@ const CameraCanvas = () => {
document_type: documentName, document_type: documentName,
fields: fields, fields: fields,
}), }),
}),
fetch("https://bot.kediritechnopark.com/webhook/solid-data/newtype-prompt", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
document_type: documentName,
prompt: promptContent,
}),
}),
]);
const dataResult = await dataResponse.json();
const promptResult = await promptResponse.json();
console.log("Server response for newtype-data:", dataResult);
console.log("Server response for newtype-prompt:", promptResult);
// Re-fetch document types to update the list, regardless of success or failure
const fetchDocumentTypes = async () => {
try {
setLoadingDocumentTypes(true);
const response = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/show");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const activeDocumentTypes = data.filter(doc => doc.document_type !== "INACTIVE");
setDocumentTypes(activeDocumentTypes);
} catch (error) {
console.error("Error re-fetching document types:", error);
} finally {
setLoadingDocumentTypes(false);
} }
); };
await fetchDocumentTypes(); // Re-fetch after creation attempt
const result = await response.json(); // Always show success notification as requested
alert(`Dokumen tipe "${documentName}" berhasil dibuat (atau percobaan pembuatan selesai).`);
if (response.ok && result.status) { // The following states and onClose should be handled by NewDocumentModal's handleSubmit
localStorage.setItem("document_id", result.document_id); // setSelectedDocumentType(
// documentName.toLowerCase().replace(/\s+/g, "_")
// );
// setShowDocumentSelection(false);
// initializeCamera();
setSelectedDocumentType( console.log("New Document Type Creation Attempt Finished:", documentName, "with fields:", fields);
result.document_type.toLowerCase().replace(/\s+/g, "_")
);
setShowDocumentSelection(false);
initializeCamera();
console.log("Document ID:", result.document_id);
console.log(
"New Document Type Created:",
result.document_type,
"with fields:",
fields
);
} else {
throw new Error(result.message || "Gagal membuat document type");
}
} catch (error) { } catch (error) {
// Log the error for debugging, but still show a "success" message to the user as requested
console.error("Error submitting new document type:", error); console.error("Error submitting new document type:", error);
console.log("Gagal membuat dokumen. Coba lagi."); alert(`Dokumen tipe "${documentName}" berhasil dibuat (atau percobaan pembuatan selesai).`); // Still show success as requested
} }
// Removed the finally block from here, as state resets and onClose belong to NewDocumentModal
}; };
const rectRef = useRef({ const rectRef = useRef({
@@ -694,26 +788,23 @@ const CameraCanvas = () => {
}; };
const getDocumentDisplayInfo = (docType) => { const getDocumentDisplayInfo = (docType) => {
const foundDoc = documentTypes.find(doc => doc.document_type === docType);
if (foundDoc) {
return {
icon: "📄", // Generic icon for fetched types, or could be dynamic if provided by API
name: foundDoc.document_type.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
fullName: foundDoc.document_type.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
};
}
switch (docType) { switch (docType) {
case "ktp":
return { icon: "🆔", name: "KTP", fullName: "Kartu Tanda Penduduk" };
case "kk":
return { icon: "👨‍👩‍👧‍👦", name: "KK", fullName: "Kartu Keluarga" };
case "akta_kelahiran":
return {
icon: "👶",
name: "Akta Kelahiran",
fullName: "Akta Kelahiran",
};
case "new": case "new":
return { icon: "✨", name: "New Document", fullName: "Dokumen Baru" }; return { icon: "✨", name: "New Document", fullName: "Dokumen Baru" };
default: default:
return { return {
icon: "📄", icon: "📄",
name: docType, name: docType.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
fullName: docType fullName: docType.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase()),
}; };
} }
}; };
@@ -781,66 +872,67 @@ const CameraCanvas = () => {
{showDocumentSelection ? ( {showDocumentSelection ? (
<div style={styles.selectionContainer}> <div style={styles.selectionContainer}>
<div style={styles.selectionContent}> <div style={styles.selectionContent}>
<h2 style={styles.selectionTitle}>Pilih Jenis Dokumen</h2> <div style={styles.selectionHeader}> {/* New div for header */}
<h2 style={styles.selectionTitle}>Pilih Jenis Dokumen</h2>
<button
onClick={() => setIsEditMode(!isEditMode)}
style={styles.editButton}
>
{isEditMode ? "Selesai" : "Edit"}
</button>
</div>
<p style={styles.selectionSubtitle}> <p style={styles.selectionSubtitle}>
Silakan pilih jenis dokumen yang akan Anda scan Silakan pilih jenis dokumen yang akan Anda scan
</p> </p>
<div style={styles.documentGrid}> <div style={styles.documentGrid}>
<button {loadingDocumentTypes ? (
onClick={() => handleDocumentTypeSelection("new")} <div style={styles.spinnerContainer}>
style={styles.documentCard} <div style={styles.spinner} />
> <style>{spinnerStyle}</style>
<div style={styles.documentIconContainer}>
<div style={styles.plusIcon}>+</div>
</div> </div>
<div style={styles.documentLabel}>new</div> ) : (
</button> <>
<button
<button onClick={() => handleDocumentTypeSelection("new")}
onClick={() => handleDocumentTypeSelection("ktp")} style={styles.documentCard}
style={styles.documentCard} >
> <div style={styles.documentIconContainer}>
<div <div style={styles.plusIcon}>+</div>
style={{ </div>
...styles.documentIconContainer, <div style={styles.documentLabel}>new</div>
backgroundColor: "#f0f0f0", </button>
}} {documentTypes.map((doc) => {
> const displayInfo = getDocumentDisplayInfo(doc.document_type);
<div style={styles.documentIcon}>🆔</div> return (
</div> <div key={doc.id} style={styles.documentCardWrapper}> {/* Wrapper for card and delete icon */}
<div style={styles.documentLabel}>ktp</div> <button
</button> onClick={() => handleDocumentTypeSelection(doc.document_type)}
style={styles.documentCard}
<button >
onClick={() => handleDocumentTypeSelection("akta_kelahiran")} <div
style={styles.documentCard} style={{
> ...styles.documentIconContainer,
<div backgroundColor: "#f0f0f0",
style={{ }}
...styles.documentIconContainer, >
backgroundColor: "#f0f0f0", <div style={styles.documentIcon}>{displayInfo.icon}</div>
}} </div>
> <div style={styles.documentLabel}>{displayInfo.name}</div>
<div style={styles.documentIcon}>👶</div> </button>
</div> {isEditMode && (
<div style={styles.documentLabel}>akta</div> <button
</button> style={styles.deleteIcon}
onClick={() => handleDeleteDocumentType(doc.id, doc.document_type)}
<button >
onClick={() => handleDocumentTypeSelection("kk")}
style={styles.documentCard} </button>
> )}
<div </div>
style={{ );
...styles.documentIconContainer, })}
backgroundColor: "#f0f0f0", </>
}} )}
>
<div style={styles.documentIcon}>👨👩👧👦</div>
</div>
<div style={styles.documentLabel}>kk</div>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1338,6 +1430,47 @@ const styles = {
fontSize: "14px", fontSize: "14px",
color: "#495057", color: "#495057",
}, },
selectionHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "10px",
},
editButton: {
backgroundColor: "#007bff",
color: "white",
padding: "8px 15px",
borderRadius: "8px",
border: "none",
fontSize: "14px",
fontWeight: "bold",
cursor: "pointer",
transition: "background-color 0.2s",
},
documentCardWrapper: {
position: "relative",
display: "flex",
flexDirection: "column",
alignItems: "center",
},
deleteIcon: {
position: "absolute",
top: "-10px",
right: "-10px",
backgroundColor: "#dc3545",
color: "white",
borderRadius: "50%",
width: "28px",
height: "28px",
fontSize: "20px",
display: "flex",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
border: "2px solid white",
boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
zIndex: 10,
},
}; };
export default CameraCanvas; export default CameraCanvas;