Merge pull request 'issues_Oct_4W' (#498) from issues_Oct_4W into OnFieldWork_V1

Reviewed-on: #498
Merged
This commit is contained in:
pramod.mahajan 2025-11-01 11:21:09 +00:00
commit d8712c0d04
20 changed files with 468 additions and 286 deletions

View File

@ -94,3 +94,44 @@
.h-screen{ height: 100vh; } .h-screen{ height: 100vh; }
.h-min { height: min-content; } .h-min { height: min-content; }
.h-max { height: max-content; } .h-max { height: max-content; }
/* ------------------------Text------------------------- */
@media (min-width: 576px) {
.fs-sm-1 { font-size: calc(1.3rem + 1.6vw) !important; }
.fs-sm-2 { font-size: calc(1.2rem + 1.2vw) !important; }
.fs-sm-3 { font-size: calc(1.1rem + 0.8vw) !important; }
.fs-sm-4 { font-size: calc(1rem + 0.5vw) !important; }
.fs-sm-5 { font-size: 1.05rem !important; }
.fs-sm-6 { font-size: 0.9rem !important; }
.fs-sm-tiny { font-size: 72% !important; }
.fs-sm-big { font-size: 115% !important; }
.fs-sm-large { font-size: 155% !important; }
.fs-sm-xlarge { font-size: 175% !important; }
.fs-sm-xxlarge { font-size: calc(1.6rem + 3.5vw) !important; }
}
/* 💻 Medium devices (≥768px) */
@media (min-width: 768px) {
.fs-md-1 { font-size: calc(1.4125rem + 1.95vw) !important; }
.fs-md-2 { font-size: calc(1.3625rem + 1.35vw) !important; }
.fs-md-3 { font-size: calc(1.3rem + 0.6vw) !important; }
.fs-md-4 { font-size: calc(1.275rem + 0.3vw) !important; }
.fs-md-5 { font-size: 1.125rem !important; }
.fs-md-6 { font-size: 0.9375rem !important; }
.fs-md-tiny { font-size: 70% !important; }
.fs-md-big { font-size: 112% !important; }
.fs-md-large { font-size: 150% !important; }
.fs-md-xlarge { font-size: 170% !important; }
.fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
}
/* Tables */
.table th.actions-col,
.table td.actions-col {
width: 1%;
white-space: nowrap;
text-align: center;
}

View File

@ -124,7 +124,7 @@ const AttendLogs = ({ Id }) => {
return ( return (
<div className="table-responsive"> <div className="table-responsive">
<div className="mb-3"> <div className="mb-3">
<h5 className="fw-bold mb-4">Attendance Logs</h5> <h5 className="mb-4">Attendance Logs</h5>
{logs && !loading && ( {logs && !loading && (
<p className="mb-0 text-start"> <p className="mb-0 text-start">
Showing logs for{" "} Showing logs for{" "}

View File

@ -12,8 +12,17 @@ import { useQueryClient } from "@tanstack/react-query";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import { SpinnerLoader } from "../common/Loader";
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, includeInactive, date }) => { const Attendance = ({
getRole,
handleModalData,
searchTerm,
projectId,
organizationId,
includeInactive,
date,
}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
@ -24,7 +33,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
attendance, attendance,
loading: attLoading, loading: attLoading,
recall: attrecall, recall: attrecall,
isFetching isFetching,
} = useAttendance(selectedProject, organizationId, includeInactive, date); } = useAttendance(selectedProject, organizationId, includeInactive, date);
const filteredAttendance = ShowPending const filteredAttendance = ShowPending
? attendance?.filter( ? attendance?.filter(
@ -71,19 +80,19 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
); );
// Reset pagination when the filter or search term changes // Reset pagination when the filter or search term changes
useEffect(() => { useEffect(() => {}, [finalFilteredData]);
}, [finalFilteredData]);
const handler = useCallback( const handler = useCallback(
(msg) => { (msg) => {
if (selectedProject == msg.projectId) { if (selectedProject == msg.projectId) {
queryClient.setQueryData(["attendance", selectedProject], (oldData) => { queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
if (!oldData) { if (!oldData) {
queryClient.invalidateQueries({ queryKey: ["attendance"] }) queryClient.invalidateQueries({ queryKey: ["attendance"] });
}; }
return oldData.map((record) => return oldData.map((record) =>
record.employeeId === msg.response.employeeId ? { ...record, ...msg.response } : record record.employeeId === msg.response.employeeId
? { ...record, ...msg.response }
: record
); );
}); });
} }
@ -109,16 +118,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
return () => eventBus.off("employee", employeeHandler); return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]); }, [employeeHandler]);
return ( return (
<> <>
<div> <div>
<div <div className="table-responsive text-nowrap ">
className="table-responsive text-nowrap h-100" <div className="d-flex justify-content-between align-items-center py-2">
style={{ minHeight: "200px" }} // Ensures fixed height
>
<div className="d-flex text-start align-items-center py-2">
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong> <strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
<div className="form-check form-switch text-start m-0 ms-5"> <div className="form-check form-switch text-start m-0 ms-5">
<input <input
@ -134,23 +138,29 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
</div> </div>
</div> </div>
{attLoading ? ( {attLoading ? (
<div>Loading...</div> <div
className="d-flex justify-content-center align-items-center"
style={{ minHeight: "70vh" }}
>
<SpinnerLoader />
</div>
) : currentItems?.length > 0 ? ( ) : currentItems?.length > 0 ? (
<> <>
<table className="table "> <table className="table table-hover ">
<thead> <thead>
<tr className="border-top-1"> <tr className="border-top-1">
<th colSpan={2}>Name</th> <th colSpan={2}>Name</th>
<th>Role</th> <th className="text-start actions-col text-center">Role</th>
{/* <th>Organization</th> */} {/* <th>Organization</th> */}
<th> <th>
<i className="bx bxs-down-arrow-alt text-success"></i> <i className="bx bxs-down-arrow-alt text-success"></i>
Check-In Check-In
</th> </th>
<th> <th>
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out <i className="bx bxs-up-arrow-alt text-danger"></i>
Check-Out
</th> </th>
<th>Actions</th> <th className="actions-col">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="table-border-bottom-0 "> <tbody className="table-border-bottom-0 ">
@ -190,7 +200,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
</div> </div>
</td> </td>
<td>{item.jobRoleName}</td> <td className="text-start action-col">{item.jobRoleName}</td>
{/* <td>{item.organizationName || "--"}</td> */} {/* <td>{item.organizationName || "--"}</td> */}
<td> <td>
@ -204,7 +214,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
: "--"} : "--"}
</td> </td>
<td className="text-center"> <td className="text-center actions-col">
<RenderAttendanceStatus <RenderAttendanceStatus
attendanceData={item} attendanceData={item}
handleModalData={handleModalData} handleModalData={handleModalData}

View File

@ -1,11 +1,15 @@
import React, { useEffect, useState, useMemo, useCallback } from "react"; import React, { useEffect, useState, useMemo, useCallback } from "react";
import moment from "moment"; import moment from "moment";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils"; import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus"; import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import DateRangePicker from "../common/DateRangePicker"; import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager"; import {
clearCacheKey,
getCachedData,
useSelectedProject,
} from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import AttendanceRepository from "../../repositories/AttendanceRepository"; import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance"; import { useAttendancesLogs } from "../../hooks/useAttendance";
@ -13,6 +17,7 @@ import { queryClient } from "../../layouts/AuthLayout";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { SpinnerLoader } from "../common/Loader";
const usePagination = (data, itemsPerPage) => { const usePagination = (data, itemsPerPage) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -36,14 +41,11 @@ const usePagination = (data, itemsPerPage) => {
}; };
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch(); const dispatch = useDispatch();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false) const [showPending, setShowPending] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [processedData, setProcessedData] = useState([]); const [processedData, setProcessedData] = useState([]);
@ -87,7 +89,8 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
dateRange.endDate, dateRange.endDate,
organizationId organizationId
); );
const filtering = useCallback((dataToFilter) => { const filtering = useCallback(
(dataToFilter) => {
const filteredData = showPending const filteredData = showPending
? dataToFilter.filter((item) => item.checkOutTime === null) ? dataToFilter.filter((item) => item.checkOutTime === null)
: dataToFilter; : dataToFilter;
@ -111,7 +114,14 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
.filter((d) => d.activity === 5) .filter((d) => d.activity === 5)
.sort(sortByName); .sort(sortByName);
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6]; const sortedList = [
...group1,
...group2,
...group3,
...group4,
...group5,
...group6,
];
// Group by date // Group by date
const groupedByDate = sortedList.reduce((acc, item) => { const groupedByDate = sortedList.reduce((acc, item) => {
@ -129,15 +139,15 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
const finalData = sortedDates.flatMap((date) => groupedByDate[date]); const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
setProcessedData(finalData); setProcessedData(finalData);
}, [showPending]); },
[showPending]
);
useEffect(() => { useEffect(() => {
if (data?.length) { if (data?.length) {
filtering(data); filtering(data);
} }
}, [data, showPending]); }, [data, showPending]);
// New useEffect to handle search filtering // New useEffect to handle search filtering
const filteredSearchData = useMemo(() => { const filteredSearchData = useMemo(() => {
@ -151,8 +161,6 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
}); });
}, [processedData, searchTerm]); }, [processedData, searchTerm]);
const { const {
currentPage, currentPage,
totalPages, totalPages,
@ -210,7 +218,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
// }) // })
// ); // );
refetch() refetch();
} }
}, },
[selectedProject, dateRange, data, refetch] [selectedProject, dateRange, data, refetch]
@ -221,40 +229,29 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
return () => eventBus.off("employee", employeeHandler); return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]); }, [employeeHandler]);
return ( return (
<> <>
<div <div
className="dataTables_length text-start py-2 d-flex justify-content-between" className="dataTables_length text-start py-2 d-flex justify-content-between "
id="DataTables_Table_0_length" id="DataTables_Table_0_length"
> >
<div className="d-flex align-items-center my-0 ms-sm-8 "> <div className=" col-12">
<DateRangePicker <DateRangePicker
onRangeChange={setDateRange} onRangeChange={setDateRange}
defaultStartDate={yesterday} defaultStartDate={yesterday}
/> />
<div className="form-check form-switch text-start ms-1 ms-md-5 align-items-center mb-0">
<input
type="checkbox"
className="form-check-input"
role="switch"
disabled={isFetching}
id="inactiveEmployeesCheckbox"
checked={showPending}
onChange={(e) => setShowPending(e.target.checked)}
/>
<label className="form-check-label ms-0">Show Pending</label>
</div> </div>
</div> </div>
<div className="table-responsive text-nowrap ">
</div>
<div className="table-responsive text-nowrap " >
{isLoading ? ( {isLoading ? (
<div className="d-flex justify-content-center align-items-center"> <div
<p className="text-secondary">Loading...</p> className="d-flex justify-content-center align-items-center"
style={{ minHeight: "70vh" }}
>
<SpinnerLoader/>
</div> </div>
) : filteredSearchData?.length > 0 ? ( ) : filteredSearchData?.length > 0 ? (
<table className="table mb-0"> <table className="table mb-0 table-hover">
<thead> <thead>
<tr> <tr>
<th className="border-top-1" colSpan={2}> <th className="border-top-1" colSpan={2}>
@ -264,12 +261,13 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
{/* <th>Organization</th> */} {/* <th>Organization</th> */}
<th> <th>
<i className="bx bxs-down-arrow-alt text-success"></i> Check-In <i className="bx bxs-down-arrow-alt text-success"></i>{" "}
Check-In
</th> </th>
<th> <th>
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out <i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
</th> </th>
<th>Action</th> <th className="actions-col">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -292,8 +290,8 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
className="table-row-header" className="table-row-header"
> >
<td colSpan={8} className="text-start"> <td colSpan={8} className="text-start">
<strong> <strong className="d-inline-block my-1 ms-2">
{moment(currentDate).format("DD-MM-YYYY")} {formatUTCToLocalTime(currentDate)}
</strong> </strong>
</td> </td>
</tr> </tr>
@ -310,7 +308,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<a <a
onClick={() => onClick={() =>
navigate(`/employee/${attendance.employeeId}?for=attendance`) navigate(
`/employee/${attendance.employeeId}?for=attendance`
)
} }
className="text-heading text-truncate cursor-pointer" className="text-heading text-truncate cursor-pointer"
> >
@ -333,7 +333,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
? convertShortTime(attendance.checkOutTime) ? convertShortTime(attendance.checkOutTime)
: "--"} : "--"}
</td> </td>
<td className="text-center"> <td className="text-center actions-col">
<RenderAttendanceStatus <RenderAttendanceStatus
attendanceData={attendance} attendanceData={attendance}
handleModalData={handleModalData} handleModalData={handleModalData}
@ -348,7 +348,14 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
</tbody> </tbody>
</table> </table>
) : ( ) : (
<div className="my-12"><span className="text-secondary">No data for this date range. Please choose another.</span></div> <div
className="d-flex justify-content-center align-items-center"
style={{ minHeight: "70vh" }}
>
<p className="text-secondary mb-0">
No data for this date range. Please choose another.
</p>
</div>
)} )}
</div> </div>
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && ( {paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (

View File

@ -12,6 +12,7 @@ import { useQueryClient } from "@tanstack/react-query";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { employee } from "../../data/masters"; import { employee } from "../../data/masters";
import { SpinnerLoader } from "../common/Loader";
const Regularization = ({ handleRequest, searchTerm, projectId, organizationId, IncludeInActive }) => { const Regularization = ({ handleRequest, searchTerm, projectId, organizationId, IncludeInActive }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -135,11 +136,14 @@ const Regularization = ({ handleRequest, searchTerm, projectId, organizationId,
<div> <div>
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}> <div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
{loading ? ( {loading ? (
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}> <div
<p className="text-secondary">Loading...</p> className="d-flex justify-content-center align-items-center"
style={{ minHeight: "70vh" }}
>
<SpinnerLoader/>
</div> </div>
) : currentItems?.length > 0 ? ( ) : currentItems?.length > 0 ? (
<table className="table mb-0"> <table className="table mb-0 table-hover">
<thead> <thead>
<tr> <tr>
<th colSpan={2}>Name</th> <th colSpan={2}>Name</th>
@ -154,7 +158,7 @@ const Regularization = ({ handleRequest, searchTerm, projectId, organizationId,
<th>Request By</th> <th>Request By</th>
<th>Requested At</th> <th>Requested At</th>
<th>Action</th> <th className="actions-col">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -6,10 +6,9 @@ import { useSelectedProject } from "../../hooks/useSelectedProject";
const Attendance = () => { const Attendance = () => {
const { projects } = useProjects(); const { projects } = useProjects();
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD const today = new Date().toISOString().split("T")[0];
const [selectedDate, setSelectedDate] = useState(today); const [selectedDate, setSelectedDate] = useState(today);
// central project selection hook
const selectedProjectId = useSelectedProject() const selectedProjectId = useSelectedProject()
const { const {
@ -136,7 +135,6 @@ const selectedProjectId = useSelectedProject()
</div> </div>
)} )}
{/* Details */}
{AttendanceData?.activeTab === "Details" && ( {AttendanceData?.activeTab === "Details" && (
<div className="table-responsive" style={{ maxHeight: "300px" }}> <div className="table-responsive" style={{ maxHeight: "300px" }}>
<table className="table table-hover mb-0 text-start"> <table className="table table-hover mb-0 text-start">

View File

@ -7,17 +7,17 @@ import ChartSkeleton from "../Charts/Skelton";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { formatDate_DayMonth } from "../../utils/dateUtils"; import { formatDate_DayMonth } from "../../utils/dateUtils";
const AttendanceOverview = () => { const AttendanceOverview = () => {
const [dayRange, setDayRange] = useState(7); const [dayRange, setDayRange] = useState(7);
const [view, setView] = useState("chart"); const [view, setView] = useState("chart");
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const { data: attendanceOverviewData, isLoading, isError, error } = useAttendanceOverviewData( const {
selectedProject, data: attendanceOverviewData,
dayRange isLoading,
); isError,
error,
} = useAttendanceOverviewData(selectedProject, dayRange);
// Use empty array while loading // Use empty array while loading
const attendanceData = attendanceOverviewData || []; const attendanceData = attendanceOverviewData || [];
@ -55,73 +55,129 @@ const AttendanceOverview = () => {
})); }));
const chartOptions = { const chartOptions = {
chart: { type: "bar", stacked: true, height: 400, toolbar: { show: false } }, chart: {
type: "bar",
stacked: true,
height: 400,
toolbar: { show: false },
},
plotOptions: { bar: { borderRadius: 2, columnWidth: "60%" } }, plotOptions: { bar: { borderRadius: 2, columnWidth: "60%" } },
xaxis: { categories: tableData.map((row) => row.date) }, xaxis: { categories: tableData.map((row) => row.date) },
yaxis: { show: true, axisBorder: { show: true, color: "#78909C" }, axisTicks: { show: true, color: "#78909C", width: 6 } }, yaxis: {
show: true,
axisBorder: { show: true, color: "#78909C" },
axisTicks: { show: true, color: "#78909C", width: 6 },
},
legend: { position: "bottom" }, legend: { position: "bottom" },
fill: { opacity: 1 }, fill: { opacity: 1 },
colors: roles.map((_, i) => flatColors[i % flatColors.length]), colors: roles.map((_, i) => flatColors[i % flatColors.length]),
}; };
return ( return (
<div className="bg-white p-4 rounded shadow d-flex flex-column position-relative " > <div className="bg-white p-4 rounded shadow d-flex flex-column position-relative">
{/* Optional subtle loading overlay */} <div className="row mb-3 align-items-center">
{isLoading && ( <div className="col-md-6 text-start">
<div className="position-absolute w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-50 z-index-1"> <p className="mb-1 fs-6 fs-md-5 fw-medium">Attendance Overview</p>
<span>Loading...</span> <p className="card-subtitle text-muted mb-0">
Role-wise present count
</p>
</div> </div>
)}
{/* Header */} <div className="col-md-6 d-flex flex-column align-items-end gap-2">
<div className="d-flex justify-content-between align-items-center mb-3"> <select
<div className="card-title mb-0 text-start"> className="form-select form-select-sm w-auto"
<h5 className="mb-1 card-title">Attendance Overview</h5> value={dayRange}
<p className="card-subtitle">Role-wise present count</p> onChange={(e) => setDayRange(Number(e.target.value))}
</div> >
<div className="d-flex gap-2">
<select className="form-select form-select-sm" value={dayRange} onChange={(e) => setDayRange(Number(e.target.value))}>
<option value={7}>Last 7 Days</option> <option value={7}>Last 7 Days</option>
<option value={15}>Last 15 Days</option> <option value={15}>Last 15 Days</option>
<option value={30}>Last 30 Days</option> <option value={30}>Last 30 Days</option>
</select> </select>
<button className={`btn btn-sm p-1 ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`} onClick={() => setView("chart")} title="Chart View">
<i className="bx bx-bar-chart-alt-2"></i> <div className="d-flex gap-2 justify-content-end">
<button
className={`btn btn-sm p-1 ${
view === "chart" ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setView("chart")}
title="Chart View"
>
<i className="bx bx-bar-chart-alt-2 fs-5"></i>
</button> </button>
<button className={`btn btn-sm p-1 ${view === "table" ? "btn-primary" : "btn-outline-primary"}`} onClick={() => setView("table")} title="Table View">
<button
className={`btn btn-sm p-1 ${
view === "table" ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setView("table")}
title="Table View"
>
<i className="bx bx-list-ul fs-5"></i> <i className="bx bx-list-ul fs-5"></i>
</button> </button>
</div> </div>
</div> </div>
</div>
{/* Content Section */}
<div className="flex-grow-1 d-flex align-items-center justify-content-center position-relative">
{isLoading && (
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-50">
<span>Loading...</span>
</div>
)}
{/* Content */}
<div className="flex-grow-1 d-flex align-items-center justify-content-center ">
{!isLoading && (!attendanceData || attendanceData.length === 0) ? ( {!isLoading && (!attendanceData || attendanceData.length === 0) ? (
<div <div
className="text-muted fw-semibold d-flex align-items-center justify-content-center" className="text-muted fw-semibold d-flex align-items-center justify-content-center"
style={{ minHeight: "250px" }}>No data found</div> style={{ minHeight: "250px" }}
>
No data found
</div>
) : view === "chart" ? ( ) : view === "chart" ? (
<div className="w-100"> <div className="w-100">
<ReactApexChart options={chartOptions} series={chartSeries} type="bar" height={300} /> <ReactApexChart
options={chartOptions}
series={chartSeries}
type="bar"
height={300}
/>
</div> </div>
) : ( ) : (
<div className="table-responsive w-100" style={{ maxHeight: "350px", overflowY: "auto" }}> <div
<table className="table table-bordered table-sm text-start align-middle mb-0"> className="table-responsive w-100"
<thead className="table-light" style={{ position: "sticky", top: 0, zIndex: 1 }}> style={{ maxHeight: "350px", overflowY: "auto" }}
>
<table className="table table-bordered table-sm align-middle mb-0">
<thead
className="table-light"
style={{ position: "sticky", top: 0, zIndex: 1 }}
>
<tr> <tr>
<th style={{ background: "#f8f9fa", textTransform: "none" }}>Role</th> <th style={{ background: "#f8f9fa" }}>Role</th>
{dates.map((date, idx) => ( {dates.map((date, idx) => (
<th key={idx} style={{ background: "#f8f9fa", textTransform: "none" }}>{date}</th> <th key={idx} style={{ background: "#f8f9fa" }}>
{date}
</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{roles.map((role) => ( {roles.map((role) => (
<tr key={role}> <tr key={role}>
<td>{role}</td> <td className="fw-medium text-start table-cell">{role}</td>
{tableData.map((row, idx) => { {tableData.map((row, idx) => {
const value = row[role]; const value = row[role];
return <td key={idx} style={value > 0 ? { backgroundColor: "#d5d5d5" } : {}}>{value}</td>; return (
<td
key={idx}
style={
value > 0 ? { backgroundColor: "#e9ecef" } : {}
}
>
{value}
</td>
);
})} })}
</tr> </tr>
))} ))}

View File

@ -19,6 +19,13 @@ import { useProjectName } from "../../hooks/useProjects";
import ExpenseAnalysis from "./ExpenseAnalysis"; import ExpenseAnalysis from "./ExpenseAnalysis";
import ExpenseStatus from "./ExpenseStatus"; import ExpenseStatus from "./ExpenseStatus";
import ExpenseByProject from "./ExpenseByProject"; import ExpenseByProject from "./ExpenseByProject";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
APPROVE_EXPENSE,
EXPENSE_MANAGE,
VIEW_ALL_EXPNESE,
} from "../../utils/constants";
import { useHasAnyPermission } from "../../hooks/useExpense";
const Dashboard = () => { const Dashboard = () => {
// const { projectsCardData } = useDashboardProjectsCardData(); // const { projectsCardData } = useDashboardProjectsCardData();
@ -29,8 +36,14 @@ const Dashboard = () => {
const projectId = useSelector((store) => store.localVariables.projectId); const projectId = useSelector((store) => store.localVariables.projectId);
const isAllProjectsSelected = projectId === null; const isAllProjectsSelected = projectId === null;
const isViewExpense = useHasAnyPermission(
VIEW_ALL_EXPNESE,
APPROVE_EXPENSE,
EXPENSE_MANAGE
);
return ( return (
<div className="container-fluid py-5"> <div className="container-fluid py-5">
{isViewExpense && (
<div className="row mb-6 g-6"> <div className="row mb-6 g-6">
<div className="col-12 col-xl-8"> <div className="col-12 col-xl-8">
<div className="card h-100"> <div className="card h-100">
@ -39,13 +52,14 @@ const Dashboard = () => {
</div> </div>
<div className="col-12 col-xl-4 col-md-6"> <div className="col-12 col-xl-4 col-md-6">
<div className="card "> <div className="card h-100">
<ExpenseStatus /> <ExpenseStatus />
</div> </div>
</div> </div>
</div> </div>
)}
<div className="row "> <div className="row vh-100">
{!isAllProjectsSelected && ( {!isAllProjectsSelected && (
<div className="col-12 col-md-6 mb-sm-0 mb-4 "> <div className="col-12 col-md-6 mb-sm-0 mb-4 ">
<AttendanceOverview /> <AttendanceOverview />
@ -55,8 +69,7 @@ const Dashboard = () => {
<ExpenseByProject /> <ExpenseByProject />
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View File

@ -56,9 +56,27 @@ const ExpenseAnalysis = () => {
}, },
responsive: [ responsive: [
{ {
breakpoint: 576, // mobile breakpoint breakpoint: 1200,
options: { options: {
chart: { width: "100%" }, chart: { width: "100%", height: 350 },
legend: { position: "bottom" },
},
},
{
breakpoint: 992,
options: {
chart: { width: "100%", height: 300 },
dataLabels: { style: { fontSize: "11px" } },
},
},
{
breakpoint: 576,
options: {
chart: { width: "100%", height: 250 },
legend: { fontSize: "10px" },
plotOptions: {
pie: { donut: { size: "65%" } },
},
}, },
}, },
], ],
@ -69,18 +87,18 @@ const ExpenseAnalysis = () => {
{/* Header */} {/* Header */}
<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="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"> <div className="text-start w-100">
<h5 className="mb-1 card-title">Expense Breakdown</h5> <p className="mb-1 fw-medium fs-6 fs-md-5">Expense Breakdown</p>
<p className="card-subtitle mb-0">Category Wise Expense Breakdown</p> <p className="card-subtitle mb-0">Category Wise Expense Breakdown</p>
</div> </div>
<div className="text-start text-sm-end w-75"> <div className="d-flex justify-content-start justify-content-md-end te w-75">
<FormProvider {...methods}> <FormProvider {...methods}>
<DateRangePicker1 /> <DateRangePicker1 />
</FormProvider> </FormProvider>
</div> </div>
</div> </div>
{/* Card body */}
<div className="card-body position-relative"> <div className="card-body position-relative">
{isLoading && ( {isLoading && (
<div <div
@ -92,7 +110,7 @@ const ExpenseAnalysis = () => {
)} )}
{!isLoading && report.length === 0 && ( {!isLoading && report.length === 0 && (
<div className="text-center py-5 text-muted">No data found</div> <div className="d-flex justify-content-center align-items-center">No data found</div>
)} )}
{!isLoading && report.length > 0 && ( {!isLoading && report.length > 0 && (

View File

@ -67,13 +67,19 @@ const ExpenseByProject = () => {
}, },
]; ];
const ExpenseCategoryType = [
{id:1,category:"Category",label:"Category"},
{id:2,category:"Project",label:"Project"}
]
return ( return (
<div className="card shadow-sm rounded "> <div className="card shadow-sm rounded ">
{/* Header */} {/* Header */}
<div className="card-header"> <div className="card-header">
<div className="d-flex justify-content-start align-items-center mb-3 mt-3"> <div className="d-flex justify-content-between align-items-center mb-3 mt-3">
<div> <div className="text-start">
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5> <p className="mb-1 fw-medium fs-6 fs-md-5 ">Monthly Expense -</p>
<p className="card-subtitle me-5 mb-0">Detailed project expenses</p> <p className="card-subtitle me-5 mb-0">Detailed project expenses</p>
</div> </div>
<div className="btn-group mb-4 ms-n8"> <div className="btn-group mb-4 ms-n8">
@ -86,28 +92,19 @@ const ExpenseByProject = () => {
{viewMode} {viewMode}
</button> </button>
<ul className="dropdown-menu dropdown-menu-end "> <ul className="dropdown-menu dropdown-menu-end ">
{ExpenseCategoryType.map((cat)=>(
<li> <li>
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => { onClick={() => {
setViewMode("Category"); setViewMode(cat.category);
setSelectedType(""); setSelectedType("");
}} }}
> >
Category {cat.label}
</button>
</li>
<li>
<button
className="dropdown-item"
onClick={() => {
setViewMode("Project");
setSelectedType("");
}}
>
Project
</button> </button>
</li> </li>
))}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -36,14 +36,14 @@ const ExpenseStatus = () => {
<> <>
<div className="card-header d-flex justify-content-between text-start "> <div className="card-header d-flex justify-content-between text-start ">
<div className="m-0"> <div className="m-0">
<h5 className="card-title mb-1">Expense - By Status</h5> <p className="fs-6 fw-medium fs-md-5 mb-1">Expense - By Status</p>
<p className="card-subtitle m-0 ">{projectName}</p> <p className="card-subtitle m-0 ">{projectName}</p>
</div> </div>
</div> </div>
<div className="card-body "> <div className="card-body ">
<div className="report-list text-start"> <div className="report-list text-start h-max">
{[ {[
{ {
title: "Pending Payment", title: "Pending Payment",

View File

@ -74,7 +74,7 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
return ( return (
<> <>
<div className="d-flex justify-content-between align-items-center mt-2"> <div className="d-flex justify-content-between align-items-center mt-2 h-25" >
<p className="m-0 fw-normal">Add Employee</p> <p className="m-0 fw-normal">Add Employee</p>
<div className="px-1"> <div className="px-1">
<input <input
@ -87,7 +87,7 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
</div> </div>
</div> </div>
<div className="table-responsive px-1 my-1 px-sm-0"> <div className="table-responsive px-1 my-1 px-sm-0" style={{maxHeight:'200px'}}>
<table className="table align-middle mb-0"> <table className="table align-middle mb-0">
<thead className="table-light"> <thead className="table-light">
<tr> <tr>

View File

@ -124,7 +124,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
}, },
{ {
key: "expensesType", key: "expensesType",
label: "Expense Type", label: "Expense Category",
getValue: (e) => e.expensesType?.name || "N/A", getValue: (e) => e.expensesType?.name || "N/A",
align: "text-start", align: "text-start",
}, },
@ -250,10 +250,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
(col.isAlwaysVisible || groupBy !== col.key) && ( (col.isAlwaysVisible || groupBy !== col.key) && (
<th <th
key={col.key} key={col.key}
className={`sorting d-table-cell`} className={`sorting d-table-cell `}
aria-sort="descending" aria-sort="descending"
> >
<div className={`${col.align}`}>{col.label}</div> <div className={`${col.align} p`}>{col.label}</div>
</th> </th>
) )
)} )}
@ -267,8 +267,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
Object.values(grouped).map(({ key, displayField, items }) => ( Object.values(grouped).map(({ key, displayField, items }) => (
<React.Fragment key={key}> <React.Fragment key={key}>
<tr className="tr-group text-dark"> <tr className="tr-group text-dark">
<td colSpan={8} className="text-start"> <td colSpan={8} className="text-start ">
<div className="d-flex align-items-center"> <div className="d-flex align-items-center px-2">
{" "} {" "}
<small className="fs-6 py-1"> <small className="fs-6 py-1">
{displayField} :{" "} {displayField} :{" "}

View File

@ -0,0 +1,53 @@
import React from "react";
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
const Filelist = ({ files, removeFile, expenseToEdit }) => {
return (
<div className="d-block">
{files
.filter((file) => {
if (expenseToEdit) {
return file.isActive;
}
return true;
})
.map((file, idx) => (
<div className="col-12 col-sm-6 col-md-4 col-lg-8 bg-white shadow-sm rounded p-2 m-2">
<div className="row align-items-center">
{/* File icon and info */}
<div className="col-10 d-flex align-items-center gap-2">
<i
className={`bx ${getIconByFileType(
file?.contentType
)} fs-3`}
style={{ minWidth: "30px" }}
></i>
<div className="d-flex flex-column text-truncate">
<span className="fw-medium small text-truncate">
{file.fileName}
</span>
<span className="text-body-secondary small">
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</span>
</div>
</div>
<div className="col-2 text-end">
<i
className="bx bx-trash fs-4 cursor-pointer text-danger bx-sm "
role="button"
onClick={(e) => {
e.preventDefault();
removeFile(expenseToEdit ? file.documentId : idx);
}}
></i>
</div>
</div>
</div>
))}
</div>
);
};
export default Filelist;

View File

@ -29,6 +29,7 @@ import DatePicker from "../common/DatePicker";
import ErrorPage from "../../pages/ErrorPage"; import ErrorPage from "../../pages/ErrorPage";
import Label from "../common/Label"; import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput"; import EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const { const {
@ -330,7 +331,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
control={control} control={control}
name="paidById" name="paidById"
projectId={null} projectId={null}
forAll={expenseToEdit ? true :false} forAll={expenseToEdit ? true : false}
/> />
</div> </div>
</div> </div>
@ -342,6 +343,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</Label> </Label>
<DatePicker <DatePicker
name="transactionDate" name="transactionDate"
className="w-100"
control={control} control={control}
maxDate={new Date()} maxDate={new Date()}
/> />
@ -512,42 +514,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{errors.billAttachments.message} {errors.billAttachments.message}
</small> </small>
)} )}
{files.length > 0 && ( {files.length > 0 && <Filelist files={files} removeFile={removeFile} expenseToEdit={expenseToEdit}/>}
<div className="d-block">
{files
.filter((file) => {
if (expenseToEdit) {
return file.isActive;
}
return true;
})
.map((file, idx) => (
<a
key={idx}
className="d-flex justify-content-between text-start p-1"
href={file.preSignedUrl || "#"}
target="_blank"
rel="noopener noreferrer"
>
<div>
<span className="mb-0 text-secondary small d-block">
{file.fileName}
</span>
<span className="text-body-secondary small d-block">
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</span>
</div>
<i
className="bx bx-trash bx-sm cursor-pointer text-danger"
onClick={(e) => {
e.preventDefault();
removeFile(expenseToEdit ? file.documentId : idx);
}}
></i>
</a>
))}
</div>
)}
{Array.isArray(errors.billAttachments) && {Array.isArray(errors.billAttachments) &&
errors.billAttachments.map((fileError, index) => ( errors.billAttachments.map((fileError, index) => (

View File

@ -47,7 +47,7 @@ const DatePicker = ({
const displayValue = value ? flatpickr.formatDate(new Date(value), "d-m-Y") : ""; const displayValue = value ? flatpickr.formatDate(new Date(value), "d-m-Y") : "";
return ( return (
<div className={`position-relative ${className}`}> <div className={`position-relative ${className} w-max `}>
<input <input
type="text" type="text"
className="form-control form-control-sm" className="form-control form-control-sm"

View File

@ -63,30 +63,27 @@ const DateRangePicker = ({
}; };
return ( return (
<div className={`position-relative w-auto justify-content-center`}> <div className={`position-relative w-max `}>
<input <input
type="text" type="text"
className="form-control form-control-sm w-100 pe-8 " className="form-control form-control-sm ps-2 pe-3 me-2 cursor-pointer fw-medium"
placeholder="From to End" placeholder="From to End"
id="flatpickr-range" id="flatpickr-range"
ref={inputRef} ref={inputRef}
/> />
<i <span
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 end-0 translate-middle-y me-2 " className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
onClick={handleIconClick} onClick={handleIconClick}
/> >
<i className="bx bx-calendar bx-sm fs-5 text-muted"></i>
</span>
</div> </div>
); );
}; };
export default DateRangePicker; export default DateRangePicker;
export const DateRangePicker1 = ({ export const DateRangePicker1 = ({
startField = "startDate", startField = "startDate",
endField = "endDate", endField = "endDate",
@ -173,16 +170,15 @@ export const DateRangePicker1 = ({
} }
}, [resetSignal, defaultRange, setValue, startField, endField]); }, [resetSignal, defaultRange, setValue, startField, endField]);
const start = getValues(startField); const start = getValues(startField);
const end = getValues(endField); const end = getValues(endField);
const formattedValue = start && end ? `${start} To ${end}` : ""; const formattedValue = start && end ? `${start} To ${end}` : "";
return ( return (
<div className={`position-relative ${className}`}> <div className={`position-relative ${className} w-max `}>
<input <input
type="text" type="text"
className="form-control form-control-sm ps-2 pe-5 me-4 cursor-pointer" className="form-control form-control-sm ps-2 pe-3 me-2 cursor-pointer fw-medium"
placeholder={placeholder} placeholder={placeholder}
defaultValue={formattedValue} defaultValue={formattedValue}
ref={(el) => { ref={(el) => {
@ -201,4 +197,3 @@ export const DateRangePicker1 = ({
</div> </div>
); );
}; };

View File

@ -19,3 +19,14 @@ const Loader = () => {
export default Loader; export default Loader;
export const SpinnerLoader = ()=>{
return (
<div className="text-center">
<div className="spinner-border text-primary mb-3" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="text-secondary mb-0">Loading attendance data...</p>
</div>
)
}

View File

@ -169,7 +169,7 @@ const AttendancePage = () => {
</div> </div>
{/* Search + Organization filter */} {/* Search + Organization filter */}
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto d-flex gap-2 align-items-center"> <div className="col-12 col-md-auto pb-2 mt-md-0 ms-md-auto d-flex gap-2 align-items-center nav-tabs">
{/* Organization Dropdown */} {/* Organization Dropdown */}
{/* <select {/* <select
className="form-select form-select-sm" className="form-select form-select-sm"
@ -206,7 +206,7 @@ const AttendancePage = () => {
</div> </div>
</div> </div>
<div className="tab-content attedanceTabs py-0 px-1 px-sm-3 pb-10"> <div className="tab-content attedanceTabs py-0 px-1 px-sm-3 pb-10 page-min-h">
<> <>
{activeTab === "all" && ( {activeTab === "all" && (

View File

@ -51,17 +51,29 @@ export const useDebounce = (value, delay = 500) => {
export const getIconByFileType = (type = "") => { export const getIconByFileType = (type = "") => {
const lower = type.toLowerCase(); const lower = type.toLowerCase();
if (lower === "application/pdf") return "bxs-file-pdf"; const map = [
if (lower.includes("word")) return "bxs-file-doc"; { match: "pdf", icon: "bxs-file-pdf", color: "text-danger" },
if (lower.includes("excel") || lower.includes("spreadsheet")) { match: "word", icon: "bxs-file-doc", color: "text-primary" },
return "bxs-file-xls"; { match: "excel", icon: "bxs-file-xls", color: "text-success" },
if (lower === "image/png") return "bxs-file-png"; { match: "spreadsheet", icon: "bxs-file-xls", color: "text-success" },
if (lower === "image/jpeg" || lower === "image/jpg") return "bxs-file-jpg"; { match: "zip", icon: "bxs-file-archive", color: "text-warning" },
if (lower.includes("zip") || lower.includes("rar")) return "bxs-file-archive"; { match: "rar", icon: "bxs-file-archive", color: "text-warning" },
{ match: "png", icon: "bxs-file-png", color: "text-info" },
{ match: "jpg", icon: "bxs-file-jpg", color: "text-info" },
{ match: "jpeg", icon: "bxs-file-jpg", color: "text-info" },
{ match: "image", icon: "bxs-file-image", color: "text-info" },
];
return "bx bx-file"; // Find first match
const found = map.find((m) => lower.includes(m.match));
// Default if nothing matched
return found
? `bx ${found.icon} ${found.color}`
: "bx bx-file text-secondary";
}; };
export const normalizeAllowedContentTypes = (allowedContentType) => { export const normalizeAllowedContentTypes = (allowedContentType) => {
if (!allowedContentType) return []; if (!allowedContentType) return [];
if (Array.isArray(allowedContentType)) return allowedContentType; if (Array.isArray(allowedContentType)) return allowedContentType;