update admin page

This commit is contained in:
DmsAnhr 2025-12-22 15:27:29 +07:00
parent 60d39a36e6
commit 8921e2a118
8 changed files with 523 additions and 254 deletions

View File

@ -9,9 +9,22 @@ import {
DropdownMenuSeparator DropdownMenuSeparator
} from "../../../components/ui/dropdown-menu"; } from "../../../components/ui/dropdown-menu";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useState } from "react";
export default function ViewsAdminHome() { export default function ViewsAdminHome() {
const { datasets, loading, errorMsg } = useAdminHomeController(); 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 ( return (
<div className="max-w-6xl mx-auto py-10"> <div className="max-w-6xl mx-auto py-10">
@ -29,8 +42,83 @@ export default function ViewsAdminHome() {
</Link> </Link>
</div> </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 */} {/* Empty State */}
{datasets.length === 0 && !loading && ( {filteredData.length === 0 && !loading && (
<p className="text-gray-500 text-center mt-10"> <p className="text-gray-500 text-center mt-10">
Belum ada metadata dataset yang tersimpan. Belum ada metadata dataset yang tersimpan.
</p> </p>
@ -38,7 +126,7 @@ export default function ViewsAdminHome() {
{/* CARD LIST */} {/* CARD LIST */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{datasets.map((item) => ( {filteredData.map((item) => (
<div <div
key={item.id} key={item.id}
className="bg-white border border-gray-200 rounded-xl shadow-sm p-6 hover:shadow-md transition" 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 */} {/* STATUS BADGE */}
<span <span
className={`text-xs px-2 py-1 rounded-full ${ className={`text-xs px-2 py-1 rounded-full ${
item.dataset_status === "completed" item.process === "FINISHED"
? "bg-green-100 text-green-700" ? "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> </span>
</div> </div>
@ -71,11 +161,6 @@ export default function ViewsAdminHome() {
{item.table_title} {item.table_title}
</p> </p>
<p>
<span className="font-medium">Kategori:</span>{" "}
{item.topic_category}
</p>
<p> <p>
<span className="font-medium">Organisasi:</span>{" "} <span className="font-medium">Organisasi:</span>{" "}
{item.organization_name} {item.organization_name}
@ -134,9 +219,9 @@ export default function ViewsAdminHome() {
{/* MORE MENU */} {/* MORE MENU */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<button className="p-2 rounded-full hover:bg-gray-200"> <div className="p-2 rounded-full hover:bg-gray-200">
</button> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-40"> <DropdownMenuContent className="w-40">

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setFile, setResult, setValidatedData, setPdfPageCount, setSelectedPages } from "../../../store/slices/uploadSlice"; 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 { useNavigate } from "react-router-dom";
import * as pdfjsLib from "pdfjs-dist"; import * as pdfjsLib from "pdfjs-dist";
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@ -14,7 +14,7 @@ import * as XLSX from 'xlsx';
export function useUploadController() { export function useUploadController() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); 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 [loading, setLoading] = useState(false);
const [selectedTable, setSelectedTable] = useState(null); const [selectedTable, setSelectedTable] = useState(null);
@ -24,6 +24,11 @@ export function useUploadController() {
const [selectedSheet, setSelectedSheet] = useState(null); const [selectedSheet, setSelectedSheet] = useState(null);
const [sheetCount, setSheetCount] = useState(null); const [sheetCount, setSheetCount] = useState(null);
const [sheetNames, setSheetNames] = useState([]); const [sheetNames, setSheetNames] = useState([]);
// const [filedesc, setFileDesc] = useState(null);
const [geosStyle, setGeosStyle] = useState([]);
const [fileReady, setFileReady] = useState(false);
// 🔹 handle drop file // 🔹 handle drop file
const handleFileSelect = async (f) => { 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") { else if (ext === "xlsx" || ext === "xls") {
const data = await f.arrayBuffer(); const data = await f.arrayBuffer();
@ -81,7 +87,7 @@ export function useUploadController() {
if (!file) return; if (!file) return;
setLoading(true); setLoading(true);
try { try {
const res = await uploadFile(file, selectedPages, selectedSheet); const res = await uploadFile(file, selectedPages, selectedSheet, fileDesc);
dispatch(setResult(res)); dispatch(setResult(res));
if (res.file_type !== ".pdf" || (res.file_type === ".pdf" && !res.tables)) { if (res.file_type !== ".pdf" || (res.file_type === ".pdf" && !res.tables)) {
@ -99,7 +105,7 @@ export function useUploadController() {
if (!selectedTable) return; if (!selectedTable) return;
setLoading(true); setLoading(true);
try { try {
const res = await uploadPdf(selectedTable); const res = await uploadPdf(selectedTable, file.name, fileDesc);
dispatch(setResult(res)); dispatch(setResult(res));
navigate("/admin/upload/validate"); navigate("/admin/upload/validate");
} catch(err){ } catch(err){
@ -108,14 +114,15 @@ export function useUploadController() {
} }
}; };
const handleConfirmUpload = async (metadata) => { const handleConfirmUpload = async (metadata, style) => {
setLoading(true); setLoading(true);
try { try {
const data = { const data = {
title: metadata.title, title: metadata.title,
columns: result.columns, columns: result.columns,
rows: result.preview, rows: result.preview,
author: metadata author: metadata,
style: style
}; };
const res = await saveToDatabase(data); const res = await saveToDatabase(data);
dispatch(setValidatedData(res)); 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 { return {
loading, loading,
file, file,
@ -145,5 +161,7 @@ export function useUploadController() {
handleUpload, handleUpload,
handleNextPdf, handleNextPdf,
handleConfirmUpload, handleConfirmUpload,
handleGetStyles,
geosStyle
}; };
} }

View File

@ -13,7 +13,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
export function usePdfViewerController() { export function usePdfViewerController() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { file, selectedPages } = useSelector((state) => state.upload); const { file, fileDesc } = useSelector((state) => state.upload);
const [pages, setPages] = useState([]); const [pages, setPages] = useState([]);
const [selectedPagesLocal, setSelectedPagesLocal] = useState([]); const [selectedPagesLocal, setSelectedPagesLocal] = useState([]);
@ -73,7 +73,7 @@ export function usePdfViewerController() {
if (selectedPagesLocal.length === 0) return; if (selectedPagesLocal.length === 0) return;
try { try {
setLoading(true); setLoading(true);
const res = await uploadFile(file, selectedPagesLocal); const res = await uploadFile(file, selectedPagesLocal, null, fileDesc);
dispatch(setResult(res)); dispatch(setResult(res));
if (!res.tables) { if (!res.tables) {

View File

@ -26,13 +26,15 @@
import api from "../../../services/api"; 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(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("page", page); formData.append("page", page);
if (sheet != null) { if (sheet != null) {
formData.append("sheet", sheet); formData.append("sheet", sheet);
} }
formData.append("file_desc", file_desc);
try { try {
const response = await api.post("/upload/file", formData, { 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 { try {
const response = await api.post("/upload/process-pdf", data, { const response = await api.post(`/upload/process-pdf`, payload, {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
return response.data.data; return response.data.data;
@ -56,7 +63,6 @@ export async function uploadPdf(data) {
} }
export async function saveToDatabase(data) { export async function saveToDatabase(data) {
console.log("send:", data);
try { try {
const response = await api.post("/upload/to-postgis", data, { const response = await api.post("/upload/to-postgis", data, {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -66,3 +72,27 @@ export async function saveToDatabase(data) {
throw error.response?.data.detail.message || { message: "Gagal upload 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;
}
}

View File

@ -8,7 +8,7 @@ export function useTablePickerController() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); 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( const [selectedTable, setSelectedTableLocal] = useState(
result?.tables?.[0] || null result?.tables?.[0] || null
); );
@ -22,7 +22,8 @@ export function useTablePickerController() {
if (!selectedTable) return; if (!selectedTable) return;
setLoading(true); setLoading(true);
try { try {
const res = await uploadPdf(selectedTable); console.log('pdf', selectedTable);
const res = await uploadPdf(selectedTable, file.name, fileDesc);
dispatch(setResult(res)); dispatch(setResult(res));
navigate("/admin/upload/validate"); navigate("/admin/upload/validate");
} catch(err){ } catch(err){

View File

@ -1,3 +1,4 @@
import { useEffect, useState, useRef } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
@ -13,14 +14,110 @@ export default function ViewsAdminUploadSuccess() {
MultiPolygon: "🗾", MultiPolygon: "🗾",
GeometryCollection: "🧩", 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) { if (!validatedData) {
return <Navigate to="/admin/upload" />; 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 ( 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> <h1 className="text-3xl font-bold text-green-600 mb-4"> Upload Berhasil!</h1>
<p className="text-gray-700 mb-8"> <p className="text-gray-700 mb-8">
Data Anda berhasil disimpan ke database. Data Anda berhasil disimpan ke database.
@ -93,16 +190,53 @@ export default function ViewsAdminUploadSuccess() {
</div> </div>
)} )}
{validatedData.message && ( {/* {validatedData.message && (
<div className="bg-green-50 border border-green-200 px-5 py-4 rounded-lg mt-4"> <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"> <p className="w-full text-center text-green-700 font-semibold">
{/* {validatedData.message} */} Data sedang diproses <br />
Datasets berhasil di upload
</p> </p>
</div> </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> </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 */} {/* Metadata Section */}
{validatedData.metadata && ( {validatedData.metadata && (
<div className="mt-8 relative z-10"> <div className="mt-8 relative z-10">
@ -114,13 +248,20 @@ export default function ViewsAdminUploadSuccess() {
)} )}
</div> </div>
<div className="flex flex-col w-full items-center ">
<Link <Link
to="/admin/upload" to="/admin/home"
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition" className="w-fit bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition"
> >
Kembali ke Dashboard Kembali ke Dashboard
</Link> </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> </div>
); );
} }

View File

@ -1,14 +1,17 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useUploadController } from "./controller_admin_upload"; import { useUploadController } from "./controller_admin_upload";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import LoadingOverlay from "../../../components/LoadingOverlay"; import LoadingOverlay from "../../../components/LoadingOverlay";
import ErrorNotification from "../../../components/ErrorNotification"; import ErrorNotification from "../../../components/ErrorNotification";
import FileDropzone from "../../../components/FileDropzone"; import FileDropzone from "../../../components/FileDropzone";
import PdfPageSelector from "../../../components/PdfPageSelector"; import PdfPageSelector from "../../../components/PdfPageSelector";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { reset } from "../../../store/slices/uploadSlice"; import { reset } from "../../../store/slices/uploadSlice";
import { setFileDesc } from "../../../store/slices/uploadSlice";
export default function ViewsAdminUploadStep1() { export default function ViewsAdminUploadStep1() {
const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
loading, loading,
@ -37,10 +40,14 @@ export default function ViewsAdminUploadStep1() {
}; };
const handleProcess = async () => { const handleProcess = async () => {
try { if (ext === 'pdf') {
await handleUpload(); navigate("/admin/upload/pdf");
} catch (err) { } else {
setErrorMsg(err); try {
await handleUpload();
} catch (err) {
setErrorMsg(err);
}
} }
}; };
@ -80,7 +87,7 @@ export default function ViewsAdminUploadStep1() {
{/* {!file && ( {/* {!file && (
)} */} )} */}
{file && ext!= 'pdf' && ( {file && (
<div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm"> <div className="mt-6 border border-gray-200 bg-white rounded-xl p-6 shadow-sm">
{/* Info File */} {/* Info File */}
<div className=""> <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 */} {/* Tombol Upload */}
<div className={`mt-6 flex justify-center ${(result && result.file_type === ".pdf" && result.tables?.length > 1) ? 'hidden' : 'block' }`}> <div className={`mt-6 flex justify-center ${(result && result.file_type === ".pdf" && result.tables?.length > 1) ? 'hidden' : 'block' }`}>
<button <button

View File

@ -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 { useEffect, useState } from "react";
import { useUploadController } from "./controller_admin_upload"; import { useUploadController } from "./controller_admin_upload";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@ -167,6 +8,11 @@ import Notification from "../../../components/Notification";
import ErrorNotification from "../../../components/ErrorNotification"; import ErrorNotification from "../../../components/ErrorNotification";
import MetadataForm from "../../../components/MetaDataForm"; import MetadataForm from "../../../components/MetaDataForm";
import FilePreview from "../../../components/upload/FilePreview"; 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) // shadcn accordion (pastikan path sesuai proyekmu)
import { import {
@ -175,6 +21,14 @@ import {
AccordionTrigger, AccordionTrigger,
AccordionContent, AccordionContent,
} from "../../../components/ui/accordion"; } from "../../../components/ui/accordion";
import {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
} from "../../../components/ui/sheet";
export default function ViewsAdminUploadValidate() { export default function ViewsAdminUploadValidate() {
const { result } = useSelector((state) => state.upload); const { result } = useSelector((state) => state.upload);
@ -183,13 +37,20 @@ export default function ViewsAdminUploadValidate() {
tableTitle, tableTitle,
setTableTitle, setTableTitle,
handleConfirmUpload, handleConfirmUpload,
handleGetStyles,
geosStyle
} = useUploadController(); } = useUploadController();
const [styleConfig, setStyleConfig] = useState(null);
const [errorMsg, setErrorMsg] = useState(""); const [errorMsg, setErrorMsg] = useState("");
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const [alertMessage, setAlertMessage] = useState(""); const [alertMessage, setAlertMessage] = useState("");
const [alertType, setAlertType] = useState("info"); const [alertType, setAlertType] = useState("info");
const [openSheet, setOpenSheet] = useState(false);
const [showStylePreview, setShowStylePreview] = useState(false);
// Local state: index tabel yg dipilih (default 0) // Local state: index tabel yg dipilih (default 0)
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
// Metadata form state is emitted via onChange from MetadataForm; simpan jika perlu // 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 // Keep selectedIndex valid ketika result berubah
useEffect(() => { useEffect(() => {
console.log('review', result);
handleGetStyles()
setTimeout(() => setShowStylePreview(true), 500);
if (!result || !result.tables || result.tables.length === 0) { if (!result || !result.tables || result.tables.length === 0) {
setSelectedIndex(0); setSelectedIndex(0);
return; return;
@ -213,6 +79,15 @@ export default function ViewsAdminUploadValidate() {
}); });
}, [result]); }, [result]);
useEffect(() => {
if (openSheet) {
const timer = setTimeout(() => setShowStylePreview(true), 600);
return () => clearTimeout(timer);
} else {
setShowStylePreview(false);
}
}, [openSheet]);
const handleUploadClick = async () => { const handleUploadClick = async () => {
if (!tableTitle || !tableTitle.trim()) { if (!tableTitle || !tableTitle.trim()) {
setAlertMessage("❗Judul tabel belum diisi. Silakan isi sebelum melanjutkan."); setAlertMessage("❗Judul tabel belum diisi. Silakan isi sebelum melanjutkan.");
@ -221,7 +96,7 @@ export default function ViewsAdminUploadValidate() {
return; return;
} }
try { try {
await handleConfirmUpload(metadata); await handleConfirmUpload(metadata, styleConfig.sldContent);
} catch (err) { } catch (err) {
// tangani error dari controller/service // tangani error dari controller/service
const message = const message =
@ -234,8 +109,16 @@ export default function ViewsAdminUploadValidate() {
const selectedTable = result.tables?.[selectedIndex] || null; 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 ( return (
<div className="py-10"> <div className="p-0 h-[calc(100vh-(57px+48px))] overflow-hidden">
{/* Alerts */} {/* Alerts */}
{showAlert && ( {showAlert && (
<Notification <Notification
@ -247,66 +130,158 @@ export default function ViewsAdminUploadValidate() {
<ErrorNotification message={errorMsg} onClose={() => setErrorMsg("")} /> <ErrorNotification message={errorMsg} onClose={() => setErrorMsg("")} />
<LoadingOverlay show={loading} text="Upload to database..." /> <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 */} {/* <GeoPreview features={result.preview} /> */}
<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>
<AccordionContent> {/* SINGLE ACCORDION */}
<div className="mt-4 grid grid-cols-1 lg:grid-cols-12 gap-8"> <Accordion type="single" collapsible defaultValue="validate-panel" className="w-full">
{/* LEFT: tabel preview (6 kolom pada layout 12) */} <AccordionItem value="validate-panel" className="bg-white rounded-xl border shadow-sm px-3 mb-4">
<div className="lg:col-span-6 col-span-1 min-w-0"> <AccordionTrigger className="text-lg font-semibold">
<h3 className="text-xl font-semibold mb-3">🧾 Cuplikan Data</h3> 📄 {result.file_name}
<div className="mb-3"> </AccordionTrigger>
<div className="flex gap-2 min-w-0">
<FilePreview result={result} /> <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> </div>
</div>
{/* RIGHT: metadata form (6 kolom) */} {/* ACTIONS di bawah accordion content */}
<div className="lg:col-span-6 col-span-1"> <div className="mt-6 flex justify-between">
<h3 className="text-xl font-semibold mb-3">🧾 Metadata</h3> <button
onClick={() => history.back()}
className="px-5 py-2 text-blue-600 hover:underline"
>
Kembali
</button>
{/* MetadataForm menyimpan hasil ke parent via onChange */} <div className="flex items-center gap-3">
<MetadataForm onChange={(data) => setMetadata(data)}/> {/* optional: show metadata summary brief */}
{metadata && (
</div> <div className="text-xs text-gray-600">
</div> Metadata siap preview: <span className="font-medium">{metadata.title || "-"}</span>
</div>
{/* ACTIONS di bawah accordion content */} )}
<div className="mt-6 flex justify-between"> {/* <button
<button onClick={handleGetStyles}
onClick={() => history.back()} className="px-5 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
className="px-5 py-2 text-blue-600 hover:underline" ></button> */}
> <button
Kembali onClick={() => setIndex(1)}
</button> className="px-5 py-2 bg-blue-600 text-white rounded"
>
<div className="flex items-center gap-3"> Selanjutnya
{/* optional: show metadata summary brief */} </button>
{metadata && ( {/* <button
<div className="text-xs text-gray-600"> onClick={handleUploadClick}
Metadata siap preview: <span className="font-medium">{metadata.title || "-"}</span> 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>
</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 <button
onClick={handleUploadClick}
disabled={loading} 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"} {loading ? "Mengunggah..." : "Upload ke Database"}
</button> </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> </div>
</AccordionContent>
</AccordionItem> <div className="w-[30%] h-full overflow-auto pb-20">
</Accordion> <StylingLayers data={result.preview} geometryType={result.geometry_type} onSubmit={handleStyleSubmit} geosStyle={geosStyle}/>
</div>
</div>
</SheetContent>
</Sheet> */}
</div> </div>
); );
} }