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>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Marco PMS</title>
<title>OnFieldWork.com</title>
<meta name="description" content="" />

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}
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>
{attLoading ? (
@ -223,8 +223,20 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
)}
</tbody>
</table>
</>
) : (
<div
className="d-flex justify-content-center align-items-center text-muted"
style={{ height: "200px" }}
>
{searchTerm
? "No results found for your search."
: attendanceList.length === 0
? "No employees assigned to the project."
: "No pending records available."}
</div>
)}
</div>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
@ -268,20 +280,6 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
</nav>
)}
</>
) : (
<div
className="d-flex justify-content-center align-items-center text-muted"
style={{ height: "200px" }}
>
{searchTerm
? "No results found for your search."
: attendanceList.length === 0
? "No employees assigned to the project."
: "No pending records available."}
</div>
)}
</div>
</>
);
};

View File

@ -15,6 +15,7 @@ import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance";
import { queryClient } from "../../layouts/AuthLayout";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { useNavigate } from "react-router-dom";
const usePagination = (data, itemsPerPage) => {
const [currentPage, setCurrentPage] = useState(1);
@ -38,47 +39,48 @@ const usePagination = (data, itemsPerPage) => {
};
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const navigate = useNavigate();
const today = new Date();
today.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const isSameDay = (dateStr) => {
const isSameDay = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() === today.getTime();
};
};
const isBeforeToday = (dateStr) => {
const isBeforeToday = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() < today.getTime();
};
};
const sortByName = (a, b) => {
const sortByName = (a, b) => {
const nameA = (a.firstName + a.lastName).toLowerCase();
const nameB = (b.firstName + b.lastName).toLowerCase();
return nameA.localeCompare(nameB);
};
};
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
selectedProject,
dateRange.startDate,
dateRange.endDate,
organizationId
);
);
const processedData = useMemo(() => {
const processedData = useMemo(() => {
const filteredData = showPending
? data.filter((item) => item.checkOutTime === null)
: data;
@ -103,30 +105,30 @@ const processedData = useMemo(() => {
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
return sortedDates.flatMap((date) => groupedByDate[date]);
}, [data, showPending]);
}, [data, showPending]);
const filteredSearchData = useMemo(() => {
const filteredSearchData = useMemo(() => {
if (!searchTerm) return processedData;
const lowercased = searchTerm.toLowerCase();
return processedData.filter((item) =>
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
);
}, [processedData, searchTerm]);
}, [processedData, searchTerm]);
const {
const {
currentPage,
totalPages,
currentItems: paginatedAttendances,
paginate,
resetPage,
} = usePagination(filteredSearchData, 20);
} = usePagination(filteredSearchData, 20);
useEffect(() => {
useEffect(() => {
resetPage();
}, [filteredSearchData]);
}, [filteredSearchData]);
const handler = useCallback(
const handler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
const checkIn = msg.response.checkInTime.substring(0, 10);
@ -145,14 +147,14 @@ const handler = useCallback(
}
},
[selectedProject, dateRange, resetPage]
);
);
useEffect(() => {
useEffect(() => {
eventBus.on("attendance_log", handler);
return () => eventBus.off("attendance_log", handler);
}, [handler]);
}, [handler]);
const employeeHandler = useCallback(
const employeeHandler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
if (data.some((item) => item.employeeId == msg.employeeId)) {
@ -160,40 +162,49 @@ const employeeHandler = useCallback(
}
},
[data, refetch]
);
);
useEffect(() => {
useEffect(() => {
eventBus.on("employee", employeeHandler);
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
}, [employeeHandler]);
return (
<>
<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"
>
<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">
{/* Date Range Picker */}
<div className="flex-grow-1 flex-md-grow-0">
<DateRangePicker
onRangeChange={setDateRange}
defaultStartDate={yesterday}
/>
<div className="form-check form-switch text-start ms-1 ms-md-2 align-items-center mb-0">
</div>
{/* Pending Attendance Switch */}
<div className="form-check form-switch text-start mb-0">
<input
type="checkbox"
className="form-check-input"
role="switch"
disabled={isFetching}
id="inactiveEmployeesCheckbox"
disabled={isFetching}
checked={showPending}
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
className="table-responsive text-nowrap"
style={{ minHeight: "200px" }}
@ -260,7 +271,12 @@ useEffect(() => {
lastName={attendance.lastName}
/>
<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">
{attendance.firstName} {attendance.lastName}
</span>
@ -297,8 +313,7 @@ useEffect(() => {
) : (
<div className="my-12">
<span className="text-secondary">
No data available for the selected date range. Please Select
another date.
No attendance record found in selected date range.
</span>
</div>
)}
@ -326,8 +341,7 @@ useEffect(() => {
(pageNumber) => (
<li
key={pageNumber}
className={`page-item ${
currentPage === pageNumber ? "active" : ""
className={`page-item ${currentPage === pageNumber ? "active" : ""
}`}
>
<button
@ -340,8 +354,7 @@ useEffect(() => {
)
)}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button

View File

@ -33,7 +33,7 @@ const InfraPlanning = () => {
const selectedService = useCurrentService();
const { projectInfra, isLoading, isError, error, isFetched } =
useProjectInfra(selectedProject, selectedService || "" );
useProjectInfra(selectedProject, selectedService || "");
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const canApproveTask = useHasUserPermission(APPROVE_TASK);
@ -62,9 +62,13 @@ const InfraPlanning = () => {
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
return (
<div className="text-center">
<p className="my-3">No Result Found</p>
<div
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>
);
}

View File

@ -14,6 +14,7 @@ import {
} from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query";
import Pagination from "../../components/common/Pagination";
import { useNavigate } from "react-router-dom";
const Regularization = ({
handleRequest,
@ -26,6 +27,7 @@ const Regularization = ({
// var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]);
const navigate = useNavigate();
const { regularizes, loading, error, refetch } = useRegularizationRequests(
selectedProject,
organizationId,
@ -33,8 +35,8 @@ const Regularization = ({
);
useEffect(() => {
if(!regularizes) return
if(regularizes?.length) {
if (!regularizes) return
if (regularizes?.length) {
setregularizedList(regularizes);
}
@ -102,8 +104,9 @@ const Regularization = ({
}, [employeeHandler]);
return (
<div>
<div
className="table-responsive text-nowrap pb-4"
className="table-responsive pt-3 text-nowrap pb-4"
style={{ minHeight: "200px" }}
>
{loading ? (
@ -141,8 +144,11 @@ const Regularization = ({
<td colSpan={2}>
<div className="d-flex justify-content-start align-items-center">
<Avatar firstName={att.firstName} lastName={att.lastName} />
<div className="d-flex flex-column">
<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">
{att.firstName} {att.lastName}
</span>
@ -160,7 +166,7 @@ const Regularization = ({
</td>
<td colSpan={2}>
{att.requestedBy ? ( <div className="d-flex justify-content-start align-items-center">
{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">
@ -169,10 +175,10 @@ const Regularization = ({
</span>
</a>
</div>
</div>):(<small>--</small>)}
</div>) : (<small>--</small>)}
</td>
<td>
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt,true) : "--"}
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt, true) : "--"}
</td>
<td className="text-center ">
<RegularizationActions
@ -197,46 +203,7 @@ const Regularization = ({
</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>
)} */}
</div>
{totalPages > 0 && (
<Pagination
currentPage={currentPage}
@ -244,6 +211,7 @@ const Regularization = ({
onPageChange={paginate}
/>
)}
</div>
);
};

View File

@ -23,7 +23,7 @@ const HorizontalBarChart = ({
if (loading) {
return (
<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 */}
</div>
);

View File

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

View File

@ -29,7 +29,7 @@ const TaskReportList = () => {
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const { service, openModal, closeModal,filter } = useDailyProgrssContext();
const { service, openModal, closeModal, filter } = useDailyProgrssContext();
const selectedProject = useSelectedProject();
const { projectNames } = useProjectName();
@ -37,7 +37,7 @@ const TaskReportList = () => {
selectedProject,
ITEMS_PER_PAGE,
currentPage,
service,filter
service, filter
);
const ProgrssReportColumn = [
@ -192,6 +192,7 @@ const TaskReportList = () => {
if (isLoading) return <TaskReportListSkeleton />;
if (isError) return <div>Loading....</div>;
return (
<div>
<div className="mt-2 table-responsive text-nowrap">
<table className="table">
<thead>
@ -287,14 +288,18 @@ const TaskReportList = () => {
))}
</tbody>
</table>
{data?.data?.length > 0 && (
</div>
{
data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
)
}
</div >
);
};

View File

@ -14,11 +14,11 @@ import ProjectCompletionChart from "./ProjectCompletionChart";
import ProjectProgressChart from "./ProjectProgressChart";
import ProjectOverview from "../Project/ProjectOverview";
import AttendanceOverview from "./AttendanceChart";
import ExpenseAnalysis from "./ExpenseAnalysis";
import ExpenseStatus from "./ExpenseStatus";
import ExpenseByProject from "./ExpenseByProject";
const Dashboard = () => {
const { projectsCardData } = useDashboardProjectsCardData();
const { teamsCardData } = useDashboardTeamsCardData();
const { tasksCardData } = useDashboardTasksCardData();
// Get the selected project ID from Redux store
const projectId = useSelector((store) => store.localVariables.projectId);
@ -29,16 +29,16 @@ const Dashboard = () => {
<div className="row gy-4">
{isAllProjectsSelected && (
<div className="col-sm-6 col-lg-4">
<Projects projectsCardData={projectsCardData} />
<Projects />
</div>
)}
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<Teams teamsCardData={teamsCardData} />
<Teams />
</div>
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<TasksCard tasksCardData={tasksCardData} />
<TasksCard/>
</div>
{isAllProjectsSelected && (
@ -56,11 +56,25 @@ const Dashboard = () => {
<div className="col-xxl-6 col-lg-6">
<ProjectProgressChart />
</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 && (
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview /> {/* ✅ Removed unnecessary projectId prop */}
<div className="col-12 col-md-6 mb-sm-0 mb-4 ">
<AttendanceOverview />
</div>
)}
<div className="col-12 col-md-6">
<ExpenseByProject />
</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";
const ProjectCompletionChart = () => {
const { projects, loading } = useProjects();
const { data: projects = [], isLoading: loading, isError, error } = useProjects();
// Bar chart logic
const projectNames = projects?.map((p) => p.name) || [];
@ -11,7 +12,7 @@ const ProjectCompletionChart = () => {
projects?.map((p) => {
const completed = p.completedWork || 0;
const planned = p.plannedWork || 1;
const percent = (completed / planned) * 100;
const percent = planned ? (completed / planned) * 100 : 0;
return Math.min(Math.round(percent), 100);
}) || [];

View File

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

View File

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

View File

@ -4,16 +4,20 @@ import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus";
import { useQueryClient } from "@tanstack/react-query";
import { useSelectedProject } from "../../slices/apiDataManager";
import { TeamsSkeleton } from "./DashboardSkeleton";
import { formatFigure } from "../../utils/appUtils";
const Teams = () => {
const queryClient = useQueryClient();
const projectId = useSelectedProject()
const projectId = useSelectedProject();
const {
data: teamsCardData,
isLoading,
isError,
error,
isFetching,
refetch,
} = useDashboardTeamsCardData(projectId);
// Handle real-time updates via eventBus
@ -40,6 +44,7 @@ const Teams = () => {
const inToday = teamsCardData?.inToday ?? 0;
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
if (isLoading) return <TeamsSkeleton />;
return (
<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">
@ -48,24 +53,41 @@ const Teams = () => {
</h5>
</div>
{isLoading ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : isError ? (
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
{isError ? (
<div className="d-flex flex-column justify-content-center align-items-center p-1">
<i className="bx bx-error-circle bx-sm fs-2 "></i>
<small className="text-muted mb-2">
{error?.message || "Unable to load data at the moment."}
</small>
<span
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 className="d-flex justify-content-around align-items-start mt-n2">
<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>
</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>
</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>
) : (
<i
className={`bx ${
isPending && activeContact === row.id
className={`bx ${isPending && activeContact === row.id
? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
@ -188,14 +187,14 @@ const ListViewContact = ({ data, Pagination }) => {
)}
</tbody>
</table>
</div>
</div>
{Pagination && (
<div className="d-flex justify-content-start">
{Pagination}
</div>
)}
</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 { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@ -9,16 +9,34 @@ import {
import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
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 { status } = useParams();
const { data, isError, isLoading, error } =
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({
resolver: zodResolver(DocumentFilterSchema),
defaultValues: DocumentFilterDefaultValues,
defaultValues: dynamicDocumentFilterDefaultValues,
});
const { handleSubmit, reset, setValue, watch } = methods;
@ -32,6 +50,24 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
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) => {
onApply({
...values,
@ -42,14 +78,14 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
: null,
});
closePanel();
// closePanel();
};
const onClear = () => {
reset(DocumentFilterDefaultValues);
setResetKey((prev) => prev + 1);
onApply(DocumentFilterDefaultValues);
closePanel();
// closePanel();
};
if (isLoading) return <div>Loading...</div>;
@ -63,6 +99,8 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
documentTag = [],
} = data?.data || {};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
@ -73,8 +111,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isUploadedAt ? "active btn-secondary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", true)}
>
@ -82,8 +119,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isUploadedAt ? "active btn-secondary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : ""
}`}
onClick={() => setValue("isUploadedAt", false)}
>
@ -189,18 +225,18 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
});
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 NewDocument from "./ManageDocument";
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument";
import DocumentViewerModal from "./DocumentViewerModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProfile } from "../../hooks/useProfile";
import DocumentFilterChips from "./DocumentFilterChips";
// Context
export const DocumentContext = createContext();
@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => {
const [isSelf, setIsSelf] = useState(false);
const [searchText, setSearchText] = useState("");
const [isActive, setIsActive] = useState(true);
const [filters, setFilter] = useState();
const [filters, setFilter] = useState(DocumentFilterDefaultValues);
const [isRefetching, setIsRefetching] = useState(false);
const [refetchFn, setRefetchFn] = useState(null);
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
const { employeeId } = useParams();
const [OpenDocument, setOpenDocument] = useState(false);
const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues);
const updatedRef = useRef();
const [ManageDoc, setManageDoc] = useState({
document: null,
isOpen: false,
@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => {
setShowTrigger(true);
setOffcanvasContent(
"Document Filters",
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} setFilterdata={setFilterdata} ref={updatedRef} />
);
return () => {
@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => {
setDocumentEntity(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 (
<DocumentContext.Provider value={contextValues}>
<div className="mt-5">
<div className="card page-min-h d-flex p-2">
<div className="mt-2">
<div className="card page-min-h d-flex p-5">
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
<div className="row align-items-center">
{/* 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">
{" "}
<input
@ -149,7 +174,7 @@ const Documents = ({ Document_Entity, Entity }) => {
</label>
</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) && (
<button
className="btn btn-sm btn-primary me-3"

View File

@ -82,9 +82,9 @@ const DocumentsList = ({
if (isLoading || isFetching) return <DocumentTableSkeleton />;
if (isError)
return <div>Error: {error?.message || "Something went wrong"}</div>;
if (isInitialEmpty) return <div>No documents found yet.</div>;
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
if (isFilterEmpty) return <div>No documents match your filter.</div>;
if (isInitialEmpty) return <div className="py-12 my-12">No documents found yet.</div>;
if (isSearchEmpty) return <div className="py-12 my-12">No results found for "{debouncedSearch}"</div>;
if (isFilterEmpty) return <div className="py-12 my-12">No documents match your filter.</div>;
const handleDelete = () => {
ActiveInActive(
@ -138,15 +138,13 @@ const DocumentsList = ({
lastName={e.uploadedBy?.lastName}
/>
<span className="text-truncate ms-1">
{`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
{`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
),
getValue: (e) =>
`${e.uploadedBy?.firstName ?? ""} ${
e.uploadedBy?.lastName ?? ""
`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
}`.trim() || "N/A",
},
{

View File

@ -51,7 +51,6 @@ const EmpAttendance = () => {
new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime()
);
console.log(sorted);
const { currentPage, totalPages, currentItems, paginate } = usePagination(
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 { zodResolver } from "@hookform/resolvers/zod";
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
@ -13,9 +13,11 @@ import { useSelector } from "react-redux";
import moment from "moment";
import { useExpenseFilter } from "../../hooks/useExpense";
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(
(store) => store.localVariables.projectId
);
@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
{ id: "submittedBy", name: "Submitted By" },
{ id: "project", name: "Project" },
{ id: "paymentMode", name: "Payment Mode" },
{ id: "expensesType", name: "Expense Type" },
{ id: "expensesType", name: "Expense Category" },
{ id: "createdAt", name: "Submitted Date" },
].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 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({
resolver: zodResolver(SearchSchema),
defaultValues: defaultFilter,
defaultValues: dynamicDefaultFilter,
});
const { control, handleSubmit, reset, setValue, watch } = methods;
@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
// Change here
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
const handleGroupChange = (e) => {
const group = groupByList.find((g) => g.id === e.target.value);
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) => {
onApply({
...formData,
@ -71,17 +106,55 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
onApply(defaultFilter);
handleGroupBy(groupByList[0].id);
closePanel();
if (status) {
navigate("/expenses", { replace: true });
}
};
// Close popup when navigating to another component
const location = useLocation();
useEffect(() => {
closePanel();
}, [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 (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return (
<>
<FormProvider {...methods}>
@ -92,8 +165,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isTransactionDate ? "active btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", true)}
>
@ -101,8 +173,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isTransactionDate ? "active btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", false)}
>
@ -143,6 +214,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
labelKey={(item) => item.name}
valueKey="id"
/>
<SelectMultiple
name="ExpenseTypeIds"
label="Category :"
options={data.expensesType}
labelKey={(item) => item.name}
valueKey="id"
/>
<div className="mb-3">
<label className="form-label">Status :</label>
@ -214,6 +292,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</FormProvider>
</>
);
};
});
export default ExpenseFilterPanel;

View File

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

View File

@ -1,54 +1,137 @@
import { useState } from "react";
import { useState, useRef ,useEffect} from "react";
const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true);
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 (
<>
<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
className="bx bx-rotate-right cursor-pointer"
className="bx bx-rotate-right fs-4 cursor-pointer"
title="Rotate Right"
onClick={() => setRotation((prev) => prev + 90)}
></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
className="d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: "60%" }}
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 mb-2">Loading...</div>
<div className="text-secondary text-center position-absolute">
Loading...
</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center">
<img
src={imageUrl}
alt="Full View"
className="img-fluid"
style={{
maxHeight: "80vh",
objectFit: "contain",
display: loading ? "none" : "block",
transform: `rotate(${rotation}deg)`,
transition: "transform 0.3s ease",
}}
alt="Preview"
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 className="d-flex justify-content-center gap-2">
{/* <div className="d-flex justify-content-center gap-2 mt-2">
<button
className="btn btn-outline-secondary"
onClick={() => setRotation(0)}
title="Reset Rotation"
className="btn btn-sm btn-outline-secondary"
onClick={handleReset}
title="Reset View"
>
<i className="bx bx-reset"></i> Reset
<i className="bx bx-reset"></i> Reset View
</button>
</div>
</div>
</div> */}
</>
);
};
export default PreviewDocument;

View File

@ -9,7 +9,11 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
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 { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
@ -301,7 +305,7 @@ const ViewExpense = ({ ExpenseId }) => {
<div className="d-flex flex-wrap gap-2">
{data?.documents?.map((doc) => {
const isImage = doc.contentType?.includes("image");
const isImage = doc.contentType?.startsWith("image");
return (
<div
@ -309,7 +313,7 @@ const ViewExpense = ({ ExpenseId }) => {
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{
width: "80px",
cursor: isImage ? "pointer" : "default",
cursor: "pointer",
}}
onClick={() => {
if (isImage) {
@ -317,6 +321,8 @@ const ViewExpense = ({ ExpenseId }) => {
IsOpen: true,
Image: doc.preSignedUrl,
});
} else {
window.open(doc.preSignedUrl, "_blank");
}
}}
>
@ -332,7 +338,7 @@ const ViewExpense = ({ ExpenseId }) => {
</small>
</div>
);
})}
}) ?? "No Attachment"}
</div>
</div>
@ -418,7 +424,9 @@ const ViewExpense = ({ ExpenseId }) => {
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
(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
className="form-control form-control-sm"
{...register("comment")}

View File

@ -15,7 +15,7 @@ const Sidebar = () => {
>
<div className="app-brand" style={{ paddingLeft: "30px" }}>
<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
className="app-brand-logo-sidebar"
src="/img/brand/marco.png"
@ -23,8 +23,19 @@ const Sidebar = () => {
aria-label="logo image"
style={{ margin: "5px", paddingRight: "5px" }}
/>
</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 className="app-brand-text menu-text fw-bold ms-2">PMS</span>
<span class="text-blue">OnField</span>
<span>Work</span>
<span class="text-dark">.com</span>
</a>
</Link>
<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 { useQueryClient } from "@tanstack/react-query";
// Zod schema: only allow exactly 4 digits
const OrgPickerFromSPId = ({ title, placeholder }) => {
const { onClose, startStep, flowType, onOpen, prevStep,orgData } =
const { onClose, startStep, flowType, onOpen, prevStep, orgData } =
useOrganizationModal();
const clientQuery = useQueryClient()
const clientQuery = useQueryClient();
const {
register,
handleSubmit,
@ -39,31 +38,40 @@ const OrgPickerFromSPId = ({ title, placeholder }) => {
};
const handleCrateOrg = () => {
clientQuery.removeQueries({queryKey:["organization"]})
onOpen({ startStep: 4,orgData:null })
clientQuery.removeQueries({ queryKey: ["organization"] });
onOpen({ startStep: 4, orgData: null });
};
const SP = watch("spridSearchText");
return (
<div className="d-block">
<form
className="d-flex flex-row gap-6 text-start align-items-center"
onSubmit={handleSubmit(onSubmit)}
>
<div className="d-flex flex-row align-items-center gap-2">
<Label className="text-secondary">Search by SPRID</Label>
<div className="d-block mt-4">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row align-items-center g-2">
{/* Input Section */}
<div className="col-12 col-md-8 d-block d-md-flex align-items-center gap-2 m-0 text-start">
<Label className="text-nowrap mb-1 mb-md-0" required>
Search by SPRID
</Label>
<input
type="search"
{...register("spridSearchText")}
className="form-control form-control-sm w-auto"
className="form-control form-control-sm flex-grow-1"
placeholder="Enter SPRID"
maxLength={4}
/>
</div>
<button type="submit" className="btn btn-sm btn-primary">
{/* Button Section */}
<div className="col-12 col-md-4 text-md-start text-center mt-2 mt-md-0">
<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>
<div className="text-start danger-text">
{" "}
{errors.spridSearchText && (
@ -124,7 +132,7 @@ const OrgPickerFromSPId = ({ title, placeholder }) => {
No organization found for "{SPRID}"
</div>
) : 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">
Do not have SPRID or could not find organization ?
</small>

View File

@ -93,9 +93,11 @@ const OrganizationsList = ({searchText}) => {
if (isError) return <div>{error?.message || "Something went wrong"}</div>;
return (
<div className="card px-0 px-sm-4 pb-12 pt-5">
<div className="card-datatable table-responsive" id="horizontal-example">
<div className="dataTables_wrapper no-footer px-2">
<div
className="card-datatable table-responsive overflow-auto"
id="horizontal-example"
>
<div className="dataTables_wrapper no-footer px-2 ">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="table_header_border">
@ -131,7 +133,7 @@ const OrganizationsList = ({searchText}) => {
<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-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>
</td>
</tr>
@ -157,7 +159,6 @@ const OrganizationsList = ({searchText}) => {
)}
</div>
</div>
</div>
);
};

View File

@ -22,8 +22,7 @@ const VieworgDataanization = ({ orgId }) => {
</div>
<div className="text-end">
<span
className={`badge bg-label-${
data?.isActive ? "primary" : "secondary"
className={`badge bg-label-${data?.isActive ? "primary" : "secondary"
} `}
>
{data?.isActive ? "Active" : "In-Active"}{" "}
@ -105,9 +104,101 @@ const VieworgDataanization = ({ orgId }) => {
<div className="text-muted text-start">{data?.address}</div>
</div>
</div>
<div className="d-flex text-secondary mb-2">
{" "}
<i className="bx bx-sm bx-briefcase me-1" /> Projects And Services
<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-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>
);

View File

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

View File

@ -40,19 +40,19 @@ const ProjectNav = ({ onPillClick, activePill }) => {
label: "Directory",
hidden: !(DirAdmin || DireManager || DirUser),
},
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
{ key: "documents", icon: "bx bx-folder-open", label: "Documents", hidden: !(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "organization", icon: "bx bx-buildings", label: "Organization" },
{ key: "setting", icon: "bx bxs-cog", label: "Setting", hidden: !isManageTeam },
];
return (
<div className="table-responsive">
<div className="nav-align-top">
<ul className="nav nav-tabs">
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
<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`}
onClick={(e) => {
e.preventDefault();
@ -66,6 +66,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
))}
</ul>
</div>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ const DateRangePicker = ({
/>
<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}
/>
</div>

View File

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

View File

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

View File

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

View File

@ -200,10 +200,10 @@ export const useAttendanceOverviewData = (projectId, days) => {
// })
// }
export const useDashboard_AttendanceData = (date,projectId)=>{
export const useDashboard_AttendanceData = (date, projectId) => {
return useQuery({
queryKey:["dashboardAttendances",date,projectId],
queryFn:async()=> {
queryKey: ["dashboardAttendances", date, projectId],
queryFn: async () => {
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
return resp.data;
@ -211,10 +211,10 @@ export const useDashboard_AttendanceData = (date,projectId)=>{
})
}
export const useDashboardTeamsCardData =(projectId)=>{
export const useDashboardTeamsCardData = (projectId) => {
return useQuery({
queryKey:["dashboardTeams",projectId],
queryFn:async()=> {
queryKey: ["dashboardTeams", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
return resp.data;
@ -224,8 +224,8 @@ export const useDashboardTeamsCardData =(projectId)=>{
export const useDashboardTasksCardData = (projectId) => {
return useQuery({
queryKey:["dashboardTasks",projectId],
queryFn:async()=> {
queryKey: ["dashboardTasks", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
return resp.data;
@ -245,11 +245,52 @@ export const useDashboardTasksCardData = (projectId) => {
export const useDashboardProjectsCardData = () => {
return useQuery({
queryKey:["dashboardProjects"],
queryFn:async()=> {
queryKey: ["dashboardProjects"],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardProjectsCardData();
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="/"
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
src="/img/brand/marco.png"
alt="marco-logo"
className="app-brand-logo-login"
/>
</span>
</span> */}
</Link>
<Outlet />
</div>

View File

@ -179,10 +179,9 @@ const AttendancePage = () => {
</div>
{/* 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">
{/* Organization Dropdown */}
<div className="row">
<div className="col-12 col-sm-6 mb-2 mb-sm-0">
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto">
<div className="row g-2">
<div className="col-12 col-sm-6">
<select
className="form-select form-select-sm"
value={appliedFilters.selectedOrganization}
@ -203,7 +202,6 @@ const AttendancePage = () => {
</select>
</div>
<div className="col-12 col-sm-6">
{/* Search Input */}
<input
type="text"
className="form-control form-control-sm"
@ -213,10 +211,8 @@ const AttendancePage = () => {
/>
</div>
</div>
</div>
</div>
</div>
@ -224,7 +220,7 @@ const AttendancePage = () => {
{selectedProject ? (
<>
{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
handleModalData={handleModalData}
getRole={getRole}
@ -234,7 +230,7 @@ const AttendancePage = () => {
</div>
)}
{activeTab === "logs" && (
<div className="tab-pane fade show active py-0">
<div className="tab-pane fade p-3 show active py-0">
<AttendanceLog
handleModalData={handleModalData}
searchTerm={searchTerm}
@ -243,7 +239,7 @@ const AttendancePage = () => {
</div>
)}
{activeTab === "regularization" && DoRegularized && (
<div className="tab-pane fade show active py-0">
<div className="tab-pane fade p-3 show active py-0">
<Regularization
searchTerm={searchTerm}
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">
{data?.length === 0 ? (
<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 React from "react";
import React, {
useEffect,
useImperativeHandle,
forwardRef,
useMemo,
} from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
contactsFilter,
@ -8,51 +13,88 @@ import {
import { useContactFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple";
import { useParams } from "react-router-dom";
const ContactFilterPanel = ({ onApply, clearFilter }) => {
const ContactFilterPanel = forwardRef(
({ onApply, clearFilter, setFilterdata }, ref) => {
const { data, isError, isLoading, error, isFetched, isFetching } =
useContactFilter();
const { status } = useParams();
useEffect(() => {
return () => {
closePanel();
};
}, []);
const dynamicdefaultContactFilter = useMemo(() => {
return {
...defaultContactFilter,
bucketIds: defaultContactFilter.bucketIds || [],
categoryIds: defaultContactFilter.categoryIds || [],
};
}, [status]);
const methods = useForm({
resolver: zodResolver(contactsFilter),
defaultValues: defaultContactFilter,
defaultValues: dynamicdefaultContactFilter,
});
const { handleSubmit, reset, setValue, getValues } = methods;
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
setTimeout(() => {
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...getValues(), [name]: defaultContactFilter[name] });
}
}, 0);
},
getValues,
}));
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const { register, handleSubmit, reset, watch } = methods;
const onSubmit = (formData) => {
onApply(formData);
closePanel();
// closePanel();
};
const handleClose = () => {
reset(defaultContactFilter);
onApply(defaultContactFilter);
closePanel();
// closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
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}
label="Buckets:"
options={data?.buckets || []}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="categoryIds"
label="Contact Category :"
options={data.contactCategories}
label="Contact Category:"
options={data?.contactCategories || []}
labelKey={(item) => item.name}
valueKey="id"
/>
@ -62,16 +104,18 @@ const ContactFilterPanel = ({ onApply, clearFilter }) => {
type="button"
className="btn btn-label-secondary btn-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
<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 { useContactList } from "../../hooks/useDirectory";
import { useDirectoryContext } from "./DirectoryPage";
import CardViewContact from "../../components/Directory/CardViewContact";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import ContactFilterPanel from "./ContactFilterPanel";
import ContactFilterChips from "../../components/Directory/ContactFilterChips";
import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
import { useDebounce } from "../../utils/appUtils";
import Pagination from "../../components/common/Pagination";
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) => {
return contacts.map(contact => ({
Email: contact.contactEmails?.map(e => e.emailAddress).join(", ") || "",
Phone: contact.contactPhones?.map(p => p.phoneNumber).join(", ") || "",
Created: contact.createdAt ? new Date(contact.createdAt).toLocaleString() : "",
return contacts.map((contact) => ({
Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "",
Phone: contact.contactPhones?.map((p) => p.phoneNumber).join(", ") || "",
Created: contact.createdAt
? new Date(contact.createdAt).toLocaleString()
: "",
Location: contact.address || "",
Organization: contact.organization || "",
Category: contact.contactCategory?.name || "",
Tags: contact.tags?.map(t => t.name).join(", ") || "",
Tags: contact.tags?.map((t) => t.name).join(", ") || "",
Buckets: contact.bucketIds?.join(", ") || "",
}));
};
@ -28,8 +32,10 @@ const formatExportData = (contacts) => {
const ContactsPage = ({ projectId, searchText, onExport }) => {
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilter] = useState(defaultContactFilter);
const [filterData, setFilterdata] = useState(null);
const debouncedSearch = useDebounce(searchText, 500);
const { showActive, gridView } = useDirectoryContext();
const updatedRef = useRef();
const { data, isError, isLoading, error } = useContactList(
showActive,
projectId,
@ -40,13 +46,19 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
);
const { setOffcanvasContent, setShowTrigger } = useFab();
// clear filters
const clearFilter = () => setFilter(defaultContactFilter);
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent(
"Contacts Filters",
<ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} />
<ContactFilterPanel
ref={updatedRef}
onApply={setFilter}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>
);
return () => {
@ -55,7 +67,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
};
}, []);
// 🔹 Format contacts for export
// export data
useEffect(() => {
if (data?.data && onExport) {
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 (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
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 ? (
<>
{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) => (
<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} />
</div>
))}
@ -95,6 +146,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
<div className="col-12">
<ListViewContact
data={data?.data}
isLoading={isLoading}
Pagination={
<Pagination
currentPage={currentPage}

View File

@ -139,8 +139,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
<ul className="nav nav-tabs">
<li className="nav-item cursor-pointer">
<a
className={`nav-link ${
activeTab === "notes" ? "active" : ""
className={`nav-link ${activeTab === "notes" ? "active" : ""
} fs-6`}
onClick={(e) => handleTabClick("notes", e)}
>
@ -150,8 +149,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</li>
<li className="nav-item cursor-pointer">
<a
className={`nav-link ${
activeTab === "contacts" ? "active" : ""
className={`nav-link ${activeTab === "contacts" ? "active" : ""
} fs-6`}
onClick={(e) => handleTabClick("contacts", e)}
>
@ -190,23 +188,21 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
/>
</div>
<button
className={`btn btn-sm p-1 ${
!gridView ? "btn-primary" : "btn-outline-primary"
className={`btn btn-sm p-1 ${gridView ? " btn-primary" : " btn-outline-primary"
}`}
onClick={() => setGridView(true)}
>
<i className="bx bx-grid-alt"></i>
</button>
<button
className={`btn btn-sm p-1 ${!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)}
>
<i className="bx bx-grid-alt"></i>
</button>
<div className="form-check form-switch d-flex align-items-center d-none d-md-flex">
<div className="form-check form-switch d-flex align-items-end d-none d-md-flex">
<input
type="checkbox"
className="form-check-input"
@ -219,32 +215,14 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
>
{showActive ? "Active" : "Inactive"} Contacts
{showActive ? "Active" : "In-active"} Contacts
</label>
</div>
</div>
</div>
)}
</div>
<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" }`}>
<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" : "Inactive"} Contacts
</label>
</div>
<div className=" btn-group">
<button
className="btn btn-sm btn-label-secondary dropdown-toggle"
@ -266,7 +244,6 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,58 +1,95 @@
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 {
defaultNotesFilter,
notesFilter,
} from "../../components/Directory/DirectorySchema";
import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory";
import { useNoteFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple";
const NoteFilterPanel = ({ onApply, clearFilter }) => {
const { data, isError, isLoading, error, isFetched, isFetching } =
useNoteFilter();
const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref) => {
const { data, isError, isLoading, error, isFetched, isFetching } = 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({
resolver: zodResolver(notesFilter),
defaultValues: defaultNotesFilter,
defaultValues: dynamicdefaultNotesFilter,
});
const { handleSubmit, reset, setValue, getValues } = methods;
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const { register, handleSubmit, reset, watch } = methods;
const onSubmit = (formData) => {
onApply(formData);
closePanel();
// closePanel();
};
const handleClose = () => {
reset(defaultNotesFilter);
onApply(defaultNotesFilter);
closePanel();
// closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
//Add this for Filter chip remover
useImperativeHandle(ref, () => ({
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 (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
{isLoading || isFetching ? (
<ExpenseFilterSkeleton />
) : isError && isFetched ? (
<div>Something went wrong here: {error?.message}</div>
) : (
<>
<div className="row g-2">
<SelectMultiple
name="createdByIds"
label="Created By :"
options={data.createdBy}
options={data?.createdBy || []}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="organizations"
label="Organization:"
options={data.organizations}
options={data?.organizations || []}
labelKey={(item) => item.name}
valueKey="id"
/>
@ -62,16 +99,19 @@ const NoteFilterPanel = ({ onApply, clearFilter }) => {
type="button"
className="btn btn-label-secondary btn-sm"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
<button type="submit" className="btn btn-primary btn-sm" >
Apply
</button>
</div>
</>
)}
</form>
</FormProvider>
);
};
});
export default NoteFilterPanel;

View File

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

View File

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

View File

@ -654,3 +654,15 @@ nav.layout-navbar.navbar-active::after {
height: 100%;
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>
</button>
{/* Mobile menu toggle: End*/}
<a href="/" className="app-brand-link">
{/* <a href="/" className="app-brand-link">
<span className="app-brand-logo demo">
<img src="/img/brand/marco.png" width={50}></img>
</span>
<span className="app-brand-text demo menu-text fw-bold ms-2 ps-1 ">
{/* <Link> */} PMS
{/* </Link> */}
PMS
</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>
</div>
{/* Menu logo wrapper: End */}
@ -226,7 +238,7 @@ const LandingPage = () => {
</SwiperSlide>
<SwiperSlide>
<SwaperSlideContent
ImageUrl="/app/dashboard-light-04.png"
ImageUrl="/img/app/dashboard-light-09.png"
Title="Role-based Permissions"
Body="Securely control access with customizable roles and permissions."
></SwaperSlideContent>
@ -367,7 +379,7 @@ const LandingPage = () => {
</div>{" "}
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
<div className="text-center mb-4">
<img src="/img/icons/inventory.svg" alt="keyboard" />
<img src="/img/icons/user.svg" alt="keyboard" />
</div>
<h5 className="mb-2">Inventory Management</h5>
<p className="features-icon-description">
@ -436,7 +448,7 @@ const LandingPage = () => {
{" "}
<SwaperBlogContent
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."
></SwaperBlogContent>
</SwiperSlide>
@ -627,7 +639,7 @@ const LandingPage = () => {
aria-expanded="true"
aria-controls="accordionOne"
>
What is MarcoPMS?
What is OnFieldWork.com?
</button>
</h2>
@ -684,7 +696,7 @@ const LandingPage = () => {
aria-expanded="false"
aria-controls="accordionThree"
>
How secure is Marco PMS?
How secure is OnFieldWork.com?
</button>
</h2>
<div
@ -694,7 +706,7 @@ const LandingPage = () => {
data-bs-parent="#accordionExample"
>
<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
in transit and advanced encryption to safeguard data at
rest. Role-based access controls ensure that only
@ -754,13 +766,13 @@ const LandingPage = () => {
data-bs-parent="#accordionExample"
>
<div className="accordion-body text-start">
Marco PMS operate under a proprietary license combined
with a subscription model. This means customers dont
own the software but are granted the right to access and
use it through the cloud under our Terms of Service.
Depending on the plan, licensing may be based on users,
features, or usage, and you can upgrade, downgrade, or
cancel at any time. non!
OnFieldWork.com operate under a proprietary license
combined with a subscription model. This means customers
dont own the software but are granted the right to
access and use it through the cloud under our Terms of
Service. Depending on the plan, licensing may be based
on users, features, or usage, and you can upgrade,
downgrade, or cancel at any time. non!
</div>
</div>
</div>
@ -774,7 +786,7 @@ const LandingPage = () => {
aria-expanded="false"
aria-controls="accordionSix"
>
Can I customize Marco PMS for my business needs?
Can I customize OnFieldWork.com for my business needs?
</button>
</h2>
<div
@ -784,11 +796,11 @@ const LandingPage = () => {
data-bs-parent="#accordionExample"
>
<div className="accordion-body text-start">
Yes, Marco PMS is designed to be flexible and adaptable.
You can customize workflows, user roles, permissions,
and reporting to match your organizations unique
processes. Depending on your plan, we also support
advanced customization such as integrating with
Yes, OnFieldWork.com is designed to be flexible and
adaptable. You can customize workflows, user roles,
permissions, and reporting to match your organizations
unique processes. Depending on your plan, we also
support advanced customization such as integrating with
third-party tools, adding custom fields, and tailoring
modules to fit your business requirements.
</div>
@ -823,7 +835,12 @@ const LandingPage = () => {
alt="hero elements"
></img>
</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">
{" "}
<h4 className="text-start mb-1">
@ -1164,7 +1181,7 @@ const LandingPage = () => {
src="/img/brand/marco.png"
width="50"
/>
<span> Marco PMS</span>
<span> OnFieldWork.com</span>
</div>
</Link>
</span>
@ -1252,7 +1269,7 @@ const LandingPage = () => {
<img src="/img/icons/apple-icon.png" alt="apple icon" />
</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"
>
<img

View File

@ -13,7 +13,7 @@ const OrganizationPage = () => {
<Breadcrumb
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="row align-items-center">
<div className="col-6 d-flex ">
@ -42,9 +42,13 @@ const OrganizationPage = () => {
</button>
</div>
</div>
</div>
</div>
<div className="card page-min-h px-sm-4">
<OrganizationsList searchText={searchText} />
</div>
</div>
);

View File

@ -121,7 +121,7 @@ const TenantPage = () => {
{ 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 */}
{isSuperTenant && (
<div className="p-0">

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { AuthWrapper } from "./AuthWrapper"
import { AuthWrapper } from "./AuthWrapper";
import "./page-auth.css";
import AuthRepository from "../../repositories/AuthRepository";
import showToast from "../../services/toastService";
@ -8,54 +8,76 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const forgotPassSceham = z.object({
email: z.string().trim().email(),
})
});
const ForgotPasswordPage = () => {
const [loding, setLoading] = useState(false);
const [loding, setLoading] = useState(false)
const { register,
const {
register,
handleSubmit,
formState: { errors },
reset,
getValues } = useForm({
getValues,
} = useForm({
resolver: zodResolver(forgotPassSceham),
defaultValues: {
email: ""
}
})
email: "",
},
});
const onSubmit = async (data) => {
try {
setLoading(true)
const response = await AuthRepository.forgotPassword(data)
setLoading(true);
const response = await AuthRepository.forgotPassword(data);
if (response.data && response.success)
showToast("verification email has been sent to your registered email address", "success")
reset()
setLoading(false)
showToast(
"verification email has been sent to your registered email address",
"success"
);
reset();
setLoading(false);
} catch (err) {
reset()
reset();
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 {
showToast("Something wrong", "error")
showToast("Something wrong", "error");
}
setLoading(false)
}
setLoading(false);
}
};
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="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
<h4 className="mb-2">Forgot Password? 🔒</h4>
<div className="w-100 m-auto" style={{ maxWidth: 420 }}>
<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">
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>
<form id="formAuthentication" className="mb-3" onSubmit={handleSubmit(onSubmit)}>
<form
id="formAuthentication"
className="mb-3"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-3 text-start">
<label htmlFor="email" className="form-label">
Email
@ -78,7 +100,10 @@ const ForgotPasswordPage = () => {
</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"}
</button>
</form>
@ -94,7 +119,6 @@ const ForgotPasswordPage = () => {
</div>
{/* Footer Text */}
</div>
</div>
);

View File

@ -45,11 +45,11 @@ const LoginPage = () => {
if (data.rememberMe) {
localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken);
removeSession("session")
removeSession("session");
} else {
sessionStorage.setItem("jwtToken", response.data.token);
sessionStorage.setItem("refreshToken", response.data.refreshToken);
removeSession("local")
removeSession("local");
}
setLoading(false);
navigate("/auth/switch/org");
@ -79,24 +79,34 @@ const LoginPage = () => {
useEffect(() => {
const token =
localStorage.getItem("jwtToken") ||
sessionStorage.getItem("jwtToken");
localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken");
if (token) {
navigate("/dashboard", { replace: true });
}
}, []);
}, []);
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="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">
{IsLoginWithOTP
? "Enter your email to receive a one-time password (OTP)."
: "Please sign in to your account and start the adventure"}
</p>
<form id="formAuthentication" onSubmit={handleSubmit(onSubmit)}>
{/* Email */}
<div className="mb-3 text-start">
@ -219,7 +229,6 @@ const LoginPage = () => {
</>
)}
</form>
{/* Footer Text */}
{!IsLoginWithOTP ? (
<p className="text-center mt-3">

View File

@ -37,33 +37,36 @@ const registerSchema = z.object({
const RegisterPage = () => {
const [registered, setRegristered] = useState(false);
const [industries, setIndustries] = useState([]);
const [Loading,setLoading] = useState(false)
const [Loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },reset
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(registerSchema),
});
const onSubmit = async (data) => {
try {
setLoading(true)
setLoading(true);
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);
setLoading(false)
reset()
setLoading(false);
reset();
} catch (error) {
showToast(error.message, "error");
setLoading(false)
setLoading(false);
}
};
useEffect(() => {
fetchIndustries();
}, []);
useEffect(() => { }, [industries]);
useEffect(() => {}, [industries]);
const fetchIndustries = async () => {
try {
@ -76,11 +79,20 @@ const RegisterPage = () => {
};
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="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
<h4 className="mb-2">Adventure starts here </h4>
<div className="d-flex align-items-center justify-content-center ">
<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>
<form
@ -88,7 +100,6 @@ const RegisterPage = () => {
className="mb-2"
onSubmit={handleSubmit(onSubmit)}
>
<div className="row">
<div className="col-12 col-sm-6 mb-2 text-start">
<label htmlFor="organizatioinName" className="form-label">
@ -276,7 +287,6 @@ const RegisterPage = () => {
privacy policy & terms
</Link>
</label>
</div>
{errors.terms && (
<div
@ -291,7 +301,7 @@ const RegisterPage = () => {
aria-label="Click me "
className="btn btn-primary d-grid w-100"
>
{Loading ? "Please Wait..." :" Request Demo"}
{Loading ? "Please Wait..." : " Request Demo"}
</button>
</form>

View File

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

View File

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

View File

@ -38,6 +38,7 @@ import usePagination from "../../hooks/usePagination";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import Pagination from "../../components/common/Pagination";
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
const EmployeeList = () => {
const selectedProjectId = useSelector(
@ -134,26 +135,11 @@ const EmployeeList = () => {
const tableRef = useRef(null);
const handleExport = (type) => {
if (!currentItems || currentItems.length === 0) return;
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;
}
handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef);
};
const handleAllEmployeesToggle = (e) => {
const isChecked = e.target.checked;
setShowInactive(false);
@ -176,11 +162,9 @@ const EmployeeList = () => {
useEffect(() => {
if (!loading && Array.isArray(employees)) {
const sorted = [...employees].sort((a, b) => {
const nameA = `${a.firstName || ""}${a.middleName || ""}${
a.lastName || ""
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
}`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${
b.lastName || ""
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
}`.toLowerCase();
return nameA?.localeCompare(nameB);
});
@ -258,8 +242,7 @@ const EmployeeList = () => {
? "Suspend Employee"
: "Reactivate Employee"
}
message={`Are you sure you want to ${
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
} this employee?`}
onSubmit={(id) =>
suspendEmployee({
@ -309,7 +292,7 @@ const EmployeeList = () => {
className="form-check-label ms-0"
htmlFor="inactiveEmployeesCheckbox"
>
Show Inactive Employees
In-active Employees
</label>
</div>
</div>
@ -471,8 +454,7 @@ const EmployeeList = () => {
Status
</th>
<th
className={`sorting_disabled ${
!Manage_Employee && "d-none"
className={`sorting_disabled ${!Manage_Employee && "d-none"
}`}
rowSpan="1"
colSpan="1"
@ -494,7 +476,7 @@ const EmployeeList = () => {
{!loading &&
displayData?.length === 0 &&
(!searchText ) ? (
(!searchText) ? (
<tr>
<td colSpan={8} className="border-0 py-3">
<div className="py-4">
@ -506,7 +488,7 @@ const EmployeeList = () => {
{!loading &&
displayData?.length === 0 &&
(searchText ) ? (
(searchText) ? (
<tr>
<td colSpan={8} className="border-0 py-3">
<div className="py-4">
@ -549,11 +531,10 @@ const EmployeeList = () => {
{item.email}
</span>
) : (
<span className="text-truncate text-italic">
-
</span>
<span className="d-block text-start text-muted fst-italic">NA</span>
)}
</td>
<td className="text-start d-none d-sm-table-cell">
<span className="text-truncate">
<i className="bx bxs-phone-call text-primary me-2"></i>
@ -567,9 +548,14 @@ const EmployeeList = () => {
</span>
</td>
<td className=" d-none d-md-table-cell">
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
<td className="d-none d-md-table-cell">
{item.joiningDate ? (
moment(item.joiningDate).format("DD-MMM-YYYY")
) : (
<span className="d-block text-center text-muted fst-italic">NA</span>
)}
</td>
<td>
{showInactive ? (
<span
@ -664,7 +650,8 @@ const EmployeeList = () => {
))}
</tbody>
</table>
</div>
</div>
{displayData?.length > 0 && (
<Pagination
currentPage={currentPage}
@ -673,8 +660,6 @@ const EmployeeList = () => {
/>
)}
</div>
</div>
</div>
) : (
<div className="card">
<div className="text-center">

View File

@ -186,8 +186,8 @@ const MasterPage = () => {
)}
</select>
</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-6 col-md-3">
<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-8 col-md-3">
<input
type="search"
className="form-control form-control-sm"

View File

@ -77,7 +77,7 @@ const ProjectDetails = () => {
<AboutProject />
<ProjectOverview project={projectId} />
</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" />
<div className="mt-5">
<AttendanceOverview />

View File

@ -96,8 +96,8 @@ const ProjectPage = () => {
}, [data, isLoading, selectedStatuses]);
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 (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>
return (
<ProjectContext.Provider value={contextDispatcher}>
<div className="container-fluid">
@ -128,8 +128,7 @@ const ProjectPage = () => {
<div className="d-flex gap-2 mb-2">
<button
type="button"
className={`btn btn-sm p-1 ${
!listView ? "btn-primary" : "btn-outline-primary"
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
@ -140,8 +139,7 @@ const ProjectPage = () => {
</button>
<button
type="button"
className={`btn btn-sm p-1 ${
listView ? "btn-primary" : "btn-outline-primary"
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
@ -180,8 +178,9 @@ const ProjectPage = () => {
</div>
</div>
<div>
{HasManageProject && ( <button
{HasManageProject && (
<div className="ms-auto">
<button
className="btn btn-sm btn-primary"
type="button"
onClick={() =>
@ -192,8 +191,9 @@ const ProjectPage = () => {
<span className="d-none d-md-inline-block">
Add New Project
</span>
</button>)}
</button>
</div>
)}
</div>
</div>
</div>

View File

@ -3,12 +3,12 @@ import { api } from "../utils/axiosClient";
const GlobalRepository = {
getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => {
let params;
if(projectId == null){
if (projectId == null) {
params = new URLSearchParams({
days: days.toString(),
FromDate,
});
}else{
} else {
params = new URLSearchParams({
days: days.toString(),
FromDate,
@ -19,10 +19,10 @@ const GlobalRepository = {
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
},
getDashboardAttendanceData: ( date,projectId ) => {
getDashboardAttendanceData: (date, projectId) => {
return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`);
},
},
getDashboardProjectsCardData: () => {
return api.get(`/api/Dashboard/projects`);
},
@ -32,17 +32,59 @@ const GlobalRepository = {
? `/api/Dashboard/teams?projectId=${projectId}`
: `/api/Dashboard/teams`;
return api.get(url);
},
},
getDashboardTasksCardData: (projectId) => {
const url = projectId
? `/api/Dashboard/tasks?projectId=${projectId}`
: `/api/Dashboard/tasks`;
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;

View File

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

View File

@ -154,4 +154,13 @@ export const PROJECT_STATUS = [
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";

View File

@ -51,7 +51,7 @@ export const convertShortTime = (dateString) => {
};
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();
@ -84,7 +84,7 @@ export const formatUTCToLocalTime = (datetime, timeRequired = false) => {
: moment.utc(datetime).local().format("DD MMM YYYY");
};
export const getCompletionPercentage = (completedWork, plannedWork)=> {
export const getCompletionPercentage = (completedWork, plannedWork) => {
if (!plannedWork || plannedWork === 0) return 0;
const percentage = (completedWork / plannedWork) * 100;
@ -93,6 +93,16 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> {
return clamped.toFixed(2);
}
export const getTenantStatus =(statusId)=>{
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
export const formatDate_DayMonth = (monthName, year) => {
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 {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;
// Create a new PDF document
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
// Set up the font
const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Use Helvetica font
// Default options
const {
columnWidths = [], // array of widths per column
fontSizeHeader = 12,
fontSizeRow = 10,
rowHeight = 25,
} = options;
// Calculate column widths dynamically based on data content
const headers = Object.keys(data[0]);
const rows = data.map(item => headers.map(header => item[header] || ''));
const pageWidth = 1000;
const pageHeight = 600;
let page = pdfDoc.addPage([pageWidth, pageHeight]);
const margin = 30;
let y = pageHeight - margin;
const getMaxColumnWidth = (columnIndex) => {
let maxWidth = font.widthOfTextAtSize(headers[columnIndex], 12);
rows.forEach(row => {
const cellText = row[columnIndex].toString();
maxWidth = Math.max(maxWidth, font.widthOfTextAtSize(cellText, 10));
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 });
});
return maxWidth + 10; // Padding for better spacing
};
y -= rowHeight;
const columnWidths = headers.map((_, index) => getMaxColumnWidth(index));
const tableX = 30; // X-coordinate for the table start
const rowHeight = 20; // Height of each row (can be adjusted)
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),
// 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 });
});
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
});
tableY -= rowHeight; // Move down after adding headers
};
y -= rowHeight;
// 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
if (y < margin) {
page = pdfDoc.addPage([pageWidth, pageHeight]);
y = pageHeight - margin;
}
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();
// Trigger a download of the PDF
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
@ -153,15 +98,110 @@ export const exportToPDF = async (data, fileName = "data") => {
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.
* @param {HTMLElement} table - The table element (or ref) to print
*/
export const printTable = (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>");
const style = document.createElement('style');
style.innerHTML = `
@ -173,14 +213,12 @@ export const printTable = (table) => {
newWindow.document.head.appendChild(style);
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.close(); // Close the document stream
// Wait for the document to load before triggering print
newWindow.document.close();
newWindow.onload = () => {
newWindow.print(); // Trigger the print dialog after the content is loaded
newWindow.print();
};
}
};