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) => (
Loading...
Date: {hoverDate}
Time: {hoverTime}
Activity:{" "} {batch.activityName}
No images match the selected filters.
)}Loading...
} {!hasNextPage && !isLoading && images.length > 0 && (You've reached the end of the images.
)}