Merge pull request 'issues_Oct_4W' (#498) from issues_Oct_4W into OnFieldWork_V1
Reviewed-on: #498 Merged
This commit is contained in:
commit
d8712c0d04
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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{" "}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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} :{" "}
|
||||||
|
|||||||
53
src/components/Expenses/Filelist.jsx
Normal file
53
src/components/Expenses/Filelist.jsx
Normal 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;
|
||||||
@ -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) => (
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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" && (
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user