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) => ( + + ))} + + + + {entries.map((entry) => ( + + {headers.map((header) => ( + + ))} + + ))} + +
+ {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 => )} + + + {entries.map(entry => {headers.map(header => )})} + +
{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) => (
{file.name} @@ -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})

+ +
+ +
+ {currentResult.deleted && ( +
+

DITANDAI UNTUK DIHAPUS

+ +
+ )} +
+ + +
+
+ {visibleFields.map(([key, value]) => ( +
+ + {renderField(key, value)} +
+ ))} +
+ + {totalFieldPages > 1 && ( +
+ + + {fieldPage + 1} / {totalFieldPages} + + +
+ )} +
+ +
+
+ + +
+
+ )}
); } \ 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.

- -