294 lines
11 KiB
JavaScript
294 lines
11 KiB
JavaScript
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 { useProjectAssignedServices, 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";
|
|
import { useFab } from "../../Context/FabContext";
|
|
import ImageGallerySkeleton from "../../components/Charts/ImageGallerySkeleton";
|
|
|
|
const ImageGalleryPage = () => {
|
|
const dispatch = useDispatch();
|
|
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
|
const { projectNames } = useProjectName();
|
|
const loaderRef = useRef(null);
|
|
const { openModal } = useModal();
|
|
const { setOffcanvasContent, setShowTrigger } = useFab();
|
|
|
|
const { data: assignedServices = [], isLoading: servicesLoading } =
|
|
useProjectAssignedServices(selectedProjectId);
|
|
|
|
const [selectedService, setSelectedService] = useState("");
|
|
|
|
const handleServiceChange = (e) => {
|
|
setSelectedService(e.target.value);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!selectedProjectId && projectNames?.length) {
|
|
dispatch(setProjectId(projectNames[0].id));
|
|
}
|
|
}, [selectedProjectId, projectNames, dispatch]);
|
|
|
|
const [appliedFilters, setAppliedFilters] = useState({
|
|
buildingIds: [],
|
|
floorIds: [],
|
|
activityIds: [],
|
|
uploadedByIds: [],
|
|
workCategoryIds: [],
|
|
workAreaIds: [],
|
|
startDate: null,
|
|
endDate: null,
|
|
});
|
|
|
|
const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage, refetch } =
|
|
useImageGallery(selectedProjectId, appliedFilters);
|
|
|
|
const images = data?.pages.flatMap((page) => page.data) || [];
|
|
|
|
const [labelMaps, setLabelMaps] = useState({
|
|
buildings: new Map(),
|
|
floors: new Map(),
|
|
activities: new Map(),
|
|
workAreas: new Map(),
|
|
workCategories: new Map(),
|
|
uploadedByUsers: new Map(),
|
|
});
|
|
|
|
useEffect(() => {
|
|
const buildingsMap = new Map(labelMaps.buildings);
|
|
const floorsMap = new Map(labelMaps.floors);
|
|
const activitiesMap = new Map(labelMaps.activities);
|
|
const workAreasMap = new Map(labelMaps.workAreas);
|
|
const workCategoriesMap = new Map(labelMaps.workCategories);
|
|
const uploadedByMap = new Map(labelMaps.uploadedByUsers);
|
|
|
|
images.forEach((batch) => {
|
|
if (batch.buildingId && batch.buildingName) buildingsMap.set(batch.buildingId, batch.buildingName);
|
|
if (batch.floorIds && batch.floorName) floorsMap.set(batch.floorIds, batch.floorName);
|
|
if (batch.activityId && batch.activityName) activitiesMap.set(batch.activityId, batch.activityName);
|
|
if (batch.workAreaId && batch.workAreaName) workAreasMap.set(batch.workAreaId, batch.workAreaName);
|
|
if (batch.workCategoryId && batch.workCategoryName) workCategoriesMap.set(batch.workCategoryId, batch.workCategoryName);
|
|
batch.documents?.forEach((doc) => {
|
|
const name = `${doc.uploadedBy?.firstName || ""} ${doc.uploadedBy?.lastName || ""}`.trim();
|
|
if (doc.uploadedBy?.id && name) uploadedByMap.set(doc.uploadedBy.id, name);
|
|
});
|
|
});
|
|
|
|
setLabelMaps({
|
|
buildings: buildingsMap,
|
|
floors: floorsMap,
|
|
activities: activitiesMap,
|
|
workAreas: workAreasMap,
|
|
workCategories: workCategoriesMap,
|
|
uploadedByUsers: uploadedByMap,
|
|
});
|
|
}, [images]);
|
|
|
|
const handleApplyFilters = useCallback((values) => setAppliedFilters(values), []);
|
|
|
|
const handleRemoveFilter = (filterKey, valueId) => {
|
|
setAppliedFilters((prev) => {
|
|
const updated = { ...prev };
|
|
if (Array.isArray(updated[filterKey])) {
|
|
updated[filterKey] = updated[filterKey].filter((id) => id !== valueId);
|
|
} else if (filterKey === "startDate" || filterKey === "endDate" || filterKey === "dateRange") {
|
|
updated.startDate = null;
|
|
updated.endDate = null;
|
|
}
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
const appliedFiltersChips = useMemo(() => {
|
|
const chips = [];
|
|
const { buildings, floors, activities, workAreas, workCategories, uploadedByUsers } = labelMaps;
|
|
|
|
appliedFilters.buildingIds?.forEach((id) =>
|
|
chips.push({ label: "Building", value: buildings.get(id) || id, key: "buildingIds", id })
|
|
);
|
|
appliedFilters.floorIds?.forEach((id) =>
|
|
chips.push({ label: "Floor", value: floors.get(id) || id, key: "floorIds", id })
|
|
);
|
|
appliedFilters.workAreaIds?.forEach((id) =>
|
|
chips.push({ label: "Work Area", value: workAreas.get(id) || id, key: "workAreaIds", id })
|
|
);
|
|
appliedFilters.activityIds?.forEach((id) =>
|
|
chips.push({ label: "Activity", value: activities.get(id) || id, key: "activityIds", id })
|
|
);
|
|
appliedFilters.uploadedByIds?.forEach((id) =>
|
|
chips.push({ label: "Uploaded By", value: uploadedByUsers.get(id) || id, key: "uploadedByIds", id })
|
|
);
|
|
appliedFilters.workCategoryIds?.forEach((id) =>
|
|
chips.push({ label: "Work Category", value: workCategories.get(id) || id, key: "workCategoryIds", id })
|
|
);
|
|
|
|
if (appliedFilters.startDate || appliedFilters.endDate) {
|
|
const start = appliedFilters.startDate ? moment(appliedFilters.startDate).format("DD MMM, YYYY") : "";
|
|
const end = appliedFilters.endDate ? moment(appliedFilters.endDate).format("DD MMM, YYYY") : "";
|
|
chips.push({ label: "Date Range", value: `${start} - ${end}`, key: "dateRange" });
|
|
}
|
|
return chips;
|
|
}, [appliedFilters, labelMaps]);
|
|
|
|
useEffect(() => { refetch(); }, [appliedFilters, refetch]);
|
|
|
|
const filterPanelElement = useMemo(
|
|
() => (
|
|
<ImageGalleryFilters
|
|
buildings={[...labelMaps.buildings]}
|
|
floors={[...labelMaps.floors]}
|
|
activities={[...labelMaps.activities]}
|
|
workAreas={[...labelMaps.workAreas]}
|
|
workCategories={[...labelMaps.workCategories]}
|
|
uploadedByUsers={[...labelMaps.uploadedByUsers]}
|
|
appliedFilters={appliedFilters}
|
|
onApplyFilters={handleApplyFilters}
|
|
removeBg
|
|
/>
|
|
),
|
|
[labelMaps, appliedFilters, handleApplyFilters]
|
|
);
|
|
|
|
useEffect(() => {
|
|
setShowTrigger(true);
|
|
setOffcanvasContent("Gallery Filters", filterPanelElement);
|
|
return () => {
|
|
setShowTrigger(false);
|
|
setOffcanvasContent("", null);
|
|
};
|
|
}, [filterPanelElement, setOffcanvasContent, setShowTrigger]);
|
|
|
|
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 () => loaderRef.current && observer.unobserve(loaderRef.current);
|
|
}, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]);
|
|
|
|
return (
|
|
<div className="container my-3">
|
|
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Gallery" }]} />
|
|
|
|
{/* Card wrapper */}
|
|
<div className="card shadow-sm">
|
|
<div
|
|
className="card-body"
|
|
style={{
|
|
minHeight: (!images?.length && !isLoading) ? "500px" : "auto",
|
|
}}
|
|
>
|
|
<div className="dataTables_length text-start py-1 px-0 col-md-4 col-12 mb-3">
|
|
{!servicesLoading && assignedServices?.length > 0 && (
|
|
assignedServices.length > 1 ? (
|
|
<label>
|
|
<select
|
|
name="DataTables_Table_0_length"
|
|
aria-controls="DataTables_Table_0"
|
|
className="form-select form-select-sm"
|
|
aria-label="Select Service"
|
|
value={selectedService}
|
|
onChange={handleServiceChange}
|
|
style={{ fontSize: "0.875rem", height: "35px", width: "200px" }}
|
|
>
|
|
<option value="">All Services</option>
|
|
{assignedServices.map((service) => (
|
|
<option key={service.id} value={service.id}>
|
|
{service.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
) : (
|
|
<h5>{assignedServices[0].name}</h5>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{/* Filter Chips */}
|
|
{appliedFiltersChips.length > 0 && (
|
|
<div className="mb-3 d-flex flex-wrap gap-2 align-items-center">
|
|
<strong className="me-2">Filters:</strong>
|
|
{["Building", "Floor", "Work Area", "Activity", "Uploaded By", "Work Category"].map((label) => {
|
|
const chips = appliedFiltersChips.filter(c => c.label === label);
|
|
if (!chips.length) return null;
|
|
return (
|
|
<div key={label} className="d-flex align-items-center gap-1">
|
|
<strong>{label}:</strong>
|
|
{chips.map(chip => (
|
|
<span
|
|
key={chip.id}
|
|
className="d-flex align-items-center bg-label-secondary px-2 py-1 rounded"
|
|
>
|
|
{chip.value}
|
|
<button
|
|
type="button"
|
|
className="btn-close btn-close-white btn-sm ms-1"
|
|
onClick={() => handleRemoveFilter(chip.key, chip.id)}
|
|
/>
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* Date Range */}
|
|
{appliedFiltersChips.some(c => c.label === "Date Range") && (
|
|
<div className="d-flex align-items-center bg-label-secondary px-2 py-1 rounded">
|
|
<strong>Date Range:</strong>
|
|
{appliedFiltersChips.filter(c => c.label === "Date Range").map((chip, idx) => (
|
|
<span key={idx} className="d-flex align-items-center ms-1">
|
|
{chip.value}
|
|
<button
|
|
type="button"
|
|
className="btn-close btn-close-white btn-sm ms-1"
|
|
onClick={() => handleRemoveFilter(chip.key, chip.id)}
|
|
/>
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Gallery */}
|
|
{isLoading ? (
|
|
<ImageGallerySkeleton count={4} />
|
|
) : (
|
|
<ImageGalleryListView
|
|
images={images}
|
|
isLoading={isLoading}
|
|
isFetchingNextPage={isFetchingNextPage}
|
|
hasNextPage={hasNextPage}
|
|
loaderRef={loaderRef}
|
|
openModal={openModal}
|
|
formatUTCToLocalTime={formatUTCToLocalTime}
|
|
moment={moment}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ImageGalleryPage;
|