diff --git a/src/data/menuData.json b/src/data/menuData.json index cadf34bf..bbd37e2c 100644 --- a/src/data/menuData.json +++ b/src/data/menuData.json @@ -50,7 +50,7 @@ { "text": "Daily Expenses", "available": true, - "link": "/activities/gallary" + "link": "/activities/reports" } ] }, diff --git a/src/pages/Gallary/ImageGallary.jsx b/src/pages/Gallary/ImageGallary.jsx index bfa30e79..016d2bc6 100644 --- a/src/pages/Gallary/ImageGallary.jsx +++ b/src/pages/Gallary/ImageGallary.jsx @@ -1,4 +1,3 @@ -// ImageGallery.js import React, { useState, useEffect, useRef, useCallback } from "react"; import "./ImageGallery.css"; import { ImageGalleryAPI } from "./ImageGalleryAPI"; @@ -7,10 +6,19 @@ import { useSelector } from "react-redux"; import { useModal } from "./ModalContext"; import ImagePop from "./ImagePop"; import Avatar from "../../components/common/Avatar"; -import DateRangePicker from "../../components/common/DateRangePicker"; // Assuming this is the path to your DateRangePicker +import DateRangePicker from "../../components/common/DateRangePicker"; +import eventBus from "../../services/eventBus"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import {formatUTCToLocalTime} from "../../utils/dateUtils"; + +const PAGE_SIZE = 10; +const SCROLL_THRESHOLD = 5; const ImageGallery = () => { const [images, setImages] = useState([]); + const [allImagesData, setAllImagesData] = useState([]); + const [pageNumber, setPageNumber] = useState(1); + const [hasMore, setHasMore] = useState(true); const selectedProjectId = useSelector((store) => store.localVariables.projectId); const { openModal } = useModal(); @@ -51,8 +59,10 @@ const ImageGallery = () => { const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); const [hoveredImage, setHoveredImage] = useState(null); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); const imageGroupRefs = useRef({}); + const loaderRef = useRef(null); const filterPanelRef = useRef(null); const filterButtonRef = useRef(null); @@ -68,61 +78,159 @@ const ImageGallery = () => { } }; - document.addEventListener("mousedown", handleClickOutside); + if (isFilterPanelOpen) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); + } + return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, []); + }, [isFilterPanelOpen]); useEffect(() => { if (!selectedProjectId) { setImages([]); + setAllImagesData([]); setLoading(false); + setHasMore(false); return; } + setImages([]); + setPageNumber(1); + setHasMore(true); setLoading(true); - ImageGalleryAPI.ImagesGet(selectedProjectId, appliedFilters) - .then((res) => { - setImages(res.data); - }) - .catch((err) => { - console.error("Error fetching images:", err); - setImages([]); - }) - .finally(() => { - setLoading(false); - }); + setAllImagesData([]); + fetchImages(1, appliedFilters, true); }, [selectedProjectId, appliedFilters]); - const getUniqueValuesWithIds = useCallback( - (idKey, nameKey) => { - const uniqueMap = new Map(); - images.forEach((img) => { - if (img[idKey] && img[nameKey]) { - uniqueMap.set(img[idKey], img[nameKey]); - } + const fetchImages = useCallback(async (page, filters) => { + if (!selectedProjectId) return; + + try { + if (page === 1) { + setLoading(true); + } else { + setLoadingMore(true); + } + + const res = await ImageGalleryAPI.ImagesGet(selectedProjectId, filters, page, PAGE_SIZE); + const newBatches = res.data || []; + const receivedCount = newBatches.length; + + setImages((prevImages) => { + const uniqueNewBatches = newBatches.filter( + (newBatch) => !prevImages.some((prevBatch) => prevBatch.batchId === newBatch.batchId) + ); + return [...prevImages, ...uniqueNewBatches]; }); - return Array.from(uniqueMap.entries()); - }, - [images] - ); + + setAllImagesData((prevAllImages) => { + const uniqueAllImages = newBatches.filter( + (newBatch) => !prevAllImages.some((prevBatch) => prevBatch.batchId === newBatch.batchId) + ); + return [...prevAllImages, ...uniqueAllImages]; + }); + + setHasMore(receivedCount === PAGE_SIZE); + } catch (err) { + console.error("Error fetching images:", err); + if (page === 1) { + setImages([]); + setAllImagesData([]); + } + setHasMore(false); + } finally { + setLoading(false); + setLoadingMore(false); + } + }, [selectedProjectId]); + + useEffect(() => { + const handleExternalEvent = (data) => { + if (selectedProjectId === data.projectId) { + setImages([]); + setAllImagesData([]); + setPageNumber(1); + setHasMore(true); + fetchImages(1, appliedFilters, true); + } + }; + + eventBus.on("image_gallery", handleExternalEvent); + + return () => { + eventBus.off("image_gallery", handleExternalEvent); + }; + }, [appliedFilters, fetchImages, selectedProjectId]); + + useEffect(() => { + if (!loaderRef.current) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loadingMore && !loading) { + setPageNumber((prevPageNumber) => prevPageNumber + 1); + } + }, + { + root: null, + rootMargin: "200px", + threshold: 0.1, + } + ); + + observer.observe(loaderRef.current); + + return () => { + if (loaderRef.current) { + observer.unobserve(loaderRef.current); + } + }; + }, [hasMore, loadingMore, loading]); + + useEffect(() => { + if (pageNumber > 1) { + fetchImages(pageNumber, appliedFilters); + } + }, [pageNumber, fetchImages, appliedFilters]); + + const getUniqueValuesWithIds = useCallback((idKey, nameKey) => { + const map = new Map(); + allImagesData.forEach(batch => { + let id; + if (idKey === "floorIds") { + id = batch.floorIds; + } else { + id = batch[idKey]; + } + + const name = batch[nameKey]; + + if (id && name && !map.has(id)) { + map.set(id, name); + } + }); + return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1])); + }, [allImagesData]); const getUniqueUploadedByUsers = useCallback(() => { const uniqueUsersMap = new Map(); - images.forEach((img) => { - if (img.uploadedBy && img.uploadedBy.id) { - const fullName = `${img.uploadedBy.firstName || ""} ${ - img.uploadedBy.lastName || "" - }`.trim(); - if (fullName) { - uniqueUsersMap.set(img.uploadedBy.id, fullName); + allImagesData.forEach(batch => { + batch.documents.forEach(doc => { + if (doc.uploadedBy && doc.uploadedBy.id) { + const fullName = `${doc.uploadedBy.firstName || ""} ${doc.uploadedBy.lastName || ""}`.trim(); + if (fullName) { + uniqueUsersMap.set(doc.uploadedBy.id, fullName); + } } - } + }); }); - return Array.from(uniqueUsersMap.entries()); - }, [images]); + return Array.from(uniqueUsersMap.entries()).sort((a, b) => a[1].localeCompare(b[1])); + }, [allImagesData]); const buildings = getUniqueValuesWithIds("buildingId", "buildingName"); const floors = getUniqueValuesWithIds("floorIds", "floorName"); @@ -191,9 +299,35 @@ const ImageGallery = () => { startDate: selectedFilters.startDate || null, endDate: selectedFilters.endDate || null, }; - setAppliedFilters(payload); - setIsFilterPanelOpen(false); - }, [selectedFilters]); + + const areFiltersChanged = Object.keys(payload).some(key => { + const oldVal = appliedFilters[key]; + const newVal = payload[key]; + + if (Array.isArray(oldVal) && Array.isArray(newVal)) { + if (oldVal.length !== newVal.length) return true; + const oldSet = new Set(oldVal); + const newSet = new Set(newVal); + if (oldSet.size !== newSet.size) return true; + for (const item of newSet) { + if (!oldSet.has(item)) return true; + } + return false; + } + if ((oldVal === null && newVal === "") || (oldVal === "" && newVal === null)) { + return false; + } + return oldVal !== newVal; + }); + + if (areFiltersChanged) { + setAppliedFilters(payload); + setImages([]); + setPageNumber(1); + setHasMore(true); + } + // Removed setIsFilterPanelOpen(false); to keep the drawer open + }, [selectedFilters, appliedFilters]); const handleClearAllFilters = useCallback(() => { const initialStateSelected = { @@ -219,49 +353,11 @@ const ImageGallery = () => { endDate: null, }; setAppliedFilters(initialStateApplied); + setImages([]); + setPageNumber(1); + setHasMore(true); }, []); - const filteredImages = images.filter((img) => { - const uploadedAtMoment = moment(img.uploadedAt); - const startDateMoment = appliedFilters.startDate - ? moment(appliedFilters.startDate) - : null; - const endDateMoment = appliedFilters.endDate - ? moment(appliedFilters.endDate) - : null; - - const isWithinDateRange = - (!startDateMoment || uploadedAtMoment.isSameOrAfter(startDateMoment, "day")) && - (!endDateMoment || uploadedAtMoment.isSameOrBefore(endDateMoment, "day")); - - const passesCategoryFilters = - (appliedFilters.buildingIds === null || - appliedFilters.buildingIds.includes(img.buildingId)) && - (appliedFilters.floorIds === null || - appliedFilters.floorIds.includes(img.floorIds)) && - (appliedFilters.activityIds === null || - appliedFilters.activityIds.includes(img.activityId)) && - (appliedFilters.workAreaIds === null || - appliedFilters.workAreaIds.includes(img.workAreaId)) && - (appliedFilters.uploadedByIds === null || - appliedFilters.uploadedByIds.includes(img.uploadedBy?.id)) && - (appliedFilters.workCategoryIds === null || - appliedFilters.workCategoryIds.includes(img.workCategoryId)); - - return isWithinDateRange && passesCategoryFilters; - }); - - const imagesByActivityUser = {}; - filteredImages.forEach((img) => { - const userName = `${img.uploadedBy?.firstName || ""} ${ - img.uploadedBy?.lastName || "" - }`.trim(); - const workArea = img.workAreaName || "Unknown"; - const key = `${img.activityName}__${userName}__${workArea}`; - if (!imagesByActivityUser[key]) imagesByActivityUser[key] = []; - imagesByActivityUser[key].push(img); - }); - const scrollLeft = useCallback((key) => { imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }); }, []); @@ -271,8 +367,8 @@ const ImageGallery = () => { }, []); const renderFilterCategory = (label, items, type) => ( -
-
toggleCollapse(type)}> +
+
toggleCollapse(type)}> {label}
{type === "dateRange" && (selectedFilters.startDate || selectedFilters.endDate) && ( @@ -307,7 +403,15 @@ const ImageGallery = () => { {!collapsedFilters[type] && (
{type === "dateRange" ? ( - null +
+ +
) : ( items.map((item) => { const itemId = item[0]; @@ -334,38 +438,59 @@ const ImageGallery = () => { ); return ( -
+
+
+ +
- {loading ? ( + {loading && pageNumber === 1 ? (
- ) : Object.entries(imagesByActivityUser).length > 0 ? ( - Object.entries(imagesByActivityUser).map(([key, imgs]) => { - const [activity, userName, workArea] = key.split("__"); - const { buildingName, floorName, uploadedAt, workCategoryName } = imgs[0]; - const date = moment(uploadedAt).format("YYYY-MM-DD"); - const time = moment(uploadedAt).format("hh:mm A"); + ) : images.length > 0 ? ( + images.map((batch) => { + const firstDoc = batch.documents[0]; + const userName = `${firstDoc?.uploadedBy?.firstName || ""} ${firstDoc?.uploadedBy?.lastName || "" + }`.trim(); + const date = formatUTCToLocalTime(firstDoc?.uploadedAt) + + + + const showScrollButtons = batch.documents.length > SCROLL_THRESHOLD; return ( -
+
- {imgs[0].uploadedBy?.firstName}{" "} - {imgs[0].uploadedBy?.lastName} + {userName} - {date} {time} + {date}
@@ -373,13 +498,14 @@ const ImageGallery = () => {
- {buildingName} > {floorName} > {workArea} >{" "} - {activity} + {batch.buildingName} > {batch.floorName} >{" "} + {batch.workAreaName || "Unknown"} >{" "} + {batch.activityName}
- {workCategoryName && ( + {batch.workCategoryName && (
- {workCategoryName} + {batch.workCategoryName}
)} @@ -387,34 +513,36 @@ const ImageGallery = () => {
- + {showScrollButtons && ( + + )}
(imageGroupRefs.current[key] = el)} + ref={(el) => (imageGroupRefs.current[batch.batchId] = el)} > - {imgs.map((img, idx) => { - const hoverDate = moment(img.uploadedAt).format("YYYY-MM-DD"); - const hoverTime = moment(img.uploadedAt).format("hh:mm A"); + {batch.documents.map((doc, idx) => { + const hoverDate = moment(doc.uploadedAt).format("DD-MM-YYYY"); + const hoverTime = moment(doc.uploadedAt).format("hh:mm A"); return (
- openModal() + openModal() } - onMouseEnter={() => setHoveredImage(img)} + onMouseEnter={() => setHoveredImage(doc)} onMouseLeave={() => setHoveredImage(null)} >
- {`Image + {`Image
- {hoveredImage === img && ( + {hoveredImage === doc && (

Date: {hoverDate} @@ -423,7 +551,7 @@ const ImageGallery = () => { Time: {hoverTime}

- Activity: {img.activityName} + Activity: {batch.activityName}

)} @@ -431,69 +559,52 @@ const ImageGallery = () => { ); })}
- + {showScrollButtons && ( + + )}
); }) ) : ( -

+ !loading &&

No images match the selected filters.

)} + +
+ {loadingMore && hasMore &&
} + {!hasMore && !loading && images.length > 0 && ( +

You've reached the end of the images.

+ )} +
-
- -
-
-
toggleCollapse('dateRange')}> - Date Range - - - {collapsedFilters.dateRange ? '+' : '-'} - -
- {!collapsedFilters.dateRange && ( -
- -
- )} -
- {renderFilterCategory("Building", buildings, "building")} - {renderFilterCategory("Floor", floors, "floor")} - {renderFilterCategory("Work Area", workAreas, "workArea")} - {renderFilterCategory("Activity", activities, "activity")} - {renderFilterCategory("Uploaded By (User)", uploadedByUsers, "uploadedBy")} - {renderFilterCategory("Work Category", workCategories, "workCategory")} - -
+
+
+
+ Filters +
+ +
+
@@ -501,6 +612,16 @@ const ImageGallery = () => { Apply Filters
+
+ {renderFilterCategory("Date Range", [], "dateRange")} + {renderFilterCategory("Building", buildings, "building")} + {renderFilterCategory("Floor", floors, "floor")} + {renderFilterCategory("Work Area", workAreas, "workArea")} + {renderFilterCategory("Activity", activities, "activity")} + {renderFilterCategory("Uploaded By (User)", uploadedByUsers, "uploadedBy")} + {renderFilterCategory("Work Category", workCategories, "workCategory")} + +
diff --git a/src/pages/Gallary/ImageGallery.css b/src/pages/Gallary/ImageGallery.css index 377fe531..55c9b625 100644 --- a/src/pages/Gallary/ImageGallery.css +++ b/src/pages/Gallary/ImageGallery.css @@ -1,13 +1,9 @@ -/* ImageGallery.css */ .gallery-container { display: grid; - grid-template-columns: 1fr 50px; gap: 4px; - padding: 25px; + /* padding: 25px; */ font-family: sans-serif; - height: calc(100vh - 20px); box-sizing: border-box; - background-color: #f7f9fc; transition: grid-template-columns 0.3s ease-in-out; } @@ -69,8 +65,9 @@ transition: background-color 0.2s ease, box-shadow 0.2s ease, width 0.3s ease-in-out, height 0.3s ease-in-out, border-radius 0.3s ease-in-out, padding 0.3s ease-in-out; position: absolute; - top: 0; + top: 150px; right: 0; + position: fixed; height: 40px; width: 40px; z-index: 100; @@ -140,7 +137,7 @@ border-radius: 0 0 4px 4px; max-height: 150px; /* Default max-height for scrollable dropdowns */ - overflow-y: auto; + /* Default overflow for scrollable dropdowns */ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } @@ -197,9 +194,6 @@ transition: background 0.2s; } -.dropdown-content label:hover { - background-color: #eef2ff; -} .dropdown-content input[type="checkbox"] { -webkit-appearance: none; @@ -400,6 +394,7 @@ -webkit-overflow-scrolling: touch; scroll-behavior: smooth; width: 100%; + margin-left: 34px; } .scroll-arrow { @@ -543,4 +538,4 @@ hr { .datepicker { margin-right: 135px; margin-top: 6px; -} \ No newline at end of file +} diff --git a/src/pages/Gallary/ImageGalleryAPI.jsx b/src/pages/Gallary/ImageGalleryAPI.jsx index 13e6a56a..9ca09bfe 100644 --- a/src/pages/Gallary/ImageGalleryAPI.jsx +++ b/src/pages/Gallary/ImageGalleryAPI.jsx @@ -1,9 +1,9 @@ import { api } from "../../utils/axiosClient"; export const ImageGalleryAPI = { - - ImagesGet: (projectId, filter) => { + ImagesGet: (projectId, filter, pageNumber, pageSize) => { const payloadJsonString = JSON.stringify(filter); - return api.get(`/api/image/images/${projectId}?filter=${payloadJsonString}`) + // Corrected API endpoint with pagination parameters + return api.get(`/api/image/images/${projectId}?filter=${payloadJsonString}&pageNumber=${pageNumber}&pageSize=${pageSize}`); }, -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/pages/Gallary/ImagePop.jsx b/src/pages/Gallary/ImagePop.jsx index 62f4b8da..86cad334 100644 --- a/src/pages/Gallary/ImagePop.jsx +++ b/src/pages/Gallary/ImagePop.jsx @@ -2,32 +2,38 @@ import React, { useState, useEffect } from "react"; import "./ImagePop.css"; import { useModal } from "./ModalContext"; import moment from "moment"; +import {formatUTCToLocalTime} from "../../utils/dateUtils"; -const ImagePop = ({ images, initialIndex = 0 }) => { +const ImagePop = ({ batch, initialIndex = 0 }) => { const { closeModal } = useModal(); - // State to keep track of the currently displayed image's index const [currentIndex, setCurrentIndex] = useState(initialIndex); - // Effect to update currentIndex if the initialIndex prop changes (e.g., if the modal is reused) + // Effect to update currentIndex if the initialIndex prop changes useEffect(() => { setCurrentIndex(initialIndex); - }, [initialIndex, images]); + }, [initialIndex, batch]); - // If no images are provided or the array is empty, don't render - if (!images || images.length === 0) return null; + // If no batch or documents are provided, don't render + if (!batch || !batch.documents || batch.documents.length === 0) return null; - // Get the current image based on currentIndex - const image = images[currentIndex]; + // 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 + // Format details for display from the individual image document const fullName = `${image.uploadedBy?.firstName || ""} ${ image.uploadedBy?.lastName || "" }`.trim(); - const date = moment(image.uploadedAt).format("YYYY-MM-DD"); - const time = moment(image.uploadedAt).format("hh:mm A"); + 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 = () => { @@ -37,13 +43,13 @@ const ImagePop = ({ images, initialIndex = 0 }) => { // Handler for navigating to the next image const handleNext = () => { setCurrentIndex((prevIndex) => - Math.min(images.length - 1, prevIndex + 1) + Math.min(batch.documents.length - 1, prevIndex + 1) ); }; // Determine if previous/next buttons should be enabled/visible const hasPrev = currentIndex > 0; - const hasNext = currentIndex < images.length - 1; + const hasNext = currentIndex < batch.documents.length - 1; return (
@@ -61,7 +67,7 @@ const ImagePop = ({ images, initialIndex = 0 }) => { )} {/* The main image display */} - Preview + Preview {/* Next button, only shown if there's a next image */} {hasNext && ( @@ -76,14 +82,15 @@ const ImagePop = ({ images, initialIndex = 0 }) => { 👤 Uploaded By: {fullName}

- 📅 Date: {date} {time} + 📅 Date: {date}

- 🏢 Location: {image.buildingName} >{" "} - {image.floorName} > {image.activityName} + 🏢 Location: {buildingName} > {floorName} >{" "} + {workAreaName || "Unknown"} > {activityName}

- 📝 Comments: {image.comment} + {/* Display the comment from the batch object */} + 📝 Comments: {batchComment || "N/A"}

diff --git a/src/services/signalRService.js b/src/services/signalRService.js index 9fdcab8b..b66b6736 100644 --- a/src/services/signalRService.js +++ b/src/services/signalRService.js @@ -103,6 +103,17 @@ export function startSignalR(loggedUser) { queryClient eventBus.emit("employee", data); } + + if (data.keyword == "Task_Report") { + if(data.numberOfImages > 0){ + eventBus.emit("image_gallery", data); + } + } + if (data.keyword == "Task_Comment") { + if(data.numberOfImages > 0){ + eventBus.emit("image_gallery", data); + } + } } }); diff --git a/src/utils/dateUtils.jsx b/src/utils/dateUtils.jsx index a2275042..69c66c23 100644 --- a/src/utils/dateUtils.jsx +++ b/src/utils/dateUtils.jsx @@ -1,4 +1,5 @@ -// utils/dateUtils.js +import moment from "moment"; + export const getDateDifferenceInDays = (startDate, endDate) => { if (!startDate || !endDate) { throw new Error("Both startDate and endDate must be provided"); @@ -59,7 +60,7 @@ export const checkIfCurrentDate = (dateString) => { currentDate.setHours(0, 0, 0, 0); inputDate.setHours(0, 0, 0, 0); - return currentDate.getTime() === inputDate.getTime(); + return currentDate?.getTime() === inputDate?.getTime(); }; export const formatNumber = (num) => {