Change the filter design and refactore the compoents for Image Gallery.
This commit is contained in:
parent
9dddba4e30
commit
c23de9bf7c
171
src/components/ImageGallery/ImageGalleryFilters.jsx
Normal file
171
src/components/ImageGallery/ImageGalleryFilters.jsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import moment from "moment";
|
||||||
|
import DateRangePicker, { DateRangePicker1 } from "../../components/common/DateRangePicker";
|
||||||
|
import SelectMultiple from "../../components/common/SelectMultiple";
|
||||||
|
|
||||||
|
const defaultGalleryFilterValues = {
|
||||||
|
buildingIds: [],
|
||||||
|
floorIds: [],
|
||||||
|
activityIds: [],
|
||||||
|
uploadedByIds: [],
|
||||||
|
workCategoryIds: [],
|
||||||
|
workAreaIds: [],
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageGalleryFilters = ({
|
||||||
|
buildings = [],
|
||||||
|
floors = [],
|
||||||
|
activities = [],
|
||||||
|
workAreas = [],
|
||||||
|
workCategories = [],
|
||||||
|
uploadedByUsers = [],
|
||||||
|
onApplyFilters,
|
||||||
|
appliedFilters,
|
||||||
|
}) => {
|
||||||
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
|
const methods = useForm({
|
||||||
|
defaultValues: defaultGalleryFilterValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, reset } = methods;
|
||||||
|
|
||||||
|
// Prefill form when appliedFilters changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (appliedFilters) {
|
||||||
|
reset({
|
||||||
|
buildingIds: appliedFilters.buildingIds || [],
|
||||||
|
floorIds: appliedFilters.floorIds || [],
|
||||||
|
activityIds: appliedFilters.activityIds || [],
|
||||||
|
uploadedByIds: appliedFilters.uploadedByIds || [],
|
||||||
|
workCategoryIds: appliedFilters.workCategoryIds || [],
|
||||||
|
workAreaIds: appliedFilters.workAreaIds || [],
|
||||||
|
startDate: appliedFilters.startDate || null,
|
||||||
|
endDate: appliedFilters.endDate || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [appliedFilters, reset]);
|
||||||
|
|
||||||
|
// Submit → Apply filters
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(formData) => {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
startDate: formData.startDate
|
||||||
|
? moment(formData.startDate).utc().toISOString()
|
||||||
|
: null,
|
||||||
|
endDate: formData.endDate
|
||||||
|
? moment(formData.endDate).utc().toISOString()
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
onApplyFilters(payload);
|
||||||
|
},
|
||||||
|
[onApplyFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
const onClear = useCallback(() => {
|
||||||
|
reset(defaultGalleryFilterValues);
|
||||||
|
setResetKey((prev) => prev + 1); // reset DateRangePicker
|
||||||
|
onApplyFilters(defaultGalleryFilterValues);
|
||||||
|
}, [onApplyFilters, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3">
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="d-flex flex-column">
|
||||||
|
{/* Date Range */}
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<label className="">Date Range :</label>
|
||||||
|
<DateRangePicker1
|
||||||
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
|
startField="startDate"
|
||||||
|
endField="endDate"
|
||||||
|
resetSignal={resetKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Multi-select dropdowns */}
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="buildingIds"
|
||||||
|
label="Buildings :"
|
||||||
|
options={buildings.map(([id, name]) => ({ id, name }))}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="floorIds"
|
||||||
|
label="Floors :"
|
||||||
|
options={floors.map(([id, name]) => ({ id, name }))}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="workAreaIds"
|
||||||
|
label="Work Areas :"
|
||||||
|
options={workAreas.map(([id, name]) => ({ id, name }))}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="activityIds"
|
||||||
|
label="Activities :"
|
||||||
|
options={activities.map(([id, name]) => ({ id, name }))}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="uploadedByIds"
|
||||||
|
label="Uploaded By (User) :"
|
||||||
|
options={uploadedByUsers.map(([id, name]) => ({ id, name }))}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="workCategoryIds"
|
||||||
|
label="Work Categories :"
|
||||||
|
options={workCategories.map(([id, name]) => ({ id, name }))}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer buttons */}
|
||||||
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-xs"
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary btn-xs">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageGalleryFilters;
|
169
src/components/ImageGallery/ImageGalleryListView.jsx
Normal file
169
src/components/ImageGallery/ImageGalleryListView.jsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import React, { useRef, useState, useCallback } from "react";
|
||||||
|
import Avatar from "../../components/common/Avatar";
|
||||||
|
import ImagePopup from "./ImagePopup";
|
||||||
|
|
||||||
|
const ImageGalleryListView = ({
|
||||||
|
images,
|
||||||
|
isLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
loaderRef,
|
||||||
|
openModal,
|
||||||
|
SCROLL_THRESHOLD,
|
||||||
|
formatUTCToLocalTime,
|
||||||
|
moment,
|
||||||
|
}) => {
|
||||||
|
const [hoveredImage, setHoveredImage] = useState(null);
|
||||||
|
const imageGroupRefs = useRef({});
|
||||||
|
|
||||||
|
const scrollLeft = useCallback(
|
||||||
|
(key) =>
|
||||||
|
imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const scrollRight = useCallback(
|
||||||
|
(key) =>
|
||||||
|
imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<div className="activity-section">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
) : 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 (
|
||||||
|
<div key={batch.batchId} className="grouped-section">
|
||||||
|
<div className="group-heading">
|
||||||
|
{/* Uploader Info */}
|
||||||
|
<div className="d-flex align-items-center mb-1">
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
firstName={doc.uploadedBy?.firstName}
|
||||||
|
lastName={doc.uploadedBy?.lastName}
|
||||||
|
className="me-2"
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-column align-items-start">
|
||||||
|
<strong className="user-name-text">{userName}</strong>
|
||||||
|
<span className="text-muted small">{date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location Info */}
|
||||||
|
<div className="location-line text-secondary">
|
||||||
|
<div className="d-flex align-items-center flex-wrap gap-1 text-secondary">
|
||||||
|
<span className="d-flex align-items-center">
|
||||||
|
<span>{batch.buildingName}</span>
|
||||||
|
<i className="bx bx-chevron-right " />
|
||||||
|
</span>
|
||||||
|
<span className="d-flex align-items-center">
|
||||||
|
<span>{batch.floorName}</span>
|
||||||
|
<i className="bx bx-chevron-right m" />
|
||||||
|
</span>
|
||||||
|
<span className="d-flex align-items-center ">
|
||||||
|
<span>{batch.workAreaName || "Unknown"}</span>
|
||||||
|
<i className="bx bx-chevron-right " />
|
||||||
|
<span>{batch.activityName}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{batch.workCategoryName && (
|
||||||
|
<span className="badge bg-label-primary ms-2">
|
||||||
|
{batch.workCategoryName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Images */}
|
||||||
|
<div className="image-group-wrapper">
|
||||||
|
{hasArrows && (
|
||||||
|
<button
|
||||||
|
className="scroll-arrow left-arrow"
|
||||||
|
onClick={() => scrollLeft(batch.batchId)}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="image-group-horizontal"
|
||||||
|
ref={(el) =>
|
||||||
|
(imageGroupRefs.current[batch.batchId] = el)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{batch.documents.map((d, i) => {
|
||||||
|
const hoverDate = moment(d.uploadedAt).format(
|
||||||
|
"DD MMMM, YYYY"
|
||||||
|
);
|
||||||
|
const hoverTime = moment(d.uploadedAt).format("hh:mm A");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={d.id}
|
||||||
|
className="image-card"
|
||||||
|
onClick={() =>
|
||||||
|
openModal(<ImagePopup batch={batch} initialIndex={i} />)
|
||||||
|
}
|
||||||
|
onMouseEnter={() => setHoveredImage(d)}
|
||||||
|
onMouseLeave={() => setHoveredImage(null)}
|
||||||
|
>
|
||||||
|
<div className="image-wrapper">
|
||||||
|
<img src={d.url} alt={`Image ${i + 1}`} />
|
||||||
|
</div>
|
||||||
|
{hoveredImage === d && (
|
||||||
|
<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>
|
||||||
|
{hasArrows && (
|
||||||
|
<button
|
||||||
|
className="scroll-arrow right-arrow"
|
||||||
|
onClick={() => scrollRight(batch.batchId)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted mt-5">
|
||||||
|
No images match the selected filters.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={loaderRef}>
|
||||||
|
{isFetchingNextPage && hasNextPage && <p>Loading...</p>}
|
||||||
|
{!hasNextPage && !isLoading && images.length > 0 && (
|
||||||
|
<p className="text-muted">You've reached the end of the images.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageGalleryListView;
|
90
src/components/ImageGallery/ImagePopup.jsx
Normal file
90
src/components/ImageGallery/ImagePopup.jsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import "./ImagePopup.css"
|
||||||
|
import { useModal } from "./ModalContext";
|
||||||
|
import moment from "moment";
|
||||||
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
|
|
||||||
|
const ImagePopup = ({ batch, initialIndex = 0 }) => {
|
||||||
|
const { closeModal } = useModal();
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||||
|
|
||||||
|
// Effect to update currentIndex if the initialIndex prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentIndex(initialIndex);
|
||||||
|
}, [initialIndex, batch]);
|
||||||
|
|
||||||
|
// If no batch or documents are provided, don't render
|
||||||
|
if (!batch || !batch.documents || batch.documents.length === 0) return null;
|
||||||
|
|
||||||
|
// 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 from the individual image document
|
||||||
|
const fullName = `${image.uploadedBy?.firstName || ""} ${image.uploadedBy?.lastName || ""
|
||||||
|
}`.trim();
|
||||||
|
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 = () => {
|
||||||
|
setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler for navigating to the next image
|
||||||
|
const handleNext = () => {
|
||||||
|
setCurrentIndex((prevIndex) =>
|
||||||
|
Math.min(batch.documents.length - 1, prevIndex + 1)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if previous/next buttons should be enabled/visible
|
||||||
|
const hasPrev = currentIndex > 0;
|
||||||
|
const hasNext = currentIndex < batch.documents.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="image-modal-overlay">
|
||||||
|
<div className="image-modal-content">
|
||||||
|
|
||||||
|
<i className='bx bx-x close-button' onClick={closeModal}></i>
|
||||||
|
|
||||||
|
{hasPrev && (
|
||||||
|
<button className="nav-button prev-button" onClick={handlePrev}>
|
||||||
|
<i className='bx bx-chevron-left'></i>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="image-container">
|
||||||
|
<img src={image.url} alt="Preview" className="modal-image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasNext && (
|
||||||
|
<button className="nav-button next-button" onClick={handleNext}>
|
||||||
|
<i className='bx bx-chevron-right'></i>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="image-details">
|
||||||
|
|
||||||
|
<div className="flex alig-items-center"> <i className='bx bxs-user'></i> <span className="text-muted">Uploaded By : </span> <span className="text-secondary">{fullName}</span></div>
|
||||||
|
<div className="flex alig-items-center"> <i class='bx bxs-calendar' ></i> <span className="text-muted">Date : </span> <span className="text-secondary"> {date}</span></div>
|
||||||
|
<div className="flex alig-items-center"> <i class='bx bx-map' ></i> <span className="text-muted">Uploaded By : </span> <span className="text-secondary">{buildingName} <i className='bx bx-chevron-right'></i> {floorName} <i className='bx bx-chevron-right'></i>
|
||||||
|
{workAreaName || "Unknown"} <i className='bx bx-chevron-right'></i> {activityName}</span></div>
|
||||||
|
<div className="flex alig-items-center"> <i className='bx bx-comment-dots'></i> <span className="text-muted">comment : </span> <span className="text-secondary">{batchComment}</span></div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImagePopup;
|
@ -8,7 +8,7 @@ import { Provider } from 'react-redux';
|
|||||||
import { store } from './store/store';
|
import { store } from './store/store';
|
||||||
import { ModalProvider } from './ModalContext.jsx';
|
import { ModalProvider } from './ModalContext.jsx';
|
||||||
import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx';
|
import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx';
|
||||||
import { ModalProvider1 } from './pages/Gallary/ModalContext.jsx';
|
import { ModalProvider1 } from './components/ImageGallery/ModalContext.jsx';
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
@ -1,499 +0,0 @@
|
|||||||
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) => (
|
|
||||||
<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)) ||
|
|
||||||
(type !== "dateRange" && selectedFilters[type]?.length > 0)) && (
|
|
||||||
<button
|
|
||||||
className="clear-button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setSelectedFilters((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[type]: type === "dateRange" ? "" : [],
|
|
||||||
...(type === "dateRange" && { startDate: "", endDate: "" }),
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<span className="collapse-icon">
|
|
||||||
{collapsedFilters[type] ? "+" : "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!collapsedFilters[type] && (
|
|
||||||
<div className="dropdown-content">
|
|
||||||
{type === "dateRange" ? (
|
|
||||||
<DateRangePicker
|
|
||||||
onRangeChange={setDateRange}
|
|
||||||
endDateMode="today"
|
|
||||||
startDate={selectedFilters.startDate}
|
|
||||||
endDate={selectedFilters.endDate}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
items.map(([id, name]) => (
|
|
||||||
<label key={id}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedFilters[type].some(([x]) => x === id)}
|
|
||||||
onChange={() => toggleFilter(type, id, name)}
|
|
||||||
/>
|
|
||||||
{name}
|
|
||||||
</label>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`gallery-container container-fluid ${
|
|
||||||
isFilterPanelOpen ? "filter-panel-open-end" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Gallery" }]} />
|
|
||||||
<div className="main-content">
|
|
||||||
<button
|
|
||||||
className={`filter-button btn-primary ${
|
|
||||||
isFilterPanelOpen ? "closed-icon" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setIsFilterPanelOpen((p) => !p)}
|
|
||||||
ref={filterButtonRef}
|
|
||||||
>
|
|
||||||
{isFilterPanelOpen ? (
|
|
||||||
<i className="fa-solid fa-times fs-5" />
|
|
||||||
) : (
|
|
||||||
<i className="bx bx-slider-alt ms-1" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<div className="activity-section">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-center">
|
|
||||||
<p>Loading...</p>
|
|
||||||
</div>
|
|
||||||
) : 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 (
|
|
||||||
<div key={batch.batchId} className="grouped-section">
|
|
||||||
<div className="group-heading">
|
|
||||||
{/* Uploader Info */}
|
|
||||||
<div className="d-flex align-items-center mb-1">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
firstName={doc.uploadedBy?.firstName}
|
|
||||||
lastName={doc.uploadedBy?.lastName}
|
|
||||||
className="me-2"
|
|
||||||
/>
|
|
||||||
<div className="d-flex flex-column align-items-start">
|
|
||||||
<strong className="user-name-text">{userName}</strong>
|
|
||||||
<span className="text-muted small">{date}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Location Info */}
|
|
||||||
<div className="location-line text-secondary">
|
|
||||||
<div className="d-flex align-items-center flex-wrap gap-1 text-secondary">
|
|
||||||
{" "}
|
|
||||||
<span className="d-flex align-items-center">
|
|
||||||
<span>{batch.buildingName}</span>
|
|
||||||
<i className="bx bx-chevron-right " />
|
|
||||||
</span>
|
|
||||||
<span className="d-flex align-items-center">
|
|
||||||
<span>{batch.floorName}</span>
|
|
||||||
<i className="bx bx-chevron-right m" />
|
|
||||||
</span>
|
|
||||||
<span className="d-flex align-items-center ">
|
|
||||||
<span>{batch.workAreaName || "Unknown"}</span>
|
|
||||||
<i className="bx bx-chevron-right " />
|
|
||||||
<span>{batch.activityName}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{batch.workCategoryName && (
|
|
||||||
<span className="badge bg-label-primary ms-2">
|
|
||||||
{batch.workCategoryName}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="image-group-wrapper">
|
|
||||||
{hasArrows && (
|
|
||||||
<button
|
|
||||||
className="scroll-arrow left-arrow"
|
|
||||||
onClick={() => scrollLeft(batch.batchId)}
|
|
||||||
>
|
|
||||||
‹
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="image-group-horizontal"
|
|
||||||
ref={(el) => (imageGroupRefs.current[batch.batchId] = el)}
|
|
||||||
>
|
|
||||||
{batch.documents.map((d, i) => {
|
|
||||||
const hoverDate = moment(d.uploadedAt).format(
|
|
||||||
"DD MMMM, YYYY"
|
|
||||||
);
|
|
||||||
const hoverTime = moment(d.uploadedAt).format(
|
|
||||||
"hh:mm A"
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={d.id}
|
|
||||||
className="image-card"
|
|
||||||
onClick={() =>
|
|
||||||
openModal(
|
|
||||||
<ImagePop batch={batch} initialIndex={i} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onMouseEnter={() => setHoveredImage(d)}
|
|
||||||
onMouseLeave={() => setHoveredImage(null)}
|
|
||||||
>
|
|
||||||
<div className="image-wrapper">
|
|
||||||
<img src={d.url} alt={`Image ${i + 1}`} />
|
|
||||||
</div>
|
|
||||||
{hoveredImage === d && (
|
|
||||||
<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>
|
|
||||||
{hasArrows && (
|
|
||||||
<button
|
|
||||||
className="scroll-arrow right-arrow"
|
|
||||||
onClick={() => scrollRight(batch.batchId)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p className="text-center text-muted mt-5">
|
|
||||||
No images match the selected filters.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div ref={loaderRef}>
|
|
||||||
{isFetchingNextPage && hasNextPage && <p>Loading...</p>}
|
|
||||||
{!hasNextPage && !isLoading && images.length > 0 && (
|
|
||||||
<p className="text-muted">
|
|
||||||
You've reached the end of the images.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`offcanvas offcanvas-end ${isFilterPanelOpen ? "show" : ""}`}
|
|
||||||
ref={filterPanelRef}
|
|
||||||
>
|
|
||||||
<div className="offcanvas-header">
|
|
||||||
<h5>Filters</h5>
|
|
||||||
<button
|
|
||||||
className="btn-close"
|
|
||||||
onClick={() => setIsFilterPanelOpen(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="filter-actions mt-auto mx-2">
|
|
||||||
<button className="btn btn-secondary btn-xs" onClick={handleClear}>
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-xs"
|
|
||||||
onClick={handleApplyFilters}
|
|
||||||
>
|
|
||||||
Apply Filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="offcanvas-body d-flex flex-column">
|
|
||||||
{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")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImageGallery;
|
|
@ -1,91 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import "./ImagePop.css";
|
|
||||||
import { useModal } from "./ModalContext";
|
|
||||||
import moment from "moment";
|
|
||||||
import {formatUTCToLocalTime} from "../../utils/dateUtils";
|
|
||||||
|
|
||||||
const ImagePop = ({ batch, initialIndex = 0 }) => {
|
|
||||||
const { closeModal } = useModal();
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
||||||
|
|
||||||
// Effect to update currentIndex if the initialIndex prop changes
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentIndex(initialIndex);
|
|
||||||
}, [initialIndex, batch]);
|
|
||||||
|
|
||||||
// If no batch or documents are provided, don't render
|
|
||||||
if (!batch || !batch.documents || batch.documents.length === 0) return null;
|
|
||||||
|
|
||||||
// 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 from the individual image document
|
|
||||||
const fullName = `${image.uploadedBy?.firstName || ""} ${
|
|
||||||
image.uploadedBy?.lastName || ""
|
|
||||||
}`.trim();
|
|
||||||
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 = () => {
|
|
||||||
setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handler for navigating to the next image
|
|
||||||
const handleNext = () => {
|
|
||||||
setCurrentIndex((prevIndex) =>
|
|
||||||
Math.min(batch.documents.length - 1, prevIndex + 1)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine if previous/next buttons should be enabled/visible
|
|
||||||
const hasPrev = currentIndex > 0;
|
|
||||||
const hasNext = currentIndex < batch.documents.length - 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-modal-overlay">
|
|
||||||
<div className="image-modal-content">
|
|
||||||
|
|
||||||
<i className='bx bx-x close-button' onClick={closeModal}></i>
|
|
||||||
|
|
||||||
{hasPrev && (
|
|
||||||
<button className="nav-button prev-button" onClick={handlePrev}>
|
|
||||||
<i className='bx bx-chevron-left'></i>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="image-container">
|
|
||||||
<img src={image.url} alt="Preview" className="modal-image" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasNext && (
|
|
||||||
<button className="nav-button next-button" onClick={handleNext}>
|
|
||||||
<i className='bx bx-chevron-right'></i>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="image-details">
|
|
||||||
|
|
||||||
<div className="flex alig-items-center"> <i className='bx bxs-user'></i> <span className="text-muted">Uploaded By : </span> <span className="text-secondary">{fullName}</span></div>
|
|
||||||
<div className="flex alig-items-center"> <i class='bx bxs-calendar' ></i> <span className="text-muted">Date : </span> <span className="text-secondary"> {date}</span></div>
|
|
||||||
<div className="flex alig-items-center"> <i class='bx bx-map' ></i> <span className="text-muted">Uploaded By : </span> <span className="text-secondary">{buildingName} <i className='bx bx-chevron-right'></i> {floorName} <i className='bx bx-chevron-right'></i>
|
|
||||||
{workAreaName || "Unknown"} <i className='bx bx-chevron-right'></i> {activityName}</span></div>
|
|
||||||
<div className="flex alig-items-center"> <i className='bx bx-comment-dots'></i> <span className="text-muted">comment : </span> <span className="text-secondary">{batchComment}</span></div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImagePop;
|
|
196
src/pages/ImageGallery/ImageGalleryPage.jsx
Normal file
196
src/pages/ImageGallery/ImageGalleryPage.jsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import moment from "moment";
|
||||||
|
import { useModal } from "../../components/ImageGallery/ModalContext";
|
||||||
|
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";
|
||||||
|
import ImageGalleryListView from "../../components/ImageGallery/ImageGalleryListView";
|
||||||
|
import ImageGalleryFilters from "../../components/ImageGallery/ImageGalleryFilters";
|
||||||
|
import "../../components/ImageGallery/ImageGallery.css";
|
||||||
|
|
||||||
|
// --- NEW IMPORTS ---
|
||||||
|
import { useFab } from "../../Context/FabContext";
|
||||||
|
|
||||||
|
const SCROLL_THRESHOLD = 5;
|
||||||
|
|
||||||
|
const ImageGalleryPage = () => {
|
||||||
|
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { projectNames } = useProjectName();
|
||||||
|
const loaderRef = useRef(null);
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
// Auto-select first project if none selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedProjectId && projectNames?.length) {
|
||||||
|
dispatch(setProjectId(projectNames[0].id));
|
||||||
|
}
|
||||||
|
}, [selectedProjectId, projectNames, dispatch]);
|
||||||
|
|
||||||
|
// --- Filters ---
|
||||||
|
const [appliedFilters, setAppliedFilters] = useState({
|
||||||
|
buildingIds: null,
|
||||||
|
floorIds: null,
|
||||||
|
activityIds: null,
|
||||||
|
uploadedByIds: null,
|
||||||
|
workCategoryIds: null,
|
||||||
|
workAreaIds: null,
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
|
refetch,
|
||||||
|
} = useImageGallery(selectedProjectId, appliedFilters);
|
||||||
|
|
||||||
|
const images = data?.pages.flatMap((page) => page.data) || [];
|
||||||
|
|
||||||
|
// --- 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();
|
||||||
|
|
||||||
|
// --- Apply filters callback ---
|
||||||
|
const handleApplyFilters = useCallback((values) => {
|
||||||
|
setAppliedFilters(values);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// --- Filter Panel Memoization ---
|
||||||
|
const filterPanelElement = useMemo(
|
||||||
|
() => (
|
||||||
|
<ImageGalleryFilters
|
||||||
|
buildings={buildings}
|
||||||
|
floors={floors}
|
||||||
|
activities={activities}
|
||||||
|
workAreas={workAreas}
|
||||||
|
workCategories={workCategories}
|
||||||
|
uploadedByUsers={uploadedByUsers}
|
||||||
|
appliedFilters={appliedFilters}
|
||||||
|
onApplyFilters={handleApplyFilters}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
buildings,
|
||||||
|
floors,
|
||||||
|
activities,
|
||||||
|
workAreas,
|
||||||
|
workCategories,
|
||||||
|
uploadedByUsers,
|
||||||
|
appliedFilters,
|
||||||
|
handleApplyFilters,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Fab Offcanvas Integration ---
|
||||||
|
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||||
|
useEffect(() => {
|
||||||
|
setShowTrigger(true);
|
||||||
|
setOffcanvasContent("Gallery Filters", filterPanelElement);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setShowTrigger(false);
|
||||||
|
setOffcanvasContent("", null);
|
||||||
|
};
|
||||||
|
}, [filterPanelElement, setOffcanvasContent, setShowTrigger]);
|
||||||
|
|
||||||
|
// --- Refetch on project or filters ---
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedProjectId) refetch();
|
||||||
|
}, [selectedProjectId, appliedFilters, refetch]);
|
||||||
|
|
||||||
|
// --- EventBus Refetch ---
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (data) => {
|
||||||
|
if (data.projectId === selectedProjectId) refetch();
|
||||||
|
};
|
||||||
|
eventBus.on("image_gallery", handler);
|
||||||
|
return () => eventBus.off("image_gallery", handler);
|
||||||
|
}, [selectedProjectId, refetch]);
|
||||||
|
|
||||||
|
// --- Infinite scroll observer ---
|
||||||
|
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]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gallery-container container-fluid">
|
||||||
|
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Gallery" }]} />
|
||||||
|
<ImageGalleryListView
|
||||||
|
images={images}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
loaderRef={loaderRef}
|
||||||
|
openModal={openModal}
|
||||||
|
SCROLL_THRESHOLD={SCROLL_THRESHOLD}
|
||||||
|
formatUTCToLocalTime={formatUTCToLocalTime}
|
||||||
|
moment={moment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageGalleryPage;
|
@ -26,7 +26,6 @@ import AttendancePage from "../pages/Activities/AttendancePage";
|
|||||||
import DailyTask from "../pages/Activities/DailyTask";
|
import DailyTask from "../pages/Activities/DailyTask";
|
||||||
import TaskPlannng from "../pages/Activities/TaskPlannng";
|
import TaskPlannng from "../pages/Activities/TaskPlannng";
|
||||||
import Reports from "../pages/reports/Reports";
|
import Reports from "../pages/reports/Reports";
|
||||||
import ImageGallary from "../pages/Gallary/ImageGallary";
|
|
||||||
import MasterPage from "../pages/master/MasterPage";
|
import MasterPage from "../pages/master/MasterPage";
|
||||||
import Support from "../pages/support/Support";
|
import Support from "../pages/support/Support";
|
||||||
import Documentation from "../pages/support/Documentation";
|
import Documentation from "../pages/support/Documentation";
|
||||||
@ -44,6 +43,7 @@ import ExpensePage from "../pages/Expense/ExpensePage";
|
|||||||
import TenantDetails from "../pages/Tenant/TenantDetails";
|
import TenantDetails from "../pages/Tenant/TenantDetails";
|
||||||
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
|
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
|
||||||
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
|
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
|
||||||
|
import ImageGalleryPage from "../pages/ImageGallery/ImageGalleryPage";
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
@ -81,7 +81,7 @@ const router = createBrowserRouter(
|
|||||||
{ path: "/activities/records/:projectId?", element: <DailyTask /> },
|
{ path: "/activities/records/:projectId?", element: <DailyTask /> },
|
||||||
{ path: "/activities/task", element: <TaskPlannng /> },
|
{ path: "/activities/task", element: <TaskPlannng /> },
|
||||||
{ path: "/activities/reports", element: <Reports /> },
|
{ path: "/activities/reports", element: <Reports /> },
|
||||||
{ path: "/gallary", element: <ImageGallary /> },
|
{ path: "/gallary", element: <ImageGalleryPage /> },
|
||||||
{ path: "/expenses", element: <ExpensePage /> },
|
{ path: "/expenses", element: <ExpensePage /> },
|
||||||
{ path: "/masters", element: <MasterPage /> },
|
{ path: "/masters", element: <MasterPage /> },
|
||||||
{ path: "/tenants", element: <TenantPage /> },
|
{ path: "/tenants", element: <TenantPage /> },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user