Merge pull request 'Issues_July_2W' (#252) from Issues_July_2W into main

Reviewed-on: #252
This commit is contained in:
Vikas Nale 2025-07-14 10:40:18 +00:00
commit 3493445b19
15 changed files with 544 additions and 541 deletions

View File

@ -836,7 +836,7 @@ progress {
} }
.row { .row {
--bs-gutter-x: 1.625rem; --bs-gutter-x: 3.625rem;
--bs-gutter-y: 0; --bs-gutter-y: 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -2553,7 +2553,7 @@ progress {
} }
.table-responsive { .table-responsive {
overflow-x: auto; /* overflow-x: auto; */
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@ -8966,10 +8966,8 @@ a:not([href]):hover {
} }
/* Autofill input bg and text color issue on different OS and browsers */ /* Autofill input bg and text color issue on different OS and browsers */
input:-webkit-autofill,
input:-webkit-autofill:hover, input:-webkit-autofill:hover,
input:-webkit-autofill:focus, input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover, textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus, textarea:-webkit-autofill:focus,
select:-webkit-autofill, select:-webkit-autofill,
@ -8978,6 +8976,15 @@ select:-webkit-autofill:focus,
input:-internal-autofill-selected { input:-internal-autofill-selected {
background-clip: text !important; background-clip: text !important;
} }
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset !important;
box-shadow: 0 0 0px 1000px white inset !important;
-webkit-text-fill-color: #000 !important;
caret-color: #000 !important;
transition: background-color 5000s ease-in-out 0s;
}
h1, h1,
.h1 { .h1 {

View File

@ -2436,7 +2436,7 @@ progress {
} }
.table-responsive { .table-responsive {
overflow-x: auto; /* overflow-x: auto; */
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }

View File

@ -234,7 +234,7 @@ const AttendanceLog = ({
</div> </div>
<div <div
className="table-responsive text-nowrap" className="table-responsive text-nowrap"
style={{ minHeight: "250px" }} style={{ minHeight: "200px", display: 'flex', alignItems: 'center', justifyContent: 'center' }}
> >
{data && data.length > 0 && ( {data && data.length > 0 && (
<table className="table mb-0"> <table className="table mb-0">
@ -332,7 +332,7 @@ const AttendanceLog = ({
</table> </table>
)} )}
{!loading && !isRefreshing && data.length === 0 && ( {!loading && !isRefreshing && data.length === 0 && (
<span>No employee logs</span> <span className="text-muted">No employee logs</span>
)} )}
{/* {error && !loading && !isRefreshing && ( {/* {error && !loading && !isRefreshing && (
<tr> <tr>

View File

@ -14,7 +14,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
<ul className="nav nav-tabs "> <ul className="nav nav-tabs ">
<li className="nav-item"> <li className="nav-item">
<a <a
className={`nav-link ${activePill === "profile" ? "active" : ""}`} className={`nav-link ${activePill === "profile" ? "active" : ""} fs-6`}
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -26,7 +26,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
</li> </li>
<li className="nav-item"> <li className="nav-item">
<a <a
className={`nav-link ${activePill === "teams" ? "active" : ""}`} className={`nav-link ${activePill === "teams" ? "active" : ""}fs-6`}
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -38,7 +38,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
</li> </li>
<li className={`nav-item ${!HasViewInfraStructure && "d-none"} `}> <li className={`nav-item ${!HasViewInfraStructure && "d-none"} `}>
<a <a
className={`nav-link ${activePill === "infra" ? "active" : ""}`} className={`nav-link ${activePill === "infra" ? "active" : ""} fs-6`}
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -53,7 +53,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
<a <a
className={`nav-link ${ className={`nav-link ${
activePill === "imagegallary" ? "active" : "" activePill === "imagegallary" ? "active" : ""
}`} }fs-6`}
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); // Prevent page reload e.preventDefault(); // Prevent page reload
@ -66,7 +66,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
{(DirAdmin || DireManager || DirUser) && ( {(DirAdmin || DireManager || DirUser) && (
<li className="nav-item"> <li className="nav-item">
<a <a
className={`nav-link ${activePill === "directory" ? "active" : ""}`} className={`nav-link ${activePill === "directory" ? "active" : ""} fs-6`}
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); // Prevent page reload e.preventDefault(); // Prevent page reload

View File

@ -26,7 +26,8 @@ const DateRangePicker = ({
altFormat: "d-m-Y", altFormat: "d-m-Y",
defaultDate: [startDate, endDate], defaultDate: [startDate, endDate],
static: true, static: true,
clickOpens: true, clickOpens: true,
maxDate: endDate, // Disable future dates
onChange: (selectedDates, dateStr) => { onChange: (selectedDates, dateStr) => {
const [startDateString, endDateString] = dateStr.split(" to "); const [startDateString, endDateString] = dateStr.split(" to ");
onRangeChange?.({ startDate: startDateString, endDate: endDateString }); onRangeChange?.({ startDate: startDateString, endDate: endDateString });
@ -54,4 +55,4 @@ const DateRangePicker = ({
); );
}; };
export default DateRangePicker; export default DateRangePicker;

View File

@ -0,0 +1,85 @@
import { useState, useCallback } from "react";
// import { ImageGalleryAPI } from "../repositories/ImageGalleyRepository";
import { ImageGalleryAPI } from "../repositories/ImageGalleryAPI";
const PAGE_SIZE = 10;
const useImageGallery = (selectedProjectId) => {
const [images, setImages] = useState([]);
const [allImagesData, setAllImagesData] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const fetchImages = useCallback(async (page = 1, filters = {}, reset = false) => {
if (!selectedProjectId) return;
try {
if (page === 1) {
setLoading(true);
} else {
setLoadingMore(true);
}
const res = await ImageGalleryAPI.ImagesGet(
selectedProjectId,
filters,
page,
PAGE_SIZE
);
const newBatches = res.data || [];
const receivedCount = newBatches.length;
setImages((prev) => {
if (page === 1 || reset) return newBatches;
const uniqueNew = newBatches.filter(
(batch) => !prev.some((b) => b.batchId === batch.batchId)
);
return [...prev, ...uniqueNew];
});
setAllImagesData((prev) => {
if (page === 1 || reset) return newBatches;
const uniqueAll = newBatches.filter(
(batch) => !prev.some((b) => b.batchId === batch.batchId)
);
return [...prev, ...uniqueAll];
});
setHasMore(receivedCount === PAGE_SIZE);
} catch (error) {
console.error("Error fetching images:", error);
if (page === 1) {
setImages([]);
setAllImagesData([]);
}
setHasMore(false);
} finally {
setLoading(false);
setLoadingMore(false);
}
}, [selectedProjectId]);
const resetGallery = useCallback(() => {
setImages([]);
setAllImagesData([]);
setPageNumber(1);
setHasMore(true);
}, []);
return {
images,
allImagesData,
pageNumber,
setPageNumber,
hasMore,
loading,
loadingMore,
fetchImages,
resetGallery,
};
};
export default useImageGallery;

View File

@ -1,12 +1,31 @@
import { useState, useMemo } from "react"; import { useState, useMemo,useEffect } from "react";
const usePagination = (data, itemsPerPage) => { const usePagination = (data, itemsPerPage) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(data?.length / itemsPerPage);
// const totalPages = Math.ceil(data?.length / itemsPerPage);
// add this new line
const totalPages = useMemo(() => {
return Math.ceil((data?.length || 0) / itemsPerPage);
}, [data?.length, itemsPerPage]);
useEffect(() => {
if (currentPage > totalPages && totalPages > 0) {
setCurrentPage(1);
} else if (!data || data.length === 0) {
setCurrentPage(1);
} else if (currentPage === 0 && totalPages > 0) {
setCurrentPage(1);
}
}, [data, totalPages, currentPage]);
const currentItems = useMemo(() => { const currentItems = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage; const startIndex = (currentPage - 1) * itemsPerPage;
return data?.slice(startIndex, startIndex + itemsPerPage);
// return data?.slice(startIndex, startIndex + itemsPerPage);
return data?.slice(startIndex, startIndex + itemsPerPage) || [];
}, [data, currentPage, itemsPerPage]); }, [data, currentPage, itemsPerPage]);
const paginate = (pageNumber) => { const paginate = (pageNumber) => {

View File

@ -233,7 +233,7 @@ const AttendancePage = () => {
<li className="nav-item"> <li className="nav-item">
<button <button
type="button" type="button"
className={`nav-link ${activeTab === "all" ? "active" : ""}`} className={`nav-link ${activeTab === "all" ? "active" : ""} fs-6`}
onClick={() => setActiveTab("all")} onClick={() => setActiveTab("all")}
data-bs-toggle="tab" data-bs-toggle="tab"
data-bs-target="#navs-top-home" data-bs-target="#navs-top-home"
@ -244,7 +244,7 @@ const AttendancePage = () => {
<li className="nav-item"> <li className="nav-item">
<button <button
type="button" type="button"
className={`nav-link ${activeTab === "logs" ? "active" : ""}`} className={`nav-link ${activeTab === "logs" ? "active" : ""} fs-6`}
onClick={() => setActiveTab("logs")} onClick={() => setActiveTab("logs")}
data-bs-toggle="tab" data-bs-toggle="tab"
data-bs-target="#navs-top-profile" data-bs-target="#navs-top-profile"
@ -257,7 +257,7 @@ const AttendancePage = () => {
type="button" type="button"
className={`nav-link ${ className={`nav-link ${
activeTab === "regularization" ? "active" : "" activeTab === "regularization" ? "active" : ""
}`} } fs-6`}
onClick={() => setActiveTab("regularization")} onClick={() => setActiveTab("regularization")}
data-bs-toggle="tab" data-bs-toggle="tab"
data-bs-target="#navs-top-messages" data-bs-target="#navs-top-messages"

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import "./ImageGallery.css"; import "./ImageGallery.css";
import { ImageGalleryAPI } from "./ImageGalleryAPI";
import moment from "moment"; import moment from "moment";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useModal } from "./ModalContext"; import { useModal } from "./ModalContext";
@ -9,17 +8,25 @@ import Avatar from "../../components/common/Avatar";
import DateRangePicker from "../../components/common/DateRangePicker"; import DateRangePicker from "../../components/common/DateRangePicker";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import {formatUTCToLocalTime} from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import useImageGallery from "../../hooks/useImageGallery";
const PAGE_SIZE = 10;
const SCROLL_THRESHOLD = 5; const SCROLL_THRESHOLD = 5;
const ImageGallery = () => { const ImageGallery = () => {
const [images, setImages] = useState([]);
const [allImagesData, setAllImagesData] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(true);
const selectedProjectId = useSelector((store) => store.localVariables.projectId); const selectedProjectId = useSelector((store) => store.localVariables.projectId);
const {
images,
allImagesData,
pageNumber,
setPageNumber,
hasMore,
loading,
loadingMore,
fetchImages,
resetGallery,
} = useImageGallery(selectedProjectId);
const { openModal } = useModal(); const { openModal } = useModal();
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD'); const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
@ -58,8 +65,6 @@ const ImageGallery = () => {
const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false);
const [hoveredImage, setHoveredImage] = useState(null); const [hoveredImage, setHoveredImage] = useState(null);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const imageGroupRefs = useRef({}); const imageGroupRefs = useRef({});
const loaderRef = useRef(null); const loaderRef = useRef(null);
@ -91,71 +96,18 @@ const ImageGallery = () => {
useEffect(() => { useEffect(() => {
if (!selectedProjectId) { if (!selectedProjectId) {
setImages([]); resetGallery();
setAllImagesData([]);
setLoading(false);
setHasMore(false);
return; return;
} }
setImages([]); resetGallery();
setPageNumber(1);
setHasMore(true);
setLoading(true);
setAllImagesData([]);
fetchImages(1, appliedFilters, true); fetchImages(1, appliedFilters, true);
}, [selectedProjectId, appliedFilters]); }, [selectedProjectId, appliedFilters]);
const fetchImages = useCallback(async (page, filters) => {
if (!selectedProjectId) return;
try {
if (page === 1) {
setLoading(true);
} else {
setLoadingMore(true);
}
const res = await ImageGalleryAPI.ImagesGet(selectedProjectId, filters, page, PAGE_SIZE);
const newBatches = res.data || [];
const receivedCount = newBatches.length;
setImages((prevImages) => {
const uniqueNewBatches = newBatches.filter(
(newBatch) => !prevImages.some((prevBatch) => prevBatch.batchId === newBatch.batchId)
);
return [...prevImages, ...uniqueNewBatches];
});
setAllImagesData((prevAllImages) => {
const uniqueAllImages = newBatches.filter(
(newBatch) => !prevAllImages.some((prevBatch) => prevBatch.batchId === newBatch.batchId)
);
return [...prevAllImages, ...uniqueAllImages];
});
setHasMore(receivedCount === PAGE_SIZE);
} catch (err) {
console.error("Error fetching images:", err);
if (page === 1) {
setImages([]);
setAllImagesData([]);
}
setHasMore(false);
} finally {
setLoading(false);
setLoadingMore(false);
}
}, [selectedProjectId]);
useEffect(() => { useEffect(() => {
const handleExternalEvent = (data) => { const handleExternalEvent = (data) => {
if (selectedProjectId === data.projectId) { if (selectedProjectId === data.projectId) {
setImages([]); resetGallery();
setAllImagesData([]);
setPageNumber(1);
setHasMore(true);
fetchImages(1, appliedFilters, true); fetchImages(1, appliedFilters, true);
} }
}; };
@ -196,20 +148,13 @@ const ImageGallery = () => {
if (pageNumber > 1) { if (pageNumber > 1) {
fetchImages(pageNumber, appliedFilters); fetchImages(pageNumber, appliedFilters);
} }
}, [pageNumber, fetchImages, appliedFilters]); }, [pageNumber]);
const getUniqueValuesWithIds = useCallback((idKey, nameKey) => { const getUniqueValuesWithIds = useCallback((idKey, nameKey) => {
const map = new Map(); const map = new Map();
allImagesData.forEach(batch => { allImagesData.forEach(batch => {
let id; let id = idKey === "floorIds" ? batch.floorIds : batch[idKey];
if (idKey === "floorIds") {
id = batch.floorIds;
} else {
id = batch[idKey];
}
const name = batch[nameKey]; const name = batch[nameKey];
if (id && name && !map.has(id)) { if (id && name && !map.has(id)) {
map.set(id, name); map.set(id, name);
} }
@ -229,7 +174,7 @@ const ImageGallery = () => {
} }
}); });
}); });
return Array.from(uniqueUsersMap.entries()).sort((a, b) => a[1].localeCompare(b[1])); return Array.from(uniqueUsersMap.entries()).sort((a, b) => a[1].localeCompare(b[1]));
}, [allImagesData]); }, [allImagesData]);
const buildings = getUniqueValuesWithIds("buildingId", "buildingName"); const buildings = getUniqueValuesWithIds("buildingId", "buildingName");
@ -272,30 +217,12 @@ const ImageGallery = () => {
const handleApplyFilters = useCallback(() => { const handleApplyFilters = useCallback(() => {
const payload = { const payload = {
buildingIds: buildingIds: selectedFilters.building.length ? selectedFilters.building.map((item) => item[0]) : null,
selectedFilters.building.length > 0 floorIds: selectedFilters.floor.length ? selectedFilters.floor.map((item) => item[0]) : null,
? selectedFilters.building.map((item) => item[0]) workAreaIds: selectedFilters.workArea.length ? selectedFilters.workArea.map((item) => item[0]) : null,
: null, workCategoryIds: selectedFilters.workCategory.length ? selectedFilters.workCategory.map((item) => item[0]) : null,
floorIds: activityIds: selectedFilters.activity.length ? selectedFilters.activity.map((item) => item[0]) : null,
selectedFilters.floor.length > 0 uploadedByIds: selectedFilters.uploadedBy.length ? selectedFilters.uploadedBy.map((item) => item[0]) : null,
? 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, startDate: selectedFilters.startDate || null,
endDate: selectedFilters.endDate || null, endDate: selectedFilters.endDate || null,
}; };
@ -303,30 +230,23 @@ const ImageGallery = () => {
const areFiltersChanged = Object.keys(payload).some(key => { const areFiltersChanged = Object.keys(payload).some(key => {
const oldVal = appliedFilters[key]; const oldVal = appliedFilters[key];
const newVal = payload[key]; const newVal = payload[key];
if (Array.isArray(oldVal) && Array.isArray(newVal)) { if (Array.isArray(oldVal) && Array.isArray(newVal)) {
if (oldVal.length !== newVal.length) return true; if (oldVal.length !== newVal.length) return true;
const oldSet = new Set(oldVal); const oldSet = new Set(oldVal);
const newSet = new Set(newVal); const newSet = new Set(newVal);
if (oldSet.size !== newSet.size) return true;
for (const item of newSet) { for (const item of newSet) {
if (!oldSet.has(item)) return true; if (!oldSet.has(item)) return true;
} }
return false; return false;
} }
if ((oldVal === null && newVal === "") || (oldVal === "" && newVal === null)) { if ((oldVal === null && newVal === "") || (oldVal === "" && newVal === null)) return false;
return false;
}
return oldVal !== newVal; return oldVal !== newVal;
}); });
if (areFiltersChanged) { if (areFiltersChanged) {
setAppliedFilters(payload); setAppliedFilters(payload);
setImages([]); resetGallery();
setPageNumber(1);
setHasMore(true);
} }
// Removed setIsFilterPanelOpen(false); to keep the drawer open
}, [selectedFilters, appliedFilters]); }, [selectedFilters, appliedFilters]);
const handleClearAllFilters = useCallback(() => { const handleClearAllFilters = useCallback(() => {
@ -353,9 +273,7 @@ const ImageGallery = () => {
endDate: null, endDate: null,
}; };
setAppliedFilters(initialStateApplied); setAppliedFilters(initialStateApplied);
setImages([]); resetGallery();
setPageNumber(1);
setHasMore(true);
}, []); }, []);
const scrollLeft = useCallback((key) => { const scrollLeft = useCallback((key) => {
@ -382,22 +300,18 @@ const ImageGallery = () => {
Clear Clear
</button> </button>
)} )}
{type !== "dateRange" && {type !== "dateRange" && selectedFilters[type]?.length > 0 && (
selectedFilters[type] && <button
selectedFilters[type].length > 0 && ( className="clear-button"
<button onClick={(e) => {
className="clear-button" e.stopPropagation();
onClick={(e) => { setSelectedFilters((prev) => ({ ...prev, [type]: [] }));
e.stopPropagation(); }}
setSelectedFilters((prev) => ({ ...prev, [type]: [] })); >
}} Clear
> </button>
Clear )}
</button> <span className="collapse-icon">{collapsedFilters[type] ? '+' : '-'}</span>
)}
<span className="collapse-icon">
{collapsedFilters[type] ? '+' : '-'}
</span>
</div> </div>
</div> </div>
{!collapsedFilters[type] && ( {!collapsedFilters[type] && (
@ -406,31 +320,22 @@ const ImageGallery = () => {
<div className="date-range-inputs"> <div className="date-range-inputs">
<DateRangePicker <DateRangePicker
onRangeChange={setDateRange} onRangeChange={setDateRange}
defaultStartDate={selectedFilters.startDate || yesterday} endDateMode="today"
defaultEndDate={selectedFilters.endDate || moment().format('YYYY-MM-DD')}
startDate={selectedFilters.startDate} startDate={selectedFilters.startDate}
endDate={selectedFilters.endDate} endDate={selectedFilters.endDate}
/> />
</div> </div>
) : ( ) : (
items.map((item) => { items.map(([itemId, itemName]) => (
const itemId = item[0]; <label key={itemId}>
const itemName = item[1]; <input
const isChecked = selectedFilters[type].some( type="checkbox"
(selectedItem) => selectedItem[0] === itemId checked={selectedFilters[type].some((item) => item[0] === itemId)}
); onChange={() => toggleFilter(type, itemId, itemName)}
/>
return ( {itemName}
<label key={itemId}> </label>
<input ))
type="checkbox"
checked={isChecked}
onChange={() => toggleFilter(type, itemId, itemName)}
/>
{itemName}
</label>
);
})
)} )}
</div> </div>
)} )}
@ -438,40 +343,25 @@ const ImageGallery = () => {
); );
return ( return (
<div className={`gallery-container container-fluid ${ isFilterPanelOpen ? "filter-panel-open-end" : "" }`}> <div className={`gallery-container container-fluid ${isFilterPanelOpen ? "filter-panel-open-end" : ""}`}>
<Breadcrumb <Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Gallary", link: null }]} />
data={[
{ label: "Home", link: "/" },
{ label: "Gallary", link: null },
]}
></Breadcrumb>
<div className="main-content"> <div className="main-content">
<button <button
className={`filter-button btn-primary ${isFilterPanelOpen ? "closed-icon" : ""}`} className={`filter-button btn-primary ${isFilterPanelOpen ? "closed-icon" : ""}`}
onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
ref={filterButtonRef} ref={filterButtonRef}
> >
{isFilterPanelOpen ? ( {isFilterPanelOpen ? <i className="fa-solid fa-times fs-5"></i> : <i className="fa-solid fa-filter ms-1 fs-5"></i>}
<i className="fa-solid fa-times fs-5"></i>
) : (
<><i className="fa-solid fa-filter ms-1 fs-5"></i></>
)}
</button> </button>
<div className="activity-section"> <div className="activity-section">
{loading && pageNumber === 1 ? ( {loading && pageNumber === 1 ? (
<div className="spinner-container"> <div className="spinner-container"><div className="spinner" /></div>
<div className="spinner" />
</div>
) : images.length > 0 ? ( ) : images.length > 0 ? (
images.map((batch) => { images.map((batch) => {
const firstDoc = batch.documents[0]; const firstDoc = batch.documents[0];
const userName = `${firstDoc?.uploadedBy?.firstName || ""} ${firstDoc?.uploadedBy?.lastName || "" const userName = `${firstDoc?.uploadedBy?.firstName || ""} ${firstDoc?.uploadedBy?.lastName || ""}`.trim();
}`.trim(); const date = formatUTCToLocalTime(firstDoc?.uploadedAt);
const date = formatUTCToLocalTime(firstDoc?.uploadedAt)
const showScrollButtons = batch.documents.length > SCROLL_THRESHOLD; const showScrollButtons = batch.documents.length > SCROLL_THRESHOLD;
return ( return (
@ -479,29 +369,15 @@ const ImageGallery = () => {
<div className="group-heading"> <div className="group-heading">
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<div className="d-flex align-items-center mb-1"> <div className="d-flex align-items-center mb-1">
<Avatar <Avatar size="xs" firstName={firstDoc?.uploadedBy?.firstName} lastName={firstDoc?.uploadedBy?.lastName} className="me-2" />
size="xs"
firstName={firstDoc?.uploadedBy?.firstName}
lastName={firstDoc?.uploadedBy?.lastName}
className="me-2"
/>
<div className="d-flex flex-column align-items-start"> <div className="d-flex flex-column align-items-start">
<strong className="user-name-text"> <strong className="user-name-text">{userName}</strong>
{userName} <span className="me-2">{date}</span>
</strong>
<span className="me-2">
{date}
</span>
</div> </div>
</div> </div>
</div> </div>
<div className="location-line"> <div className="location-line">
<div> <div>{batch.buildingName} &gt; {batch.floorName} &gt; <strong>{batch.workAreaName || "Unknown"} &gt; {batch.activityName}</strong></div>
{batch.buildingName} &gt; {batch.floorName} &gt;{" "}
<strong>{batch.workAreaName || "Unknown"} &gt;{" "}
{batch.activityName}</strong>
</div>
{batch.workCategoryName && ( {batch.workCategoryName && (
<div className="work-category-display ms-2"> <div className="work-category-display ms-2">
<span className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1"> <span className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1">
@ -511,107 +387,53 @@ const ImageGallery = () => {
)} )}
</div> </div>
</div> </div>
<div className="image-group-wrapper"> <div className="image-group-wrapper">
{showScrollButtons && ( {showScrollButtons && <button className="scroll-arrow left-arrow" onClick={() => scrollLeft(batch.batchId)}>&#8249;</button>}
<button <div className="image-group-horizontal" ref={(el) => (imageGroupRefs.current[batch.batchId] = el)}>
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) => { {batch.documents.map((doc, idx) => {
const hoverDate = moment(doc.uploadedAt).format("DD-MM-YYYY"); const hoverDate = moment(doc.uploadedAt).format("DD-MM-YYYY");
const hoverTime = moment(doc.uploadedAt).format("hh:mm A"); const hoverTime = moment(doc.uploadedAt).format("hh:mm A");
return ( return (
<div <div key={doc.id} className="image-card" onClick={() => openModal(<ImagePop batch={batch} initialIndex={idx} />)} onMouseEnter={() => setHoveredImage(doc)} onMouseLeave={() => setHoveredImage(null)}>
key={doc.id}
className="image-card"
onClick={() =>
openModal(<ImagePop batch={batch} initialIndex={idx} />)
}
onMouseEnter={() => setHoveredImage(doc)}
onMouseLeave={() => setHoveredImage(null)}
>
<div className="image-wrapper"> <div className="image-wrapper">
<img src={doc.url} alt={`Image ${idx + 1}`} /> <img src={doc.url} alt={`Image ${idx + 1}`} />
</div> </div>
{hoveredImage === doc && ( {hoveredImage === doc && (
<div className="image-hover-description"> <div className="image-hover-description">
<p> <p><strong>Date:</strong> {hoverDate}</p>
<strong>Date:</strong> {hoverDate} <p><strong>Time:</strong> {hoverTime}</p>
</p> <p><strong>Activity:</strong> {batch.activityName}</p>
<p>
<strong>Time:</strong> {hoverTime}
</p>
<p>
<strong>Activity:</strong> {batch.activityName}
</p>
</div> </div>
)} )}
</div> </div>
); );
})} })}
</div> </div>
{showScrollButtons && ( {showScrollButtons && <button className="scroll-arrow right-arrow" onClick={() => scrollRight(batch.batchId)}>&#8250;</button>}
<button
className="scroll-arrow right-arrow"
onClick={() => scrollRight(batch.batchId)}
>
&#8250;
</button>
)}
</div> </div>
</div> </div>
); );
}) })
) : ( ) : (
!loading && <p style={{ textAlign: "center", color: "#777", marginTop: "50px" }}> !loading && <p style={{ textAlign: "center", color: "#777", marginTop: "50px" }}>No images match the selected filters.</p>
No images match the selected filters.
</p>
)} )}
<div ref={loaderRef} style={{ height: '50px', margin: '20px 0', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <div ref={loaderRef} style={{ height: '50px', margin: '20px 0', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{loadingMore && hasMore && <div className="spinner" />} {loadingMore && hasMore && <div className="spinner" />}
{!hasMore && !loading && images.length > 0 && ( {!hasMore && !loading && images.length > 0 && <p style={{ color: '#aaa' }}>You've reached the end of the images.</p>}
<p style={{ color: '#aaa' }}>You've reached the end of the images.</p>
)}
</div> </div>
</div> </div>
</div> </div>
<div <div className={`offcanvas offcanvas-end ${isFilterPanelOpen ? "show" : ""}`} tabIndex="-1" id="filterOffcanvas" aria-labelledby="filterOffcanvasLabel" ref={filterPanelRef}>
className={`offcanvas offcanvas-end ${isFilterPanelOpen ? "show" : ""}`}
tabIndex="-1"
id="filterOffcanvas"
aria-labelledby="filterOffcanvasLabel"
ref={filterPanelRef}
>
<div className="offcanvas-header"> <div className="offcanvas-header">
<h5 className="offcanvas-title" id="filterOffcanvasLabel"> <h5 className="offcanvas-title" id="filterOffcanvasLabel">Filters</h5>
Filters <button type="button" className="btn-close" onClick={() => setIsFilterPanelOpen(false)} aria-label="Close" />
</h5>
<button
type="button"
className="btn-close"
onClick={() => setIsFilterPanelOpen(false)}
aria-label="Close"
></button>
</div> </div>
<div className="filter-actions mt-auto mx-2"> <div className="filter-actions mt-auto mx-2">
<button className="btn btn-secondary btn-xs" onClick={handleClearAllFilters}> <button className="btn btn-secondary btn-xs" onClick={handleClearAllFilters}>Clear All</button>
Clear All <button className="btn btn-primary btn-xs" onClick={handleApplyFilters}>Apply Filters</button>
</button> </div>
<button className="btn btn-primary btn-xs" onClick={handleApplyFilters}>
Apply Filters
</button>
</div>
<div className="offcanvas-body d-flex flex-column"> <div className="offcanvas-body d-flex flex-column">
{renderFilterCategory("Date Range", [], "dateRange")} {renderFilterCategory("Date Range", [], "dateRange")}
{renderFilterCategory("Building", buildings, "building")} {renderFilterCategory("Building", buildings, "building")}
@ -620,12 +442,10 @@ const ImageGallery = () => {
{renderFilterCategory("Activity", activities, "activity")} {renderFilterCategory("Activity", activities, "activity")}
{renderFilterCategory("Uploaded By (User)", uploadedByUsers, "uploadedBy")} {renderFilterCategory("Uploaded By (User)", uploadedByUsers, "uploadedBy")}
{renderFilterCategory("Work Category", workCategories, "workCategory")} {renderFilterCategory("Work Category", workCategories, "workCategory")}
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default ImageGallery; export default ImageGallery;

View File

@ -1,103 +1,149 @@
/* Image Modal Overlay */
.image-modal-overlay { .image-modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 9999; /* High z-index to ensure it's on top */ z-index: 9999;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.85); /* Dark semi-transparent background */ background-color: rgba(0, 0, 0, 0.85);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
/* Main Modal Content Box */
.image-modal-content { .image-modal-content {
background: #fff; background: #fff;
padding: 24px; padding: 24px;
max-width: 90%; /* Responsive max-width */ max-width: 50%;
max-height: 100%; /* Responsive max-height */ max-height: 95vh; /* Limits the modal's height to 95% of viewport height */
border-radius: 12px; border-radius: 12px;
position: relative; position: relative;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
text-align: center; text-align: center;
display: flex; /* Use flexbox for internal layout */ display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow-y: auto; /* Enables vertical scrolling */
/* --- HIDE SCROLLBAR FOR MAIN MODAL CONTENT --- */
/* For Webkit browsers (Chrome, Safari, Edge) */
&::-webkit-scrollbar {
width: 0px; /* Hide vertical scrollbar */
height: 0px; /* Hide horizontal scrollbar, though unlikely needed here */
}
/* For Firefox */
scrollbar-width: none; /* Hide scrollbar in Firefox */
/* For Internet Explorer and Edge (legacy) */
-ms-overflow-style: none;
/* --- END HIDE SCROLLBAR --- */
} }
/* Image Styles */
.modal-image { .modal-image {
max-width: 100%; max-width: 100%;
max-height: 70vh; /* Limits image height to 70% of viewport height */ max-height: 70vh;
width: auto;
border-radius: 10px; border-radius: 10px;
object-fit: contain; /* Ensures the whole image is visible without cropping */ object-fit: contain;
margin-bottom: 20px; margin-bottom: 20px;
flex-shrink: 0; /* Prevent image from shrinking if content is too large */ flex-shrink: 0;
} }
.image-details { /* Scrollable Container for Text Details */
.image-details-scroll-container {
width: 100%;
flex-grow: 1;
max-height: calc(95vh - 70vh - (24px * 2) - 20px); /* Approximate calculation for text area height */
overflow-y: auto; /* Enables vertical scrolling for details */
text-align: left; text-align: left;
padding-right: 5px; /* Add some padding so text doesn't touch the hidden scrollbar area */
/* --- HIDE SCROLLBAR FOR TEXT DETAILS SECTION --- */
/* For Webkit browsers (Chrome, Safari, Edge) */
&::-webkit-scrollbar {
width: 0px; /* Hide vertical scrollbar */
height: 0px; /* Hide horizontal scrollbar */
}
/* For Firefox */
scrollbar-width: none; /* Hide scrollbar in Firefox */
/* For Internet Explorer and Edge (legacy) */
-ms-overflow-style: none;
/* --- END HIDE SCROLLBAR --- */
}
/* Image Details Section (inside the scroll container) */
.image-details {
color: #444; color: #444;
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; /* Ensure details section takes full width */ width: 100%;
text-align: left;
} }
.image-details p { .image-details p {
margin: 4px 0; /* Reduce vertical space between lines in details */ margin: 4px 0;
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
text-overflow: initial;
text-align: left;
} }
/* Close Button */
.close-button { .close-button {
position: absolute; position: absolute;
top: 1px; /* Position relative to the modal content */ top: 1px;
right: 8px; right: 8px;
font-size: 30px; font-size: 30px;
background: none; background: none;
border: none; border: none;
color: black; /* White color for visibility on dark overlay */ color: black;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
line-height: 1; line-height: 1;
z-index: 10000; /* Ensure it's above everything else */ z-index: 10000;
} }
/* Styles for the navigation buttons */ /* Navigation Buttons */
.nav-button { .nav-button {
position: absolute; position: absolute;
top: 50%; /* Vertically center them */ top: 50%;
transform: translateY(-50%); /* Adjust for perfect vertical centering */ transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */ background-color: rgba(0, 0, 0, 0.5);
color: white; color: white;
border: none; border: none;
padding: 10px 15px; padding: 10px 15px;
font-size: 30px; font-size: 30px;
cursor: pointer; cursor: pointer;
z-index: 1000; /* Ensure buttons are above the image */ z-index: 1000;
border-radius: 50%; /* Make them circular */ border-radius: 50%;
width: 50px; width: 50px;
height: 50px; height: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background-color 0.3s ease; /* Smooth hover effect */ transition: background-color 0.3s ease;
} }
.nav-button:hover { .nav-button:hover {
background-color: rgba(0, 0, 0, 0.8); /* Darker on hover */ background-color: rgba(0, 0, 0, 0.8);
} }
.nav-button.prev-button { .nav-button.prev-button {
left: 0px; /* Position left arrow */ left: 0px;
} }
.nav-button.next-button { .nav-button.next-button {
right: 0px; /* Position right arrow */ right: 0px;
} }
/* Style for disabled buttons (optional) */ /* Disabled Button Style */
.nav-button:disabled { .nav-button:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@ -69,22 +69,17 @@ const ChangePasswordPage = ({ onClose }) => {
className="modal d-flex align-items-center justify-content-center show" className="modal d-flex align-items-center justify-content-center show"
tabIndex="-1" tabIndex="-1"
role="dialog" role="dialog"
style={{ display: "flex", backgroundColor: "rgba(0,0,0,0.5)" }} style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
> >
<div className="modal-dialog" role="document"> <div className="modal-dialog" role="document">
<div className="modal-header"> <div className="modal-content p-10 rounded shadow bg-white position-relative">
{" "}
<button <button
type="button" type="button"
class="btn-close" className="btn-close"
data-bs-dismiss="modal" data-bs-dismiss="modal" aria-label="Close"
style={{ top: "40px", right: "15px" }} onClick={onClose} // Use onClick to trigger onClose function
aria-label="Close" style={{ position: "absolute", top: "15px", right: "15px" }} // Example positioning
></button> ></button>
</div>
<div className="modal-content p-10 rounded shadow bg-white position-relative">
{/* Close Button */}
<h5 className="mb-2">Change Password</h5> <h5 className="mb-2">Change Password</h5>
<p className="mb-4" style={{ fontSize: "14px" }}> <p className="mb-4" style={{ fontSize: "14px" }}>
@ -119,7 +114,6 @@ const ChangePasswordPage = ({ onClose }) => {
)} )}
</div> </div>
{/* <div className="row"> */}
<div className="mb-3 text-start"> <div className="mb-3 text-start">
<label className="form-label">New Password</label> <label className="form-label">New Password</label>
<div className="input-group input-group-merge d-flex align-items-center border rounded px-2"> <div className="input-group input-group-merge d-flex align-items-center border rounded px-2">
@ -180,14 +174,14 @@ const ChangePasswordPage = ({ onClose }) => {
</div> </div>
)} )}
</div> </div>
{/* </div> */}
<div className="d-flex justify-content-center pt-2"> <div className="d-flex justify-content-center pt-2 text-muted small">
Your password must have at least 8 characters and include a lower Your password must have at least 8 characters and include a lower
case latter, an uppercase letter, a number, and a special case letter, an uppercase letter, a number, and a special
character. character.
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
<div className="d-flex justify-content-center pt-2"> <div className="d-flex justify-content-center pt-4">
<button <button
type="submit" type="submit"
className="btn btn-primary btn-sm me-2" className="btn btn-primary btn-sm me-2"
@ -211,4 +205,4 @@ const ChangePasswordPage = ({ onClose }) => {
); );
}; };
export default ChangePasswordPage; export default ChangePasswordPage;

View File

@ -18,9 +18,9 @@ const otpSchema = z.object({
const LoginWithOtp = () => { const LoginWithOtp = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [ loading, setLoading ] = useState( false ); const [loading, setLoading] = useState(false);
const [ timeLeft, setTimeLeft ] = useState( 0 ); const [timeLeft, setTimeLeft] = useState(0);
const inputRefs = useRef([]); const inputRefs = useRef([]);
@ -29,68 +29,97 @@ const LoginWithOtp = () => {
handleSubmit, handleSubmit,
formState: { errors, isSubmitted }, formState: { errors, isSubmitted },
getValues, getValues,
setValue,
trigger,
} = useForm({ } = useForm({
resolver: zodResolver(otpSchema), resolver: zodResolver(otpSchema),
}); });
const onSubmit = async (data) => { const onSubmit = async (data) => {
const finalOtp = data.otp1 + data.otp2 + data.otp3 + data.otp4; const finalOtp = data.otp1 + data.otp2 + data.otp3 + data.otp4;
const username = localStorage.getItem( "otpUsername" ); const username = localStorage.getItem("otpUsername");
setLoading(true); setLoading(true);
try { try {
let requestedData = { let requestedData = {
email: username, email: username,
otp:finalOtp otp: finalOtp
} }
const response = await AuthRepository.verifyOTP( requestedData ) const response = await AuthRepository.verifyOTP(requestedData)
localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken); localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading( false ); setLoading(false);
localStorage.removeItem( "otpUsername" ); localStorage.removeItem("otpUsername");
localStorage.removeItem( "otpSentTime" ); localStorage.removeItem("otpSentTime");
navigate( "/dashboard" ); navigate("/dashboard");
} catch (err) { } catch (err) {
showToast( "Invalid or expired OTP.", "error" ); showToast("Invalid or expired OTP.", "error");
setLoading(false); setLoading(false);
} }
}; };
const formatTime = (seconds) => { const formatTime = (seconds) => {
const min = Math.floor(seconds / 60).toString().padStart(2, "0"); const min = Math.floor(seconds / 60).toString().padStart(2, "0");
const sec = (seconds % 60).toString().padStart(2, "0"); const sec = (seconds % 60).toString().padStart(2, "0");
return `${min}:${sec}`; return `${min}:${sec}`;
}; };
useEffect(() => { // Time Logic for OTP expiry
const otpSentTime = localStorage.getItem("otpSentTime"); useEffect(() => {
const now = Date.now(); const otpSentTime = localStorage.getItem("otpSentTime");
const now = Date.now();
if (otpSentTime) { if (otpSentTime) {
const elapsed = Math.floor((now - Number(otpSentTime)) / 1000); // in seconds const elapsed = Math.floor((now - Number(otpSentTime)) / 1000); //in seconds
const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0); // prevent negatives const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0); //prevent negatives
setTimeLeft(remaining); setTimeLeft(remaining);
} }
}, []); }, []);
useEffect(() => {
if (timeLeft <= 0) return;
const timer = setInterval(() => {
setTimeLeft((prev) => { useEffect(() => {
if (prev <= 1) { if (timeLeft <= 0) return;
clearInterval(timer);
localStorage.removeItem( "otpSentTime" ); const timer = setInterval(() => {
localStorage.removeItem("otpUsername"); setTimeLeft((prev) => {
return 0; if (prev <= 1) {
clearInterval(timer);
localStorage.removeItem("otpSentTime");
localStorage.removeItem("otpUsername");
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [timeLeft]);
// Handle Paste Event
const handlePaste = (e) => {
e.preventDefault();
const pastedData = e.clipboardData.getData("text/plain").trim();
if (pastedData.match(/^\d{4}$/)) {
for (let i = 0; i < pastedData.length; i++) {
setValue(`otp${i + 1}`, pastedData[i], { shouldValidate: true });
if (inputRefs.current[i + 1]) {
inputRefs.current[i + 1].focus();
}
} }
return prev - 1; trigger(["otp1", "otp2", "otp3", "otp4"]);
}); } else {
}, 1000); showToast("Invalid OTP format pasted. Please enter 4 digits")
return () => clearInterval(timer); for (let i = 0; i < 4; i++) {
}, [timeLeft]); setValue(`otp${i + 1}`, "")
}
}
}
return ( return (
@ -109,9 +138,8 @@ useEffect(() => {
key={num} key={num}
type="text" type="text"
maxLength={1} maxLength={1}
className={`form-control text-center ${ className={`form-control text-center ${errors[`otp${num}`] ? "is-invalid" : ""
errors[`otp${num}`] ? "is-invalid" : "" }`}
}`}
ref={(el) => { ref={(el) => {
inputRefs.current[idx] = el; inputRefs.current[idx] = el;
ref(el); ref(el);
@ -121,6 +149,9 @@ useEffect(() => {
onChange(e); onChange(e);
if (/^\d$/.test(val) && idx < 3) { if (/^\d$/.test(val) && idx < 3) {
inputRefs.current[idx + 1]?.focus(); inputRefs.current[idx + 1]?.focus();
} else if (val === "" && idx > 0) {
inputRefs.current[idx - 1]?.focus();
} }
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
@ -132,6 +163,8 @@ useEffect(() => {
inputRefs.current[idx - 1]?.focus(); inputRefs.current[idx - 1]?.focus();
} }
}} }}
onPaste={idx === 0 ? handlePaste : undefined}
style={{ width: "40px", height: "40px", fontSize: "15px" }} style={{ width: "40px", height: "40px", fontSize: "15px" }}
{...rest} {...rest}
/> />
@ -163,17 +196,17 @@ useEffect(() => {
> >
This OTP will expire in <strong>{formatTime(timeLeft)}</strong> This OTP will expire in <strong>{formatTime(timeLeft)}</strong>
</p> </p>
) : ( ) : (
<div> <div>
<p <p
className="text-center text-danger mt-2 text small-text m-0" className="text-center text-danger mt-2 text small-text m-0"
> >
OTP has expired. Please request a new one. OTP has expired. Please request a new one.
</p> </p>
<a className="text-primary cursor-pointer" onClick={()=>navigate('/auth/login')}>Try Again</a> <a className="text-primary cursor-pointer" onClick={() => navigate('/auth/login')}>Try Again</a>
</div> </div>
)} )}
</form> </form>
</div> </div>

View File

@ -85,7 +85,7 @@ const AttendancesEmployeeRecords = ({ employee }) => {
const currentDate = new Date().toLocaleDateString("en-CA"); const currentDate = new Date().toLocaleDateString("en-CA");
const { currentPage, totalPages, currentItems, paginate } = usePagination( const { currentPage, totalPages, currentItems, paginate } = usePagination(
sortedFinalList, sortedFinalList,
10 20
); );
useEffect(() => { useEffect(() => {
@ -141,13 +141,12 @@ const AttendancesEmployeeRecords = ({ employee }) => {
id="DataTables_Table_0_length" id="DataTables_Table_0_length"
> >
<div className="col-md-3 my-0 "> <div className="col-md-3 my-0 ">
<DateRangePicker onRangeChange={setDateRange} endDateMode="yesterday"/> <DateRangePicker onRangeChange={setDateRange} endDateMode="yesterday" />
</div> </div>
<div className="col-md-2 m-0 text-end"> <div className="col-md-2 m-0 text-end">
<i <i
className={`bx bx-refresh cursor-pointer fs-4 ${ className={`bx bx-refresh cursor-pointer fs-4 ${loading ? "spin" : ""
loading ? "spin" : "" }`}
}`}
data-toggle="tooltip" data-toggle="tooltip"
title="Refresh" title="Refresh"
onClick={() => setIsRefreshing(!isRefreshing)} onClick={() => setIsRefreshing(!isRefreshing)}
@ -224,7 +223,7 @@ const AttendancesEmployeeRecords = ({ employee }) => {
</table> </table>
)} )}
</div> </div>
{!loading && data.length > 5 && ( {!loading && sortedFinalList.length > 20 && (
<nav aria-label="Page "> <nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1"> <ul className="pagination pagination-sm justify-content-end py-1">
<li <li
@ -240,9 +239,8 @@ const AttendancesEmployeeRecords = ({ employee }) => {
{[...Array(totalPages)].map((_, index) => ( {[...Array(totalPages)].map((_, index) => (
<li <li
key={index} key={index}
className={`page-item ${ className={`page-item ${currentPage === index + 1 ? "active" : ""
currentPage === index + 1 ? "active" : "" }`}
}`}
> >
<button <button
className="page-link " className="page-link "
@ -253,9 +251,8 @@ const AttendancesEmployeeRecords = ({ employee }) => {
</li> </li>
))} ))}
<li <li
className={`page-item ${ className={`page-item ${currentPage === totalPages ? "disabled" : ""
currentPage === totalPages ? "disabled" : "" }`}
}`}
> >
<button <button
className="page-link " className="page-link "

View File

@ -263,153 +263,155 @@ const ProjectList = () => {
<p className="text-center text-muted">No projects found.</p> <p className="text-center text-muted">No projects found.</p>
)} )}
{listView ? ( {listView ? (
<div className="card cursor-pointer"> <div className="card cursor-pointer">
<div className="card-body p-2"> <div className="card-body p-2" style={{ minHeight: "200px" }}>
<div className="table-responsive text-nowrap py-2 " style={{minHeight:"400px"}}> <div className="table-responsive text-nowrap py-2">
<table className="table m-3"> <table className="table m-3">
<thead> <thead>
<tr> <tr>
<th className="text-start" colSpan={5} > <th className="text-start" colSpan={5}>
Project Name Project Name
</th> </th>
<th className="mx-2 text-start">Contact Person</th> <th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th> <th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th> <th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th> <th className="mx-2">Task</th>
<th className="mx-2">Progress</th> <th className="mx-2">Progress</th>
<th className="mx-2"> <th className="mx-2">
<div className="dropdown"> <div className="dropdown">
<a <a
className="dropdown-toggle hide-arrow cursor-pointer" className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
>
Status <i className="bx bx-filter bx-sm"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{
id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
label: "Active",
},
{
id: "cdad86aa-8a56-4ff4-b633-9c629057dfef",
label:"In Progress"
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">
{label}
</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{currentItems.length === 0 ? (
<tr className="text-center">
<td colSpan="12" rowSpan='12'style={{height:"200px"}} >
No projects found
</td>
</tr>
) : (
currentItems.map((project) => (
<ProjectListView
key={project.id}
projectData={project}
recall={sortingProject}
/>
))
)}
</tbody>
</table>
</div>{" "}
</div>{" "}
</div>
) : (
<div className="row">
{currentItems.map((project) => (
<ProjectCard
key={project.id}
projectData={project}
recall={sortingProject}
/>
))}
</div>
)}
{!loading && totalPages > 1 && (
<nav>
<ul className="pagination pagination-sm justify-content-end py-2">
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
<button
className="page-link"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, i) => (
<li
key={i}
className={`page-item ${currentPage === i + 1 && "active"}`}
>
<button
className="page-link"
onClick={() => setCurrentPage(i + 1)}
> >
{i + 1} Status <i className="bx bx-filter bx-sm"></i>
</button> </a>
</li> <ul className="dropdown-menu p-2 text-capitalize">
))} {[
<li {
className={`page-item ${ id: "b74da4c2-d07e-46f2-9919-e75e49b12731",
currentPage === totalPages && "disabled" label: "Active",
},
{
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba",
label: "On Hold",
},
{
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d",
label: "Inactive",
},
{
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81",
label: "Completed",
},
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`} }`}
> >
<button Action
className="page-link" </th>
onClick={() => </tr>
setCurrentPage((p) => Math.min(totalPages, p + 1)) </thead>
} <tbody className="table-border-bottom-0" style={{ height: "200px" }}>
{currentItems.length === 0 ? (
<tr>
<td
colSpan="12"
className="text-center"
style={{
verticalAlign: "middle",
height: "200px",
paddingTop: 0,
paddingBottom: 0,
}}
> >
&raquo; <div className="d-flex flex-column justify-content-center align-items-center h-100">
</button> <p className="mb-0">No projects found</p>
</li> </div>
</ul> </td>
</nav> </tr>
)} ) : (
currentItems.map((project) => (
<ProjectListView
key={project.id}
projectData={project}
recall={sortingProject}
/>
))
)}
</tbody>
</table>
</div>
</div>
</div>
) : (
<div className="row">
{currentItems.map((project) => (
<ProjectCard
key={project.id}
projectData={project}
recall={sortingProject}
/>
))}
</div>
)}
{!loading && totalPages > 1 && (
<nav>
<ul className="pagination pagination-sm justify-content-end py-2">
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
<button
className="page-link"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, i) => (
<li
key={i}
className={`page-item ${currentPage === i + 1 && "active"}`}
>
<button
className="page-link"
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</button>
</li>
))}
<li className={`page-item ${currentPage === totalPages && "disabled"}`}>
<button
className="page-link"
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</div> </div>
</> </>
); );

View File

@ -1,9 +1,8 @@
import { api } from "../../utils/axiosClient"; import { api } from "../utils/axiosClient";
export const ImageGalleryAPI = { export const ImageGalleryAPI = {
ImagesGet: (projectId, filter, pageNumber, pageSize) => { ImagesGet: (projectId, filter, pageNumber, pageSize) => {
const payloadJsonString = JSON.stringify(filter); const payloadJsonString = JSON.stringify(filter);
// Corrected API endpoint with pagination parameters
return api.get(`/api/image/images/${projectId}?filter=${payloadJsonString}&pageNumber=${pageNumber}&pageSize=${pageSize}`); return api.get(`/api/image/images/${projectId}?filter=${payloadJsonString}&pageNumber=${pageNumber}&pageSize=${pageSize}`);
}, },
}; };