516 lines
18 KiB
JavaScript
516 lines
18 KiB
JavaScript
// ImageGallery.js
|
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
import "./ImageGallery.css";
|
|
import { ImageGalleryAPI } from "./ImageGalleryAPI";
|
|
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"; // Assuming this is the path to your DateRangePicker
|
|
|
|
const ImageGallery = () => {
|
|
const [images, setImages] = useState([]);
|
|
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
|
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 [loading, setLoading] = useState(true);
|
|
|
|
const imageGroupRefs = useRef({});
|
|
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);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!selectedProjectId) {
|
|
setImages([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
ImageGalleryAPI.ImagesGet(selectedProjectId, appliedFilters)
|
|
.then((res) => {
|
|
setImages(res.data);
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error fetching images:", err);
|
|
setImages([]);
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}, [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]);
|
|
}
|
|
});
|
|
return Array.from(uniqueMap.entries());
|
|
},
|
|
[images]
|
|
);
|
|
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
return Array.from(uniqueUsersMap.entries());
|
|
}, [images]);
|
|
|
|
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 > 0
|
|
? selectedFilters.building.map((item) => item[0])
|
|
: null,
|
|
floorIds:
|
|
selectedFilters.floor.length > 0
|
|
? selectedFilters.floor.map((item) => item[0])
|
|
: null,
|
|
workAreaIds:
|
|
selectedFilters.workArea.length > 0
|
|
? selectedFilters.workArea.map((item) => item[0])
|
|
: null,
|
|
workCategoryIds:
|
|
selectedFilters.workCategory.length > 0
|
|
? selectedFilters.workCategory.map((item) => item[0])
|
|
: null,
|
|
activityIds:
|
|
selectedFilters.activity.length > 0
|
|
? selectedFilters.activity.map((item) => item[0])
|
|
: null,
|
|
uploadedByIds:
|
|
selectedFilters.uploadedBy.length > 0
|
|
? selectedFilters.uploadedBy.map((item) => item[0])
|
|
: null,
|
|
startDate: selectedFilters.startDate || null,
|
|
endDate: selectedFilters.endDate || null,
|
|
};
|
|
setAppliedFilters(payload);
|
|
setIsFilterPanelOpen(false);
|
|
}, [selectedFilters]);
|
|
|
|
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);
|
|
|
|
setIsFilterPanelOpen(false);
|
|
}, []);
|
|
|
|
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" });
|
|
}, []);
|
|
|
|
const scrollRight = useCallback((key) => {
|
|
imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" });
|
|
}, []);
|
|
|
|
const renderFilterCategory = (label, items, type) => (
|
|
<div className={`dropdown ${collapsedFilters[type] ? "collapsed" : ""}`}>
|
|
<div className="dropdown-header" 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] &&
|
|
selectedFilters[type].length > 0 && (
|
|
<button
|
|
className="clear-button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedFilters((prev) => ({ ...prev, [type]: [] }));
|
|
}}
|
|
>
|
|
Clear
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{!collapsedFilters[type] && (
|
|
<div className="dropdown-content">
|
|
{type === "dateRange" ? (
|
|
// The DateRangePicker will be rendered outside this function, at the end of the filter panel
|
|
null
|
|
) : (
|
|
items.map((item) => {
|
|
const itemId = item[0];
|
|
const itemName = item[1];
|
|
const isChecked = selectedFilters[type].some(
|
|
(selectedItem) => selectedItem[0] === itemId
|
|
);
|
|
|
|
return (
|
|
<label key={itemId}>
|
|
<input
|
|
type="checkbox"
|
|
checked={isChecked}
|
|
onChange={() => toggleFilter(type, itemId, itemName)}
|
|
/>
|
|
{itemName}
|
|
</label>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={`gallery-container ${isFilterPanelOpen ? "filter-panel-open" : ""}`}>
|
|
<div className="main-content">
|
|
<div className="activity-section">
|
|
{loading ? (
|
|
<div className="spinner-container">
|
|
<div className="spinner" />
|
|
</div>
|
|
) : 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");
|
|
|
|
return (
|
|
<div key={key} className="grouped-section">
|
|
<div className="group-heading">
|
|
<div className="d-flex flex-column">
|
|
<div className="d-flex align-items-center mb-1">
|
|
<Avatar
|
|
size="xxs"
|
|
firstName={imgs[0].uploadedBy?.firstName}
|
|
lastName={imgs[0].uploadedBy?.lastName}
|
|
className="me-2"
|
|
/>
|
|
<div className="d-flex flex-column align-items-start">
|
|
<strong className="user-name-text">
|
|
{imgs[0].uploadedBy?.firstName}{" "}
|
|
{imgs[0].uploadedBy?.lastName}
|
|
</strong>
|
|
<span className="me-2">
|
|
{date} {time}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="location-line">
|
|
<div>
|
|
{buildingName} > {floorName} > {workArea} >{" "}
|
|
{activity}
|
|
</div>
|
|
{workCategoryName && (
|
|
<div className="work-category-display ms-2">
|
|
{workCategoryName}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="image-group-wrapper">
|
|
<button
|
|
className="scroll-arrow left-arrow"
|
|
onClick={() => scrollLeft(key)}
|
|
>
|
|
‹
|
|
</button>
|
|
<div
|
|
className="image-group-horizontal"
|
|
ref={(el) => (imageGroupRefs.current[key] = el)}
|
|
>
|
|
{imgs.map((img, idx) => {
|
|
const hoverDate = moment(img.uploadedAt).format("YYYY-MM-DD");
|
|
const hoverTime = moment(img.uploadedAt).format("hh:mm A");
|
|
|
|
return (
|
|
<div
|
|
key={img.imageUrl}
|
|
className="image-card"
|
|
onClick={() =>
|
|
openModal(<ImagePop images={imgs} initialIndex={idx} />)
|
|
}
|
|
onMouseEnter={() => setHoveredImage(img)}
|
|
onMouseLeave={() => setHoveredImage(null)}
|
|
>
|
|
<div className="image-wrapper">
|
|
<img src={img.imageUrl} alt={`Image ${idx + 1}`} />
|
|
</div>
|
|
{hoveredImage === img && (
|
|
<div className="image-hover-description">
|
|
<p>
|
|
<strong>Date:</strong> {hoverDate}
|
|
</p>
|
|
<p>
|
|
<strong>Time:</strong> {hoverTime}
|
|
</p>
|
|
<p>
|
|
<strong>Activity:</strong> {img.activityName}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<button
|
|
className="scroll-arrow right-arrow"
|
|
onClick={() => scrollRight(key)}
|
|
>
|
|
›
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<p style={{ textAlign: "center", color: "#777", marginTop: "50px" }}>
|
|
No images match the selected filters.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="filter-drawer-wrapper">
|
|
<button
|
|
className={`filter-button ${isFilterPanelOpen ? "" : "closed-icon"}`}
|
|
onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
|
|
ref={filterButtonRef}
|
|
>
|
|
{isFilterPanelOpen ? (
|
|
<>
|
|
Filter <span>✖</span>
|
|
</>
|
|
) : (
|
|
<span>▼</span>
|
|
)}
|
|
</button>
|
|
<div className={`filter-panel ${isFilterPanelOpen ? "open" : ""}`} ref={filterPanelRef}>
|
|
<div className={`dropdown ${collapsedFilters.dateRange ? 'collapsed' : ''}`}>
|
|
<div className="dropdown-header" onClick={() => toggleCollapse('dateRange')}>
|
|
<strong>Date Range</strong>
|
|
{(selectedFilters.startDate || selectedFilters.endDate) && (
|
|
<button
|
|
className="clear-button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedFilters(prev => ({ ...prev, startDate: "", endDate: "" }));
|
|
}}
|
|
>
|
|
Clear
|
|
</button>
|
|
)}
|
|
</div>
|
|
{!collapsedFilters.dateRange && (
|
|
<div >
|
|
<DateRangePicker
|
|
onRangeChange={setDateRange}
|
|
defaultStartDate={selectedFilters.startDate || yesterday} // Use selected date or yesterday as default
|
|
defaultEndDate={selectedFilters.endDate || moment().format('YYYY-MM-DD')} // Use selected date or today as default
|
|
startDate={selectedFilters.startDate} // Pass current selected start date
|
|
endDate={selectedFilters.endDate} // Pass current selected end date
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{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")}
|
|
|
|
{/* DateRangePicker at the end */}
|
|
|
|
|
|
<div className="filter-actions">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ImageGallery; |