From b33970db571a9e0fe4b1c6e63a462a404ac65286 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Mon, 16 Jun 2025 09:59:16 +0800 Subject: [PATCH 01/18] created my branch --- src/pages/Home.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 1b75d2e..668ec62 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -34,4 +34,5 @@ function Home() { } + export default Home \ No newline at end of file From 11d73b239546d3ca48bc697595bd88b92b8b9146 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Fri, 4 Jul 2025 16:11:20 +0800 Subject: [PATCH 02/18] Deleted Home.jsx --- src/pages/Home.jsx | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/pages/Home.jsx diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx deleted file mode 100644 index 668ec62..0000000 --- a/src/pages/Home.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Button, Text } from '@chakra-ui/react' -import React, { useEffect, useState } from 'react' -import { useSelector } from 'react-redux' -import server, { JSONResponse } from "../networking"; - -function Home() { - const { systemVersion, systemName } = useSelector((state) => state.universal) - const [healthCheckMessage, setHealthCheckMessage] = useState("Checking..."); - - const checkHealth = () => { - setHealthCheckMessage("Checking...") - server.get("/api/health") - .then(res => { - const parsedResponse = res.data - setHealthCheckMessage(parsedResponse.message) - }) - .catch(err => { - const parsedResponse = err.response.data - console.log("HEALTHCHECK ERROR: Error in checking health; response:", parsedResponse.fullMessage()) - }) - } - - useEffect(() => { - checkHealth() - }, []) - - return <> - Hello world! - {systemName} {systemVersion} - Health Check: {healthCheckMessage} - This is a link! -
- - -} - - -export default Home \ No newline at end of file From 837c73fa86b55590dfde618321f5f83cbc9a2c77 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Sat, 5 Jul 2025 17:32:40 +0800 Subject: [PATCH 03/18] Added DataImport Page --- src/main.jsx | 3 +- src/pages/DataImport.jsx | 115 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/pages/DataImport.jsx diff --git a/src/main.jsx b/src/main.jsx index eaf4bbd..202899e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -12,6 +12,7 @@ import Layout from './Layout.jsx' import MainTheme from './themes/MainTheme.js' import Health from './pages/Health.jsx' import Homepage from './pages/Homepage.jsx' +import DataImport from './pages/DataImport.jsx'; const store = configureStore({ reducer: { @@ -28,7 +29,7 @@ createRoot(document.getElementById('root')).render( } /> } /> } /> - + } /> {/* } /> */} diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx new file mode 100644 index 0000000..d87c145 --- /dev/null +++ b/src/pages/DataImport.jsx @@ -0,0 +1,115 @@ +import { Button, Text, Image, VStack, HStack, Flex, Box, Card, Spinner, Spacer, IconButton, ProgressCircle, AbsoluteCenter } from "@chakra-ui/react"; +import { IoReload } from "react-icons/io5"; +import { GoPlus } from "react-icons/go"; +import { MdOutlineRemoveRedEye } from "react-icons/md"; +import hp1 from '../assets/hp1.png'; +import React, { useState } from "react"; +import { useSelector } from "react-redux"; +import server from "../networking"; + +function DataImport() { + const imgResponsiveSizing = { base: "50px", md: "50px" } + + return ( + + + Data Processing + + + + + + New Batch + + + + + + + Vetting + + + + + + Batch #1 + + 105/456 Images Confirmed | Yesterday, 13:10 + + + 23% + + + + + + + + + Processing + + + + + + Batch #2 + + 105/456 Images Confirmed | Yesterday, 13:10 + + + 23% + + + + + + + + + + + + + + Bird's Eye View + + + + + + + + + + + + 291/561 Images Processed + + + Meeting Minutes + + 145 + + + Event Photos + + 146 + + + ETA + + 34 mins, 23 seconds + + + + + + + ); +} + +export default DataImport; \ No newline at end of file From d0b9cc38c35849fac028acc65c71ada43a322221 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Fri, 11 Jul 2025 17:12:45 +0800 Subject: [PATCH 04/18] remove dataimport link to sync up --- src/main.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.jsx b/src/main.jsx index 202899e..eaf4bbd 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -12,7 +12,6 @@ import Layout from './Layout.jsx' import MainTheme from './themes/MainTheme.js' import Health from './pages/Health.jsx' import Homepage from './pages/Homepage.jsx' -import DataImport from './pages/DataImport.jsx'; const store = configureStore({ reducer: { @@ -29,7 +28,7 @@ createRoot(document.getElementById('root')).render( } /> } /> } /> - } /> + {/* } /> */} From 54c5c90aac29985ef3acb0e4f99d84292ff50594 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Tue, 15 Jul 2025 00:39:59 +0800 Subject: [PATCH 05/18] Made BatchCard Component for batch --- src/components/BatchCard.jsx | 32 +++++++++ src/main.jsx | 2 + src/pages/DataImport.jsx | 132 ++++++++++++++++++++++++----------- 3 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 src/components/BatchCard.jsx diff --git a/src/components/BatchCard.jsx b/src/components/BatchCard.jsx new file mode 100644 index 0000000..f9a0114 --- /dev/null +++ b/src/components/BatchCard.jsx @@ -0,0 +1,32 @@ +import { HStack, VStack, Text, Image, Button, Spinner, Card } from "@chakra-ui/react"; + +function BatchCard({ batchName, confirmed, total, timestamp, progress, isProcessing, thumbnail, onClick }) { + return ( + + + + + + {batchName} + + {confirmed}/{total} Images Confirmed | {timestamp} + + + {progress}% + {isProcessing ? ( + + ) : ( + Done + )} + + + + + ); +} + +export default BatchCard; + + diff --git a/src/main.jsx b/src/main.jsx index 32e7b94..1ffd9cc 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -19,6 +19,7 @@ import DefaultLayout from './DefaultLayout.jsx'; import SampleProtected from './pages/SampleProtected.jsx'; import ProtectedLayout from './ProtectedLayout.jsx'; import AnimateIn from './AnimateIn.jsx'; +import DataImport from './pages/DataImport.jsx'; const store = configureStore({ reducer: { @@ -47,6 +48,7 @@ createRoot(document.getElementById('root')).render( {/* Protected Pages */} }> + } /> } /> diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index d87c145..3574be8 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -4,11 +4,77 @@ import { GoPlus } from "react-icons/go"; import { MdOutlineRemoveRedEye } from "react-icons/md"; import hp1 from '../assets/hp1.png'; import React, { useState } from "react"; -import { useSelector } from "react-redux"; +import ToastWizard from '../components/toastWizard' import server from "../networking"; +import BatchCard from "../components/BatchCard"; function DataImport() { const imgResponsiveSizing = { base: "50px", md: "50px" } + const fileInputRef = React.useRef(null); + const [uploading, setUploading] = useState(false); + + const handleUpload = async (e) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + const formData = new FormData(); + for (let file of files) { + formData.append("file", file); + } + + for (let [key, val] of formData.entries()) { + console.log(`${key}:`, val); + } + + setUploading(true); + try { + const res = await server.post("/upload/", formData, { + withCredentials: true, + }); + + if (res.data?.type === "ERROR") { + ToastWizard.standard("error", "Upload failed.", res.data.message || "Something went wrong. Please try again."); + } else { + ToastWizard.standard("success", "Upload successful", res.data.message || "Your batch has been uploaded."); + } + } catch (err) { + if (err.response) { + console.error("Upload error response data:", err.response.data); + ToastWizard.standard("error", "Upload failed.", err.response.data.message || "Unknown error from server."); + } else { + console.error("Upload error:", err); + ToastWizard.standard("error", "Unexpected Error", "An unexpected error occurred during upload."); + } + } finally { + setUploading(false); + } + }; + + const batches = [ + { + batchName: "Batch #1", + confirmed: 105, + total: 456, + timestamp: "Yesterday, 13:10", + progress: 23, + isProcessing: true, + thumbnail: hp1, + onClick: () => console.log("Continue Batch #1") + }, + { + batchName: "Batch #2", + confirmed: 300, + total: 300, + timestamp: "Today, 09:22", + progress: 100, + isProcessing: false, + thumbnail: hp1, + onClick: () => console.log("Details Batch #2") + }, + ]; + + const vettingBatches = batches.filter(b => b.isProcessing); + const completedBatches = batches.filter(b => !b.isProcessing); return ( @@ -18,55 +84,43 @@ function DataImport() { - + fileInputRef.current?.click()} + isLoading={uploading} + > New Batch + + + + - + Vetting - - - - - - Batch #1 - - 105/456 Images Confirmed | Yesterday, 13:10 - - - 23% - - - - - + {vettingBatches.map((batch, idx) => ( + + ))} Processing - - - - - - Batch #2 - - 105/456 Images Confirmed | Yesterday, 13:10 - - - 23% - - - - - + {completedBatches.map((batch, idx) => ( + + ))} From 546ddcbe1a0839a9662a0cedce6c8e36b918a6c2 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Thu, 31 Jul 2025 18:03:29 +0800 Subject: [PATCH 06/18] Fetch batches and used BatchCard component for each batch --- src/components/{ => DataImport}/BatchCard.jsx | 25 ++- src/components/DataImport/DialogUpload.jsx | 45 +++++ src/pages/DataImport.jsx | 179 +++++++++--------- 3 files changed, 150 insertions(+), 99 deletions(-) rename src/components/{ => DataImport}/BatchCard.jsx (50%) create mode 100644 src/components/DataImport/DialogUpload.jsx diff --git a/src/components/BatchCard.jsx b/src/components/DataImport/BatchCard.jsx similarity index 50% rename from src/components/BatchCard.jsx rename to src/components/DataImport/BatchCard.jsx index f9a0114..7ef1239 100644 --- a/src/components/BatchCard.jsx +++ b/src/components/DataImport/BatchCard.jsx @@ -1,25 +1,38 @@ import { HStack, VStack, Text, Image, Button, Spinner, Card } from "@chakra-ui/react"; -function BatchCard({ batchName, confirmed, total, timestamp, progress, isProcessing, thumbnail, onClick }) { +function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessing, isCancelled, thumbnail, onClick }) { return ( - + {batchName} - {confirmed}/{total} Images Confirmed | {timestamp} + {displayMessage} | {timestamp} + {progress}% - {isProcessing ? ( + + {isCancelled ? ( + + Cancelled + + ) : isProcessing ? ( ) : ( - Done + + Done + )} + diff --git a/src/components/DataImport/DialogUpload.jsx b/src/components/DataImport/DialogUpload.jsx new file mode 100644 index 0000000..412d168 --- /dev/null +++ b/src/components/DataImport/DialogUpload.jsx @@ -0,0 +1,45 @@ +import { Button, CloseButton, FileUpload, Dialog, Portal } from "@chakra-ui/react" +import { HiUpload } from "react-icons/hi" + +function DialogUpload() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default DialogUpload \ No newline at end of file diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 3574be8..274a75d 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -1,80 +1,58 @@ -import { Button, Text, Image, VStack, HStack, Flex, Box, Card, Spinner, Spacer, IconButton, ProgressCircle, AbsoluteCenter } from "@chakra-ui/react"; +import { VStack, HStack, Flex, Box, Card, Text, Center, Spacer, IconButton, ProgressCircle, AbsoluteCenter, Dialog } from "@chakra-ui/react"; import { IoReload } from "react-icons/io5"; import { GoPlus } from "react-icons/go"; import { MdOutlineRemoveRedEye } from "react-icons/md"; import hp1 from '../assets/hp1.png'; -import React, { useState } from "react"; +import { useEffect, useState } from "react"; import ToastWizard from '../components/toastWizard' import server from "../networking"; -import BatchCard from "../components/BatchCard"; +import BatchCard from "../components/DataImport/BatchCard"; +import DialogUpload from "../components/DataImport/DialogUpload"; +import CentredSpinner from "../components/centredSpinner.jsx"; function DataImport() { - const imgResponsiveSizing = { base: "50px", md: "50px" } - const fileInputRef = React.useRef(null); - const [uploading, setUploading] = useState(false); + const [batches, setBatches] = useState([]); + const [loading, setLoading] = useState(true); - const handleUpload = async (e) => { - const files = e.target.files; - if (!files || files.length === 0) return; - - const formData = new FormData(); - for (let file of files) { - formData.append("file", file); - } + async function fetchBatches() { + try { + const res = await server.get("/dataImport/batches"); + const data = res.data; + console.log("API response:", res.data); - for (let [key, val] of formData.entries()) { - console.log(`${key}:`, val); - } + if (!data || typeof data !== "object" || !data.raw.batches) { + ToastWizard.standard("error", "Invalid response", "Failed to load batches"); + return; + } - setUploading(true); - try { - const res = await server.post("/upload/", formData, { - withCredentials: true, + const rawBatches = data.raw.batches; + const batchList = Object.entries(rawBatches).map(([id, batch]) => { + const artefacts = batch.artefacts || {}; + const total = Object.keys(artefacts).length; + const processed = Object.values(artefacts).filter(a => a.stage === "processed").length; + + return { + id, + ...batch, + artefactSummary: { processed, total }, + }; }); - if (res.data?.type === "ERROR") { - ToastWizard.standard("error", "Upload failed.", res.data.message || "Something went wrong. Please try again."); - } else { - ToastWizard.standard("success", "Upload successful", res.data.message || "Your batch has been uploaded."); - } + setBatches(batchList); } catch (err) { - if (err.response) { - console.error("Upload error response data:", err.response.data); - ToastWizard.standard("error", "Upload failed.", err.response.data.message || "Unknown error from server."); - } else { - console.error("Upload error:", err); - ToastWizard.standard("error", "Unexpected Error", "An unexpected error occurred during upload."); - } + ToastWizard.standard("error", "Error fetching batches", "Please try again later"); } finally { - setUploading(false); + setLoading(false); } - }; + } - const batches = [ - { - batchName: "Batch #1", - confirmed: 105, - total: 456, - timestamp: "Yesterday, 13:10", - progress: 23, - isProcessing: true, - thumbnail: hp1, - onClick: () => console.log("Continue Batch #1") - }, - { - batchName: "Batch #2", - confirmed: 300, - total: 300, - timestamp: "Today, 09:22", - progress: 100, - isProcessing: false, - thumbnail: hp1, - onClick: () => console.log("Details Batch #2") - }, - ]; + useEffect(() => { + fetchBatches(); + }, []); - const vettingBatches = batches.filter(b => b.isProcessing); - const completedBatches = batches.filter(b => !b.isProcessing); + if (loading) { + return ; + } return ( @@ -84,46 +62,61 @@ function DataImport() { - fileInputRef.current?.click()} - isLoading={uploading} - > - New Batch - - - - - + - - - Vetting - {vettingBatches.map((batch, idx) => ( - + + Vetting + {batches + .filter(batch => batch.job?.status === "completed") + .map((batch, idx) => ( + console.log("Clicked batch", batch.id)} + /> ))} - - - Processing - {completedBatches.map((batch, idx) => ( - - ))} - - + Processing + {batches + .filter(batch => batch.job?.status !== "completed") + .map((batch, idx) => { + const isCancelled = batch.job?.status === "cancelled"; + const isProcessing = batch.job?.status === "processing"; + const processed = batch.artefactSummary.processed || 0; + const total = batch.artefactSummary.total || 1; + const progress = Math.round((processed / total) * 100); + + return ( + console.log("Clicked batch", batch.id)} + /> + ); + })} + From f904608e0688be525aaf34e681bc8a9cc75be557 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Fri, 1 Aug 2025 03:54:13 +0800 Subject: [PATCH 07/18] Modified DataImport to count len of MM/HF Modified DialogUpload with upload api and shows error --- src/components/DataImport/BatchCard.jsx | 7 +- src/components/DataImport/DialogUpload.jsx | 150 +++++++++++++++++++-- src/pages/DataImport.jsx | 33 ++++- 3 files changed, 168 insertions(+), 22 deletions(-) diff --git a/src/components/DataImport/BatchCard.jsx b/src/components/DataImport/BatchCard.jsx index 7ef1239..c4c1fd6 100644 --- a/src/components/DataImport/BatchCard.jsx +++ b/src/components/DataImport/BatchCard.jsx @@ -1,6 +1,6 @@ import { HStack, VStack, Text, Image, Button, Spinner, Card } from "@chakra-ui/react"; -function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessing, isCancelled, thumbnail, onClick }) { +function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessing, isCancelled, isCompleted, thumbnail, onClick }) { return ( @@ -8,7 +8,7 @@ function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessin {batchName} @@ -32,8 +32,9 @@ function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessin )} + diff --git a/src/components/DataImport/DialogUpload.jsx b/src/components/DataImport/DialogUpload.jsx index 412d168..6690fdc 100644 --- a/src/components/DataImport/DialogUpload.jsx +++ b/src/components/DataImport/DialogUpload.jsx @@ -1,11 +1,65 @@ -import { Button, CloseButton, FileUpload, Dialog, Portal } from "@chakra-ui/react" -import { HiUpload } from "react-icons/hi" +import { Button, FileUpload, VStack, Text, Box, CloseButton, Dialog, Portal, Badge, Spacer } from "@chakra-ui/react"; +import { HiUpload } from "react-icons/hi"; +import { useState, useRef } from "react"; +import server from "../../networking"; +import ToastWizard from '../../components/toastWizard' function DialogUpload() { + const [files, setFiles] = useState([]); + const [fileStatuses, setFileStatuses] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const inputRef = useRef(); + const isUploading = useRef(false); + + const handleFileChange = async (e) => { + // prevent double submission + if (isUploading.current) return; + isUploading.current = true; + + const selected = Array.from(e.target.files); + setFiles(selected); + setFileStatuses({}); + + if (selected.length === 0) return; + + const formData = new FormData(); + selected.forEach((file) => formData.append("file", file)); + + setIsSubmitting(true); + try { + const res = await server.post("/dataImport/upload", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + const data = res.data; + + setFileStatuses(data.raw.updates || {}); + + const batchID = data.raw?.batchID || "Unknown"; + ToastWizard.standard( + "success", + "Upload complete", + `Batch ID: ${batchID}` + ); + + } catch (err) { + const updates = err?.response?.data?.raw?.updates; + if (updates) { + setFileStatuses(updates); + } + + ToastWizard.standard("error", "Upload error", "Failed to upload files."); + + } finally { + setIsSubmitting(false); + isUploading.current = false; + } + }; + return ( - + - @@ -19,27 +73,99 @@ function DialogUpload() { + - + {/* Hidden input to capture files */} + + + {/* Custom trigger button */} - - + + {/* Custom file list with statuses */} + {files.length > 0 && ( + + {files.map((file) => { + const status = fileStatuses[file.name]; + return ( + + + + {file.name} + + + {(file.size / 1024).toFixed(1)} KB + + + + {status && ( + + {status.startsWith("ERROR") && ( + + Error + + )} + + {status} + + + + { + setFiles(prev => prev.filter(f => f.name !== file.name)); + setFileStatuses(prev => { + const updated = { ...prev }; + delete updated[file.name]; + return updated; + }); + }} + /> + + )} + + ); + })} + + )} + - - - + ); -}; +} -export default DialogUpload \ No newline at end of file +export default DialogUpload; \ No newline at end of file diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 274a75d..f7d18a2 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -13,6 +13,12 @@ import CentredSpinner from "../components/centredSpinner.jsx"; function DataImport() { const [batches, setBatches] = useState([]); const [loading, setLoading] = useState(true); + const [mmCount, setMmCount] = useState(0); + const [hfCount, setHfCount] = useState(0); + + const processedTotal = batches + .filter(b => b.job?.status !== "completed") + .reduce((sum, b) => sum + (b.artefactSummary?.processed || 0), 0); async function fetchBatches() { try { @@ -38,7 +44,12 @@ function DataImport() { }; }); + const mmTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); + const hfTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); + setBatches(batchList); + setMmCount(mmTotal); + setHfCount(hfTotal); } catch (err) { ToastWizard.standard("error", "Error fetching batches", "Please try again later"); } finally { @@ -62,7 +73,7 @@ function DataImport() { - + @@ -82,6 +93,7 @@ function DataImport() { }).toUpperCase()} progress={0} isProcessing={true} + isCompleted={true} thumbnail={hp1} onClick={() => console.log("Clicked batch", batch.id)} /> @@ -111,6 +123,7 @@ function DataImport() { progress={progress} isProcessing={isProcessing} isCancelled={isCancelled} + isCompleted={false} thumbnail={hp1} onClick={() => console.log("Clicked batch", batch.id)} /> @@ -124,8 +137,13 @@ function DataImport() { Bird's Eye View + - + @@ -134,17 +152,18 @@ function DataImport() { - 291/561 Images Processed + {processedTotal}/{mmCount + hfCount} Images Processed - Meeting Minutes + Meeting Minutes - 145 + {mmCount} + - Event Photos + Event Photos - 146 + {hfCount} ETA From bc2c70eabaf777065341a17a9ceb7e7761664e58 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Fri, 1 Aug 2025 05:05:08 +0800 Subject: [PATCH 08/18] Added Confirm api --- src/components/DataImport/DialogUpload.jsx | 146 ++++++++++++++++----- 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/src/components/DataImport/DialogUpload.jsx b/src/components/DataImport/DialogUpload.jsx index 6690fdc..e44cc80 100644 --- a/src/components/DataImport/DialogUpload.jsx +++ b/src/components/DataImport/DialogUpload.jsx @@ -1,25 +1,27 @@ -import { Button, FileUpload, VStack, Text, Box, CloseButton, Dialog, Portal, Badge, Spacer } from "@chakra-ui/react"; +import { Button, FileUpload, VStack, Text, Box, CloseButton, Dialog, Portal, Badge } from "@chakra-ui/react"; import { HiUpload } from "react-icons/hi"; import { useState, useRef } from "react"; import server from "../../networking"; -import ToastWizard from '../../components/toastWizard' +import ToastWizard from "../../components/toastWizard"; function DialogUpload() { const [files, setFiles] = useState([]); const [fileStatuses, setFileStatuses] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); + const [batchID, setBatchID] = useState(null); + const [isConfirmed, setIsConfirmed] = useState(false); + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const inputRef = useRef(); const isUploading = useRef(false); + // Handle file upload const handleFileChange = async (e) => { - // prevent double submission if (isUploading.current) return; isUploading.current = true; const selected = Array.from(e.target.files); setFiles(selected); setFileStatuses({}); - if (selected.length === 0) return; const formData = new FormData(); @@ -32,34 +34,54 @@ function DialogUpload() { }); const data = res.data; - - setFileStatuses(data.raw.updates || {}); + const updates = data.raw?.updates || {}; + const newBatchID = data.raw?.batchID; - const batchID = data.raw?.batchID || "Unknown"; - ToastWizard.standard( - "success", - "Upload complete", - `Batch ID: ${batchID}` - ); + setFileStatuses(updates); + setBatchID(newBatchID); + setIsConfirmed(false); + ToastWizard.standard("success","Upload Complete",`Batch ID: ${newBatchID}. Ready for confirmation.`); } catch (err) { const updates = err?.response?.data?.raw?.updates; if (updates) { setFileStatuses(updates); } + ToastWizard.standard("error", "Upload Failed", "Failed to upload files."); + } finally { + setIsSubmitting(false); + isUploading.current = false; + } + }; + + const handleConfirmBatch = async () => { + if (!batchID) return; - ToastWizard.standard("error", "Upload error", "Failed to upload files."); + setIsSubmitting(true); + try { + const res = await server.post("/dataImport/confirm", { batchID }); + + if (res.status === 200 || res.data.raw.success !== false) { + setIsConfirmed(true); + ToastWizard.standard("success","Processing Started",`Batch ${batchID} is now being processed.`); + setIsConfirmDialogOpen(false); + + } else { + ToastWizard.standard("error","Failed to Confirm",res.data.message || "Unknown error."); + } + } catch (err) { + const errorMsg = err?.response?.data?.message || "Failed to confirm batch."; + ToastWizard.standard("error", "Confirm Error", errorMsg); } finally { setIsSubmitting(false); - isUploading.current = false; } }; return ( - @@ -69,6 +91,7 @@ function DialogUpload() { + Upload Files @@ -76,10 +99,7 @@ function DialogUpload() { - {/* Hidden input to capture files */} - - {/* Custom trigger button */} + + {/* Only show Confirm button after upload */} + {batchID && !isConfirmed && ( + + )} + + {isConfirmed && ( + + Processing started for Batch {batchID} + + )} + + setIsConfirmDialogOpen(open)}> + + + + + Confirm Processing + + + Are you sure you want to process batch{" "} + + {batchID} + + ? This will start processing the artefacts in the batch. + + + + + + + + + + ); } From 73a1101367ad485389bbd59dc940f97cacaa8446 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Fri, 1 Aug 2025 18:09:02 +0800 Subject: [PATCH 09/18] Fixed UI and error handling --- src/components/DataImport/BatchCard.jsx | 42 +++-- src/components/DataImport/DialogUpload.jsx | 10 +- src/networking.js | 8 +- src/pages/DataImport.jsx | 198 +++++++++++++-------- 4 files changed, 163 insertions(+), 95 deletions(-) diff --git a/src/components/DataImport/BatchCard.jsx b/src/components/DataImport/BatchCard.jsx index c4c1fd6..7b1bea1 100644 --- a/src/components/DataImport/BatchCard.jsx +++ b/src/components/DataImport/BatchCard.jsx @@ -1,46 +1,58 @@ -import { HStack, VStack, Text, Image, Button, Spinner, Card } from "@chakra-ui/react"; +import { HStack, VStack, Text, Image, Button, Card, Progress, Box, Spacer } from "@chakra-ui/react"; function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessing, isCancelled, isCompleted, thumbnail, onClick }) { return ( - + + {/* Thumbnail */} - - {batchName} - + + {/* Info Texts */} + + {batchName} + {displayMessage} | {timestamp} - {progress}% - + {/* Progress Bar beside the button */} + + + + + + {progress}% + + + + {/* Status Text below */} {isCancelled ? ( - + Cancelled ) : isProcessing ? ( - + + Processing + ) : ( - + Done )} + {/* Button */} - ); } -export default BatchCard; - - +export default BatchCard; \ No newline at end of file diff --git a/src/components/DataImport/DialogUpload.jsx b/src/components/DataImport/DialogUpload.jsx index e44cc80..b72a79e 100644 --- a/src/components/DataImport/DialogUpload.jsx +++ b/src/components/DataImport/DialogUpload.jsx @@ -41,7 +41,7 @@ function DialogUpload() { setBatchID(newBatchID); setIsConfirmed(false); - ToastWizard.standard("success","Upload Complete",`Batch ID: ${newBatchID}. Ready for confirmation.`); + ToastWizard.standard("success", "Upload Complete", `Batch ID: ${newBatchID}. Ready for confirmation.`); } catch (err) { const updates = err?.response?.data?.raw?.updates; if (updates) { @@ -63,11 +63,11 @@ function DialogUpload() { if (res.status === 200 || res.data.raw.success !== false) { setIsConfirmed(true); - ToastWizard.standard("success","Processing Started",`Batch ${batchID} is now being processed.`); + ToastWizard.standard("success", "Processing Started", `Batch ${batchID} is now being processed.`); setIsConfirmDialogOpen(false); - + } else { - ToastWizard.standard("error","Failed to Confirm",res.data.message || "Unknown error."); + ToastWizard.standard("error", "Failed to Confirm", res.data.message || "Unknown error."); } } catch (err) { const errorMsg = err?.response?.data?.message || "Failed to confirm batch."; @@ -81,7 +81,7 @@ function DialogUpload() { return ( - diff --git a/src/networking.js b/src/networking.js index 9225523..550c733 100644 --- a/src/networking.js +++ b/src/networking.js @@ -42,10 +42,14 @@ const instance = axios.create({ }) instance.interceptors.request.use((config) => { - if (config.method == 'post') { + // Only set Content-Type to application/json if data is not FormData + if (config.method === 'post' && !(config.data instanceof FormData)) { config.headers["Content-Type"] = "application/json"; + } else { + // If sending FormData, remove Content-Type so browser can set it + delete config.headers["Content-Type"]; } - + config.headers["APIKey"] = import.meta.env.VITE_BACKEND_API_KEY; config.withCredentials = true; diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index f7d18a2..c462a65 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -5,11 +5,45 @@ import { MdOutlineRemoveRedEye } from "react-icons/md"; import hp1 from '../assets/hp1.png'; import { useEffect, useState } from "react"; import ToastWizard from '../components/toastWizard' -import server from "../networking"; +import server, { JSONResponse } from "../networking"; import BatchCard from "../components/DataImport/BatchCard"; import DialogUpload from "../components/DataImport/DialogUpload"; import CentredSpinner from "../components/centredSpinner.jsx"; +function formatTimestamp(dateString) { + const created = new Date(dateString); + const now = new Date(); + + const isSameDay = (d1, d2) => + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); + + const yesterday = new Date(now); + yesterday.setDate(now.getDate() - 1); + + let dateLabel; + if (isSameDay(created, now)) { + dateLabel = "Today"; + } else if (isSameDay(created, yesterday)) { + dateLabel = "Yesterday"; + } else { + dateLabel = created.toLocaleDateString("en-SG", { + day: "2-digit", + month: "short", + year: "numeric", + }).toUpperCase(); + } + + const timeLabel = created.toLocaleTimeString("en-SG", { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }).toUpperCase(); + + return `${dateLabel}, ${timeLabel}`; +} + function DataImport() { const [batches, setBatches] = useState([]); const [loading, setLoading] = useState(true); @@ -21,37 +55,64 @@ function DataImport() { .reduce((sum, b) => sum + (b.artefactSummary?.processed || 0), 0); async function fetchBatches() { - try { - const res = await server.get("/dataImport/batches"); - const data = res.data; - console.log("API response:", res.data); + const ambiguousErrorToast = () => { + ToastWizard.standard("error", "Failed to load batches.", "Something went wrong. Please try again."); + } - if (!data || typeof data !== "object" || !data.raw.batches) { - ToastWizard.standard("error", "Invalid response", "Failed to load batches"); - return; + try { + const response = await server.get("/dataImport/batches"); + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw errObject; + } + + const data = response.data; + const rawBatches = data.raw?.batches; + + if (!rawBatches) { + ambiguousErrorToast(); + return; + } + + const batchList = Object.entries(rawBatches).map(([id, batch]) => { + const artefacts = batch.artefacts || {}; + const total = Object.keys(artefacts).length; + const processed = Object.values(artefacts).filter(a => a.stage === "processed").length; + + return { + id, + ...batch, + artefactSummary: { processed, total }, + }; + }); + + const mmTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); + const hfTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); + + setBatches(batchList); + setMmCount(mmTotal); + setHfCount(hfTotal); + } else { + throw new Error("Unexpected response format"); } - - const rawBatches = data.raw.batches; - const batchList = Object.entries(rawBatches).map(([id, batch]) => { - const artefacts = batch.artefacts || {}; - const total = Object.keys(artefacts).length; - const processed = Object.values(artefacts).filter(a => a.stage === "processed").length; - - return { - id, - ...batch, - artefactSummary: { processed, total }, - }; - }); - - const mmTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); - const hfTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); - - setBatches(batchList); - setMmCount(mmTotal); - setHfCount(hfTotal); } catch (err) { - ToastWizard.standard("error", "Error fetching batches", "Please try again later"); + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in fetchBatches:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Failed to load batches", err.response.data.message); + } else { + ambiguousErrorToast(); + } + } else { + console.log("Unexpected error in fetchBatches:", err); + ambiguousErrorToast(); + } } finally { setLoading(false); } @@ -67,30 +128,28 @@ function DataImport() { return ( - + Data Processing - + - + + + - + - Vetting + Data Studio {batches .filter(batch => batch.job?.status === "completed") .map((batch, idx) => ( Processing + Processing {batches .filter(batch => batch.job?.status !== "completed") .map((batch, idx) => { @@ -114,12 +173,8 @@ function DataImport() { - - + + - + Bird's Eye View - - - - - - - - - - - {processedTotal}/{mmCount + hfCount} Images Processed + + + + + + + + + + + + + {processedTotal}/{mmCount + hfCount} Images Processed - - Meeting Minutes + + Meeting Minutes {mmCount} - Event Photos + Event Photos {hfCount} - - ETA - - 34 mins, 23 seconds - From c65b4a6e8b520c3842a9722014db92f0a2e37af0 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Sat, 2 Aug 2025 00:54:30 +0800 Subject: [PATCH 10/18] Fetch different stages of batch --- src/pages/DataImport.jsx | 109 ++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index c462a65..7234852 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -49,10 +49,18 @@ function DataImport() { const [loading, setLoading] = useState(true); const [mmCount, setMmCount] = useState(0); const [hfCount, setHfCount] = useState(0); + const [processedTotal, setProcessedTotal] = useState(0); - const processedTotal = batches - .filter(b => b.job?.status !== "completed") - .reduce((sum, b) => sum + (b.artefactSummary?.processed || 0), 0); + const stages = ["upload_pending", "unprocessed", "processed", "vetting", "integration", "completed"]; + + const stageLabels = { + upload_pending: "Pending", + unprocessed: "Unprocessed", + processed: "Processed", + vetting: "Vetting", + integration: "Integration", + completed: "Completed", + }; async function fetchBatches() { const ambiguousErrorToast = () => { @@ -74,6 +82,7 @@ function DataImport() { const data = response.data; const rawBatches = data.raw?.batches; + console.log(rawBatches) if (!rawBatches) { ambiguousErrorToast(); @@ -81,21 +90,25 @@ function DataImport() { } const batchList = Object.entries(rawBatches).map(([id, batch]) => { - const artefacts = batch.artefacts || {}; - const total = Object.keys(artefacts).length; - const processed = Object.values(artefacts).filter(a => a.stage === "processed").length; + const artefacts = Object.values(batch.artefacts || {}); + const processed = artefacts.filter(a => a.stage === "processed").length; + const unprocessed = artefacts.filter(a => a.stage === "unprocessed").length; + const total = processed + unprocessed return { id, ...batch, artefactSummary: { processed, total }, + artefactTypeSummary: batch.artefactTypeSummary || { mm: 0, hf: 0 } }; }); - const mmTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); - const hfTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); + const processedTotalValue = batchList.reduce((sum, b) => sum + (b.artefactSummary?.processed || 0), 0); + const mmTotal = batches.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); + const hfTotal = batches.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); setBatches(batchList); + setProcessedTotal(processedTotalValue); setMmCount(mmTotal); setHfCount(hfTotal); } else { @@ -126,6 +139,9 @@ function DataImport() { return ; } + const totalCount = batches.reduce((sum, b) => sum + b.artefactSummary.total, 0); // only processed + unprocessed + const percent = totalCount === 0 ? 0 : Math.round((processedTotal / totalCount) * 100); + return ( @@ -141,49 +157,36 @@ function DataImport() { - Data Studio - {batches - .filter(batch => batch.job?.status === "completed") - .map((batch, idx) => ( - console.log("Clicked batch", batch.id)} - /> - ))} + + {stages.map(stage => ( + + {stageLabels[stage]} + + {batches + .filter(batch => batch.stage === stage) + .map((batch, idx) => ( + console.log("Clicked batch", batch.id)} + /> + ))} + + ))} + - Processing - {batches - .filter(batch => batch.job?.status !== "completed") - .map((batch, idx) => { - const isCancelled = batch.job?.status === "cancelled"; - const isProcessing = batch.job?.status === "processing"; - const processed = batch.artefactSummary.processed || 0; - const total = batch.artefactSummary.total || 1; - const progress = Math.round((processed / total) * 100); - - return ( - console.log("Clicked batch", batch.id)} - /> - ); - })} @@ -193,12 +196,13 @@ function DataImport() { Bird's Eye View - + @@ -209,8 +213,9 @@ function DataImport() { - {processedTotal}/{mmCount + hfCount} Images Processed + {processedTotal}/{totalCount} Images Processed + Meeting Minutes From 3a729d205fc5ffb5aaae64729f94ff54521af92e Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Sat, 2 Aug 2025 04:09:36 +0800 Subject: [PATCH 11/18] Fixed batch to show cancelled batch --- src/pages/DataImport.jsx | 55 ++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 7234852..9d98883 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -82,8 +82,6 @@ function DataImport() { const data = response.data; const rawBatches = data.raw?.batches; - console.log(rawBatches) - if (!rawBatches) { ambiguousErrorToast(); return; @@ -104,8 +102,8 @@ function DataImport() { }); const processedTotalValue = batchList.reduce((sum, b) => sum + (b.artefactSummary?.processed || 0), 0); - const mmTotal = batches.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); - const hfTotal = batches.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); + const mmTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); + const hfTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); setBatches(batchList); setProcessedTotal(processedTotalValue); @@ -163,26 +161,35 @@ function DataImport() { {stageLabels[stage]} - {batches - .filter(batch => batch.stage === stage) - .map((batch, idx) => ( - console.log("Clicked batch", batch.id)} - /> - ))} + {(() => { + const filtered = batches.filter(batch => batch.stage === stage); + if (filtered.length === 0) { + return ( + + There are no batches that are in this {stageLabels[stage].toLowerCase()} stage currently. + + ); + } + return filtered.map((batch, idx) => ( + + console.log("Clicked batch", batch.id)} + /> + + )); + })()} ))} From a37370b5474d26c376911182e7fad2910e86511e Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 4 Aug 2025 20:30:06 +0800 Subject: [PATCH 12/18] changed automatic content-type setting for POST requests applicable only if one isn't already set --- src/networking.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/networking.js b/src/networking.js index 550c733..42a63e1 100644 --- a/src/networking.js +++ b/src/networking.js @@ -42,12 +42,8 @@ const instance = axios.create({ }) instance.interceptors.request.use((config) => { - // Only set Content-Type to application/json if data is not FormData - if (config.method === 'post' && !(config.data instanceof FormData)) { + if (config.method === 'post' && !config.headers.get('Content-Type')) { config.headers["Content-Type"] = "application/json"; - } else { - // If sending FormData, remove Content-Type so browser can set it - delete config.headers["Content-Type"]; } config.headers["APIKey"] = import.meta.env.VITE_BACKEND_API_KEY; From cadcb179a9ebd89c176ba732af035325f2098487 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Sat, 9 Aug 2025 23:10:06 +0800 Subject: [PATCH 13/18] Removed Bird's Eye View --- src/pages/DataImport.jsx | 67 +++++----------------------------------- 1 file changed, 8 insertions(+), 59 deletions(-) diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 9d98883..d0b0e54 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -4,6 +4,7 @@ import { GoPlus } from "react-icons/go"; import { MdOutlineRemoveRedEye } from "react-icons/md"; import hp1 from '../assets/hp1.png'; import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; import ToastWizard from '../components/toastWizard' import server, { JSONResponse } from "../networking"; import BatchCard from "../components/DataImport/BatchCard"; @@ -47,9 +48,8 @@ function formatTimestamp(dateString) { function DataImport() { const [batches, setBatches] = useState([]); const [loading, setLoading] = useState(true); - const [mmCount, setMmCount] = useState(0); - const [hfCount, setHfCount] = useState(0); - const [processedTotal, setProcessedTotal] = useState(0); + + const { loaded } = useSelector(state => state.auth); const stages = ["upload_pending", "unprocessed", "processed", "vetting", "integration", "completed"]; @@ -97,18 +97,11 @@ function DataImport() { id, ...batch, artefactSummary: { processed, total }, - artefactTypeSummary: batch.artefactTypeSummary || { mm: 0, hf: 0 } }; }); - const processedTotalValue = batchList.reduce((sum, b) => sum + (b.artefactSummary?.processed || 0), 0); - const mmTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.mm || 0), 0); - const hfTotal = batchList.reduce((sum, b) => sum + (b.artefactTypeSummary?.hf || 0), 0); - setBatches(batchList); - setProcessedTotal(processedTotalValue); - setMmCount(mmTotal); - setHfCount(hfTotal); + } else { throw new Error("Unexpected response format"); } @@ -130,16 +123,15 @@ function DataImport() { } useEffect(() => { - fetchBatches(); - }, []); + if (loaded) { + fetchBatches(); + } + }, [loaded]); if (loading) { return ; } - const totalCount = batches.reduce((sum, b) => sum + b.artefactSummary.total, 0); // only processed + unprocessed - const percent = totalCount === 0 ? 0 : Math.round((processedTotal / totalCount) * 100); - return ( @@ -193,50 +185,7 @@ function DataImport() { ))} - - - - - - - Bird's Eye View - - - - - - - - - - - - - - - {processedTotal}/{totalCount} Images Processed - - - - Meeting Minutes - - {mmCount} - - - - Event Photos - - {hfCount} - - - - ); From 35fb8f3558873bcc1959471e845408de5b5d87af Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Sun, 10 Aug 2025 00:43:08 +0800 Subject: [PATCH 14/18] Added Batch stages filter multiselect bar --- src/pages/DataImport.jsx | 155 ++++++++++++++++++++++++++------------- 1 file changed, 104 insertions(+), 51 deletions(-) diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index d0b0e54..5f7de1f 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -1,9 +1,7 @@ -import { VStack, HStack, Flex, Box, Card, Text, Center, Spacer, IconButton, ProgressCircle, AbsoluteCenter, Dialog } from "@chakra-ui/react"; +import { VStack, HStack, Flex, Box, Select, Portal, Text, Spacer, IconButton, createListCollection } from "@chakra-ui/react"; import { IoReload } from "react-icons/io5"; -import { GoPlus } from "react-icons/go"; -import { MdOutlineRemoveRedEye } from "react-icons/md"; import hp1 from '../assets/hp1.png'; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useSelector } from "react-redux"; import ToastWizard from '../components/toastWizard' import server, { JSONResponse } from "../networking"; @@ -11,6 +9,17 @@ import BatchCard from "../components/DataImport/BatchCard"; import DialogUpload from "../components/DataImport/DialogUpload"; import CentredSpinner from "../components/centredSpinner.jsx"; +const stageCollection = createListCollection({ + items: [ + { label: "Pending", value: "upload_pending" }, + { label: "Unprocessed", value: "unprocessed" }, + { label: "Processed", value: "processed" }, + { label: "Vetting", value: "vetting" }, + { label: "Integration", value: "integration" }, + { label: "Completed", value: "completed" }, + ], +}); + function formatTimestamp(dateString) { const created = new Date(dateString); const now = new Date(); @@ -48,6 +57,7 @@ function formatTimestamp(dateString) { function DataImport() { const [batches, setBatches] = useState([]); const [loading, setLoading] = useState(true); + const [selectedStages, setSelectedStages] = useState([]); const { loaded } = useSelector(state => state.auth); @@ -128,66 +138,109 @@ function DataImport() { } }, [loaded]); + const stageGroups = useMemo(() => { + return stages + .filter(stage => selectedStages.length === 0 || selectedStages.includes(stage)) + .map(stage => { + const filtered = batches.filter(batch => batch.stage === stage); + return { stage, label: stageLabels[stage], batches: filtered }; + }); + }, [batches, selectedStages]); + if (loading) { return ; } return ( - + + {/* Header */} Data Processing - - - + } + onClick={fetchBatches} + aria-label="Reload batches" + /> - - - + + {/* Multi-Select Stage Filter */} + setSelectedStages(e.value)} + > + + Filter Batch Stages + + + + + + + + + + + + {stageCollection.items.map((item) => ( + + {item.label} + + + ))} + + + + + + + {/* Stage Sections */} - - - - {stages.map(stage => ( - - {stageLabels[stage]} - - {(() => { - const filtered = batches.filter(batch => batch.stage === stage); - if (filtered.length === 0) { - return ( - - There are no batches that are in this {stageLabels[stage].toLowerCase()} stage currently. - - ); - } - return filtered.map((batch, idx) => ( - - console.log("Clicked batch", batch.id)} - /> - - )); - })()} - - ))} - + + {stageGroups.map(({ stage, label, batches: filtered }) => ( + + + {label} + + + {filtered.length === 0 ? ( + + There are no batches in the {label.toLowerCase()} stage. + + ) : ( + filtered.map((batch, idx) => ( + + console.log("Clicked batch", batch.id)} + /> + + )) + )} + + ))} - + ); } From 9fb792065a3cb800068e206856407de515485b54 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Sun, 10 Aug 2025 01:04:31 +0800 Subject: [PATCH 15/18] Modified Reload button to reload fetchbatches --- src/pages/DataImport.jsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 5f7de1f..f5b4f97 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -58,6 +58,7 @@ function DataImport() { const [batches, setBatches] = useState([]); const [loading, setLoading] = useState(true); const [selectedStages, setSelectedStages] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); const { loaded } = useSelector(state => state.auth); @@ -73,6 +74,8 @@ function DataImport() { }; async function fetchBatches() { + setIsRefreshing(true); + const ambiguousErrorToast = () => { ToastWizard.standard("error", "Failed to load batches.", "Something went wrong. Please try again."); } @@ -129,6 +132,7 @@ function DataImport() { } } finally { setLoading(false); + setIsRefreshing(false); } } @@ -156,14 +160,18 @@ function DataImport() { {/* Header */} Data Processing - } onClick={fetchBatches} aria-label="Reload batches" - /> + fontSize="16px" + transform={isRefreshing ? "rotate(360deg)" : "rotate(0deg)"} + transition="transform 0.3s ease" + isDisabled={isRefreshing} + > + + {/* Multi-Select Stage Filter */} From 199ba6cb49a3fefca6d6a2147e826a5ad55de871 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Mon, 11 Aug 2025 13:06:06 +0800 Subject: [PATCH 16/18] Modified DataImport & BatchCard to be responsive --- src/components/DataImport/BatchCard.jsx | 177 +++++++++++++++------ src/pages/DataImport.jsx | 196 +++++++++++++----------- 2 files changed, 241 insertions(+), 132 deletions(-) diff --git a/src/components/DataImport/BatchCard.jsx b/src/components/DataImport/BatchCard.jsx index 7b1bea1..59eecad 100644 --- a/src/components/DataImport/BatchCard.jsx +++ b/src/components/DataImport/BatchCard.jsx @@ -1,57 +1,144 @@ -import { HStack, VStack, Text, Image, Button, Card, Progress, Box, Spacer } from "@chakra-ui/react"; +import { HStack, VStack, Text, Image, Button, Card, Progress, useBreakpointValue, Flex, Box } from "@chakra-ui/react"; function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessing, isCancelled, isCompleted, thumbnail, onClick }) { + const status = isCancelled + ? { label: "Cancelled", color: "red.500" } + : isProcessing + ? { label: "Processing", color: "blue.500" } + : { label: "Done", color: "green.500" }; + + const isMobile = useBreakpointValue({ base: true, md: false }); + const imageWidth = useBreakpointValue({ base: "80px", md: "120px" }); + const imageHeight = useBreakpointValue({ base: "80px", md: "80px" }); + const progressWidth = useBreakpointValue({ base: "100px", md: "200px" }); + const buttonMinWidth = useBreakpointValue({ base: "80px", md: "100px" }); + const infoMaxWidth = useBreakpointValue({ base: "180px", md: "400px" }); + const fontSize = useBreakpointValue({ base: "sm", md: "md" }); + return ( - - - - {/* Thumbnail */} - - - {/* Info Texts */} - - {batchName} - - {displayMessage} | {timestamp} - - + + + {isMobile ? ( + + + + + + {batchName} + + + {displayMessage} + + + {timestamp} + + + + {status.label} + + - {/* Progress Bar beside the button */} - - - - - - {progress}% + + + + + + + + {progress}% + - - {/* Status Text below */} - {isCancelled ? ( - - Cancelled - - ) : isProcessing ? ( - - Processing - - ) : ( - - Done + + + ) : ( + + {/* 1. Thumbnail */} + + + + + {/* 2. Info */} + + + {batchName} + + + {displayMessage} | {timestamp} + + + + {/* 3. Progress */} + + + + + + + + {progress}% + + + + + + {/* 4. Status */} + + {status.label} - )} - {/* Button */} - - + {/* 5. Button */} + + + )} - + ); } diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index f5b4f97..3afb6db 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -156,99 +156,121 @@ function DataImport() { } return ( - - {/* Header */} - - Data Processing - + {/* Header Section */} + + {/* First Row: Title + Reload Button */} + - - - - - {/* Multi-Select Stage Filter */} - setSelectedStages(e.value)} + + Data Processing + + + + + + + + {/* Second Row: Stage Filter + Upload Button */} + - - Filter Batch Stages - - - - - - - - - - - - {stageCollection.items.map((item) => ( - - {item.label} - - - ))} - - - - - - - + {/* Stage Filter - Full width on mobile */} + setSelectedStages(e.value)} + > + + Filter Batch Stages + + + + + + + + + + + + {stageCollection.items.map((item) => ( + + {item.label} + + + ))} + + + + + + {/* Upload Button */} + + + + + {/* Stage Sections */} - - - {stageGroups.map(({ stage, label, batches: filtered }) => ( - - - {label} - + + {stageGroups.map(({ stage, label, batches: filtered }) => ( + + + {label} + - {filtered.length === 0 ? ( - - There are no batches in the {label.toLowerCase()} stage. - - ) : ( - filtered.map((batch, idx) => ( - - console.log("Clicked batch", batch.id)} - /> - - )) - )} - - ))} - + {filtered.length === 0 ? ( + + No batches in the {label.toLowerCase()} stage. + + ) : ( + filtered.map((batch, idx) => ( + + console.log("Clicked batch", batch.id)} + /> + + )) + )} + + ))} - + ); } From adfc930e3e67f272714d7487c5cc27bda4324979 Mon Sep 17 00:00:00 2001 From: ZacTohZY <234453p@mymail.nyp.edu.sg> Date: Mon, 11 Aug 2025 14:10:48 +0800 Subject: [PATCH 17/18] Moved new batch button to same row as data processing title --- src/components/DataImport/BatchCard.jsx | 2 +- src/pages/DataImport.jsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/DataImport/BatchCard.jsx b/src/components/DataImport/BatchCard.jsx index 59eecad..1a28033 100644 --- a/src/components/DataImport/BatchCard.jsx +++ b/src/components/DataImport/BatchCard.jsx @@ -108,7 +108,7 @@ function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessin } /> - + {progress}% diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 3afb6db..8c5ee36 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -184,6 +184,11 @@ function DataImport() { + + {/* Upload Button */} + + + {/* Second Row: Stage Filter + Upload Button */} @@ -197,7 +202,7 @@ function DataImport() { {/* Stage Filter - Full width on mobile */} - - {/* Upload Button */} - - - From 3b3bca005bf623c2dd01ba2777ce4e6ab1846f2f Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 16:15:38 +0800 Subject: [PATCH 18/18] fixed PR issues --- src/components/DataImport/BatchCard.jsx | 4 +- src/components/DataImport/DialogUpload.jsx | 5 ++- src/pages/DataImport.jsx | 50 +++++++++++----------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/components/DataImport/BatchCard.jsx b/src/components/DataImport/BatchCard.jsx index 1a28033..8ae512b 100644 --- a/src/components/DataImport/BatchCard.jsx +++ b/src/components/DataImport/BatchCard.jsx @@ -9,7 +9,7 @@ function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessin const isMobile = useBreakpointValue({ base: true, md: false }); const imageWidth = useBreakpointValue({ base: "80px", md: "120px" }); - const imageHeight = useBreakpointValue({ base: "80px", md: "80px" }); + const imageHeight = '80px'; const progressWidth = useBreakpointValue({ base: "100px", md: "200px" }); const buttonMinWidth = useBreakpointValue({ base: "80px", md: "100px" }); const infoMaxWidth = useBreakpointValue({ base: "180px", md: "400px" }); @@ -24,7 +24,7 @@ function BatchCard({ batchName, displayMessage, timestamp, progress, isProcessin diff --git a/src/components/DataImport/DialogUpload.jsx b/src/components/DataImport/DialogUpload.jsx index b72a79e..ff7c556 100644 --- a/src/components/DataImport/DialogUpload.jsx +++ b/src/components/DataImport/DialogUpload.jsx @@ -3,6 +3,7 @@ import { HiUpload } from "react-icons/hi"; import { useState, useRef } from "react"; import server from "../../networking"; import ToastWizard from "../../components/toastWizard"; +import { FaPlus } from "react-icons/fa"; function DialogUpload() { const [files, setFiles] = useState([]); @@ -81,8 +82,8 @@ function DialogUpload() { return ( - diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 8c5ee36..8cfca43 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -161,42 +161,41 @@ function DataImport() { {/* First Row: Title + Reload Button */} Data Processing - - - - + + + + + + {/* Upload Button */} - - - + {/* Second Row: Stage Filter + Upload Button */} {/* Stage Filter - Full width on mobile */} @@ -209,10 +208,10 @@ function DataImport() { onValueChange={(e) => setSelectedStages(e.value)} > - Filter Batch Stages + Viewing stage(s): - - + + @@ -222,7 +221,7 @@ function DataImport() { {stageCollection.items.map((item) => ( - + {item.label} @@ -260,7 +259,6 @@ function DataImport() { )} isProcessing={stage === "unprocessed"} isCancelled={batch.job?.status === "cancelled"} - isCompleted={stage === "completed"} thumbnail={hp1} onClick={() => console.log("Clicked batch", batch.id)} />