From c23de9bf7ceec1c958f158f2906f0b0a278c9310 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 2 Sep 2025 15:45:12 +0530 Subject: [PATCH 01/11] Change the filter design and refactore the compoents for Image Gallery. --- .../ImageGallery}/ImageGallery.css | 0 .../ImageGallery/ImageGalleryFilters.jsx | 171 ++++++ .../ImageGallery/ImageGalleryListView.jsx | 169 ++++++ .../ImageGallery/ImagePopup.css} | 0 src/components/ImageGallery/ImagePopup.jsx | 90 ++++ .../ImageGallery}/ModalContext.jsx | 0 src/main.tsx | 2 +- src/pages/Gallary/ImageGallary.jsx | 499 ------------------ src/pages/Gallary/ImagePop.jsx | 91 ---- src/pages/ImageGallery/ImageGalleryPage.jsx | 196 +++++++ src/router/AppRoutes.jsx | 4 +- 11 files changed, 629 insertions(+), 593 deletions(-) rename src/{pages/Gallary => components/ImageGallery}/ImageGallery.css (100%) create mode 100644 src/components/ImageGallery/ImageGalleryFilters.jsx create mode 100644 src/components/ImageGallery/ImageGalleryListView.jsx rename src/{pages/Gallary/ImagePop.css => components/ImageGallery/ImagePopup.css} (100%) create mode 100644 src/components/ImageGallery/ImagePopup.jsx rename src/{pages/Gallary => components/ImageGallery}/ModalContext.jsx (100%) delete mode 100644 src/pages/Gallary/ImageGallary.jsx delete mode 100644 src/pages/Gallary/ImagePop.jsx create mode 100644 src/pages/ImageGallery/ImageGalleryPage.jsx diff --git a/src/pages/Gallary/ImageGallery.css b/src/components/ImageGallery/ImageGallery.css similarity index 100% rename from src/pages/Gallary/ImageGallery.css rename to src/components/ImageGallery/ImageGallery.css diff --git a/src/components/ImageGallery/ImageGalleryFilters.jsx b/src/components/ImageGallery/ImageGalleryFilters.jsx new file mode 100644 index 00000000..86a0efe2 --- /dev/null +++ b/src/components/ImageGallery/ImageGalleryFilters.jsx @@ -0,0 +1,171 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import moment from "moment"; +import DateRangePicker, { DateRangePicker1 } from "../../components/common/DateRangePicker"; +import SelectMultiple from "../../components/common/SelectMultiple"; + +const defaultGalleryFilterValues = { + buildingIds: [], + floorIds: [], + activityIds: [], + uploadedByIds: [], + workCategoryIds: [], + workAreaIds: [], + startDate: null, + endDate: null, +}; + +const ImageGalleryFilters = ({ + buildings = [], + floors = [], + activities = [], + workAreas = [], + workCategories = [], + uploadedByUsers = [], + onApplyFilters, + appliedFilters, +}) => { + const [resetKey, setResetKey] = useState(0); + + const methods = useForm({ + defaultValues: defaultGalleryFilterValues, + }); + + const { handleSubmit, reset } = methods; + + // Prefill form when appliedFilters changes + useEffect(() => { + if (appliedFilters) { + reset({ + buildingIds: appliedFilters.buildingIds || [], + floorIds: appliedFilters.floorIds || [], + activityIds: appliedFilters.activityIds || [], + uploadedByIds: appliedFilters.uploadedByIds || [], + workCategoryIds: appliedFilters.workCategoryIds || [], + workAreaIds: appliedFilters.workAreaIds || [], + startDate: appliedFilters.startDate || null, + endDate: appliedFilters.endDate || null, + }); + } + }, [appliedFilters, reset]); + + // Submit → Apply filters + const onSubmit = useCallback( + (formData) => { + const payload = { + ...formData, + startDate: formData.startDate + ? moment(formData.startDate).utc().toISOString() + : null, + endDate: formData.endDate + ? moment(formData.endDate).utc().toISOString() + : null, + }; + onApplyFilters(payload); + }, + [onApplyFilters] + ); + + // Clear all filters + const onClear = useCallback(() => { + reset(defaultGalleryFilterValues); + setResetKey((prev) => prev + 1); // reset DateRangePicker + onApplyFilters(defaultGalleryFilterValues); + }, [onApplyFilters, reset]); + + return ( +
+ +
+ {/* Date Range */} +
+ + +
+ + {/* Multi-select dropdowns */} +
+ ({ id, name }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ ({ id, name }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ ({ id, name }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ ({ id, name }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ ({ id, name }))} + labelKey="name" + valueKey="id" + /> +
+ +
+ ({ id, name }))} + labelKey="name" + valueKey="id" + /> +
+ + {/* Footer buttons */} +
+ + +
+
+
+
+ ); +}; + +export default ImageGalleryFilters; diff --git a/src/components/ImageGallery/ImageGalleryListView.jsx b/src/components/ImageGallery/ImageGalleryListView.jsx new file mode 100644 index 00000000..157fbafa --- /dev/null +++ b/src/components/ImageGallery/ImageGalleryListView.jsx @@ -0,0 +1,169 @@ +import React, { useRef, useState, useCallback } from "react"; +import Avatar from "../../components/common/Avatar"; +import ImagePopup from "./ImagePopup"; + +const ImageGalleryListView = ({ + images, + isLoading, + isFetchingNextPage, + hasNextPage, + loaderRef, + openModal, + SCROLL_THRESHOLD, + formatUTCToLocalTime, + moment, +}) => { + const [hoveredImage, setHoveredImage] = useState(null); + const imageGroupRefs = useRef({}); + + const scrollLeft = useCallback( + (key) => + imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }), + [] + ); + const scrollRight = useCallback( + (key) => + imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }), + [] + ); + + return ( +
+
+ {isLoading ? ( +
+

Loading...

+
+ ) : images.length ? ( + images.map((batch) => { + const doc = batch.documents[0]; + const userName = `${doc.uploadedBy?.firstName || ""} ${ + doc.uploadedBy?.lastName || "" + }`.trim(); + const date = formatUTCToLocalTime(doc.uploadedAt); + const hasArrows = batch.documents.length > SCROLL_THRESHOLD; + + return ( +
+
+ {/* Uploader Info */} +
+ +
+ {userName} + {date} +
+
+ + {/* Location Info */} +
+
+ + {batch.buildingName} + + + + {batch.floorName} + + + + {batch.workAreaName || "Unknown"} + + {batch.activityName} + +
+ {batch.workCategoryName && ( + + {batch.workCategoryName} + + )} +
+
+ + {/* Images */} +
+ {hasArrows && ( + + )} +
+ (imageGroupRefs.current[batch.batchId] = el) + } + > + {batch.documents.map((d, i) => { + const hoverDate = moment(d.uploadedAt).format( + "DD MMMM, YYYY" + ); + const hoverTime = moment(d.uploadedAt).format("hh:mm A"); + + return ( +
+ openModal() + } + onMouseEnter={() => setHoveredImage(d)} + onMouseLeave={() => setHoveredImage(null)} + > +
+ {`Image +
+ {hoveredImage === d && ( +
+

+ Date: {hoverDate} +

+

+ Time: {hoverTime} +

+

+ Activity: {batch.activityName} +

+
+ )} +
+ ); + })} +
+ {hasArrows && ( + + )} +
+
+ ); + }) + ) : ( +

+ No images match the selected filters. +

+ )} + +
+ {isFetchingNextPage && hasNextPage &&

Loading...

} + {!hasNextPage && !isLoading && images.length > 0 && ( +

You've reached the end of the images.

+ )} +
+
+
+ ); +}; + +export default ImageGalleryListView; diff --git a/src/pages/Gallary/ImagePop.css b/src/components/ImageGallery/ImagePopup.css similarity index 100% rename from src/pages/Gallary/ImagePop.css rename to src/components/ImageGallery/ImagePopup.css diff --git a/src/components/ImageGallery/ImagePopup.jsx b/src/components/ImageGallery/ImagePopup.jsx new file mode 100644 index 00000000..9d773195 --- /dev/null +++ b/src/components/ImageGallery/ImagePopup.jsx @@ -0,0 +1,90 @@ +import React, { useState, useEffect } from "react"; +import "./ImagePopup.css" +import { useModal } from "./ModalContext"; +import moment from "moment"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; + +const ImagePopup = ({ batch, initialIndex = 0 }) => { + const { closeModal } = useModal(); + const [currentIndex, setCurrentIndex] = useState(initialIndex); + + // Effect to update currentIndex if the initialIndex prop changes + useEffect(() => { + setCurrentIndex(initialIndex); + }, [initialIndex, batch]); + + // If no batch or documents are provided, don't render + if (!batch || !batch.documents || batch.documents.length === 0) return null; + + // Get the current image document from the batch's documents array + const image = batch.documents[currentIndex]; + + // Fallback if for some reason the image at the current index doesn't exist + if (!image) return null; + + // Format details for display from the individual image document + const fullName = `${image.uploadedBy?.firstName || ""} ${image.uploadedBy?.lastName || "" + }`.trim(); + const date = formatUTCToLocalTime(image.uploadedAt); + + // Location and category details from the 'batch' object (as previously corrected) + const buildingName = batch.buildingName; + const floorName = batch.floorName; + const workAreaName = batch.workAreaName; + const activityName = batch.activityName; + const batchComment = batch.comment; + + // Handler for navigating to the previous image + const handlePrev = () => { + setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1)); + }; + + // Handler for navigating to the next image + const handleNext = () => { + setCurrentIndex((prevIndex) => + Math.min(batch.documents.length - 1, prevIndex + 1) + ); + }; + + // Determine if previous/next buttons should be enabled/visible + const hasPrev = currentIndex > 0; + const hasNext = currentIndex < batch.documents.length - 1; + + return ( +
+
+ + + + {hasPrev && ( + + )} + +
+ Preview +
+ + {hasNext && ( + + )} + +
+ +
Uploaded By : {fullName}
+
Date : {date}
+
Uploaded By : {buildingName} {floorName} + {workAreaName || "Unknown"} {activityName}
+
comment : {batchComment}
+ + +
+
+
+ ); +}; + +export default ImagePopup; \ No newline at end of file diff --git a/src/pages/Gallary/ModalContext.jsx b/src/components/ImageGallery/ModalContext.jsx similarity index 100% rename from src/pages/Gallary/ModalContext.jsx rename to src/components/ImageGallery/ModalContext.jsx diff --git a/src/main.tsx b/src/main.tsx index ac7175fe..5997bb8d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,7 +8,7 @@ import { Provider } from 'react-redux'; import { store } from './store/store'; import { ModalProvider } from './ModalContext.jsx'; import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx'; -import { ModalProvider1 } from './pages/Gallary/ModalContext.jsx'; +import { ModalProvider1 } from './components/ImageGallery/ModalContext.jsx'; createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Gallary/ImageGallary.jsx b/src/pages/Gallary/ImageGallary.jsx deleted file mode 100644 index d92ce8ca..00000000 --- a/src/pages/Gallary/ImageGallary.jsx +++ /dev/null @@ -1,499 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from "react"; -import "./ImageGallery.css"; -import moment from "moment"; -import { useDispatch, useSelector } from "react-redux"; -import { useModal } from "./ModalContext"; -import ImagePop from "./ImagePop"; -import Avatar from "../../components/common/Avatar"; -import DateRangePicker from "../../components/common/DateRangePicker"; -import eventBus from "../../services/eventBus"; -import Breadcrumb from "../../components/common/Breadcrumb"; -import { formatUTCToLocalTime } from "../../utils/dateUtils"; -import useImageGallery from "../../hooks/useImageGallery"; -import { useProjectName } from "../../hooks/useProjects"; -import { setProjectId } from "../../slices/localVariablesSlice"; - -const SCROLL_THRESHOLD = 5; - -const ImageGallery = () => { - const selectedProjectId = useSelector( - (store) => store.localVariables.projectId - ); - const dispatch = useDispatch(); - const { projectNames } = useProjectName(); - - // Auto-select a project on mount - useEffect(() => { - if (!selectedProjectId && projectNames?.length) { - dispatch(setProjectId(projectNames[0].id)); - } - }, [selectedProjectId, projectNames, dispatch]); - - // Filter states - const [selectedFilters, setSelectedFilters] = useState({ - building: [], - floor: [], - activity: [], - uploadedBy: [], - workCategory: [], - workArea: [], - startDate: "", - endDate: "", - }); - const [appliedFilters, setAppliedFilters] = useState({ - buildingIds: null, - floorIds: null, - activityIds: null, - uploadedByIds: null, - workCategoryIds: null, - workAreaIds: null, - startDate: null, - endDate: null, - }); - const [collapsedFilters, setCollapsedFilters] = useState({ - dateRange: false, - building: false, - floor: false, - activity: false, - uploadedBy: false, - workCategory: false, - workArea: false, - }); - const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); - const [hoveredImage, setHoveredImage] = useState(null); - - const imageGroupRefs = useRef({}); - const loaderRef = useRef(null); - const filterPanelRef = useRef(null); - const filterButtonRef = useRef(null); - const { openModal } = useModal(); - - const { - data, - fetchNextPage, - hasNextPage, - isLoading, - isFetchingNextPage, - refetch, - } = useImageGallery(selectedProjectId, appliedFilters); - - const images = data?.pages.flatMap((page) => page.data) || []; - - useEffect(() => { - const handleClick = (e) => { - if ( - filterPanelRef.current && - !filterPanelRef.current.contains(e.target) && - filterButtonRef.current && - !filterButtonRef.current.contains(e.target) - ) { - setIsFilterPanelOpen(false); - } - }; - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, []); - - useEffect(() => { - if (selectedProjectId) refetch(); - }, [selectedProjectId, appliedFilters, refetch]); - - useEffect(() => { - const handler = (data) => { - if (data.projectId === selectedProjectId) refetch(); - }; - eventBus.on("image_gallery", handler); - return () => eventBus.off("image_gallery", handler); - }, [selectedProjectId, refetch]); - -useEffect(() => { - if (!loaderRef.current) return; - - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting && hasNextPage && !isFetchingNextPage && !isLoading) { - fetchNextPage(); - } - }, - { rootMargin: "200px", threshold: 0.1 } - ); - - observer.observe(loaderRef.current); - - return () => { - if (loaderRef.current) { - observer.unobserve(loaderRef.current); - } - }; -}, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]); - - - // Utility: derive filter options - const getUniqueValues = useCallback( - (idKey, nameKey) => { - const m = new Map(); - images.forEach((batch) => { - const id = idKey === "floorIds" ? batch.floorIds : batch[idKey]; - if (id && batch[nameKey] && !m.has(id)) { - m.set(id, batch[nameKey]); - } - }); - return [...m.entries()].sort((a, b) => a[1].localeCompare(b[1])); - }, - [images] - ); - - const getUploadedBy = useCallback(() => { - const m = new Map(); - images.forEach((batch) => { - batch.documents.forEach((doc) => { - const name = `${doc.uploadedBy?.firstName || ""} ${ - doc.uploadedBy?.lastName || "" - }`.trim(); - if (doc.uploadedBy?.id && name && !m.has(doc.uploadedBy.id)) { - m.set(doc.uploadedBy.id, name); - } - }); - }); - return [...m.entries()].sort((a, b) => a[1].localeCompare(b[1])); - }, [images]); - - const buildings = getUniqueValues("buildingId", "buildingName"); - const floors = getUniqueValues("floorIds", "floorName"); - const activities = getUniqueValues("activityId", "activityName"); - const workAreas = getUniqueValues("workAreaId", "workAreaName"); - const workCategories = getUniqueValues("workCategoryId", "workCategoryName"); - const uploadedByUsers = getUploadedBy(); - - const toggleFilter = useCallback((type, id, name) => { - setSelectedFilters((prev) => { - const arr = prev[type]; - const exists = arr.some(([x]) => x === id); - const updated = exists - ? arr.filter(([x]) => x !== id) - : [...arr, [id, name]]; - return { ...prev, [type]: updated }; - }); - }, []); - - const setDateRange = useCallback(({ startDate, endDate }) => { - setSelectedFilters((prev) => ({ - ...prev, - startDate: startDate || "", - endDate: endDate || "", - })); - }, []); - - const toggleCollapse = useCallback((type) => { - setCollapsedFilters((prev) => ({ ...prev, [type]: !prev[type] })); - }, []); - - const handleApplyFilters = useCallback(() => { - const payload = { - buildingIds: selectedFilters.building.map(([x]) => x) || null, - floorIds: selectedFilters.floor.map(([x]) => x) || null, - activityIds: selectedFilters.activity.map(([x]) => x) || null, - uploadedByIds: selectedFilters.uploadedBy.map(([x]) => x) || null, - workCategoryIds: selectedFilters.workCategory.map(([x]) => x) || null, - workAreaIds: selectedFilters.workArea.map(([x]) => x) || null, - startDate: selectedFilters.startDate || null, - endDate: selectedFilters.endDate || null, - }; - const changed = Object.keys(payload).some((key) => { - const oldVal = appliedFilters[key], - newVal = payload[key]; - return Array.isArray(oldVal) - ? oldVal.length !== newVal.length || - oldVal.some((x) => !newVal.includes(x)) - : oldVal !== newVal; - }); - if (changed) setAppliedFilters(payload); - }, [selectedFilters, appliedFilters]); - - const handleClear = useCallback(() => { - setSelectedFilters({ - building: [], - floor: [], - activity: [], - uploadedBy: [], - workCategory: [], - workArea: [], - startDate: "", - endDate: "", - }); - setAppliedFilters({ - buildingIds: null, - floorIds: null, - activityIds: null, - uploadedByIds: null, - workCategoryIds: null, - workAreaIds: null, - startDate: null, - endDate: null, - }); - }, []); - - const scrollLeft = useCallback( - (key) => - imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }), - [] - ); - const scrollRight = useCallback( - (key) => - imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }), - [] - ); - - const renderCategory = (label, items, type) => ( -
-
toggleCollapse(type)} - > - {label} -
- {((type === "dateRange" && - (selectedFilters.startDate || selectedFilters.endDate)) || - (type !== "dateRange" && selectedFilters[type]?.length > 0)) && ( - - )} - - {collapsedFilters[type] ? "+" : "-"} - -
-
- {!collapsedFilters[type] && ( -
- {type === "dateRange" ? ( - - ) : ( - items.map(([id, name]) => ( - - )) - )} -
- )} -
- ); - - return ( -
- -
- -
- {isLoading ? ( -
-

Loading...

-
- ) : images.length ? ( - images.map((batch) => { - const doc = batch.documents[0]; - const userName = `${doc.uploadedBy?.firstName || ""} ${ - doc.uploadedBy?.lastName || "" - }`.trim(); - const date = formatUTCToLocalTime(doc.uploadedAt); - const hasArrows = batch.documents.length > SCROLL_THRESHOLD; - return ( -
-
- {/* Uploader Info */} -
- -
- {userName} - {date} -
-
- - {/* Location Info */} -
-
- {" "} - - {batch.buildingName} - - - - {batch.floorName} - - - - {batch.workAreaName || "Unknown"} - - {batch.activityName} - -
- {batch.workCategoryName && ( - - {batch.workCategoryName} - - )} -
-
- -
- {hasArrows && ( - - )} -
(imageGroupRefs.current[batch.batchId] = el)} - > - {batch.documents.map((d, i) => { - const hoverDate = moment(d.uploadedAt).format( - "DD MMMM, YYYY" - ); - const hoverTime = moment(d.uploadedAt).format( - "hh:mm A" - ); - return ( -
- openModal( - - ) - } - onMouseEnter={() => setHoveredImage(d)} - onMouseLeave={() => setHoveredImage(null)} - > -
- {`Image -
- {hoveredImage === d && ( -
-

- Date: {hoverDate} -

-

- Time: {hoverTime} -

-

- Activity:{" "} - {batch.activityName} -

-
- )} -
- ); - })} -
- {hasArrows && ( - - )} -
-
- ); - }) - ) : ( -

- No images match the selected filters. -

- )} -
- {isFetchingNextPage && hasNextPage &&

Loading...

} - {!hasNextPage && !isLoading && images.length > 0 && ( -

- You've reached the end of the images. -

- )} -
-
-
- -
-
-
Filters
-
-
- - -
-
- {renderCategory("Date Range", [], "dateRange")} - {renderCategory("Building", buildings, "building")} - {renderCategory("Floor", floors, "floor")} - {renderCategory("Work Area", workAreas, "workArea")} - {renderCategory("Activity", activities, "activity")} - {renderCategory("Uploaded By (User)", uploadedByUsers, "uploadedBy")} - {renderCategory("Work Category", workCategories, "workCategory")} -
-
-
- ); -}; - -export default ImageGallery; diff --git a/src/pages/Gallary/ImagePop.jsx b/src/pages/Gallary/ImagePop.jsx deleted file mode 100644 index 55a079df..00000000 --- a/src/pages/Gallary/ImagePop.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState, useEffect } from "react"; -import "./ImagePop.css"; -import { useModal } from "./ModalContext"; -import moment from "moment"; -import {formatUTCToLocalTime} from "../../utils/dateUtils"; - -const ImagePop = ({ batch, initialIndex = 0 }) => { - const { closeModal } = useModal(); - const [currentIndex, setCurrentIndex] = useState(initialIndex); - - // Effect to update currentIndex if the initialIndex prop changes - useEffect(() => { - setCurrentIndex(initialIndex); - }, [initialIndex, batch]); - - // If no batch or documents are provided, don't render - if (!batch || !batch.documents || batch.documents.length === 0) return null; - - // Get the current image document from the batch's documents array - const image = batch.documents[currentIndex]; - - // Fallback if for some reason the image at the current index doesn't exist - if (!image) return null; - - // Format details for display from the individual image document - const fullName = `${image.uploadedBy?.firstName || ""} ${ - image.uploadedBy?.lastName || "" - }`.trim(); - const date = formatUTCToLocalTime(image.uploadedAt); - - // Location and category details from the 'batch' object (as previously corrected) - const buildingName = batch.buildingName; - const floorName = batch.floorName; - const workAreaName = batch.workAreaName; - const activityName = batch.activityName; - const batchComment = batch.comment; - - // Handler for navigating to the previous image - const handlePrev = () => { - setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1)); - }; - - // Handler for navigating to the next image - const handleNext = () => { - setCurrentIndex((prevIndex) => - Math.min(batch.documents.length - 1, prevIndex + 1) - ); - }; - - // Determine if previous/next buttons should be enabled/visible - const hasPrev = currentIndex > 0; - const hasNext = currentIndex < batch.documents.length - 1; - - return ( -
-
- - - - {hasPrev && ( - - )} - -
- Preview -
- - {hasNext && ( - - )} - -
- -
Uploaded By : {fullName}
-
Date : {date}
-
Uploaded By : {buildingName} {floorName} - {workAreaName || "Unknown"} {activityName}
-
comment : {batchComment}
- - -
-
-
- ); -}; - -export default ImagePop; \ No newline at end of file diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx new file mode 100644 index 00000000..c34a8d71 --- /dev/null +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -0,0 +1,196 @@ +import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import moment from "moment"; +import { useModal } from "../../components/ImageGallery/ModalContext"; +import eventBus from "../../services/eventBus"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import useImageGallery from "../../hooks/useImageGallery"; +import { useProjectName } from "../../hooks/useProjects"; +import { setProjectId } from "../../slices/localVariablesSlice"; +import ImageGalleryListView from "../../components/ImageGallery/ImageGalleryListView"; +import ImageGalleryFilters from "../../components/ImageGallery/ImageGalleryFilters"; +import "../../components/ImageGallery/ImageGallery.css"; + +// --- NEW IMPORTS --- +import { useFab } from "../../Context/FabContext"; + +const SCROLL_THRESHOLD = 5; + +const ImageGalleryPage = () => { + const selectedProjectId = useSelector((store) => store.localVariables.projectId); + const dispatch = useDispatch(); + const { projectNames } = useProjectName(); + const loaderRef = useRef(null); + const { openModal } = useModal(); + + // Auto-select first project if none selected + useEffect(() => { + if (!selectedProjectId && projectNames?.length) { + dispatch(setProjectId(projectNames[0].id)); + } + }, [selectedProjectId, projectNames, dispatch]); + + // --- Filters --- + const [appliedFilters, setAppliedFilters] = useState({ + buildingIds: null, + floorIds: null, + activityIds: null, + uploadedByIds: null, + workCategoryIds: null, + workAreaIds: null, + startDate: null, + endDate: null, + }); + + const { + data, + fetchNextPage, + hasNextPage, + isLoading, + isFetchingNextPage, + refetch, + } = useImageGallery(selectedProjectId, appliedFilters); + + const images = data?.pages.flatMap((page) => page.data) || []; + + // --- Utility: derive filter options --- + const getUniqueValues = useCallback( + (idKey, nameKey) => { + const m = new Map(); + images.forEach((batch) => { + const id = idKey === "floorIds" ? batch.floorIds : batch[idKey]; + if (id && batch[nameKey] && !m.has(id)) { + m.set(id, batch[nameKey]); + } + }); + return [...m.entries()].sort((a, b) => a[1].localeCompare(b[1])); + }, + [images] + ); + + const getUploadedBy = useCallback(() => { + const m = new Map(); + images.forEach((batch) => { + batch.documents.forEach((doc) => { + const name = `${doc.uploadedBy?.firstName || ""} ${ + doc.uploadedBy?.lastName || "" + }`.trim(); + if (doc.uploadedBy?.id && name && !m.has(doc.uploadedBy.id)) { + m.set(doc.uploadedBy.id, name); + } + }); + }); + return [...m.entries()].sort((a, b) => a[1].localeCompare(b[1])); + }, [images]); + + const buildings = getUniqueValues("buildingId", "buildingName"); + const floors = getUniqueValues("floorIds", "floorName"); + const activities = getUniqueValues("activityId", "activityName"); + const workAreas = getUniqueValues("workAreaId", "workAreaName"); + const workCategories = getUniqueValues("workCategoryId", "workCategoryName"); + const uploadedByUsers = getUploadedBy(); + + // --- Apply filters callback --- + const handleApplyFilters = useCallback((values) => { + setAppliedFilters(values); + }, []); + + // --- Filter Panel Memoization --- + const filterPanelElement = useMemo( + () => ( + + ), + [ + buildings, + floors, + activities, + workAreas, + workCategories, + uploadedByUsers, + appliedFilters, + handleApplyFilters, + ] + ); + + // --- Fab Offcanvas Integration --- + const { setOffcanvasContent, setShowTrigger } = useFab(); + useEffect(() => { + setShowTrigger(true); + setOffcanvasContent("Gallery Filters", filterPanelElement); + + return () => { + setShowTrigger(false); + setOffcanvasContent("", null); + }; + }, [filterPanelElement, setOffcanvasContent, setShowTrigger]); + + // --- Refetch on project or filters --- + useEffect(() => { + if (selectedProjectId) refetch(); + }, [selectedProjectId, appliedFilters, refetch]); + + // --- EventBus Refetch --- + useEffect(() => { + const handler = (data) => { + if (data.projectId === selectedProjectId) refetch(); + }; + eventBus.on("image_gallery", handler); + return () => eventBus.off("image_gallery", handler); + }, [selectedProjectId, refetch]); + + // --- Infinite scroll observer --- + useEffect(() => { + if (!loaderRef.current) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if ( + entry.isIntersecting && + hasNextPage && + !isFetchingNextPage && + !isLoading + ) { + fetchNextPage(); + } + }, + { rootMargin: "200px", threshold: 0.1 } + ); + + observer.observe(loaderRef.current); + + return () => { + if (loaderRef.current) { + observer.unobserve(loaderRef.current); + } + }; + }, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]); + + return ( +
+ + +
+ ); +}; + +export default ImageGalleryPage; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 0e269a6f..ac2577c1 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -26,7 +26,6 @@ import AttendancePage from "../pages/Activities/AttendancePage"; import DailyTask from "../pages/Activities/DailyTask"; import TaskPlannng from "../pages/Activities/TaskPlannng"; import Reports from "../pages/reports/Reports"; -import ImageGallary from "../pages/Gallary/ImageGallary"; import MasterPage from "../pages/master/MasterPage"; import Support from "../pages/support/Support"; import Documentation from "../pages/support/Documentation"; @@ -44,6 +43,7 @@ import ExpensePage from "../pages/Expense/ExpensePage"; import TenantDetails from "../pages/Tenant/TenantDetails"; import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails"; import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails"; +import ImageGalleryPage from "../pages/ImageGallery/ImageGalleryPage"; const router = createBrowserRouter( [ @@ -81,7 +81,7 @@ const router = createBrowserRouter( { path: "/activities/records/:projectId?", element: }, { path: "/activities/task", element: }, { path: "/activities/reports", element: }, - { path: "/gallary", element: }, + { path: "/gallary", element: }, { path: "/expenses", element: }, { path: "/masters", element: }, { path: "/tenants", element: }, -- 2.43.0 From d67a1851465167f5eb901f5ea7f91294395af024 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Thu, 18 Sep 2025 11:11:47 +0530 Subject: [PATCH 02/11] Changes in images gallery filter. --- .../ImageGallery/ImageGalleryFilters.jsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/ImageGallery/ImageGalleryFilters.jsx b/src/components/ImageGallery/ImageGalleryFilters.jsx index 86a0efe2..f6147eb3 100644 --- a/src/components/ImageGallery/ImageGalleryFilters.jsx +++ b/src/components/ImageGallery/ImageGalleryFilters.jsx @@ -1,7 +1,7 @@ import React, { useState, useCallback, useEffect } from "react"; import { FormProvider, useForm } from "react-hook-form"; import moment from "moment"; -import DateRangePicker, { DateRangePicker1 } from "../../components/common/DateRangePicker"; +import DateRangePicker from "../../components/common/DateRangePicker"; import SelectMultiple from "../../components/common/SelectMultiple"; const defaultGalleryFilterValues = { @@ -31,7 +31,7 @@ const ImageGalleryFilters = ({ defaultValues: defaultGalleryFilterValues, }); - const { handleSubmit, reset } = methods; + const { handleSubmit, reset, setValue } = methods; // Prefill form when appliedFilters changes useEffect(() => { @@ -49,6 +49,15 @@ const ImageGalleryFilters = ({ } }, [appliedFilters, reset]); + // Handle date range change and set form values + const handleDateRangeChange = useCallback( + ({ startDate, endDate }) => { + setValue("startDate", startDate); + setValue("endDate", endDate); + }, + [setValue] + ); + // Submit → Apply filters const onSubmit = useCallback( (formData) => { @@ -80,10 +89,10 @@ const ImageGalleryFilters = ({ {/* Date Range */}
-
@@ -153,7 +162,7 @@ const ImageGalleryFilters = ({
+ )} + + { hasNextPage={hasNextPage} loaderRef={loaderRef} openModal={openModal} - SCROLL_THRESHOLD={SCROLL_THRESHOLD} formatUTCToLocalTime={formatUTCToLocalTime} moment={moment} /> -- 2.43.0 From 40fb0101282f189da70fbf530ceb989147c53ceb Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Fri, 19 Sep 2025 17:53:28 +0530 Subject: [PATCH 04/11] Creating a Skeleton file for Image Gallery. --- .../Charts/ImageGallerySkeleton.jsx | 46 +++++++++++++++++++ src/pages/ImageGallery/ImageGalleryPage.jsx | 26 +++++++---- 2 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 src/components/Charts/ImageGallerySkeleton.jsx diff --git a/src/components/Charts/ImageGallerySkeleton.jsx b/src/components/Charts/ImageGallerySkeleton.jsx new file mode 100644 index 00000000..a3f871d9 --- /dev/null +++ b/src/components/Charts/ImageGallerySkeleton.jsx @@ -0,0 +1,46 @@ +import React from "react"; + +const ImageCardSkeleton = ({ count = 1 }) => { + const cards = Array.from({ length: count }); + + return ( +
+ {cards.map((_, idx) => ( +
+
+
+
+
+
+ +
+ + + +
+
+ +
+ +
+
+ +
+
+ +
+
+
+
+ ))} +
+ ); +}; + +export default ImageCardSkeleton; diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index 70a9d37d..c1d52bbe 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -12,6 +12,7 @@ import ImageGalleryListView from "../../components/ImageGallery/ImageGalleryList import ImageGalleryFilters from "../../components/ImageGallery/ImageGalleryFilters"; import "../../components/ImageGallery/ImageGallery.css"; import { useFab } from "../../Context/FabContext"; +import ImageGallerySkeleton from "../../components/Charts/ImageGallerySkeleton"; const ImageGalleryPage = () => { const dispatch = useDispatch(); @@ -246,16 +247,21 @@ const ImageGalleryPage = () => { )} - + {isLoading ? ( + + ) : ( + + )} +
); }; -- 2.43.0 From d4be35b44d9afb929562ffe8a378d0fb3c8b5652 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Sat, 20 Sep 2025 14:25:33 +0530 Subject: [PATCH 05/11] Changes in Filter Date Range picker. --- .../ImageGallery/ImageGalleryFilters.jsx | 34 ++++++++----------- src/components/common/DateRangePicker.jsx | 2 ++ src/utils/appUtils.js | 11 +++--- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/components/ImageGallery/ImageGalleryFilters.jsx b/src/components/ImageGallery/ImageGalleryFilters.jsx index f6147eb3..3e541012 100644 --- a/src/components/ImageGallery/ImageGalleryFilters.jsx +++ b/src/components/ImageGallery/ImageGalleryFilters.jsx @@ -1,8 +1,9 @@ import React, { useState, useCallback, useEffect } from "react"; import { FormProvider, useForm } from "react-hook-form"; import moment from "moment"; -import DateRangePicker from "../../components/common/DateRangePicker"; +import DateRangePicker, { DateRangePicker1 } from "../../components/common/DateRangePicker"; import SelectMultiple from "../../components/common/SelectMultiple"; +import { localToUtc } from "../../utils/appUtils"; const defaultGalleryFilterValues = { buildingIds: [], @@ -58,27 +59,21 @@ const ImageGalleryFilters = ({ [setValue] ); - // Submit → Apply filters - const onSubmit = useCallback( - (formData) => { - const payload = { + const onSubmit =(formData)=>{ + const inputStartDate = localToUtc(formData.startDate) + const inputEndDate = localToUtc(formData.endDate) + const payload = { ...formData, - startDate: formData.startDate - ? moment(formData.startDate).utc().toISOString() - : null, - endDate: formData.endDate - ? moment(formData.endDate).utc().toISOString() - : null, + startDate: inputStartDate, + endDate: inputEndDate, }; - onApplyFilters(payload); - }, - [onApplyFilters] - ); + onApplyFilters(payload); + } // Clear all filters const onClear = useCallback(() => { reset(defaultGalleryFilterValues); - setResetKey((prev) => prev + 1); // reset DateRangePicker + setResetKey((prev) => prev + 1); onApplyFilters(defaultGalleryFilterValues); }, [onApplyFilters, reset]); @@ -86,13 +81,12 @@ const ImageGalleryFilters = ({
- {/* Date Range */}
-
diff --git a/src/components/common/DateRangePicker.jsx b/src/components/common/DateRangePicker.jsx index c349e25a..5973d4f4 100644 --- a/src/components/common/DateRangePicker.jsx +++ b/src/components/common/DateRangePicker.jsx @@ -67,6 +67,8 @@ const DateRangePicker = ({ style={{ right: "22px", bottom: "-8px" }} >
+ + ); }; diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js index 21f7fa5a..8ee3b42e 100644 --- a/src/utils/appUtils.js +++ b/src/utils/appUtils.js @@ -72,8 +72,11 @@ export const normalizeAllowedContentTypes = (allowedContentType) => { export function localToUtc(localDateString) { - if (!localDateString || localDateString.trim() === "") return null; // return null instead of undefined - const date = new Date(localDateString); - if (isNaN(date.getTime())) return null; // invalid date check + if (!localDateString || localDateString.trim() === "") return null; + + const [day, month, year] = localDateString.split("-").map(Number); + if (!day || !month || !year) return null; + + const date = new Date(Date.UTC(year, month - 1, day)); return date.toISOString(); -} \ No newline at end of file +} -- 2.43.0 From eaccf0fca288e1ef8d3f0b24e63785d059db4114 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Sat, 20 Sep 2025 14:33:22 +0530 Subject: [PATCH 06/11] Changes in Image-Gallery Page then DateRange cross button will work correctly. --- src/pages/ImageGallery/ImageGalleryPage.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index c1d52bbe..0f6ff869 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -99,15 +99,23 @@ const ImageGalleryPage = () => { const handleRemoveFilter = (filterKey, valueId) => { setAppliedFilters((prev) => { const updated = { ...prev }; + if (Array.isArray(updated[filterKey])) { updated[filterKey] = updated[filterKey].filter((id) => id !== valueId); - } else if (filterKey === "startDate" || filterKey === "endDate") { + } + else if (filterKey === "startDate" || filterKey === "endDate") { updated[filterKey] = null; } + else if (filterKey === "dateRange") { + updated.startDate = null; + updated.endDate = null; + } + return updated; }); }; + // --- Chips --- const appliedFiltersChips = useMemo(() => { const chips = []; -- 2.43.0 From 32bcaf2602c46eee2eed2a3d34291009f279330f Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Sat, 20 Sep 2025 14:52:32 +0530 Subject: [PATCH 07/11] Adding Card inside Image Gallery. --- src/pages/ImageGallery/ImageGalleryPage.jsx | 135 ++++++-------------- 1 file changed, 42 insertions(+), 93 deletions(-) diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index 0f6ff869..85f8cf85 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -22,14 +22,12 @@ const ImageGalleryPage = () => { const { openModal } = useModal(); const { setOffcanvasContent, setShowTrigger } = useFab(); - // Auto-select first project if none selected useEffect(() => { if (!selectedProjectId && projectNames?.length) { dispatch(setProjectId(projectNames[0].id)); } }, [selectedProjectId, projectNames, dispatch]); - // --- Filters state --- const [appliedFilters, setAppliedFilters] = useState({ buildingIds: [], floorIds: [], @@ -41,18 +39,11 @@ const ImageGalleryPage = () => { endDate: null, }); - const { - data, - fetchNextPage, - hasNextPage, - isLoading, - isFetchingNextPage, - refetch, - } = useImageGallery(selectedProjectId, appliedFilters); + const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage, refetch } = + useImageGallery(selectedProjectId, appliedFilters); const images = data?.pages.flatMap((page) => page.data) || []; - // --- Utility: store mappings independent of images --- const [labelMaps, setLabelMaps] = useState({ buildings: new Map(), floors: new Map(), @@ -92,31 +83,21 @@ const ImageGalleryPage = () => { }); }, [images]); - // --- Apply filters --- const handleApplyFilters = useCallback((values) => setAppliedFilters(values), []); - // --- Remove single filter --- const handleRemoveFilter = (filterKey, valueId) => { setAppliedFilters((prev) => { const updated = { ...prev }; - if (Array.isArray(updated[filterKey])) { updated[filterKey] = updated[filterKey].filter((id) => id !== valueId); - } - else if (filterKey === "startDate" || filterKey === "endDate") { - updated[filterKey] = null; - } - else if (filterKey === "dateRange") { + } else if (filterKey === "startDate" || filterKey === "endDate" || filterKey === "dateRange") { updated.startDate = null; updated.endDate = null; } - return updated; }); }; - - // --- Chips --- const appliedFiltersChips = useMemo(() => { const chips = []; const { buildings, floors, activities, workAreas, workCategories, uploadedByUsers } = labelMaps; @@ -139,6 +120,7 @@ const ImageGalleryPage = () => { appliedFilters.workCategoryIds?.forEach((id) => chips.push({ label: "Work Category", value: workCategories.get(id) || id, key: "workCategoryIds", id }) ); + if (appliedFilters.startDate || appliedFilters.endDate) { const start = appliedFilters.startDate ? moment(appliedFilters.startDate).format("DD MMM, YYYY") : ""; const end = appliedFilters.endDate ? moment(appliedFilters.endDate).format("DD MMM, YYYY") : ""; @@ -147,10 +129,8 @@ const ImageGalleryPage = () => { return chips; }, [appliedFilters, labelMaps]); - // --- Refetch on filter change --- useEffect(() => { refetch(); }, [appliedFilters, refetch]); - // --- Filter Panel --- const filterPanelElement = useMemo( () => ( { [labelMaps, appliedFilters, handleApplyFilters] ); - // --- Fab Offcanvas --- useEffect(() => { setShowTrigger(true); setOffcanvasContent("Gallery Filters", filterPanelElement); @@ -178,14 +157,12 @@ const ImageGalleryPage = () => { }; }, [filterPanelElement, setOffcanvasContent, setShowTrigger]); - // --- EventBus --- useEffect(() => { const handler = (data) => { if (data.projectId === selectedProjectId) refetch(); }; eventBus.on("image_gallery", handler); return () => eventBus.off("image_gallery", handler); }, [selectedProjectId, refetch]); - // --- Infinite scroll --- useEffect(() => { if (!loaderRef.current) return; const observer = new IntersectionObserver( @@ -199,77 +176,49 @@ const ImageGalleryPage = () => { }, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]); return ( -
+
- {appliedFiltersChips.length > 0 && ( -
- Filters: + {/* Card wrapper */} +
+
- {/* Group chips by label */} - {["Building", "Floor", "Work Area", "Activity", "Uploaded By", "Work Category"].map((label) => { - const chipsForLabel = appliedFiltersChips.filter((chip) => chip.label === label); - if (chipsForLabel.length === 0) return null; - - return ( - - {label} : - {chipsForLabel.map((chip, idx) => ( - - {chip.value} -
)} + + {/* Gallery */} + {isLoading ? ( + + ) : ( + + )} +
- )} - - - {isLoading ? ( - - ) : ( - - )} - +
); }; -- 2.43.0 From 3df68dff894c1c67ef461839f593af8f1ebcaaf5 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Mon, 22 Sep 2025 17:13:16 +0530 Subject: [PATCH 08/11] Adding Services Dropdown in Image Gallery. --- src/pages/ImageGallery/ImageGalleryPage.jsx | 39 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index 85f8cf85..46c6c86a 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -6,7 +6,7 @@ import eventBus from "../../services/eventBus"; import Breadcrumb from "../../components/common/Breadcrumb"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; import useImageGallery from "../../hooks/useImageGallery"; -import { useProjectName } from "../../hooks/useProjects"; +import { useProjectAssignedServices, useProjectName } from "../../hooks/useProjects"; import { setProjectId } from "../../slices/localVariablesSlice"; import ImageGalleryListView from "../../components/ImageGallery/ImageGalleryListView"; import ImageGalleryFilters from "../../components/ImageGallery/ImageGalleryFilters"; @@ -22,6 +22,15 @@ const ImageGalleryPage = () => { const { openModal } = useModal(); const { setOffcanvasContent, setShowTrigger } = useFab(); + const { data: assignedServices = [], isLoading: servicesLoading } = + useProjectAssignedServices(selectedProjectId); + + const [selectedService, setSelectedService] = useState(""); + + const handleServiceChange = (e) => { + setSelectedService(e.target.value); + }; + useEffect(() => { if (!selectedProjectId && projectNames?.length) { dispatch(setProjectId(projectNames[0].id)); @@ -182,10 +191,36 @@ const ImageGalleryPage = () => { {/* Card wrapper */}
+
+ {!servicesLoading && assignedServices?.length > 0 && ( + assignedServices.length > 1 ? ( + + ) : ( +
{assignedServices[0].name}
+ ) + )} +
{/* Filter Chips */} {appliedFiltersChips.length > 0 && ( -
+
Filters: {appliedFiltersChips.map((chip, idx) => ( -- 2.43.0 From 59b62d2f104373b7c7af4d6c31ff33345b097803 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 23 Sep 2025 10:27:14 +0530 Subject: [PATCH 09/11] Changes in Images gallery popup and page. --- .../ImageGallery/ImageGalleryListView.jsx | 24 +++- src/components/ImageGallery/ImagePopup.jsx | 110 ++++++++++++------ src/pages/ImageGallery/ImageGalleryPage.jsx | 54 ++++++--- 3 files changed, 133 insertions(+), 55 deletions(-) diff --git a/src/components/ImageGallery/ImageGalleryListView.jsx b/src/components/ImageGallery/ImageGalleryListView.jsx index 157fbafa..3d16ff6c 100644 --- a/src/components/ImageGallery/ImageGalleryListView.jsx +++ b/src/components/ImageGallery/ImageGalleryListView.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useCallback } from "react"; +import React, { useRef, useState, useCallback, useEffect } from "react"; import Avatar from "../../components/common/Avatar"; import ImagePopup from "./ImagePopup"; @@ -14,8 +14,23 @@ const ImageGalleryListView = ({ moment, }) => { const [hoveredImage, setHoveredImage] = useState(null); + const [scrollThreshold, setScrollThreshold] = useState(5); const imageGroupRefs = useRef({}); + useEffect(() => { + const updateThreshold = () => { + if (window.innerWidth >= 1400) setScrollThreshold(6); // xl screens + else if (window.innerWidth >= 992) setScrollThreshold(5); // lg + else if (window.innerWidth >= 768) setScrollThreshold(4); // md + else setScrollThreshold(3); // sm & xs + }; + updateThreshold(); + window.addEventListener("resize", updateThreshold); + return () => window.removeEventListener("resize", updateThreshold); + }, []); + + + const scrollLeft = useCallback( (key) => imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }), @@ -37,11 +52,10 @@ const ImageGalleryListView = ({ ) : images.length ? ( images.map((batch) => { const doc = batch.documents[0]; - const userName = `${doc.uploadedBy?.firstName || ""} ${ - doc.uploadedBy?.lastName || "" - }`.trim(); + const userName = `${doc.uploadedBy?.firstName || ""} ${doc.uploadedBy?.lastName || "" + }`.trim(); const date = formatUTCToLocalTime(doc.uploadedAt); - const hasArrows = batch.documents.length > SCROLL_THRESHOLD; + const hasArrows = batch.documents.length > scrollThreshold; return (
diff --git a/src/components/ImageGallery/ImagePopup.jsx b/src/components/ImageGallery/ImagePopup.jsx index 9d773195..f197c778 100644 --- a/src/components/ImageGallery/ImagePopup.jsx +++ b/src/components/ImageGallery/ImagePopup.jsx @@ -1,90 +1,128 @@ import React, { useState, useEffect } from "react"; -import "./ImagePopup.css" import { useModal } from "./ModalContext"; -import moment from "moment"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; const ImagePopup = ({ batch, initialIndex = 0 }) => { const { closeModal } = useModal(); const [currentIndex, setCurrentIndex] = useState(initialIndex); - // Effect to update currentIndex if the initialIndex prop changes useEffect(() => { setCurrentIndex(initialIndex); }, [initialIndex, batch]); - // If no batch or documents are provided, don't render if (!batch || !batch.documents || batch.documents.length === 0) return null; - // Get the current image document from the batch's documents array const image = batch.documents[currentIndex]; - - // Fallback if for some reason the image at the current index doesn't exist if (!image) return null; - // Format details for display from the individual image document const fullName = `${image.uploadedBy?.firstName || ""} ${image.uploadedBy?.lastName || "" }`.trim(); const date = formatUTCToLocalTime(image.uploadedAt); - // Location and category details from the 'batch' object (as previously corrected) const buildingName = batch.buildingName; const floorName = batch.floorName; const workAreaName = batch.workAreaName; const activityName = batch.activityName; const batchComment = batch.comment; - // Handler for navigating to the previous image const handlePrev = () => { setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1)); }; - // Handler for navigating to the next image const handleNext = () => { setCurrentIndex((prevIndex) => Math.min(batch.documents.length - 1, prevIndex + 1) ); }; - // Determine if previous/next buttons should be enabled/visible const hasPrev = currentIndex > 0; const hasNext = currentIndex < batch.documents.length - 1; return ( -
-
+
+
+
+ {/* Header */} +
+
Image Preview
+ +
- + {/* Body */} +
+
+ {hasPrev && ( + + )} - {hasPrev && ( - - )} + Preview -
- Preview -
+ {hasNext && ( + + )} +
- {hasNext && ( - - )} + {/* Details */} +
+

+ + Uploaded By: + {fullName} +

-
- -
Uploaded By : {fullName}
-
Date : {date}
-
Uploaded By : {buildingName} {floorName} - {workAreaName || "Unknown"} {activityName}
-
comment : {batchComment}
+

+ + Date: + {date} +

+

+ + Location: + + {buildingName} {floorName}{" "} + {workAreaName || "Unknown"}{" "} + {activityName} + +

+

+ + Comment: + {batchComment} +

+
+
); }; -export default ImagePopup; \ No newline at end of file +export default ImagePopup; diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index 46c6c86a..e5e9b52d 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -218,24 +218,50 @@ const ImageGalleryPage = () => { )}
+ {/* Filter Chips */} {/* Filter Chips */} {appliedFiltersChips.length > 0 && ( -
- Filters: - {appliedFiltersChips.map((chip, idx) => ( - - {chip.label} : {chip.value} -
+ ); + })} + + {/* Date Range */} + {appliedFiltersChips.some(c => c.label === "Date Range") && ( +
+ Date Range: + {appliedFiltersChips.filter(c => c.label === "Date Range").map((chip, idx) => ( + + {chip.value} +
+ )}
)} - {/* Gallery */} {isLoading ? ( -- 2.43.0 From 1e25e2d1adf3f317ec8c41ab55b2a4b336ee176f Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 23 Sep 2025 11:38:28 +0530 Subject: [PATCH 10/11] set ImageGallery filter minheight. --- src/pages/ImageGallery/ImageGalleryPage.jsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index e5e9b52d..74d50208 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -190,7 +190,12 @@ const ImageGalleryPage = () => { {/* Card wrapper */}
-
+
{!servicesLoading && assignedServices?.length > 0 && ( assignedServices.length > 1 ? ( @@ -218,12 +223,10 @@ const ImageGalleryPage = () => { )}
- {/* Filter Chips */} {/* Filter Chips */} {appliedFiltersChips.length > 0 && (
Filters: - {["Building", "Floor", "Work Area", "Activity", "Uploaded By", "Work Category"].map((label) => { const chips = appliedFiltersChips.filter(c => c.label === label); if (!chips.length) return null; @@ -231,7 +234,10 @@ const ImageGalleryPage = () => {
{label}: {chips.map(chip => ( - + {chip.value}
)} + {/* Gallery */} {isLoading ? ( @@ -277,7 +284,6 @@ const ImageGalleryPage = () => { moment={moment} /> )} -
-- 2.43.0 From c2bac4e640fbda63b941ee65818648057832c97e Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 23 Sep 2025 16:17:28 +0530 Subject: [PATCH 11/11] Integrating api for imageGallery. --- .../ImageGallery/ImageGalleryListView.jsx | 211 ++++++++---------- src/hooks/useImageGallery.js | 17 +- src/pages/ImageGallery/ImageGalleryPage.jsx | 3 +- 3 files changed, 102 insertions(+), 129 deletions(-) diff --git a/src/components/ImageGallery/ImageGalleryListView.jsx b/src/components/ImageGallery/ImageGalleryListView.jsx index 3d16ff6c..e9f8f8e0 100644 --- a/src/components/ImageGallery/ImageGalleryListView.jsx +++ b/src/components/ImageGallery/ImageGalleryListView.jsx @@ -9,7 +9,6 @@ const ImageGalleryListView = ({ hasNextPage, loaderRef, openModal, - SCROLL_THRESHOLD, formatUTCToLocalTime, moment, }) => { @@ -19,18 +18,16 @@ const ImageGalleryListView = ({ useEffect(() => { const updateThreshold = () => { - if (window.innerWidth >= 1400) setScrollThreshold(6); // xl screens - else if (window.innerWidth >= 992) setScrollThreshold(5); // lg - else if (window.innerWidth >= 768) setScrollThreshold(4); // md - else setScrollThreshold(3); // sm & xs + if (window.innerWidth >= 1400) setScrollThreshold(6); + else if (window.innerWidth >= 992) setScrollThreshold(5); + else if (window.innerWidth >= 768) setScrollThreshold(4); + else setScrollThreshold(3); }; updateThreshold(); window.addEventListener("resize", updateThreshold); return () => window.removeEventListener("resize", updateThreshold); }, []); - - const scrollLeft = useCallback( (key) => imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }), @@ -42,132 +39,104 @@ const ImageGalleryListView = ({ [] ); + if (!images.length && !isLoading) { + return

No images match the selected filters.

; + } + return (
- {isLoading ? ( -
-

Loading...

-
- ) : images.length ? ( - images.map((batch) => { - const doc = batch.documents[0]; - const userName = `${doc.uploadedBy?.firstName || ""} ${doc.uploadedBy?.lastName || "" - }`.trim(); - const date = formatUTCToLocalTime(doc.uploadedAt); - const hasArrows = batch.documents.length > scrollThreshold; + {images.map((batch) => { + if (!batch.documents?.length) return null; // skip empty batches - return ( -
-
- {/* Uploader Info */} -
- -
- {userName} - {date} -
-
+ const doc = batch.documents[0]; + const userName = `${doc.uploadedBy?.firstName || ""} ${doc.uploadedBy?.lastName || ""}`.trim(); + const date = formatUTCToLocalTime(doc.uploadedAt); + const hasArrows = batch.documents.length > scrollThreshold; - {/* Location Info */} -
-
- - {batch.buildingName} - - - - {batch.floorName} - - - - {batch.workAreaName || "Unknown"} - - {batch.activityName} - -
- {batch.workCategoryName && ( - - {batch.workCategoryName} - - )} + return ( +
+
+
+ +
+ {userName} + {date}
- {/* Images */} -
- {hasArrows && ( - - )} -
- (imageGroupRefs.current[batch.batchId] = el) - } - > - {batch.documents.map((d, i) => { - const hoverDate = moment(d.uploadedAt).format( - "DD MMMM, YYYY" - ); - const hoverTime = moment(d.uploadedAt).format("hh:mm A"); - - return ( -
- openModal() - } - onMouseEnter={() => setHoveredImage(d)} - onMouseLeave={() => setHoveredImage(null)} - > -
- {`Image -
- {hoveredImage === d && ( -
-

- Date: {hoverDate} -

-

- Time: {hoverTime} -

-

- Activity: {batch.activityName} -

-
- )} -
- ); - })} +
+
+ + {batch.buildingName} + + + + {batch.floorName} + + + + {batch.workAreaName || "Unknown"} + + {batch.activityName} +
- {hasArrows && ( - + {batch.workCategoryName && ( + {batch.workCategoryName} )}
- ); - }) - ) : ( -

- No images match the selected filters. -

- )} + +
+ {hasArrows && ( + + )} +
(imageGroupRefs.current[batch.batchId] = el)} + > + {batch.documents.map((d, i) => { + const hoverDate = moment(d.uploadedAt).format("DD MMMM, YYYY"); + const hoverTime = moment(d.uploadedAt).format("hh:mm A"); + + return ( +
openModal()} + onMouseEnter={() => setHoveredImage(d)} + onMouseLeave={() => setHoveredImage(null)} + > +
+ {`Image +
+ {hoveredImage === d && ( +
+

Date: {hoverDate}

+

Time: {hoverTime}

+

Activity: {batch.activityName}

+
+ )} +
+ ); + })} +
+ {hasArrows && ( + + )} +
+
+ ); + })}
{isFetchingNextPage && hasNextPage &&

Loading...

} diff --git a/src/hooks/useImageGallery.js b/src/hooks/useImageGallery.js index b1511d93..4df92fc9 100644 --- a/src/hooks/useImageGallery.js +++ b/src/hooks/useImageGallery.js @@ -89,16 +89,19 @@ import { useInfiniteQuery } from "@tanstack/react-query"; const PAGE_SIZE = 10; const useImageGallery = (selectedProjectId, filters) => { - const hasFilters = filters && Object.values(filters).some( - value => Array.isArray(value) ? value.length > 0 : value !== null && value !== "" - ); + const hasFilters = + filters && + Object.values(filters).some((value) => + Array.isArray(value) ? value.length > 0 : value !== null && value !== "" + ); return useInfiniteQuery({ queryKey: ["imageGallery", selectedProjectId, hasFilters ? filters : null], enabled: !!selectedProjectId, - getNextPageParam: (lastPage, allPages) => { - if (!lastPage?.data?.length) return undefined; - return allPages.length + 1; + getNextPageParam: (lastPage) => { + const currentPage = lastPage?.data?.currentPage || 1; + const totalPages = lastPage?.data?.totalPages || 1; + return currentPage < totalPages ? currentPage + 1 : undefined; }, queryFn: async ({ pageParam = 1 }) => { const res = await ImageGalleryAPI.ImagesGet( @@ -107,7 +110,7 @@ const useImageGallery = (selectedProjectId, filters) => { pageParam, PAGE_SIZE ); - return res; + return res.data; // Important: use res.data to match API response }, }); }; diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx index 74d50208..f191ed48 100644 --- a/src/pages/ImageGallery/ImageGalleryPage.jsx +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -185,7 +185,8 @@ const ImageGalleryPage = () => { }, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]); return ( -
+ +
{/* Card wrapper */} -- 2.43.0