marco.pms.web/src/pages/Gallary/ImageGallary.jsx

452 lines
16 KiB
JavaScript

import React, { useState, useEffect, useRef, useCallback } from "react";
import "./ImageGallery.css";
import moment from "moment";
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";
import eventBus from "../../services/eventBus";
import Breadcrumb from "../../components/common/Breadcrumb";
import {formatUTCToLocalTime} from "../../utils/dateUtils";
import DateRangePickerNoDefault from "./DateRangePickerNoDefault";
const SCROLL_THRESHOLD = 5;
const ImageGallery = () => {
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
const {
images,
allImagesData,
pageNumber,
setPageNumber,
hasMore,
loading,
loadingMore,
fetchImages,
resetGallery,
} = useImageGallery(selectedProjectId);
const { openModal } = useModal();
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
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);
useEffect(() => {
const handleClickOutside = (event) => {
if (
filterPanelRef.current &&
!filterPanelRef.current.contains(event.target) &&
filterButtonRef.current &&
!filterButtonRef.current.contains(event.target)
) {
setIsFilterPanelOpen(false);
}
};
if (isFilterPanelOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isFilterPanelOpen]);
useEffect(() => {
if (!selectedProjectId) {
resetGallery();
return;
}
resetGallery();
fetchImages(1, appliedFilters, true);
}, [selectedProjectId, appliedFilters]);
useEffect(() => {
const handleExternalEvent = (data) => {
if (selectedProjectId === data.projectId) {
resetGallery();
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]);
const getUniqueValuesWithIds = useCallback((idKey, nameKey) => {
const map = new Map();
allImagesData.forEach(batch => {
let id = idKey === "floorIds" ? batch.floorIds : 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();
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()).sort((a, b) => a[1].localeCompare(b[1]));
}, [allImagesData]);
const buildings = getUniqueValuesWithIds("buildingId", "buildingName");
const floors = getUniqueValuesWithIds("floorIds", "floorName");
const activities = getUniqueValuesWithIds("activityId", "activityName");
const workAreas = getUniqueValuesWithIds("workAreaId", "workAreaName");
const uploadedByUsers = getUniqueUploadedByUsers();
const workCategories = getUniqueValuesWithIds("workCategoryId", "workCategoryName");
const toggleFilter = useCallback((type, itemId, itemName) => {
setSelectedFilters((prev) => {
const current = prev[type];
const isSelected = current.some((item) => item[0] === itemId);
const newArray = isSelected
? current.filter((item) => item[0] !== itemId)
: [...current, [itemId, itemName]];
return {
...prev,
[type]: newArray,
};
});
}, []);
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.length ? selectedFilters.building.map((item) => item[0]) : null,
floorIds: selectedFilters.floor.length ? selectedFilters.floor.map((item) => item[0]) : null,
workAreaIds: selectedFilters.workArea.length ? selectedFilters.workArea.map((item) => item[0]) : null,
workCategoryIds: selectedFilters.workCategory.length ? selectedFilters.workCategory.map((item) => item[0]) : null,
activityIds: selectedFilters.activity.length ? selectedFilters.activity.map((item) => item[0]) : null,
uploadedByIds: selectedFilters.uploadedBy.length ? selectedFilters.uploadedBy.map((item) => item[0]) : null,
startDate: selectedFilters.startDate || null,
endDate: selectedFilters.endDate || null,
};
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);
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);
resetGallery();
}
}, [selectedFilters, appliedFilters]);
const handleClearAllFilters = useCallback(() => {
const initialStateSelected = {
building: [],
floor: [],
activity: [],
uploadedBy: [],
workCategory: [],
workArea: [],
startDate: "",
endDate: "",
};
setSelectedFilters(initialStateSelected);
const initialStateApplied = {
buildingIds: null,
floorIds: null,
activityIds: null,
uploadedByIds: null,
workCategoryIds: null,
workAreaIds: null,
startDate: null,
endDate: null,
};
setAppliedFilters(initialStateApplied);
resetGallery();
}, []);
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 renderFilterCategory = (label, items, type) => (
<div className={`dropdown my-2 ${collapsedFilters[type] ? "collapsed" : ""}`}>
<div className="dropdown-header bg-label-primary" onClick={() => toggleCollapse(type)}>
<strong>{label}</strong>
<div className="header-controls">
{type === "dateRange" && (selectedFilters.startDate || selectedFilters.endDate) && (
<button
className="clear-button"
onClick={(e) => {
e.stopPropagation();
setSelectedFilters((prev) => ({ ...prev, startDate: "", endDate: "" }));
}}
>
Clear
</button>
)}
{type !== "dateRange" && selectedFilters[type]?.length > 0 && (
<button
className="clear-button"
onClick={(e) => {
e.stopPropagation();
setSelectedFilters((prev) => ({ ...prev, [type]: [] }));
}}
>
Clear
</button>
)}
<span className="collapse-icon">{collapsedFilters[type] ? '+' : '-'}</span>
</div>
</div>
{!collapsedFilters[type] && (
<div className="dropdown-content">
{type === "dateRange" ? (
<div className="date-range-inputs">
<DateRangePickerNoDefault
onRangeChange={setDateRange}
endDateMode="today"
startDate={selectedFilters.startDate}
endDate={selectedFilters.endDate}
/>
</div>
) : (
items.map(([itemId, itemName]) => (
<label key={itemId}>
<input
type="checkbox"
checked={selectedFilters[type].some((item) => item[0] === itemId)}
onChange={() => toggleFilter(type, itemId, itemName)}
/>
{itemName}
</label>
))
)}
</div>
)}
</div>
);
return (
<div className={`gallery-container container-fluid ${isFilterPanelOpen ? "filter-panel-open-end" : ""}`}>
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Gallary", link: null }]} />
<div className="main-content">
<button
className={`filter-button btn-primary ${isFilterPanelOpen ? "closed-icon" : ""}`}
onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
ref={filterButtonRef}
>
{isFilterPanelOpen ? <i className="fa-solid fa-times fs-5"></i> : <i className="fa-solid fa-filter ms-1 fs-5"></i>}
</button>
<div className="activity-section">
{loading && pageNumber === 1 ? (
<div className="spinner-container"><div className="spinner" /></div>
) : 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 (
<div key={batch.batchId} className="grouped-section">
<div className="group-heading">
<div className="d-flex flex-column">
<div className="d-flex align-items-center mb-1">
<Avatar size="xs" firstName={firstDoc?.uploadedBy?.firstName} lastName={firstDoc?.uploadedBy?.lastName} className="me-2" />
<div className="d-flex flex-column align-items-start">
<strong className="user-name-text">{userName}</strong>
<span className="me-2">{date}</span>
</div>
</div>
</div>
<div className="location-line">
<div>{batch.buildingName} &gt; {batch.floorName} &gt; <strong>{batch.workAreaName || "Unknown"} &gt; {batch.activityName}</strong></div>
{batch.workCategoryName && (
<div className="work-category-display ms-2">
<span className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1">
{batch.workCategoryName}
</span>
</div>
)}
</div>
</div>
<div className="image-group-wrapper">
{showScrollButtons && <button className="scroll-arrow left-arrow" onClick={() => scrollLeft(batch.batchId)}>&#8249;</button>}
<div className="image-group-horizontal" ref={(el) => (imageGroupRefs.current[batch.batchId] = el)}>
{batch.documents.map((doc, idx) => {
const hoverDate = moment(doc.uploadedAt).format("DD-MM-YYYY");
const hoverTime = moment(doc.uploadedAt).format("hh:mm A");
return (
<div key={doc.id} className="image-card" onClick={() => openModal(<ImagePop batch={batch} initialIndex={idx} />)} onMouseEnter={() => setHoveredImage(doc)} onMouseLeave={() => setHoveredImage(null)}>
<div className="image-wrapper">
<img src={doc.url} alt={`Image ${idx + 1}`} />
</div>
{hoveredImage === doc && (
<div className="image-hover-description">
<p><strong>Date:</strong> {hoverDate}</p>
<p><strong>Time:</strong> {hoverTime}</p>
<p><strong>Activity:</strong> {batch.activityName}</p>
</div>
)}
</div>
);
})}
</div>
{showScrollButtons && <button className="scroll-arrow right-arrow" onClick={() => scrollRight(batch.batchId)}>&#8250;</button>}
</div>
</div>
);
})
) : (
!loading && <p style={{ textAlign: "center", color: "#777", marginTop: "50px" }}>No images match the selected filters.</p>
)}
<div ref={loaderRef} style={{ height: '50px', margin: '20px 0', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{loadingMore && hasMore && <div className="spinner" />}
{!hasMore && !loading && images.length > 0 && <p style={{ color: '#aaa' }}>You've reached the end of the images.</p>}
</div>
</div>
</div>
<div className={`offcanvas offcanvas-end ${isFilterPanelOpen ? "show" : ""}`} tabIndex="-1" id="filterOffcanvas" aria-labelledby="filterOffcanvasLabel" ref={filterPanelRef}>
<div className="offcanvas-header">
<h5 className="offcanvas-title" id="filterOffcanvasLabel">Filters</h5>
<button type="button" className="btn-close" onClick={() => setIsFilterPanelOpen(false)} aria-label="Close" />
</div>
<div className="filter-actions mt-auto mx-2">
<button className="btn btn-secondary btn-xs" onClick={handleClearAllFilters}>Clear All</button>
<button className="btn btn-primary btn-xs" onClick={handleApplyFilters}>Apply Filters</button>
</div>
<div className="offcanvas-body d-flex flex-column">
{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")}
</div>
</div>
</div>
);
};
export default ImageGallery;