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 (
+
+
+ }>
+ New Batch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ Upload
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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 (
-
+
- }>
+ }>
New Batch
@@ -19,27 +73,99 @@ function DialogUpload() {
+
-
+ {/* Hidden input to capture files */}
+
+
+ {/* Custom trigger button */}
- }>
+ }
+ isLoading={isSubmitting}
+ >
Upload
-
+
+ {/* 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 (
- }>
+ }>
New Batch
@@ -69,6 +91,7 @@ function DialogUpload() {
+ Upload Files
@@ -76,10 +99,7 @@ function DialogUpload() {
- {/* Hidden input to capture files */}
-
- {/* Custom trigger button */}
- {/* Custom file list with statuses */}
+ {/* Custom file list with status and remove button */}
{files.length > 0 && (
{files.map((file) => {
@@ -103,23 +123,35 @@ function DialogUpload() {
borderWidth="1px"
borderRadius="md"
bg={status?.startsWith("ERROR") ? "red.50" : "gray.50"}
+ display="flex"
+ flexDirection="column"
>
-
+
{file.name}
-
+
{(file.size / 1024).toFixed(1)} KB
{status && (
-
+
{status.startsWith("ERROR") && (
-
+
Error
)}
@@ -133,13 +165,14 @@ function DialogUpload() {
>
{status}
-
-
+
{
- setFiles(prev => prev.filter(f => f.name !== file.name));
- setFileStatuses(prev => {
+ setFiles((prev) =>
+ prev.filter((f) => f.name !== file.name)
+ );
+ setFileStatuses((prev) => {
const updated = { ...prev };
delete updated[file.name];
return updated;
@@ -157,13 +190,64 @@ function DialogUpload() {
-
+
+ setIsConfirmDialogOpen(open)}>
+
+
+
+
+ Confirm Processing
+
+
+ Are you sure you want to process batch{" "}
+
+ {batchID}
+
+ ? This will start processing the artefacts in the batch.
+
+
+
+ setIsConfirmDialogOpen(false)}>
+ Cancel
+
+
+ Confirm
+
+
+
+
+
+
);
}
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 */}
{isCancelled ? "Details" : isCompleted ? "Continue" : "Details"}
-
);
}
-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 (
- }>
+ } size="lg">
New Batch
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
+
+ {isCancelled ? "Details" : isCompleted ? "Continue" : "Details"}
+
+
+ ) : (
+
+ {/* 1. Thumbnail */}
+
+
+
+
+ {/* 2. Info */}
+
+
+ {batchName}
+
+
+ {displayMessage} | {timestamp}
+
+
+
+ {/* 3. Progress */}
+
+
+
+
+
+
+
+ {progress}%
+
+
+
+
+
+ {/* 4. Status */}
+
+ {status.label}
- )}
- {/* Button */}
-
- {isCancelled ? "Details" : isCompleted ? "Continue" : "Details"}
-
-
+ {/* 5. Button */}
+
+ {isCancelled ? "Details" : isCompleted ? "Continue" : "Details"}
+
+
+ )}
-
+
);
}
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 (
- } size="lg">
- New Batch
+
+
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)}
/>