diff --git a/src/pages/admin/home/views_admin_home.jsx b/src/pages/admin/home/views_admin_home.jsx index 28c1b7d..d203740 100644 --- a/src/pages/admin/home/views_admin_home.jsx +++ b/src/pages/admin/home/views_admin_home.jsx @@ -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 (
@@ -29,8 +42,83 @@ export default function ViewsAdminHome() {
+
+ + {/* ALL */} + + + {/* CLEANSING */} + + + {/* ERROR */} + + + {/* FINISHED */} + + + {/* TESTING */} + + +
+ + {/* Empty State */} - {datasets.length === 0 && !loading && ( + {filteredData.length === 0 && !loading && (

Belum ada metadata dataset yang tersimpan.

@@ -38,7 +126,7 @@ export default function ViewsAdminHome() { {/* CARD LIST */}
- {datasets.map((item) => ( + {filteredData.map((item) => (
- {item.dataset_status} + {item.process}
@@ -71,11 +161,6 @@ export default function ViewsAdminHome() { {item.table_title}

-

- Kategori:{" "} - {item.topic_category} -

-

Organisasi:{" "} {item.organization_name} @@ -134,9 +219,9 @@ export default function ViewsAdminHome() { {/* MORE MENU */} - +

diff --git a/src/pages/admin/upload/controller_admin_upload.jsx b/src/pages/admin/upload/controller_admin_upload.jsx index 7126e10..0c2fc78 100644 --- a/src/pages/admin/upload/controller_admin_upload.jsx +++ b/src/pages/admin/upload/controller_admin_upload.jsx @@ -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 }; } diff --git a/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx b/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx index 18621bd..cde9c5c 100644 --- a/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx +++ b/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx @@ -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) { diff --git a/src/pages/admin/upload/service_admin_upload.jsx b/src/pages/admin/upload/service_admin_upload.jsx index 48687ed..c1b45fa 100644 --- a/src/pages/admin/upload/service_admin_upload.jsx +++ b/src/pages/admin/upload/service_admin_upload.jsx @@ -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; + } +} diff --git a/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx b/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx index c6406da..d83a328 100644 --- a/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx +++ b/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx @@ -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){ diff --git a/src/pages/admin/upload/views_admin_success_upload.jsx b/src/pages/admin/upload/views_admin_success_upload.jsx index d0fc52e..3a6040f 100644 --- a/src/pages/admin/upload/views_admin_success_upload.jsx +++ b/src/pages/admin/upload/views_admin_success_upload.jsx @@ -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 = () => ( + + ); + const renderIcon = (status) => { + if (status === "running") return ; + if (status === "done") return "โœ”"; + if (status === "error") return "โŒ"; + return "โฌœ"; + }; + + + const [stepStatus, setStepStatus] = useState(INITIAL_STEP_STATUS); + const wsRef = useRef(null); + if (!validatedData) { return ; - }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 ( -
+

โœ… Upload Berhasil!

Data Anda berhasil disimpan ke database. @@ -93,16 +190,53 @@ export default function ViewsAdminUploadSuccess() {

)} - {validatedData.message && ( + {/* {validatedData.message && (

- {/* {validatedData.message} */} - Datasets berhasil di upload + Data sedang diproses

+ )} */} + {validatedData.message && ( +
+ {PROCESS_STEPS.map((step) => ( +
+ + + {renderIcon(stepStatus[step.key] || "-")} + + + + {step.label} + +
+ ))} +
)}
+

+ Sistem sedang melakukan cleansing data dan publikasi ke GeoServer dan GeoNetwork.
+ Anda tidak perlu menunggu di halaman ini. +

+ {/* Metadata Section */} {validatedData.metadata && (
@@ -114,13 +248,20 @@ export default function ViewsAdminUploadSuccess() { )}
- - - Kembali ke Dashboard - +
+ + Kembali ke Dashboard + + + Upload data lagi + +
); } diff --git a/src/pages/admin/upload/views_admin_upload.jsx b/src/pages/admin/upload/views_admin_upload.jsx index f922db0..4b08440 100644 --- a/src/pages/admin/upload/views_admin_upload.jsx +++ b/src/pages/admin/upload/views_admin_upload.jsx @@ -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 && (
{/* Info File */}
@@ -138,7 +145,19 @@ export default function ViewsAdminUploadStep1() { )} - +
+ + 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`} + /> +
+ {/* Tombol Upload */}
1) ? 'hidden' : 'block' }`}> - -// -//
-//
-// ); -// } - - - - - - - //
- // - //
- - - - 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 ( -
+
{/* Alerts */} {showAlert && ( setErrorMsg("")} /> -

โœ… Validasi & Konfirmasi Data

+
+
+

โœ… Validasi & Konfirmasi Data

- {/* SINGLE ACCORDION */} - - - - ๐Ÿ“„ Dataset 1 - + {/* */} - -
- {/* LEFT: tabel preview (6 kolom pada layout 12) */} -
-

๐Ÿงพ Cuplikan Data

-
-
- + {/* SINGLE ACCORDION */} + + + + ๐Ÿ“„ {result.file_name} + + + +
+ {/* LEFT: tabel preview (6 kolom pada layout 12) */} +
+

๐Ÿงพ Cuplikan Data

+
+
+ +
+
+
+ + {/* RIGHT: metadata form (6 kolom) */} +
+
+

๐Ÿงพ Info dataset

+ AI Generate +
+ + {/* MetadataForm menyimpan hasil ke parent via onChange */} + setMetadata(data)} + /> +
-
- {/* RIGHT: metadata form (6 kolom) */} -
-

๐Ÿงพ Metadata

+ {/* ACTIONS di bawah accordion content */} +
+ - {/* MetadataForm menyimpan hasil ke parent via onChange */} - setMetadata(data)}/> - -
-
- - {/* ACTIONS di bawah accordion content */} -
- - -
- {/* optional: show metadata summary brief */} - {metadata && ( -
- Metadata siap โ€” preview: {metadata.title || "-"} +
+ {/* optional: show metadata summary brief */} + {metadata && ( +
+ Metadata siap โ€” preview: {metadata.title || "-"} +
+ )} + {/* */} + + {/* */}
- )} - +
+ + + +
+ +
+

Preview Style

+
+ {showStylePreview && + + } +
+
+ +
+
+ + {/* */} + {loading ? "Mengunggah..." : "Upload ke Database"} -
+ } + /> + +
+
+
+ + {/* + + + + Style Editor + + Edit your preview and styling layers. + + + +
+
+ {showStylePreview && }
- - - + +
+ +
+
+
+
*/}
); }