diff --git a/package-lock.json b/package-lock.json
index b480f8a..b1930fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,8 @@
"react-dom": "^19.1.1",
"react-router-dom": "^7.7.1",
"react-scripts": "5.0.1",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "xlsx": "^0.18.5"
},
"devDependencies": {
"autoprefixer": "^10.4.21",
@@ -4096,6 +4097,15 @@
"node": ">=8.9"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -5107,6 +5117,19 @@
"node": ">=4"
}
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5311,6 +5334,15 @@
"node": ">=4"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
@@ -5522,6 +5554,18 @@
"node": ">=10"
}
},
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7732,6 +7776,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -14028,6 +14081,18 @@
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@@ -15811,6 +15876,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -16185,6 +16268,27 @@
}
}
},
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
diff --git a/package.json b/package.json
index 225d615..0065145 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"react-dom": "^19.1.1",
"react-router-dom": "^7.7.1",
"react-scripts": "5.0.1",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "xlsx": "^0.18.5"
},
"scripts": {
"start": "react-scripts start",
diff --git a/src/components/EntriesTable.js b/src/components/EntriesTable.js
new file mode 100644
index 0000000..c06319b
--- /dev/null
+++ b/src/components/EntriesTable.js
@@ -0,0 +1,54 @@
+import React, { useEffect, useState } from 'react';
+import { fetchEntries } from '../api';
+
+export default function EntriesTable({ dataTypeId }) {
+ const [entries, setEntries] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadEntries = async () => {
+ try {
+ const result = await fetchEntries(dataTypeId);
+ setEntries(result);
+ } catch (error) {
+ console.error("Error fetching entries:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadEntries();
+ }, [dataTypeId]);
+
+ if (loading) return
Loading...
;
+ if (!entries.length) return No entries found.
;
+
+ // Ambil header kolom dari key properti data entry pertama
+ const headers = Object.keys(entries[0].data || {});
+
+ return (
+
+
+
+
+ {headers.map((header) => (
+ |
+ {header}
+ |
+ ))}
+
+
+
+ {entries.map((entry) => (
+
+ {headers.map((header) => (
+ |
+ {entry.data[header]}
+ |
+ ))}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/InputDataPage.js b/src/pages/InputDataPage.js
index 653b21a..befbcc2 100644
--- a/src/pages/InputDataPage.js
+++ b/src/pages/InputDataPage.js
@@ -1,13 +1,14 @@
-import { useState, useRef } from 'react';
+import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useParams, Link, useLocation } from 'react-router-dom';
+import { fetchEntries } from '../api';
-// Ikon (tidak ada perubahan)
+// Icons (no changes)
const BackIcon = () => ();
const UploadIcon = () => ();
-const CameraIcon = () => ();
+const CameraIcon = () => ();
const ImageIcon = () => ();
-const TrashIcon = () => ();
-
+const TrashIcon = () => ();
+const CheckCircleIcon = () => ();
export default function InputDataPage() {
const location = useLocation();
@@ -19,113 +20,286 @@ export default function InputDataPage() {
const fileInputRef = useRef(null);
const cameraInputRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
- console.log(expectation)
- // --- PERUBAHAN: State baru untuk progress upload ---
const [uploadProgress, setUploadProgress] = useState(0);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [uploadResults, setUploadResults] = useState([]);
+ const [entries, setEntries] = useState([]);
+ const [loadingEntries, setLoadingEntries] = useState(false);
+ const [resultIndex, setResultIndex] = useState(0);
+ const [isSavingAll, setIsSavingAll] = useState(false);
+
+ // State baru untuk paginasi field
+ const [fieldPage, setFieldPage] = useState(0);
+
+ const loadEntries = useCallback(async () => {
+ if (!data_type_id) return;
+ setLoadingEntries(true);
+ try {
+ const data = await fetchEntries(data_type_id);
+ setEntries(data);
+ } catch (error) {
+ console.error("Error fetch entries:", error);
+ } finally {
+ setLoadingEntries(false);
+ }
+ }, [data_type_id]);
+
+ useEffect(() => {
+ loadEntries();
+ }, [loadEntries]);
+
+ // Reset field page saat dokumen utama berganti
+ useEffect(() => {
+ setFieldPage(0);
+ }, [resultIndex]);
const handleFiles = (newFiles) => {
const imageFiles = Array.from(newFiles).filter(file => file.type.startsWith('image/'));
setFilesToUpload(prevFiles => {
- const uniqueNewFiles = imageFiles.filter(newFile =>
+ const uniqueNewFiles = imageFiles.filter(newFile =>
!prevFiles.some(existingFile => existingFile.name === newFile.name && existingFile.size === newFile.size)
);
return [...prevFiles, ...uniqueNewFiles];
});
};
-
+
const removeFile = (index) => {
setFilesToUpload(prevFiles => prevFiles.filter((_, i) => i !== index));
};
-
- // --- PERUBAHAN: Fungsi handleUpload diubah total ---
+
const handleUpload = async () => {
if (filesToUpload.length === 0) return;
-
setIsUploading(true);
setUploadProgress(0);
-
const totalFiles = filesToUpload.length;
- const successfulUploads = [];
- const failedUploads = [];
- const allResults = []; // <-- Variabel untuk mengumpulkan semua hasil
-
+ const finalResults = [];
for (let i = 0; i < totalFiles; i++) {
const file = filesToUpload[i];
- setUploadProgress(i + 1); // Update progress sebelum upload
-
+ setUploadProgress(i + 1);
const formData = new FormData();
formData.append("image", file);
formData.append("expectation", JSON.stringify(expectation));
formData.append("data_type_id", data_type_id);
-
try {
- console.log(`Mengupload file ${i + 1}/${totalFiles}: ${file.name}`);
const response = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/scan", {
method: "POST",
body: formData,
});
-
- if (!response.ok) {
- // Lemparkan error agar ditangkap oleh catch block
- throw new Error(`Gagal mengupload ${file.name}`);
- }
-
+ if (!response.ok) throw new Error(`Gagal mengupload ${file.name}`);
const resultJson = await response.json();
- successfulUploads.push({ file: file.name, result: resultJson });
- allResults.push(resultJson); // <-- Kumpulkan hasil response di sini
- console.log(`Sukses mengupload ${file.name}:`, resultJson);
-
+ if (resultJson && resultJson.length > 0) {
+ finalResults.push({
+ file,
+ result: resultJson,
+ formData: { ...resultJson[0] },
+ deleted: false,
+ saved: false,
+ });
+ } else {
+ console.warn(`Hasil scan kosong untuk file ${file.name}`);
+ }
} catch (error) {
- failedUploads.push(file.name);
- console.error(`Error saat mengupload ${file.name}:`, error);
+ console.error(`Gagal mengupload ${file.name}:`, error);
}
}
-
+ setUploadResults(finalResults);
+ setModalVisible(true);
+ setResultIndex(0);
+ setFilesToUpload([]);
setIsUploading(false);
setUploadProgress(0);
-
- // allResults sekarang berisi array hasil response dari semua file
- console.log("Semua hasil response:", allResults);
-
- // Beri ringkasan hasil upload
- alert(`Selesai! Berhasil: ${successfulUploads.length}, Gagal: ${failedUploads.length}`);
-
- // Reset daftar file setelah semua proses selesai
- setFilesToUpload([]);
};
+ const updateField = (key, value) => {
+ const newResults = [...uploadResults];
+ if (newResults[resultIndex]) {
+ newResults[resultIndex].formData[key] = value;
+ setUploadResults(newResults);
+ }
+ };
+
+
+ // Render field berdasarkan tipe (sama seperti sebelumnya)
+ const renderField = (key, value) => {
+ let type = "text";
+ let options = [];
+ if (typeof expectation[key] === "object" && expectation[key] !== null) {
+ type = expectation[key].type;
+ options = expectation[key].options || [];
+ } else {
+ type = expectation[key] || "text";
+ }
+
+ if (type === "date") {
+ return (
+ updateField(key, e.target.value)}
+ className="mt-1 block w-full border border-gray-300 rounded-md p-1"
+ />
+ );
+ }
+ if (type === "selection") {
+ return (
+
+ );
+ }
+ if (type === "number") {
+ return (
+ updateField(key, e.target.value)}
+ className="mt-1 block w-full border border-gray-300 rounded-md p-1"
+ />
+ );
+ }
+ return (
+ updateField(key, e.target.value)}
+ className="mt-1 block w-full border border-gray-300 rounded-md p-1"
+ />
+ );
+ };
+
+ const toggleDelete = () => {
+ const newResults = [...uploadResults];
+ if (newResults[resultIndex]) {
+ newResults[resultIndex].deleted = !newResults[resultIndex].deleted;
+ setUploadResults(newResults);
+ }
+ };
+
+ const handleSaveAllValid = async () => {
+ const itemsToSave = uploadResults.filter(r => !r.deleted && !r.saved);
+ if (itemsToSave.length === 0) {
+ alert("Tidak ada data baru yang valid untuk disimpan.");
+ return;
+ }
+ setIsSavingAll(true);
+ const token = localStorage.getItem("token");
+ const orgId = JSON.parse(localStorage.getItem("selected_organization"))?.organization_id;
+ const savePromises = itemsToSave.map(item => {
+ const payload = { data: item.formData, organization_id: orgId, data_type_id };
+ return fetch("https://bot.kediritechnopark.com/webhook/solid-data/save", {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
+ body: JSON.stringify(payload),
+ }).then(res => res.ok ? { success: true, item } : { success: false, item });
+ });
+ try {
+ const results = await Promise.all(savePromises);
+ const successfulSaves = results.filter(r => r.success).map(r => r.item);
+ const failedSavesCount = results.length - successfulSaves.length;
+ if (successfulSaves.length > 0) {
+ setUploadResults(prevResults =>
+ prevResults.map(res =>
+ successfulSaves.some(s => s.file.name === res.file.name && s.file.size === res.file.size)
+ ? { ...res, saved: true }
+ : res
+ )
+ );
+ await loadEntries();
+ }
+ alert(`Selesai! Berhasil menyimpan ${successfulSaves.length} data. Gagal: ${failedSavesCount} data.`);
+ } catch (error) {
+ console.error("Terjadi error saat menyimpan semua data:", error);
+ alert("Terjadi error saat mencoba menyimpan data.");
+ } finally {
+ setIsSavingAll(false);
+ }
+ };
+
+ const goToPrevious = () => setResultIndex(prev => Math.max(0, prev - 1));
+ const goToNext = () => setResultIndex(prev => Math.min(uploadResults.length - 1, prev + 1));
+
+ const renderEntriesTable = () => {
+ if (loadingEntries) return Memuat data...
;
+ if (!entries.length || entries[0]?.success === true) {
+ return (
+
+
+
Belum ada data yang disimpan.
+
Silakan upload gambar dan simpan data terlebih dahulu.
+
+
+ );
+ }
+ const headers = Object.keys(entries[0]?.data || {});
+ return (
+
+
Data Tersimpan
+
+
+
+ {headers.map(header => | {header} | )}
+
+
+ {entries.map(entry => {headers.map(header => | {entry.data[header]} | )}
)}
+
+
+
+
+ );
+ };
+
+ const currentResult = uploadResults[resultIndex];
+
+ // Logika untuk paginasi field
+ const fieldsPerPage = 3;
+ const allFields = currentResult?.formData ? Object.entries(currentResult.formData) : [];
+ const totalFieldPages = Math.ceil(allFields.length / fieldsPerPage);
+ const visibleFields = allFields.slice(
+ fieldPage * fieldsPerPage,
+ fieldPage * fieldsPerPage + fieldsPerPage
+ );
+ const goToNextFields = () => setFieldPage(prev => Math.min(prev + 1, totalFieldPages - 1));
+ const goToPreviousFields = () => setFieldPage(prev => Math.max(0, prev - 1));
+
return (
-
-
- Input Data untuk {docType.replace('-', ' ')}
+
+
+ Input Data untuk {docType.replace('-', ' ')}
-
+
-
setIsDragging(true)}
- onDragLeave={() => setIsDragging(false)}
- onDragOver={(e) => e.preventDefault()}
- onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFiles(e.dataTransfer.files); }}
- >
-
handleFiles(e.target.files)}/>
-
handleFiles(e.target.files)}/>
+
setIsDragging(true)} onDragLeave={() => setIsDragging(false)} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFiles(e.dataTransfer.files); }}>
+
handleFiles(e.target.files)} accept="image/*" />
+
handleFiles(e.target.files)} />
Upload Dokumen
Pilih file atau ambil gambar dari kamera.
-
-
+
+
-
Atau, seret & lepas file di area ini.
+
Atau, seret & lepas file di area ini.
-
-
+
Pratinjau
{filesToUpload.length > 0 ? (
-
+
{filesToUpload.map((file, index) => (
})
@@ -136,27 +310,91 @@ export default function InputDataPage() {
))}
) : (
-
+
Pratinjau gambar akan muncul di sini.
)}
-
{filesToUpload.length > 0 && (
-
)}
+ {renderEntriesTable()}
+
+ {modalVisible && currentResult && (
+
+
+
+ Verifikasi Scan ({resultIndex + 1} / {uploadResults.length})
+ setModalVisible(false)} className="text-gray-500 hover:text-gray-800 text-2xl font-bold">×
+
+
+
+ {currentResult.deleted && (
+
+
DITANDAI UNTUK DIHAPUS
+
Batalkan
+
+ )}
+
+
+
+
+
+ {visibleFields.map(([key, value]) => (
+
+
+ {renderField(key, value)}
+
+ ))}
+
+
+ {totalFieldPages > 1 && (
+
+
+ Sebelumnya
+
+
+ {fieldPage + 1} / {totalFieldPages}
+
+ = totalFieldPages - 1} className="bg-white text-gray-700 py-2 px-4 rounded-lg border border-gray-300 disabled:opacity-50 hover:bg-gray-100 text-sm">
+ Selanjutnya
+
+
+ )}
+
+
+
+
+
+
+
+
+ )}
);
}
\ No newline at end of file
diff --git a/src/pages/browser.html b/src/pages/browser.html
deleted file mode 100644
index bd89d9e..0000000
--- a/src/pages/browser.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
Bypass Situs
-
-
-
-
🔓 Akses Situs Lain dari File Lokal
-
-
Tips: Kalau tidak bisa dibuka, coba salin link-nya lalu tempel di tab baru.
-
-