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 (
+
+ );
+};
+
+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 ? (
+
+ ) : 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)}
+ >
+
+

+
+ {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 && (
+
+ )}
+
+
+

+
+
+ {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 ? (
-
- ) : 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)}
- >
-
-

-
- {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 && (
-
- )}
-
-
-

-
-
- {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: },