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/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..3e541012 --- /dev/null +++ b/src/components/ImageGallery/ImageGalleryFilters.jsx @@ -0,0 +1,174 @@ +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"; +import { localToUtc } from "../../utils/appUtils"; + +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, setValue } = 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]); + + // Handle date range change and set form values + const handleDateRangeChange = useCallback( + ({ startDate, endDate }) => { + setValue("startDate", startDate); + setValue("endDate", endDate); + }, + [setValue] + ); + + const onSubmit =(formData)=>{ + const inputStartDate = localToUtc(formData.startDate) + const inputEndDate = localToUtc(formData.endDate) + const payload = { + ...formData, + startDate: inputStartDate, + endDate: inputEndDate, + }; + onApplyFilters(payload); + } + + // Clear all filters + const onClear = useCallback(() => { + reset(defaultGalleryFilterValues); + setResetKey((prev) => prev + 1); + onApplyFilters(defaultGalleryFilterValues); + }, [onApplyFilters, reset]); + + return ( +
+ +
+
+ + +
+ + {/* 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; \ No newline at end of file diff --git a/src/components/ImageGallery/ImageGalleryListView.jsx b/src/components/ImageGallery/ImageGalleryListView.jsx new file mode 100644 index 00000000..e9f8f8e0 --- /dev/null +++ b/src/components/ImageGallery/ImageGalleryListView.jsx @@ -0,0 +1,152 @@ +import React, { useRef, useState, useCallback, useEffect } from "react"; +import Avatar from "../../components/common/Avatar"; +import ImagePopup from "./ImagePopup"; + +const ImageGalleryListView = ({ + images, + isLoading, + isFetchingNextPage, + hasNextPage, + loaderRef, + openModal, + formatUTCToLocalTime, + moment, +}) => { + const [hoveredImage, setHoveredImage] = useState(null); + const [scrollThreshold, setScrollThreshold] = useState(5); + const imageGroupRefs = useRef({}); + + useEffect(() => { + const updateThreshold = () => { + 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" }), + [] + ); + const scrollRight = useCallback( + (key) => + imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }), + [] + ); + + if (!images.length && !isLoading) { + return

No images match the selected filters.

; + } + + return ( +
+
+ {images.map((batch) => { + if (!batch.documents?.length) return null; // skip empty batches + + 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; + + return ( +
+
+
+ +
+ {userName} + {date} +
+
+ +
+
+ + {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 && ( + + )} +
+
+ ); + })} + +
+ {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..f197c778 --- /dev/null +++ b/src/components/ImageGallery/ImagePopup.jsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from "react"; +import { useModal } from "./ModalContext"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; + +const ImagePopup = ({ batch, initialIndex = 0 }) => { + const { closeModal } = useModal(); + const [currentIndex, setCurrentIndex] = useState(initialIndex); + + useEffect(() => { + setCurrentIndex(initialIndex); + }, [initialIndex, batch]); + + if (!batch || !batch.documents || batch.documents.length === 0) return null; + + const image = batch.documents[currentIndex]; + if (!image) return null; + + const fullName = `${image.uploadedBy?.firstName || ""} ${image.uploadedBy?.lastName || "" + }`.trim(); + const date = formatUTCToLocalTime(image.uploadedAt); + + const buildingName = batch.buildingName; + const floorName = batch.floorName; + const workAreaName = batch.workAreaName; + const activityName = batch.activityName; + const batchComment = batch.comment; + + const handlePrev = () => { + setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1)); + }; + + const handleNext = () => { + setCurrentIndex((prevIndex) => + Math.min(batch.documents.length - 1, prevIndex + 1) + ); + }; + + const hasPrev = currentIndex > 0; + const hasNext = currentIndex < batch.documents.length - 1; + + return ( +
+
+
+ {/* Header */} +
+
Image Preview
+ +
+ + {/* Body */} +
+
+ {hasPrev && ( + + )} + + Preview + + {hasNext && ( + + )} +
+ + {/* Details */} +
+

+ + Uploaded By: + {fullName} +

+ +

+ + Date: + {date} +

+ +

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

+ +

+ + Comment: + {batchComment} +

+
+
+
+
+
+ ); +}; + +export default ImagePopup; 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/components/common/DateRangePicker.jsx b/src/components/common/DateRangePicker.jsx index 72aad155..bd464500 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/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/main.tsx b/src/main.tsx index 90385d5b..d4eb60a1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,7 +6,7 @@ import App from './App.tsx' import { Provider } from 'react-redux'; import { store } from './store/store'; 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 52bee8cd..00000000 --- a/src/pages/Gallary/ImagePop.jsx +++ /dev/null @@ -1,111 +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; diff --git a/src/pages/ImageGallery/ImageGalleryPage.jsx b/src/pages/ImageGallery/ImageGalleryPage.jsx new file mode 100644 index 00000000..f191ed48 --- /dev/null +++ b/src/pages/ImageGallery/ImageGalleryPage.jsx @@ -0,0 +1,294 @@ +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 { useProjectAssignedServices, 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"; +import { useFab } from "../../Context/FabContext"; +import ImageGallerySkeleton from "../../components/Charts/ImageGallerySkeleton"; + +const ImageGalleryPage = () => { + const dispatch = useDispatch(); + const selectedProjectId = useSelector((store) => store.localVariables.projectId); + const { projectNames } = useProjectName(); + const loaderRef = useRef(null); + 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)); + } + }, [selectedProjectId, projectNames, dispatch]); + + const [appliedFilters, setAppliedFilters] = useState({ + buildingIds: [], + floorIds: [], + activityIds: [], + uploadedByIds: [], + workCategoryIds: [], + workAreaIds: [], + startDate: null, + endDate: null, + }); + + const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage, refetch } = + useImageGallery(selectedProjectId, appliedFilters); + + const images = data?.pages.flatMap((page) => page.data) || []; + + const [labelMaps, setLabelMaps] = useState({ + buildings: new Map(), + floors: new Map(), + activities: new Map(), + workAreas: new Map(), + workCategories: new Map(), + uploadedByUsers: new Map(), + }); + + useEffect(() => { + const buildingsMap = new Map(labelMaps.buildings); + const floorsMap = new Map(labelMaps.floors); + const activitiesMap = new Map(labelMaps.activities); + const workAreasMap = new Map(labelMaps.workAreas); + const workCategoriesMap = new Map(labelMaps.workCategories); + const uploadedByMap = new Map(labelMaps.uploadedByUsers); + + images.forEach((batch) => { + if (batch.buildingId && batch.buildingName) buildingsMap.set(batch.buildingId, batch.buildingName); + if (batch.floorIds && batch.floorName) floorsMap.set(batch.floorIds, batch.floorName); + if (batch.activityId && batch.activityName) activitiesMap.set(batch.activityId, batch.activityName); + if (batch.workAreaId && batch.workAreaName) workAreasMap.set(batch.workAreaId, batch.workAreaName); + if (batch.workCategoryId && batch.workCategoryName) workCategoriesMap.set(batch.workCategoryId, batch.workCategoryName); + batch.documents?.forEach((doc) => { + const name = `${doc.uploadedBy?.firstName || ""} ${doc.uploadedBy?.lastName || ""}`.trim(); + if (doc.uploadedBy?.id && name) uploadedByMap.set(doc.uploadedBy.id, name); + }); + }); + + setLabelMaps({ + buildings: buildingsMap, + floors: floorsMap, + activities: activitiesMap, + workAreas: workAreasMap, + workCategories: workCategoriesMap, + uploadedByUsers: uploadedByMap, + }); + }, [images]); + + const handleApplyFilters = useCallback((values) => setAppliedFilters(values), []); + + 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" || filterKey === "dateRange") { + updated.startDate = null; + updated.endDate = null; + } + return updated; + }); + }; + + const appliedFiltersChips = useMemo(() => { + const chips = []; + const { buildings, floors, activities, workAreas, workCategories, uploadedByUsers } = labelMaps; + + appliedFilters.buildingIds?.forEach((id) => + chips.push({ label: "Building", value: buildings.get(id) || id, key: "buildingIds", id }) + ); + appliedFilters.floorIds?.forEach((id) => + chips.push({ label: "Floor", value: floors.get(id) || id, key: "floorIds", id }) + ); + appliedFilters.workAreaIds?.forEach((id) => + chips.push({ label: "Work Area", value: workAreas.get(id) || id, key: "workAreaIds", id }) + ); + appliedFilters.activityIds?.forEach((id) => + chips.push({ label: "Activity", value: activities.get(id) || id, key: "activityIds", id }) + ); + appliedFilters.uploadedByIds?.forEach((id) => + chips.push({ label: "Uploaded By", value: uploadedByUsers.get(id) || id, key: "uploadedByIds", id }) + ); + 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") : ""; + chips.push({ label: "Date Range", value: `${start} - ${end}`, key: "dateRange" }); + } + return chips; + }, [appliedFilters, labelMaps]); + + useEffect(() => { refetch(); }, [appliedFilters, refetch]); + + const filterPanelElement = useMemo( + () => ( + + ), + [labelMaps, appliedFilters, handleApplyFilters] + ); + + useEffect(() => { + setShowTrigger(true); + setOffcanvasContent("Gallery Filters", filterPanelElement); + return () => { + setShowTrigger(false); + setOffcanvasContent("", null); + }; + }, [filterPanelElement, setOffcanvasContent, setShowTrigger]); + + 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 () => loaderRef.current && observer.unobserve(loaderRef.current); + }, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]); + + return ( + +
+ + + {/* Card wrapper */} +
+
+
+ {!servicesLoading && assignedServices?.length > 0 && ( + assignedServices.length > 1 ? ( + + ) : ( +
{assignedServices[0].name}
+ ) + )} +
+ + {/* 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; + return ( +
+ {label}: + {chips.map(chip => ( + + {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 ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default ImageGalleryPage; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 823ebb3a..1b02e874 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -22,7 +22,6 @@ import Inventory from "../pages/project/Inventory"; import AttendancePage from "../pages/Activities/AttendancePage"; 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"; @@ -37,6 +36,8 @@ 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"; + import DirectoryPage from "../pages/Directory/DirectoryPage"; import RootRedirect from "./RootRedirect"; import MainLogin from "../pages/authentication/MainLogin"; @@ -92,7 +93,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: },