Issues_July_2W #252

Merged
vikas.nale merged 116 commits from Issues_July_2W into main 2025-07-14 10:40:20 +00:00
15 changed files with 544 additions and 541 deletions

View File

@ -836,7 +836,7 @@ progress {
}
.row {
--bs-gutter-x: 1.625rem;
--bs-gutter-x: 3.625rem;
--bs-gutter-y: 0;
display: flex;
flex-wrap: wrap;
@ -2553,7 +2553,7 @@ progress {
}
.table-responsive {
overflow-x: auto;
/* overflow-x: auto; */
-webkit-overflow-scrolling: touch;
}
@ -8966,10 +8966,8 @@ a:not([href]):hover {
}
/* Autofill input bg and text color issue on different OS and browsers */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
@ -8978,6 +8976,15 @@ select:-webkit-autofill:focus,
input:-internal-autofill-selected {
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 {

View File

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

View File

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

View File

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

View File

@ -26,7 +26,8 @@ const DateRangePicker = ({
altFormat: "d-m-Y",
defaultDate: [startDate, endDate],
static: true,
clickOpens: true,
clickOpens: true,
maxDate: endDate, // Disable future dates
onChange: (selectedDates, dateStr) => {
const [startDateString, endDateString] = dateStr.split(" to ");
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 [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 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]);
const paginate = (pageNumber) => {

View File

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

View File

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

View File

@ -1,103 +1,149 @@
/* Image Modal Overlay */
.image-modal-overlay {
position: fixed;
top: 0;
left: 0;
z-index: 9999; /* High z-index to ensure it's on top */
z-index: 9999;
width: 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;
justify-content: center;
align-items: center;
}
/* Main Modal Content Box */
.image-modal-content {
background: #fff;
padding: 24px;
max-width: 90%; /* Responsive max-width */
max-height: 100%; /* Responsive max-height */
max-width: 50%;
max-height: 95vh; /* Limits the modal's height to 95% of viewport height */
border-radius: 12px;
position: relative;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
text-align: center;
display: flex; /* Use flexbox for internal layout */
display: flex;
flex-direction: column;
align-items: 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 {
max-width: 100%;
max-height: 70vh; /* Limits image height to 70% of viewport height */
max-height: 70vh;
width: auto;
border-radius: 10px;
object-fit: contain; /* Ensures the whole image is visible without cropping */
object-fit: contain;
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;
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;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
width: 100%; /* Ensure details section takes full width */
width: 100%;
text-align: left;
}
.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 {
position: absolute;
top: 1px; /* Position relative to the modal content */
top: 1px;
right: 8px;
font-size: 30px;
background: none;
border: none;
color: black; /* White color for visibility on dark overlay */
color: black;
cursor: pointer;
padding: 0;
line-height: 1;
z-index: 10000; /* Ensure it's above everything else */
z-index: 10000;
}
/* Styles for the navigation buttons */
/* Navigation Buttons */
.nav-button {
position: absolute;
top: 50%; /* Vertically center them */
transform: translateY(-50%); /* Adjust for perfect vertical centering */
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
padding: 10px 15px;
font-size: 30px;
cursor: pointer;
z-index: 1000; /* Ensure buttons are above the image */
border-radius: 50%; /* Make them circular */
z-index: 1000;
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease; /* Smooth hover effect */
transition: background-color 0.3s ease;
}
.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 {
left: 0px; /* Position left arrow */
left: 0px;
}
.nav-button.next-button {
right: 0px; /* Position right arrow */
right: 0px;
}
/* Style for disabled buttons (optional) */
/* Disabled Button Style */
.nav-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}

View File

@ -69,22 +69,17 @@ const ChangePasswordPage = ({ onClose }) => {
className="modal d-flex align-items-center justify-content-center show"
tabIndex="-1"
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-header">
{" "}
<div className="modal-content p-10 rounded shadow bg-white position-relative">
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
style={{ top: "40px", right: "15px" }}
aria-label="Close"
className="btn-close"
data-bs-dismiss="modal" aria-label="Close"
onClick={onClose} // Use onClick to trigger onClose function
style={{ position: "absolute", top: "15px", right: "15px" }} // Example positioning
></button>
</div>
<div className="modal-content p-10 rounded shadow bg-white position-relative">
{/* Close Button */}
<h5 className="mb-2">Change Password</h5>
<p className="mb-4" style={{ fontSize: "14px" }}>
@ -119,7 +114,6 @@ const ChangePasswordPage = ({ onClose }) => {
)}
</div>
{/* <div className="row"> */}
<div className="mb-3 text-start">
<label className="form-label">New Password</label>
<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 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
case latter, an uppercase letter, a number, and a special
case letter, an uppercase letter, a number, and a special
character.
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-center pt-2">
<div className="d-flex justify-content-center pt-4">
<button
type="submit"
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 navigate = useNavigate();
const [ loading, setLoading ] = useState( false );
const [ timeLeft, setTimeLeft ] = useState( 0 );
const [loading, setLoading] = useState(false);
const [timeLeft, setTimeLeft] = useState(0);
const inputRefs = useRef([]);
@ -29,68 +29,97 @@ const LoginWithOtp = () => {
handleSubmit,
formState: { errors, isSubmitted },
getValues,
setValue,
trigger,
} = useForm({
resolver: zodResolver(otpSchema),
});
const onSubmit = async (data) => {
const finalOtp = data.otp1 + data.otp2 + data.otp3 + data.otp4;
const username = localStorage.getItem( "otpUsername" );
const username = localStorage.getItem("otpUsername");
setLoading(true);
try {
let requestedData = {
email: username,
otp:finalOtp
}
const response = await AuthRepository.verifyOTP( requestedData )
localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading( false );
localStorage.removeItem( "otpUsername" );
localStorage.removeItem( "otpSentTime" );
navigate( "/dashboard" );
let requestedData = {
email: username,
otp: finalOtp
}
const response = await AuthRepository.verifyOTP(requestedData)
localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading(false);
localStorage.removeItem("otpUsername");
localStorage.removeItem("otpSentTime");
navigate("/dashboard");
} catch (err) {
showToast( "Invalid or expired OTP.", "error" );
setLoading(false);
showToast("Invalid or expired OTP.", "error");
setLoading(false);
}
};
const formatTime = (seconds) => {
const min = Math.floor(seconds / 60).toString().padStart(2, "0");
const sec = (seconds % 60).toString().padStart(2, "0");
return `${min}:${sec}`;
};
const formatTime = (seconds) => {
const min = Math.floor(seconds / 60).toString().padStart(2, "0");
const sec = (seconds % 60).toString().padStart(2, "0");
return `${min}:${sec}`;
};
useEffect(() => {
const otpSentTime = localStorage.getItem("otpSentTime");
const now = Date.now();
// Time Logic for OTP expiry
useEffect(() => {
const otpSentTime = localStorage.getItem("otpSentTime");
const now = Date.now();
if (otpSentTime) {
const elapsed = Math.floor((now - Number(otpSentTime)) / 1000); // in seconds
const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0); // prevent negatives
setTimeLeft(remaining);
}
}, []);
useEffect(() => {
if (timeLeft <= 0) return;
if (otpSentTime) {
const elapsed = Math.floor((now - Number(otpSentTime)) / 1000); //in seconds
const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0); //prevent negatives
setTimeLeft(remaining);
}
}, []);
const timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(timer);
localStorage.removeItem( "otpSentTime" );
localStorage.removeItem("otpUsername");
return 0;
useEffect(() => {
if (timeLeft <= 0) return;
const timer = setInterval(() => {
setTimeLeft((prev) => {
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;
});
}, 1000);
trigger(["otp1", "otp2", "otp3", "otp4"]);
} else {
showToast("Invalid OTP format pasted. Please enter 4 digits")
return () => clearInterval(timer);
}, [timeLeft]);
for (let i = 0; i < 4; i++) {
setValue(`otp${i + 1}`, "")
}
}
}
return (
@ -109,9 +138,8 @@ useEffect(() => {
key={num}
type="text"
maxLength={1}
className={`form-control text-center ${
errors[`otp${num}`] ? "is-invalid" : ""
}`}
className={`form-control text-center ${errors[`otp${num}`] ? "is-invalid" : ""
}`}
ref={(el) => {
inputRefs.current[idx] = el;
ref(el);
@ -121,6 +149,9 @@ useEffect(() => {
onChange(e);
if (/^\d$/.test(val) && idx < 3) {
inputRefs.current[idx + 1]?.focus();
} else if (val === "" && idx > 0) {
inputRefs.current[idx - 1]?.focus();
}
}}
onKeyDown={(e) => {
@ -132,6 +163,8 @@ useEffect(() => {
inputRefs.current[idx - 1]?.focus();
}
}}
onPaste={idx === 0 ? handlePaste : undefined}
style={{ width: "40px", height: "40px", fontSize: "15px" }}
{...rest}
/>
@ -163,17 +196,17 @@ useEffect(() => {
>
This OTP will expire in <strong>{formatTime(timeLeft)}</strong>
</p>
) : (
<div>
<p
className="text-center text-danger mt-2 text small-text m-0"
>
OTP has expired. Please request a new one.
</p>
<a className="text-primary cursor-pointer" onClick={()=>navigate('/auth/login')}>Try Again</a>
</div>
) : (
<div>
<p
className="text-center text-danger mt-2 text small-text m-0"
>
OTP has expired. Please request a new one.
</p>
<a className="text-primary cursor-pointer" onClick={() => navigate('/auth/login')}>Try Again</a>
</div>
)}
</form>
</div>

View File

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

View File

@ -263,153 +263,155 @@ const ProjectList = () => {
<p className="text-center text-muted">No projects found.</p>
)}
{listView ? (
<div className="card cursor-pointer">
<div className="card-body p-2">
<div className="table-responsive text-nowrap py-2 " style={{minHeight:"400px"}}>
<table className="table m-3">
<thead>
<tr>
<th className="text-start" colSpan={5} >
Project Name
</th>
<th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
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)}
{listView ? (
<div className="card cursor-pointer">
<div className="card-body p-2" style={{ minHeight: "200px" }}>
<div className="table-responsive text-nowrap py-2">
<table className="table m-3">
<thead>
<tr>
<th className="text-start" colSpan={5}>
Project Name
</th>
<th className="mx-2 text-start">Contact Person</th>
<th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{i + 1}
</button>
</li>
))}
<li
className={`page-item ${
currentPage === totalPages && "disabled"
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: "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
className="page-link"
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
Action
</th>
</tr>
</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;
</button>
</li>
</ul>
</nav>
)}
<div className="d-flex flex-column justify-content-center align-items-center h-100">
<p className="mb-0">No projects found</p>
</div>
</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}
</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>
</>
);

View File

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