update admin page
This commit is contained in:
parent
60d39a36e6
commit
8921e2a118
|
|
@ -9,9 +9,22 @@ import {
|
|||
DropdownMenuSeparator
|
||||
} from "../../../components/ui/dropdown-menu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ViewsAdminHome() {
|
||||
const { datasets, loading, errorMsg } = useAdminHomeController();
|
||||
const [filter, setFilter] = useState("ALL");
|
||||
const counts = {
|
||||
ALL: datasets.length,
|
||||
CLEANSING: datasets.filter(d => d.process === "CLEANSING").length,
|
||||
ERROR: datasets.filter(d => d.process === "ERROR").length,
|
||||
FINISHED: datasets.filter(d => d.process === "FINISHED").length,
|
||||
TESTING: datasets.filter(d => d.process === "TESTING").length,
|
||||
};
|
||||
const filteredData =
|
||||
filter === "ALL"
|
||||
? datasets
|
||||
: datasets.filter(d => d.process === filter);
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto py-10">
|
||||
|
|
@ -29,8 +42,83 @@ export default function ViewsAdminHome() {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mb-6">
|
||||
|
||||
{/* ALL */}
|
||||
<button
|
||||
onClick={() => setFilter("ALL")}
|
||||
className={`
|
||||
px-4 py-1 rounded-sm text-sm font-medium border transition
|
||||
${filter === "ALL"
|
||||
? "bg-blue-200 text-blue-700 border-blue-200"
|
||||
: "bg-white text-blue-700 border-blue-700 hover:bg-blue-200 cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
ALL ({counts.ALL})
|
||||
</button>
|
||||
|
||||
{/* CLEANSING */}
|
||||
<button
|
||||
onClick={() => setFilter("CLEANSING")}
|
||||
className={`
|
||||
px-4 py-1 rounded-sm text-sm font-medium border transition
|
||||
${filter === "CLEANSING"
|
||||
? "bg-yellow-200 text-yellow-700 border-yellow-200"
|
||||
: "bg-white text-yellow-400 border-yellow-400 hover:bg-yellow-200 hover:text-yellow-700 cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
CLEANSING ({counts.CLEANSING})
|
||||
</button>
|
||||
|
||||
{/* ERROR */}
|
||||
<button
|
||||
onClick={() => setFilter("ERROR")}
|
||||
className={`
|
||||
px-4 py-1 rounded-sm text-sm font-medium border transition
|
||||
${filter === "ERROR"
|
||||
? "bg-red-200 text-red-500 border-red-200"
|
||||
: "bg-white text-red-500 border-red-500 hover:bg-red-200 cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
ERROR ({counts.ERROR})
|
||||
</button>
|
||||
|
||||
{/* FINISHED */}
|
||||
<button
|
||||
onClick={() => setFilter("FINISHED")}
|
||||
className={`
|
||||
px-4 py-1 rounded-sm text-sm font-medium border transition
|
||||
${filter === "FINISHED"
|
||||
? "bg-green-200 text-green-500 border-green-200"
|
||||
: "bg-white text-green-500 border-green-500 hover:bg-green-200 cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
FINISHED ({counts.FINISHED})
|
||||
</button>
|
||||
|
||||
{/* TESTING */}
|
||||
<button
|
||||
onClick={() => setFilter("TESTING")}
|
||||
className={`
|
||||
px-4 py-1 rounded-sm text-sm font-medium border transition
|
||||
${filter === "TESTING"
|
||||
? "bg-gray-300 text-gray-700 border-gray-300"
|
||||
: "bg-white text-gray-700 border-gray-700 hover:bg-gray-300"
|
||||
}
|
||||
`}
|
||||
>
|
||||
TESTING ({counts.TESTING})
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* Empty State */}
|
||||
{datasets.length === 0 && !loading && (
|
||||
{filteredData.length === 0 && !loading && (
|
||||
<p className="text-gray-500 text-center mt-10">
|
||||
Belum ada metadata dataset yang tersimpan.
|
||||
</p>
|
||||
|
|
@ -38,7 +126,7 @@ export default function ViewsAdminHome() {
|
|||
|
||||
{/* CARD LIST */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{datasets.map((item) => (
|
||||
{filteredData.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 rounded-xl shadow-sm p-6 hover:shadow-md transition"
|
||||
|
|
@ -51,12 +139,14 @@ export default function ViewsAdminHome() {
|
|||
{/* STATUS BADGE */}
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded-full ${
|
||||
item.dataset_status === "completed"
|
||||
item.process === "FINISHED"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700"
|
||||
: item.process === "CLEANSING"
|
||||
? "bg-yellow-100 text-yellow-700"
|
||||
: "bg-red-100 text-red-700"
|
||||
}`}
|
||||
>
|
||||
{item.dataset_status}
|
||||
{item.process}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -71,11 +161,6 @@ export default function ViewsAdminHome() {
|
|||
{item.table_title}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">Kategori:</span>{" "}
|
||||
{item.topic_category}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">Organisasi:</span>{" "}
|
||||
{item.organization_name}
|
||||
|
|
@ -134,9 +219,9 @@ export default function ViewsAdminHome() {
|
|||
{/* MORE MENU */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<button className="p-2 rounded-full hover:bg-gray-200">
|
||||
<div className="p-2 rounded-full hover:bg-gray-200">
|
||||
⋮
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="w-40">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setFile, setResult, setValidatedData, setPdfPageCount, setSelectedPages } from "../../../store/slices/uploadSlice";
|
||||
import { uploadFile, uploadPdf, saveToDatabase } from "./service_admin_upload";
|
||||
import { uploadFile, uploadPdf, saveToDatabase, getStyles } from "./service_admin_upload";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
|
|
@ -14,7 +14,7 @@ import * as XLSX from 'xlsx';
|
|||
export function useUploadController() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { file, result, pdfPageCount, selectedPages } = useSelector((state) => state.upload);
|
||||
const { file, fileDesc, result, pdfPageCount, selectedPages } = useSelector((state) => state.upload);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedTable, setSelectedTable] = useState(null);
|
||||
|
|
@ -24,6 +24,11 @@ export function useUploadController() {
|
|||
const [selectedSheet, setSelectedSheet] = useState(null);
|
||||
const [sheetCount, setSheetCount] = useState(null);
|
||||
const [sheetNames, setSheetNames] = useState([]);
|
||||
|
||||
// const [filedesc, setFileDesc] = useState(null);
|
||||
|
||||
const [geosStyle, setGeosStyle] = useState([]);
|
||||
const [fileReady, setFileReady] = useState(false);
|
||||
|
||||
// 🔹 handle drop file
|
||||
const handleFileSelect = async (f) => {
|
||||
|
|
@ -60,7 +65,8 @@ export function useUploadController() {
|
|||
|
||||
|
||||
|
||||
navigate("/admin/upload/pdf");
|
||||
// navigate("/admin/upload/pdf");
|
||||
setFileReady(true);
|
||||
}
|
||||
else if (ext === "xlsx" || ext === "xls") {
|
||||
const data = await f.arrayBuffer();
|
||||
|
|
@ -81,7 +87,7 @@ export function useUploadController() {
|
|||
if (!file) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await uploadFile(file, selectedPages, selectedSheet);
|
||||
const res = await uploadFile(file, selectedPages, selectedSheet, fileDesc);
|
||||
dispatch(setResult(res));
|
||||
|
||||
if (res.file_type !== ".pdf" || (res.file_type === ".pdf" && !res.tables)) {
|
||||
|
|
@ -99,7 +105,7 @@ export function useUploadController() {
|
|||
if (!selectedTable) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await uploadPdf(selectedTable);
|
||||
const res = await uploadPdf(selectedTable, file.name, fileDesc);
|
||||
dispatch(setResult(res));
|
||||
navigate("/admin/upload/validate");
|
||||
} catch(err){
|
||||
|
|
@ -108,14 +114,15 @@ export function useUploadController() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleConfirmUpload = async (metadata) => {
|
||||
const handleConfirmUpload = async (metadata, style) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = {
|
||||
title: metadata.title,
|
||||
columns: result.columns,
|
||||
rows: result.preview,
|
||||
author: metadata
|
||||
author: metadata,
|
||||
style: style
|
||||
};
|
||||
const res = await saveToDatabase(data);
|
||||
dispatch(setValidatedData(res));
|
||||
|
|
@ -126,6 +133,15 @@ export function useUploadController() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleGetStyles = async () => {
|
||||
try {
|
||||
const data = await getStyles();
|
||||
setGeosStyle(data.styles);
|
||||
} catch (err) {
|
||||
setErrorMsg(err?.message || "Terjadi kesalahan saat memuat data.");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
file,
|
||||
|
|
@ -145,5 +161,7 @@ export function useUploadController() {
|
|||
handleUpload,
|
||||
handleNextPdf,
|
||||
handleConfirmUpload,
|
||||
handleGetStyles,
|
||||
geosStyle
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||
export function usePdfViewerController() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { file, selectedPages } = useSelector((state) => state.upload);
|
||||
const { file, fileDesc } = useSelector((state) => state.upload);
|
||||
|
||||
const [pages, setPages] = useState([]);
|
||||
const [selectedPagesLocal, setSelectedPagesLocal] = useState([]);
|
||||
|
|
@ -73,7 +73,7 @@ export function usePdfViewerController() {
|
|||
if (selectedPagesLocal.length === 0) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await uploadFile(file, selectedPagesLocal);
|
||||
const res = await uploadFile(file, selectedPagesLocal, null, fileDesc);
|
||||
dispatch(setResult(res));
|
||||
|
||||
if (!res.tables) {
|
||||
|
|
|
|||
|
|
@ -26,13 +26,15 @@
|
|||
|
||||
import api from "../../../services/api";
|
||||
|
||||
export async function uploadFile(file, page = null, sheet = null) {
|
||||
export async function uploadFile(file, page = null, sheet = null, file_desc) {
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("page", page);
|
||||
if (sheet != null) {
|
||||
formData.append("sheet", sheet);
|
||||
}
|
||||
formData.append("file_desc", file_desc);
|
||||
|
||||
try {
|
||||
const response = await api.post("/upload/file", formData, {
|
||||
|
|
@ -44,9 +46,14 @@ export async function uploadFile(file, page = null, sheet = null) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function uploadPdf(data) {
|
||||
export async function uploadPdf(data, fileName, fileDesc) {
|
||||
const payload = {
|
||||
...data,
|
||||
fileName,
|
||||
fileDesc
|
||||
};
|
||||
try {
|
||||
const response = await api.post("/upload/process-pdf", data, {
|
||||
const response = await api.post(`/upload/process-pdf`, payload, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
return response.data.data;
|
||||
|
|
@ -56,7 +63,6 @@ export async function uploadPdf(data) {
|
|||
}
|
||||
|
||||
export async function saveToDatabase(data) {
|
||||
console.log("send:", data);
|
||||
try {
|
||||
const response = await api.post("/upload/to-postgis", data, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -66,3 +72,27 @@ export async function saveToDatabase(data) {
|
|||
throw error.response?.data.detail.message || { message: "Gagal upload data." };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getStyles() {
|
||||
try {
|
||||
const res = await api.get("/dataset/styles");
|
||||
return res.data || [];
|
||||
} catch (err) {
|
||||
console.error("Fetch datasets error:", err);
|
||||
throw err.response?.data || err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getStylesFile(styleName) {
|
||||
try {
|
||||
const res = await api.get(`/dataset/styles/${styleName}`);
|
||||
return res.data?.data || [];
|
||||
} catch (err) {
|
||||
console.error("Fetch datasets error:", err);
|
||||
throw err.response?.data || err;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export function useTablePickerController() {
|
|||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { result } = useSelector((state) => state.upload); // result dari BE upload PDF
|
||||
const { result, file, fileDesc } = useSelector((state) => state.upload); // result dari BE upload PDF
|
||||
const [selectedTable, setSelectedTableLocal] = useState(
|
||||
result?.tables?.[0] || null
|
||||
);
|
||||
|
|
@ -22,7 +22,8 @@ export function useTablePickerController() {
|
|||
if (!selectedTable) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await uploadPdf(selectedTable);
|
||||
console.log('pdf', selectedTable);
|
||||
const res = await uploadPdf(selectedTable, file.name, fileDesc);
|
||||
dispatch(setResult(res));
|
||||
navigate("/admin/upload/validate");
|
||||
} catch(err){
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useEffect, useState, useRef } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
|
@ -13,14 +14,110 @@ export default function ViewsAdminUploadSuccess() {
|
|||
MultiPolygon: "🗾",
|
||||
GeometryCollection: "🧩",
|
||||
};
|
||||
|
||||
const PROCESS_STEPS = [
|
||||
{ key: "upload", label: "Upload data" },
|
||||
{ key: "cleansing", label: "Cleansing data" },
|
||||
{ key: "publish_geoserver", label: "Publish GeoServer" },
|
||||
{ key: "done", label: "Publish GeoNetwork" },
|
||||
];
|
||||
const INITIAL_STEP_STATUS = {
|
||||
upload: "done",
|
||||
cleansing: "pending",
|
||||
publish_geoserver: "pending",
|
||||
done: "pending",
|
||||
};
|
||||
|
||||
const Spinner = () => (
|
||||
<span className="inline-block w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
);
|
||||
const renderIcon = (status) => {
|
||||
if (status === "running") return <Spinner />;
|
||||
if (status === "done") return "✔";
|
||||
if (status === "error") return "❌";
|
||||
return "⬜";
|
||||
};
|
||||
|
||||
|
||||
const [stepStatus, setStepStatus] = useState(INITIAL_STEP_STATUS);
|
||||
const wsRef = useRef(null);
|
||||
|
||||
if (!validatedData) {
|
||||
return <Navigate to="/admin/upload" />;
|
||||
}else{
|
||||
console.log('success', validatedData);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!validatedData?.job_id) return;
|
||||
|
||||
const wsUrl = `ws://localhost:8000/ws/job/${validatedData.job_id}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WS connected:", validatedData.job_id);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const finishedStep = data.step;
|
||||
|
||||
setStepStatus((prev) => {
|
||||
const updated = { ...prev };
|
||||
|
||||
const stepIndex = PROCESS_STEPS.findIndex(
|
||||
(s) => s.key === finishedStep
|
||||
);
|
||||
|
||||
// 1️⃣ step yang dikirim WS → DONE
|
||||
if (stepIndex >= 0) {
|
||||
updated[finishedStep] = "done";
|
||||
}
|
||||
|
||||
// 2️⃣ step setelahnya → RUNNING
|
||||
const nextStep = PROCESS_STEPS[stepIndex + 1];
|
||||
if (nextStep) {
|
||||
updated[nextStep.key] = "running";
|
||||
}
|
||||
|
||||
// 3️⃣ step setelah itu → PENDING
|
||||
PROCESS_STEPS.slice(stepIndex + 2).forEach((s) => {
|
||||
if (updated[s.key] !== "done") {
|
||||
updated[s.key] = "pending";
|
||||
}
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
// 🔥 AUTO CLOSE WS JIKA SELESAI
|
||||
if (finishedStep === "done") {
|
||||
setTimeout(() => {
|
||||
wsRef.current?.close();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error("WS error", err);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WS closed");
|
||||
};
|
||||
|
||||
// 🔥 CLEANUP: ketika pindah halaman
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, [validatedData?.job_id]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-20 text-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h1 className="text-3xl font-bold text-green-600 mb-4">✅ Upload Berhasil!</h1>
|
||||
<p className="text-gray-700 mb-8">
|
||||
Data Anda berhasil disimpan ke database.
|
||||
|
|
@ -93,16 +190,53 @@ export default function ViewsAdminUploadSuccess() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{validatedData.message && (
|
||||
{/* {validatedData.message && (
|
||||
<div className="bg-green-50 border border-green-200 px-5 py-4 rounded-lg mt-4">
|
||||
<p className="w-full text-center text-green-700 font-semibold">
|
||||
{/* {validatedData.message} */}
|
||||
Datasets berhasil di upload
|
||||
Data sedang diproses <br />
|
||||
</p>
|
||||
</div>
|
||||
)} */}
|
||||
{validatedData.message && (
|
||||
<div className="border border-gray-200 rounded-lg mt-4 overflow-hidden">
|
||||
{PROCESS_STEPS.map((step) => (
|
||||
<div
|
||||
key={step.key}
|
||||
className={`px-4 flex items-center gap-3 text-sm py-3 border-b border-gray-200 ${
|
||||
stepStatus[step.key] === "done"
|
||||
? "bg-green-50"
|
||||
: stepStatus[step.key] === "running"
|
||||
? "bg-blue-50"
|
||||
: "bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
|
||||
<span className="w-5 flex justify-center">
|
||||
{renderIcon(stepStatus[step.key] || "-")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={
|
||||
stepStatus[step.key] === "done"
|
||||
? "text-green-600 font-medium"
|
||||
: stepStatus[step.key] === "running"
|
||||
? "text-blue-600 font-medium"
|
||||
: "text-gray-500"
|
||||
}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-center text-gray-500">
|
||||
Sistem sedang melakukan cleansing data dan publikasi ke GeoServer dan GeoNetwork.<br />
|
||||
Anda tidak perlu menunggu di halaman ini.
|
||||
</p>
|
||||
|
||||
{/* Metadata Section */}
|
||||
{validatedData.metadata && (
|
||||
<div className="mt-8 relative z-10">
|
||||
|
|
@ -114,13 +248,20 @@ export default function ViewsAdminUploadSuccess() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<Link
|
||||
to="/admin/upload"
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition"
|
||||
>
|
||||
Kembali ke Dashboard
|
||||
</Link>
|
||||
<div className="flex flex-col w-full items-center ">
|
||||
<Link
|
||||
to="/admin/home"
|
||||
className="w-fit bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition"
|
||||
>
|
||||
Kembali ke Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/upload"
|
||||
className="w-fit mt-3 text-gray-500 px-6 py-2 hover:text-gray-600 transition"
|
||||
>
|
||||
Upload data lagi
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useUploadController } from "./controller_admin_upload";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import LoadingOverlay from "../../../components/LoadingOverlay";
|
||||
import ErrorNotification from "../../../components/ErrorNotification";
|
||||
import FileDropzone from "../../../components/FileDropzone";
|
||||
import PdfPageSelector from "../../../components/PdfPageSelector";
|
||||
import { Link } from "react-router-dom";
|
||||
import { reset } from "../../../store/slices/uploadSlice";
|
||||
import { setFileDesc } from "../../../store/slices/uploadSlice";
|
||||
|
||||
export default function ViewsAdminUploadStep1() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
loading,
|
||||
|
|
@ -37,10 +40,14 @@ export default function ViewsAdminUploadStep1() {
|
|||
};
|
||||
|
||||
const handleProcess = async () => {
|
||||
try {
|
||||
await handleUpload();
|
||||
} catch (err) {
|
||||
setErrorMsg(err);
|
||||
if (ext === 'pdf') {
|
||||
navigate("/admin/upload/pdf");
|
||||
} else {
|
||||
try {
|
||||
await handleUpload();
|
||||
} catch (err) {
|
||||
setErrorMsg(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -80,7 +87,7 @@ export default function ViewsAdminUploadStep1() {
|
|||
{/* {!file && (
|
||||
)} */}
|
||||
|
||||
{file && ext!= 'pdf' && (
|
||||
{file && (
|
||||
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
|
||||
{/* Info File */}
|
||||
<div className="">
|
||||
|
|
@ -138,7 +145,19 @@ export default function ViewsAdminUploadStep1() {
|
|||
</>
|
||||
)}
|
||||
|
||||
|
||||
<div className="mt-6 ">
|
||||
<label htmlFor="fileDesc" className="block text-sm font-semibold text-gray-700 mb-1">
|
||||
Deskripsi Singkat File<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="fileDesc"
|
||||
name="fileDesc"
|
||||
type='text'
|
||||
onChange={(e) => dispatch(setFileDesc(e.target.value))}
|
||||
className={`w-full border border-gray-300 rounded-md p-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition bg-white border-red-500 ring-1 ring-red-400`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tombol Upload */}
|
||||
<div className={`mt-6 flex justify-center ${(result && result.file_type === ".pdf" && result.tables?.length > 1) ? 'hidden' : 'block' }`}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,162 +1,3 @@
|
|||
// import { useState, useEffect } from "react";
|
||||
// import { useUploadController } from "./controller_admin_upload";
|
||||
// import { useSelector } from "react-redux";
|
||||
// import LoadingOverlay from "../../../components/LoadingOverlay";
|
||||
// import Notification from "../../../components/Notification";
|
||||
// import ErrorNotification from "../../../components/ErrorNotification";
|
||||
// import { Navigate } from "react-router-dom";
|
||||
// import FilePreview from "../../../components/upload/FilePreview";
|
||||
// import MetadataForm from "../../../components/MetaDataForm";
|
||||
|
||||
// export default function ViewsAdminUploadValidate() {
|
||||
// const { result, file } = useSelector((state) => state.upload);
|
||||
// const {
|
||||
// loading,
|
||||
// tableTitle,
|
||||
// setTableTitle,
|
||||
// handleConfirmUpload,
|
||||
// } = useUploadController();
|
||||
|
||||
// const [showAlert, setShowAlert] = useState(false);
|
||||
// const [alertMessage, setAlertMessage] = useState("");
|
||||
// const [alertType, setAlertType] = useState("info");
|
||||
// const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleBeforeUnload = (e) => {
|
||||
// if (result && !loading) {
|
||||
// e.preventDefault();
|
||||
// e.returnValue =
|
||||
// "Data upload Anda belum disimpan. Jika Anda meninggalkan halaman ini, data akan hilang.";
|
||||
// return e.returnValue;
|
||||
// }
|
||||
// };
|
||||
|
||||
// window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
// return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
// }, [result, loading]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleNavigation = (e) => {
|
||||
// if (result && !loading) {
|
||||
// const confirmLeave = window.confirm(
|
||||
// "Data upload Anda belum disimpan. Jika Anda meninggalkan halaman ini, data akan hilang."
|
||||
// );
|
||||
// if (!confirmLeave) {
|
||||
// e.preventDefault();
|
||||
// window.history.pushState(null, "", window.location.href); // tetap di halaman
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// window.addEventListener("popstate", handleNavigation);
|
||||
// return () => window.removeEventListener("popstate", handleNavigation);
|
||||
// }, [result, loading]);
|
||||
|
||||
// const handleMetadataChange = (data) => {
|
||||
// console.log("Metadata Updated:", data);
|
||||
// };
|
||||
|
||||
// // if (!result)
|
||||
// // return <div className="text-center mt-10">Data belum diupload.</div>;
|
||||
|
||||
// if (!result) return <Navigate to="/admin/upload" />;
|
||||
|
||||
// const handleUploadClick = async () => {
|
||||
// if (!tableTitle.trim()) {
|
||||
// setAlertMessage(
|
||||
// "❗Judul tabel belum diisi. Silakan isi sebelum melanjutkan."
|
||||
// );
|
||||
// setAlertType("error");
|
||||
// setShowAlert(true);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // handleConfirmUpload();
|
||||
// try {
|
||||
// await handleConfirmUpload();
|
||||
// } catch (err) {
|
||||
// setErrorMsg(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="max-w-4xl mx-auto py-10">
|
||||
// {showAlert && (
|
||||
// <Notification
|
||||
// message={alertMessage}
|
||||
// type={alertType}
|
||||
// onClose={() => setShowAlert(false)}
|
||||
// />
|
||||
// )}
|
||||
|
||||
// <ErrorNotification
|
||||
// message={errorMsg}
|
||||
// onClose={() => setErrorMsg("")}
|
||||
// />
|
||||
|
||||
// <LoadingOverlay show={loading} text="Upload to database..." />
|
||||
|
||||
// <h1 className="text-2xl font-bold mb-4">✅ Validasi & Konfirmasi Data</h1>
|
||||
|
||||
// <MetadataForm onChange={handleMetadataChange} />
|
||||
|
||||
// <div className="w-full mx-auto mt-6">
|
||||
// <label
|
||||
// htmlFor="tableTitle"
|
||||
// className="block text-sm font-medium text-gray-700 mb-2"
|
||||
// >
|
||||
// Judul Tabel
|
||||
// </label>
|
||||
// <input
|
||||
// id="tableTitle"
|
||||
// type="text"
|
||||
// value={tableTitle}
|
||||
// onChange={(e) => setTableTitle(e.target.value)}
|
||||
// placeholder="Masukkan judul tabel..."
|
||||
// className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 ${
|
||||
// !tableTitle ? "border-red-400" : ""
|
||||
// }`}
|
||||
// />
|
||||
// <small className="text-gray-500">
|
||||
// Judul akan dijadikan nama tabel pada database
|
||||
// </small>
|
||||
// </div>
|
||||
|
||||
// {result && <FilePreview result={result} />}
|
||||
|
||||
// <div className="mt-6 flex justify-between">
|
||||
// <button
|
||||
// onClick={() => history.back()}
|
||||
// className="px-5 py-2 text-blue-600 hover:underline"
|
||||
// >
|
||||
// ← Kembali
|
||||
// </button>
|
||||
|
||||
// <button
|
||||
// onClick={handleUploadClick}
|
||||
// disabled={loading}
|
||||
// className="px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400"
|
||||
// >
|
||||
// {loading ? "Mengunggah..." : "Upload ke Database"}
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// <div className="bg-white border rounded-xl shadow-sm p-6 mt-4">
|
||||
// <FilePreview result={result} />
|
||||
// </div>
|
||||
|
||||
|
||||
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUploadController } from "./controller_admin_upload";
|
||||
import { useSelector } from "react-redux";
|
||||
|
|
@ -167,6 +8,11 @@ import Notification from "../../../components/Notification";
|
|||
import ErrorNotification from "../../../components/ErrorNotification";
|
||||
import MetadataForm from "../../../components/MetaDataForm";
|
||||
import FilePreview from "../../../components/upload/FilePreview";
|
||||
import ConfirmDialog from "../../../components/common/ConfirmDialog"
|
||||
|
||||
import GeoPreview from "@/components/layers_preview/GeoPreview";
|
||||
import StylingLayers from "@/components/layers_style/StylingLayers";
|
||||
import SpatialStylePreview from "@/components/layers_style/StylePreview";
|
||||
|
||||
// shadcn accordion (pastikan path sesuai proyekmu)
|
||||
import {
|
||||
|
|
@ -175,6 +21,14 @@ import {
|
|||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from "../../../components/ui/accordion";
|
||||
import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from "../../../components/ui/sheet";
|
||||
|
||||
export default function ViewsAdminUploadValidate() {
|
||||
const { result } = useSelector((state) => state.upload);
|
||||
|
|
@ -183,13 +37,20 @@ export default function ViewsAdminUploadValidate() {
|
|||
tableTitle,
|
||||
setTableTitle,
|
||||
handleConfirmUpload,
|
||||
handleGetStyles,
|
||||
geosStyle
|
||||
} = useUploadController();
|
||||
|
||||
const [styleConfig, setStyleConfig] = useState(null);
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [alertMessage, setAlertMessage] = useState("");
|
||||
const [alertType, setAlertType] = useState("info");
|
||||
|
||||
const [openSheet, setOpenSheet] = useState(false);
|
||||
const [showStylePreview, setShowStylePreview] = useState(false);
|
||||
|
||||
// Local state: index tabel yg dipilih (default 0)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
// Metadata form state is emitted via onChange from MetadataForm; simpan jika perlu
|
||||
|
|
@ -200,6 +61,11 @@ export default function ViewsAdminUploadValidate() {
|
|||
|
||||
// Keep selectedIndex valid ketika result berubah
|
||||
useEffect(() => {
|
||||
console.log('review', result);
|
||||
|
||||
handleGetStyles()
|
||||
setTimeout(() => setShowStylePreview(true), 500);
|
||||
|
||||
if (!result || !result.tables || result.tables.length === 0) {
|
||||
setSelectedIndex(0);
|
||||
return;
|
||||
|
|
@ -213,6 +79,15 @@ export default function ViewsAdminUploadValidate() {
|
|||
});
|
||||
}, [result]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openSheet) {
|
||||
const timer = setTimeout(() => setShowStylePreview(true), 600);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setShowStylePreview(false);
|
||||
}
|
||||
}, [openSheet]);
|
||||
|
||||
const handleUploadClick = async () => {
|
||||
if (!tableTitle || !tableTitle.trim()) {
|
||||
setAlertMessage("❗Judul tabel belum diisi. Silakan isi sebelum melanjutkan.");
|
||||
|
|
@ -221,7 +96,7 @@ export default function ViewsAdminUploadValidate() {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
await handleConfirmUpload(metadata);
|
||||
await handleConfirmUpload(metadata, styleConfig.sldContent);
|
||||
} catch (err) {
|
||||
// tangani error dari controller/service
|
||||
const message =
|
||||
|
|
@ -234,8 +109,16 @@ export default function ViewsAdminUploadValidate() {
|
|||
|
||||
const selectedTable = result.tables?.[selectedIndex] || null;
|
||||
|
||||
const handleStyleSubmit = (config) => {
|
||||
setStyleConfig(config)
|
||||
console.log("Konfigurasi styling:", config);
|
||||
// Kirim ke backend → generate SLD otomatis → publish ke GeoServer
|
||||
};
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="py-10">
|
||||
<div className="p-0 h-[calc(100vh-(57px+48px))] overflow-hidden">
|
||||
{/* Alerts */}
|
||||
{showAlert && (
|
||||
<Notification
|
||||
|
|
@ -247,66 +130,158 @@ export default function ViewsAdminUploadValidate() {
|
|||
<ErrorNotification message={errorMsg} onClose={() => setErrorMsg("")} />
|
||||
<LoadingOverlay show={loading} text="Upload to database..." />
|
||||
|
||||
<h1 className="text-2xl font-bold mb-4">✅ Validasi & Konfirmasi Data</h1>
|
||||
<div
|
||||
className="h-full w-full transition-transform duration-700 ease-in-out"
|
||||
style={{ transform: `translateY(-${index * 100}vh)` }}
|
||||
>
|
||||
<div className="w-full h-full">
|
||||
<h1 className="text-2xl font-bold mb-4">✅ Validasi & Konfirmasi Data</h1>
|
||||
|
||||
{/* SINGLE ACCORDION */}
|
||||
<Accordion type="single" collapsible defaultValue="validate-panel" className="w-full">
|
||||
<AccordionItem value="validate-panel" className="bg-white rounded-xl border shadow-sm px-3 mb-4">
|
||||
<AccordionTrigger className="text-lg font-semibold">
|
||||
📄 Dataset 1
|
||||
</AccordionTrigger>
|
||||
{/* <GeoPreview features={result.preview} /> */}
|
||||
|
||||
<AccordionContent>
|
||||
<div className="mt-4 grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* LEFT: tabel preview (6 kolom pada layout 12) */}
|
||||
<div className="lg:col-span-6 col-span-1 min-w-0">
|
||||
<h3 className="text-xl font-semibold mb-3">🧾 Cuplikan Data</h3>
|
||||
<div className="mb-3">
|
||||
<div className="flex gap-2 min-w-0">
|
||||
<FilePreview result={result} />
|
||||
{/* SINGLE ACCORDION */}
|
||||
<Accordion type="single" collapsible defaultValue="validate-panel" className="w-full">
|
||||
<AccordionItem value="validate-panel" className="bg-white rounded-xl border shadow-sm px-3 mb-4">
|
||||
<AccordionTrigger className="text-lg font-semibold">
|
||||
📄 {result.file_name}
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent>
|
||||
<div className="mt-4 grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* LEFT: tabel preview (6 kolom pada layout 12) */}
|
||||
<div className="lg:col-span-8 col-span-1 min-w-0">
|
||||
<h3 className="text-xl font-semibold mb-3">🧾 Cuplikan Data</h3>
|
||||
<div className="mb-3">
|
||||
<div className="flex gap-2 min-w-0">
|
||||
<FilePreview result={result} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: metadata form (6 kolom) */}
|
||||
<div className="lg:col-span-4 col-span-1">
|
||||
<div className="mb-3 flex justify-between items-center">
|
||||
<h3 className="text-xl font-semibold mb-0">🧾 Info dataset</h3>
|
||||
<span className="text-gray-500 italic">AI Generate</span>
|
||||
</div>
|
||||
|
||||
{/* MetadataForm menyimpan hasil ke parent via onChange */}
|
||||
<MetadataForm
|
||||
initialValues={result.metadata_suggest}
|
||||
onChange={(data) => setMetadata(data)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: metadata form (6 kolom) */}
|
||||
<div className="lg:col-span-6 col-span-1">
|
||||
<h3 className="text-xl font-semibold mb-3">🧾 Metadata</h3>
|
||||
{/* ACTIONS di bawah accordion content */}
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
onClick={() => history.back()}
|
||||
className="px-5 py-2 text-blue-600 hover:underline"
|
||||
>
|
||||
← Kembali
|
||||
</button>
|
||||
|
||||
{/* MetadataForm menyimpan hasil ke parent via onChange */}
|
||||
<MetadataForm onChange={(data) => setMetadata(data)}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACTIONS di bawah accordion content */}
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
onClick={() => history.back()}
|
||||
className="px-5 py-2 text-blue-600 hover:underline"
|
||||
>
|
||||
← Kembali
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* optional: show metadata summary brief */}
|
||||
{metadata && (
|
||||
<div className="text-xs text-gray-600">
|
||||
Metadata siap — preview: <span className="font-medium">{metadata.title || "-"}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* optional: show metadata summary brief */}
|
||||
{metadata && (
|
||||
<div className="text-xs text-gray-600">
|
||||
Metadata siap — preview: <span className="font-medium">{metadata.title || "-"}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* <button
|
||||
onClick={handleGetStyles}
|
||||
className="px-5 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
|
||||
></button> */}
|
||||
<button
|
||||
onClick={() => setIndex(1)}
|
||||
className="px-5 py-2 bg-blue-600 text-white rounded"
|
||||
>
|
||||
Selanjutnya ↓
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={handleUploadClick}
|
||||
disabled={loading}
|
||||
className="px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading ? "Mengunggah..." : "Upload ke Database"} ↓
|
||||
</button> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div className="mt-[81px] pt-6 h-full w-full flex flex-wrap items-stretch">
|
||||
<h2 className="w-full mb-2 text-xl font-bold">Preview Style</h2>
|
||||
<div className="w-[60%] h-[calc(100%-68px)]">
|
||||
{showStylePreview &&
|
||||
<SpatialStylePreview data={result.preview} geometryType={result.geometry_type} styleConfig={styleConfig}/>
|
||||
}
|
||||
</div>
|
||||
<div className="w-[40%] h-[calc(100%-68px)]">
|
||||
<StylingLayers data={result.preview} geometryType={result.geometry_type} onSubmit={handleStyleSubmit} geosStyle={geosStyle}/>
|
||||
</div>
|
||||
<div className="mt-3 w-full h-fit flex gap-1">
|
||||
<button
|
||||
onClick={() => setIndex(0)}
|
||||
className="w-[60%] px-4 py-2 bg-blue-600 text-white rounded"
|
||||
>
|
||||
Kembali ↑
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={handleUploadClick}
|
||||
disabled={loading}
|
||||
className="w-[40%] px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading ? "Mengunggah..." : "Upload ke Database"}
|
||||
</button> */}
|
||||
<ConfirmDialog
|
||||
title="Upload ke database?"
|
||||
description="Pastikan data sudah benar sebelum diunggah."
|
||||
confirmText="Ya, Upload"
|
||||
cancelText="Batal"
|
||||
onConfirm={handleUploadClick}
|
||||
trigger={
|
||||
<button
|
||||
onClick={handleUploadClick}
|
||||
disabled={loading}
|
||||
className="px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400"
|
||||
className="w-[40%] px-5 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading ? "Mengunggah..." : "Upload ke Database"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Sheet open={openSheet} onOpenChange={setOpenSheet}>
|
||||
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
className="h-[90vh] overflow-hidden p-0"
|
||||
>
|
||||
<SheetHeader className="px-4 py-2 border-b">
|
||||
<SheetTitle>Style Editor</SheetTitle>
|
||||
<SheetDescription>
|
||||
Edit your preview and styling layers.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-[70%] h-full border-r overflow-auto">
|
||||
{showStylePreview && <SpatialStylePreview data={result.preview} geometryType={result.geometry_type} styleConfig={styleConfig}/>}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div className="w-[30%] h-full overflow-auto pb-20">
|
||||
<StylingLayers data={result.preview} geometryType={result.geometry_type} onSubmit={handleStyleSubmit} geosStyle={geosStyle}/>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user