From c23de9bf7ceec1c958f158f2906f0b0a278c9310 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 2 Sep 2025 15:45:12 +0530 Subject: [PATCH] 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: },