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 [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(() => {
const fetchFiles = async () => {
const token = localStorage.getItem("token");
@@ -47,7 +110,7 @@ const FileListComponent = ({
const today = new Date().toISOString().slice(0, 10);
const totalToday = fileData.filter((f) =>
f.created_at.startsWith(today)
f.created_at && f.created_at.startsWith(today)
).length;
setTotalFilesSentToday(totalToday);
@@ -55,6 +118,7 @@ const FileListComponent = ({
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
const totalThisMonth = fileData.filter((f) => {
if (!f.created_at) return false;
const d = new Date(f.created_at);
return (
d.getMonth() === currentMonth && d.getFullYear() === currentYear
@@ -64,7 +128,10 @@ const FileListComponent = ({
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) {
const minDate = new Date(Math.min(...dateObjects));
const maxDate = new Date(Math.max(...dateObjects));
@@ -81,14 +148,16 @@ const FileListComponent = ({
current.setMonth(current.getMonth() + 1);
}
fileData.forEach((item) => {
const d = new Date(item.created_at);
const monthKey = `${d.getFullYear()}-${String(
d.getMonth() + 1
).padStart(2, "0")}`;
if (monthlyDataMap[monthKey] !== undefined)
monthlyDataMap[monthKey]++;
});
fileData
.filter(item => item.created_at)
.forEach((item) => {
const d = new Date(item.created_at);
const monthKey = `${d.getFullYear()}-${String(
d.getMonth() + 1
).padStart(2, "0")}`;
if (monthlyDataMap[monthKey] !== undefined)
monthlyDataMap[monthKey]++;
});
const performanceArray = Object.entries(monthlyDataMap).map(
([month, count]) => {
@@ -133,7 +202,7 @@ const FileListComponent = ({
try {
const response = await fetch(
`https://bot.kediritechnopark.com/webhook/solid-data/merged?nama_lengkap=${encodeURIComponent(
file.nama_lengkap
file.nama_lengkap || ''
)}`,
{
method: "GET",
@@ -155,6 +224,9 @@ const FileListComponent = ({
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]);
} catch (error) {
console.error("Gagal mengambil detail:", error.message);
@@ -177,38 +249,21 @@ const FileListComponent = ({
phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3");
const exportToExcel = (data) => {
const modifiedData = data.map((item) => ({
ID: item.id,
Petugas_ID: item.petugas_id,
Petugas: item.username,
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 processedData = processDataForExcel(data);
if (processedData.length === 0) {
alert("Tidak ada data untuk diekspor.");
return;
}
const worksheet = XLSX.utils.json_to_sheet(modifiedData);
const worksheet = XLSX.utils.json_to_sheet(processedData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
XLSX.writeFile(workbook, "data-export.xlsx");
};
// Get unique document types for dropdown
const documentTypes = getUniqueDocumentTypes(files);
return (
<div className={styles.fileListSection}>
<div className={styles.fileListHeader}>
@@ -220,9 +275,9 @@ const FileListComponent = ({
className={styles.fileCount}
>
<option value="">Semua</option>
<option value="ktp">KTP</option>
<option value="kk">KK</option>
<option value="akta_kelahiran">Akta Kelahiran</option>
{documentTypes.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
<button
onClick={() => {
@@ -250,14 +305,14 @@ const FileListComponent = ({
<div className={styles.emptyState}>
<div className={styles.emptyStateTitle}>Belum ada data</div>
<p className={styles.emptyStateText}>
Tidak ada data KK yang tersedia saat ini.
Tidak ada data yang tersedia saat ini.
</p>
</div>
) : (
<table className={styles.fileTable}>
<thead>
<tr>
<th>ID</th>
<th>No</th>
<th>NIK</th>
<th>Jenis</th>
<th className={styles.nameColumn}>Nama Lengkap</th>
@@ -266,33 +321,32 @@ const FileListComponent = ({
<tbody>
{filteredFiles.map((file, index) => (
<tr
key={file.id}
key={file.id || index}
onClick={() => handleRowClick(file)}
className={styles.tableRow}
>
<td>{index + 1}</td>
<td>{file.nik}</td>
<td>{file.document_type}</td>
<td className={styles.nameColumn}>{file.nama_lengkap}</td>
<td>{file.nik || '-'}</td>
<td>{file.document_type || '-'}</td>
<td className={styles.nameColumn}>{file.nama_lengkap || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Modal dan komponen lainnya tetap seperti sebelumnya */}
{/* Modal */}
{selectedFile && (
<div className={styles.modalOverlay} onClick={closeModal}>
{" "}
<div
className={styles.modalContent}
onClick={(e) => e.stopPropagation()}
>
{" "}
{selectedFile.data && (
<img
src={getImageSrc(selectedFile.data)}
alt={`Foto KTP - ${selectedFile.nik}`}
alt={`Foto Document - ${selectedFile.nik || 'Unknown'}`}
style={{
width: "100%",
maxHeight: "300px",
@@ -302,7 +356,7 @@ const FileListComponent = ({
boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
}}
/>
)}{" "}
)}
<h3>🪪 Detail Data Document</h3>
<div style={{ marginBottom: "1rem" }}>
<PDFDownloadLink
@@ -319,7 +373,7 @@ const FileListComponent = ({
}}
/>
}
fileName={`KTP_${selectedFile.nik}.pdf`}
fileName={`Document_${selectedFile.nik || selectedFile.id || 'unknown'}.pdf`}
style={{
textDecoration: "none",
padding: "8px 16px",
@@ -336,46 +390,97 @@ const FileListComponent = ({
</div>
<table className={styles.detailTable}>
<tbody>
{[
["NIK", selectedFile.nik],
["No.Al", selectedFile.no_al],
["Nomor Akta Kelahiran", selectedFile.akta_kelahiran_nomor],
["Nama Lengkap", selectedFile.nama_lengkap],
["Anak Ke", selectedFile.anak_ke],
["Tempat Lahir", selectedFile.tempat_lahir],
["Tanggal Lahir", selectedFile.tanggal_lahir],
["Jenis Kelamin", selectedFile.jenis_kelamin],
["Alamat", selectedFile.alamat],
["Ayah", selectedFile.ayah],
["ibu", selectedFile.ibu],
["RT", selectedFile.rt],
["RW", selectedFile.rw],
["Kelurahan/Desa", selectedFile.kel_desa],
["Kecamatan", selectedFile.kecamatan],
["Agama", selectedFile.agama],
["Status Perkawinan", selectedFile.status_perkawinan],
["Pekerjaan", selectedFile.pekerjaan],
["Kewarganegaraan", selectedFile.kewarganegaraan],
["No HP", selectedFile.no_hp],
["Email", selectedFile.email],
["Berlaku Hingga", selectedFile.berlaku_hingga],
["Tanggal Pembuatan", selectedFile.pembuatan],
["Kota Pembuatan", selectedFile.kota_pembuatan],
]
.filter(([_, value]) => value !== null && value !== "")
.map(([label, value]) => (
<tr key={label}>
<td>{label}</td>
<td>{value}</td>
</tr>
))}
{selectedFile && (console.log("selectedFile in modal:", selectedFile), true) &&
Object.entries(selectedFile)
.map(([key, value]) => {
console.log(`Processing: ${key} = ${value} (type: ${typeof value})`);
return [key, value];
})
.filter(([key, value]) => {
console.log(`Filtering: ${key} = ${value}`);
// Exclude specific keys that are not part of the display data
const excludedKeys = [
"id",
"document_type",
"created_at",
"data", // Exclude image data
"foto_url", // Exclude image URL
];
if (excludedKeys.includes(key)) {
console.log(`Excluded key: ${key}`);
return false;
}
if (value === null) {
console.log(`Null value for key: ${key}`);
return false;
}
if (value === undefined) {
console.log(`Undefined value for key: ${key}`);
return false;
}
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>
</table>
<button className={styles.closeButton} onClick={closeModal}>
{" "}
Tutup{" "}
</button>{" "}
</div>{" "}
Tutup
</button>
</div>
</div>
)}
</div>

View File

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

View File

@@ -179,12 +179,76 @@ const CameraCanvas = () => {
const [selectedDocumentType, setSelectedDocumentType] = useState(null);
const [cameraInitialized, setCameraInitialized] = 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
const [isScanned, setIsScanned] = useState(false);
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
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) => {
if (type === "new") {
setShowNewDocumentModal(true);
@@ -205,9 +269,12 @@ const CameraCanvas = () => {
try {
const token = localStorage.getItem("token");
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/solid-data/newtype",
{
// Construct the prompt based on fields
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",
headers: {
"Content-Type": "application/json",
@@ -217,35 +284,62 @@ const CameraCanvas = () => {
document_type: documentName,
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) {
localStorage.setItem("document_id", result.document_id);
// The following states and onClose should be handled by NewDocumentModal's handleSubmit
// setSelectedDocumentType(
// documentName.toLowerCase().replace(/\s+/g, "_")
// );
// setShowDocumentSelection(false);
// initializeCamera();
setSelectedDocumentType(
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");
}
console.log("New Document Type Creation Attempt Finished:", documentName, "with fields:", fields);
} 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.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({
@@ -694,26 +788,23 @@ const CameraCanvas = () => {
};
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) {
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":
return { icon: "✨", name: "New Document", fullName: "Dokumen Baru" };
default:
return {
icon: "📄",
name: docType,
fullName: docType
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase()),
name: docType.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
fullName: docType.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
};
}
};
@@ -781,66 +872,67 @@ const CameraCanvas = () => {
{showDocumentSelection ? (
<div style={styles.selectionContainer}>
<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}>
Silakan pilih jenis dokumen yang akan Anda scan
</p>
<div style={styles.documentGrid}>
<button
onClick={() => handleDocumentTypeSelection("new")}
style={styles.documentCard}
>
<div style={styles.documentIconContainer}>
<div style={styles.plusIcon}>+</div>
{loadingDocumentTypes ? (
<div style={styles.spinnerContainer}>
<div style={styles.spinner} />
<style>{spinnerStyle}</style>
</div>
<div style={styles.documentLabel}>new</div>
</button>
<button
onClick={() => handleDocumentTypeSelection("ktp")}
style={styles.documentCard}
>
<div
style={{
...styles.documentIconContainer,
backgroundColor: "#f0f0f0",
}}
>
<div style={styles.documentIcon}>🆔</div>
</div>
<div style={styles.documentLabel}>ktp</div>
</button>
<button
onClick={() => handleDocumentTypeSelection("akta_kelahiran")}
style={styles.documentCard}
>
<div
style={{
...styles.documentIconContainer,
backgroundColor: "#f0f0f0",
}}
>
<div style={styles.documentIcon}>👶</div>
</div>
<div style={styles.documentLabel}>akta</div>
</button>
<button
onClick={() => handleDocumentTypeSelection("kk")}
style={styles.documentCard}
>
<div
style={{
...styles.documentIconContainer,
backgroundColor: "#f0f0f0",
}}
>
<div style={styles.documentIcon}>👨👩👧👦</div>
</div>
<div style={styles.documentLabel}>kk</div>
</button>
) : (
<>
<button
onClick={() => handleDocumentTypeSelection("new")}
style={styles.documentCard}
>
<div style={styles.documentIconContainer}>
<div style={styles.plusIcon}>+</div>
</div>
<div style={styles.documentLabel}>new</div>
</button>
{documentTypes.map((doc) => {
const displayInfo = getDocumentDisplayInfo(doc.document_type);
return (
<div key={doc.id} style={styles.documentCardWrapper}> {/* Wrapper for card and delete icon */}
<button
onClick={() => handleDocumentTypeSelection(doc.document_type)}
style={styles.documentCard}
>
<div
style={{
...styles.documentIconContainer,
backgroundColor: "#f0f0f0",
}}
>
<div style={styles.documentIcon}>{displayInfo.icon}</div>
</div>
<div style={styles.documentLabel}>{displayInfo.name}</div>
</button>
{isEditMode && (
<button
style={styles.deleteIcon}
onClick={() => handleDeleteDocumentType(doc.id, doc.document_type)}
>
</button>
)}
</div>
);
})}
</>
)}
</div>
</div>
</div>
@@ -1338,6 +1430,47 @@ const styles = {
fontSize: "14px",
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;