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-MM-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;