add handler pdf read by page

This commit is contained in:
dmsanhrProject 2025-11-04 14:24:45 +07:00
parent 0c23043b4b
commit e9ab559127
9 changed files with 525 additions and 33 deletions

198
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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) => (

View File

@ -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])
} }

View 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>
);
}

View File

@ -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,

View File

@ -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, {

View File

@ -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 */}
<button <div className="">
onClick={handleUpload} <p className="text-gray-800 text-sm font-medium flex items-center gap-2">
disabled={loading} 📎
className="mt-3 px-5 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-400" <span
> className={`${
{loading ? "Mengunggah..." : "Upload"} file.name.endsWith('.pdf')
</button> ? '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
onClick={handleUpload}
disabled={loading || (result && result.file_type === ".pdf" && result.tables?.length > 1) || (ext === "pdf" && pdfPageCount > 3 && selectedPages === "")}
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"}
</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>

View File

@ -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) => {