Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Payment_Getway_Management

This commit is contained in:
pramod.mahajan 2025-11-07 14:56:27 +05:30
commit 3259a1ba81
77 changed files with 3356 additions and 1488 deletions

View File

@ -5,7 +5,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Marco PMS</title> <title>OnFieldWork.com</title>
<meta name="description" content="" /> <meta name="description" content="" />

View File

@ -442,4 +442,4 @@ font-weight: normal;
.fs-md-large { font-size: 150% !important; } .fs-md-large { font-size: 150% !important; }
.fs-md-xlarge { font-size: 170% !important; } .fs-md-xlarge { font-size: 170% !important; }
.fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; } .fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,5 @@
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" d="M46.5001 10.5288H32.5001L20.2251 26.5288L32.5001 56.5288L60.5001 26.5288L46.5001 10.5288Z" fill="#03C3EC"/>
<path d="M18.5 10.5288H46.5L60.5 26.5288L32.5 56.5288L4.5 26.5288L18.5 10.5288Z" stroke="#03C3EC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2934 9.92012C33.1042 9.67343 32.8109 9.52881 32.5 9.52881C32.1891 9.52881 31.8958 9.67343 31.7066 9.92012L19.7318 25.5288H4.5C3.94772 25.5288 3.5 25.9765 3.5 26.5288C3.5 27.0811 3.94772 27.5288 4.5 27.5288H19.5537L31.5745 56.9075C31.7282 57.2833 32.094 57.5288 32.5 57.5288C32.906 57.5288 33.2718 57.2833 33.4255 56.9075L45.4463 27.5288H60.5C61.0523 27.5288 61.5 27.0811 61.5 26.5288C61.5 25.9765 61.0523 25.5288 60.5 25.5288H45.2682L33.2934 9.92012ZM42.7474 25.5288L32.5 12.1717L22.2526 25.5288H42.7474ZM21.7146 27.5288L32.5 53.8881L43.2854 27.5288H21.7146Z" fill="#03C3EC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -126,7 +126,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
checked={ShowPending} checked={ShowPending}
onChange={(e) => setShowPending(e.target.checked)} onChange={(e) => setShowPending(e.target.checked)}
/> />
<label className="form-check-label ms-0">Show Pending</label> <label className="form-check-label ms-0">Pending Attendance</label>
</div> </div>
</div> </div>
{attLoading ? ( {attLoading ? (
@ -223,50 +223,6 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
)} )}
</tbody> </tbody>
</table> </table>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<li
className={`page-item ${currentPage === 1 ? "disabled" : ""
}`}
>
<button
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
<li
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</> </>
) : ( ) : (
<div <div
@ -281,6 +237,48 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
</div> </div>
)} )}
</div> </div>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<li
className={`page-item ${currentPage === 1 ? "disabled" : ""
}`}
>
<button
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
<li
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</> </>
); );
}; };

View File

@ -15,6 +15,7 @@ import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance"; import { useAttendancesLogs } from "../../hooks/useAttendance";
import { queryClient } from "../../layouts/AuthLayout"; import { queryClient } from "../../layouts/AuthLayout";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import { useNavigate } from "react-router-dom";
const usePagination = (data, itemsPerPage) => { const usePagination = (data, itemsPerPage) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -38,162 +39,172 @@ const usePagination = (data, itemsPerPage) => {
}; };
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch(); const dispatch = useDispatch();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false); const [showPending, setShowPending] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const navigate = useNavigate();
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
const yesterday = new Date(); const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
const isSameDay = (dateStr) => { const isSameDay = (dateStr) => {
if (!dateStr) return false; if (!dateStr) return false;
const d = new Date(dateStr); const d = new Date(dateStr);
d.setHours(0, 0, 0, 0); d.setHours(0, 0, 0, 0);
return d.getTime() === today.getTime(); return d.getTime() === today.getTime();
}; };
const isBeforeToday = (dateStr) => { const isBeforeToday = (dateStr) => {
if (!dateStr) return false; if (!dateStr) return false;
const d = new Date(dateStr); const d = new Date(dateStr);
d.setHours(0, 0, 0, 0); d.setHours(0, 0, 0, 0);
return d.getTime() < today.getTime(); return d.getTime() < today.getTime();
}; };
const sortByName = (a, b) => { const sortByName = (a, b) => {
const nameA = (a.firstName + a.lastName).toLowerCase(); const nameA = (a.firstName + a.lastName).toLowerCase();
const nameB = (b.firstName + b.lastName).toLowerCase(); const nameB = (b.firstName + b.lastName).toLowerCase();
return nameA.localeCompare(nameB); return nameA.localeCompare(nameB);
}; };
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs( const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
selectedProject, selectedProject,
dateRange.startDate, dateRange.startDate,
dateRange.endDate, dateRange.endDate,
organizationId organizationId
);
const processedData = useMemo(() => {
const filteredData = showPending
? data.filter((item) => item.checkOutTime === null)
: data;
const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName);
const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName);
const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName);
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime));
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName);
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName);
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6];
const groupedByDate = sortedList.reduce((acc, item) => {
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
if (date) {
acc[date] = acc[date] || [];
acc[date].push(item);
}
return acc;
}, {});
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
return sortedDates.flatMap((date) => groupedByDate[date]);
}, [data, showPending]);
const filteredSearchData = useMemo(() => {
if (!searchTerm) return processedData;
const lowercased = searchTerm.toLowerCase();
return processedData.filter((item) =>
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
); );
}, [processedData, searchTerm]);
const { const processedData = useMemo(() => {
currentPage, const filteredData = showPending
totalPages, ? data.filter((item) => item.checkOutTime === null)
currentItems: paginatedAttendances, : data;
paginate,
resetPage,
} = usePagination(filteredSearchData, 20);
useEffect(() => { const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName);
resetPage(); const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName);
}, [filteredSearchData]); const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName);
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime));
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName);
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName);
const handler = useCallback( const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6];
(msg) => {
const { startDate, endDate } = dateRange;
const checkIn = msg.response.checkInTime.substring(0, 10);
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) { const groupedByDate = sortedList.reduce((acc, item) => {
queryClient.setQueriesData(["attendanceLogs"], (oldData) => { const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
if (!oldData) { if (date) {
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] }); acc[date] = acc[date] || [];
return; acc[date].push(item);
} }
return oldData.map((record) => return acc;
record.id === msg.response.id ? { ...record, ...msg.response } : record }, {});
);
});
resetPage();
}
},
[selectedProject, dateRange, resetPage]
);
useEffect(() => { const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
eventBus.on("attendance_log", handler); return sortedDates.flatMap((date) => groupedByDate[date]);
return () => eventBus.off("attendance_log", handler); }, [data, showPending]);
}, [handler]);
const employeeHandler = useCallback( const filteredSearchData = useMemo(() => {
(msg) => { if (!searchTerm) return processedData;
const { startDate, endDate } = dateRange;
if (data.some((item) => item.employeeId == msg.employeeId)) {
refetch();
}
},
[data, refetch]
);
useEffect(() => { const lowercased = searchTerm.toLowerCase();
eventBus.on("employee", employeeHandler); return processedData.filter((item) =>
return () => eventBus.off("employee", employeeHandler); `${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
}, [employeeHandler]); );
}, [processedData, searchTerm]);
const {
currentPage,
totalPages,
currentItems: paginatedAttendances,
paginate,
resetPage,
} = usePagination(filteredSearchData, 20);
useEffect(() => {
resetPage();
}, [filteredSearchData]);
const handler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
const checkIn = msg.response.checkInTime.substring(0, 10);
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) {
queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
if (!oldData) {
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
return;
}
return oldData.map((record) =>
record.id === msg.response.id ? { ...record, ...msg.response } : record
);
});
resetPage();
}
},
[selectedProject, dateRange, resetPage]
);
useEffect(() => {
eventBus.on("attendance_log", handler);
return () => eventBus.off("attendance_log", handler);
}, [handler]);
const employeeHandler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
if (data.some((item) => item.employeeId == msg.employeeId)) {
refetch();
}
},
[data, refetch]
);
useEffect(() => {
eventBus.on("employee", employeeHandler);
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
return ( return (
<> <>
<div <div
className="dataTables_length text-start py-2 d-flex justify-content-between " className="dataTables_length text-start py-2 d-flex flex-wrap justify-content-between"
id="DataTables_Table_0_length" id="DataTables_Table_0_length"
> >
<div className="d-flex align-items-center my-0 "> <div className="d-flex flex-wrap align-items-center gap-2 gap-md-3 my-0">
<DateRangePicker {/* Date Range Picker */}
onRangeChange={setDateRange} <div className="flex-grow-1 flex-md-grow-0">
defaultStartDate={yesterday} <DateRangePicker
/> onRangeChange={setDateRange}
<div className="form-check form-switch text-start ms-1 ms-md-2 align-items-center mb-0"> defaultStartDate={yesterday}
/>
</div>
{/* Pending Attendance Switch */}
<div className="form-check form-switch text-start mb-0">
<input <input
type="checkbox" type="checkbox"
className="form-check-input" className="form-check-input"
role="switch" role="switch"
disabled={isFetching}
id="inactiveEmployeesCheckbox" id="inactiveEmployeesCheckbox"
disabled={isFetching}
checked={showPending} checked={showPending}
onChange={(e) => setShowPending(e.target.checked)} onChange={(e) => setShowPending(e.target.checked)}
/> />
<label className="form-check-label ms-0">Show Pending</label> <label className="form-check-label ms-0 ms-md-0">
Pending Attendance
</label>
</div> </div>
</div> </div>
</div> </div>
<div <div
className="table-responsive text-nowrap" className="table-responsive text-nowrap"
style={{ minHeight: "200px" }} style={{ minHeight: "200px" }}
@ -232,9 +243,9 @@ useEffect(() => {
const previousAttendance = arr[index - 1]; const previousAttendance = arr[index - 1];
const previousDate = previousAttendance const previousDate = previousAttendance
? moment( ? moment(
previousAttendance.checkInTime || previousAttendance.checkInTime ||
previousAttendance.checkOutTime previousAttendance.checkOutTime
).format("YYYY-MM-DD") ).format("YYYY-MM-DD")
: null; : null;
if (!previousDate || currentDate !== previousDate) { if (!previousDate || currentDate !== previousDate) {
@ -260,7 +271,12 @@ useEffect(() => {
lastName={attendance.lastName} lastName={attendance.lastName}
/> />
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate"> <a
onClick={() =>
navigate(`/employee/${attendance.employeeId}?for=attendance`)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal"> <span className="fw-normal">
{attendance.firstName} {attendance.lastName} {attendance.firstName} {attendance.lastName}
</span> </span>
@ -297,8 +313,7 @@ useEffect(() => {
) : ( ) : (
<div className="my-12"> <div className="my-12">
<span className="text-secondary"> <span className="text-secondary">
No data available for the selected date range. Please Select No attendance record found in selected date range.
another date.
</span> </span>
</div> </div>
)} )}
@ -326,9 +341,8 @@ useEffect(() => {
(pageNumber) => ( (pageNumber) => (
<li <li
key={pageNumber} key={pageNumber}
className={`page-item ${ className={`page-item ${currentPage === pageNumber ? "active" : ""
currentPage === pageNumber ? "active" : "" }`}
}`}
> >
<button <button
className="page-link" className="page-link"
@ -340,9 +354,8 @@ useEffect(() => {
) )
)} )}
<li <li
className={`page-item ${ className={`page-item ${currentPage === totalPages ? "disabled" : ""
currentPage === totalPages ? "disabled" : "" }`}
}`}
> >
<button <button
className="page-link" className="page-link"

View File

@ -33,7 +33,7 @@ const InfraPlanning = () => {
const selectedService = useCurrentService(); const selectedService = useCurrentService();
const { projectInfra, isLoading, isError, error, isFetched } = const { projectInfra, isLoading, isError, error, isFetched } =
useProjectInfra(selectedProject, selectedService || "" ); useProjectInfra(selectedProject, selectedService || "");
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA); const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const canApproveTask = useHasUserPermission(APPROVE_TASK); const canApproveTask = useHasUserPermission(APPROVE_TASK);
@ -62,9 +62,13 @@ const InfraPlanning = () => {
if (isFetched && (!projectInfra || projectInfra.length === 0)) { if (isFetched && (!projectInfra || projectInfra.length === 0)) {
return ( return (
<div className="text-center"> <div
<p className="my-3">No Result Found</p> className="text-center d-flex justify-content-center align-items-center text-muted"
style={{ minHeight: "40vh", fontSize: "0.9rem" }}
>
<p className="my-3 m-0">No Result Found</p>
</div> </div>
); );
} }

View File

@ -14,6 +14,7 @@ import {
} from "../../slices/apiDataManager"; } from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import { useNavigate } from "react-router-dom";
const Regularization = ({ const Regularization = ({
handleRequest, handleRequest,
@ -26,6 +27,7 @@ const Regularization = ({
// var selectedProject = useSelector((store) => store.localVariables.projectId); // var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]); const [regularizesList, setregularizedList] = useState([]);
const navigate = useNavigate();
const { regularizes, loading, error, refetch } = useRegularizationRequests( const { regularizes, loading, error, refetch } = useRegularizationRequests(
selectedProject, selectedProject,
organizationId, organizationId,
@ -33,9 +35,9 @@ const Regularization = ({
); );
useEffect(() => { useEffect(() => {
if(!regularizes) return if (!regularizes) return
if(regularizes?.length) { if (regularizes?.length) {
setregularizedList(regularizes); setregularizedList(regularizes);
} }
}, [regularizes]); }, [regularizes]);
@ -102,141 +104,106 @@ const Regularization = ({
}, [employeeHandler]); }, [employeeHandler]);
return ( return (
<div <div>
className="table-responsive text-nowrap pb-4" <div
style={{ minHeight: "200px" }} className="table-responsive pt-3 text-nowrap pb-4"
> style={{ minHeight: "200px" }}
{loading ? ( >
<div {loading ? (
className="d-flex justify-content-center align-items-center" <div
style={{ height: "200px" }} className="d-flex justify-content-center align-items-center"
> style={{ height: "200px" }}
<p className="text-secondary">Loading...</p> >
</div> <p className="text-secondary">Loading...</p>
) : currentItems?.length > 0 ? ( </div>
<table className="table mb-0"> ) : currentItems?.length > 0 ? (
<thead> <table className="table mb-0">
<tr> <thead>
<th colSpan={2}>Name</th> <tr>
<th>Date</th> <th colSpan={2}>Name</th>
<th>Organization</th> <th>Date</th>
<th> <th>Organization</th>
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In <th>
</th> <i className="bx bxs-down-arrow-alt text-success"></i>Check-In
<th> </th>
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out <th>
</th> <i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
<th colSpan={2}> </th>
Requested By <th colSpan={2}>
</th> Requested By
<th > </th>
Requested At <th >
</th> Requested At
<th>Action</th> </th>
</tr> <th>Action</th>
</thead> </tr>
<tbody> </thead>
{currentItems?.map((att, index) => ( <tbody>
<tr key={index}> {currentItems?.map((att, index) => (
<td colSpan={2}> <tr key={index}>
<div className="d-flex justify-content-start align-items-center"> <td colSpan={2}>
<Avatar firstName={att.firstName} lastName={att.lastName} /> <div className="d-flex justify-content-start align-items-center">
<div className="d-flex flex-column"> <Avatar firstName={att.firstName} lastName={att.lastName} />
<a href="#" className="text-heading text-truncate"> <div className="d-flex flex-column"> <a
onClick={() =>
navigate(`/employee/${att.employeeId}?for=attendance`)
}
className="text-heading text-truncate cursor-pointer" >
<span className="fw-normal"> <span className="fw-normal">
{att.firstName} {att.lastName} {att.firstName} {att.lastName}
</span> </span>
</a> </a>
</div>
</div> </div>
</div> </td>
</td> <td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
<td>{att.organizationName || "--"}</td> <td>{att.organizationName || "--"}</td>
<td>{convertShortTime(att.checkInTime)}</td> <td>{convertShortTime(att.checkInTime)}</td>
<td>
{att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
</td>
<td colSpan={2}>
{att.requestedBy ? ( <div className="d-flex justify-content-start align-items-center">
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
<div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate">
<span className="fw-normal">
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName}
</span>
</a>
</div>
</div>):(<small>--</small>)}
</td>
<td> <td>
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt,true) : "--"} {att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
</td> </td>
<td className="text-center ">
<RegularizationActions
attendanceData={att}
handleRequest={handleRequest}
refresh={refetch}
/>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "200px" }}
>
<span className="text-secondary">
{searchTerm
? "No results found for your search."
: "No Requests Found !"}
</span>
</div>
)}
{/* {!loading && totalPages > 1 && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
<button
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
<li
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)} */}
<td colSpan={2}>
{att.requestedBy ? (<div className="d-flex justify-content-start align-items-center">
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
<div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate">
<span className="fw-normal">
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName}
</span>
</a>
</div>
</div>) : (<small>--</small>)}
</td>
<td>
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt, true) : "--"}
</td>
<td className="text-center ">
<RegularizationActions
attendanceData={att}
handleRequest={handleRequest}
refresh={refetch}
/>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "200px" }}
>
<span className="text-secondary">
{searchTerm
? "No results found for your search."
: "No Requests Found !"}
</span>
</div>
)}
</div>
{totalPages > 0 && ( {totalPages > 0 && (
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
@ -244,6 +211,7 @@ const Regularization = ({
onPageChange={paginate} onPageChange={paginate}
/> />
)} )}
</div> </div>
); );
}; };

View File

@ -23,7 +23,7 @@ const HorizontalBarChart = ({
if (loading) { if (loading) {
return ( return (
<div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl"> <div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl">
<span className="text-gray-500 text-sm">Loading chart...</span> <span className="text-gray-500">Loading chart...</span>
{/* Replace this with a skeleton or spinner if you prefer */} {/* Replace this with a skeleton or spinner if you prefer */}
</div> </div>
); );

View File

@ -29,7 +29,9 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = methods; } = methods;
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const onSubmit = (formData) => { const onSubmit = (formData) => {
const filterPayload = { const filterPayload = {
...formData, ...formData,
@ -37,12 +39,14 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
dateTo: localToUtc(formData.dateTo), dateTo: localToUtc(formData.dateTo),
}; };
handleFilter(filterPayload); handleFilter(filterPayload);
closePanel();
}; };
const onClear = () => { const onClear = () => {
setResetKey((prev) => prev + 1); setResetKey((prev) => prev + 1);
handleFilter(TaskReportDefaultValue); handleFilter(TaskReportDefaultValue);
reset(TaskReportDefaultValue); reset(TaskReportDefaultValue);
closePanel();
}; };
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>

View File

@ -29,7 +29,7 @@ const TaskReportList = () => {
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK); const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK); const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const { service, openModal, closeModal,filter } = useDailyProgrssContext(); const { service, openModal, closeModal, filter } = useDailyProgrssContext();
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const { projectNames } = useProjectName(); const { projectNames } = useProjectName();
@ -37,7 +37,7 @@ const TaskReportList = () => {
selectedProject, selectedProject,
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
currentPage, currentPage,
service,filter service, filter
); );
const ProgrssReportColumn = [ const ProgrssReportColumn = [
@ -192,109 +192,114 @@ const TaskReportList = () => {
if (isLoading) return <TaskReportListSkeleton />; if (isLoading) return <TaskReportListSkeleton />;
if (isError) return <div>Loading....</div>; if (isError) return <div>Loading....</div>;
return ( return (
<div className="mt-2 table-responsive text-nowrap"> <div>
<table className="table"> <div className="mt-2 table-responsive text-nowrap">
<thead> <table className="table">
<tr> <thead>
<th className="text-start">Activity</th>
<th>
<span>
Total Pending{" "}
<HoverPopup
title="Total Pending Task"
content={<p>This shows the total pending tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>
<span>
Reported/Planned{" "}
<HoverPopup
title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>Assign Date</th>
<th>Team</th>
<th className="text-center">Actions</th>
</tr>
</thead>
<tbody>
{groupedTasks.length === 0 && (
<tr> <tr>
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}> <th className="text-start">Activity</th>
No reports available <th>
</td> <span>
Total Pending{" "}
<HoverPopup
title="Total Pending Task"
content={<p>This shows the total pending tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>
<span>
Reported/Planned{" "}
<HoverPopup
title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>Assign Date</th>
<th>Team</th>
<th className="text-center">Actions</th>
</tr> </tr>
)} </thead>
<tbody>
{groupedTasks.map(({ date, tasks }) => ( {groupedTasks.length === 0 && (
<React.Fragment key={date}> <tr>
<tr className="table-row-header text-start"> <td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
<td colSpan={6}> No reports available
<strong>{formatUTCToLocalTime(date)}</strong>
</td> </td>
</tr> </tr>
{tasks.map((task, idx) => ( )}
<tr key={task.id || idx}>
<td className="flex-wrap text-start"> {groupedTasks.map(({ date, tasks }) => (
<div> <React.Fragment key={date}>
{task.workItem.activityMaster?.activityName || "No Activity Name"} <tr className="table-row-header text-start">
</div> <td colSpan={6}>
<div className="text-sm py-2"> <strong>{formatUTCToLocalTime(date)}</strong>
{task.workItem.workArea?.floor?.building?.name} {" "}
{task.workItem.workArea?.floor?.floorName} {" "}
{task.workItem.workArea?.areaName}
</div>
</td>
<td>
{formatNumber(task.workItem.plannedWork)}
</td>
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td className="text-center">{renderTeamMembers(task, idx)}</td>
<td className="text-center">
<div className="d-flex justify-content-end gap-2">
{ReportTaskRights && !task.reportedDate && (
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
Report
</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button
className="btn btn-xs btn-warning"
onClick={() => openModal("comments", { task, isActionAllow: true })}
>
QC
</button>
)}
<button
className="btn btn-xs btn-primary"
onClick={() => openModal("comments", { task, isActionAllow: false })}
>
Comment
</button>
</div>
</td> </td>
</tr> </tr>
))} {tasks.map((task, idx) => (
</React.Fragment> <tr key={task.id || idx}>
))} <td className="flex-wrap text-start">
</tbody> <div>
</table> {task.workItem.activityMaster?.activityName || "No Activity Name"}
{data?.data?.length > 0 && ( </div>
<Pagination <div className="text-sm py-2">
currentPage={currentPage} {task.workItem.workArea?.floor?.building?.name} {" "}
totalPages={data.totalPages} {task.workItem.workArea?.floor?.floorName} {" "}
onPageChange={paginate} {task.workItem.workArea?.areaName}
/> </div>
)} </td>
</div> <td>
{formatNumber(task.workItem.plannedWork)}
</td>
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td className="text-center">{renderTeamMembers(task, idx)}</td>
<td className="text-center">
<div className="d-flex justify-content-end gap-2">
{ReportTaskRights && !task.reportedDate && (
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
Report
</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button
className="btn btn-xs btn-warning"
onClick={() => openModal("comments", { task, isActionAllow: true })}
>
QC
</button>
)}
<button
className="btn btn-xs btn-primary"
onClick={() => openModal("comments", { task, isActionAllow: false })}
>
Comment
</button>
</div>
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
{
data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)
}
</div >
); );
}; };

View File

@ -14,11 +14,11 @@ import ProjectCompletionChart from "./ProjectCompletionChart";
import ProjectProgressChart from "./ProjectProgressChart"; import ProjectProgressChart from "./ProjectProgressChart";
import ProjectOverview from "../Project/ProjectOverview"; import ProjectOverview from "../Project/ProjectOverview";
import AttendanceOverview from "./AttendanceChart"; import AttendanceOverview from "./AttendanceChart";
import ExpenseAnalysis from "./ExpenseAnalysis";
import ExpenseStatus from "./ExpenseStatus";
import ExpenseByProject from "./ExpenseByProject";
const Dashboard = () => { const Dashboard = () => {
const { projectsCardData } = useDashboardProjectsCardData();
const { teamsCardData } = useDashboardTeamsCardData();
const { tasksCardData } = useDashboardTasksCardData();
// Get the selected project ID from Redux store // Get the selected project ID from Redux store
const projectId = useSelector((store) => store.localVariables.projectId); const projectId = useSelector((store) => store.localVariables.projectId);
@ -29,16 +29,16 @@ const Dashboard = () => {
<div className="row gy-4"> <div className="row gy-4">
{isAllProjectsSelected && ( {isAllProjectsSelected && (
<div className="col-sm-6 col-lg-4"> <div className="col-sm-6 col-lg-4">
<Projects projectsCardData={projectsCardData} /> <Projects />
</div> </div>
)} )}
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}> <div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<Teams teamsCardData={teamsCardData} /> <Teams />
</div> </div>
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}> <div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<TasksCard tasksCardData={tasksCardData} /> <TasksCard/>
</div> </div>
{isAllProjectsSelected && ( {isAllProjectsSelected && (
@ -56,11 +56,25 @@ const Dashboard = () => {
<div className="col-xxl-6 col-lg-6"> <div className="col-xxl-6 col-lg-6">
<ProjectProgressChart /> <ProjectProgressChart />
</div> </div>
<div className="col-12 col-xl-8">
<div className="card h-100">
<ExpenseAnalysis />
</div>
</div>
<div className="col-12 col-xl-4 col-md-6">
<div className="card ">
<ExpenseStatus />
</div>
</div>
{!isAllProjectsSelected && ( {!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6"> <div className="col-12 col-md-6 mb-sm-0 mb-4 ">
<AttendanceOverview /> {/* ✅ Removed unnecessary projectId prop */} <AttendanceOverview />
</div> </div>
)} )}
<div className="col-12 col-md-6">
<ExpenseByProject />
</div>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,110 @@
import React from "react";
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton ${className}`}
style={{
height,
width,
borderRadius: "4px",
background: "linear-gradient(90deg, #eee, #f5f5f5, #eee)",
backgroundSize: "200% 100%",
animation: "skeleton-loading 1.5s infinite",
}}
></div>
);
const skeletonStyle = `
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`;
export const ProjectCardSkeleton = () => {
return (
<>
{/* Inject animation CSS once */}
<style>{skeletonStyle}</style>
<div className="card p-3 h-100 text-center d-flex justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2">
<i className="rounded-circle bx bx-building-house text-primary"></i>{" "}
Projects
</h5>
</div>
{/* Skeleton body */}
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="40px" className="mx-auto" />
</div>
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="40px" className="mx-auto" />
</div>
</div>
</div>
</>
);
};
export const TeamsSkeleton = () => {
return (
<>
<style>{skeletonStyle}</style>
<div className="card p-3 h-100 text-center d-flex justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2">
<i className="bx bx-group text-warning"></i> Teams
</h5>
</div>
{/* Skeleton Body */}
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="90px" className="mx-auto" />
</div>
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="70px" className="mx-auto" />
</div>
</div>
</div>
</>
);
};
export const TasksSkeleton = () => {
return (
<>
<style>{skeletonStyle}</style>
<div className="card p-3 h-100 text-center d-flex justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2">
<i className="bx bx-task text-success"></i> Tasks
</h5>
</div>
{/* Skeleton Body */}
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="70px" className="mx-auto" />
</div>
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="90px" className="mx-auto" />
</div>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,167 @@
import React, { useEffect, useMemo, useState } from "react";
import Chart from "react-apexcharts";
import { useExpenseAnalysis } from "../../hooks/useDashboard_Data";
import { useSelectedProject } from "../../slices/apiDataManager";
import { DateRangePicker1 } from "../common/DateRangePicker";
import { FormProvider, useForm } from "react-hook-form";
import { formatCurrency, localToUtc } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects";
const ExpenseAnalysis = () => {
const projectId = useSelectedProject();
const [projectName, setProjectName] = useState("All Project");
const { projectNames, loading } = useProjectName();
const methods = useForm({
defaultValues: { startDate: "", endDate: "" },
});
useEffect(() => {
if (projectId && projectNames?.length) {
const project = projectNames.find((p) => p.id === projectId);
setProjectName(project?.name || "All Project");
} else {
setProjectName("All Project");
}
}, [projectNames, projectId]);
const { watch } = methods;
const [startDate, endDate] = watch(["startDate", "endDate"]);
const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis(
projectId,
startDate ? localToUtc(startDate) : null,
endDate ? localToUtc(endDate) : null
);
if (isError) return <div>{error.message}</div>;
const report = data?.report ?? [];
const { labels, series, total } = useMemo(() => {
const labels = report.map((item) => item.projectName);
const series = report.map((item) => item.totalApprovedAmount || 0);
const total = formatCurrency(data?.totalAmount || 0);
return { labels, series, total };
}, [report, data?.totalAmount]);
const donutOptions = {
chart: { type: "donut" },
labels,
legend: { show: false },
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
plotOptions: {
pie: {
donut: {
size: "70%",
labels: {
show: true,
total: {
show: true,
label: "Total",
fontSize: "16px",
formatter: () => `${total}`,
},
},
},
},
},
responsive: [
{
breakpoint: 576, // mobile breakpoint
options: {
chart: { width: "100%" },
},
},
],
};
return (
<>
<div className="card-header d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2">
<div className="text-start w-100">
<h5 className="mb-1 card-title">Expense Breakdown</h5>
{/* <p className="card-subtitle mb-0">Category Wise Expense Breakdown</p> */}
<p className="card-subtitle m-0">{projectName}</p>
</div>
<div className="text-start text-sm-end w-75">
<FormProvider {...methods}>
<DateRangePicker1 />
</FormProvider>
</div>
</div>
{/* Card body */}
<div className="card-body position-relative">
{isLoading && (
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "200px" }}
>
<span>Loading...</span>
</div>
)}
{!isLoading && report.length === 0 && (
<div className="text-center py-5 text-muted">No data found</div>
)}
{!isLoading && report.length > 0 && (
<>
{isFetching && (
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
<span>Loading...</span>
</div>
)}
<div className="d-flex justify-content-center mb-3">
<Chart
options={donutOptions}
series={series}
type="donut"
width="100%"
height={320}
/>
</div>
<div className="mb-2 w-100">
<div className="row g-2">
{report.map((item, idx) => (
<div
className="col-12 col-sm-6 d-flex align-items-start"
key={idx}
>
<div className="avatar me-2">
<span
className="avatar-initial rounded-2"
style={{
backgroundColor:
donutOptions.colors[idx % donutOptions.colors.length],
}}
>
<i className="bx bx-receipt fs-4"></i>
</span>
</div>
<div className="d-flex flex-column gap-1 text-start">
<small className="fw-semibold">{item.projectName}</small>
<span className="fw-semibold text-muted ms-1">
{formatCurrency(item.totalApprovedAmount)}
</span>
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
{/* Header */}
</>
);
};
export default ExpenseAnalysis;

View File

@ -0,0 +1,178 @@
import React, { useState, useEffect } from "react";
import Chart from "react-apexcharts";
import { useExpenseType } from "../../hooks/masterHook/useMaster";
import { useSelector } from "react-redux";
import { useExpenseDataByProject } from "../../hooks/useDashboard_Data";
import { formatCurrency } from "../../utils/appUtils";
import { formatDate_DayMonth } from "../../utils/dateUtils";
import { useProjectName } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
const ExpenseByProject = () => {
const projectId = useSelector((store) => store.localVariables.projectId);
const [projectName, setProjectName] = useState("All Project");
const [range, setRange] = useState("12M");
const { projectNames, loading } = useProjectName();
const [selectedType, setSelectedType] = useState("");
const [viewMode, setViewMode] = useState("Category");
const [chartData, setChartData] = useState({ categories: [], data: [] });
const selectedProject = useSelectedProject();
const { ExpenseTypes, loading: typeLoading } = useExpenseType();
const { data: expenseApiData, isLoading } = useExpenseDataByProject(
projectId,
selectedType,
range === "All" ? null : parseInt(range)
);
useEffect(() => {
if (selectedProject && projectNames?.length) {
const project = projectNames.find((p) => p.id === selectedProject);
setProjectName(project?.name || "All Project");
} else {
setProjectName("All Project");
}
}, [projectNames, selectedProject]);
useEffect(() => {
if (expenseApiData) {
const categories = expenseApiData.map((item) =>
formatDate_DayMonth(item.monthName, item.year)
);
const data = expenseApiData.map((item) => item.total);
setChartData({ categories, data });
} else {
setChartData({ categories: [], data: [] });
}
}, [expenseApiData]);
const getSelectedTypeName = () => {
if (!selectedType) return "All Types";
const found = ExpenseTypes.find((t) => t.id === selectedType);
return found ? found.name : "All Types";
};
const options = {
chart: { type: "bar", toolbar: { show: false } },
plotOptions: {
bar: { horizontal: false, columnWidth: "55%", borderRadius: 4 },
},
dataLabels: { enabled: true, formatter: (val) => formatCurrency(val) },
xaxis: {
categories: chartData.categories,
labels: { style: { fontSize: "12px" }, rotate: -45 },
},
tooltip: {
y: {
formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`,
},
},
annotations: { xaxis: [{ x: 0, strokeDashArray: 0 }] },
fill: { opacity: 1 },
colors: ["#2196f3"],
};
const series = [
{
name: getSelectedTypeName(),
data: chartData.data,
},
];
return (
<div className="card shadow-sm rounded ">
{/* Header */}
<div className="card-header">
<div className="d-flex justify-content-start align-items-center mb-3 mt-3">
<div className="text-start">
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5>
<p className="card-subtitle m-0">{projectName}</p>
</div>
<div className="btn-group mb-4 ms-n8">
<button
className="btn btn-sm dropdown-toggle fs-5"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{viewMode}
</button>
<ul className="dropdown-menu dropdown-menu-end ">
<li>
<button
className="dropdown-item"
onClick={() => {
setViewMode("Category");
setSelectedType("");
}}
>
Category
</button>
</li>
<li>
<button
className="dropdown-item"
onClick={() => {
setViewMode("Project");
setSelectedType("");
}}
>
Project
</button>
</li>
</ul>
</div>
</div>
{/* Range Buttons + Expense Dropdown */}
<div className="d-flex align-items-center flex-wrap ">
{["1M", "3M", "6M", "12M", "All"].map((item) => (
<button
key={item}
className={`border-0 px-2 py-1 text-sm rounded ${range === item
? "text-white bg-primary"
: "text-body bg-transparent"
}`}
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
onClick={() => setRange(item)}
>
{item}
</button>
))}
{viewMode === "Category" && (
<select
className="form-select form-select-sm ms-auto mb-3 mt-1 mt-sm-0"
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
disabled={typeLoading}
style={{ maxWidth: "200px" }}
>
<option value="">All Types</option>
{ExpenseTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
)}
</div>
</div>
{/* Chart */}
<div className="card-body bg-white text-dark p-3 rounded" style={{ minHeight: "210px" }}>
{isLoading ? (
<p>Loading chart...</p>
) : !expenseApiData || expenseApiData.length === 0 ? (
<div className="text-center text-muted py-5">No data found</div>
) : (
<Chart options={options} series={series} type="bar" height={235} />
)}
</div>
</div>
);
};
export default ExpenseByProject;

View File

@ -0,0 +1,157 @@
import React, { useEffect, useState } from "react";
import { useExpense } from "../../hooks/useExpense";
import { useExpenseStatus } from "../../hooks/useDashboard_Data";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectName } from "../../hooks/useProjects";
import { countDigit, formatCurrency } from "../../utils/appUtils";
import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants";
import { useNavigate } from "react-router-dom";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const ExpenseStatus = () => {
const [projectName, setProjectName] = useState("All Project");
const selectedProject = useSelectedProject();
const { projectNames, loading } = useProjectName();
const { data, isPending, error } = useExpenseStatus(selectedProject);
const navigate = useNavigate();
const isManageExpense = useHasUserPermission(EXPENSE_MANAGE)
useEffect(() => {
if (selectedProject && projectNames?.length) {
const project = projectNames.find((p) => p.id === selectedProject);
setProjectName(project?.name || "All Project");
} else {
setProjectName("All Project");
}
}, [projectNames, selectedProject]);
const handleNavigate = (status) => {
if (selectedProject) {
navigate(`/expenses/${status}/${selectedProject}`);
} else {
navigate(`/expenses/${status}`);
}
};
return (
<>
<div className="card-header d-flex justify-content-between text-start ">
<div className="m-0">
<h5 className="card-title mb-1">Expense - By Status</h5>
<p className="card-subtitle m-0 ">{projectName}</p>
</div>
</div>
<div className="card-body ">
<div className="report-list text-start">
{[
{
title: "Pending Payment",
count: data?.processPending?.count || 0,
amount: data?.processPending?.totalAmount || 0,
icon: "bx bx-rupee",
iconColor: "text-primary",
status: EXPENSE_STATUS.payment_pending,
},
{
title: "Pending Approve",
count: data?.approvePending?.count || 0,
amount: data?.approvePending?.totalAmount || 0,
icon: "fa-solid fa-check",
iconColor: "text-warning",
status: EXPENSE_STATUS.approve_pending,
},
{
title: "Pending Review",
count: data?.reviewPending?.count || 0,
amount: data?.reviewPending?.totalAmount || 0,
icon: "bx bx-search-alt-2",
iconColor: "text-secondary",
status: EXPENSE_STATUS.review_pending,
},
{
title: "Draft",
count: data?.draft?.count || 0,
amount: data?.draft?.totalAmount || 0,
icon: "bx bx-file-blank",
iconColor: "text-info",
status: EXPENSE_STATUS.daft,
},
].map((item, idx) => (
<div
key={idx}
className="report-list-item rounded-2 mb-4 bg-lighter px-2 py-1 cursor-pointer"
onClick={() => handleNavigate(item?.status)}
>
<div className="d-flex align-items-center">
<div className="report-list-icon shadow-xs me-2">
<span className="d-inline-flex align-items-center justify-content-center rounded-circle border p-2">
<i className={`${item?.icon} ${item?.iconColor} bx-lg`}></i>
</span>
</div>
<div className="d-flex justify-content-between align-items-center w-100 flex-wrap gap-2">
<div className="d-flex flex-column gap-2">
<span className="fw-bold">{item?.title}</span>
{item?.amount ? (
<small className="mb-0 text-primary">
{formatCurrency(item?.amount)}
</small>
) : (
<small className="mb-0 text-primary">{formatCurrency(0)}</small>
)}
</div>
<div>
<small
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
} text-gray-500`}
>
{item?.count || 0}
</small>
<small className="text-muted fs-semibold text-royalblue text-md">
<i className="bx bx-chevron-right"></i>
</small>
</div>
</div>
</div>
</div>
))}
</div>
<div className=" py-0 text-start mb-2">
{isManageExpense && (
<div
className="d-flex justify-content-between align-items-center cursor-pointer"
onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
>
<div className="d-block">
<span
className={`fs-semibold d-block ${countDigit(data?.totalAmount || 0) > 3 ? "text-base" : "text-lg"
}`}
>
Project Spendings:
</span>{" "}
<small className="d-block text-xxs text-gary-80">
(All Processed Payments)
</small>
</div>
<div className="d-flex align-items-center gap-2">
<span
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
} text-md`}
>
{formatCurrency(data?.totalAmount || 0)}
</span>
<small className="text-muted fs-semibold text-royalblue text-md">
<i className="bx bx-chevron-right"></i>
</small>
</div>
</div>
)}
</div>
</div>
</>
);
};
export default ExpenseStatus;

View File

@ -3,7 +3,8 @@ import HorizontalBarChart from "../Charts/HorizontalBarChart";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
const ProjectCompletionChart = () => { const ProjectCompletionChart = () => {
const { projects, loading } = useProjects(); const { data: projects = [], isLoading: loading, isError, error } = useProjects();
// Bar chart logic // Bar chart logic
const projectNames = projects?.map((p) => p.name) || []; const projectNames = projects?.map((p) => p.name) || [];
@ -11,7 +12,7 @@ const ProjectCompletionChart = () => {
projects?.map((p) => { projects?.map((p) => {
const completed = p.completedWork || 0; const completed = p.completedWork || 0;
const planned = p.plannedWork || 1; const planned = p.plannedWork || 1;
const percent = (completed / planned) * 100; const percent = planned ? (completed / planned) * 100 : 0;
return Math.min(Math.round(percent), 100); return Math.min(Math.round(percent), 100);
}) || []; }) || [];

View File

@ -1,6 +1,9 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data"; import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import ProjectInfra from "../Project/ProjectInfra";
import { ProjectCardSkeleton } from "./DashboardSkeleton";
import { formatFigure } from "../../utils/appUtils";
const Projects = () => { const Projects = () => {
const { const {
@ -8,6 +11,7 @@ const Projects = () => {
isLoading, isLoading,
isError, isError,
error, error,
isFetching,
refetch, refetch,
} = useDashboardProjectsCardData(); } = useDashboardProjectsCardData();
@ -23,7 +27,7 @@ const Projects = () => {
const totalProjects = projectsCardData?.totalProjects ?? 0; const totalProjects = projectsCardData?.totalProjects ?? 0;
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0; const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
if (isLoading) return <ProjectCardSkeleton />;
return ( return (
<div className="card p-3 h-100 text-center d-flex justify-content-between"> <div className="card p-3 h-100 text-center d-flex justify-content-between">
<div className="d-flex justify-content-start align-items-center mb-3"> <div className="d-flex justify-content-start align-items-center mb-3">
@ -33,24 +37,41 @@ const Projects = () => {
</h5> </h5>
</div> </div>
{isLoading ? ( {isError ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1"> <div className="d-flex flex-column justify-content-center align-items-center p-1">
<div className="spinner-border text-primary" role="status"> <i className="bx bx-error-circle bx-sm fs-2 "></i>
<span className="visually-hidden">Loading...</span> <small className="text-muted mb-2">
</div> {error?.message || "Unable to load data at the moment."}
</div> </small>
) : isError ? ( <span
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center"> className={`text-muted ${
{error?.message || "Error loading data"} isFetching ? "cursor-wait" : "cursor-pointer"
}`}
onClick={refetch}
>
<i
className={`bx bx-refresh me-1 ${isFetching ? "bx-spin" : ""}`}
></i>{" "}
Retry
</span>
</div> </div>
) : ( ) : (
<div className="d-flex justify-content-around align-items-start mt-n2"> <div className="d-flex justify-content-around align-items-start mt-n2">
<div> <div>
<h4 className="mb-0 fw-bold">{totalProjects.toLocaleString()}</h4> <h4 className="mb-0 fw-bold">
{formatFigure(totalProjects ?? 0, {
notation: "compact",
})}
</h4>
<small className="text-muted">Total</small> <small className="text-muted">Total</small>
</div> </div>
<div> <div>
<h4 className="mb-0 fw-bold">{ongoingProjects.toLocaleString()}</h4> <h4 className="mb-0 fw-bold">
{formatFigure(ongoingProjects ?? 0, {
notation: "compact",
})}
</h4>
<small className="text-muted">Ongoing</small> <small className="text-muted">Ongoing</small>
</div> </div>
</div> </div>

View File

@ -1,6 +1,8 @@
import React from "react"; import React from "react";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data"; import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
import { TasksSkeleton } from "./DashboardSkeleton";
import { formatCurrency, formatFigure } from "../../utils/appUtils";
const TasksCard = () => { const TasksCard = () => {
const projectId = useSelectedProject(); const projectId = useSelectedProject();
@ -10,42 +12,57 @@ const TasksCard = () => {
isLoading, isLoading,
isError, isError,
error, error,
isFetching,
refetch,
} = useDashboardTasksCardData(projectId); } = useDashboardTasksCardData(projectId);
if (isLoading) return <TasksSkeleton />;
return ( return (
<div className="card p-3 h-100 text-center d-flex justify-content-between"> <div className="card p-3 h-100 text-center d-flex flex-column justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3"> <div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2"> <h5 className="fw-bold mb-0 ms-2">
<i className="bx bx-task text-success"></i> Tasks <i className="bx bx-task text-success"></i> Tasks
</h5> </h5>
</div> </div>
{isLoading ? ( {isError ? (
// Loader while fetching <div className="d-flex flex-column justify-content-center align-items-center p-3">
<div className="d-flex justify-content-center align-items-center flex-grow-1"> <i className="bx bx-error-circle bx-sm fs-2 mb-2"></i>
<div className="spinner-border text-primary" role="status"> <small className="text-muted mb-2">
<span className="visually-hidden">Loading...</span> {error?.message || "Unable to load data at the moment."}
</div> </small>
</div> <span
) : isError ? ( className={`text-muted ${
// Show error isFetching ? "cursor-wait" : "cursor-pointer"
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center"> }`}
{error?.message || "Error loading data"} onClick={refetch}
>
<i
className={`bx bx-refresh me-1 ${isFetching ? "bx-spin" : ""}`}
></i>
Retry
</span>
</div> </div>
) : ( ) : (
// Show data <div className="d-flex justify-content-around align-items-start flex-wrap mt-n2">
<div className="d-flex justify-content-around align-items-start mt-n2"> {/* Total Tasks */}
<div> <div className="text-center flex-fill p-2">
<h4 className="mb-0 fw-bold"> <h4 className="mb-0 fw-bold text-truncate">
{tasksCardData?.totalTasks?.toLocaleString() ?? 0} {formatFigure(tasksCardData?.totalTasks ?? 0, {
notation: "compact",
})}
</h4> </h4>
<small className="text-muted">Total</small> <small className="text-muted d-block">Total</small>
</div> </div>
<div>
<h4 className="mb-0 fw-bold"> {/* Completed Tasks */}
{tasksCardData?.completedTasks?.toLocaleString() ?? 0} <div className="text-center flex-fill p-2">
<h4 className="mb-0 fw-bold text-truncate">
{formatFigure(tasksCardData?.completedTasks ?? 0, {
notation: "compact",
})}
</h4> </h4>
<small className="text-muted">Completed</small> <small className="text-muted d-block">Completed</small>
</div> </div>
</div> </div>
)} )}

View File

@ -4,16 +4,20 @@ import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { TeamsSkeleton } from "./DashboardSkeleton";
import { formatFigure } from "../../utils/appUtils";
const Teams = () => { const Teams = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const projectId = useSelectedProject() const projectId = useSelectedProject();
const { const {
data: teamsCardData, data: teamsCardData,
isLoading, isLoading,
isError, isError,
error, error,
isFetching,
refetch,
} = useDashboardTeamsCardData(projectId); } = useDashboardTeamsCardData(projectId);
// Handle real-time updates via eventBus // Handle real-time updates via eventBus
@ -40,6 +44,7 @@ const Teams = () => {
const inToday = teamsCardData?.inToday ?? 0; const inToday = teamsCardData?.inToday ?? 0;
const totalEmployees = teamsCardData?.totalEmployees ?? 0; const totalEmployees = teamsCardData?.totalEmployees ?? 0;
if (isLoading) return <TeamsSkeleton />;
return ( return (
<div className="card p-3 h-100 text-center d-flex justify-content-between"> <div className="card p-3 h-100 text-center d-flex justify-content-between">
<div className="d-flex justify-content-start align-items-center mb-3"> <div className="d-flex justify-content-start align-items-center mb-3">
@ -48,24 +53,41 @@ const Teams = () => {
</h5> </h5>
</div> </div>
{isLoading ? ( {isError ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1"> <div className="d-flex flex-column justify-content-center align-items-center p-1">
<div className="spinner-border text-primary" role="status"> <i className="bx bx-error-circle bx-sm fs-2 "></i>
<span className="visually-hidden">Loading...</span>
</div> <small className="text-muted mb-2">
</div> {error?.message || "Unable to load data at the moment."}
) : isError ? ( </small>
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center"> <span
{error?.message || "Error loading data"} className={`text-muted ${
isFetching ? "cursor-wait" : "cursor-pointer"
}`}
onClick={refetch}
>
<i
className={`bx bx-refresh me-1 ${isFetching ? "bx-spin" : ""}`}
></i>{" "}
Retry
</span>
</div> </div>
) : ( ) : (
<div className="d-flex justify-content-around align-items-start mt-n2"> <div className="d-flex justify-content-around align-items-start mt-n2">
<div> <div>
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4> <h4 className="mb-0 fw-bold">
{formatFigure(totalEmployees ?? 0, {
notation: "compact",
})}
</h4>
<small className="text-muted">Total Employees</small> <small className="text-muted">Total Employees</small>
</div> </div>
<div> <div>
<h4 className="mb-0 fw-bold">{inToday.toLocaleString()}</h4> <h4 className="mb-0 fw-bold">
{formatFigure(inToday ?? 0, {
notation: "compact",
})}
</h4>
<small className="text-muted">In Today</small> <small className="text-muted">In Today</small>
</div> </div>
</div> </div>

View File

@ -0,0 +1,56 @@
import React, { useMemo } from "react";
const ContactFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => {
const data = filterData?.data || filterData || {};
const filterChips = useMemo(() => {
const chips = [];
const addGroup = (ids, list, label, key) => {
if (!ids?.length) return;
const items = ids.map((id) => ({
id,
name: list?.find((i) => i.id === id)?.name || id,
}));
chips.push({ key, label, items });
};
addGroup(filters.bucketIds, data.buckets, "Buckets", "bucketIds");
addGroup(filters.categoryIds, data.contactCategories, "Category", "categoryIds");
return chips;
}, [filters, filterData]);
if (!filterChips.length) return null;
return (
<div className="d-flex flex-wrap align-items-center gap-2">
{filterChips.map((chipGroup) => (
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
<span className="fw-semibold me-2">{chipGroup.label}:</span>
{chipGroup.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chipGroup.key, item.id)}
/>
</span>
))}
</div>
))}
</div>
);
};
export default ContactFilterChips;

View File

@ -160,8 +160,7 @@ const ListViewContact = ({ data, Pagination }) => {
</div> </div>
) : ( ) : (
<i <i
className={`bx ${ className={`bx ${isPending && activeContact === row.id
isPending && activeContact === row.id
? "bx-loader-alt bx-spin" ? "bx-loader-alt bx-spin"
: "bx-recycle" : "bx-recycle"
} me-1 text-primary cursor-pointer`} } me-1 text-primary cursor-pointer`}
@ -188,13 +187,13 @@ const ListViewContact = ({ data, Pagination }) => {
)} )}
</tbody> </tbody>
</table> </table>
{Pagination && (
<div className="d-flex justify-content-start">
{Pagination}
</div>
)}
</div> </div>
</div> </div>
{Pagination && (
<div className="d-flex justify-content-start">
{Pagination}
</div>
)}
</div> </div>
</> </>
); );

View File

@ -0,0 +1,79 @@
import React, { useMemo } from "react";
import moment from "moment";
const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => {
// Normalize data (in case its wrapped in .data)
const data = filterData?.data || filterData || {};
const filterChips = useMemo(() => {
const chips = [];
const buildGroup = (ids, list, label, key) => {
if (!ids?.length) return;
const items = ids.map((id) => ({
id,
name: list?.find((item) => item.id === id)?.name || id,
}));
chips.push({ key, label, items });
};
// Build chips dynamically
buildGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds");
buildGroup(filters.organizations, data.organizations, "Organization", "organizations");
// Example: Add date range if you ever add in future
if (filters.startDate || filters.endDate) {
const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : "";
const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : "";
chips.push({
key: "dateRange",
label: "Date Range",
items: [{ id: "dateRange", name: `${start} - ${end}` }],
});
}
return chips;
}, [filters, filterData]);
if (!filterChips.length) return null;
return (
<div className="row my-2">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-2">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1"
style={{ fontSize: "0.9rem" }}
>
<span className="fw-semibold me-2">{chip.label}:</span>
<div className="d-flex flex-wrap align-items-center gap-1">
{chip.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chip.key, item.id)}
/>
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default NoteFilterChips;

View File

@ -0,0 +1,94 @@
import React, { useMemo } from "react";
import moment from "moment";
const DocumentFilterChips = ({ filters, filterData, removeFilterChip }) => {
// Normalize structure: handle both "filterData.data" and plain "filterData"
const data = filterData?.data || filterData || {};
const filterChips = useMemo(() => {
const chips = [];
const buildGroup = (ids, list, label, key) => {
if (!ids?.length) return;
const items = ids.map((id) => ({
id,
name: list?.find((item) => item.id === id)?.name || id,
}));
chips.push({ key, label, items });
};
// Build chips using normalized data
buildGroup(filters.uploadedByIds, data.uploadedBy || [], "Uploaded By", "uploadedByIds");
buildGroup(filters.documentCategoryIds, data.documentCategory || [], "Category", "documentCategoryIds");
buildGroup(filters.documentTypeIds, data.documentType || [], "Type", "documentTypeIds");
buildGroup(filters.documentTagIds, data.documentTag || [], "Tags", "documentTagIds");
if (filters.statusIds?.length) {
const items = filters.statusIds.map((status) => ({
id: status,
name:
status === true
? "Verified"
: status === false
? "Rejected"
: "Pending",
}));
chips.push({ key: "statusIds", label: "Status", items });
}
if (filters.startDate || filters.endDate) {
const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : "";
const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : "";
chips.push({
key: "dateRange",
label: "Date Range",
items: [{ id: "dateRange", name: `${start} - ${end}` }],
});
}
return chips;
}, [filters, filterData]);
if (!filterChips.length) return null;
return (
<div className="row my-2">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-1">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1"
style={{ fontSize: "0.9rem" }}
>
<span className="fw-semibold me-2">{chip.label}:</span>
<div className="d-flex flex-wrap align-items-center gap-1">
{chip.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chip.key, item.id)}
/>
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default DocumentFilterChips;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useEffect, useState, useMemo, useImperativeHandle, forwardRef } from "react";
import { useDocumentFilterEntities } from "../../hooks/useDocument"; import { useDocumentFilterEntities } from "../../hooks/useDocument";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -9,16 +9,34 @@ import {
import { DateRangePicker1 } from "../common/DateRangePicker"; import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple"; import SelectMultiple from "../common/SelectMultiple";
import moment from "moment"; import moment from "moment";
import { useParams } from "react-router-dom";
const DocumentFilterPanel = ({ entityTypeId, onApply }) => { const DocumentFilterPanel = forwardRef(
({ entityTypeId, onApply, setFilterdata }, ref) => {
const [resetKey, setResetKey] = useState(0); const [resetKey, setResetKey] = useState(0);
const { status } = useParams();
const { data, isError, isLoading, error } = const { data, isError, isLoading, error } =
useDocumentFilterEntities(entityTypeId); useDocumentFilterEntities(entityTypeId);
//changes
const dynamicDocumentFilterDefaultValues = useMemo(() => {
return {
...DocumentFilterDefaultValues,
uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [],
documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [],
documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [],
documentTagIds: DocumentFilterDefaultValues.documentTagIds || [],
startDate: DocumentFilterDefaultValues.startDate,
endDate: DocumentFilterDefaultValues.endDate,
};
}, [status]);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(DocumentFilterSchema), resolver: zodResolver(DocumentFilterSchema),
defaultValues: DocumentFilterDefaultValues, defaultValues: dynamicDocumentFilterDefaultValues,
}); });
const { handleSubmit, reset, setValue, watch } = methods; const { handleSubmit, reset, setValue, watch } = methods;
@ -32,6 +50,24 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
document.querySelector(".offcanvas.show .btn-close")?.click(); document.querySelector(".offcanvas.show .btn-close")?.click();
}; };
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
}
},
getValues: methods.getValues, // optional, to read current filter state
}));
//changes
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
const onSubmit = (values) => { const onSubmit = (values) => {
onApply({ onApply({
...values, ...values,
@ -42,14 +78,14 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString() ? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
: null, : null,
}); });
closePanel(); // closePanel();
}; };
const onClear = () => { const onClear = () => {
reset(DocumentFilterDefaultValues); reset(DocumentFilterDefaultValues);
setResetKey((prev) => prev + 1); setResetKey((prev) => prev + 1);
onApply(DocumentFilterDefaultValues); onApply(DocumentFilterDefaultValues);
closePanel(); // closePanel();
}; };
if (isLoading) return <div>Loading...</div>; if (isLoading) return <div>Loading...</div>;
@ -63,6 +99,8 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
documentTag = [], documentTag = [],
} = data?.data || {}; } = data?.data || {};
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
@ -73,18 +111,16 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none"> <div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${ className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : ""
isUploadedAt ? "active btn-secondary text-white" : "" }`}
}`}
onClick={() => setValue("isUploadedAt", true)} onClick={() => setValue("isUploadedAt", true)}
> >
Uploaded On Uploaded On
</button> </button>
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${ className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : ""
!isUploadedAt ? "active btn-secondary text-white" : "" }`}
}`}
onClick={() => setValue("isUploadedAt", false)} onClick={() => setValue("isUploadedAt", false)}
> >
Updated On Updated On
@ -189,18 +225,18 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
<div className="d-flex justify-content-end py-3 gap-2"> <div className="d-flex justify-content-end py-3 gap-2">
<button <button
type="button" type="button"
className="btn btn-label-secondary btn-xs" className="btn btn-label-secondary btn-sm"
onClick={onClear} onClick={onClear}
> >
Clear Clear
</button> </button>
<button type="submit" className="btn btn-primary btn-xs"> <button type="submit" className="btn btn-primary btn-sm">
Apply Apply
</button> </button>
</div> </div>
</form> </form>
</FormProvider> </FormProvider>
); );
}; });
export default DocumentFilterPanel; export default DocumentFilterPanel;

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useRef, useState } from "react";
import GlobalModel from "../common/GlobalModel"; import GlobalModel from "../common/GlobalModel";
import NewDocument from "./ManageDocument"; import NewDocument from "./ManageDocument";
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants"; import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument";
import DocumentViewerModal from "./DocumentViewerModal"; import DocumentViewerModal from "./DocumentViewerModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProfile } from "../../hooks/useProfile"; import { useProfile } from "../../hooks/useProfile";
import DocumentFilterChips from "./DocumentFilterChips";
// Context // Context
export const DocumentContext = createContext(); export const DocumentContext = createContext();
@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => {
const [isSelf, setIsSelf] = useState(false); const [isSelf, setIsSelf] = useState(false);
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [isActive, setIsActive] = useState(true); const [isActive, setIsActive] = useState(true);
const [filters, setFilter] = useState(); const [filters, setFilter] = useState(DocumentFilterDefaultValues);
const [isRefetching, setIsRefetching] = useState(false); const [isRefetching, setIsRefetching] = useState(false);
const [refetchFn, setRefetchFn] = useState(null); const [refetchFn, setRefetchFn] = useState(null);
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity); const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
const { employeeId } = useParams(); const { employeeId } = useParams();
const [OpenDocument, setOpenDocument] = useState(false); const [OpenDocument, setOpenDocument] = useState(false);
const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues);
const updatedRef = useRef();
const [ManageDoc, setManageDoc] = useState({ const [ManageDoc, setManageDoc] = useState({
document: null, document: null,
isOpen: false, isOpen: false,
@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
"Document Filters", "Document Filters",
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} /> <DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} setFilterdata={setFilterdata} ref={updatedRef} />
); );
return () => { return () => {
@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => {
setDocumentEntity(Document_Entity); setDocumentEntity(Document_Entity);
} }
}, [Document_Entity]); }, [Document_Entity]);
const removeFilterChip = (key, id) => {
const updatedFilters = { ...filters };
if (Array.isArray(updatedFilters[key])) {
updatedFilters[key] = updatedFilters[key].filter((v) => v !== id);
updatedRef.current?.resetFieldValue(key,updatedFilters[key]);
}
else if (key === "dateRange") {
updatedFilters.startDate = null;
updatedFilters.endDate = null;
updatedRef.current?.resetFieldValue("startDate",null);
updatedRef.current?.resetFieldValue("endDate",null);
}
else {
updatedFilters[key] = null;
}
setFilter(updatedFilters);
return updatedFilters;
};
return ( return (
<DocumentContext.Provider value={contextValues}> <DocumentContext.Provider value={contextValues}>
<div className="mt-5"> <div className="mt-2">
<div className="card page-min-h d-flex p-2"> <div className="card page-min-h d-flex p-5">
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
<div className="row align-items-center"> <div className="row align-items-center">
{/* Search */} {/* Search */}
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center"> <div className="d-flex flex-row gap-2 col-12 col-md-8 col-lg-4 mb-md-0 align-items-center mb-2">
<div className="d-flex"> <div className="d-flex">
{" "} {" "}
<input <input
@ -149,7 +174,7 @@ const Documents = ({ Document_Entity, Entity }) => {
</label> </label>
</div> </div>
<div className="col-6 col-md-6 col-lg-8 text-end"> <div className="col-12 col-md-6 col-lg-8 text-end">
{(isSelf || canUploadDocument) && ( {(isSelf || canUploadDocument) && (
<button <button
className="btn btn-sm btn-primary me-3" className="btn btn-sm btn-primary me-3"
@ -231,4 +256,4 @@ const Documents = ({ Document_Entity, Entity }) => {
); );
}; };
export default Documents; export default Documents;

View File

@ -82,9 +82,9 @@ const DocumentsList = ({
if (isLoading || isFetching) return <DocumentTableSkeleton />; if (isLoading || isFetching) return <DocumentTableSkeleton />;
if (isError) if (isError)
return <div>Error: {error?.message || "Something went wrong"}</div>; return <div>Error: {error?.message || "Something went wrong"}</div>;
if (isInitialEmpty) return <div>No documents found yet.</div>; if (isInitialEmpty) return <div className="py-12 my-12">No documents found yet.</div>;
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>; if (isSearchEmpty) return <div className="py-12 my-12">No results found for "{debouncedSearch}"</div>;
if (isFilterEmpty) return <div>No documents match your filter.</div>; if (isFilterEmpty) return <div className="py-12 my-12">No documents match your filter.</div>;
const handleDelete = () => { const handleDelete = () => {
ActiveInActive( ActiveInActive(
@ -138,16 +138,14 @@ const DocumentsList = ({
lastName={e.uploadedBy?.lastName} lastName={e.uploadedBy?.lastName}
/> />
<span className="text-truncate ms-1"> <span className="text-truncate ms-1">
{`${e.uploadedBy?.firstName ?? ""} ${ {`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
e.uploadedBy?.lastName ?? "" }`.trim() || "N/A"}
}`.trim() || "N/A"}
</span> </span>
</div> </div>
), ),
getValue: (e) => getValue: (e) =>
`${e.uploadedBy?.firstName ?? ""} ${ `${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
e.uploadedBy?.lastName ?? "" }`.trim() || "N/A",
}`.trim() || "N/A",
}, },
{ {
key: "uploadedAt", key: "uploadedAt",
@ -217,7 +215,7 @@ const DocumentsList = ({
} }
></i> ></i>
{(isSelf || canModifyDocument) && ( {(isSelf || canModifyDocument) && (
<i <i
className="bx bx-edit text-secondary cursor-pointer" className="bx bx-edit text-secondary cursor-pointer"
onClick={() => onClick={() =>
@ -226,7 +224,7 @@ const DocumentsList = ({
></i> ></i>
)} )}
{(isSelf || canDeleteDocument) && ( {(isSelf || canDeleteDocument) && (
<i <i
className="bx bx-trash text-danger cursor-pointer" className="bx bx-trash text-danger cursor-pointer"
onClick={() => { onClick={() => {

View File

@ -51,7 +51,6 @@ const EmpAttendance = () => {
new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime() new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime()
); );
console.log(sorted);
const { currentPage, totalPages, currentItems, paginate } = usePagination( const { currentPage, totalPages, currentItems, paginate } = usePagination(
sorted, sorted,

View File

@ -0,0 +1,84 @@
import moment from "moment";
import { exportToExcel, exportToCSV, exportToPDF, printTable } from "../../utils/tableExportUtils";
/**
* Handles export operations for employee data.
* @param {string} type - Export type: 'csv', 'excel', 'pdf', or 'print'
* @param {Array} employeeList - Full employee data array
* @param {Array} filteredData - Filtered employee data (if search applied)
* @param {string} searchText - Current search text (used to decide dataset)
* @param {RefObject} tableRef - Table reference (used for print)
*/
const handleEmployeeExport = (type, employeeList, filteredData, searchText, tableRef) => {
// Export full list (filtered if search applied)
const dataToExport = searchText ? filteredData : employeeList;
if (!dataToExport || dataToExport.length === 0) return;
// Map and format employee data for export
const exportData = dataToExport.map((item) => ({
"First Name": item.firstName || "",
"Middle Name": item.middleName || "",
"Last Name": item.lastName || "",
"Email": item.email || "",
"Gender": item.gender || "",
"Birth Date": item.birthdate
? moment(item.birthdate).format("DD-MMM-YYYY")
: "",
"Joining Date": item.joiningDate
? moment(item.joiningDate).format("DD-MMM-YYYY")
: "",
"Permanent Address": item.permanentAddress || "",
"Current Address": item.currentAddress || "",
"Phone Number": item.phoneNumber || "",
"Emergency Phone Number": item.emergencyPhoneNumber || "",
"Emergency Contact Person": item.emergencyContactPerson || "",
"Is Active": item.isActive ? "Active" : "Inactive",
"Job Role": item.jobRole || "",
}));
switch (type) {
case "csv":
exportToCSV(exportData, "employees");
break;
case "excel":
exportToExcel(exportData, "employees");
break;
case "pdf":
exportToPDF(
dataToExport.map((item) => ({
Name: `${item.firstName || ""} ${item.lastName || ""}`.trim(),
Email: item.email || "",
"Phone Number": item.phoneNumber || "",
"Job Role": item.jobRole || "",
"Joining Date": item.joiningDate
? moment(item.joiningDate).format("DD-MMM-YYYY")
: "",
Gender: item.gender || "",
Status: item.isActive ? "Active" : "Inactive",
})),
"employees",
[
"Name",
"Email",
"Phone Number",
"Job Role",
"Joining Date",
"Gender",
"Status",
]
);
break;
case "print":
printTable(tableRef.current);
break;
default:
break;
}
};
export default handleEmployeeExport;

View File

@ -0,0 +1,86 @@
import React, { useMemo } from "react";
const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => {
// Build chips from filters
const filterChips = useMemo(() => {
const chips = [];
const buildGroup = (ids, list, label, key) => {
if (!ids?.length) return;
const items = ids.map((id) => ({
id,
name: list.find((item) => item.id === id)?.name || id,
}));
chips.push({ key, label, items });
};
buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds");
buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds");
buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById");
buildGroup(filters.statusIds, filterData.status, "Status", "statusIds");
buildGroup(filters.ExpenseTypeIds, filterData.expensesType, "Category", "ExpenseTypeIds");
if (filters.startDate || filters.endDate) {
const start = filters.startDate
? new Date(filters.startDate).toLocaleDateString()
: "";
const end = filters.endDate
? new Date(filters.endDate).toLocaleDateString()
: "";
chips.push({
key: "dateRange",
label: "Date Range",
items: [{ id: "dateRange", name: `${start} - ${end}` }],
});
}
return chips;
}, [filters, filterData]);
if (!filterChips.length) return null;
return (
<div className="row">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-1 text-start">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1 "
style={{ fontSize: "0.9rem", maxWidth: "100%" }}
>
{/* Chip Label */}
<span className="fw-semibold me-2">{chip.label}:</span>
{/* Chip Items */}
<div className="d-flex flex-wrap align-items-center gap-1">
{chip.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chip.key, item.id)}
/>
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default ExpenseFilterChips;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo } from "react"; import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react";
import { FormProvider, useForm, Controller } from "react-hook-form"; import { FormProvider, useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { defaultFilter, SearchSchema } from "./ExpenseSchema"; import { defaultFilter, SearchSchema } from "./ExpenseSchema";
@ -13,9 +13,11 @@ import { useSelector } from "react-redux";
import moment from "moment"; import moment from "moment";
import { useExpenseFilter } from "../../hooks/useExpense"; import { useExpenseFilter } from "../../hooks/useExpense";
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton"; import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
import { useLocation } from "react-router-dom"; import { useLocation, useNavigate, useParams } from "react-router-dom";
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => {
const { status } = useParams();
const navigate = useNavigate();
const selectedProjectId = useSelector( const selectedProjectId = useSelector(
(store) => store.localVariables.projectId (store) => store.localVariables.projectId
); );
@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
{ id: "submittedBy", name: "Submitted By" }, { id: "submittedBy", name: "Submitted By" },
{ id: "project", name: "Project" }, { id: "project", name: "Project" },
{ id: "paymentMode", name: "Payment Mode" }, { id: "paymentMode", name: "Payment Mode" },
{ id: "expensesType", name: "Expense Type" }, { id: "expensesType", name: "Expense Category" },
{ id: "createdAt", name: "Submitted Date" }, { id: "createdAt", name: "Submitted Date" },
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}, []); }, []);
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]); const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
const [resetKey, setResetKey] = useState(0); const [resetKey, setResetKey] = useState(0);
const dynamicDefaultFilter = useMemo(() => {
return {
...defaultFilter,
statusIds: status ? [status] : defaultFilter.statusIds || [],
projectIds: defaultFilter.projectIds || [],
createdByIds: defaultFilter.createdByIds || [],
paidById: defaultFilter.paidById || [],
ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [],
isTransactionDate: defaultFilter.isTransactionDate ?? true,
startDate: defaultFilter.startDate,
endDate: defaultFilter.endDate,
};
}, [status]);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(SearchSchema), resolver: zodResolver(SearchSchema),
defaultValues: defaultFilter, defaultValues: dynamicDefaultFilter,
}); });
const { control, handleSubmit, reset, setValue, watch } = methods; const { control, handleSubmit, reset, setValue, watch } = methods;
@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
document.querySelector(".offcanvas.show .btn-close")?.click(); document.querySelector(".offcanvas.show .btn-close")?.click();
}; };
// Change here
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
const handleGroupChange = (e) => { const handleGroupChange = (e) => {
const group = groupByList.find((g) => g.id === e.target.value); const group = groupByList.find((g) => g.id === e.target.value);
if (group) setSelectedGroup(group); if (group) setSelectedGroup(group);
}; };
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
// Reset specific field
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
}
},
getValues: methods.getValues, // optional, to read current filter state
}));
const onSubmit = (formData) => { const onSubmit = (formData) => {
onApply({ onApply({
...formData, ...formData,
@ -71,17 +106,55 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
onApply(defaultFilter); onApply(defaultFilter);
handleGroupBy(groupByList[0].id); handleGroupBy(groupByList[0].id);
closePanel(); closePanel();
if (status) {
navigate("/expenses", { replace: true });
}
}; };
// Close popup when navigating to another component
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
closePanel(); closePanel();
}, [location]); }, [location]);
const [appliedStatusId, setAppliedStatusId] = useState(null);
useEffect(() => {
if (!status || !data) return;
if (status !== appliedStatusId) {
const filterWithStatus = {
...dynamicDefaultFilter,
projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [],
startDate: dynamicDefaultFilter.startDate
? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString()
: undefined,
endDate: dynamicDefaultFilter.endDate
? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString()
: undefined,
};
onApply(filterWithStatus);
handleGroupBy(selectedGroup.id);
setAppliedStatusId(status);
}
}, [
status,
data,
dynamicDefaultFilter,
onApply,
handleGroupBy,
selectedGroup.id,
appliedStatusId,
selectedProjectId, // Added dependency
]);
if (isLoading || isFetching) return <ExpenseFilterSkeleton />; if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched) if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>; return <div>Something went wrong Here- {error.message} </div>;
return ( return (
<> <>
<FormProvider {...methods}> <FormProvider {...methods}>
@ -92,18 +165,16 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none"> <div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${ className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
isTransactionDate ? "active btn-primary text-white" : "" }`}
}`}
onClick={() => setValue("isTransactionDate", true)} onClick={() => setValue("isTransactionDate", true)}
> >
Transaction Date Transaction Date
</button> </button>
<button <button
type="button" type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${ className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
!isTransactionDate ? "active btn-primary text-white" : "" }`}
}`}
onClick={() => setValue("isTransactionDate", false)} onClick={() => setValue("isTransactionDate", false)}
> >
Submitted Date Submitted Date
@ -143,6 +214,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
labelKey={(item) => item.name} labelKey={(item) => item.name}
valueKey="id" valueKey="id"
/> />
<SelectMultiple
name="ExpenseTypeIds"
label="Category :"
options={data.expensesType}
labelKey={(item) => item.name}
valueKey="id"
/>
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Status :</label> <label className="form-label">Status :</label>
@ -214,6 +292,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</FormProvider> </FormProvider>
</> </>
); );
}; });
export default ExpenseFilterPanel; export default ExpenseFilterPanel;

View File

@ -10,20 +10,29 @@ import {
EXPENSE_REJECTEDBY, EXPENSE_REJECTEDBY,
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
} from "../../utils/constants"; } from "../../utils/constants";
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils"; import {
formatCurrency,
getColorNameFromHex,
useDebounce,
} from "../../utils/appUtils";
import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
import ConfirmModal from "../common/ConfirmModal"; import ConfirmModal from "../common/ConfirmModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import ExpenseFilterChips from "./ExpenseFilterChips";
import { defaultFilter } from "./ExpenseSchema";
import { useNavigate } from "react-router-dom";
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
const [deletingId, setDeletingId] = useState(null); const [deletingId, setDeletingId] = useState(null);
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { setViewExpense, setManageExpenseModal } = useExpenseContext(); const { setViewExpense, setManageExpenseModal, filterData, removeFilterChip } = useExpenseContext();
const IsExpenseEditable = useHasUserPermission(); const IsExpenseEditable = useHasUserPermission();
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE); const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const navigate = useNavigate();
const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { mutate: DeleteExpense, isPending } = useDeleteExpense();
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList( const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
@ -59,40 +68,60 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
const groupByField = (items, field) => { const groupByField = (items, field) => {
return items.reduce((acc, item) => { return items.reduce((acc, item) => {
let key; let key;
let displayField;
switch (field) { switch (field) {
case "transactionDate": case "transactionDate":
key = item.transactionDate?.split("T")[0]; key = item?.transactionDate?.split("T")[0];
displayField = "Transaction Date";
break; break;
case "status": case "status":
key = item.status?.displayName || "Unknown"; key = item?.status?.displayName || "Unknown";
displayField = "Status";
break; break;
case "submittedBy": case "submittedBy":
key = `${item.createdBy?.firstName ?? ""} ${ key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
item.createdBy?.lastName ?? "" }`.trim();
}`.trim(); displayField = "Submitted By";
break; break;
case "project": case "project":
key = item.project?.name || "Unknown Project"; key = item?.project?.name || "Unknown Project";
displayField = "Project";
break; break;
case "paymentMode": case "paymentMode":
key = item.paymentMode?.name || "Unknown Mode"; key = item?.paymentMode?.name || "Unknown Mode";
displayField = "Payment Mode";
break; break;
case "expensesType": case "expensesType":
key = item.expensesType?.name || "Unknown Type"; key = item?.expensesType?.name || "Unknown Type";
displayField = "Expense Category";
break; break;
case "createdAt": case "createdAt":
key = item.createdAt?.split("T")[0] || "Unknown Type"; key = item?.createdAt?.split("T")[0] || "Unknown Date";
displayField = "Created Date";
break; break;
default: default:
key = "Others"; key = "Others";
displayField = "Others";
} }
if (!acc[key]) acc[key] = [];
acc[key].push(item); const groupKey = `${field}_${key}`; // unique key for object property
if (!acc[groupKey]) {
acc[groupKey] = { key, displayField, items: [] };
}
acc[groupKey].items.push(item);
return acc; return acc;
}, {}); }, {});
}; };
const expenseColumns = [ const expenseColumns = [
{
key: "expenseUId",
label: "Expense Id",
getValue: (e) => e.expenseUId || "N/A",
align: "text-start mx-2",
},
{ {
key: "expensesType", key: "expensesType",
label: "Expense Type", label: "Expense Type",
@ -110,11 +139,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
label: "Submitted By", label: "Submitted By",
align: "text-start", align: "text-start",
getValue: (e) => getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${ `${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
e.createdBy?.lastName ?? "" }`.trim() || "N/A",
}`.trim() || "N/A",
customRender: (e) => ( customRender: (e) => (
<div className="d-flex align-items-center"> <div className="d-flex align-items-center cursor-pointer"
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}>
<Avatar <Avatar
size="xs" size="xs"
classAvatar="m-0" classAvatar="m-0"
@ -122,9 +151,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
lastName={e.createdBy?.lastName} lastName={e.createdBy?.lastName}
/> />
<span className="text-truncate"> <span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${ {`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
e.createdBy?.lastName ?? "" }`.trim() || "N/A"}
}`.trim() || "N/A"}
</span> </span>
</div> </div>
), ),
@ -138,11 +166,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
{ {
key: "amount", key: "amount",
label: "Amount", label: "Amount",
getValue: (e) => ( getValue: (e) => <>{formatCurrency(e?.amount)}</>,
<>
<i className="bx bx-rupee b-xs"></i> {e?.amount}
</>
),
isAlwaysVisible: true, isAlwaysVisible: true,
align: "text-end", align: "text-end",
}, },
@ -152,9 +176,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
align: "text-center", align: "text-center",
getValue: (e) => ( getValue: (e) => (
<span <span
className={`badge bg-label-${ className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
getColorNameFromHex(e?.status?.color) || "secondary" }`}
}`} className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
}`}
> >
{e.status?.name || "Unknown"} {e.status?.name || "Unknown"}
</span> </span>
@ -162,27 +187,30 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
}, },
]; ];
if (isInitialLoading) return <ExpenseTableSkeleton />; if (isInitialLoading && !data) return <ExpenseTableSkeleton />;
if (isError) return <div>{error.message}</div>; if (isError) return <div>{error?.message}</div>;
const grouped = groupBy const grouped = groupBy
? groupByField(data?.data ?? [], groupBy) ? groupByField(data?.data ?? [], groupBy)
: { All: data?.data ?? [] }; : { All: data?.data ?? [] };
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy); const IsGroupedByDate = [
{ key: "transactionDate", displayField: "Transaction Date" },
{ key: "createdAt", displayField: "created Date" },
]?.includes(groupBy);
const canEditExpense = (expense) => { const canEditExpense = (expense) => {
return ( return (
(expense.status.id === EXPENSE_DRAFT || (expense?.status?.id === EXPENSE_DRAFT ||
EXPENSE_REJECTEDBY.includes(expense.status.id)) && EXPENSE_REJECTEDBY.includes(expense?.status?.id)) &&
expense.createdBy?.id === SelfId expense?.createdBy?.id === SelfId
); );
}; };
const canDetetExpense = (expense) => { const canDetetExpense = (expense) => {
return ( return (
expense.status.id === EXPENSE_DRAFT && expense.createdBy.id === SelfId expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId
); );
}; };
return ( return (
<> <>
{IsDeleteModalOpen && ( {IsDeleteModalOpen && (
@ -198,7 +226,14 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
/> />
)} )}
<div className="card px-0 px-sm-4"> <div className="card page-min-h px-sm-4">
{/* Filter Chips */}
<ExpenseFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={removeFilterChip}
groupBy={groupBy}
/>
<div <div
className="card-datatable table-responsive " className="card-datatable table-responsive "
id="horizontal-example" id="horizontal-example"
@ -226,18 +261,24 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
</thead> </thead>
<tbody> <tbody>
{Object.keys(grouped).length > 0 ? ( {Object.keys(grouped).length > 0 ? (
Object.entries(grouped).map(([group, expenses]) => ( Object.values(grouped).map(({ key, displayField, items }) => (
<React.Fragment key={group}> <React.Fragment key={key}>
<tr className="tr-group text-dark"> <tr className="tr-group text-dark">
<td colSpan={8} className="text-start"> <td colSpan={8} className="text-start">
<strong> <div className="d-flex align-items-center">
{IsGroupedByDate {" "}
? formatUTCToLocalTime(group) <small className="fs-6 py-1">
: group} {displayField} :{" "}
</strong> </small>{" "}
<small className="fs-6 ms-3">
{IsGroupedByDate
? formatUTCToLocalTime(key)
: key}
</small>
</div>
</td> </td>
</tr> </tr>
{expenses.map((expense) => ( {items?.map((expense) => (
<tr key={expense.id}> <tr key={expense.id}>
{expenseColumns.map( {expenseColumns.map(
(col) => (col) =>
@ -263,27 +304,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
}) })
} }
></i> ></i>
{canEditExpense(expense) && ( {canDetetExpense(expense) &&
<i canEditExpense(expense) && (
className="bx bx-edit text-secondary cursor-pointer" <div className="dropdown z-2">
onClick={() => <button
setManageExpenseModal({ type="button"
IsOpen: true, className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
expenseId: expense.id, data-bs-toggle="dropdown"
}) aria-expanded="false"
} >
></i> <i
)} className="bx bx-dots-vertical-rounded text-muted p-0"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
{canDetetExpense(expense) && (
<li
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: expense.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
<span className="align-left ">
Modify
</span>
</a>
</li>
)}
{canDetetExpense(expense) && ( {canDetetExpense(expense) && (
<i <li
className="bx bx-trash text-danger cursor-pointer" onClick={() => {
onClick={() => { setIsDeleteModalOpen(true);
setIsDeleteModalOpen(true); setDeletingId(expense.id);
setDeletingId(expense.id); }}
}} >
></i> <a className="dropdown-item px-2 cursor-pointer py-1">
)} <i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left">
Delete
</span>
</a>
</li>
)}
</ul>
</div>
)}
</div> </div>
</td> </td>
</tr> </tr>
@ -292,8 +367,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan={8} className="text-center py-4"> <td colSpan={8} className="text-center border-0 ">
No Expense Found <div className="py-8">
<p>No Expense Found</p>
</div>
</td> </td>
</tr> </tr>
)} )}

View File

@ -1,54 +1,137 @@
import { useState } from "react"; import { useState, useRef ,useEffect} from "react";
const PreviewDocument = ({ imageUrl }) => { const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [rotation, setRotation] = useState(0); const [rotation, setRotation] = useState(0);
const [zoom, setZoom] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const containerRef = useRef(null);
// Zoom handlers
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.2, 3));
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.2, 0.5));
// Mouse wheel zoom
const handleWheel = (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setZoom((prev) => Math.min(Math.max(prev + delta, 0.5), 3));
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener("wheel", handleWheel, { passive: false });
return () => {
container.removeEventListener("wheel", handleWheel);
};
}, []);
const handleMouseDown = (e) => {
if (zoom <= 1) return;
setIsDragging(true);
setStartPos({
x: e.clientX - position.x,
y: e.clientY - position.y,
});
};
const handleMouseMove = (e) => {
if (!isDragging) return;
setPosition({
x: e.clientX - startPos.x,
y: e.clientY - startPos.y,
});
};
const handleMouseUp = () => setIsDragging(false);
const handleMouseLeave = () => setIsDragging(false);
const handleReset = () => {
setRotation(0);
setZoom(1);
setPosition({ x: 0, y: 0 });
};
return ( return (
<> <>
<div className="d-flex justify-content-start"> <div className="d-flex justify-content-start align-items-center gap-3 mb-2 px-3 py-2 px-md-0 py-md-0">
<i <i
className="bx bx-rotate-right cursor-pointer" className="bx bx-rotate-right fs-4 cursor-pointer"
title="Rotate Right"
onClick={() => setRotation((prev) => prev + 90)} onClick={() => setRotation((prev) => prev + 90)}
></i> ></i>
<i
className="bx bx-zoom-in fs-4 cursor-pointer"
title="Zoom In"
onClick={handleZoomIn}
></i>
<i
className="bx bx-zoom-out fs-4 cursor-pointer"
title="Zoom Out"
onClick={handleZoomOut}
></i>
<i
className="bx bx-reset fs-4 cursor-pointer"
title="Reset"
onClick={handleReset}
></i>
</div> </div>
<div
className="d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: "60%" }}
>
{loading && (
<div className="text-secondary text-center mb-2">Loading...</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center"> <div
ref={containerRef}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
className="d-flex justify-content-center align-items-center overflow-hidden border rounded "
style={{
width: "100%",
height: "80vh",
background: "#f8f9fa",
cursor: zoom > 1 ? (isDragging ? "grabbing" : "grab") : "default",
userSelect: "none",
position: "relative",
}}
>
{loading && (
<div className="text-secondary text-center position-absolute">
Loading...
</div>
)}
<img <img
src={imageUrl} src={imageUrl}
alt="Full View" alt="Preview"
className="img-fluid"
style={{
maxHeight: "80vh",
objectFit: "contain",
display: loading ? "none" : "block",
transform: `rotate(${rotation}deg)`,
transition: "transform 0.3s ease",
}}
onLoad={() => setLoading(false)} onLoad={() => setLoading(false)}
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${zoom})`,
transition: isDragging ? "none" : "transform 0.3s ease",
objectFit: "contain",
maxWidth: "100%",
maxHeight: "100%",
display: loading ? "none" : "block",
pointerEvents: "none",
}}
/> />
</div> </div>
<div className="d-flex justify-content-center gap-2"> {/* <div className="d-flex justify-content-center gap-2 mt-2">
<button <button
className="btn btn-outline-secondary" className="btn btn-sm btn-outline-secondary"
onClick={() => setRotation(0)} onClick={handleReset}
title="Reset Rotation" title="Reset View"
> >
<i className="bx bx-reset"></i> Reset <i className="bx bx-reset"></i> Reset View
</button> </button>
</div> </div> */}
</div> </>
</>
); );
}; };
export default PreviewDocument; export default PreviewDocument;

View File

@ -9,7 +9,11 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema"; import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage"; import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { getColorNameFromHex, getIconByFileType, localToUtc } from "../../utils/appUtils"; import {
getColorNameFromHex,
getIconByFileType,
localToUtc,
} from "../../utils/appUtils";
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton"; import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { import {
@ -301,15 +305,15 @@ const ViewExpense = ({ ExpenseId }) => {
<div className="d-flex flex-wrap gap-2"> <div className="d-flex flex-wrap gap-2">
{data?.documents?.map((doc) => { {data?.documents?.map((doc) => {
const isImage = doc.contentType?.includes("image"); const isImage = doc.contentType?.startsWith("image");
return ( return (
<div <div
key={doc.documentId} key={doc.documentId}
className="border rounded hover-scale p-2 d-flex flex-column align-items-center" className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{ style={{
width: "80px", width: "80px",
cursor: isImage ? "pointer" : "default", cursor: "pointer",
}} }}
onClick={() => { onClick={() => {
if (isImage) { if (isImage) {
@ -317,6 +321,8 @@ const ViewExpense = ({ ExpenseId }) => {
IsOpen: true, IsOpen: true,
Image: doc.preSignedUrl, Image: doc.preSignedUrl,
}); });
} else {
window.open(doc.preSignedUrl, "_blank");
} }
}} }}
> >
@ -332,7 +338,7 @@ const ViewExpense = ({ ExpenseId }) => {
</small> </small>
</div> </div>
); );
})} }) ?? "No Attachment"}
</div> </div>
</div> </div>
@ -418,7 +424,9 @@ const ViewExpense = ({ ExpenseId }) => {
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) || {((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
(IsRejectedExpense && isCreatedBy)) && ( (IsRejectedExpense && isCreatedBy)) && (
<> <>
<Label className="form-label me-2 mb-0" required>Comment</Label> <Label className="form-label me-2 mb-0" required>
Comment
</Label>
<textarea <textarea
className="form-control form-control-sm" className="form-control form-control-sm"
{...register("comment")} {...register("comment")}
@ -440,7 +448,7 @@ const ViewExpense = ({ ExpenseId }) => {
key={status.id || index} key={status.id || index}
type="button" type="button"
onClick={() => { onClick={() => {
setClickedStatusId(status.id); setClickedStatusId(status.id);
setValue("statusId", status.id); setValue("statusId", status.id);
handleSubmit(onSubmit)(); handleSubmit(onSubmit)();
}} }}

View File

@ -15,7 +15,7 @@ const Sidebar = () => {
> >
<div className="app-brand" style={{ paddingLeft: "30px" }}> <div className="app-brand" style={{ paddingLeft: "30px" }}>
<Link to="/dashboard" className="app-brand-link"> <Link to="/dashboard" className="app-brand-link">
<span className="app-brand-logo rounded-circle app-brand-logo-border"> {/* <span className="app-brand-logo rounded-circle app-brand-logo-border">
<img <img
className="app-brand-logo-sidebar" className="app-brand-logo-sidebar"
src="/img/brand/marco.png" src="/img/brand/marco.png"
@ -23,8 +23,19 @@ const Sidebar = () => {
aria-label="logo image" aria-label="logo image"
style={{ margin: "5px", paddingRight: "5px" }} style={{ margin: "5px", paddingRight: "5px" }}
/> />
</span> </span> */}
<span className="app-brand-text menu-text fw-bold ms-2">PMS</span>
<a
href="/"
class="app-brand-link fw-bold navbar-brand text-green fs-6"
>
<span class="app-brand-logo demo">
<img src="/img/brand/marco.png" width="50" />
</span>
<span class="text-blue">OnField</span>
<span>Work</span>
<span class="text-dark">.com</span>
</a>
</Link> </Link>
<a className="layout-menu-toggle menu-link text-large ms-auto"> <a className="layout-menu-toggle menu-link text-large ms-auto">

View File

@ -12,13 +12,12 @@ import { spridSchema } from "./OrganizationSchema";
import { OrgCardSkeleton } from "./OrganizationSkeleton"; import { OrgCardSkeleton } from "./OrganizationSkeleton";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
// Zod schema: only allow exactly 4 digits // Zod schema: only allow exactly 4 digits
const OrgPickerFromSPId = ({ title, placeholder }) => { const OrgPickerFromSPId = ({ title, placeholder }) => {
const { onClose, startStep, flowType, onOpen, prevStep,orgData } = const { onClose, startStep, flowType, onOpen, prevStep, orgData } =
useOrganizationModal(); useOrganizationModal();
const clientQuery = useQueryClient() const clientQuery = useQueryClient();
const { const {
register, register,
handleSubmit, handleSubmit,
@ -37,33 +36,42 @@ const OrgPickerFromSPId = ({ title, placeholder }) => {
const onSubmit = (formdata) => { const onSubmit = (formdata) => {
setSPRID(formdata.spridSearchText); setSPRID(formdata.spridSearchText);
}; };
const handleCrateOrg = () => { const handleCrateOrg = () => {
clientQuery.removeQueries({queryKey:["organization"]}) clientQuery.removeQueries({ queryKey: ["organization"] });
onOpen({ startStep: 4,orgData:null }) onOpen({ startStep: 4, orgData: null });
}; };
const SP = watch("spridSearchText"); const SP = watch("spridSearchText");
return ( return (
<div className="d-block"> <div className="d-block mt-4">
<form <form onSubmit={handleSubmit(onSubmit)}>
className="d-flex flex-row gap-6 text-start align-items-center" <div className="row align-items-center g-2">
onSubmit={handleSubmit(onSubmit)} {/* Input Section */}
> <div className="col-12 col-md-8 d-block d-md-flex align-items-center gap-2 m-0 text-start">
<div className="d-flex flex-row align-items-center gap-2"> <Label className="text-nowrap mb-1 mb-md-0" required>
<Label className="text-secondary">Search by SPRID</Label> Search by SPRID
<input </Label>
type="search" <input
{...register("spridSearchText")} type="search"
className="form-control form-control-sm w-auto" {...register("spridSearchText")}
placeholder="Enter SPRID" className="form-control form-control-sm flex-grow-1"
maxLength={4} placeholder="Enter SPRID"
/> maxLength={4}
</div> />
</div>
<button type="submit" className="btn btn-sm btn-primary"> {/* Button Section */}
<i className="bx bx-sm bx-search-alt-2"></i> Search <div className="col-12 col-md-4 text-md-start text-center mt-2 mt-md-0">
</button> <button
type="submit"
className="btn btn-sm btn-primary w-100 w-md-auto"
>
<i className="bx bx-sm bx-search-alt-2"></i> Search
</button>
</div>
</div>
</form> </form>
<div className="text-start danger-text"> <div className="text-start danger-text">
{" "} {" "}
{errors.spridSearchText && ( {errors.spridSearchText && (
@ -124,7 +132,7 @@ const OrgPickerFromSPId = ({ title, placeholder }) => {
No organization found for "{SPRID}" No organization found for "{SPRID}"
</div> </div>
) : null} ) : null}
<div className="py-12 text-center text-tiny text-black"> <div className="py-2 text-center text-tiny text-black">
<small className="d-block text-secondary"> <small className="d-block text-secondary">
Do not have SPRID or could not find organization ? Do not have SPRID or could not find organization ?
</small> </small>

View File

@ -93,10 +93,12 @@ const OrganizationsList = ({searchText}) => {
if (isError) return <div>{error?.message || "Something went wrong"}</div>; if (isError) return <div>{error?.message || "Something went wrong"}</div>;
return ( return (
<div className="card px-0 px-sm-4 pb-12 pt-5"> <div
<div className="card-datatable table-responsive" id="horizontal-example"> className="card-datatable table-responsive overflow-auto"
<div className="dataTables_wrapper no-footer px-2"> id="horizontal-example"
<table className="table border-top dataTable text-nowrap"> >
<div className="dataTables_wrapper no-footer px-2 ">
<table className="table border-top dataTable text-nowrap">
<thead> <thead>
<tr className="table_header_border"> <tr className="table_header_border">
{organizationsColumns.map((col) => ( {organizationsColumns.map((col) => (
@ -131,7 +133,7 @@ const OrganizationsList = ({searchText}) => {
<div className="d-flex justify-content-center gap-2"> <div className="d-flex justify-content-center gap-2">
<i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i> <i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i>
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i> <i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
<i className="bx bx-trash text-danger cursor-pointer"></i> <i className="bx bx-trash text-danger cursor-not-allowed"></i>
</div> </div>
</td> </td>
</tr> </tr>
@ -157,7 +159,6 @@ const OrganizationsList = ({searchText}) => {
)} )}
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -22,9 +22,8 @@ const VieworgDataanization = ({ orgId }) => {
</div> </div>
<div className="text-end"> <div className="text-end">
<span <span
className={`badge bg-label-${ className={`badge bg-label-${data?.isActive ? "primary" : "secondary"
data?.isActive ? "primary" : "secondary" } `}
} `}
> >
{data?.isActive ? "Active" : "In-Active"}{" "} {data?.isActive ? "Active" : "In-Active"}{" "}
</span> </span>
@ -105,9 +104,101 @@ const VieworgDataanization = ({ orgId }) => {
<div className="text-muted text-start">{data?.address}</div> <div className="text-muted text-start">{data?.address}</div>
</div> </div>
</div> </div>
<div className="d-flex text-secondary mb-2"> <div className="col-12 mb-3">
{" "} <div
<i className="bx bx-sm bx-briefcase me-1" /> Projects And Services className="d-flex justify-content-between align-items-center text-secondary mb-2 cursor-pointer"
data-bs-toggle="collapse"
data-bs-target="#collapse-projects-services"
aria-expanded="false"
>
<div>
<i className="bx bx-sm bx-briefcase me-1" /> Projects
</div>
<i className="bx bx-chevron-down me-2"></i>
</div>
{/* remove "show" from className */}
<div id="collapse-projects-services" className="collapse">
{data?.projects && data.projects.length > 0 ? (
data.projects
.reduce((acc, curr) => {
const projectId = curr.project.id;
if (!acc.find((p) => p.id === projectId)) {
acc.push(curr.project);
}
return acc;
}, [])
.map((project) => (
<div key={project.id} className="mb-2 rounded p-2">
<div
className="d-flex justify-content-between align-items-center cursor-pointer"
data-bs-toggle="collapse"
data-bs-target={`#collapse-${project.id}`}
aria-expanded="false"
>
<label className="form-label fw-semibold">
<i className="bx bx-buildings me-2"></i>
{project.name}
</label>
<i className="bx bx-chevron-down"></i>
</div>
<div id={`collapse-${project.id}`} className="collapse mt-2 ps-5">
{data.projects
.filter((p) => p.project.id === project.id)
.map((p) => (
<div key={p.service.id} className="mb-1 text-muted">
<i className="bx bx-wrench me-2"></i>
{p.service.name}
</div>
))}
</div>
</div>
))
) : (
<div className="text-muted fst-italic ps-2">No projects available</div>
)}
</div>
</div>
{/* Services Section */}
<div className="col-12 mb-3">
<div
className="d-flex justify-content-between align-items-center text-secondary mb-2 cursor-pointer"
data-bs-toggle="collapse"
data-bs-target="#collapse-services"
aria-expanded="false"
>
<div>
<i className="bx bx-sm bx-cog me-1" /> Services
</div>
<i className="bx bx-chevron-down me-2"></i>
</div>
{/* collapse is closed initially */}
<div id="collapse-services" className="collapse">
{data?.services && data.services.length > 0 ? (
<div className="row">
{data.services.map((service) => (
<div key={service.id} className="col-md-12 mb-3">
<div className="card h-100 shadow-sm border-0">
<div className="card-body">
<h6 className="fw-semibold mb-1">
<i className="bx bx-wrench me-1"></i>
{service.name}
</h6>
<p className="text-muted small mb-0">
{service.description || "No description available."}
</p>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-muted fst-italic ps-2">No services available</div>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -104,7 +104,7 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
</span> </span>
</div> </div>
<div className="col-2"> <div className="col-6 col-md-2">
<ProgressBar <ProgressBar
completedWork={formatNumber(workArea?.completedWork)} completedWork={formatNumber(workArea?.completedWork)}
plannedWork={formatNumber(workArea?.plannedWork)} plannedWork={formatNumber(workArea?.plannedWork)}

View File

@ -40,31 +40,32 @@ const ProjectNav = ({ onPillClick, activePill }) => {
label: "Directory", label: "Directory",
hidden: !(DirAdmin || DireManager || DirUser), hidden: !(DirAdmin || DireManager || DirUser),
}, },
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) }, { key: "documents", icon: "bx bx-folder-open", label: "Documents", hidden: !(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"}, { key: "organization", icon: "bx bx-buildings", label: "Organization" },
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam }, { key: "setting", icon: "bx bxs-cog", label: "Setting", hidden: !isManageTeam },
]; ];
return ( return (
<div className="nav-align-top"> <div className="table-responsive">
<ul className="nav nav-tabs"> <div className="nav-align-top">
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => ( <ul className="nav nav-tabs">
<li key={tab.key} className="nav-item cursor-pointer"> {ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
<a <li key={tab.key} className="nav-item cursor-pointer">
<a
className={`nav-link ${
activePill === tab.key ? "active cursor-pointer" : "" className={`nav-link ${activePill === tab.key ? "active cursor-pointer" : ""
} fs-6`} } fs-6`}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
onPillClick(tab.key); onPillClick(tab.key);
}} }}
> >
<i className={`${tab.icon} bx-sm me-1_5`}></i> <i className={`${tab.icon} bx-sm me-1_5`}></i>
<span className="d-none d-md-inline ">{tab.label}</span> <span className="d-none d-md-inline ">{tab.label}</span>
</a> </a>
</li> </li>
))} ))}
</ul> </ul>
</div>
</div> </div>
); );
}; };

View File

@ -201,7 +201,7 @@ const Teams = () => {
className="form-check-label ms-2" className="form-check-label ms-2"
htmlFor="activeEmployeeSwitch" htmlFor="activeEmployeeSwitch"
> >
{activeEmployee ? "Active Employees" : "Include Inactive Employees"} {activeEmployee ? "Active Employees" : "In-active Employees"}
</label> </label>
</div> </div>
</div> </div>

View File

@ -136,7 +136,7 @@ const TenantsList = ({
); );
return ( return (
<> <>
<div className="p-2 mt-3"> <div className=" mt-3">
<div className=" text-nowrap table-responsive"> <div className=" text-nowrap table-responsive">
<table className="table border-top dataTable text-nowrap"> <table className="table border-top dataTable text-nowrap">
<thead> <thead>

View File

@ -151,7 +151,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
if (isError) return <p>{error.message}</p>; if (isError) return <p>{error.message}</p>;
return ( return (
<div className="card "> <div className="card px-sm-4 px-0">
<div <div
className="card-datatable table-responsive page-min-h" className="card-datatable table-responsive page-min-h"
id="horizontal-example" id="horizontal-example"

View File

@ -172,15 +172,15 @@ const ViewCollection = ({ onClose }) => {
<div className="d-flex flex-wrap gap-2"> <div className="d-flex flex-wrap gap-2">
{data?.attachments?.map((doc) => { {data?.attachments?.map((doc) => {
const isImage = doc.contentType?.includes("image"); const isImage = doc.contentType?.startsWith("image");
return ( return (
<div <div
key={doc.documentId} key={doc.documentId}
className="border rounded hover-scale p-2 d-flex flex-column align-items-center" className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{ style={{
width: "80px", width: "80px",
cursor: isImage ? "pointer" : "default", cursor: "pointer",
}} }}
onClick={() => { onClick={() => {
if (isImage) { if (isImage) {
@ -188,6 +188,8 @@ const ViewCollection = ({ onClose }) => {
IsOpen: true, IsOpen: true,
Image: doc.preSignedUrl, Image: doc.preSignedUrl,
}); });
} else {
window.open(doc.preSignedUrl, "_blank");
} }
}} }}
> >

View File

@ -73,7 +73,7 @@ const DateRangePicker = ({
/> />
<i <i
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 end-0 translate-middle-y me-2 " className="bx bx-calendar calendar-icon cursor-pointer position-absolute ms-n6 top-50 end-30 translate-middle-y me-2 "
onClick={handleIconClick} onClick={handleIconClick}
/> />
</div> </div>

View File

@ -37,13 +37,13 @@ const GalleryFilterPanel = ({ onApply }) => {
startDate: localToUtc(formData.startDate), startDate: localToUtc(formData.startDate),
endDate: localToUtc(formData.endDate), endDate: localToUtc(formData.endDate),
}); });
closePanel() // closePanel()
}; };
const onClear=()=>{ const onClear=()=>{
reset(defaultGalleryFilterValue); reset(defaultGalleryFilterValue);
setResetKey((prev) => prev + 1); setResetKey((prev) => prev + 1);
closePanel() // closePanel()
} }
if (isLoading) return <div>Loading....</div>; if (isLoading) return <div>Loading....</div>;

View File

@ -28,13 +28,21 @@ const ImageGalleryListView = ({filter}) => {
} }
}; };
if (!data?.data?.length && !isLoading) { if (!data?.data?.length && !isLoading) {
return ( return (
<p className="text-center text-muted mt-5"> <div
{selectedProject ? " No images match the selected filters.":"Please Select Project!"} className="d-flex justify-content-center align-items-center text-muted"
</p> style={{ minHeight: "50vh" }}
); >
} <span style={{ fontSize: "0.9rem" }}>
{selectedProject
? "No images match the selected filters."
: "Please Select Project!"}
</span>
</div>
);
}
if (isLoading) { if (isLoading) {
return ( return (

View File

@ -4,7 +4,6 @@ import { formatUTCToLocalTime } from "../../utils/dateUtils";
const ViewGallery = ({ batch, index }) => { const ViewGallery = ({ batch, index }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentIndex, setCurrentIndex] = useState(index); const [currentIndex, setCurrentIndex] = useState(index);
console.log(batch);
useEffect(() => { useEffect(() => {
setCurrentIndex(index); setCurrentIndex(index);
}, [index, batch]); }, [index, batch]);

View File

@ -200,35 +200,35 @@ export const useAttendanceOverviewData = (projectId, days) => {
// }) // })
// } // }
export const useDashboard_AttendanceData = (date,projectId)=>{ export const useDashboard_AttendanceData = (date, projectId) => {
return useQuery({ return useQuery({
queryKey:["dashboardAttendances",date,projectId], queryKey: ["dashboardAttendances", date, projectId],
queryFn:async()=> { queryFn: async () => {
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId) const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
return resp.data; return resp.data;
} }
}) })
} }
export const useDashboardTeamsCardData =(projectId)=>{ export const useDashboardTeamsCardData = (projectId) => {
return useQuery({ return useQuery({
queryKey:["dashboardTeams",projectId], queryKey: ["dashboardTeams", projectId],
queryFn:async()=> { queryFn: async () => {
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId) const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
return resp.data; return resp.data;
} }
}) })
} }
export const useDashboardTasksCardData = (projectId) => { export const useDashboardTasksCardData = (projectId) => {
return useQuery({ return useQuery({
queryKey:["dashboardTasks",projectId], queryKey: ["dashboardTasks", projectId],
queryFn:async()=> { queryFn: async () => {
const resp = await GlobalRepository.getDashboardTasksCardData(projectId) const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
return resp.data; return resp.data;
} }
}) })
} }
@ -236,7 +236,7 @@ export const useDashboardTasksCardData = (projectId) => {
// return useQuery({ // return useQuery({
// queryKey:["dashboardAttendanceOverView",projectId], // queryKey:["dashboardAttendanceOverView",projectId],
// queryFn:async()=> { // queryFn:async()=> {
// const resp = await GlobalRepository.getAttendanceOverview(projectId, days); // const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
// return resp.data; // return resp.data;
// } // }
@ -244,12 +244,53 @@ export const useDashboardTasksCardData = (projectId) => {
// } // }
export const useDashboardProjectsCardData = () => { export const useDashboardProjectsCardData = () => {
return useQuery({ return useQuery({
queryKey:["dashboardProjects"], queryKey: ["dashboardProjects"],
queryFn:async()=> { queryFn: async () => {
const resp = await GlobalRepository.getDashboardProjectsCardData(); const resp = await GlobalRepository.getDashboardProjectsCardData();
return resp.data; return resp.data;
} }
}) })
} }
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
const hasBothDates = !!startDate && !!endDate;
const noDatesSelected = !startDate && !endDate;
const shouldFetch =
noDatesSelected ||
hasBothDates;
return useQuery({
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
return resp.data;
},
enabled: shouldFetch,
refetchOnWindowFocus: true, // refetch when you come back
refetchOnMount: "always", // always refetch on remount
staleTime: 0,
});
};
export const useExpenseStatus = (projectId) => {
return useQuery({
queryKey: ["expense_stauts", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseStatus(projectId);
return resp.data;
}
})
}
export const useExpenseDataByProject = (projectId, categoryId, months) => {
return useQuery({
queryKey: ["expenseByProject", projectId, categoryId, months],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
return resp.data;
},
});
};

View File

@ -23,13 +23,13 @@ const AuthLayout = () => {
to="/" to="/"
className="app-brand-link gap-2 position-fixed top-2 start-0 mx-2 mx-sm-4" className="app-brand-link gap-2 position-fixed top-2 start-0 mx-2 mx-sm-4"
> >
<span className="app-brand-logo rounded-circle "> {/* <span className="app-brand-logo rounded-circle ">
<img <img
src="/img/brand/marco.png" src="/img/brand/marco.png"
alt="marco-logo" alt="marco-logo"
className="app-brand-logo-login" className="app-brand-logo-login"
/> />
</span> </span> */}
</Link> </Link>
<Outlet /> <Outlet />
</div> </div>

View File

@ -179,44 +179,40 @@ const AttendancePage = () => {
</div> </div>
{/* Search + Organization filter */} {/* Search + Organization filter */}
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto d-flex gap-2 align-items-center"> <div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto">
{/* Organization Dropdown */} <div className="row g-2">
<div className="row"> <div className="col-12 col-sm-6">
<div className="col-12 col-sm-6 mb-2 mb-sm-0"> <select
<select className="form-select form-select-sm"
className="form-select form-select-sm" value={appliedFilters.selectedOrganization}
value={appliedFilters.selectedOrganization} onChange={(e) =>
onChange={(e) => setAppliedFilters((prev) => ({
setAppliedFilters((prev) => ({ ...prev,
...prev, selectedOrganization: e.target.value,
selectedOrganization: e.target.value, }))
})) }
} disabled={orgLoading}
disabled={orgLoading} >
> <option value="">All Organizations</option>
<option value="">All Organizations</option> {organizations?.map((org, ind) => (
{organizations?.map((org, ind) => ( <option key={`${org.id}-${ind}`} value={org.id}>
<option key={`${org.id}-${ind}`} value={org.id}> {org.name}
{org.name} </option>
</option> ))}
))} </select>
</select>
</div> </div>
<div className="col-12 col-sm-6"> <div className="col-12 col-sm-6">
{/* Search Input */} <input
<input type="text"
type="text" className="form-control form-control-sm"
className="form-control form-control-sm" placeholder="Search Employee..."
placeholder="Search Employee..." value={searchTerm}
value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)}
onChange={(e) => setSearchTerm(e.target.value)} />
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -224,7 +220,7 @@ const AttendancePage = () => {
{selectedProject ? ( {selectedProject ? (
<> <>
{activeTab === "all" && ( {activeTab === "all" && (
<div className="tab-pane fade show active py-0 mx-5"> <div className="tab-pane fade show active py-0 mx-2">
<Attendance <Attendance
handleModalData={handleModalData} handleModalData={handleModalData}
getRole={getRole} getRole={getRole}
@ -234,7 +230,7 @@ const AttendancePage = () => {
</div> </div>
)} )}
{activeTab === "logs" && ( {activeTab === "logs" && (
<div className="tab-pane fade show active py-0"> <div className="tab-pane fade p-3 show active py-0">
<AttendanceLog <AttendanceLog
handleModalData={handleModalData} handleModalData={handleModalData}
searchTerm={searchTerm} searchTerm={searchTerm}
@ -243,7 +239,7 @@ const AttendancePage = () => {
</div> </div>
)} )}
{activeTab === "regularization" && DoRegularized && ( {activeTab === "regularization" && DoRegularized && (
<div className="tab-pane fade show active py-0"> <div className="tab-pane fade p-3 show active py-0">
<Regularization <Regularization
searchTerm={searchTerm} searchTerm={searchTerm}
organizationId={appliedFilters.selectedOrganization} organizationId={appliedFilters.selectedOrganization}

View File

@ -38,7 +38,7 @@ const TaskPlanning = () => {
]} ]}
/> />
<div className="card py-2 "> <div className="card py-2 page-min-h">
<div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start"> <div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start">
{data?.length === 0 ? ( {data?.length === 0 ? (
<p className="badge bg-label-secondary m-0">Service not assigned</p> <p className="badge bg-label-secondary m-0">Service not assigned</p>

View File

@ -1,5 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React from "react"; import React, {
useEffect,
useImperativeHandle,
forwardRef,
useMemo,
} from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { import {
contactsFilter, contactsFilter,
@ -8,70 +13,109 @@ import {
import { useContactFilter } from "../../hooks/useDirectory"; import { useContactFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple"; import SelectMultiple from "../../components/common/SelectMultiple";
import { useParams } from "react-router-dom";
const ContactFilterPanel = ({ onApply, clearFilter }) => { const ContactFilterPanel = forwardRef(
const { data, isError, isLoading, error, isFetched, isFetching } = ({ onApply, clearFilter, setFilterdata }, ref) => {
useContactFilter(); const { data, isError, isLoading, error, isFetched, isFetching } =
useContactFilter();
const { status } = useParams();
const methods = useForm({ useEffect(() => {
resolver: zodResolver(contactsFilter), return () => {
defaultValues: defaultContactFilter, closePanel();
}); };
}, []);
const closePanel = () => { const dynamicdefaultContactFilter = useMemo(() => {
document.querySelector(".offcanvas.show .btn-close")?.click(); return {
}; ...defaultContactFilter,
bucketIds: defaultContactFilter.bucketIds || [],
categoryIds: defaultContactFilter.categoryIds || [],
};
}, [status]);
const { register, handleSubmit, reset, watch } = methods; const methods = useForm({
resolver: zodResolver(contactsFilter),
defaultValues: dynamicdefaultContactFilter,
});
const onSubmit = (formData) => { const { handleSubmit, reset, setValue, getValues } = methods;
onApply(formData);
closePanel();
};
const handleClose = () => { useImperativeHandle(ref, () => ({
reset(defaultContactFilter); resetFieldValue: (name, value) => {
onApply(defaultContactFilter); setTimeout(() => {
closePanel(); if (value !== undefined) {
}; setValue(name, value);
} else {
reset({ ...getValues(), [name]: defaultContactFilter[name] });
}
}, 0);
},
getValues,
}));
if (isLoading || isFetching) return <ExpenseFilterSkeleton />; useEffect(() => {
if (isError && isFetched) if (data && setFilterdata) {
return <div>Something went wrong Here- {error.message} </div>; setFilterdata(data);
return ( }
<FormProvider {...methods}> }, [data, setFilterdata]);
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="bucketIds"
label="Buckets :"
options={data.buckets}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="categoryIds"
label="Contact Category :"
options={data.contactCategories}
labelKey={(item) => item.name}
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default ContactFilterPanel; const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const onSubmit = (formData) => {
onApply(formData);
// closePanel();
};
const handleClose = () => {
reset(defaultContactFilter);
onApply(defaultContactFilter);
// closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong {error?.message}</div>;
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="bucketIds"
label="Buckets:"
options={data?.buckets || []}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="categoryIds"
label="Contact Category:"
options={data?.contactCategories || []}
labelKey={(item) => item.name}
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm" >
Apply
</button>
</div>
</form>
</FormProvider>
);
}
);
export default ContactFilterPanel;

View File

@ -1,26 +1,30 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { useContactList } from "../../hooks/useDirectory"; import { useContactList } from "../../hooks/useDirectory";
import { useDirectoryContext } from "./DirectoryPage"; import { useDirectoryContext } from "./DirectoryPage";
import CardViewContact from "../../components/Directory/CardViewContact"; import CardViewContact from "../../components/Directory/CardViewContact";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import ContactFilterPanel from "./ContactFilterPanel"; import ContactFilterPanel from "./ContactFilterPanel";
import ContactFilterChips from "../../components/Directory/ContactFilterChips";
import { defaultContactFilter } from "../../components/Directory/DirectorySchema"; import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
import { useDebounce } from "../../utils/appUtils"; import { useDebounce } from "../../utils/appUtils";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import ListViewContact from "../../components/Directory/ListViewContact"; import ListViewContact from "../../components/Directory/ListViewContact";
import { CardViewContactSkeleton, ListViewContactSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import Loader from "../../components/common/Loader";
// Utility function to format contacts for CSV export // Utility for CSV export
const formatExportData = (contacts) => { const formatExportData = (contacts) => {
return contacts.map(contact => ({ return contacts.map((contact) => ({
Email: contact.contactEmails?.map(e => e.emailAddress).join(", ") || "", Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "",
Phone: contact.contactPhones?.map(p => p.phoneNumber).join(", ") || "", Phone: contact.contactPhones?.map((p) => p.phoneNumber).join(", ") || "",
Created: contact.createdAt ? new Date(contact.createdAt).toLocaleString() : "", Created: contact.createdAt
? new Date(contact.createdAt).toLocaleString()
: "",
Location: contact.address || "", Location: contact.address || "",
Organization: contact.organization || "", Organization: contact.organization || "",
Category: contact.contactCategory?.name || "", Category: contact.contactCategory?.name || "",
Tags: contact.tags?.map(t => t.name).join(", ") || "", Tags: contact.tags?.map((t) => t.name).join(", ") || "",
Buckets: contact.bucketIds?.join(", ") || "", Buckets: contact.bucketIds?.join(", ") || "",
})); }));
}; };
@ -28,8 +32,10 @@ const formatExportData = (contacts) => {
const ContactsPage = ({ projectId, searchText, onExport }) => { const ContactsPage = ({ projectId, searchText, onExport }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilter] = useState(defaultContactFilter); const [filters, setFilter] = useState(defaultContactFilter);
const [filterData, setFilterdata] = useState(null);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const { showActive, gridView } = useDirectoryContext(); const { showActive, gridView } = useDirectoryContext();
const updatedRef = useRef();
const { data, isError, isLoading, error } = useContactList( const { data, isError, isLoading, error } = useContactList(
showActive, showActive,
projectId, projectId,
@ -40,13 +46,19 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
); );
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
// clear filters
const clearFilter = () => setFilter(defaultContactFilter); const clearFilter = () => setFilter(defaultContactFilter);
useEffect(() => { useEffect(() => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
"Contacts Filters", "Contacts Filters",
<ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} /> <ContactFilterPanel
ref={updatedRef}
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
); );
return () => { return () => {
@ -55,7 +67,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
}; };
}, []); }, []);
// 🔹 Format contacts for export // export data
useEffect(() => { useEffect(() => {
if (data?.data && onExport) { if (data?.data && onExport) {
onExport(formatExportData(data.data)); onExport(formatExportData(data.data));
@ -68,15 +80,54 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
} }
}; };
const handleRemoveChip = (key, id) => {
setFilter((prev) => {
const updated = { ...prev };
if (Array.isArray(updated[key])) {
updated[key] = updated[key].filter((v) => v !== id);
updatedRef.current?.resetFieldValue(key, updated[key]);
} else {
updated[key] = null;
updatedRef.current?.resetFieldValue(key, null);
}
return updated;
});
};
if (isError) return <div>{error.message}</div>; if (isError) return <div>{error.message}</div>;
// if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return ( return (
<div className="row mt-5"> <div className="row mt-4">
{/* Chips Section */}
<div className="col-12 mb-2">
<ContactFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={handleRemoveChip}
clearFilter={clearFilter}
/>
</div>
{/* Grid / List View */}
{gridView ? ( {gridView ? (
<> <>
{isLoading && <Loader />}
{data?.data?.length === 0 && (
<div className="py-4 text-center">
{searchText
? `No contact found for "${searchText}"`
: "No contacts found"}
</div>
)}
{data?.data?.map((contact) => ( {data?.data?.map((contact) => (
<div key={contact.id} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"> <div
key={contact.id}
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
>
<CardViewContact IsActive={showActive} contact={contact} /> <CardViewContact IsActive={showActive} contact={contact} />
</div> </div>
))} ))}
@ -95,6 +146,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
<div className="col-12"> <div className="col-12">
<ListViewContact <ListViewContact
data={data?.data} data={data?.data}
isLoading={isLoading}
Pagination={ Pagination={
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
@ -109,4 +161,4 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
); );
}; };
export default ContactsPage; export default ContactsPage;

View File

@ -139,9 +139,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
<li className="nav-item cursor-pointer"> <li className="nav-item cursor-pointer">
<a <a
className={`nav-link ${ className={`nav-link ${activeTab === "notes" ? "active" : ""
activeTab === "notes" ? "active" : "" } fs-6`}
} fs-6`}
onClick={(e) => handleTabClick("notes", e)} onClick={(e) => handleTabClick("notes", e)}
> >
<i className="bx bx-notepad bx-sm me-1_5"></i> <i className="bx bx-notepad bx-sm me-1_5"></i>
@ -150,9 +149,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</li> </li>
<li className="nav-item cursor-pointer"> <li className="nav-item cursor-pointer">
<a <a
className={`nav-link ${ className={`nav-link ${activeTab === "contacts" ? "active" : ""
activeTab === "contacts" ? "active" : "" } fs-6`}
} fs-6`}
onClick={(e) => handleTabClick("contacts", e)} onClick={(e) => handleTabClick("contacts", e)}
> >
<i className="bx bxs-contact bx-sm me-1_5"></i> <i className="bx bxs-contact bx-sm me-1_5"></i>
@ -168,105 +166,84 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
{activeTab === "notes" && ( {activeTab === "notes" && (
<div className="col-8 col-md-3"> <div className="col-8 col-md-3">
<input <input
type="search" type="search"
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Search notes..." placeholder="Search notes..."
value={searchNote} value={searchNote}
onChange={(e) => setSearchNote(e.target.value)} onChange={(e) => setSearchNote(e.target.value)}
/> />
</div> </div>
)} )}
{activeTab === "contacts" && ( {activeTab === "contacts" && (
<div className="d-flex align-items-center gap-3"> <div className="d-flex align-items-center gap-3">
<div className="col-12 col-md-8 d-flex flex-row gap-2"> <div className="col-12 col-md-8 d-flex flex-row gap-2">
<div className="col-7 col-md-4"> <div className="col-7 col-md-4">
<input <input
type="search" type="search"
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Search contacts..." placeholder="Search contacts..."
value={searchContact} value={searchContact}
onChange={(e) => setsearchContact(e.target.value)} onChange={(e) => setsearchContact(e.target.value)}
/> />
</div> </div>
<button <button
className={`btn btn-sm p-1 ${ className={`btn btn-sm p-1 ${gridView ? " btn-primary" : " btn-outline-primary"
!gridView ? "btn-primary" : "btn-outline-primary" }`}
}`}
onClick={() => setGridView(false)}
>
<i className="bx bx-list-ul"></i>
</button>
<button
className={`btn btn-sm p-1 ${
gridView ? " btn-primary" : " btn-outline-primary"
}`}
onClick={() => setGridView(true)} onClick={() => setGridView(true)}
> >
<i className="bx bx-grid-alt"></i> <i className="bx bx-grid-alt"></i>
</button> </button>
<div className="form-check form-switch d-flex align-items-center d-none d-md-flex"> <button
<input className={`btn btn-sm p-1 ${!gridView ? "btn-primary" : "btn-outline-primary"
type="checkbox" }`}
className="form-check-input" onClick={() => setGridView(false)}
role="switch"
id="inactiveEmployeesCheckbox"
checked={showActive}
onChange={(e) => setShowActive(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
> >
{showActive ? "Active" : "Inactive"} Contacts <i className="bx bx-list-ul"></i>
</label> </button>
<div className="form-check form-switch d-flex align-items-end d-none d-md-flex">
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
checked={showActive}
onChange={(e) => setShowActive(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
>
{showActive ? "Active" : "In-active"} Contacts
</label>
</div>
</div> </div>
</div>
</div> </div>
)} )}
</div> </div>
<div className="col-12 col-md-2 d-flex justify-content-end align-items-center gap-2"> <div className="col-12 col-md-2 d-flex justify-content-end align-items-center gap-2">
<div className={`form-check form-switch d-flex align-items-center ${activeTab === "contacts" ? " d-flex d-md-none m-0":"d-none" }`}> <div className=" btn-group">
<input <button
type="checkbox" className="btn btn-sm btn-label-secondary dropdown-toggle"
className="form-check-input" type="button"
role="switch" data-bs-toggle="dropdown"
id="inactiveEmployeesCheckbox" aria-expanded="false"
checked={showActive} >
onChange={(e) => setShowActive(e.target.checked)} <i className="bx bx-export me-2 bx-sm"></i>Export
/> </button>
<label <ul className="dropdown-menu">
className="form-check-label ms-2" <li>
htmlFor="inactiveEmployeesCheckbox" <a
className="dropdown-item cursor-pointer"
onClick={() => handleExport("csv")}
> >
{showActive ? "Active" : "Inactive"} Contacts <i className="bx bx-file me-1"></i> CSV
</label> </a>
</div> </li>
<div className=" btn-group"> </ul>
<button </div>
className="btn btn-sm btn-label-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-export me-2 bx-sm"></i>Export
</button>
<ul className="dropdown-menu">
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-1"></i> CSV
</a>
</li>
</ul>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,77 +1,117 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React from "react"; import React, { useEffect, useImperativeHandle, forwardRef, useMemo } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { import {
defaultNotesFilter, defaultNotesFilter,
notesFilter, notesFilter,
} from "../../components/Directory/DirectorySchema"; } from "../../components/Directory/DirectorySchema";
import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory"; import { useNoteFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton"; import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple"; import SelectMultiple from "../../components/common/SelectMultiple";
const NoteFilterPanel = ({ onApply, clearFilter }) => { const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref) => {
const { data, isError, isLoading, error, isFetched, isFetching } = const { data, isError, isLoading, error, isFetched, isFetching } = useNoteFilter();
useNoteFilter();
useEffect(() => {
return () => {
closePanel();
};
}, []);
//Add this for Filter chip remover
const dynamicdefaultNotesFilter = useMemo(() => {
return {
...defaultNotesFilter,
bucketIds: defaultNotesFilter.bucketIds || [],
categoryIds: defaultNotesFilter.categoryIds || [],
};
}, [status]);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(notesFilter), resolver: zodResolver(notesFilter),
defaultValues: defaultNotesFilter, defaultValues: dynamicdefaultNotesFilter,
}); });
const { handleSubmit, reset, setValue, getValues } = methods;
const closePanel = () => { const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click(); document.querySelector(".offcanvas.show .btn-close")?.click();
}; };
const { register, handleSubmit, reset, watch } = methods;
const onSubmit = (formData) => { const onSubmit = (formData) => {
onApply(formData); onApply(formData);
closePanel(); // closePanel();
}; };
const handleClose = () => { const handleClose = () => {
reset(defaultNotesFilter); reset(defaultNotesFilter);
onApply(defaultNotesFilter); onApply(defaultNotesFilter);
closePanel(); // closePanel();
}; };
if (isLoading || isFetching) return <ExpenseFilterSkeleton />; //Add this for Filter chip remover
if (isError && isFetched) useImperativeHandle(ref, () => ({
return <div>Something went wrong Here- {error.message} </div>; resetFieldValue: (name, value) => {
setTimeout(() => {
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...getValues(), [name]: defaultNotesFilter[name] });
}
}, 0);
},
getValues,
}));
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start"> <form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2"> {isLoading || isFetching ? (
<SelectMultiple <ExpenseFilterSkeleton />
name="createdByIds" ) : isError && isFetched ? (
label="Created By :" <div>Something went wrong here: {error?.message}</div>
options={data.createdBy} ) : (
labelKey="name" <>
valueKey="id" <div className="row g-2">
/> <SelectMultiple
<SelectMultiple name="createdByIds"
name="organizations" label="Created By :"
label="Organization:" options={data?.createdBy || []}
options={data.organizations} labelKey="name"
labelKey={(item) => item.name} valueKey="id"
valueKey="id" />
/> <SelectMultiple
</div> name="organizations"
<div className="d-flex justify-content-end py-3 gap-2"> label="Organization:"
<button options={data?.organizations || []}
type="button" labelKey={(item) => item.name}
className="btn btn-label-secondary btn-sm" valueKey="id"
onClick={handleClose} />
> </div>
Clear <div className="d-flex justify-content-end py-3 gap-2">
</button> <button
<button type="submit" className="btn btn-primary btn-sm"> type="button"
Apply className="btn btn-label-secondary btn-sm"
</button> onClick={handleClose}
</div>
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm" >
Apply
</button>
</div>
</>
)}
</form> </form>
</FormProvider> </FormProvider>
); );
}; });
export default NoteFilterPanel; export default NoteFilterPanel;

View File

@ -1,5 +1,4 @@
// NotesPage.jsx import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { useNotes } from "../../hooks/useDirectory"; import { useNotes } from "../../hooks/useDirectory";
import NoteFilterPanel from "./NoteFilterPanel"; import NoteFilterPanel from "./NoteFilterPanel";
@ -9,11 +8,14 @@ import { useDebounce } from "../../utils/appUtils";
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable"; import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton"; import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
import NoteFilterChips from "../../components/Directory/NoteFilterChips";
const NotesPage = ({ projectId, searchText, onExport }) => { const NotesPage = ({ projectId, searchText, onExport }) => {
const [filters, setFilter] = useState(defaultNotesFilter); const [filters, setFilter] = useState(defaultNotesFilter);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const [filterData, setFilterdata] = useState(null);
const updatedRef = useRef();
const { data, isLoading, isError, error } = useNotes( const { data, isLoading, isError, error } = useNotes(
projectId, projectId,
@ -33,7 +35,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent( setOffcanvasContent(
"Notes Filters", "Notes Filters",
<NoteFilterPanel onApply={setFilter} clearFilter={clearFilter} /> <NoteFilterPanel
ref={updatedRef} //Call here
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
); );
return () => { return () => {
@ -42,11 +49,27 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
}; };
}, []); }, []);
// 🔹 Format data for export const handleRemoveChip = (key, id) => {
setFilter((prev) => {
const updated = { ...prev };
if (Array.isArray(updated[key])) {
updated[key] = updated[key].filter((v) => v !== id);
updatedRef.current?.resetFieldValue(key, updated[key]); //IMP
} else {
updated[key] = null;
updatedRef.current?.resetFieldValue(key, null);
}
return updated;
});
};
// Format data for export
const formatExportData = (notes) => { const formatExportData = (notes) => {
return notes.map((n) => ({ return notes.map((n) => ({
ContactName: n.contactName || "", ContactName: n.contactName || "",
Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", // strip HTML tags Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "",
Organization: n.organizationName || "", Organization: n.organizationName || "",
CreatedBy: n.createdBy CreatedBy: n.createdBy
? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim() ? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim()
@ -59,7 +82,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
})); }));
}; };
// 🔹 Pass formatted notes to parent for export
useEffect(() => { useEffect(() => {
if (data?.data && onExport) { if (data?.data && onExport) {
onExport(formatExportData(data.data)); onExport(formatExportData(data.data));
@ -77,6 +99,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
return ( return (
<div className="d-flex flex-column text-start mt-3"> <div className="d-flex flex-column text-start mt-3">
<NoteFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={handleRemoveChip}
/>
{data?.data?.length > 0 ? ( {data?.data?.length > 0 ? (
<> <>
{data.data.map((noteItem) => ( {data.data.map((noteItem) => (
@ -96,7 +124,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
</div> </div>
</> </>
) : ( ) : (
// Card for "No notes available"
<div <div
className="card text-center d-flex align-items-center justify-content-center" className="card text-center d-flex align-items-center justify-content-center"
style={{ height: "200px" }} style={{ height: "200px" }}
@ -104,9 +131,9 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
<p className="text-muted mb-0"> <p className="text-muted mb-0">
{debouncedSearch {debouncedSearch
? `No notes found matching "${searchText}"` ? `No notes found matching "${searchText}"`
: Object.keys(filters).some((k) => filters[k] && filters[k].length) : Object.keys(filters).some((k) => filters[k]?.length)
? "No notes found for the applied filters." ? "No notes found for the applied filters."
: "No notes available."} : "No notes available."}
</p> </p>
</div> </div>
)} )}
@ -114,4 +141,4 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
); );
}; };
export default NotesPage; export default NotesPage;

View File

@ -1,17 +1,16 @@
import React, { createContext, useContext, useState, useEffect } from "react"; import React, { createContext, useContext, useState, useEffect, useRef } from "react";
import { useForm } from "react-hook-form"; import { useForm, useFormContext } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import ExpenseList from "../../components/Expenses/ExpenseList";
import ViewExpense from "../../components/Expenses/ViewExpense";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import PreviewDocument from "../../components/Expenses/PreviewDocument"; import ExpenseList from "../../components/Expenses/ExpenseList";
import ViewExpense from "../../components/Expenses/ViewExpense";
import ManageExpense from "../../components/Expenses/ManageExpense"; import ManageExpense from "../../components/Expenses/ManageExpense";
import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel"; import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
import ExpenseFilterChips from "../../components/Expenses/ExpenseFilterChips";
// Context & Hooks
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { import {
@ -20,11 +19,8 @@ import {
VIEW_SELF_EXPENSE, VIEW_SELF_EXPENSE,
} from "../../utils/constants"; } from "../../utils/constants";
// Schema & Defaults import { defaultFilter, SearchSchema } from "../../components/Expenses/ExpenseSchema";
import { import PreviewDocument from "../../components/Expenses/PreviewDocument";
defaultFilter,
SearchSchema,
} from "../../components/Expenses/ExpenseSchema";
// Context // Context
export const ExpenseContext = createContext(); export const ExpenseContext = createContext();
@ -41,10 +37,10 @@ const ExpensePage = () => {
(store) => store.localVariables.projectId (store) => store.localVariables.projectId
); );
const [filters, setFilter] = useState(); const [filters, setFilters] = useState(defaultFilter);
const [groupBy, setGroupBy] = useState("transactionDate"); const [groupBy, setGroupBy] = useState("transactionDate");
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const filterPanelRef = useRef();
const [ManageExpenseModal, setManageExpenseModal] = useState({ const [ManageExpenseModal, setManageExpenseModal] = useState({
IsOpen: null, IsOpen: null,
expenseId: null, expenseId: null,
@ -63,19 +59,22 @@ const ExpensePage = () => {
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE); const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE); const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE);
const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE); const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE);
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
const [filterData, setFilterdata] = useState(defaultFilter);
const methods = useForm({ const removeFilterChip = (key, id) => {
resolver: zodResolver(SearchSchema), setFilters((prev) => {
defaultValues: defaultFilter, const updated = { ...prev };
}); if (Array.isArray(updated[key])) {
updated[key] = updated[key].filter((v) => v !== id);
const { reset } = methods; filterPanelRef.current?.resetFieldValue(key, updated[key]);
} else if (key === "dateRange") {
const clearFilter = () => { updated.startDate = null;
setFilter(defaultFilter); updated.endDate = null;
reset(); filterPanelRef.current?.resetFieldValue("startDate", null);
filterPanelRef.current?.resetFieldValue("endDate", null);
}
return updated;
});
}; };
useEffect(() => { useEffect(() => {
@ -84,9 +83,10 @@ const ExpensePage = () => {
setOffcanvasContent( setOffcanvasContent(
"Expense Filters", "Expense Filters",
<ExpenseFilterPanel <ExpenseFilterPanel
onApply={setFilter} ref={filterPanelRef}
onApply={setFilters}
handleGroupBy={setGroupBy} handleGroupBy={setGroupBy}
clearFilter={clearFilter} setFilterdata={setFilterdata}
/> />
); );
} }
@ -101,6 +101,8 @@ const ExpensePage = () => {
setViewExpense, setViewExpense,
setManageExpenseModal, setManageExpenseModal,
setDocumentView, setDocumentView,
filterData,
removeFilterChip
}; };
return ( return (
@ -115,20 +117,18 @@ const ExpensePage = () => {
<div className="card my-3 px-sm-4 px-0"> <div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-3"> <div className="card-body py-2 px-3">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col-6 "> <div className="col-6">
<div className="d-flex align-items-center"> <input
<input type="search"
type="search" className="form-control form-control-sm w-auto"
className="form-control form-control-sm w-auto" placeholder="Search Expense"
placeholder="Search Expense" value={searchText}
aria-describedby="search-label" onChange={(e) => setSearchText(e.target.value)}
value={searchText} />
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
</div> </div>
<div className="col-6 text-end mt-2 mt-sm-0"> <div className="col-6 text-end mt-2 mt-sm-0">
{IsCreatedAble && ( {IsCreatedAble && (
<button <button
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
@ -151,6 +151,8 @@ const ExpensePage = () => {
</div> </div>
</div> </div>
<ExpenseList <ExpenseList
filters={filters} filters={filters}
groupBy={groupBy} groupBy={groupBy}
@ -161,7 +163,7 @@ const ExpensePage = () => {
<div className="card text-center py-1"> <div className="card text-center py-1">
<i className="fa-solid fa-triangle-exclamation fs-5" /> <i className="fa-solid fa-triangle-exclamation fs-5" />
<p> <p>
Access Denied: You don't have permission to perform this action ! Access Denied: You don't have permission to perform this action!
</p> </p>
</div> </div>
)} )}

View File

@ -654,3 +654,15 @@ nav.layout-navbar.navbar-active::after {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
.light-style .landing-hero {
background: linear-gradient(138.18deg, #eae8fd, #ede7e7 94.44%);
}
.text-green {
color: #49bf3c !important;
}
.text-blue {
color: var(--bs-blue);
}

View File

@ -54,15 +54,27 @@ const LandingPage = () => {
> >
<i className="tf-icons bx bx-menu bx-lg align-middle text-heading fw-medium"></i> <i className="tf-icons bx bx-menu bx-lg align-middle text-heading fw-medium"></i>
</button> </button>
{/* Mobile menu toggle: End*/}
<a href="/" className="app-brand-link"> {/* <a href="/" className="app-brand-link">
<span className="app-brand-logo demo"> <span className="app-brand-logo demo">
<img src="/img/brand/marco.png" width={50}></img> <img src="/img/brand/marco.png" width={50}></img>
</span> </span>
<span className="app-brand-text demo menu-text fw-bold ms-2 ps-1 "> <span className="app-brand-text demo menu-text fw-bold ms-2 ps-1 ">
{/* <Link> */} PMS PMS
{/* </Link> */}
</span> </span>
</a> */}
<a
href="/"
class="app-brand-link fw-bold navbar-brand text-green fs-5"
>
<span class="app-brand-logo demo">
<img src="/img/brand/marco.png" width="50" />
</span>
<span class="text-blue">OnField</span>
<span>Work</span>
<span class="text-dark">.com</span>
</a> </a>
</div> </div>
{/* Menu logo wrapper: End */} {/* Menu logo wrapper: End */}
@ -226,7 +238,7 @@ const LandingPage = () => {
</SwiperSlide> </SwiperSlide>
<SwiperSlide> <SwiperSlide>
<SwaperSlideContent <SwaperSlideContent
ImageUrl="/app/dashboard-light-04.png" ImageUrl="/img/app/dashboard-light-09.png"
Title="Role-based Permissions" Title="Role-based Permissions"
Body="Securely control access with customizable roles and permissions." Body="Securely control access with customizable roles and permissions."
></SwaperSlideContent> ></SwaperSlideContent>
@ -367,7 +379,7 @@ const LandingPage = () => {
</div>{" "} </div>{" "}
<div className="col-lg-3 col-sm-4 text-center features-icon-box"> <div className="col-lg-3 col-sm-4 text-center features-icon-box">
<div className="text-center mb-4"> <div className="text-center mb-4">
<img src="/img/icons/inventory.svg" alt="keyboard" /> <img src="/img/icons/user.svg" alt="keyboard" />
</div> </div>
<h5 className="mb-2">Inventory Management</h5> <h5 className="mb-2">Inventory Management</h5>
<p className="features-icon-description"> <p className="features-icon-description">
@ -436,7 +448,7 @@ const LandingPage = () => {
{" "} {" "}
<SwaperBlogContent <SwaperBlogContent
ImageUrl="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQs28_JnxJUKdAgaZsiWW4NyekVmfmLtpUtaA&s" ImageUrl="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQs28_JnxJUKdAgaZsiWW4NyekVmfmLtpUtaA&s"
Title="Transforming Residential Project Delivery with Marco PMS" Title="Transforming Residential Project Delivery with OnFieldWork.com"
Body="Sunrise Builders struggled with delays, cost overruns, and lack of transparency in their multi-phase township project. Different contractors for civil, electrical, and finishing works operated in silos, and communication gaps with the Project Management Consultant (PMC) often led to rework and disputes." Body="Sunrise Builders struggled with delays, cost overruns, and lack of transparency in their multi-phase township project. Different contractors for civil, electrical, and finishing works operated in silos, and communication gaps with the Project Management Consultant (PMC) often led to rework and disputes."
></SwaperBlogContent> ></SwaperBlogContent>
</SwiperSlide> </SwiperSlide>
@ -627,7 +639,7 @@ const LandingPage = () => {
aria-expanded="true" aria-expanded="true"
aria-controls="accordionOne" aria-controls="accordionOne"
> >
What is MarcoPMS? What is OnFieldWork.com?
</button> </button>
</h2> </h2>
@ -684,7 +696,7 @@ const LandingPage = () => {
aria-expanded="false" aria-expanded="false"
aria-controls="accordionThree" aria-controls="accordionThree"
> >
How secure is Marco PMS? How secure is OnFieldWork.com?
</button> </button>
</h2> </h2>
<div <div
@ -694,7 +706,7 @@ const LandingPage = () => {
data-bs-parent="#accordionExample" data-bs-parent="#accordionExample"
> >
<div className="accordion-body text-start"> <div className="accordion-body text-start">
Security is at the core of Marco PMS. We use Security is at the core of OnFieldWork.com. We use
industry-standard encryption (SSL/TLS) to protect data industry-standard encryption (SSL/TLS) to protect data
in transit and advanced encryption to safeguard data at in transit and advanced encryption to safeguard data at
rest. Role-based access controls ensure that only rest. Role-based access controls ensure that only
@ -754,13 +766,13 @@ const LandingPage = () => {
data-bs-parent="#accordionExample" data-bs-parent="#accordionExample"
> >
<div className="accordion-body text-start"> <div className="accordion-body text-start">
Marco PMS operate under a proprietary license combined OnFieldWork.com operate under a proprietary license
with a subscription model. This means customers dont combined with a subscription model. This means customers
own the software but are granted the right to access and dont own the software but are granted the right to
use it through the cloud under our Terms of Service. access and use it through the cloud under our Terms of
Depending on the plan, licensing may be based on users, Service. Depending on the plan, licensing may be based
features, or usage, and you can upgrade, downgrade, or on users, features, or usage, and you can upgrade,
cancel at any time. non! downgrade, or cancel at any time. non!
</div> </div>
</div> </div>
</div> </div>
@ -774,7 +786,7 @@ const LandingPage = () => {
aria-expanded="false" aria-expanded="false"
aria-controls="accordionSix" aria-controls="accordionSix"
> >
Can I customize Marco PMS for my business needs? Can I customize OnFieldWork.com for my business needs?
</button> </button>
</h2> </h2>
<div <div
@ -784,11 +796,11 @@ const LandingPage = () => {
data-bs-parent="#accordionExample" data-bs-parent="#accordionExample"
> >
<div className="accordion-body text-start"> <div className="accordion-body text-start">
Yes, Marco PMS is designed to be flexible and adaptable. Yes, OnFieldWork.com is designed to be flexible and
You can customize workflows, user roles, permissions, adaptable. You can customize workflows, user roles,
and reporting to match your organizations unique permissions, and reporting to match your organizations
processes. Depending on your plan, we also support unique processes. Depending on your plan, we also
advanced customization such as integrating with support advanced customization such as integrating with
third-party tools, adding custom fields, and tailoring third-party tools, adding custom fields, and tailoring
modules to fit your business requirements. modules to fit your business requirements.
</div> </div>
@ -823,7 +835,12 @@ const LandingPage = () => {
alt="hero elements" alt="hero elements"
></img> ></img>
</div> </div>
<div className="col-lg-6 text-start text-sm-center text-lg-start"> <div
className="col-lg-6 text-start text-sm-center text-lg-start p-5 rounded"
style={{
border: "1px solid #d5d5d5",
}}
>
<div className="mt-5"> <div className="mt-5">
{" "} {" "}
<h4 className="text-start mb-1"> <h4 className="text-start mb-1">
@ -1164,7 +1181,7 @@ const LandingPage = () => {
src="/img/brand/marco.png" src="/img/brand/marco.png"
width="50" width="50"
/> />
<span> Marco PMS</span> <span> OnFieldWork.com</span>
</div> </div>
</Link> </Link>
</span> </span>
@ -1252,7 +1269,7 @@ const LandingPage = () => {
<img src="/img/icons/apple-icon.png" alt="apple icon" /> <img src="/img/icons/apple-icon.png" alt="apple icon" />
</a> </a>
<a <a
href="https://play.google.com/store/apps/details?id=com.marco.aiotstage&pcampaignid=web_share" href="https://play.google.com/store/apps/details?id=com.marcoonfieldwork.aiot&pcampaignid=web_share "
target="_blank" target="_blank"
> >
<img <img

View File

@ -13,7 +13,7 @@ const OrganizationPage = () => {
<Breadcrumb <Breadcrumb
data={[{ label: "Home", link: "/" }, { label: "Organizations" }]} data={[{ label: "Home", link: "/" }, { label: "Organizations" }]}
/> />
<div className="card my-3 px-sm-2 px-0"> <div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-3"> <div className="card-body py-2 px-3">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col-6 d-flex "> <div className="col-6 d-flex ">
@ -42,9 +42,13 @@ const OrganizationPage = () => {
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<OrganizationsList searchText={searchText} /> <div className="card page-min-h px-sm-4">
<OrganizationsList searchText={searchText} />
</div>
</div> </div>
); );

View File

@ -121,7 +121,7 @@ const TenantPage = () => {
{ label: "Tenant", link: null }, { label: "Tenant", link: null },
]} ]}
/> />
<div className="card text-center my-4 p-5 pb-10"> <div className="card text-center my-4 p-md-5 px-1 py-3 pb-10">
{/* Super Tenant Actions */} {/* Super Tenant Actions */}
{isSuperTenant && ( {isSuperTenant && (
<div className="p-0"> <div className="p-0">

View File

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { AuthWrapper } from "./AuthWrapper" import { AuthWrapper } from "./AuthWrapper";
import "./page-auth.css"; import "./page-auth.css";
import AuthRepository from "../../repositories/AuthRepository"; import AuthRepository from "../../repositories/AuthRepository";
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
@ -8,54 +8,76 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
const forgotPassSceham = z.object({ const forgotPassSceham = z.object({
email: z.string().trim().email(), email: z.string().trim().email(),
}) });
const ForgotPasswordPage = () => { const ForgotPasswordPage = () => {
const [loding, setLoading] = useState(false);
const [loding, setLoading] = useState(false) const {
register,
const { register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
getValues } = useForm({ getValues,
resolver: zodResolver(forgotPassSceham), } = useForm({
defaultValues: { resolver: zodResolver(forgotPassSceham),
email: "" defaultValues: {
} email: "",
}) },
});
const onSubmit = async (data) => { const onSubmit = async (data) => {
try { try {
setLoading(true) setLoading(true);
const response = await AuthRepository.forgotPassword(data) const response = await AuthRepository.forgotPassword(data);
if (response.data && response.success) if (response.data && response.success)
showToast("verification email has been sent to your registered email address", "success") showToast(
reset() "verification email has been sent to your registered email address",
setLoading(false) "success"
);
reset();
setLoading(false);
} catch (err) { } catch (err) {
reset() reset();
if (err.response.status === 404) { if (err.response.status === 404) {
showToast("verification email has been sent to your registered email address", "success") showToast(
"verification email has been sent to your registered email address",
"success"
);
} else { } else {
showToast("Something wrong", "error") showToast("Something wrong", "error");
} }
setLoading(false) setLoading(false);
} }
} };
return ( return (
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60"> <div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}> <div className="w-100 m-auto" style={{ maxWidth: 420 }}>
<h4 className="mb-2">Forgot Password? 🔒</h4> <div className="d-flex align-items-center justify-content-center ">
<img src="/img/brand/marco.png" width="70" />
<Link aria-label="Go to Home Page" to="/">
<span class="app-brand-logo ">
<span class="text-blue fs-4">OnField</span>
<span className="text-green fs-4">Work</span>
<span class="text-dark fs-4">.com</span>
</span>
<br />
</Link>
</div>
<h5 className="mb-2 mt-5 ">Forgot Password? </h5>
<p className="mb-4"> <p className="mb-4">
Enter your email and we'll send you instructions to reset your password Enter your email and we'll send you instructions to reset your
password
</p> </p>
<form id="formAuthentication" className="mb-3" onSubmit={handleSubmit(onSubmit)}> <form
id="formAuthentication"
className="mb-3"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-3 text-start"> <div className="mb-3 text-start">
<label htmlFor="email" className="form-label"> <label htmlFor="email" className="form-label">
Email Email
@ -78,26 +100,28 @@ const ForgotPasswordPage = () => {
</div> </div>
)} )}
</div> </div>
<button aria-label="Click me" className="btn btn-primary d-grid w-100"> <button
aria-label="Click me"
className="btn btn-primary d-grid w-100"
>
{loding ? "Please Wait..." : "Send Reset Link"} {loding ? "Please Wait..." : "Send Reset Link"}
</button> </button>
</form> </form>
<div className="text-center"> <div className="text-center">
<Link <Link
aria-label="Go to Login Page" aria-label="Go to Login Page"
to="/auth/login" to="/auth/login"
className="d-flex align-items-center justify-content-center" className="d-flex align-items-center justify-content-center"
> >
<i className="bx bx-chevron-left scaleX-n1-rtl bx-sm"></i> <i className="bx bx-chevron-left scaleX-n1-rtl bx-sm"></i>
Back to login Back to login
</Link> </Link>
</div> </div>
{/* Footer Text */} {/* Footer Text */}
</div> </div>
</div> </div>
); );
}; };
export default ForgotPasswordPage; export default ForgotPasswordPage;

View File

@ -45,11 +45,11 @@ const LoginPage = () => {
if (data.rememberMe) { if (data.rememberMe) {
localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken); localStorage.setItem("refreshToken", response.data.refreshToken);
removeSession("session") removeSession("session");
} else { } else {
sessionStorage.setItem("jwtToken", response.data.token); sessionStorage.setItem("jwtToken", response.data.token);
sessionStorage.setItem("refreshToken", response.data.refreshToken); sessionStorage.setItem("refreshToken", response.data.refreshToken);
removeSession("local") removeSession("local");
} }
setLoading(false); setLoading(false);
navigate("/auth/switch/org"); navigate("/auth/switch/org");
@ -78,25 +78,35 @@ const LoginPage = () => {
}, [IsLoginWithOTP]); }, [IsLoginWithOTP]);
useEffect(() => { useEffect(() => {
const token = const token =
localStorage.getItem("jwtToken") || localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken");
sessionStorage.getItem("jwtToken");
if (token) { if (token) {
navigate("/dashboard", { replace: true }); navigate("/dashboard", { replace: true });
} }
}, []); }, []);
return ( return (
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60"> <div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}> <div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
<h4 className="mb-2">Welcome to PMS!</h4> <Link aria-label="Go to Home Page" to="/">
<span class="app-brand-logo rounded-circle app-brand-logo-border">
<img src="/img/brand/marco.png" width="70" />
</span>
<br />
<span class="text-dark fs-5">Welcome to</span> <br />
<h4 className="mb-2 ">
{" "}
<span class="text-blue ms-1">OnField</span>
<span className="text-green">Work</span>
<span class="text-dark">.com</span>
</h4>
</Link>
<p className="mb-4"> <p className="mb-4">
{IsLoginWithOTP {IsLoginWithOTP
? "Enter your email to receive a one-time password (OTP)." ? "Enter your email to receive a one-time password (OTP)."
: "Please sign in to your account and start the adventure"} : "Please sign in to your account and start the adventure"}
</p> </p>
<form id="formAuthentication" onSubmit={handleSubmit(onSubmit)}> <form id="formAuthentication" onSubmit={handleSubmit(onSubmit)}>
{/* Email */} {/* Email */}
<div className="mb-3 text-start"> <div className="mb-3 text-start">
@ -219,7 +229,6 @@ const LoginPage = () => {
</> </>
)} )}
</form> </form>
{/* Footer Text */} {/* Footer Text */}
{!IsLoginWithOTP ? ( {!IsLoginWithOTP ? (
<p className="text-center mt-3"> <p className="text-center mt-3">

View File

@ -37,33 +37,36 @@ const registerSchema = z.object({
const RegisterPage = () => { const RegisterPage = () => {
const [registered, setRegristered] = useState(false); const [registered, setRegristered] = useState(false);
const [industries, setIndustries] = useState([]); const [industries, setIndustries] = useState([]);
const [Loading,setLoading] = useState(false) const [Loading, setLoading] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors },reset formState: { errors },
reset,
} = useForm({ } = useForm({
resolver: zodResolver(registerSchema), resolver: zodResolver(registerSchema),
}); });
const onSubmit = async (data) => { const onSubmit = async (data) => {
try { try {
setLoading(true) setLoading(true);
const response = await MarketRepository.requestDemo(data); const response = await MarketRepository.requestDemo(data);
showToast("Your request has been sent successfully. Please stay in touch!"); showToast(
"Your request has been sent successfully. Please stay in touch!"
);
setRegristered(true); setRegristered(true);
setLoading(false) setLoading(false);
reset() reset();
} catch (error) { } catch (error) {
showToast(error.message, "error"); showToast(error.message, "error");
setLoading(false) setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchIndustries(); fetchIndustries();
}, []); }, []);
useEffect(() => { }, [industries]); useEffect(() => {}, [industries]);
const fetchIndustries = async () => { const fetchIndustries = async () => {
try { try {
@ -76,11 +79,20 @@ const RegisterPage = () => {
}; };
return ( return (
<> <>
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60"> <div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}> <div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
<div className="d-flex align-items-center justify-content-center ">
<h4 className="mb-2">Adventure starts here </h4> <img src="/img/brand/marco.png" width="50" />
<Link aria-label="Go to Home Page" to="/">
<span class="app-brand-logo ">
<span class="text-blue fs-4">OnField</span>
<span className="text-green fs-4">Work</span>
<span class="text-dark fs-4">.com</span>
</span>
<br />
</Link>
</div>
<h5 className="mb-2">Adventure starts here </h5>
<p className="mb-3">Make your app management easy and fun!</p> <p className="mb-3">Make your app management easy and fun!</p>
<form <form
@ -88,65 +100,64 @@ const RegisterPage = () => {
className="mb-2" className="mb-2"
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
> >
<div className="row"> <div className="row">
<div className="col-12 col-sm-6 mb-2 text-start"> <div className="col-12 col-sm-6 mb-2 text-start">
<label htmlFor="organizatioinName" className="form-label"> <label htmlFor="organizatioinName" className="form-label">
Organization Name Organization Name
</label> </label>
<input <input
type="text" type="text"
className="form-control form-control-sm" className="form-control form-control-sm"
id="organizatioinName" id="organizatioinName"
{...register("organizatioinName")} {...register("organizatioinName")}
name="organizatioinName" name="organizatioinName"
placeholder="Enter your Organization Name" placeholder="Enter your Organization Name"
autoFocus autoFocus
/> />
{errors.organizatioinName && ( {errors.organizatioinName && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.organizatioinName.message} {errors.organizatioinName.message}
</div> </div>
)} )}
</div> </div>
<div className="col-12 col-sm-6 mb-2 text-start"> <div className="col-12 col-sm-6 mb-2 text-start">
<label htmlFor="email" className="form-label"> <label htmlFor="email" className="form-label">
Email Email
</label> </label>
<input <input
type="text" type="text"
className="form-control form-control-sm" className="form-control form-control-sm"
id="email" id="email"
name="email" name="email"
placeholder="Enter your email" placeholder="Enter your email"
{...register("email")} {...register("email")}
/> />
{errors.email && ( {errors.email && (
<div <div
className="danger-text text-start" className="danger-text text-start"
style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
> >
{errors.email.message} {errors.email.message}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="mb-2 form-password-toggle text-start"> <div className="mb-2 form-password-toggle text-start">
<label className="form-label" htmlFor="contactperson"> <label className="form-label" htmlFor="contactperson">
Contact Person Contact Person
</label> </label>
<input <input
type="text" type="text"
id="contactperson" id="contactperson"
{...register("contactPerson")} {...register("contactPerson")}
className="form-control form-control-sm" className="form-control form-control-sm"
name="contactPerson" name="contactPerson"
placeholder="Contact Person" placeholder="Contact Person"
aria-describedby="contactperson" aria-describedby="contactperson"
/> />
{errors.contactPerson && ( {errors.contactPerson && (
<div <div
className="danger-text text-start" className="danger-text text-start"
@ -160,15 +171,15 @@ const RegisterPage = () => {
<label className="form-label" htmlFor="contactnumber"> <label className="form-label" htmlFor="contactnumber">
Contact Number Contact Number
</label> </label>
<input <input
type="text" type="text"
id="contactnumber" id="contactnumber"
{...register("contactNumber")} {...register("contactNumber")}
className="form-control form-control-sm" className="form-control form-control-sm"
name="contactNumber" name="contactNumber"
placeholder="Contact Number" placeholder="Contact Number"
aria-describedby="contactnumber" aria-describedby="contactnumber"
/> />
{errors.contactNumber && ( {errors.contactNumber && (
<div <div
className="danger-text text-start" className="danger-text text-start"
@ -182,14 +193,14 @@ const RegisterPage = () => {
<label className="form-label" htmlFor="contactnumber"> <label className="form-label" htmlFor="contactnumber">
About Organization About Organization
</label> </label>
<textarea <textarea
id="about" id="about"
className="form-control" className="form-control"
placeholder="About..." placeholder="About..."
aria-label="about" aria-label="about"
aria-describedby="about" aria-describedby="about"
{...register("about")} {...register("about")}
></textarea> ></textarea>
{errors.about && ( {errors.about && (
<div <div
className="danger-text text-start" className="danger-text text-start"
@ -203,20 +214,20 @@ const RegisterPage = () => {
<label className="form-label" htmlFor="oragnizationSize"> <label className="form-label" htmlFor="oragnizationSize">
Organization Size Organization Size
</label> </label>
<select <select
className="form-select form-select-sm" className="form-select form-select-sm"
id="oragnizationSize" id="oragnizationSize"
name="oragnizationSize" name="oragnizationSize"
{...register("oragnizationSize")} {...register("oragnizationSize")}
aria-label="Default select example" aria-label="Default select example"
> >
<option value="">Number of Employees</option> <option value="">Number of Employees</option>
<option value="1-10">1-10</option> <option value="1-10">1-10</option>
<option value="10-50">10-50</option> <option value="10-50">10-50</option>
<option value="50-100">50-100</option> <option value="50-100">50-100</option>
<option value="100-200">100-200</option> <option value="100-200">100-200</option>
<option value="more than 200">more than 200</option> <option value="more than 200">more than 200</option>
</select> </select>
{errors.oragnizationSize && ( {errors.oragnizationSize && (
<div <div
className="danger-text text-start" className="danger-text text-start"
@ -230,24 +241,24 @@ const RegisterPage = () => {
<label className="form-label" htmlFor="industryId"> <label className="form-label" htmlFor="industryId">
Industry Industry
</label> </label>
<select <select
className="form-select form-select-sm" className="form-select form-select-sm"
id="industryId" id="industryId"
name="industryId" name="industryId"
{...register("industryId")} {...register("industryId")}
aria-label="Default select example" aria-label="Default select example"
> >
<option value="">Select Industry</option> <option value="">Select Industry</option>
{industries.length > 0 ? ( {industries.length > 0 ? (
industries.map((item) => ( industries.map((item) => (
<option value={item.id} key={item.id}> <option value={item.id} key={item.id}>
{item.name} {item.name}
</option> </option>
)) ))
) : ( ) : (
<option disabled>Loading industries...</option> <option disabled>Loading industries...</option>
)} )}
</select> </select>
{errors.industryId && ( {errors.industryId && (
<div <div
className="danger-text text-start" className="danger-text text-start"
@ -276,7 +287,6 @@ const RegisterPage = () => {
privacy policy & terms privacy policy & terms
</Link> </Link>
</label> </label>
</div> </div>
{errors.terms && ( {errors.terms && (
<div <div
@ -291,7 +301,7 @@ const RegisterPage = () => {
aria-label="Click me " aria-label="Click me "
className="btn btn-primary d-grid w-100" className="btn btn-primary d-grid w-100"
> >
{Loading ? "Please Wait..." :" Request Demo"} {Loading ? "Please Wait..." : " Request Demo"}
</button> </button>
</form> </form>
@ -313,4 +323,4 @@ const RegisterPage = () => {
); );
}; };
export default RegisterPage; export default RegisterPage;

View File

@ -8,35 +8,43 @@ const TenantSelectionPage = () => {
const [pendingTenant, setPendingTenant] = useState(null); const [pendingTenant, setPendingTenant] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
const { data, isLoading, isError, error } = useTenants(); const { data, isLoading, isError } = useTenants();
const { mutate: chooseTenant, isPending } = useSelectTenant(() => { const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
navigate("/dashboard"); navigate("/dashboard");
}); });
const handleTenantselection = (tenantId) => { const { mutate: handleLogout, isPending: isLogouting } = useLogout();
const handleTenantSelection = (tenantId) => {
setPendingTenant(tenantId); setPendingTenant(tenantId);
localStorage.setItem("ctnt", tenantId); localStorage.setItem("ctnt", tenantId);
chooseTenant(tenantId); chooseTenant(tenantId);
}; };
const {mutate:handleLogout,isPending:isLogouting} = useLogout()
// Auto-select if already stored
useEffect(() => { useEffect(() => {
if (localStorage.getItem("ctnt")) { const storedTenant = localStorage.getItem("ctnt");
chooseTenant(localStorage.getItem("ctnt")) if (storedTenant) {
chooseTenant(storedTenant);
} }
}, [navigate]); }, []);
// Auto-select if only one tenant
useEffect(() => { useEffect(() => {
if (!isLoading && data?.data?.length === 1) { if (!isLoading && data?.data?.length === 1) {
const tenant = data.data[0]; const tenant = data.data[0];
handleTenantselection(tenant.id); handleTenantSelection(tenant.id);
} }
}, [isLoading, data]); }, [isLoading, data]);
if (isLoading) return <Loader />; // Show loader if:
// - initial loading
if (isLoading) { // - only one tenant (auto-selecting)
// - user manually selecting
if (
isLoading ||
isPending ||
(data?.data?.length === 1 && pendingTenant !== null)
) {
return <Loader />; return <Loader />;
} }
@ -48,7 +56,6 @@ const TenantSelectionPage = () => {
); );
} }
return ( return (
<div className="container-fluid"> <div className="container-fluid">
{/* Logo */} {/* Logo */}
@ -65,20 +72,24 @@ const TenantSelectionPage = () => {
<div className="text-center mb-4"> <div className="text-center mb-4">
<p className="fs-4 fw-bold mb-1">Welcome</p> <p className="fs-4 fw-bold mb-1">Welcome</p>
<p className="fs-6 fs-md-5"> <p className="fs-6 fs-md-5">
Please select which dashboard you want to explore!!! Please select which dashboard you want to explore!
</p> </p>
<div onClick={()=>handleLogout()}> <div onClick={() => handleLogout()}>
{isLogouting ? "Please Wait...":<span className="fs-6 fw-semibold cursor-pointer text-decoration-underline"><i className='bx bx-log-out'></i>SignOut</span>} {isLogouting ? (
"Please Wait..."
) : (
<span className="fs-6 fw-semibold cursor-pointer text-decoration-underline">
<i className="bx bx-log-out"></i> Sign Out
</span>
)}
</div> </div>
</div> </div>
{/* Card Section */} {/* Tenant Cards */}
<div className="row justify-content-center g-4 "> <div className="row justify-content-center g-4">
{data?.data.map((tenant) => ( {data?.data.map((tenant) => (
<div key={tenant.id} className="col-12 col-md-10 col-lg-8"> <div key={tenant.id} className="col-12 col-md-10 col-lg-8">
<div className="d-flex flex-column flex-md-row gap-4 align-items-center align-items-md-start p-3 border rounded shadow-sm bg-white h-100"> <div className="d-flex flex-column flex-md-row gap-4 align-items-center align-items-md-start p-3 border rounded shadow-sm bg-white h-100">
{/* Image */} {/* Image */}
<div className="flex-shrink-0 text-center"> <div className="flex-shrink-0 text-center">
<img <img
@ -95,12 +106,10 @@ const TenantSelectionPage = () => {
{/* Content */} {/* Content */}
<div className="d-flex flex-column text-start gap-2 w-100"> <div className="d-flex flex-column text-start gap-2 w-100">
{/* Title */}
<p className="fs-5 fs-md-4 text-dark fw-semibold mb-1"> <p className="fs-5 fs-md-4 text-dark fw-semibold mb-1">
{tenant?.name} {tenant?.name}
</p> </p>
{/* Industry */}
<div className="d-flex flex-wrap gap-2 align-items-center"> <div className="d-flex flex-wrap gap-2 align-items-center">
<p className="fw-semibold m-0">Industry:</p> <p className="fw-semibold m-0">Industry:</p>
<p className="m-0 text-muted"> <p className="m-0 text-muted">
@ -108,21 +117,19 @@ const TenantSelectionPage = () => {
</p> </p>
</div> </div>
{/* Description */}
{tenant?.description && ( {tenant?.description && (
<p className="text-start text-wrap text-muted small m-0"> <p className="text-start text-wrap text-muted small m-0">
{tenant?.description} {tenant?.description}
</p> </p>
)} )}
{/* Button */}
<button <button
className="btn btn-primary btn-sm mt-2 align-self-start" className="btn btn-primary btn-sm mt-2 align-self-start"
onClick={() => handleTenantselection(tenant?.id)} onClick={() => handleTenantSelection(tenant?.id)}
disabled={pendingTenant === tenant.id && isPending} disabled={pendingTenant === tenant.id && isPending}
> >
{isPending && pendingTenant === tenant.id {pendingTenant === tenant.id && isPending
? "Please Wait.." ? "Please Wait..."
: "Go To Dashboard"} : "Go To Dashboard"}
</button> </button>
</div> </div>
@ -131,8 +138,8 @@ const TenantSelectionPage = () => {
))} ))}
</div> </div>
</div> </div>
); );
}; };
export default TenantSelectionPage; export default TenantSelectionPage;

View File

@ -82,18 +82,29 @@ const CollectionPage = () => {
const handleMarkedPayment = (payload) => { const handleMarkedPayment = (payload) => {
MarkedReceived(payload); MarkedReceived(payload);
}; };
if (isAdmin === undefined || if (
canAddPayment === undefined || isAdmin === undefined ||
canEditCollection === undefined || canAddPayment === undefined ||
canViewCollection === undefined || canEditCollection === undefined ||
canViewCollection === undefined ||
canCreate === undefined canCreate === undefined
) { ) {
return <div className="text-center py-5">Checking access...</div>; return <div className="text-center py-5">Checking access...</div>;
} }
if (!isAdmin && !canAddPayment && !canEditCollection && !canViewCollection && !canCreate) { if (
return <AccessDenied data={[{ label: "Home", link: "/" }, { label: "Collection" }]} />; !isAdmin &&
} !canAddPayment &&
!canEditCollection &&
!canViewCollection &&
!canCreate
) {
return (
<AccessDenied
data={[{ label: "Home", link: "/" }, { label: "Collection" }]}
/>
);
}
return ( return (
<CollectionContext.Provider value={contextMassager}> <CollectionContext.Provider value={contextMassager}>
<div className="container-fluid"> <div className="container-fluid">
@ -127,28 +138,31 @@ if (!isAdmin && !canAddPayment && !canEditCollection && !canViewCollection && !c
</div> </div>
</div> </div>
<div className="col-12 col-md-6 d-flex justify-content-end gap-4"> <div className="col-12 col-md-6 d-flex justify-content-between justify-content-md-end gap-4">
<div className=" w-md-auto"> <div className="w-md-auto">
{" "} {" "}
<input <input
type="search" type="search"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
placeholder="search Collection" placeholder="Search Collection"
className="form-control form-control-sm" className="form-control form-control-sm"
/> />
</div> </div>
{ (canCreate || isAdmin) && ( {(canCreate || isAdmin) && (
<button <button
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
type="button" type="button"
onClick={() => setCollection({ isOpen: true, invoiceId: null })} onClick={() =>
> setCollection({ isOpen: true, invoiceId: null })
<i className="bx bx-plus-circle me-2"></i> }
<span className="d-none d-md-inline-block">Add New Collection</span> >
</button> <i className="bx bx-plus-circle me-2"></i>
)} <span className="d-none d-md-inline-block">
Add New Collection
</span>
</button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -38,6 +38,7 @@ import usePagination from "../../hooks/usePagination";
import { setProjectId } from "../../slices/localVariablesSlice"; import { setProjectId } from "../../slices/localVariablesSlice";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
const EmployeeList = () => { const EmployeeList = () => {
const selectedProjectId = useSelector( const selectedProjectId = useSelector(
@ -134,26 +135,11 @@ const EmployeeList = () => {
const tableRef = useRef(null); const tableRef = useRef(null);
const handleExport = (type) => { const handleExport = (type) => {
if (!currentItems || currentItems.length === 0) return; handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef);
switch (type) {
case "csv":
exportToCSV(currentItems, "employees");
break;
case "excel":
exportToExcel(currentItems, "employees");
break;
case "pdf":
exportToPDF(currentItems, "employees");
break;
case "print":
printTable(tableRef.current);
break;
default:
break;
}
}; };
const handleAllEmployeesToggle = (e) => { const handleAllEmployeesToggle = (e) => {
const isChecked = e.target.checked; const isChecked = e.target.checked;
setShowInactive(false); setShowInactive(false);
@ -176,12 +162,10 @@ const EmployeeList = () => {
useEffect(() => { useEffect(() => {
if (!loading && Array.isArray(employees)) { if (!loading && Array.isArray(employees)) {
const sorted = [...employees].sort((a, b) => { const sorted = [...employees].sort((a, b) => {
const nameA = `${a.firstName || ""}${a.middleName || ""}${ const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
a.lastName || "" }`.toLowerCase();
}`.toLowerCase(); const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
const nameB = `${b.firstName || ""}${b.middleName || ""}${ }`.toLowerCase();
b.lastName || ""
}`.toLowerCase();
return nameA?.localeCompare(nameB); return nameA?.localeCompare(nameB);
}); });
@ -258,9 +242,8 @@ const EmployeeList = () => {
? "Suspend Employee" ? "Suspend Employee"
: "Reactivate Employee" : "Reactivate Employee"
} }
message={`Are you sure you want to ${ message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
selectedEmpFordelete?.isActive ? "suspend" : "reactivate" } this employee?`}
} this employee?`}
onSubmit={(id) => onSubmit={(id) =>
suspendEmployee({ suspendEmployee({
employeeId: id, employeeId: id,
@ -309,7 +292,7 @@ const EmployeeList = () => {
className="form-check-label ms-0" className="form-check-label ms-0"
htmlFor="inactiveEmployeesCheckbox" htmlFor="inactiveEmployeesCheckbox"
> >
Show Inactive Employees In-active Employees
</label> </label>
</div> </div>
</div> </div>
@ -471,9 +454,8 @@ const EmployeeList = () => {
Status Status
</th> </th>
<th <th
className={`sorting_disabled ${ className={`sorting_disabled ${!Manage_Employee && "d-none"
!Manage_Employee && "d-none" }`}
}`}
rowSpan="1" rowSpan="1"
colSpan="1" colSpan="1"
style={{ width: "50px" }} style={{ width: "50px" }}
@ -493,20 +475,20 @@ const EmployeeList = () => {
)} )}
{!loading && {!loading &&
displayData?.length === 0 && displayData?.length === 0 &&
(!searchText ) ? ( (!searchText) ? (
<tr> <tr>
<td colSpan={8} className="border-0 py-3"> <td colSpan={8} className="border-0 py-3">
<div className="py-4"> <div className="py-4">
No Data Found No Data Found
</div> </div>
</td> </td>
</tr> </tr>
) : null} ) : null}
{!loading && {!loading &&
displayData?.length === 0 && displayData?.length === 0 &&
(searchText ) ? ( (searchText) ? (
<tr> <tr>
<td colSpan={8} className="border-0 py-3"> <td colSpan={8} className="border-0 py-3">
<div className="py-4"> <div className="py-4">
@ -542,18 +524,17 @@ const EmployeeList = () => {
</div> </div>
</div> </div>
</td> </td>
<td className="text-start d-none d-sm-table-cell"> <td className="text-start d-none d-sm-table-cell">
{item.email ? ( {item.email ? (
<span className="text-truncate"> <span className="text-truncate">
<i className="bx bxs-envelope text-primary me-2"></i> <i className="bx bxs-envelope text-primary me-2"></i>
{item.email} {item.email}
</span> </span>
) : ( ) : (
<span className="text-truncate text-italic"> <span className="d-block text-start text-muted fst-italic">NA</span>
-
</span>
)} )}
</td> </td>
<td className="text-start d-none d-sm-table-cell"> <td className="text-start d-none d-sm-table-cell">
<span className="text-truncate"> <span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i> <i className="bx bxs-phone-call text-primary me-2"></i>
@ -567,9 +548,14 @@ const EmployeeList = () => {
</span> </span>
</td> </td>
<td className=" d-none d-md-table-cell"> <td className="d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")} {item.joiningDate ? (
moment(item.joiningDate).format("DD-MMM-YYYY")
) : (
<span className="d-block text-center text-muted fst-italic">NA</span>
)}
</td> </td>
<td> <td>
{showInactive ? ( {showInactive ? (
<span <span
@ -663,17 +649,16 @@ const EmployeeList = () => {
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div>
{displayData?.length > 0 && ( </div>
{displayData?.length > 0 && (
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={totalPages} totalPages={totalPages}
onPageChange={paginate} onPageChange={paginate}
/> />
)} )}
</div>
</div>
</div> </div>
) : ( ) : (
<div className="card"> <div className="card">

View File

@ -186,8 +186,8 @@ const MasterPage = () => {
)} )}
</select> </select>
</div> </div>
<div className="col-12 col-md-9 d-flex justify-content-end align-items-center gap-2 mt-2 mt-md-0"> <div className="col-12 col-md-9 d-flex justify-content-between justify-content-md-end align-items-center gap-2 mt-2 mt-md-0">
<div className="col-6 col-md-3"> <div className="col-8 col-md-3">
<input <input
type="search" type="search"
className="form-control form-control-sm" className="form-control form-control-sm"

View File

@ -77,7 +77,7 @@ const ProjectDetails = () => {
<AboutProject /> <AboutProject />
<ProjectOverview project={projectId} /> <ProjectOverview project={projectId} />
</div> </div>
<div className="col-lg-8 col-md-7 mt-5"> <div className="col-lg-8 col-md-7 mt-2">
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" /> <ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
<div className="mt-5"> <div className="mt-5">
<AttendanceOverview /> <AttendanceOverview />

View File

@ -96,8 +96,8 @@ const ProjectPage = () => {
}, [data, isLoading, selectedStatuses]); }, [data, isLoading, selectedStatuses]);
if(isLoading) return <div className="page-min-h"><Loader/></div> if (isLoading) return <div className="page-min-h"><Loader /></div>
if(isError) return <div className="page-min-h d-flex justify-content-center align-items-center"><p>{error.message}</p></div> if (isError) return <div className="page-min-h d-flex justify-content-center align-items-center"><p>{error.message}</p></div>
return ( return (
<ProjectContext.Provider value={contextDispatcher}> <ProjectContext.Provider value={contextDispatcher}>
<div className="container-fluid"> <div className="container-fluid">
@ -128,9 +128,8 @@ const ProjectPage = () => {
<div className="d-flex gap-2 mb-2"> <div className="d-flex gap-2 mb-2">
<button <button
type="button" type="button"
className={`btn btn-sm p-1 ${ className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"
!listView ? "btn-primary" : "btn-outline-primary" }`}
}`}
onClick={() => setListView(false)} onClick={() => setListView(false)}
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-custom-class="tooltip" data-bs-custom-class="tooltip"
@ -140,9 +139,8 @@ const ProjectPage = () => {
</button> </button>
<button <button
type="button" type="button"
className={`btn btn-sm p-1 ${ className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"
listView ? "btn-primary" : "btn-outline-primary" }`}
}`}
onClick={() => setListView(true)} onClick={() => setListView(true)}
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-custom-class="tooltip" data-bs-custom-class="tooltip"
@ -180,20 +178,22 @@ const ProjectPage = () => {
</div> </div>
</div> </div>
<div> {HasManageProject && (
{HasManageProject && ( <button <div className="ms-auto">
className="btn btn-sm btn-primary" <button
type="button" className="btn btn-sm btn-primary"
onClick={() => type="button"
setMangeProject({ isOpen: true, Project: null }) onClick={() =>
} setMangeProject({ isOpen: true, Project: null })
> }
<i className="bx bx-plus-circle me-2"></i> >
<span className="d-none d-md-inline-block"> <i className="bx bx-plus-circle me-2"></i>
Add New Project <span className="d-none d-md-inline-block">
</span> Add New Project
</button>)} </span>
</div> </button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,12 +3,12 @@ import { api } from "../utils/axiosClient";
const GlobalRepository = { const GlobalRepository = {
getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => { getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => {
let params; let params;
if(projectId == null){ if (projectId == null) {
params = new URLSearchParams({ params = new URLSearchParams({
days: days.toString(), days: days.toString(),
FromDate, FromDate,
}); });
}else{ } else {
params = new URLSearchParams({ params = new URLSearchParams({
days: days.toString(), days: days.toString(),
FromDate, FromDate,
@ -19,30 +19,72 @@ const GlobalRepository = {
return api.get(`/api/Dashboard/Progression?${params.toString()}`); return api.get(`/api/Dashboard/Progression?${params.toString()}`);
}, },
getDashboardAttendanceData: ( date,projectId ) => { getDashboardAttendanceData: (date, projectId) => {
return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`); return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`);
}, },
getDashboardProjectsCardData: () => { getDashboardProjectsCardData: () => {
return api.get(`/api/Dashboard/projects`); return api.get(`/api/Dashboard/projects`);
}, },
getDashboardTeamsCardData: (projectId) => { getDashboardTeamsCardData: (projectId) => {
const url = projectId const url = projectId
? `/api/Dashboard/teams?projectId=${projectId}` ? `/api/Dashboard/teams?projectId=${projectId}`
: `/api/Dashboard/teams`; : `/api/Dashboard/teams`;
return api.get(url); return api.get(url);
}, },
getDashboardTasksCardData: (projectId) => { getDashboardTasksCardData: (projectId) => {
const url = projectId const url = projectId
? `/api/Dashboard/tasks?projectId=${projectId}` ? `/api/Dashboard/tasks?projectId=${projectId}`
: `/api/Dashboard/tasks`; : `/api/Dashboard/tasks`;
return api.get(url); return api.get(url);
}, },
getExpenseData: (projectId, startDate, endDate) => {
let url = `api/Dashboard/expense/type`
const queryParams = [];
if (projectId) {
queryParams.push(`projectId=${projectId}`);
}
if (startDate) {
queryParams.push(`startDate=${startDate}`);
}
if (endDate) {
queryParams.push(`endDate=${endDate}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
return api.get(url);
},
getExpenseStatus: (projectId) => api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}` : ""}`),
getExpenseDataByProject: (projectId, categoryId, months) => {
let url = `api/Dashboard/expense/monthly`
const queryParams = [];
if (projectId) {
queryParams.push(`projectId=${projectId}`);
}
if (categoryId) {
queryParams.push(`categoryId=${categoryId}`);
}
if (months) {
queryParams.push(`months=${months}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
return api.get(url);
},
getAttendanceOverview: (projectId, days) => api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`)
getAttendanceOverview:(projectId,days)=>api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`)
}; };
export default GlobalRepository; export default GlobalRepository;

View File

@ -104,6 +104,7 @@ const router = createBrowserRouter(
{ path: "/activities/task", element: <TaskPlannng /> }, { path: "/activities/task", element: <TaskPlannng /> },
{ path: "/activities/reports", element: <Reports /> }, { path: "/activities/reports", element: <Reports /> },
{ path: "/gallary", element: <ImageGalleryPage /> }, { path: "/gallary", element: <ImageGalleryPage /> },
{ path: "/expenses/:status?/:project?", element: <ExpensePage /> },
{ path: "/expenses", element: <ExpensePage /> }, { path: "/expenses", element: <ExpensePage /> },
{ path: "/collection", element: <CollectionPage /> }, { path: "/collection", element: <CollectionPage /> },
{ path: "/masters", element: <MasterPage /> }, { path: "/masters", element: <MasterPage /> },

View File

@ -154,4 +154,13 @@ export const PROJECT_STATUS = [
label: "Completed", label: "Completed",
}, },
]; ];
export const EXPENSE_STATUS = {
daft:"297e0d8f-f668-41b5-bfea-e03b354251c8",
review_pending:"6537018f-f4e9-4cb3-a210-6c3b2da999d7",
payment_pending:"f18c5cfd-7815-4341-8da2-2c2d65778e27",
approve_pending:"4068007f-c92f-4f37-a907-bc15fe57d4d8",
process_pending:"61578360-3a49-4c34-8604-7b35a3787b95"
}
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000"; export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";

View File

@ -51,7 +51,7 @@ export const convertShortTime = (dateString) => {
}; };
export const timeElapsed = (checkInTime, timeElapsedInHours) => { export const timeElapsed = (checkInTime, timeElapsedInHours) => {
const checkInDate = new Date( checkInTime.split( "T" )[ 0 ] ); const checkInDate = new Date(checkInTime.split("T")[0]);
const currentTime = new Date(); const currentTime = new Date();
@ -72,7 +72,7 @@ export const checkIfCurrentDate = (dateString) => {
return currentDate?.getTime() === inputDate?.getTime(); return currentDate?.getTime() === inputDate?.getTime();
}; };
export const formatNumber = (num) => { export const formatNumber = (num) => {
if (num == null || isNaN(num)) return "NA"; if (num == null || isNaN(num)) return "NA";
return Number.isInteger(num) ? num : num.toFixed(2); return Number.isInteger(num) ? num : num.toFixed(2);
}; };
@ -84,15 +84,25 @@ export const formatUTCToLocalTime = (datetime, timeRequired = false) => {
: moment.utc(datetime).local().format("DD MMM YYYY"); : moment.utc(datetime).local().format("DD MMM YYYY");
}; };
export const getCompletionPercentage = (completedWork, plannedWork)=> { export const getCompletionPercentage = (completedWork, plannedWork) => {
if (!plannedWork || plannedWork === 0) return 0; if (!plannedWork || plannedWork === 0) return 0;
const percentage = (completedWork / plannedWork) * 100; const percentage = (completedWork / plannedWork) * 100;
const clamped = Math.min(Math.max(percentage, 0), 100); const clamped = Math.min(Math.max(percentage, 0), 100);
return clamped.toFixed(2); return clamped.toFixed(2);
} }
export const getTenantStatus =(statusId)=>{ export const formatDate_DayMonth = (monthName, year) => {
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary" if (!monthName || !year) return "";
try {
const shortMonth = monthName.substring(0, 3);
return `${shortMonth} ${year}`;
} catch {
return "";
}
};
export const getTenantStatus = (statusId) => {
return ActiveTenant === statusId ? " bg-label-success" : "bg-label-secondary"
} }

View File

@ -40,112 +40,57 @@ export const exportToExcel = (data, fileName = "data") => {
* @param {Array} data - Array of objects to export * @param {Array} data - Array of objects to export
* @param {string} fileName - File name for the PDF (optional) * @param {string} fileName - File name for the PDF (optional)
*/ */
export const exportToPDF = async (data, fileName = "data") => { const sanitizeText = (text) => {
if (!text) return "";
// Replace all non-ASCII characters with "?" or remove them
return text.replace(/[^\x00-\x7F]/g, "?");
};
export const exportToPDF = async (data, fileName = "data", columns = null, options = {}) => {
if (!data || data.length === 0) return; if (!data || data.length === 0) return;
// Create a new PDF document
const pdfDoc = await PDFDocument.create(); const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
// Set up the font // Default options
const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Use Helvetica font const {
columnWidths = [], // array of widths per column
// Calculate column widths dynamically based on data content fontSizeHeader = 12,
const headers = Object.keys(data[0]); fontSizeRow = 10,
const rows = data.map(item => headers.map(header => item[header] || '')); rowHeight = 25,
} = options;
const getMaxColumnWidth = (columnIndex) => { const pageWidth = 1000;
let maxWidth = font.widthOfTextAtSize(headers[columnIndex], 12); const pageHeight = 600;
rows.forEach(row => { let page = pdfDoc.addPage([pageWidth, pageHeight]);
const cellText = row[columnIndex].toString(); const margin = 30;
maxWidth = Math.max(maxWidth, font.widthOfTextAtSize(cellText, 10)); let y = pageHeight - margin;
const headers = columns || Object.keys(data[0]);
// Draw headers
headers.forEach((header, i) => {
const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150);
page.drawText(header, { x, y, font, size: fontSizeHeader });
});
y -= rowHeight;
// Draw rows
data.forEach(row => {
headers.forEach((header, i) => {
const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150);
const text = row[header] || '';
page.drawText(text, { x, y, font, size: fontSizeRow });
}); });
return maxWidth + 10; // Padding for better spacing y -= rowHeight;
};
const columnWidths = headers.map((_, index) => getMaxColumnWidth(index)); if (y < margin) {
const tableX = 30; // X-coordinate for the table start page = pdfDoc.addPage([pageWidth, pageHeight]);
const rowHeight = 20; // Height of each row (can be adjusted) y = pageHeight - margin;
const maxPageHeight = 750; // Max available height for content (before a new page is added)
const pageMargin = 30; // Margin from the top of the page
let tableY = maxPageHeight; // Start Y position for the table
const maxPageWidth = 600; // Max available width for content (before a new page is added)
// Add the headers and rows to the page
const addHeadersToPage = (page, scaleFactor) => {
let xPosition = tableX;
headers.forEach((header, index) => {
page.drawText(header, {
x: xPosition,
y: tableY,
font,
size: 12 * scaleFactor, // Scale the header font size
color: rgb(0, 0, 0),
});
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
});
tableY -= rowHeight; // Move down after adding headers
};
// Add a new page and reset the table position
const addNewPage = (scaleFactor) => {
const page = pdfDoc.addPage([600, 800]);
tableY = maxPageHeight; // Reset Y position for the new page
addHeadersToPage(page, scaleFactor); // Re-add headers to the new page
return page;
};
// Create the first page and add headers
let page = pdfDoc.addPage([600, 800]);
// Check if the content fits within the page width, scale if necessary
const checkPageWidth = (row) => {
let totalWidth = columnWidths.reduce((acc, width) => acc + width, 0);
let scaleFactor = 1;
if (totalWidth > maxPageWidth) {
scaleFactor = maxPageWidth / totalWidth; // Scale down if necessary
} }
return scaleFactor;
};
// Function to check for page breaks when adding a new row
const checkPageBreak = () => {
if (tableY - rowHeight < pageMargin) {
page = addNewPage(scaleFactor); // Add a new page if there is no space for the next row
}
};
// Add rows to the PDF with pagination and horizontal scaling
rows.forEach(row => {
checkPageBreak(); // Check for page break before adding each row
const scaleFactor = checkPageWidth(row); // Get the scaling factor for the row
// Add headers to the first page and each new page with the same scale factor
if (tableY === maxPageHeight) {
addHeadersToPage(page, scaleFactor); // Add headers only on the first page
}
let xPosition = tableX;
row.forEach((value, index) => {
page.drawText(value.toString(), {
x: xPosition,
y: tableY,
font,
size: 10 * scaleFactor, // Scale the font size
color: rgb(0, 0, 0),
});
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
});
tableY -= rowHeight; // Move down to the next row position
}); });
// Serialize the document to bytes
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
// Trigger a download of the PDF
const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const link = document.createElement('a'); const link = document.createElement('a');
link.href = URL.createObjectURL(blob); link.href = URL.createObjectURL(blob);
@ -153,15 +98,110 @@ export const exportToPDF = async (data, fileName = "data") => {
link.click(); link.click();
}; };
/**
* Export JSON data to PDF in a card-style format
* @param {Array} data - Array of objects to export
* @param {string} fileName - File name for the PDF (optional)
*/
export const exportToPDF1 = async (data, fileName = "data") => {
if (!data || data.length === 0) return;
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const pageWidth = 600;
const pageHeight = 800;
const margin = 30;
const cardSpacing = 20;
const cardPadding = 10;
let page = pdfDoc.addPage([pageWidth, pageHeight]);
let y = pageHeight - margin;
for (const item of data) {
const title = item.ContactName || "";
const subtitle = `by ${item.CreatedBy || ""} on ${item.CreatedAt || ""}`;
const body = item.Note || "";
const cardHeight = 80 + (body.length / 60) * 14; // approximate height for body text
if (y - cardHeight < margin) {
page = pdfDoc.addPage([pageWidth, pageHeight]);
y = pageHeight - margin;
}
// Draw card border
page.drawRectangle({
x: margin,
y: y - cardHeight,
width: pageWidth - 2 * margin,
height: cardHeight,
borderColor: rgb(0.7, 0.7, 0.7),
borderWidth: 1,
color: rgb(1, 1, 1),
});
// Draw title
page.drawText(title, {
x: margin + cardPadding,
y: y - 20,
font: boldFont,
size: 12,
color: rgb(0.1, 0.1, 0.1),
});
// Draw subtitle
page.drawText(subtitle, {
x: margin + cardPadding,
y: y - 35,
font,
size: 10,
color: rgb(0.4, 0.4, 0.4),
});
// Draw body text (wrap manually)
const lines = body.match(/(.|[\r\n]){1,80}/g) || [];
lines.forEach((line, i) => {
page.drawText(line, {
x: margin + cardPadding,
y: y - 50 - i * 12,
font,
size: 10,
color: rgb(0.2, 0.2, 0.2),
});
});
y -= cardHeight + cardSpacing;
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${fileName}.pdf`;
link.click();
};
/** /**
* Print the HTML table by accepting the table element or a reference. * Print the HTML table by accepting the table element or a reference.
* @param {HTMLElement} table - The table element (or ref) to print * @param {HTMLElement} table - The table element (or ref) to print
*/ */
export const printTable = (table) => { export const printTable = (table) => {
if (table) { if (table) {
const newWindow = window.open("", "", "width=600,height=600"); // Open a new window const clone = table.cloneNode(true);
// Remove last column (Actions) from all rows
clone.querySelectorAll("tr").forEach((row) => {
row.removeChild(row.lastElementChild);
});
// Inject styles for the table and body const newWindow = window.open("", "", "width=600,height=600");
newWindow.document.write("<html><head><title>Print Table</title>"); newWindow.document.write("<html><head><title>Print Table</title>");
const style = document.createElement('style'); const style = document.createElement('style');
style.innerHTML = ` style.innerHTML = `
@ -171,16 +211,14 @@ export const printTable = (table) => {
th { background-color: #f2f2f2; } th { background-color: #f2f2f2; }
`; `;
newWindow.document.head.appendChild(style); newWindow.document.head.appendChild(style);
newWindow.document.write("</head><body>"); newWindow.document.write("</head><body>");
newWindow.document.write(table.outerHTML); // Write the table HTML to the new window newWindow.document.write(clone.outerHTML);
newWindow.document.write("</body></html>"); newWindow.document.write("</body></html>");
newWindow.document.close();
newWindow.document.close(); // Close the document stream
// Wait for the document to load before triggering print
newWindow.onload = () => { newWindow.onload = () => {
newWindow.print(); // Trigger the print dialog after the content is loaded newWindow.print();
}; };
} }
}; };