add handler pdf read by page
This commit is contained in:
parent
0c23043b4b
commit
e9ab559127
198
package-lock.json
generated
198
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"axios": "^1.13.0",
|
"axios": "^1.13.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"pdfjs-dist": "^5.4.394",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
|
|
@ -700,6 +701,191 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "0.1.81",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "0.1.81",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "0.1.81",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "0.1.81",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "0.1.81",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.81",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "0.1.81",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "0.1.81",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "0.1.81"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "0.1.81",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz",
|
||||||
|
"integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@reduxjs/toolkit": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
|
||||||
|
|
@ -2994,6 +3180,18 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pdfjs-dist": {
|
||||||
|
"version": "5.4.394",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.394.tgz",
|
||||||
|
"integrity": "sha512-9ariAYGqUJzx+V/1W4jHyiyCep6IZALmDzoaTLZ6VNu8q9LWi1/ukhzHgE2Xsx96AZi0mbZuK4/ttIbqSbLypg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.16.0 || >=22.3.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas": "^0.1.81"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"axios": "^1.13.0",
|
"axios": "^1.13.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"pdfjs-dist": "^5.4.394",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default function AdminNavbar() {
|
||||||
return (
|
return (
|
||||||
<nav className="sticky top-0 z-50 bg-white border-b border-gray-200 shadow-sm">
|
<nav className="sticky top-0 z-50 bg-white border-b border-gray-200 shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center">
|
<div className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center">
|
||||||
<h1 className="text-xl font-bold text-blue-600">Admin Panel</h1>
|
<h1 className="text-xl font-bold text-blue-600">Admin Data</h1>
|
||||||
|
|
||||||
<div className="flex items-center space-x-6">
|
<div className="flex items-center space-x-6">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import {useUploadController} from "../pages/admin/upload/controller_admin_upload"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Komponen Dropzone untuk unggah file.
|
* Komponen Dropzone untuk unggah file.
|
||||||
* @param {{ onFileSelect: (file: File) => void }} props
|
* @param {{ onFileSelect: (file: File) => void }} props
|
||||||
*/
|
*/
|
||||||
export default function FileDropzone({ onFileSelect }) {
|
export default function FileDropzone({ onFileSelect }) {
|
||||||
|
const {file} = useUploadController();
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
const handleDrop = (e) => {
|
const handleDrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
const file = e.dataTransfer.files[0];
|
const dataFile = e.dataTransfer.files[0];
|
||||||
if (file) onFileSelect(file);
|
if (dataFile) onFileSelect(dataFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -23,16 +25,19 @@ export default function FileDropzone({ onFileSelect }) {
|
||||||
}}
|
}}
|
||||||
onDragLeave={() => setIsDragging(false)}
|
onDragLeave={() => setIsDragging(false)}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
className={`border-2 border-dashed rounded-xl p-10 text-center transition
|
className={`border-2 rounded-xl p-10 text-center transition
|
||||||
${isDragging ? "border-blue-400 bg-blue-50" : "border-gray-300 bg-white"}`}
|
${isDragging || file ? "border-blue-400 bg-blue-50" : "border-gray-300 bg-white"}
|
||||||
|
${file ? "border-solid" : "border-dashed"}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-gray-500 mb-2">Tarik & lepas file di sini</p>
|
<p className="text-sm text-gray-500 mb-2">{file ? file.name : "Tarik & lepas file di sini"}</p>
|
||||||
<p className="text-xs text-gray-400 mb-3">atau</p>
|
<p className="text-xs text-gray-400 mb-3">atau</p>
|
||||||
<label className="cursor-pointer bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
|
<label className="cursor-pointer bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
|
||||||
Pilih File
|
{file ? "Ganti File" : "Pilih File"}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
|
accept=".zip, .pdf, .csv, application/zip, application/pdf, text/csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
e.target.files?.[0] && onFileSelect(e.target.files[0])
|
e.target.files?.[0] && onFileSelect(e.target.files[0])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
137
src/components/PdfPageSelector.jsx
Normal file
137
src/components/PdfPageSelector.jsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function PdfPageSelector({ totalPages = 0, onChange }) {
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// const handleInput = (e) => {
|
||||||
|
// const value = e.target.value.replace(/\s+/g, "");
|
||||||
|
// setInput(value);
|
||||||
|
|
||||||
|
// // Validasi sederhana
|
||||||
|
// if (!/^[0-9,\-]*$/.test(value)) {
|
||||||
|
// setError("Gunakan hanya angka, koma (,), atau tanda minus (-).");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Validasi jumlah halaman
|
||||||
|
// const pages = value.split(",").filter(Boolean);
|
||||||
|
// if (pages.length > 3) {
|
||||||
|
// setError("Maksimal 3 halaman yang bisa dipilih.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setError("");
|
||||||
|
// onChange && onChange(value);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const validateInput = (e) => {
|
||||||
|
const rawValue = e.target.value;
|
||||||
|
const value = rawValue.replace(/\s+/g, ""); // hapus spasi
|
||||||
|
setInput(value);
|
||||||
|
|
||||||
|
// Hanya boleh angka, koma, dan tanda minus
|
||||||
|
if (!/^[0-9,\-]*$/.test(value)) {
|
||||||
|
// alert("Gunakan hanya angka, koma (,), atau tanda minus (-).");
|
||||||
|
setError("Gunakan hanya angka, koma (,), atau tanda minus (-).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek format belum lengkap (contoh: "1-" atau "2,3-")
|
||||||
|
const incompletePattern = /(^|,)\d+[-,]$/;
|
||||||
|
if (incompletePattern.test(value)) {
|
||||||
|
// alert("Format halaman belum lengkap. Contoh benar: 1-3 atau 1,2,3");
|
||||||
|
setError("Format halaman belum lengkap.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split berdasarkan koma
|
||||||
|
const pages = value.split(",").filter(Boolean);
|
||||||
|
|
||||||
|
let totalSelectedPages = 0;
|
||||||
|
|
||||||
|
// Validasi tiap bagian input
|
||||||
|
for (const page of pages) {
|
||||||
|
if (page.includes("-")) {
|
||||||
|
const [start, end] = page.split("-").map(Number);
|
||||||
|
|
||||||
|
// Cek format range yang valid
|
||||||
|
if (isNaN(start) || isNaN(end) || start > end) {
|
||||||
|
// alert("Format rentang tidak valid. Contoh benar: 2-5");
|
||||||
|
setError("Format rentang tidak valid. Contoh benar: 2-5");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah range melebihi totalPages
|
||||||
|
if (start < 1 || end > totalPages) {
|
||||||
|
// alert(`Halaman melebihi batas. Maksimal hanya ${totalPages} halaman.`);
|
||||||
|
setError(`Halaman melebihi batas.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan jumlah halaman dari rentang ke total
|
||||||
|
totalSelectedPages += end - start + 1;
|
||||||
|
} else {
|
||||||
|
const num = Number(page);
|
||||||
|
if (num < 1 || num > totalPages) {
|
||||||
|
// alert(`Halaman ${num} melebihi batas. Maksimal hanya ${totalPages} halaman.`);
|
||||||
|
setError(`Halaman ${num} melebihi batas.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSelectedPages += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 Validasi total halaman yang dipilih (maksimal 3)
|
||||||
|
if (totalSelectedPages > 3) {
|
||||||
|
// alert(`Tidak boleh memilih lebih dari 3 halaman (kamu memilih ${totalSelectedPages}).`);
|
||||||
|
setError(`Tidak boleh memilih lebih dari 3 halaman (kamu memilih ${totalSelectedPages}).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika semua valid
|
||||||
|
setError("");
|
||||||
|
onChange && onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (e) => {
|
||||||
|
const value = e.target.value.replace(/\s+/g, "");
|
||||||
|
setInput(value);
|
||||||
|
const isValid = validateInput(e);
|
||||||
|
if (onChange) onChange(value, isValid);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Pilih Halaman PDF <span className="text-gray-400 text-xs">(maks. 3)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={handleInput}
|
||||||
|
placeholder="cth: 2 atau 3-5 atau 1,4,7"
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm"
|
||||||
|
/>
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
onClick={() => alert(`Halaman terpilih: ${input || "(kosong)"}`)}
|
||||||
|
>
|
||||||
|
Pilih
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-sm text-red-600 mt-2">{error}</p>}
|
||||||
|
|
||||||
|
{!error && input && (
|
||||||
|
<p className="text-xs text-gray-500 mt-2">
|
||||||
|
📄 Halaman terpilih: <span className="font-medium">{input}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,60 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { setFile, setResult, setValidatedData, reset } from "../../../store/slices/uploadSlice";
|
import { setFile, setResult, setValidatedData } from "../../../store/slices/uploadSlice";
|
||||||
import { uploadFile, uploadPdf, saveToDatabase } from "./service_admin_upload";
|
import { uploadFile, uploadPdf, saveToDatabase } from "./service_admin_upload";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import * as pdfjsLib from "pdfjs-dist";
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
"pdfjs-dist/build/pdf.worker.mjs",
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
// pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||||
|
|
||||||
export function useUploadController() {
|
export function useUploadController() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { file, result } = useSelector((state) => state.upload);
|
const { file, result } = useSelector((state) => state.upload);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedTable, setSelectedTable] = useState(null);
|
const [selectedTable, setSelectedTable] = useState(null);
|
||||||
|
const [selectedPages, setSelectedPages] = useState("");
|
||||||
const [tableTitle, setTableTitle] = useState("");
|
const [tableTitle, setTableTitle] = useState("");
|
||||||
|
const [pdfPageCount, setPdfPageCount] = useState(null); // 👈 halaman pdf
|
||||||
|
|
||||||
const handleFileSelect = (f) => {
|
// 🔹 handle drop file
|
||||||
|
const handleFileSelect = async (f) => {
|
||||||
dispatch(setFile(f));
|
dispatch(setFile(f));
|
||||||
|
const ext = f.name.split(".").pop().toLowerCase();
|
||||||
|
|
||||||
|
if (ext === "pdf") {
|
||||||
|
try {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const typedArray = new Uint8Array(e.target.result);
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
|
||||||
|
setPdfPageCount(pdf.numPages);
|
||||||
|
console.log(`📄 PDF terdeteksi dengan ${pdf.numPages} halaman`);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(f);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal membaca PDF:", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setPdfPageCount(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔹 upload file
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await uploadFile(file);
|
const res = await uploadFile(file, selectedPages);
|
||||||
dispatch(setResult(res));
|
dispatch(setResult(res));
|
||||||
if (res.file_type !== ".pdf" || (res.file_type === ".pdf" && res.tables.length === 1)) {
|
console.log('gtw',res);
|
||||||
|
|
||||||
|
if (res.file_type !== ".pdf" || (res.file_type === ".pdf" && !res.tables)) {
|
||||||
navigate("/admin/upload/validate");
|
navigate("/admin/upload/validate");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -62,9 +94,12 @@ export function useUploadController() {
|
||||||
loading,
|
loading,
|
||||||
file,
|
file,
|
||||||
result,
|
result,
|
||||||
selectedTable,
|
|
||||||
setSelectedTable,
|
|
||||||
tableTitle,
|
tableTitle,
|
||||||
|
selectedTable,
|
||||||
|
selectedPages,
|
||||||
|
pdfPageCount, // 👈 halaman pdf
|
||||||
|
setSelectedTable,
|
||||||
|
setSelectedPages,
|
||||||
setTableTitle,
|
setTableTitle,
|
||||||
handleFileSelect,
|
handleFileSelect,
|
||||||
handleUpload,
|
handleUpload,
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,10 @@
|
||||||
|
|
||||||
import api from "../../../services/api";
|
import api from "../../../services/api";
|
||||||
|
|
||||||
export async function uploadFile(file) {
|
export async function uploadFile(file, page = null) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
formData.append("page", page);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post("/upload", formData, {
|
const response = await api.post("/upload", formData, {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useUploadController } from "./controller_admin_upload";
|
import { useUploadController } from "./controller_admin_upload";
|
||||||
import FileDropzone from "../../../components/FileDropzone";
|
import FileDropzone from "../../../components/FileDropzone";
|
||||||
|
import PdfPageSelector from "../../../components/PdfPageSelector";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export default function ViewsAdminUploadStep1() {
|
export default function ViewsAdminUploadStep1() {
|
||||||
|
|
@ -7,13 +8,23 @@ export default function ViewsAdminUploadStep1() {
|
||||||
loading,
|
loading,
|
||||||
file,
|
file,
|
||||||
result,
|
result,
|
||||||
|
pdfPageCount,
|
||||||
|
selectedPages,
|
||||||
selectedTable,
|
selectedTable,
|
||||||
|
setSelectedPages,
|
||||||
setSelectedTable,
|
setSelectedTable,
|
||||||
handleFileSelect,
|
handleFileSelect,
|
||||||
handleUpload,
|
handleUpload,
|
||||||
handleNextPdf,
|
handleNextPdf
|
||||||
} = useUploadController();
|
} = useUploadController();
|
||||||
|
|
||||||
|
const handlePageSelection = (pages) => {
|
||||||
|
console.log("Halaman PDF yang dipilih:", pages);
|
||||||
|
setSelectedPages(pages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ext = file ? file.name.split(".").pop().toLowerCase() : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-10">
|
<div className="max-w-4xl mx-auto py-10">
|
||||||
<div className="mb-6 flex justify-between items-center">
|
<div className="mb-6 flex justify-between items-center">
|
||||||
|
|
@ -25,44 +36,147 @@ export default function ViewsAdminUploadStep1() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dropzone */}
|
||||||
<FileDropzone onFileSelect={handleFileSelect} />
|
<FileDropzone onFileSelect={handleFileSelect} />
|
||||||
|
{/* {!file && (
|
||||||
|
)} */}
|
||||||
|
|
||||||
{file && (
|
{file && (
|
||||||
<div className="mt-3">
|
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
|
||||||
<p className="text-gray-700 text-sm">File terpilih: {file.name}</p>
|
{/* Info File */}
|
||||||
|
<div className="">
|
||||||
|
<p className="text-gray-800 text-sm font-medium flex items-center gap-2">
|
||||||
|
📎
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
file.name.endsWith('.pdf')
|
||||||
|
? 'text-red-500'
|
||||||
|
: file.name.endsWith('.csv')
|
||||||
|
? 'text-green-500'
|
||||||
|
: file.name.endsWith('.zip')
|
||||||
|
? 'text-yellow-500'
|
||||||
|
: 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{ext === "pdf" && pdfPageCount && (
|
||||||
|
<p className="text-gray-500 text-xs mt-1">
|
||||||
|
File PDF <span className="font-semibold">{pdfPageCount}</span> halaman.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selector Halaman (hanya untuk PDF dengan > 1 halaman) */}
|
||||||
|
{ext === "pdf" && pdfPageCount > 1 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<PdfPageSelector totalPages={pdfPageCount} onChange={handlePageSelection} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tombol Upload */}
|
||||||
|
<div className={`mt-6 flex justify-center ${(result && result.file_type === ".pdf" && result.tables?.length > 1) ? 'hidden' : 'block' }`}>
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={loading}
|
disabled={loading || (result && result.file_type === ".pdf" && result.tables?.length > 1) || (ext === "pdf" && pdfPageCount > 3 && selectedPages === "")}
|
||||||
className="mt-3 px-5 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-400"
|
className="w-full px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
|
||||||
>
|
>
|
||||||
{loading ? "Mengunggah..." : "Upload"}
|
{loading ? "Mengunggah..." : "Upload"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{result && result.file_type === ".pdf" && result.tables?.length > 1 && (
|
{result && result.file_type === ".pdf" && result.tables?.length > 1 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
|
||||||
<h2 className="text-lg font-semibold mb-2 text-gray-700">Hasil Analisis Backend</h2>
|
<h2 className="text-lg font-semibold mb-2 text-gray-700">Hasil Analisis Backend</h2>
|
||||||
<ul className="border rounded-lg divide-y overflow-hidden">
|
<ul className="space-y-3 mt-4">
|
||||||
{result.tables.map((t, i) => (
|
{result.tables.map((t, i) => (
|
||||||
<li
|
<li
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setSelectedTable(t)}
|
onClick={() => setSelectedTable(t)}
|
||||||
className={`flex items-center gap-2 p-3 cursor-pointer hover:bg-blue-50 transition ${
|
className={`group relative border border-gray-200 rounded-lg cursor-pointer overflow-hidden transition-all duration-200
|
||||||
selectedTable?.title === t.title ? "bg-blue-100 font-semibold" : ""
|
hover:shadow-sm hover:border-blue-300 ${
|
||||||
|
selectedTable?.title === t.title ? "bg-blue-50 border-blue-400" : "bg-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className={`text-green-600 ${selectedTable?.title === t.title ? "" : "opacity-0"}`}>✅</span>
|
{/* Header nama tabel */}
|
||||||
<span>{t.title}</span>
|
<div className="flex justify-between items-center px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${
|
||||||
|
selectedTable?.title === t.title ? "text-blue-700" : "text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📄 Tabel {t.title}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`text-green-600 text-lg transition-opacity ${
|
||||||
|
selectedTable?.title === t.title ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
✅
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mini preview kolom */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full text-xs">
|
||||||
|
<thead className="bg-gray-100 text-gray-600">
|
||||||
|
<tr>
|
||||||
|
{t.columns?.map((col, idx) => (
|
||||||
|
<th
|
||||||
|
key={idx}
|
||||||
|
className="px-3 py-2 text-left font-medium whitespace-nowrap border-r last:border-none border-gray-200"
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highlight bar animasi */}
|
||||||
|
<div
|
||||||
|
className={`absolute bottom-0 left-0 h-1 bg-blue-500 transition-all duration-300 ${
|
||||||
|
selectedTable?.title === t.title ? "w-full opacity-100" : "w-0 opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleNextPdf}
|
onClick={handleNextPdf}
|
||||||
disabled={!selectedTable}
|
disabled={!selectedTable}
|
||||||
className="mt-4 px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-300"
|
className="w-full mt-4 px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-300"
|
||||||
>
|
>
|
||||||
Lanjut ke Validasi →
|
Lanjut ke Validasi →
|
||||||
</button>
|
</button>
|
||||||
|
<p className="text-xs text-gray-500 mt-1 ml-1 py-0">
|
||||||
|
<i>*Pilih tabel yang akan di proses ke database</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && result.file_type === ".pdf" && result.tables?.length == 0 && (
|
||||||
|
<div className="mt-6 flex items-start gap-3 border-l-4 border-yellow-500 bg-yellow-50 p-4 rounded-md shadow-sm">
|
||||||
|
<div className="text-yellow-500 mt-0.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m0 3.75h.007v.007H12v-.007zM21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-yellow-800">Tidak Ditemukan Tabel Valid</h3>
|
||||||
|
<p className="text-yellow-700 text-sm">
|
||||||
|
Pastikan file pdf berisi tabel yang valid dan bukan hasil scan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import axios from "axios";
|
||||||
import { getToken } from "../utils/auth";
|
import { getToken } from "../utils/auth";
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: "http://labai.polinema.ac.id:808",
|
// baseURL: "http://labai.polinema.ac.id:808",
|
||||||
|
baseURL: "http://localhost:8000",
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user