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)}
>
-

+
- {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 */}
-

+

{/* 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) => {