Compare commits
197 Commits
main
...
Issues_Exp
| Author | SHA1 | Date | |
|---|---|---|---|
| a837eaab72 | |||
| ee456f5ae1 | |||
| 0729723d5e | |||
| 72e5cf0bbe | |||
| 1278e32da9 | |||
| 28f15f649f | |||
| f18633b3d5 | |||
| 07eedfc9e2 | |||
| 9d35d00009 | |||
| 35956cc9bb | |||
| 41377167e6 | |||
| 428f22ea8e | |||
| 4d40b8e149 | |||
| e4585d6c43 | |||
| c3e030754f | |||
| deda915e74 | |||
| dfaf90ccac | |||
| a253eea33c | |||
| d9e1bfac97 | |||
| 06e8046dde | |||
| 82f173c0ed | |||
| 1f784f330d | |||
| 4ae0b403a6 | |||
| 33256981da | |||
| 1ca1f15422 | |||
| 84ed1984d7 | |||
| b668faab33 | |||
| 8229a44a74 | |||
| 324ad05771 | |||
| 418979568d | |||
| bac9f25df1 | |||
| e1e9920b3e | |||
| 9b091840d6 | |||
| b897a41f95 | |||
| ec2a1706fc | |||
| cb0ed03525 | |||
| 0372991ac1 | |||
| ac9eaa1e67 | |||
| daaebf919d | |||
| 92fa1c9a3d | |||
| c0f30397ef | |||
| 00f405069b | |||
| 2a46ac1349 | |||
| f4838441aa | |||
| c63b13f200 | |||
| 9b65c12239 | |||
| 10ec11a828 | |||
| 3b318a672c | |||
| fba19f6ead | |||
| 4069720ed0 | |||
| e8698473db | |||
| f688a7169e | |||
| 69e60caf23 | |||
| ecf34b499e | |||
| e868d27d5f | |||
| 0dc68eb20d | |||
| 7a749b14e9 | |||
| 76fe342c1e | |||
| 44a7749cd3 | |||
| 611116b70c | |||
| e79702661d | |||
| 10ff3ecd2f | |||
| bede21aff2 | |||
| bce69783f7 | |||
| a8f83571ee | |||
| 616b986deb | |||
| 57d33ab817 | |||
| 5bf5e18c6b | |||
| 190612feed | |||
| 346ef0174b | |||
| 192c04fb6f | |||
| 3aa9934f8b | |||
| d02b14272f | |||
| edafc204b8 | |||
| 1e14bfe0b1 | |||
| 1c4384a62e | |||
| 859eaa747f | |||
| 871daf6036 | |||
| 59c46f7f3b | |||
| e80223beba | |||
| de2d23ec83 | |||
| f3cfb3cf24 | |||
| 0e3c78df11 | |||
| e0c0a14777 | |||
| 17a08304e3 | |||
| c13e0acd3d | |||
| b0a58740a5 | |||
| c123373892 | |||
| 9c03303547 | |||
| d8712c0d04 | |||
| 5f0972ee32 | |||
| 0d1d51d56b | |||
| 166b0a1caf | |||
| 196069b39a | |||
| 10c2e9dfee | |||
| 494b1b2b77 | |||
| 465b67e25c | |||
| ec04426e52 | |||
| dc75121d31 | |||
| d3acf13c0a | |||
| cc4cea0f27 | |||
| 02777a8f47 | |||
| 7c5dca1665 | |||
| 0d0b7f2cbc | |||
| 394d28f80d | |||
| 95c6e71be8 | |||
| 0091b1064e | |||
| cde6794697 | |||
| 78b721dad7 | |||
| a8ae0fac70 | |||
| b318d469f6 | |||
| 73534226e3 | |||
| 9bbe479153 | |||
| ce976a6d5c | |||
| d1af4f402d | |||
| 105beb6062 | |||
| f61c7a4a35 | |||
| dacd72f945 | |||
| a2177dc2af | |||
| a9d1ba08dd | |||
| 9526cb0e5e | |||
| 432cf2122e | |||
| 765b4356a6 | |||
| 6125130cff | |||
| 006ca21e1d | |||
| bb2d7b8923 | |||
| c921dbff09 | |||
| e84da2dcb0 | |||
| 4ba7c72e78 | |||
| 9939973d8d | |||
| e17c2064ac | |||
| 9580c9234c | |||
| 44b3f842f2 | |||
| 9de0b1a0df | |||
| fbb68a4488 | |||
| c1a8b87a2c | |||
| 24501b3c76 | |||
| ef00516256 | |||
| f867a692ed | |||
| 85b4b830d3 | |||
| c1c333008f | |||
| 5370d0eb9d | |||
| 5cbd6c427a | |||
| 5057738d8b | |||
| 063bbee681 | |||
| 3dc667db73 | |||
| 2f2c31db9f | |||
| 0dc540eec2 | |||
| fc7c33ee54 | |||
| eeb886ec36 | |||
| 210b01513e | |||
| 2fe297c6b6 | |||
| ffd7f15488 | |||
| 7f33d5e6bd | |||
| 108ca1cc96 | |||
| 0b1a276898 | |||
| 6246dbd23f | |||
| af3bf11beb | |||
| 2e5951d9d1 | |||
| cdc5e32554 | |||
| bc047a84fc | |||
| 0511ed6d82 | |||
| bc933f4c64 | |||
| bd3645aae6 | |||
| 2f2215ab8a | |||
| 071f862743 | |||
| 040331e27d | |||
| db2730c02b | |||
| 9df813698d | |||
| 6ba018399c | |||
| aeb9778216 | |||
| 81ed65c0c6 | |||
| 1e49cf17a2 | |||
| ab5e0b2f79 | |||
| 7c9d2ddeb1 | |||
| f0f579beae | |||
| b53e6284b9 | |||
| b1c23aab4d | |||
| 4a05e67ff0 | |||
| 87d13d0f77 | |||
| 997971629f | |||
| 18ac8e11bd | |||
| 78808ecac0 | |||
| 3553a7b521 | |||
| aca96c60ae | |||
| 25f4f1e7a7 | |||
| 0c1889e1c1 | |||
| 9884943907 | |||
| 2531d91209 | |||
| 49b597c833 | |||
| 2aae7194b7 | |||
| ae772d925a | |||
| c1e5ff4043 | |||
| 2aff3b9e80 | |||
| 4e48478fbc | |||
| 4ea1e06a9c | |||
| 4ddb8415cc |
@ -9,6 +9,8 @@
|
||||
|
||||
<meta name="description" content="" />
|
||||
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/img/favicon.ico" />
|
||||
|
||||
@ -29,6 +31,8 @@
|
||||
<link rel="stylesheet" href="/assets/css/default.css" />
|
||||
<link rel="stylesheet" href="/assets/css/skeleton.css" />
|
||||
<link rel="stylesheet" href="/assets/css/hover-utility.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-green.css" />
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
|
||||
@ -105,6 +109,9 @@
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script> -->
|
||||
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> -->
|
||||
|
||||
|
||||
<!-- Flatpickr JS -->
|
||||
|
||||
</body>
|
||||
|
||||
@ -9,4 +9,129 @@
|
||||
}
|
||||
.table_header_border {
|
||||
border-bottom:2px solid var(--bs-table-border-color) ;
|
||||
}
|
||||
}
|
||||
.text-gary-80 {
|
||||
color:var(--bs-gray-500)
|
||||
}
|
||||
|
||||
.text-royalblue{
|
||||
color: #1796e3;
|
||||
}
|
||||
|
||||
.text-md {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.text-md-b {
|
||||
font-weight: normal;
|
||||
}
|
||||
.text-xxs { font-size: 0.55rem; } /* 8px */
|
||||
.text-xs { font-size: 0.75rem; } /* 12px */
|
||||
.text-sm { font-size: 0.875rem; } /* 14px */
|
||||
.text-base { font-size: 1rem; } /* 16px */
|
||||
.text-lg { font-size: 1.125rem; } /* 18px */
|
||||
.text-xl { font-size: 1.25rem; } /* 20px */
|
||||
.text-2xl { font-size: 1.5rem; } /* 24px */
|
||||
.text-3xl { font-size: 1.875rem; } /* 30px */
|
||||
.text-4xl { font-size: 2.25rem; } /* 36px */
|
||||
.text-5xl { font-size: 3rem; } /* 48px */
|
||||
.text-6xl { font-size: 3.75rem; } /* 60px */
|
||||
.text-7xl { font-size: 4.5rem; } /* 72px */
|
||||
.text-8xl { font-size: 6rem; } /* 96px */
|
||||
.text-9xl { font-size: 8rem; } /* 128px */
|
||||
|
||||
|
||||
/* */
|
||||
|
||||
.w-0 { width: 0px; }
|
||||
.w-px { width: 1px; }
|
||||
.w-1 { width: 0.25rem; } /* 4px */
|
||||
.w-2 { width: 0.5rem; } /* 8px */
|
||||
.w-3 { width: 0.75rem; } /* 12px */
|
||||
.w-4 { width: 1rem; } /* 16px */
|
||||
.w-5 { width: 1.25rem; } /* 20px */
|
||||
.w-6 { width: 1.5rem; } /* 24px */
|
||||
.w-8 { width: 2rem; } /* 32px */
|
||||
.w-10 { width: 2.5rem; } /* 40px */
|
||||
.w-12 { width: 3rem; } /* 48px */
|
||||
.w-16 { width: 4rem; } /* 64px */
|
||||
.w-20 { width: 5rem; } /* 80px */
|
||||
.w-24 { width: 6rem; } /* 96px */
|
||||
.w-32 { width: 8rem; } /* 128px */
|
||||
.w-40 { width: 10rem; } /* 160px */
|
||||
.w-48 { width: 12rem; } /* 192px */
|
||||
.w-56 { width: 14rem; } /* 224px */
|
||||
.w-64 { width: 16rem; } /* 256px */
|
||||
.w-auto { width: auto; }
|
||||
.w-full { width: 100%; }
|
||||
.w-screen{ width: 100vw; }
|
||||
.w-min { width: min-content; }
|
||||
.w-max { width: max-content; }
|
||||
|
||||
|
||||
|
||||
.h-0 { height: 0px; }
|
||||
.h-px { height: 1px; }
|
||||
.h-1 { height: 0.25rem; } /* 4px */
|
||||
.h-2 { height: 0.5rem; } /* 8px */
|
||||
.h-3 { height: 0.75rem; } /* 12px */
|
||||
.h-4 { height: 1rem; } /* 16px */
|
||||
.h-5 { height: 1.25rem; } /* 20px */
|
||||
.h-6 { height: 1.5rem; } /* 24px */
|
||||
.h-8 { height: 2rem; } /* 32px */
|
||||
.h-10 { height: 2.5rem; } /* 40px */
|
||||
.h-12 { height: 3rem; } /* 48px */
|
||||
.h-16 { height: 4rem; } /* 64px */
|
||||
.h-20 { height: 5rem; } /* 80px */
|
||||
.h-24 { height: 6rem; } /* 96px */
|
||||
.h-32 { height: 8rem; } /* 128px */
|
||||
.h-40 { height: 10rem; } /* 160px */
|
||||
.h-48 { height: 12rem; } /* 192px */
|
||||
.h-56 { height: 14rem; } /* 224px */
|
||||
.h-64 { height: 16rem; } /* 256px */
|
||||
.h-auto { height: auto; }
|
||||
.h-full { height: 100%; }
|
||||
.h-screen{ height: 100vh; }
|
||||
.h-min { height: min-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;
|
||||
}
|
||||
|
||||
55
public/assets/css/theme-green.css
Normal file
@ -0,0 +1,55 @@
|
||||
.btn-green {
|
||||
background-color: #49bf3c;
|
||||
color: #fff;
|
||||
border-radius: 50px;
|
||||
padding: 10px 30px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-green-outline {
|
||||
border-color: #49bf3c;
|
||||
background-color: transparent;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.btn-green-outline:hover {
|
||||
background-color: #49bf3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-square-small {
|
||||
border-radius: 3px;
|
||||
padding-bottom: 5.072px;
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: 12px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 5.072px;
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background-color: #00a85a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.text-blue {
|
||||
color: #696cff !important;
|
||||
}
|
||||
.text-green {
|
||||
color: #49bf3c !important;
|
||||
}
|
||||
|
||||
.btn-outline-green {
|
||||
border-radius: 50px;
|
||||
padding: 10px 30px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-outline-green:hover {
|
||||
background-color: #49bf3c;
|
||||
color: #fff;
|
||||
}
|
||||
3
public/assets/vendor/css/core.css
vendored
@ -32573,4 +32573,7 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
|
||||
}
|
||||
.text-red{
|
||||
color:var(--bs-red)
|
||||
}
|
||||
.bg-gray {
|
||||
background:var(--bs-body-color)
|
||||
}
|
||||
BIN
public/img/app/mobile/01.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/img/app/mobile/02.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
BIN
public/img/brand/ofw-500x500.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
public/img/favicon/favicon1.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/hero/bg-01.jpg
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
public/img/hero/bg-012.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
public/img/hero/bg-02.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
public/img/hero/bg-03.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/img/hero/bg-1.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/img/hero/bg-2.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
public/img/hero/bg-3.jpeg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 860 KiB After Width: | Height: | Size: 860 KiB |
BIN
public/img/icons/ai.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/img/icons/apple-icon-lite.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/icons/attendance.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/img/icons/cloud-service.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/icons/dashboard.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/img/icons/directory.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/img/icons/document.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/img/icons/google-play-icon-lite.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/icons/profile.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/img/icons/report.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/icons/spending.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/illustrations/contact-us.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
@ -6,7 +6,12 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { queryClient } from "./layouts/AuthLayout";
|
||||
import ModalProvider from "./ModalProvider";
|
||||
|
||||
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
if (event.reason?.message?.includes("Failed to fetch")) {
|
||||
event.preventDefault();
|
||||
console.debug("Network issue (fetch failed) - suppressed");
|
||||
}
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useOrganizationModal } from "./hooks/useOrganization";
|
||||
import OrganizationModal from "./components/Organization/OrganizationModal";
|
||||
import { useAuthModal } from "./hooks/useAuth";
|
||||
import { useAuthModal, useModal } from "./hooks/useAuth";
|
||||
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
||||
import { ProjectModal } from "./components/Project/ManageProjectInfo";
|
||||
|
||||
const ModalProvider = () => {
|
||||
const { isOpen, onClose } = useOrganizationModal();
|
||||
const { isOpen: isAuthOpen } = useAuthModal();
|
||||
const {isOpen:isOpenProject} = useModal("ManageProject")
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <OrganizationModal />}
|
||||
{isAuthOpen && <SwitchTenant />}
|
||||
{isOpenProject && <ProjectModal/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -123,15 +123,19 @@ const AttendLogs = ({ Id }) => {
|
||||
}, []);
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<div className="text-start">
|
||||
<div className="mb-3">
|
||||
<h5 className="mb-4">Attendance Logs</h5>
|
||||
{logs && !loading && (
|
||||
<p>
|
||||
Attendance logs for{" "}
|
||||
{logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName}{" "}
|
||||
on {formatUTCToLocalTime(logs[0]?.activityTime)}
|
||||
<p className="mb-0 text-start">
|
||||
Showing logs for{" "}
|
||||
<strong>
|
||||
{logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName}
|
||||
</strong>{" "}
|
||||
on <strong>{formatUTCToLocalTime(logs[0]?.activityTime)}</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <p>Loading..</p>}
|
||||
{logs && logs.length > 0 && (
|
||||
<>
|
||||
@ -142,9 +146,9 @@ const AttendLogs = ({ Id }) => {
|
||||
<table className="table table-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Activity</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Activity</th>
|
||||
<th>Location</th>
|
||||
<th>Recored By</th>
|
||||
<th>Description</th>
|
||||
@ -156,11 +160,16 @@ const AttendLogs = ({ Id }) => {
|
||||
.sort((a, b) => b.id - a.id)
|
||||
.map((log, index) => (
|
||||
<tr key={index}>
|
||||
<td>{formatUTCToLocalTime(log.activityTime)}</td>
|
||||
<td>{convertShortTime(log.activityTime)}</td>
|
||||
<td>
|
||||
{whichActivityPerform(log.activity, log.activityTime)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="py-2">
|
||||
{formatUTCToLocalTime(log.activityTime)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{convertShortTime(log.activityTime)}</td>
|
||||
|
||||
<td>
|
||||
{log?.latitude != 0 ? (
|
||||
<i
|
||||
@ -179,9 +188,8 @@ const AttendLogs = ({ Id }) => {
|
||||
)}
|
||||
</td>
|
||||
<td className="text-wrap">
|
||||
{`${log?.updatedByEmployee?.firstName ?? ""} ${
|
||||
log?.updatedByEmployee?.lastName ?? ""
|
||||
}`}
|
||||
{`${log?.updatedByEmployee?.firstName ?? ""} ${log?.updatedByEmployee?.lastName ?? ""
|
||||
}`}
|
||||
</td>
|
||||
<td className="text-wrap" colSpan={3}>
|
||||
{log?.comment?.length > 50
|
||||
|
||||
@ -11,8 +11,18 @@ import { useSelector } from "react-redux";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import eventBus from "../../services/eventBus";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
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 [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@ -23,12 +33,12 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
||||
attendance,
|
||||
loading: attLoading,
|
||||
recall: attrecall,
|
||||
isFetching
|
||||
isFetching,
|
||||
} = useAttendance(selectedProject, organizationId, includeInactive, date);
|
||||
const filteredAttendance = ShowPending
|
||||
? attendance?.filter(
|
||||
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
||||
)
|
||||
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
||||
)
|
||||
: attendance;
|
||||
|
||||
const attendanceList = Array.isArray(filteredAttendance)
|
||||
@ -70,19 +80,19 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
||||
);
|
||||
|
||||
// Reset pagination when the filter or search term changes
|
||||
useEffect(() => {
|
||||
}, [finalFilteredData]);
|
||||
|
||||
useEffect(() => {}, [finalFilteredData]);
|
||||
|
||||
const handler = useCallback(
|
||||
(msg) => {
|
||||
if (selectedProject == msg.projectId) {
|
||||
queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
|
||||
if (!oldData) {
|
||||
queryClient.invalidateQueries({ queryKey: ["attendance"] })
|
||||
};
|
||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||
}
|
||||
return oldData.map((record) =>
|
||||
record.employeeId === msg.response.employeeId ? { ...record, ...msg.response } : record
|
||||
record.employeeId === msg.response.employeeId
|
||||
? { ...record, ...msg.response }
|
||||
: record
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -110,179 +120,147 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="table-responsive text-nowrap h-100"
|
||||
style={{ minHeight: "200px" }} // Ensures fixed height
|
||||
>
|
||||
<div className="d-flex text-start align-items-center py-2">
|
||||
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
||||
<div className="form-check form-switch text-start m-0 ms-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
disabled={isFetching}
|
||||
checked={ShowPending}
|
||||
onChange={(e) => setShowPending(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label ms-0">Show Pending</label>
|
||||
<div>
|
||||
<div className="table-responsive text-nowrap ">
|
||||
<div className="d-flex justify-content-between align-items-center py-2">
|
||||
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
||||
<div className="form-check form-switch text-start m-0 ms-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
disabled={isFetching}
|
||||
checked={ShowPending}
|
||||
onChange={(e) => setShowPending(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label ms-0">Show Pending</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{attLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
<>
|
||||
<table className="table ">
|
||||
<thead>
|
||||
<tr className="border-top-1">
|
||||
<th colSpan={2}>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Organization</th>
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>
|
||||
Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0 ">
|
||||
{currentItems &&
|
||||
currentItems
|
||||
.sort((a, b) => {
|
||||
const checkInA = a?.checkInTime
|
||||
? new Date(a.checkInTime)
|
||||
: new Date(0);
|
||||
const checkInB = b?.checkInTime
|
||||
? new Date(b.checkInTime)
|
||||
: new Date(0);
|
||||
return checkInB - checkInA;
|
||||
})
|
||||
.map((item) => (
|
||||
<tr key={item.employeeId}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar
|
||||
firstName={item.firstName}
|
||||
lastName={item.lastName}
|
||||
></Avatar>
|
||||
<div className="d-flex flex-column">
|
||||
<a
|
||||
onClick={(e) =>
|
||||
navigate(
|
||||
`/employee/${item.employeeId}?for=attendance`
|
||||
)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{item.firstName} {item.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{item.jobRoleName}</td>
|
||||
<td>{item.organizationName || "--"}</td>
|
||||
|
||||
<td>
|
||||
{item.checkInTime
|
||||
? convertShortTime(item.checkInTime)
|
||||
: "--"}
|
||||
</td>
|
||||
<td>
|
||||
{item.checkOutTime
|
||||
? convertShortTime(item.checkOutTime)
|
||||
: "--"}
|
||||
</td>
|
||||
|
||||
<td className="text-center">
|
||||
<RenderAttendanceStatus
|
||||
attendanceData={item}
|
||||
handleModalData={handleModalData}
|
||||
Tab={1}
|
||||
currentDate={null}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!attendance && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="text-center text-secondary"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
No employees assigned to the project!
|
||||
</td>
|
||||
{attLoading ? (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "70vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
<>
|
||||
<table className="table table-hover ">
|
||||
<thead>
|
||||
<tr className="border-top-1">
|
||||
<th colSpan={2}>Name</th>
|
||||
<th className="text-start actions-col text-center">Role</th>
|
||||
{/* <th>Organization</th> */}
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>
|
||||
Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>
|
||||
Check-Out
|
||||
</th>
|
||||
<th className="actions-col">Actions</th>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0 ">
|
||||
{currentItems &&
|
||||
currentItems
|
||||
.sort((a, b) => {
|
||||
const checkInA = a?.checkInTime
|
||||
? new Date(a.checkInTime)
|
||||
: new Date(0);
|
||||
const checkInB = b?.checkInTime
|
||||
? new Date(b.checkInTime)
|
||||
: new Date(0);
|
||||
return checkInB - checkInA;
|
||||
})
|
||||
.map((item) => (
|
||||
<tr key={item.employeeId}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar
|
||||
firstName={item.firstName}
|
||||
lastName={item.lastName}
|
||||
></Avatar>
|
||||
<div className="d-flex flex-column">
|
||||
<a
|
||||
onClick={(e) =>
|
||||
navigate(
|
||||
`/employee/${item.employeeId}?for=attendance`
|
||||
)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{item.firstName} {item.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="text-start action-col">{item.jobRoleName}</td>
|
||||
{/* <td>{item.organizationName || "--"}</td> */}
|
||||
|
||||
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||
<nav aria-label="Page ">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||
<li
|
||||
className={`page-item ${currentPage === 1 ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
onClick={() => paginate(index + 1)}
|
||||
<td>
|
||||
{item.checkInTime
|
||||
? convertShortTime(item.checkInTime)
|
||||
: "--"}
|
||||
</td>
|
||||
<td>
|
||||
{item.checkOutTime
|
||||
? convertShortTime(item.checkOutTime)
|
||||
: "--"}
|
||||
</td>
|
||||
|
||||
<td className="text-center actions-col">
|
||||
<RenderAttendanceStatus
|
||||
attendanceData={item}
|
||||
handleModalData={handleModalData}
|
||||
Tab={1}
|
||||
currentDate={null}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!attendance && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="text-center text-secondary"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center text-muted"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: attendanceList.length === 0
|
||||
No employees assigned to the project!
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center text-muted"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: attendanceList.length === 0
|
||||
? "No employees assigned to the project."
|
||||
: "No pending records available."}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Attendance;
|
||||
export default Attendance;
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import moment from "moment";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { convertShortTime } from "../../utils/dateUtils";
|
||||
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import RenderAttendanceStatus from "./RenderAttendanceStatus";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
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 AttendanceRepository from "../../repositories/AttendanceRepository";
|
||||
import { useAttendancesLogs } from "../../hooks/useAttendance";
|
||||
import { queryClient } from "../../layouts/AuthLayout";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
const usePagination = (data, itemsPerPage) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@ -33,18 +40,16 @@ const usePagination = (data, itemsPerPage) => {
|
||||
};
|
||||
};
|
||||
|
||||
const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
// const selectedProject = useSelector(
|
||||
// (store) => store.localVariables.projectId
|
||||
// );
|
||||
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
const selectedProject = useSelectedProject();
|
||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPending, setShowPending] = useState(false)
|
||||
const [showPending, setShowPending] = useState(false);
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [processedData, setProcessedData] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@ -84,59 +89,64 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
dateRange.endDate,
|
||||
organizationId
|
||||
);
|
||||
const filtering = (data) => {
|
||||
const filteredData = showPending
|
||||
? data.filter((item) => item.checkOutTime === null)
|
||||
: data;
|
||||
const filtering = useCallback(
|
||||
(dataToFilter) => {
|
||||
const filteredData = showPending
|
||||
? dataToFilter.filter((item) => item.checkOutTime === null)
|
||||
: dataToFilter;
|
||||
|
||||
const group1 = filteredData
|
||||
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
|
||||
.sort(sortByName);
|
||||
const group2 = filteredData
|
||||
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
|
||||
.sort(sortByName);
|
||||
const group3 = filteredData
|
||||
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
|
||||
.sort(sortByName);
|
||||
const group4 = filteredData.filter(
|
||||
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
|
||||
);
|
||||
const group5 = filteredData
|
||||
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
|
||||
.sort(sortByName);
|
||||
const group6 = filteredData
|
||||
.filter((d) => d.activity === 5)
|
||||
.sort(sortByName);
|
||||
const group1 = filteredData
|
||||
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
|
||||
.sort(sortByName);
|
||||
const group2 = filteredData
|
||||
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
|
||||
.sort(sortByName);
|
||||
const group3 = filteredData
|
||||
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
|
||||
.sort(sortByName);
|
||||
const group4 = filteredData.filter(
|
||||
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
|
||||
);
|
||||
const group5 = filteredData
|
||||
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
|
||||
.sort(sortByName);
|
||||
const group6 = filteredData
|
||||
.filter((d) => d.activity === 5)
|
||||
.sort(sortByName);
|
||||
|
||||
const sortedList = [
|
||||
...group1,
|
||||
...group2,
|
||||
...group3,
|
||||
...group4,
|
||||
...group5,
|
||||
...group6,
|
||||
];
|
||||
const sortedList = [
|
||||
...group1,
|
||||
...group2,
|
||||
...group3,
|
||||
...group4,
|
||||
...group5,
|
||||
...group6,
|
||||
];
|
||||
|
||||
// Group by date
|
||||
const groupedByDate = sortedList.reduce((acc, item) => {
|
||||
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
||||
if (date) {
|
||||
acc[date] = acc[date] || [];
|
||||
acc[date].push(item);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
// Group by date
|
||||
const groupedByDate = sortedList.reduce((acc, item) => {
|
||||
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
||||
if (date) {
|
||||
acc[date] = acc[date] || [];
|
||||
acc[date].push(item);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const sortedDates = Object.keys(groupedByDate).sort(
|
||||
(a, b) => new Date(b) - new Date(a)
|
||||
);
|
||||
const sortedDates = Object.keys(groupedByDate).sort(
|
||||
(a, b) => new Date(b) - new Date(a)
|
||||
);
|
||||
|
||||
const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
|
||||
setProcessedData(finalData);
|
||||
};
|
||||
const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
|
||||
setProcessedData(finalData);
|
||||
},
|
||||
[showPending]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
filtering(data);
|
||||
if (data?.length) {
|
||||
filtering(data);
|
||||
}
|
||||
}, [data, showPending]);
|
||||
|
||||
// New useEffect to handle search filtering
|
||||
@ -151,33 +161,6 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
});
|
||||
}, [processedData, searchTerm]);
|
||||
|
||||
// const filteredSearchData = useMemo(() => {
|
||||
// let tempData = processedData;
|
||||
|
||||
// if (searchTerm) {
|
||||
// const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
// tempData = tempData.filter((item) => {
|
||||
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||
// return fullName.includes(lowercasedSearchTerm);
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (filters?.selectedOrganization) {
|
||||
// tempData = tempData.filter(
|
||||
// (item) => item.organization?.name === filters.selectedOrganization
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (filters?.selectedServices?.length > 0) {
|
||||
// tempData = tempData.filter((item) =>
|
||||
// filters.selectedServices.includes(item.service?.name)
|
||||
// );
|
||||
// }
|
||||
|
||||
// return tempData;
|
||||
// }, [processedData, searchTerm, filters]);
|
||||
|
||||
|
||||
const {
|
||||
currentPage,
|
||||
totalPages,
|
||||
@ -235,7 +218,7 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
// })
|
||||
// );
|
||||
|
||||
refetch()
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[selectedProject, dateRange, data, refetch]
|
||||
@ -249,50 +232,42 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
>
|
||||
<div className="d-flex align-items-center my-0 ">
|
||||
<div className=" col-12">
|
||||
<DateRangePicker
|
||||
onRangeChange={setDateRange}
|
||||
defaultStartDate={yesterday}
|
||||
/>
|
||||
<div className="form-check form-switch text-start m-0 ms-5">
|
||||
<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 className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
|
||||
<div className="table-responsive text-nowrap ">
|
||||
{isLoading ? (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
||||
<p className="text-secondary">Loading...</p>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "70vh" }}
|
||||
>
|
||||
<SpinnerLoader/>
|
||||
</div>
|
||||
) : filteredSearchData?.length > 0 ? (
|
||||
<table className="table mb-0">
|
||||
<table className="table mb-0 table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border-top-1" colSpan={2}>
|
||||
Name
|
||||
</th>
|
||||
<th className="border-top-1">Date</th>
|
||||
<th>Organization</th>
|
||||
{/* <th>Organization</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>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
|
||||
</th>
|
||||
<th>Action</th>
|
||||
<th className="actions-col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -303,9 +278,9 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
const previousAttendance = arr[index - 1];
|
||||
const previousDate = previousAttendance
|
||||
? moment(
|
||||
previousAttendance.checkInTime ||
|
||||
previousAttendance.checkOutTime
|
||||
).format("YYYY-MM-DD")
|
||||
previousAttendance.checkInTime ||
|
||||
previousAttendance.checkOutTime
|
||||
).format("YYYY-MM-DD")
|
||||
: null;
|
||||
|
||||
if (!previousDate || currentDate !== previousDate) {
|
||||
@ -315,8 +290,8 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
className="table-row-header"
|
||||
>
|
||||
<td colSpan={8} className="text-start">
|
||||
<strong>
|
||||
{moment(currentDate).format("DD-MM-YYYY")}
|
||||
<strong className="d-inline-block my-1 ms-2">
|
||||
{formatUTCToLocalTime(currentDate)}
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
@ -331,7 +306,14 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
lastName={attendance.lastName}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<a href="#" className="text-heading text-truncate">
|
||||
<a
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/employee/${attendance.employeeId}?for=attendance`
|
||||
)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{attendance.firstName} {attendance.lastName}
|
||||
</span>
|
||||
@ -344,14 +326,14 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
attendance.checkInTime || attendance.checkOutTime
|
||||
).format("DD-MMM-YYYY")}
|
||||
</td>
|
||||
<td>{attendance.organizationName || "--"}</td>
|
||||
{/* <td>{attendance.organizationName || "--"}</td> */}
|
||||
<td>{convertShortTime(attendance.checkInTime)}</td>
|
||||
<td>
|
||||
{attendance.checkOutTime
|
||||
? convertShortTime(attendance.checkOutTime)
|
||||
: "--"}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<td className="text-center actions-col">
|
||||
<RenderAttendanceStatus
|
||||
attendanceData={attendance}
|
||||
handleModalData={handleModalData}
|
||||
@ -366,7 +348,14 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="my-12"><span className="text-secondary">No data available for the selected date range. Please Select another date.</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>
|
||||
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
||||
@ -378,45 +367,11 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||
</div>
|
||||
)}
|
||||
{filteredSearchData.length > ITEMS_PER_PAGE && (
|
||||
<nav aria-label="Page ">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(pageNumber) => (
|
||||
<li
|
||||
key={pageNumber}
|
||||
className={`page-item ${currentPage === pageNumber ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => paginate(pageNumber)}
|
||||
>
|
||||
{pageNumber}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -9,6 +9,7 @@ import showToast from "../../services/toastService";
|
||||
import { checkIfCurrentDate } from "../../utils/dateUtils";
|
||||
import { useMarkAttendance } from "../../hooks/useAttendance";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
|
||||
const createSchema = (modeldata) => {
|
||||
return z
|
||||
@ -19,31 +20,36 @@ const createSchema = (modeldata) => {
|
||||
.max(200, "Description should be less than 200 characters")
|
||||
.optional(),
|
||||
})
|
||||
.refine((data) => {
|
||||
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
|
||||
const checkIn = new Date(modeldata.checkInTime);
|
||||
const [time, modifier] = data.markTime.split(" ");
|
||||
const [hourStr, minuteStr] = time.split(":");
|
||||
let hour = parseInt(hourStr, 10);
|
||||
const minute = parseInt(minuteStr, 10);
|
||||
.refine(
|
||||
(data) => {
|
||||
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
|
||||
const checkIn = new Date(modeldata.checkInTime);
|
||||
const [time, modifier] = data.markTime.split(" ");
|
||||
const [hourStr, minuteStr] = time.split(":");
|
||||
let hour = parseInt(hourStr, 10);
|
||||
const minute = parseInt(minuteStr, 10);
|
||||
|
||||
if (modifier === "PM" && hour !== 12) hour += 12;
|
||||
if (modifier === "AM" && hour === 12) hour = 0;
|
||||
if (modifier === "PM" && hour !== 12) hour += 12;
|
||||
if (modifier === "AM" && hour === 12) hour = 0;
|
||||
|
||||
const checkOut = new Date(checkIn);
|
||||
checkOut.setHours(hour, minute, 0, 0);
|
||||
const checkOut = new Date(checkIn);
|
||||
checkOut.setHours(hour, minute, 0, 0);
|
||||
|
||||
return checkOut > checkIn;
|
||||
return checkOut >= checkIn;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Checkout time must be later than check-in time",
|
||||
path: ["markTime"],
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: "Checkout time must be later than check-in time",
|
||||
path: ["markTime"],
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
const [currentProject, setCurrentProject] = useState(null);
|
||||
const projectId = useSelectedProject();
|
||||
const { projectNames, loading } = useProjectName();
|
||||
const { mutate: MarkAttendance } = useMarkAttendance();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const coords = usePositionTracker();
|
||||
@ -95,17 +101,24 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && projectNames) {
|
||||
setCurrentProject(
|
||||
projectNames?.find((project) => project.id === projectId)
|
||||
);
|
||||
}
|
||||
}, [projectNames, projectId, loading]);
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 d-flex justify-content-center">
|
||||
<form className="row p-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 d-flex justify-content-center mt-2">
|
||||
<label className="fs-5 text-dark text-center">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
? "Check-out :"
|
||||
: "Check-in :"}
|
||||
? `Check out for ${currentProject?.name}`
|
||||
: `Check In for ${currentProject?.name}`}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="col-6 col-md-6 text-start">
|
||||
<label className="form-label" htmlFor="checkInDate">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
@ -207,7 +220,7 @@ export const Regularization = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="row " onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<p>Regularize Attendance</p>
|
||||
<label className="form-label" htmlFor="description">
|
||||
|
||||
@ -9,19 +9,34 @@ import usePagination from "../../hooks/usePagination";
|
||||
import eventBus from "../../services/eventBus";
|
||||
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
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();
|
||||
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
||||
const selectedProject = useSelectedProject();
|
||||
const [regularizesList, setregularizedList] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const { regularizes, loading, error, refetch } =
|
||||
useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
|
||||
|
||||
useEffect(() => {
|
||||
setregularizedList(regularizes);
|
||||
if (regularizes && regularizes.length) {
|
||||
setregularizedList((prev) => {
|
||||
const prevIds = prev.map((i) => i.id).join(",");
|
||||
const newIds = regularizes.map((i) => i.id).join(",");
|
||||
if (prevIds !== newIds) {
|
||||
return regularizes;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}, [regularizes]);
|
||||
|
||||
|
||||
const sortByName = (a, b) => {
|
||||
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
|
||||
const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
|
||||
@ -116,117 +131,115 @@ const Regularization = ({ handleRequest, searchTerm,projectId, organizationId, I
|
||||
return () => eventBus.off("employee", employeeHandler);
|
||||
}, [employeeHandler]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
|
||||
{loading ? (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
||||
<p className="text-secondary">Loading...</p>
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
<table className="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}>Name</th>
|
||||
<th>Date</th>
|
||||
<th>Organization</th>
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||
</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentItems?.map((att, index) => (
|
||||
<tr key={index}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar
|
||||
firstName={att.firstName}
|
||||
lastName={att.lastName}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<a href="#" className="text-heading text-truncate">
|
||||
<div>
|
||||
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
|
||||
{loading ? (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "70vh" }}
|
||||
>
|
||||
<SpinnerLoader/>
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
<table className="table mb-0 table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}>Name</th>
|
||||
<th>Date</th>
|
||||
{/* <th>Organization</th> */}
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||
</th>
|
||||
|
||||
<th>Request By</th>
|
||||
<th>Requested At</th>
|
||||
<th className="actions-col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentItems?.map((att, index) => (
|
||||
<tr key={index}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar
|
||||
firstName={att.firstName}
|
||||
lastName={att.lastName}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
{/* <a href="#" className="text-heading text-truncate">
|
||||
<span className="fw-normal">
|
||||
{att.firstName} {att.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</a> */}
|
||||
<a
|
||||
onClick={() =>
|
||||
navigate(`/employee/${att.employeeId}?for=attendance`)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{att.firstName} {att.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||
</td>
|
||||
|
||||
<td>{att.organizationName || "--"}</td>
|
||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||
|
||||
<td>{convertShortTime(att.checkInTime)}</td>
|
||||
<td>
|
||||
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
||||
</td>
|
||||
<td className="text-center ">
|
||||
<RegularizationActions
|
||||
attendanceData={att}
|
||||
handleRequest={handleRequest}
|
||||
refresh={refetch}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* <td>{att.organizationName || "--"}</td> */}
|
||||
|
||||
) : (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<span className="text-secondary">
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: "No Requests Found !"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!loading && totalPages > 1 && (
|
||||
<nav aria-label="Page ">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
|
||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
onClick={() => paginate(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
<td>{convertShortTime(att.checkInTime)}</td>
|
||||
<td>
|
||||
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
||||
</td>
|
||||
<td>
|
||||
{att.requestedBy
|
||||
? `${att.requestedBy?.firstName} ${att.requestedBy?.lastName}`
|
||||
: "--"}
|
||||
</td>
|
||||
<td>
|
||||
{att.requestedAt
|
||||
? moment(att.requestedAt).format("DD-MMM-YYYY")
|
||||
: "--"}
|
||||
</td>
|
||||
<td className="text-center ">
|
||||
<RegularizationActions
|
||||
attendanceData={att}
|
||||
handleRequest={handleRequest}
|
||||
refresh={refetch}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
) : (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<span className="text-secondary">
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: "No Requests Found !"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!loading && totalPages > 1 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
233
src/components/AdvancePayment/AdvancePaymentList.jsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useExpenseTransactions } from "../../hooks/useExpense";
|
||||
import Error from "../common/Error";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Loader, { SpinnerLoader } from "../common/Loader";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { employee } from "../../data/masters";
|
||||
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
|
||||
import { formatFigure } from "../../utils/appUtils";
|
||||
|
||||
const AdvancePaymentList = ({ employeeId }) => {
|
||||
const { setBalance } = useAdvancePaymentContext();
|
||||
const { data, isError, isLoading, error, isFetching } =
|
||||
useExpenseTransactions(employeeId, { enabled: !!employeeId });
|
||||
|
||||
const records = Array.isArray(data) ? data : [];
|
||||
|
||||
let currentBalance = 0;
|
||||
const rowsWithBalance = records.map((r) => {
|
||||
const isCredit = r.amount > 0;
|
||||
const credit = isCredit ? r.amount : 0;
|
||||
const debit = !isCredit ? Math.abs(r.amount) : 0;
|
||||
currentBalance += credit - debit;
|
||||
return {
|
||||
id: r.id,
|
||||
description: r.title || "-",
|
||||
projectName: r.project?.name || "-",
|
||||
createdAt: r.createdAt,
|
||||
credit,
|
||||
debit,
|
||||
financeUId: r.financeUId,
|
||||
balance: currentBalance,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!employeeId) {
|
||||
setBalance(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rowsWithBalance.length > 0) {
|
||||
setBalance(rowsWithBalance[rowsWithBalance.length - 1].balance);
|
||||
} else {
|
||||
setBalance(0);
|
||||
}
|
||||
}, [employeeId, data, setBalance]);
|
||||
|
||||
if (!employeeId) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<p className="text-muted m-0">Please select an employee</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || isFetching) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="text-center py-3">
|
||||
{error?.status === 404
|
||||
? "No advance payment transactions found."
|
||||
: <Error error={error} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
key: "date",
|
||||
label: (
|
||||
<>
|
||||
Date
|
||||
</>
|
||||
),
|
||||
align: "text-start",
|
||||
},
|
||||
{ key: "description", label: "Description", align: "text-start" },
|
||||
|
||||
{
|
||||
key: "credit",
|
||||
label: (
|
||||
<>
|
||||
Credit <i className="bx bx-rupee text-success"></i>
|
||||
</>
|
||||
),
|
||||
align: "text-end",
|
||||
},
|
||||
{
|
||||
key: "debit",
|
||||
label: (
|
||||
<>
|
||||
Debit <i className="bx bx-rupee text-danger"></i>
|
||||
</>
|
||||
),
|
||||
align: "text-end",
|
||||
},
|
||||
|
||||
{
|
||||
key: "balance",
|
||||
label: (
|
||||
<>
|
||||
Balance <i className="bi bi-currency-rupee text-primary"></i>
|
||||
</>
|
||||
),
|
||||
align: "text-end fw-bold",
|
||||
},
|
||||
];
|
||||
|
||||
// Handle empty records
|
||||
if (rowsWithBalance.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-muted py-3">
|
||||
No advance payment records found.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const DecideCreditOrDebit = ({ financeUId }) => {
|
||||
if (!financeUId) return null;
|
||||
|
||||
const prefix = financeUId?.substring(0, 2).toUpperCase();
|
||||
|
||||
if (prefix === "PR") return <span className="text-success">+</span>;
|
||||
if (prefix === "EX") return <span className="text-danger">-</span>;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table align-middle">
|
||||
<thead className="table_header_border">
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th key={col.key} className={col.align}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.isArray(data) && data.length > 0 ? (
|
||||
data.map((row) => (
|
||||
<tr key={row.id}>
|
||||
{columns.map((col) => (
|
||||
<td key={col.key} className={`${col.align} p-2`}>
|
||||
{col.key === "credit" ? (
|
||||
row.amount > 0 ? (
|
||||
<span>{row.amount.toLocaleString("en-IN")}</span>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
) : col.key === "debit" ? (
|
||||
row.amount < 0 ? (
|
||||
<span>
|
||||
{Math.abs(row.amount).toLocaleString("en-IN")}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
) : col.key === "balance" ? (
|
||||
<div className="d-flex align-items-center justify-content-end">
|
||||
<DecideCreditOrDebit financeUId={row?.financeUId} />
|
||||
<span className="mx-2">
|
||||
{formatFigure(row.currentBalance)}
|
||||
</span>
|
||||
</div>
|
||||
) : col.key === "date" ? (
|
||||
<small className="text-muted px-1">
|
||||
{formatUTCToLocalTime(row.paidAt)}
|
||||
</small>
|
||||
) : (
|
||||
<div className="d-flex flex-column text-start gap-1 py-1">
|
||||
<small className="fw-semibold text-dark">
|
||||
{row.project?.name || "-"}
|
||||
</small>
|
||||
<small>{row.title || "-"}</small>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="text-center text-muted py-3"
|
||||
>
|
||||
No advance payment records found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
|
||||
<tfoot className=" fw-bold">
|
||||
<tr className="tr-group text-dark py-2">
|
||||
<td className="text-start">
|
||||
{" "}
|
||||
<div className="d-flex align-items-center px-1 py-2">
|
||||
Final Balance
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end" colSpan="4">
|
||||
<div className="d-flex align-items-center justify-content-end px-1 py-2">
|
||||
{currentBalance.toLocaleString("en-IN", {
|
||||
style: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancePaymentList;
|
||||
@ -2,14 +2,13 @@ import React, { useState, useMemo } from "react";
|
||||
import ApexChart from "../Charts/Circle";
|
||||
import { useProjects } from "../../hooks/useProjects";
|
||||
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
|
||||
import { useSelectedProject } from "../../hooks/useSelectedProject"; // ✅ your custom hook
|
||||
import { useSelectedProject } from "../../hooks/useSelectedProject";
|
||||
|
||||
const Attendance = () => {
|
||||
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);
|
||||
|
||||
// central project selection hook
|
||||
const selectedProjectId = useSelectedProject()
|
||||
|
||||
const {
|
||||
@ -31,7 +30,7 @@ const selectedProjectId = useSelectedProject()
|
||||
<div className="card-header mb-1 pb-0">
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
||||
<div className="card-title mb-0 text-start">
|
||||
<h5 className="mb-1">Attendance</h5>
|
||||
<h5 className="mb-1 card-title">Attendance</h5>
|
||||
<p className="card-subtitle">Daily Attendance Data</p>
|
||||
</div>
|
||||
|
||||
@ -136,7 +135,6 @@ const selectedProjectId = useSelectedProject()
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Details */}
|
||||
{AttendanceData?.activeTab === "Details" && (
|
||||
<div className="table-responsive" style={{ maxHeight: "300px" }}>
|
||||
<table className="table table-hover mb-0 text-start">
|
||||
|
||||
@ -1,205 +0,0 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data";
|
||||
import flatColors from "../Charts/flatColor";
|
||||
import ChartSkeleton from "../Charts/Skelton";
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
});
|
||||
};
|
||||
|
||||
const AttendanceOverview = () => {
|
||||
const [dayRange, setDayRange] = useState(7);
|
||||
const [view, setView] = useState("chart");
|
||||
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const { attendanceOverviewData, loading, error } = useAttendanceOverviewData(
|
||||
projectId,
|
||||
dayRange
|
||||
);
|
||||
|
||||
const { tableData, roles, dates } = useMemo(() => {
|
||||
const map = new Map();
|
||||
|
||||
attendanceOverviewData.forEach((entry) => {
|
||||
const date = formatDate(entry.date);
|
||||
if (!map.has(date)) map.set(date, {});
|
||||
map.get(date)[entry.role.trim()] = entry.present;
|
||||
});
|
||||
|
||||
const uniqueRoles = [
|
||||
...new Set(attendanceOverviewData.map((e) => e.role.trim())),
|
||||
];
|
||||
const sortedDates = [...map.keys()];
|
||||
const data = sortedDates.map((date) => {
|
||||
const row = { date };
|
||||
uniqueRoles.forEach((role) => {
|
||||
row[role] = map.get(date)?.[role] ?? 0;
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
return {
|
||||
tableData: data,
|
||||
roles: uniqueRoles,
|
||||
dates: sortedDates,
|
||||
};
|
||||
}, [attendanceOverviewData]);
|
||||
|
||||
const chartSeries = roles.map((role) => ({
|
||||
name: role,
|
||||
data: tableData.map((row) => row[role]),
|
||||
}));
|
||||
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
type: "bar",
|
||||
stacked: true,
|
||||
height: 400,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 2,
|
||||
columnWidth: "60%",
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: tableData.map((row) => row.date),
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
axisBorder: {
|
||||
show: true,
|
||||
color: "#78909C",
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
axisTicks: {
|
||||
show: true,
|
||||
borderType: "solid",
|
||||
color: "#78909C",
|
||||
width: 6,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
colors: roles.map((_, i) => flatColors[i % flatColors.length]),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded shadow d-flex flex-column">
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div className="card-title mb-0 text-start">
|
||||
<h5 className="mb-1 fw-bold">Attendance Overview</h5>
|
||||
<p className="card-subtitle">Role-wise present count</p>
|
||||
</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={15}>Last 15 Days</option>
|
||||
<option value={30}>Last 30 Days</option>
|
||||
</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>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-grow-1 d-flex align-items-center justify-content-center">
|
||||
{loading ? (
|
||||
<ChartSkeleton />
|
||||
) : error ? (
|
||||
<p className="text-danger">{error}</p>
|
||||
) : view === "chart" ? (
|
||||
<div className="w-100">
|
||||
<ReactApexChart
|
||||
options={chartOptions}
|
||||
series={chartSeries}
|
||||
type="bar"
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="table-responsive w-100"
|
||||
style={{ maxHeight: "350px", overflowY: "auto" }}
|
||||
>
|
||||
<table className="table table-bordered table-sm text-start align-middle mb-0">
|
||||
<thead
|
||||
className="table-light"
|
||||
style={{ position: "sticky", top: 0, zIndex: 1 }}
|
||||
>
|
||||
<tr>
|
||||
<th style={{ background: "#f8f9fa", textTransform: "none" }}>
|
||||
Role
|
||||
</th>
|
||||
{dates.map((date, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
style={{ background: "#f8f9fa", textTransform: "none" }}
|
||||
>
|
||||
{date}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{roles.map((role) => (
|
||||
<tr key={role}>
|
||||
<td>{role}</td>
|
||||
{tableData.map((row, idx) => {
|
||||
const value = row[role];
|
||||
const cellStyle =
|
||||
value > 0 ? { backgroundColor: "#d5d5d5" } : {};
|
||||
return (
|
||||
<td key={idx} style={cellStyle}>
|
||||
{value}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttendanceOverview;
|
||||
193
src/components/Dashboard/AttendanceOverview.jsx
Normal file
@ -0,0 +1,193 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data";
|
||||
import flatColors from "../Charts/flatColor";
|
||||
import ChartSkeleton from "../Charts/Skelton";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { formatDate_DayMonth } from "../../utils/dateUtils";
|
||||
|
||||
const AttendanceOverview = () => {
|
||||
const [dayRange, setDayRange] = useState(7);
|
||||
const [view, setView] = useState("chart");
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
const {
|
||||
data: attendanceOverviewData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useAttendanceOverviewData(selectedProject, dayRange);
|
||||
|
||||
// Use empty array while loading
|
||||
const attendanceData = attendanceOverviewData || [];
|
||||
|
||||
const { tableData, roles, dates } = useMemo(() => {
|
||||
if (!attendanceData || attendanceData.length === 0) {
|
||||
return { tableData: [], roles: [], dates: [] };
|
||||
}
|
||||
|
||||
const map = new Map();
|
||||
|
||||
attendanceData.forEach((entry) => {
|
||||
const date = formatDate_DayMonth(entry.date);
|
||||
if (!map.has(date)) map.set(date, {});
|
||||
map.get(date)[entry.role.trim()] = entry.present;
|
||||
});
|
||||
|
||||
const uniqueRoles = [...new Set(attendanceData.map((e) => e.role.trim()))];
|
||||
const sortedDates = [...map.keys()];
|
||||
|
||||
const tableData = sortedDates.map((date) => {
|
||||
const row = { date };
|
||||
uniqueRoles.forEach((role) => {
|
||||
row[role] = map.get(date)?.[role] ?? 0;
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
return { tableData, roles: uniqueRoles, dates: sortedDates };
|
||||
}, [attendanceData]);
|
||||
|
||||
const chartSeries = roles.map((role) => ({
|
||||
name: role,
|
||||
data: tableData.map((row) => row[role]),
|
||||
}));
|
||||
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
type: "bar",
|
||||
stacked: true,
|
||||
height: 400,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: { bar: { borderRadius: 2, columnWidth: "60%" } },
|
||||
xaxis: { categories: tableData.map((row) => row.date) },
|
||||
yaxis: {
|
||||
show: true,
|
||||
axisBorder: { show: true, color: "#78909C" },
|
||||
axisTicks: { show: true, color: "#78909C", width: 6 },
|
||||
},
|
||||
legend: { position: "bottom" },
|
||||
fill: { opacity: 1 },
|
||||
colors: roles.map((_, i) => flatColors[i % flatColors.length]),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded shadow d-flex flex-column position-relative">
|
||||
<div className="row mb-3 align-items-center">
|
||||
<div className="col-md-6 text-start">
|
||||
<p className="mb-1 fs-6 fs-md-5 fw-medium">Attendance Overview</p>
|
||||
<p className="card-subtitle text-muted mb-0">
|
||||
Role-wise present count
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 d-flex flex-column align-items-end gap-2">
|
||||
<select
|
||||
className="form-select form-select-sm w-auto"
|
||||
value={dayRange}
|
||||
onChange={(e) => setDayRange(Number(e.target.value))}
|
||||
>
|
||||
<option value={7}>Last 7 Days</option>
|
||||
<option value={15}>Last 15 Days</option>
|
||||
<option value={30}>Last 30 Days</option>
|
||||
</select>
|
||||
|
||||
<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
|
||||
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>
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{!isLoading && (!attendanceData || attendanceData.length === 0) ? (
|
||||
<div
|
||||
className="text-muted fw-semibold d-flex align-items-center justify-content-center"
|
||||
style={{ minHeight: "250px" }}
|
||||
>
|
||||
No data found
|
||||
</div>
|
||||
) : view === "chart" ? (
|
||||
<div className="w-100">
|
||||
<ReactApexChart
|
||||
options={chartOptions}
|
||||
series={chartSeries}
|
||||
type="bar"
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="table-responsive w-100"
|
||||
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>
|
||||
<th style={{ background: "#f8f9fa" }}>Role</th>
|
||||
{dates.map((date, idx) => (
|
||||
<th key={idx} style={{ background: "#f8f9fa" }}>
|
||||
{date}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{roles.map((role) => (
|
||||
<tr key={role}>
|
||||
<td className="fw-medium text-start table-cell">{role}</td>
|
||||
{tableData.map((row, idx) => {
|
||||
const value = row[role];
|
||||
return (
|
||||
<td
|
||||
key={idx}
|
||||
style={
|
||||
value > 0 ? { backgroundColor: "#e9ecef" } : {}
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttendanceOverview;
|
||||
@ -1,69 +1,76 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
useDashboardProjectsCardData,
|
||||
useDashboardTeamsCardData,
|
||||
useDashboardTasksCardData,
|
||||
useAttendanceOverviewData
|
||||
} from "../../hooks/useDashboard_Data";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
// import {
|
||||
// useDashboardProjectsCardData,
|
||||
// useDashboardTeamsCardData,
|
||||
// useDashboardTasksCardData,
|
||||
// useAttendanceOverviewData
|
||||
// } from "../../hooks/useDashboard_Data";
|
||||
|
||||
import Projects from "./Projects";
|
||||
import Teams from "./Teams";
|
||||
import TasksCard from "./Tasks";
|
||||
import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||
import ProjectProgressChart from "./ProjectProgressChart";
|
||||
import ProjectOverview from "../Project/ProjectOverview";
|
||||
import AttendanceOverview from "./AttendanceChart";
|
||||
// import Projects from "./Projects";
|
||||
// import Teams from "./Teams";
|
||||
// import TasksCard from "./Tasks";
|
||||
// import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||
// import ProjectProgressChart from "./ProjectProgressChart";
|
||||
// import ProjectOverview from "../Project/ProjectOverview";
|
||||
import AttendanceOverview from "./AttendanceOverview";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import ExpenseAnalysis from "./ExpenseAnalysis";
|
||||
import ExpenseStatus from "./ExpenseStatus";
|
||||
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 { projectsCardData } = useDashboardProjectsCardData();
|
||||
const { teamsCardData } = useDashboardTeamsCardData();
|
||||
const { tasksCardData } = useDashboardTasksCardData();
|
||||
// const { projectsCardData } = useDashboardProjectsCardData();
|
||||
// const { teamsCardData } = useDashboardTeamsCardData();
|
||||
// const { tasksCardData } = useDashboardTasksCardData();
|
||||
|
||||
// Get the selected project ID from Redux store
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isAllProjectsSelected = projectId === null;
|
||||
// Get the selected project ID from Redux store
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isAllProjectsSelected = projectId === null;
|
||||
|
||||
return (
|
||||
<div className="container-fluid mt-5">
|
||||
<div className="row gy-4">
|
||||
{isAllProjectsSelected && (
|
||||
<div className="col-sm-6 col-lg-4">
|
||||
<Projects projectsCardData={projectsCardData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||
<Teams teamsCardData={teamsCardData} />
|
||||
</div>
|
||||
|
||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||
<TasksCard tasksCardData={tasksCardData} />
|
||||
</div>
|
||||
|
||||
{isAllProjectsSelected && (
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<ProjectCompletionChart />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<ProjectOverview />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<ProjectProgressChart />
|
||||
</div>
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<AttendanceOverview /> {/* ✅ Removed unnecessary projectId prop */}
|
||||
</div>
|
||||
)}
|
||||
const isViewExpense = useHasAnyPermission(
|
||||
VIEW_ALL_EXPNESE,
|
||||
APPROVE_EXPENSE,
|
||||
EXPENSE_MANAGE
|
||||
);
|
||||
return (
|
||||
<div className="container-fluid py-5">
|
||||
{isViewExpense && (
|
||||
<div className="row mb-6 g-6">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="card h-100">
|
||||
<ExpenseAnalysis />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-xl-4 col-md-6">
|
||||
<div className="card h-100">
|
||||
<ExpenseStatus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
|
||||
<div className="row vh-100">
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-12 col-md-6 mb-sm-0 mb-4 ">
|
||||
<AttendanceOverview />
|
||||
</div>
|
||||
)}
|
||||
<div className="col-12 col-md-6">
|
||||
<ExpenseByProject />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
|
||||
169
src/components/Dashboard/ExpenseAnalysis.jsx
Normal file
@ -0,0 +1,169 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import { useExpenseAnalysis } from "../../hooks/useDashboard_Data";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { formatCurrency, localToUtc } from "../../utils/appUtils";
|
||||
|
||||
const ExpenseAnalysis = () => {
|
||||
const projectId = useSelectedProject();
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: { startDate: "", endDate: "" },
|
||||
});
|
||||
|
||||
const { watch } = methods;
|
||||
const [startDate, endDate] = watch(["startDate", "endDate"]);
|
||||
|
||||
const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis(
|
||||
projectId,
|
||||
startDate ? localToUtc(startDate) : null,
|
||||
endDate ? localToUtc(endDate) : null
|
||||
);
|
||||
|
||||
if (isError) return <div>{error.message}</div>;
|
||||
|
||||
const report = data?.report ?? [];
|
||||
const { labels, series, total } = useMemo(() => {
|
||||
const labels = report.map((item) => item.projectName);
|
||||
const series = report.map((item) => item.totalApprovedAmount || 0);
|
||||
const total = formatCurrency(data?.totalAmount || 0);
|
||||
return { labels, series, total };
|
||||
}, [report, data?.totalAmount]);
|
||||
|
||||
const donutOptions = {
|
||||
chart: { type: "donut" },
|
||||
labels,
|
||||
legend: { show: false },
|
||||
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
|
||||
colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: "70%",
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: "Total",
|
||||
fontSize: "16px",
|
||||
formatter: () => `${total}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1200,
|
||||
options: {
|
||||
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%" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 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="text-start w-100">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-start justify-content-md-end te w-75">
|
||||
<FormProvider {...methods}>
|
||||
<DateRangePicker1 />
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="card-body position-relative">
|
||||
{isLoading && (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && report.length === 0 && (
|
||||
<div className="d-flex justify-content-center align-items-center">No data found</div>
|
||||
)}
|
||||
|
||||
{!isLoading && report.length > 0 && (
|
||||
<>
|
||||
{isFetching && (
|
||||
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex justify-content-center mb-3">
|
||||
<Chart
|
||||
options={donutOptions}
|
||||
series={series}
|
||||
type="donut"
|
||||
width="100%"
|
||||
height={320}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 w-100">
|
||||
<div className="row g-2">
|
||||
{report.map((item, idx) => (
|
||||
<div
|
||||
className="col-12 col-sm-6 d-flex align-items-start"
|
||||
key={idx}
|
||||
>
|
||||
<div className="avatar me-2">
|
||||
<span
|
||||
className="avatar-initial rounded-2"
|
||||
style={{
|
||||
backgroundColor:
|
||||
donutOptions.colors[idx % donutOptions.colors.length],
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-receipt fs-4"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-1 text-start">
|
||||
<small className="fw-semibold">{item.projectName}</small>
|
||||
<span className="fw-semibold text-muted ms-1">
|
||||
{formatCurrency(item.totalApprovedAmount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseAnalysis;
|
||||
161
src/components/Dashboard/ExpenseByProject.jsx
Normal file
@ -0,0 +1,161 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useExpenseDataByProject } from "../../hooks/useDashboard_Data";
|
||||
import { formatCurrency } from "../../utils/appUtils";
|
||||
import { formatDate_DayMonth } from "../../utils/dateUtils";
|
||||
|
||||
const ExpenseByProject = () => {
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const [range, setRange] = useState("12M");
|
||||
const [selectedType, setSelectedType] = useState("");
|
||||
const [viewMode, setViewMode] = useState("Category");
|
||||
const [chartData, setChartData] = useState({ categories: [], data: [] });
|
||||
|
||||
const { ExpenseCategories, loading: typeLoading } = useExpenseCategory();
|
||||
|
||||
const { data: expenseApiData, isLoading } = useExpenseDataByProject(
|
||||
projectId,
|
||||
selectedType,
|
||||
range === "All" ? null : parseInt(range)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (expenseApiData) {
|
||||
const categories = expenseApiData.map((item) =>
|
||||
formatDate_DayMonth(item.monthName, item.year)
|
||||
);
|
||||
const data = expenseApiData.map((item) => item.total);
|
||||
setChartData({ categories, data });
|
||||
} else {
|
||||
setChartData({ categories: [], data: [] });
|
||||
}
|
||||
}, [expenseApiData]);
|
||||
|
||||
const getSelectedTypeName = () => {
|
||||
if (!selectedType) return "All Types";
|
||||
const found = ExpenseTypes.find((t) => t.id === selectedType);
|
||||
return found ? found.name : "All Types";
|
||||
};
|
||||
|
||||
const options = {
|
||||
chart: { type: "bar", toolbar: { show: false } },
|
||||
plotOptions: {
|
||||
bar: { horizontal: false, columnWidth: "55%", borderRadius: 4 },
|
||||
},
|
||||
dataLabels: { enabled: true, formatter: (val) => formatCurrency(val) },
|
||||
xaxis: {
|
||||
categories: chartData.categories,
|
||||
labels: { style: { fontSize: "12px" }, rotate: -45 },
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`,
|
||||
},
|
||||
},
|
||||
|
||||
annotations: { xaxis: [{ x: 0, strokeDashArray: 0 }] },
|
||||
fill: { opacity: 1 },
|
||||
colors: ["#2196f3"],
|
||||
};
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: getSelectedTypeName(),
|
||||
data: chartData.data,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const ExpenseCategoryType = [
|
||||
{id:1,category:"Category",label:"Category"},
|
||||
{id:2,category:"Project",label:"Project"}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="card shadow-sm rounded ">
|
||||
{/* Header */}
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3 mt-3">
|
||||
<div className="text-start">
|
||||
<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>
|
||||
</div>
|
||||
<div className="btn-group mb-4 ms-n8">
|
||||
<button
|
||||
className="btn btn-sm dropdown-toggle fs-5"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{viewMode}
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end ">
|
||||
{ExpenseCategoryType.map((cat)=>(
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
setViewMode(cat.category);
|
||||
setSelectedType("");
|
||||
}}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Range Buttons + Expense Dropdown */}
|
||||
<div className="d-flex align-items-center flex-wrap ">
|
||||
{["1M", "3M", "6M", "12M", "All"].map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
className={`border-0 px-2 py-1 text-sm rounded ${range === item
|
||||
? "text-white bg-primary"
|
||||
: "text-body bg-transparent"
|
||||
}`}
|
||||
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
|
||||
onClick={() => setRange(item)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
{viewMode === "Category" && (
|
||||
<select
|
||||
className="form-select form-select-sm ms-auto mb-3 mt-1 mt-sm-0"
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
disabled={typeLoading}
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
{ExpenseCategories?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="card-body bg-white text-dark p-3 rounded" style={{ minHeight: "210px" }}>
|
||||
{isLoading ? (
|
||||
<p>Loading chart...</p>
|
||||
) : !expenseApiData || expenseApiData.length === 0 ? (
|
||||
<div className="text-center text-muted py-5">No data found</div>
|
||||
) : (
|
||||
<Chart options={options} series={series} type="bar" height={235} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseByProject;
|
||||
156
src/components/Dashboard/ExpenseStatus.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useExpense } from "../../hooks/useExpense";
|
||||
import { useExpenseStatus } from "../../hooks/useDashboard_Data";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { countDigit, formatCurrency } from "../../utils/appUtils";
|
||||
import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
|
||||
const ExpenseStatus = () => {
|
||||
const [projectName, setProjectName] = useState("All Project");
|
||||
const selectedProject = useSelectedProject();
|
||||
const { projectNames, loading } = useProjectName();
|
||||
const { data, isPending, error } = useExpenseStatus(selectedProject);
|
||||
const navigate = useNavigate();
|
||||
const isManageExpense = useHasUserPermission(EXPENSE_MANAGE)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject && projectNames?.length) {
|
||||
const project = projectNames.find((p) => p.id === selectedProject);
|
||||
setProjectName(project?.name || "All Project");
|
||||
} else {
|
||||
setProjectName("All Project");
|
||||
}
|
||||
}, [projectNames, selectedProject]);
|
||||
|
||||
const handleNavigate = (status) => {
|
||||
if (selectedProject) {
|
||||
navigate(`/expenses/${status}/${selectedProject}`);
|
||||
} else {
|
||||
navigate(`/expenses/${status}`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="card-header d-flex justify-content-between text-start ">
|
||||
<div className="m-0">
|
||||
<p className="fs-6 fw-medium fs-md-5 mb-1">Expense - By Status</p>
|
||||
<p className="card-subtitle m-0 ">{projectName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-body ">
|
||||
|
||||
<div className="report-list text-start h-max">
|
||||
{[
|
||||
{
|
||||
title: "Pending Payment",
|
||||
count: data?.processPending?.count || 0,
|
||||
amount: data?.processPending?.totalAmount || 0,
|
||||
icon: "bx bx-rupee",
|
||||
iconColor: "text-primary",
|
||||
status: EXPENSE_STATUS.payment_pending,
|
||||
},
|
||||
{
|
||||
title: "Pending Approve",
|
||||
count: data?.approvePending?.count || 0,
|
||||
amount: data?.approvePending?.totalAmount || 0,
|
||||
icon: "fa-solid fa-check",
|
||||
iconColor: "text-warning",
|
||||
status: EXPENSE_STATUS.approve_pending,
|
||||
},
|
||||
{
|
||||
title: "Pending Review",
|
||||
count: data?.reviewPending?.count || 0,
|
||||
amount: data?.reviewPending?.totalAmount || 0,
|
||||
icon: "bx bx-search-alt-2",
|
||||
iconColor: "text-secondary",
|
||||
status: EXPENSE_STATUS.review_pending,
|
||||
},
|
||||
{
|
||||
title: "Draft",
|
||||
count: data?.draft?.count || 0,
|
||||
amount: data?.draft?.totalAmount || 0,
|
||||
icon: "bx bx-file-blank",
|
||||
iconColor: "text-info",
|
||||
status: EXPENSE_STATUS.daft,
|
||||
},
|
||||
].map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="report-list-item rounded-2 mb-4 bg-lighter px-2 py-1 cursor-pointer"
|
||||
onClick={() => handleNavigate(item?.status)}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="report-list-icon shadow-xs me-2">
|
||||
<span className="d-inline-flex align-items-center justify-content-center rounded-circle border p-2">
|
||||
<i className={`${item?.icon} ${item?.iconColor} bx-lg`}></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center w-100 flex-wrap gap-2">
|
||||
<div className="d-flex flex-column gap-2">
|
||||
<span className="fw-bold">{item?.title}</span>
|
||||
{item?.amount ? (
|
||||
<small className="mb-0 text-primary">
|
||||
{formatCurrency(item?.amount)}
|
||||
</small>
|
||||
) : (
|
||||
<small className="mb-0 text-primary">{formatCurrency(0)}</small>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<small
|
||||
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
|
||||
} text-gray-500`}
|
||||
>
|
||||
{item?.count || 0}
|
||||
</small>
|
||||
<small className="text-muted fs-semibold text-royalblue text-md">
|
||||
<i className="bx bx-chevron-right"></i>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className=" py-0 text-start mb-2">
|
||||
{isManageExpense && (
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center cursor-pointer"
|
||||
onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
|
||||
>
|
||||
<div className="d-block">
|
||||
<span
|
||||
className={`fs-semibold d-block ${countDigit(data?.totalAmount || 0) > 3 ? "text-base" : "text-lg"
|
||||
}`}
|
||||
>
|
||||
Project Spendings:
|
||||
</span>{" "}
|
||||
<small className="d-block text-xxs text-gary-80">
|
||||
(All Processed Payments)
|
||||
</small>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span
|
||||
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
|
||||
} text-md`}
|
||||
>
|
||||
{formatCurrency(data?.totalAmount || 0)}
|
||||
</span>
|
||||
<small className="text-muted fs-semibold text-royalblue text-md">
|
||||
<i className="bx bx-chevron-right"></i>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseStatus;
|
||||
@ -19,8 +19,11 @@ const AssignedBucket = ({ selectedBucket, handleClose }) => {
|
||||
}
|
||||
}, [selectedBucket, employeesList]);
|
||||
|
||||
const { mutate: AssignEmployee, isPending } = useAssignEmpToBucket(() =>
|
||||
const { mutate: AssignEmployee, isPending } = useAssignEmpToBucket(() =>{
|
||||
setSelectedEmployees([])
|
||||
handleClose()
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
|
||||
56
src/components/Directory/ContactFilterChips.jsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
const ContactFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => {
|
||||
const data = filterData?.data || filterData || {};
|
||||
|
||||
const filterChips = useMemo(() => {
|
||||
const chips = [];
|
||||
|
||||
const addGroup = (ids, list, label, key) => {
|
||||
if (!ids?.length) return;
|
||||
const items = ids.map((id) => ({
|
||||
id,
|
||||
name: list?.find((i) => i.id === id)?.name || id,
|
||||
}));
|
||||
chips.push({ key, label, items });
|
||||
};
|
||||
|
||||
addGroup(filters.bucketIds, data.buckets, "Buckets", "bucketIds");
|
||||
addGroup(filters.categoryIds, data.contactCategories, "Category", "categoryIds");
|
||||
|
||||
return chips;
|
||||
}, [filters, filterData]);
|
||||
|
||||
if (!filterChips.length) return null;
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
||||
{filterChips.map((chipGroup) => (
|
||||
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
|
||||
<span className="fw-semibold me-2">{chipGroup.label}:</span>
|
||||
{chipGroup.items.map((item) => (
|
||||
<span
|
||||
key={item.id}
|
||||
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white btn-sm ms-2"
|
||||
style={{
|
||||
filter: "invert(1) grayscale(1)",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.6rem",
|
||||
}}
|
||||
onClick={() => removeFilterChip(chipGroup.key, item.id)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactFilterChips;
|
||||
@ -74,7 +74,7 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
|
||||
|
||||
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>
|
||||
<div className="px-1">
|
||||
<input
|
||||
@ -87,7 +87,7 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
|
||||
</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">
|
||||
<thead className="table-light">
|
||||
<tr>
|
||||
|
||||
@ -4,8 +4,9 @@ import Pagination from "../common/Pagination";
|
||||
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
|
||||
import { useActiveInActiveContact } from "../../hooks/useDirectory";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import Loader from "../common/Loader";
|
||||
|
||||
const ListViewContact = ({ data, Pagination }) => {
|
||||
const ListViewContact = ({ data, Pagination, isLoading }) => {
|
||||
const { showActive, setManageContact, setContactOpen } =
|
||||
useDirectoryContext();
|
||||
const [deleteContact, setDeleteContact] = useState({
|
||||
@ -85,6 +86,8 @@ const ListViewContact = ({ data, Pagination }) => {
|
||||
ActiveInActive({ contactId: contactId, contactStatus: !showActive });
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader />
|
||||
if (!data || data.length === 0) return <div className="text-center py-12">No Contact Found</div>
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
@ -97,104 +100,96 @@ const ListViewContact = ({ data, Pagination }) => {
|
||||
paramData={deleteContact.contactId}
|
||||
isOpen={deleteContact.Open}
|
||||
/>
|
||||
<div className="card ">
|
||||
<div className="card page-min-h">
|
||||
<div
|
||||
className="card-datatable table-responsive"
|
||||
id="horizontal-example"
|
||||
>
|
||||
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||
<table className="table dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr className="table_header_border">
|
||||
{contactList?.map((col) => (
|
||||
<th key={col.key} className={col.align}>
|
||||
{col.label}
|
||||
|
||||
{data && (
|
||||
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||
<table className="table dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr className="table_header_border">
|
||||
{contactList?.map((col) => (
|
||||
<th key={col.key} className={col.align}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="sticky-action-column bg-white text-center">
|
||||
Action
|
||||
</th>
|
||||
))}
|
||||
<th className="sticky-action-column bg-white text-center">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody >
|
||||
{Array.isArray(data) && data.length > 0 ? (
|
||||
data.map((row, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
style={{ background: `${!showActive ? "#f8f6f6" : ""}` }}
|
||||
>
|
||||
{contactList.map((col) => (
|
||||
<td key={col.key} className={col.align}>
|
||||
{col.getValue(row)}
|
||||
</td>
|
||||
))}
|
||||
<td className="text-center">
|
||||
{showActive ? (
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
setContactOpen({ contact: row, Open: true })
|
||||
}
|
||||
></i>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.isArray(data) && data.length > 0 && (
|
||||
data.map((row, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
style={{
|
||||
background: `${!showActive ? "#f8f6f6" : ""}`,
|
||||
}}
|
||||
>
|
||||
{contactList.map((col) => (
|
||||
<td key={col.key} className={col.align}>
|
||||
{col.getValue(row)}
|
||||
</td>
|
||||
))}
|
||||
<td className="text-center">
|
||||
{showActive ? (
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
setContactOpen({ contact: row, Open: true })
|
||||
}
|
||||
></i>
|
||||
|
||||
<i
|
||||
className="bx bx-edit text-secondary cursor-pointer"
|
||||
onClick={() =>
|
||||
setManageContact({
|
||||
isOpen: true,
|
||||
contactId: row.id,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
<i
|
||||
className="bx bx-edit text-secondary cursor-pointer"
|
||||
onClick={() =>
|
||||
setManageContact({
|
||||
isOpen: true,
|
||||
contactId: row.id,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
|
||||
<i
|
||||
className="bx bx-trash text-danger cursor-pointer"
|
||||
onClick={() =>
|
||||
setDeleteContact({
|
||||
contactId: row.id,
|
||||
Open: true,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
</div>
|
||||
) : (
|
||||
<i
|
||||
className="bx bx-trash text-danger cursor-pointer"
|
||||
onClick={() =>
|
||||
setDeleteContact({
|
||||
contactId: row.id,
|
||||
Open: true,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
</div>
|
||||
) : (
|
||||
<i
|
||||
className={`bx ${
|
||||
isPending && activeContact === row.id
|
||||
className={`bx ${isPending && activeContact === row.id
|
||||
? "bx-loader-alt bx-spin"
|
||||
: "bx-recycle"
|
||||
} me-1 text-primary cursor-pointer`}
|
||||
title="Restore"
|
||||
onClick={() => {
|
||||
setActiveContact(row.id);
|
||||
handleActiveInactive(row.id);
|
||||
}}
|
||||
></i>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr style={{ height: "200px" }}>
|
||||
<td
|
||||
colSpan={contactList.length + 1}
|
||||
className="text-center align-middle"
|
||||
>
|
||||
No contacts found
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{Pagination && (
|
||||
<div className="d-flex justify-content-start">
|
||||
{Pagination}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
} me-1 text-primary cursor-pointer`}
|
||||
title="Restore"
|
||||
onClick={() => {
|
||||
setActiveContact(row.id);
|
||||
handleActiveInactive(row.id);
|
||||
}}
|
||||
></i>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{Pagination && (
|
||||
<div className="d-flex justify-content-start">{Pagination}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -23,7 +23,7 @@ import Label from "../common/Label";
|
||||
const ManageContact = ({ contactId, closeModal }) => {
|
||||
// fetch master data
|
||||
const { buckets, loading: bucketsLoaging } = useBuckets();
|
||||
const { projects, loading: projectLoading } = useProjects();
|
||||
const { data: projects, loading: projectLoading } = useProjects();
|
||||
const { contactCategory, loading: contactCategoryLoading } =
|
||||
useContactCategory();
|
||||
const { organizationList } = useOrganization();
|
||||
@ -205,13 +205,14 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
<Label htmlFor={"organization"} required>
|
||||
Organization
|
||||
</Label>
|
||||
<InputSuggestions
|
||||
organizationList={organizationList}
|
||||
value={watch("organization") || ""}
|
||||
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
|
||||
error={errors.organization?.message}
|
||||
/>
|
||||
|
||||
<InputSuggestions
|
||||
organizationList={organizationList}
|
||||
value={watch("organization") || ""}
|
||||
onChange={(val) =>
|
||||
setValue("organization", val, { shouldValidate: true })
|
||||
}
|
||||
error={errors.organization?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -394,6 +395,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
IsLoading={projectLoading}
|
||||
|
||||
/>
|
||||
{errors.projectIds && (
|
||||
<small className="danger-text">{errors.projectIds.message}</small>
|
||||
@ -408,6 +410,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
label="Tags"
|
||||
options={contactTags}
|
||||
isRequired={true}
|
||||
require
|
||||
/>
|
||||
{errors.tags && (
|
||||
<small className="danger-text">{errors.tags.message}</small>
|
||||
@ -417,7 +420,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
{/* Buckets */}
|
||||
<div className="row">
|
||||
<div className="col-md-12 mt-1 text-start">
|
||||
<label className="form-label ">Select Bucket</label>
|
||||
<Label required>Select Bucket</Label>
|
||||
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
||||
{bucketsLoaging && <p>Loading...</p>}
|
||||
{buckets?.map((item) => (
|
||||
@ -450,7 +453,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
</div>
|
||||
|
||||
{/* Address + Description */}
|
||||
<div className="col-12 text-start">
|
||||
<div className="col-12 text-start mb-2">
|
||||
<label className="form-label">Address</label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
@ -459,7 +462,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Description</label>
|
||||
<Label required>Description</Label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
rows="2"
|
||||
@ -479,10 +482,13 @@ const ManageContact = ({ contactId, closeModal }) => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-sm btn-primary" type="submit" disabled={isPending}>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
79
src/components/Directory/NoteFilterChips.jsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { useMemo } from "react";
|
||||
import moment from "moment";
|
||||
|
||||
const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||
// Normalize data (in case it’s wrapped in .data)
|
||||
const data = filterData?.data || filterData || {};
|
||||
|
||||
const filterChips = useMemo(() => {
|
||||
const chips = [];
|
||||
|
||||
const buildGroup = (ids, list, label, key) => {
|
||||
if (!ids?.length) return;
|
||||
const items = ids.map((id) => ({
|
||||
id,
|
||||
name: list?.find((item) => item.id === id)?.name || id,
|
||||
}));
|
||||
chips.push({ key, label, items });
|
||||
};
|
||||
|
||||
// Build chips dynamically
|
||||
buildGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds");
|
||||
buildGroup(filters.organizations, data.organizations, "Organization", "organizations");
|
||||
|
||||
// Example: Add date range if you ever add in future
|
||||
if (filters.startDate || filters.endDate) {
|
||||
const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : "";
|
||||
const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : "";
|
||||
chips.push({
|
||||
key: "dateRange",
|
||||
label: "Date Range",
|
||||
items: [{ id: "dateRange", name: `${start} - ${end}` }],
|
||||
});
|
||||
}
|
||||
|
||||
return chips;
|
||||
}, [filters, filterData]);
|
||||
|
||||
if (!filterChips.length) return null;
|
||||
|
||||
return (
|
||||
<div className="row my-2">
|
||||
<div className="col-12">
|
||||
<div className="d-flex flex-wrap align-items-start gap-2">
|
||||
{filterChips.map((chip) => (
|
||||
<div
|
||||
key={chip.key}
|
||||
className="d-flex align-items-center flex-wrap px-2 py-1"
|
||||
style={{ fontSize: "0.9rem" }}
|
||||
>
|
||||
<span className="fw-semibold me-2">{chip.label}:</span>
|
||||
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||
{chip.items.map((item) => (
|
||||
<span
|
||||
key={item.id}
|
||||
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white btn-sm ms-2"
|
||||
style={{
|
||||
filter: "invert(1) grayscale(1)",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.6rem",
|
||||
}}
|
||||
onClick={() => removeFilterChip(chip.key, item.id)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoteFilterChips;
|
||||
94
src/components/Documents/DocumentFilterChips.jsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React, { useMemo } from "react";
|
||||
import moment from "moment";
|
||||
|
||||
const DocumentFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||
// Normalize structure: handle both "filterData.data" and plain "filterData"
|
||||
const data = filterData?.data || filterData || {};
|
||||
|
||||
const filterChips = useMemo(() => {
|
||||
const chips = [];
|
||||
|
||||
const buildGroup = (ids, list, label, key) => {
|
||||
if (!ids?.length) return;
|
||||
const items = ids.map((id) => ({
|
||||
id,
|
||||
name: list?.find((item) => item.id === id)?.name || id,
|
||||
}));
|
||||
chips.push({ key, label, items });
|
||||
};
|
||||
|
||||
// Build chips using normalized data
|
||||
buildGroup(filters.uploadedByIds, data.uploadedBy || [], "Uploaded By", "uploadedByIds");
|
||||
buildGroup(filters.documentCategoryIds, data.documentCategory || [], "Category", "documentCategoryIds");
|
||||
buildGroup(filters.documentTypeIds, data.documentType || [], "Type", "documentTypeIds");
|
||||
buildGroup(filters.documentTagIds, data.documentTag || [], "Tags", "documentTagIds");
|
||||
|
||||
if (filters.statusIds?.length) {
|
||||
const items = filters.statusIds.map((status) => ({
|
||||
id: status,
|
||||
name:
|
||||
status === true
|
||||
? "Verified"
|
||||
: status === false
|
||||
? "Rejected"
|
||||
: "Pending",
|
||||
}));
|
||||
chips.push({ key: "statusIds", label: "Status", items });
|
||||
}
|
||||
|
||||
if (filters.startDate || filters.endDate) {
|
||||
const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : "";
|
||||
const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : "";
|
||||
chips.push({
|
||||
key: "dateRange",
|
||||
label: "Date Range",
|
||||
items: [{ id: "dateRange", name: `${start} - ${end}` }],
|
||||
});
|
||||
}
|
||||
|
||||
return chips;
|
||||
}, [filters, filterData]);
|
||||
|
||||
if (!filterChips.length) return null;
|
||||
|
||||
|
||||
return (
|
||||
<div className="row my-2">
|
||||
<div className="col-12">
|
||||
<div className="d-flex flex-wrap align-items-start gap-1">
|
||||
{filterChips.map((chip) => (
|
||||
<div
|
||||
key={chip.key}
|
||||
className="d-flex align-items-center flex-wrap px-2 py-1"
|
||||
style={{ fontSize: "0.9rem" }}
|
||||
>
|
||||
<span className="fw-semibold me-2">{chip.label}:</span>
|
||||
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||
{chip.items.map((item) => (
|
||||
<span
|
||||
key={item.id}
|
||||
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white btn-sm ms-2"
|
||||
style={{
|
||||
filter: "invert(1) grayscale(1)",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.6rem",
|
||||
}}
|
||||
onClick={() => removeFilterChip(chip.key, item.id)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentFilterChips;
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState, useMemo, useImperativeHandle, forwardRef } from "react";
|
||||
import { useDocumentFilterEntities } from "../../hooks/useDocument";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -9,16 +9,34 @@ import {
|
||||
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
import moment from "moment";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
const DocumentFilterPanel = forwardRef(
|
||||
({ entityTypeId, onApply, setFilterdata }, ref) => {
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
const { status } = useParams();
|
||||
|
||||
const { data, isError, isLoading, error } =
|
||||
useDocumentFilterEntities(entityTypeId);
|
||||
|
||||
//changes
|
||||
|
||||
const dynamicDocumentFilterDefaultValues = useMemo(() => {
|
||||
return {
|
||||
...DocumentFilterDefaultValues,
|
||||
uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [],
|
||||
documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [],
|
||||
documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [],
|
||||
documentTagIds: DocumentFilterDefaultValues.documentTagIds || [],
|
||||
startDate: DocumentFilterDefaultValues.startDate,
|
||||
endDate: DocumentFilterDefaultValues.endDate,
|
||||
};
|
||||
|
||||
}, [status]);
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(DocumentFilterSchema),
|
||||
defaultValues: DocumentFilterDefaultValues,
|
||||
defaultValues: dynamicDocumentFilterDefaultValues,
|
||||
});
|
||||
|
||||
const { handleSubmit, reset, setValue, watch } = methods;
|
||||
@ -32,6 +50,24 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetFieldValue: (name, value) => {
|
||||
if (value !== undefined) {
|
||||
setValue(name, value);
|
||||
} else {
|
||||
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
|
||||
}
|
||||
},
|
||||
getValues: methods.getValues, // optional, to read current filter state
|
||||
}));
|
||||
|
||||
//changes
|
||||
useEffect(() => {
|
||||
if (data && setFilterdata) {
|
||||
setFilterdata(data);
|
||||
}
|
||||
}, [data, setFilterdata]);
|
||||
|
||||
const onSubmit = (values) => {
|
||||
onApply({
|
||||
...values,
|
||||
@ -42,14 +78,14 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
|
||||
: null,
|
||||
});
|
||||
closePanel();
|
||||
// closePanel();
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
reset(DocumentFilterDefaultValues);
|
||||
setResetKey((prev) => prev + 1);
|
||||
onApply(DocumentFilterDefaultValues);
|
||||
closePanel();
|
||||
// closePanel();
|
||||
};
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
@ -63,6 +99,8 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
documentTag = [],
|
||||
} = data?.data || {};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@ -73,18 +111,16 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
isUploadedAt ? "active btn-secondary text-white" : ""
|
||||
}`}
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isUploadedAt", true)}
|
||||
>
|
||||
Uploaded On
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
!isUploadedAt ? "active btn-secondary text-white" : ""
|
||||
}`}
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isUploadedAt", false)}
|
||||
>
|
||||
Updated On
|
||||
@ -189,18 +225,18 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
<div className="d-flex justify-content-end py-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-label-secondary btn-xs"
|
||||
className="btn btn-label-secondary btn-sm"
|
||||
onClick={onClear}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-xs">
|
||||
<button type="submit" className="btn btn-primary btn-sm">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default DocumentFilterPanel;
|
||||
|
||||
@ -17,54 +17,56 @@ const SkeletonCell = ({
|
||||
/>
|
||||
);
|
||||
|
||||
export const DocumentTableSkeleton = ({ rows = 5 }) => {
|
||||
export const DocumentTableSkeleton = ({ rows = 10 }) => {
|
||||
return (
|
||||
<table className="card-body table border-top dataTable no-footer dtr-column text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-start">Name</th>
|
||||
<th className="text-start">Document Type</th>
|
||||
<th className="text-start">Uploaded By</th>
|
||||
<th className="text-center">Uploaded on</th>
|
||||
<th className="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<table className="card-body table border-top dataTable no-footer dtr-column text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-start">Name</th>
|
||||
<th className="text-start">Document Type</th>
|
||||
<th className="text-start">Uploaded By</th>
|
||||
<th className="text-center">Uploaded on</th>
|
||||
<th className="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(rows)].map((_, idx) => (
|
||||
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
|
||||
{/* Name */}
|
||||
<td className="text-start">
|
||||
<SkeletonCell width="120px" height={16} />
|
||||
</td>
|
||||
|
||||
<tbody>
|
||||
{[...Array(rows)].map((_, idx) => (
|
||||
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
|
||||
{/* Name */}
|
||||
<td className="text-start">
|
||||
<SkeletonCell width="120px" height={16} />
|
||||
</td>
|
||||
{/* Document Type */}
|
||||
<td className="text-start">
|
||||
<SkeletonCell width="100px" height={16} />
|
||||
</td>
|
||||
|
||||
{/* Document Type */}
|
||||
<td className="text-start">
|
||||
<SkeletonCell width="100px" height={16} />
|
||||
</td>
|
||||
|
||||
{/* Uploaded By (Avatar + Name) */}
|
||||
<td className="text-start">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<SkeletonCell width="30px" height={30} className="rounded-circle" />
|
||||
<SkeletonCell width="80px" height={16} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Uploaded on */}
|
||||
<td className="text-center">
|
||||
{/* Uploaded By (Avatar + Name) */}
|
||||
<td className="text-start">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<SkeletonCell
|
||||
width="30px"
|
||||
height={30}
|
||||
className="rounded-circle"
|
||||
/>
|
||||
<SkeletonCell width="80px" height={16} />
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Status */}
|
||||
<td className="text-center">
|
||||
<SkeletonCell width="70px" height={20} className="rounded" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Uploaded on */}
|
||||
<td className="text-center">
|
||||
<SkeletonCell width="80px" height={16} />
|
||||
</td>
|
||||
|
||||
{/* Status */}
|
||||
<td className="text-center">
|
||||
<SkeletonCell width="70px" height={20} className="rounded" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import GlobalModel from "../common/GlobalModel";
|
||||
import NewDocument from "./ManageDocument";
|
||||
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
|
||||
@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument";
|
||||
import DocumentViewerModal from "./DocumentViewerModal";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import DocumentFilterChips from "./DocumentFilterChips";
|
||||
|
||||
// Context
|
||||
export const DocumentContext = createContext();
|
||||
@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => {
|
||||
const [isSelf, setIsSelf] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [filters, setFilter] = useState();
|
||||
const [filters, setFilter] = useState(DocumentFilterDefaultValues);
|
||||
const [isRefetching, setIsRefetching] = useState(false);
|
||||
const [refetchFn, setRefetchFn] = useState(null);
|
||||
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
|
||||
const { employeeId } = useParams();
|
||||
const [OpenDocument, setOpenDocument] = useState(false);
|
||||
const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues);
|
||||
const updatedRef = useRef();
|
||||
const [ManageDoc, setManageDoc] = useState({
|
||||
document: null,
|
||||
isOpen: false,
|
||||
@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
||||
setShowTrigger(true);
|
||||
setOffcanvasContent(
|
||||
"Document Filters",
|
||||
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
|
||||
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} setFilterdata={setFilterdata} ref={updatedRef} />
|
||||
);
|
||||
|
||||
return () => {
|
||||
@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => {
|
||||
setDocumentEntity(Document_Entity);
|
||||
}
|
||||
}, [Document_Entity]);
|
||||
|
||||
|
||||
const removeFilterChip = (key, id) => {
|
||||
const updatedFilters = { ...filters };
|
||||
if (Array.isArray(updatedFilters[key])) {
|
||||
updatedFilters[key] = updatedFilters[key].filter((v) => v !== id);
|
||||
updatedRef.current?.resetFieldValue(key,updatedFilters[key]);
|
||||
}
|
||||
else if (key === "dateRange") {
|
||||
updatedFilters.startDate = null;
|
||||
updatedFilters.endDate = null;
|
||||
updatedRef.current?.resetFieldValue("startDate",null);
|
||||
updatedRef.current?.resetFieldValue("endDate",null);
|
||||
}
|
||||
else {
|
||||
updatedFilters[key] = null;
|
||||
}
|
||||
setFilter(updatedFilters);
|
||||
return updatedFilters;
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentContext.Provider value={contextValues}>
|
||||
<div className="mt-5">
|
||||
<div className="card page-min-h d-flex p-2">
|
||||
<div className="mt-2">
|
||||
<div className="card page-min-h d-flex p-5">
|
||||
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
|
||||
<div className="row align-items-center">
|
||||
{/* Search */}
|
||||
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
|
||||
<div className="d-flex flex-row gap-2 col-12 col-md-8 col-lg-4 mb-md-0 align-items-center mb-2">
|
||||
<div className="d-flex">
|
||||
{" "}
|
||||
<input
|
||||
@ -149,7 +174,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-6 col-md-6 col-lg-8 text-end">
|
||||
<div className="col-12 col-md-6 col-lg-8 text-end">
|
||||
{(isSelf || canUploadDocument) && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
@ -231,4 +256,4 @@ const Documents = ({ Document_Entity, Entity }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Documents;
|
||||
export default Documents;
|
||||
@ -65,7 +65,7 @@ const DocumentsList = ({
|
||||
setIsRefetching(isFetching);
|
||||
}, [isFetching, setIsRefetching]);
|
||||
|
||||
const { setManageDoc, setViewDoc } = useDocumentContext();
|
||||
const { setManageDoc, setViewDoc,removeFilterChip } = useDocumentContext();
|
||||
const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument();
|
||||
|
||||
const paginate = (page) => {
|
||||
@ -82,9 +82,9 @@ const DocumentsList = ({
|
||||
if (isLoading || isFetching) return <DocumentTableSkeleton />;
|
||||
if (isError)
|
||||
return <div>Error: {error?.message || "Something went wrong"}</div>;
|
||||
if (isInitialEmpty) return <div>No documents found yet.</div>;
|
||||
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
|
||||
if (isFilterEmpty) return <div>No documents match your filter.</div>;
|
||||
if (isInitialEmpty) return <div className="py-12 my-12">No documents found yet.</div>;
|
||||
if (isSearchEmpty) return <div className="py-12 my-12">No results found for "{debouncedSearch}"</div>;
|
||||
if (isFilterEmpty) return <div className="py-12 my-12">No documents match your filter.</div>;
|
||||
|
||||
const handleDelete = () => {
|
||||
ActiveInActive(
|
||||
@ -180,10 +180,10 @@ const DocumentsList = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="table-responsive">
|
||||
<div className="table-responsive p-2">
|
||||
<table className="table border-top dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr className="shadow-sm">
|
||||
<thead className="">
|
||||
<tr className="py-2 ">
|
||||
{DocumentColumns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
|
||||
@ -99,7 +99,7 @@ const EmpAttendance = ({ employee }) => {
|
||||
if (
|
||||
!existing ||
|
||||
new Date(rec.checkInTime || rec.checkOutTime) >
|
||||
new Date(existing.checkInTime || existing.checkOutTime)
|
||||
new Date(existing.checkInTime || existing.checkOutTime)
|
||||
) {
|
||||
uniqueMap.set(key, rec);
|
||||
}
|
||||
@ -123,7 +123,7 @@ const EmpAttendance = ({ employee }) => {
|
||||
};
|
||||
const closeModal = () => setIsModalOpen(false);
|
||||
|
||||
const onSubmit = (formData) => {};
|
||||
const onSubmit = (formData) => { };
|
||||
return (
|
||||
<>
|
||||
{isModalOpen && (
|
||||
@ -131,7 +131,7 @@ const EmpAttendance = ({ employee }) => {
|
||||
<AttendLogs Id={attendanceId} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
<div className="card px-4 mt-5 py-2 " style={{ minHeight: "500px" }}>
|
||||
<div className="card px-4 mt-2 py-2 page-min-h ">
|
||||
<div
|
||||
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
||||
id="DataTables_Table_0_length"
|
||||
@ -152,20 +152,14 @@ const EmpAttendance = ({ employee }) => {
|
||||
</form>
|
||||
</FormProvider>
|
||||
</>
|
||||
</div>
|
||||
<div className="col-md-2 m-0 text-end">
|
||||
<i
|
||||
className={`bx bx-refresh cursor-pointer fs-4 ${
|
||||
isFetching ? "spin" : ""
|
||||
}`}
|
||||
data-toggle="tooltip"
|
||||
title="Refresh"
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive text-nowrap">
|
||||
{!loading && data.length === 0 && <span>No employee logs</span>}
|
||||
{!loading && data.length === 0 && (
|
||||
<span style={{ fontSize: "0.9rem" }}>
|
||||
No attendance record found in selected date range
|
||||
</span>
|
||||
)}
|
||||
{isError && <div className="text-center">{error.message}</div>}
|
||||
{loading && !data && <div className="text-center">Loading...</div>}
|
||||
{data && data.length > 0 && (
|
||||
@ -250,9 +244,8 @@ const EmpAttendance = ({ employee }) => {
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${
|
||||
currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
@ -263,9 +256,8 @@ const EmpAttendance = ({ employee }) => {
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${
|
||||
currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
|
||||
@ -12,11 +12,11 @@ const EmpDashboard = ({ profile }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="col-12 col-sm-6 pt-5">
|
||||
<div className="col-12 col-sm-6 pt-2">
|
||||
{" "}
|
||||
<EmpOverview profile={profile}></EmpOverview>
|
||||
</div>
|
||||
<div className="col col-sm-6 pt-5">
|
||||
{/* <div className="col col-sm-6 pt-5">
|
||||
<div className="card ">
|
||||
<div className="card-body">
|
||||
<small className="card-text text-uppercase text-body-secondary small text-start d-block">
|
||||
@ -29,7 +29,6 @@ const EmpDashboard = ({ profile }) => {
|
||||
className="d-flex mb-4 align-items-start flex-wrap"
|
||||
key={project.id}
|
||||
>
|
||||
{/* Project Info */}
|
||||
<div className="flex-grow-1">
|
||||
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<div className="d-flex">
|
||||
@ -70,7 +69,6 @@ const EmpDashboard = ({ profile }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
@ -79,7 +77,7 @@ const EmpDashboard = ({ profile }) => {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -24,7 +24,7 @@ const EmployeeNav = ({ onPillClick, activePill }) => {
|
||||
icon: "bx bx-file",
|
||||
label: "Documents",
|
||||
},
|
||||
{ key: "activities", icon: "bx bx-grid-alt", label: "Activities" },
|
||||
// { key: "activities", icon: "bx bx-grid-alt", label: "Activities" },
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<div className="col-md-12">
|
||||
|
||||
@ -90,7 +90,7 @@ export const employeeSchema =
|
||||
.min(1, { message: "Phone Number is required" })
|
||||
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
||||
jobRoleId: z.string().min(1, { message: "Role is required" }),
|
||||
organizationId:z.string().min(1,{message:"Organization is required"}),
|
||||
// organizationId:z.string().min(1,{message:"Organization is required"}), // hide temp. for version 1
|
||||
hasApplicationAccess:z.boolean().default(false),
|
||||
}).refine((data) => {
|
||||
if (data.hasApplicationAccess) {
|
||||
@ -119,6 +119,6 @@ export const defatEmployeeObj = {
|
||||
permanentAddress: "",
|
||||
phoneNumber: "",
|
||||
jobRoleId: null,
|
||||
organizationId:"",
|
||||
// organizationId:"",
|
||||
hasApplicationAccess:false
|
||||
}
|
||||
@ -15,18 +15,18 @@ import {
|
||||
import Label from "../common/Label";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { defatEmployeeObj, employeeSchema } from "./EmployeeSchema";
|
||||
import { useOrganizationsList } from "../../hooks/useOrganization";
|
||||
// import { useOrganizationsList } from "../../hooks/useOrganization";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
|
||||
const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
|
||||
const {
|
||||
data: organzationList,
|
||||
isLoading,
|
||||
isError,
|
||||
error: EempError,
|
||||
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
||||
// const {
|
||||
// data: organzationList,
|
||||
// isLoading,
|
||||
// isError,
|
||||
// error: EempError,
|
||||
// } = useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
||||
const {
|
||||
employee,
|
||||
error,
|
||||
@ -113,7 +113,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
permanentAddress: currentEmployee.permanentAddress || "",
|
||||
phoneNumber: currentEmployee.phoneNumber || "",
|
||||
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
|
||||
organizationId: currentEmployee.organizationId || "",
|
||||
// organizationId: currentEmployee.organizationId || "", // Hide temp. for version 1
|
||||
hasApplicationAccess: currentEmployee.hasApplicationAccess || false,
|
||||
}
|
||||
: {}
|
||||
@ -413,9 +413,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* -------------- */}
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-sm-6">
|
||||
{/* -------------- Temp hide for Version---------------*/}
|
||||
{/* <div className="col-sm-6">
|
||||
<Label className="form-text text-start" required>
|
||||
Organization
|
||||
</Label>
|
||||
@ -446,9 +447,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
{errors.organizationId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
{/* --------------------------------------------------- */}
|
||||
|
||||
<div className="col-sm-6 d-flex align-items-center mt-2">
|
||||
<div className="col-12 d-flex align-items-center mt-2">
|
||||
<label className="form-check-label d-flex align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -584,15 +586,14 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row text-end">
|
||||
<div className="col-sm-12">
|
||||
<div className="d-flex flex-row gap-3 justify-content-end">
|
||||
<button
|
||||
aria-label="manage employee"
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
disabled={isPending}
|
||||
onClick={()=>onClosed()}
|
||||
>
|
||||
Clear
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
aria-label="manage employee"
|
||||
@ -600,9 +601,8 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : employeeId ? "Update" : "Create"}
|
||||
{isPending ? "Please Wait..." : employeeId ? "Update" : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@ -14,8 +14,7 @@ const formSchema = z.object({
|
||||
selectedRole: z.record(z.boolean()),
|
||||
});
|
||||
|
||||
const ManageRole = ( {employeeId, onClosed} ) =>
|
||||
{
|
||||
const ManageRole = ({ employeeId, onClosed }) => {
|
||||
const dispatch = useDispatch();
|
||||
const formStateRef = useRef({});
|
||||
|
||||
@ -38,7 +37,7 @@ const ManageRole = ( {employeeId, onClosed} ) =>
|
||||
});
|
||||
const {
|
||||
updateRoles,
|
||||
isPending : isLoading,
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useUpdateEmployeeRoles({
|
||||
@ -112,56 +111,57 @@ const ManageRole = ( {employeeId, onClosed} ) =>
|
||||
const isLoadingData = roleLoading || empLoading;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="text-start mb-3">
|
||||
<h5 className="lead">Select Roles :</h5>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="text-center mb-3">
|
||||
<h5 className="lead">Select Roles :</h5>
|
||||
</div>
|
||||
|
||||
{isLoadingData ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex flex-wrap justify-content-between pb-4"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto" }}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<div className="col-md-6 col-lg-4 mb-3" key={role.id}>
|
||||
<div className="form-check ms-2 text-start">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id={role.id}
|
||||
{...register(`selectedRole.${role.id}`)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={role.id}>
|
||||
<small>{role.role || "--"}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoadingData ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex flex-wrap justify-content-between pb-4"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto" }}
|
||||
>
|
||||
{roles.slice()
|
||||
.sort((a, b) => a.role.localeCompare(b.role)).map((role) => (
|
||||
<div className="col-md-6 col-lg-4 mb-3" key={role.id}>
|
||||
<div className="form-check ms-2 text-start">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id={role.id}
|
||||
{...register(`selectedRole.${role.id}`)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={role.id}>
|
||||
<small>{role.role || "--"}</small>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errors.selectedRole && (
|
||||
<div className="text-danger text-center">
|
||||
{errors.selectedRole.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-end mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={isLoading}>
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errors.selectedRole && (
|
||||
<div className="text-danger text-center">
|
||||
{errors.selectedRole.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-end mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={isLoading}>
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
50
src/components/Expenses/ActiveFilters.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
const ActiveFilters = ({ filters, optionsLookup = {}, onRemove }) => {
|
||||
const entries = Object.entries(filters || {});
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{entries.map(([key, value]) => {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) return null;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => {
|
||||
const label = optionsLookup[key]?.[v] || v;
|
||||
return (
|
||||
<span
|
||||
key={`${key}-${v}`}
|
||||
className="badge bg-label-primary cursor-pointer"
|
||||
onClick={() => onRemove(key, v)}
|
||||
>
|
||||
{label} <i className="bx bx-x ms-1"></i>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className="badge bg-label-success cursor-pointer"
|
||||
onClick={() => onRemove(key)}
|
||||
>
|
||||
{key}: {value ? "Yes" : "No"} <i className="bx bx-x ms-1"></i>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="badge bg-primary">
|
||||
{data?.startDate && data?.endDate
|
||||
? `${formatUTCToLocalTime(
|
||||
data.startDate
|
||||
)} - ${formatUTCToLocalTime(data.endDate)}`
|
||||
: "No dates"}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveFilters;
|
||||
89
src/components/Expenses/ExpenseFilterChips.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||
// Build chips from filters
|
||||
const filterChips = useMemo(() => {
|
||||
const chips = [];
|
||||
|
||||
const buildGroup = (ids, list, label, key) => {
|
||||
if (!ids?.length) return;
|
||||
const items = ids.map((id) => ({
|
||||
id,
|
||||
name: list.find((item) => item.id === id)?.name || id,
|
||||
}));
|
||||
chips.push({ key, label, items });
|
||||
};
|
||||
|
||||
buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds");
|
||||
buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds");
|
||||
buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById");
|
||||
buildGroup(filters.statusIds, filterData.status, "Status", "statusIds");
|
||||
buildGroup(filters.ExpenseTypeIds, filterData.expensesType, "Category", "ExpenseTypeIds");
|
||||
|
||||
if (filters.startDate || filters.endDate) {
|
||||
const start = filters.startDate
|
||||
? new Date(filters.startDate).toLocaleDateString()
|
||||
: "";
|
||||
const end = filters.endDate
|
||||
? new Date(filters.endDate).toLocaleDateString()
|
||||
: "";
|
||||
chips.push({
|
||||
key: "dateRange",
|
||||
label: "Date Range",
|
||||
items: [{ id: "dateRange", name: `${start} - ${end}` }],
|
||||
});
|
||||
}
|
||||
|
||||
return chips;
|
||||
}, [filters, filterData]);
|
||||
|
||||
if (!filterChips.length) return null;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="d-flex flex-wrap align-items-start gap-1 text-start">
|
||||
{filterChips.map((chip) => (
|
||||
<div
|
||||
key={chip.key}
|
||||
className="d-flex align-items-center flex-wrap px-2 py-1 "
|
||||
style={{ fontSize: "0.9rem", maxWidth: "100%" }}
|
||||
>
|
||||
{/* Chip Label */}
|
||||
<span className="fw-semibold me-2">{chip.label}:</span>
|
||||
|
||||
{/* Chip Items */}
|
||||
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||
{chip.items.map((item) => (
|
||||
<span
|
||||
key={item.id}
|
||||
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white btn-sm ms-2"
|
||||
style={{
|
||||
filter: "invert(1) grayscale(1)",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.6rem",
|
||||
}}
|
||||
onClick={() => removeFilterChip(chip.key, item.id)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseFilterChips;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react";
|
||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||
@ -13,9 +13,11 @@ import { useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
import { useExpenseFilter } from "../../hooks/useExpense";
|
||||
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => {
|
||||
const { status } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const selectedProjectId = useSelector(
|
||||
(store) => store.localVariables.projectId
|
||||
);
|
||||
@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
{ id: "submittedBy", name: "Submitted By" },
|
||||
{ id: "project", name: "Project" },
|
||||
{ id: "paymentMode", name: "Payment Mode" },
|
||||
{ id: "expensesType", name: "Expense Type" },
|
||||
{ id: "expensesType", name: "Expense Category" },
|
||||
{ id: "createdAt", name: "Submitted Date" },
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, []);
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
|
||||
const dynamicDefaultFilter = useMemo(() => {
|
||||
return {
|
||||
...defaultFilter,
|
||||
statusIds: status ? [status] : defaultFilter.statusIds || [],
|
||||
projectIds: defaultFilter.projectIds || [],
|
||||
createdByIds: defaultFilter.createdByIds || [],
|
||||
paidById: defaultFilter.paidById || [],
|
||||
ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [],
|
||||
isTransactionDate: defaultFilter.isTransactionDate ?? true,
|
||||
startDate: defaultFilter.startDate,
|
||||
endDate: defaultFilter.endDate,
|
||||
};
|
||||
}, [status, selectedProjectId]);
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(SearchSchema),
|
||||
defaultValues: defaultFilter,
|
||||
defaultValues: dynamicDefaultFilter,
|
||||
});
|
||||
|
||||
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||
@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||
};
|
||||
|
||||
// Change here
|
||||
useEffect(() => {
|
||||
if (data && setFilterdata) {
|
||||
setFilterdata(data);
|
||||
}
|
||||
}, [data, setFilterdata]);
|
||||
|
||||
const handleGroupChange = (e) => {
|
||||
const group = groupByList.find((g) => g.id === e.target.value);
|
||||
if (group) setSelectedGroup(group);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetFieldValue: (name, value) => {
|
||||
// Reset specific field
|
||||
if (value !== undefined) {
|
||||
setValue(name, value);
|
||||
} else {
|
||||
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
|
||||
}
|
||||
},
|
||||
getValues: methods.getValues, // optional, to read current filter state
|
||||
}));
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
onApply({
|
||||
...formData,
|
||||
@ -61,7 +96,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
|
||||
});
|
||||
handleGroupBy(selectedGroup.id);
|
||||
closePanel();
|
||||
// closePanel();
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
@ -70,18 +105,55 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
setSelectedGroup(groupByList[0]);
|
||||
onApply(defaultFilter);
|
||||
handleGroupBy(groupByList[0].id);
|
||||
closePanel();
|
||||
// closePanel();
|
||||
if (status) {
|
||||
navigate("/expenses", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Close popup when navigating to another component
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
closePanel();
|
||||
}, [location]);
|
||||
|
||||
const [appliedStatusId, setAppliedStatusId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!status || !data) return;
|
||||
|
||||
if (status !== appliedStatusId) {
|
||||
const filterWithStatus = {
|
||||
...dynamicDefaultFilter,
|
||||
projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [],
|
||||
startDate: dynamicDefaultFilter.startDate
|
||||
? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString()
|
||||
: undefined,
|
||||
endDate: dynamicDefaultFilter.endDate
|
||||
? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString()
|
||||
: undefined,
|
||||
};
|
||||
|
||||
onApply(filterWithStatus);
|
||||
handleGroupBy(selectedGroup.id);
|
||||
setAppliedStatusId(status);
|
||||
}
|
||||
}, [
|
||||
status,
|
||||
data,
|
||||
dynamicDefaultFilter,
|
||||
onApply,
|
||||
handleGroupBy,
|
||||
selectedGroup.id,
|
||||
appliedStatusId,
|
||||
selectedProjectId,
|
||||
]);
|
||||
|
||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
||||
if (isError && isFetched)
|
||||
return <div>Something went wrong Here- {error.message} </div>;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
@ -92,31 +164,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
isTransactionDate ? "active btn-primary text-white" : ""
|
||||
}`}
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isTransactionDate", true)}
|
||||
>
|
||||
Transaction Date
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
!isTransactionDate ? "active btn-primary text-white" : ""
|
||||
}`}
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isTransactionDate", false)}
|
||||
>
|
||||
Submitted Date
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label className="fw-semibold">Choose Date Range:</label>
|
||||
<DateRangePicker1
|
||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||
startField="startDate"
|
||||
endField="endDate"
|
||||
resetSignal={resetKey}
|
||||
defaultRange={false}
|
||||
maxDate={new Date()}
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -142,6 +213,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
labelKey={(item) => item.name}
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="ExpenseTypeIds"
|
||||
label="Category :"
|
||||
options={data.expensesType}
|
||||
labelKey={(item) => item.name}
|
||||
valueKey="id"
|
||||
/>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Status :</label>
|
||||
@ -213,6 +291,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default ExpenseFilterPanel;
|
||||
export default ExpenseFilterPanel;
|
||||
@ -10,20 +10,29 @@ import {
|
||||
EXPENSE_REJECTEDBY,
|
||||
ITEMS_PER_PAGE,
|
||||
} from "../../utils/constants";
|
||||
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils";
|
||||
import {
|
||||
formatCurrency,
|
||||
getColorNameFromHex,
|
||||
useDebounce,
|
||||
} from "../../utils/appUtils";
|
||||
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { useSelector } from "react-redux";
|
||||
import ExpenseFilterChips from "./ExpenseFilterChips";
|
||||
import { defaultFilter } from "./ExpenseSchema";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const { setViewExpense, setManageExpenseModal } = useExpenseContext();
|
||||
const { setViewExpense, setManageExpenseModal, filterData, removeFilterChip } = useExpenseContext();
|
||||
const IsExpenseEditable = useHasUserPermission();
|
||||
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const debouncedSearch = useDebounce(searchText, 500);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
||||
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
||||
@ -59,44 +68,64 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
const groupByField = (items, field) => {
|
||||
return items.reduce((acc, item) => {
|
||||
let key;
|
||||
let displayField;
|
||||
|
||||
switch (field) {
|
||||
case "transactionDate":
|
||||
key = item.transactionDate?.split("T")[0];
|
||||
key = item?.transactionDate?.split("T")[0];
|
||||
displayField = "Transaction Date";
|
||||
break;
|
||||
case "status":
|
||||
key = item.status?.displayName || "Unknown";
|
||||
key = item?.status?.displayName || "Unknown";
|
||||
displayField = "Status";
|
||||
break;
|
||||
case "submittedBy":
|
||||
key = `${item.createdBy?.firstName ?? ""} ${
|
||||
item.createdBy?.lastName ?? ""
|
||||
}`.trim();
|
||||
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
||||
}`.trim();
|
||||
displayField = "Submitted By";
|
||||
break;
|
||||
case "project":
|
||||
key = item.project?.name || "Unknown Project";
|
||||
key = item?.project?.name || "Unknown Project";
|
||||
displayField = "Project";
|
||||
break;
|
||||
case "paymentMode":
|
||||
key = item.paymentMode?.name || "Unknown Mode";
|
||||
key = item?.paymentMode?.name || "Unknown Mode";
|
||||
displayField = "Payment Mode";
|
||||
break;
|
||||
case "expensesType":
|
||||
key = item.expensesType?.name || "Unknown Type";
|
||||
case "expenseCategory":
|
||||
key = item?.expenseCategory?.name || "Unknown Type";
|
||||
displayField = "Expense Category";
|
||||
break;
|
||||
case "createdAt":
|
||||
key = item.createdAt?.split("T")[0] || "Unknown Type";
|
||||
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
||||
displayField = "Created Date";
|
||||
break;
|
||||
default:
|
||||
key = "Others";
|
||||
displayField = "Others";
|
||||
}
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push(item);
|
||||
|
||||
const groupKey = `${field}_${key}`; // unique key for object property
|
||||
if (!acc[groupKey]) {
|
||||
acc[groupKey] = { key, displayField, items: [] };
|
||||
}
|
||||
|
||||
acc[groupKey].items.push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const expenseColumns = [
|
||||
{
|
||||
key: "expensesType",
|
||||
label: "Expense Type",
|
||||
getValue: (e) => e.expensesType?.name || "N/A",
|
||||
key: "expenseUId",
|
||||
label: "Expense Id",
|
||||
getValue: (e) => e.expenseUId || "N/A",
|
||||
align: "text-start mx-2",
|
||||
},
|
||||
{
|
||||
key: "expensesCategory",
|
||||
label: "Expense Category",
|
||||
getValue: (e) => e.expenseCategory?.name || "N/A",
|
||||
align: "text-start",
|
||||
},
|
||||
{
|
||||
@ -110,11 +139,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
label: "Submitted By",
|
||||
align: "text-start",
|
||||
getValue: (e) =>
|
||||
`${e.createdBy?.firstName ?? ""} ${
|
||||
e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A",
|
||||
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A",
|
||||
customRender: (e) => (
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex align-items-center cursor-pointer"
|
||||
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
@ -122,9 +151,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
lastName={e.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-truncate">
|
||||
{`${e.createdBy?.firstName ?? ""} ${
|
||||
e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
@ -138,11 +166,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
{
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
getValue: (e) => (
|
||||
<>
|
||||
<i className="bx bx-rupee b-xs"></i> {e?.amount}
|
||||
</>
|
||||
),
|
||||
getValue: (e) => <>{formatCurrency(e?.amount)}</>,
|
||||
isAlwaysVisible: true,
|
||||
align: "text-end",
|
||||
},
|
||||
@ -152,37 +176,41 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
align: "text-center",
|
||||
getValue: (e) => (
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
getColorNameFromHex(e?.status?.color) || "secondary"
|
||||
}`}
|
||||
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
|
||||
}`}
|
||||
>
|
||||
{e.status?.name || "Unknown"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (isInitialLoading) return <ExpenseTableSkeleton />;
|
||||
if (isError) return <div>{error.message}</div>;
|
||||
const headers = ["Expense Category","Payment Mode","Submitted By","Submitted","Amount","Status","Action"]
|
||||
if (isInitialLoading && !data) return <ExpenseTableSkeleton headers={headers} />;
|
||||
if (isError) return <div>{error?.message}</div>;
|
||||
|
||||
const grouped = groupBy
|
||||
? groupByField(data?.data ?? [], groupBy)
|
||||
: { All: data?.data ?? [] };
|
||||
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy);
|
||||
const IsGroupedByDate = [
|
||||
{ key: "transactionDate", displayField: "Transaction Date" },
|
||||
{ key: "createdAt", displayField: "created Date" },
|
||||
]?.includes(groupBy);
|
||||
|
||||
const canEditExpense = (expense) => {
|
||||
return (
|
||||
(expense.status.id === EXPENSE_DRAFT ||
|
||||
EXPENSE_REJECTEDBY.includes(expense.status.id)) &&
|
||||
expense.createdBy?.id === SelfId
|
||||
(expense?.status?.id === EXPENSE_DRAFT ||
|
||||
EXPENSE_REJECTEDBY.includes(expense?.status?.id)) &&
|
||||
expense?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
|
||||
const canDetetExpense = (expense) => {
|
||||
return (
|
||||
expense.status.id === EXPENSE_DRAFT && expense.createdBy.id === SelfId
|
||||
expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
@ -198,11 +226,21 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="card px-0 px-sm-4">
|
||||
<div className="card page-min-h px-sm-4">
|
||||
{/* Filter Chips */}
|
||||
<ExpenseFilterChips
|
||||
filters={filters}
|
||||
filterData={filterData}
|
||||
removeFilterChip={removeFilterChip}
|
||||
groupBy={groupBy}
|
||||
/>
|
||||
<div
|
||||
className="card-datatable table-responsive "
|
||||
id="horizontal-example"
|
||||
>
|
||||
|
||||
|
||||
|
||||
<div className="dataTables_wrapper no-footer px-2 ">
|
||||
<table className="table border-top dataTable text-nowrap">
|
||||
<thead>
|
||||
@ -212,10 +250,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||
<th
|
||||
key={col.key}
|
||||
className={`sorting d-table-cell`}
|
||||
className={`sorting d-table-cell `}
|
||||
aria-sort="descending"
|
||||
>
|
||||
<div className={`${col.align}`}>{col.label}</div>
|
||||
<div className={`${col.align} `}>{col.label}</div>
|
||||
</th>
|
||||
)
|
||||
)}
|
||||
@ -226,34 +264,44 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(grouped).length > 0 ? (
|
||||
Object.entries(grouped).map(([group, expenses]) => (
|
||||
<React.Fragment key={group}>
|
||||
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||
<React.Fragment key={key}>
|
||||
<tr className="tr-group text-dark">
|
||||
<td colSpan={8} className="text-start">
|
||||
<strong>
|
||||
{IsGroupedByDate
|
||||
? formatUTCToLocalTime(group)
|
||||
: group}
|
||||
</strong>
|
||||
<div className="d-flex align-items-center px-2">
|
||||
{" "}
|
||||
<small className="fs-6 py-1">
|
||||
{displayField} :{" "}
|
||||
</small>{" "}
|
||||
<small className="fs-6 ms-3">
|
||||
{IsGroupedByDate
|
||||
? formatUTCToLocalTime(key)
|
||||
: key}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{expenses.map((expense) => (
|
||||
{items?.map((expense) => (
|
||||
<tr key={expense.id}>
|
||||
{expenseColumns.map(
|
||||
(col) =>
|
||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`d-table-cell ${col.align ?? ""}`}
|
||||
className={`d-table-cell ml-2 ${col.align ?? ""} `}
|
||||
>
|
||||
{col.customRender
|
||||
<div className={`d-flex px-2 ${col.key === "status" ? "justify-content-center":""}
|
||||
${col.key === "amount" ? "justify-content-end":""}
|
||||
${col.key === "submitted" ? "justify-content-center":""}
|
||||
`}>{col.customRender
|
||||
? col.customRender(expense)
|
||||
: col.getValue(expense)}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
<td className="sticky-action-column bg-white">
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
<div className="d-flex flex-row gap-2">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
@ -263,27 +311,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
})
|
||||
}
|
||||
></i>
|
||||
{canEditExpense(expense) && (
|
||||
<i
|
||||
className="bx bx-edit text-secondary cursor-pointer"
|
||||
onClick={() =>
|
||||
setManageExpenseModal({
|
||||
IsOpen: true,
|
||||
expenseId: expense.id,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
)}
|
||||
{canDetetExpense(expense) &&
|
||||
canEditExpense(expense) && (
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded text-muted p-0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||
{canDetetExpense(expense) && (
|
||||
<li
|
||||
onClick={() =>
|
||||
setManageExpenseModal({
|
||||
IsOpen: true,
|
||||
expenseId: expense.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||
<span className="align-left ">
|
||||
Modify
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{canDetetExpense(expense) && (
|
||||
<i
|
||||
className="bx bx-trash text-danger cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(expense.id);
|
||||
}}
|
||||
></i>
|
||||
)}
|
||||
{canDetetExpense(expense) && (
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(expense.id);
|
||||
}}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||
<span className="align-left">
|
||||
Delete
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -292,13 +374,17 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-4">
|
||||
No Expense Found
|
||||
<td colSpan={8} className="text-center border-0 ">
|
||||
<div className="py-8">
|
||||
<p>No Expense Found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{data?.data?.length > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
@ -306,8 +392,6 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { localToUtc } from "../../utils/appUtils";
|
||||
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ALLOWED_TYPES = [
|
||||
@ -12,20 +14,21 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
return z
|
||||
.object({
|
||||
projectId: z.string().min(1, { message: "Project is required" }),
|
||||
expensesTypeId: z
|
||||
expenseCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Expense type is required" }),
|
||||
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
||||
paidById: z.string().min(1, { message: "Employee name is required" }),
|
||||
transactionDate: z
|
||||
.string()
|
||||
.min(1, { message: "Date is required" })
|
||||
,
|
||||
transactionDate: z.string().min(1, { message: "Date is required" }),
|
||||
transactionId: z.string().optional(),
|
||||
description: z.string().min(1, { message: "Description is required" }),
|
||||
location: z.string().min(1, { message: "Location is required" }),
|
||||
supplerName: z.string().min(1, { message: "Supplier name is required" }),
|
||||
gstNumber :z.string().optional(),
|
||||
gstNumber: z.string().optional(),
|
||||
currencyId: z
|
||||
.string()
|
||||
.min(1, { message: "currency is required" })
|
||||
.default(DEFAULT_CURRENCY),
|
||||
amount: z.coerce
|
||||
.number({
|
||||
invalid_type_error: "Amount is required and must be a number",
|
||||
@ -54,8 +57,6 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
})
|
||||
)
|
||||
.nonempty({ message: "At least one file attachment is required" }),
|
||||
|
||||
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -68,9 +69,14 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
path: ["paidById"],
|
||||
}
|
||||
)
|
||||
.superRefine((data, ctx) => {
|
||||
const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId);
|
||||
if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) {
|
||||
.superRefine((data, ctx) => {
|
||||
const expenseType = expenseTypes.find(
|
||||
(et) => et.id === data.expensesCategoryId
|
||||
);
|
||||
if (
|
||||
expenseType?.noOfPersonsRequired &&
|
||||
(!data.noOfPersons || data.noOfPersons < 1)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "No. of Persons is required and must be at least 1",
|
||||
@ -82,7 +88,7 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
|
||||
export const defaultExpense = {
|
||||
projectId: "",
|
||||
expensesTypeId: "",
|
||||
expenseCategoryId: "",
|
||||
paymentModeId: "",
|
||||
paidById: "",
|
||||
transactionDate: "",
|
||||
@ -92,12 +98,15 @@ export const defaultExpense = {
|
||||
supplerName: "",
|
||||
amount: "",
|
||||
noOfPersons: "",
|
||||
gstNumber:"",
|
||||
gstNumber: "",
|
||||
currencyId: DEFAULT_CURRENCY,
|
||||
billAttachments: [],
|
||||
};
|
||||
|
||||
|
||||
export const ExpenseActionScheam = (isReimbursement = false) => {
|
||||
export const ExpenseActionScheam = (
|
||||
isReimbursement = false,
|
||||
transactionDate
|
||||
) => {
|
||||
return z
|
||||
.object({
|
||||
comment: z.string().min(1, { message: "Please leave comment" }),
|
||||
@ -105,6 +114,9 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
|
||||
reimburseTransactionId: z.string().nullable().optional(),
|
||||
reimburseDate: z.string().nullable().optional(),
|
||||
reimburseById: z.string().nullable().optional(),
|
||||
tdsPercentage: z.string().nullable().optional(),
|
||||
baseAmount: z.string().nullable().optional(),
|
||||
taxAmount: z.string().nullable().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (isReimbursement) {
|
||||
@ -122,6 +134,7 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
|
||||
message: "Reimburse Date is required",
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.reimburseById) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@ -129,26 +142,42 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
|
||||
message: "Reimburse By is required",
|
||||
});
|
||||
}
|
||||
if (!data.baseAmount) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["baseAmount"],
|
||||
message: "Base Amount i required",
|
||||
});
|
||||
}
|
||||
if (!data.taxAmount) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["taxAmount"],
|
||||
message: "Tax is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultActionValues = {
|
||||
export const defaultActionValues = {
|
||||
comment: "",
|
||||
statusId: "",
|
||||
|
||||
reimburseTransactionId: null,
|
||||
reimburseDate: null,
|
||||
reimburseById: null,
|
||||
tdsPercentage: null,
|
||||
baseAmount:null,
|
||||
taxAmount: null,
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const SearchSchema = z.object({
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
statusIds: z.array(z.string()).optional(),
|
||||
createdByIds: z.array(z.string()).optional(),
|
||||
paidById: z.array(z.string()).optional(),
|
||||
ExpenseTypeIds: z.array(z.string()).optional(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
isTransactionDate: z.boolean().default(true),
|
||||
@ -159,8 +188,8 @@ export const defaultFilter = {
|
||||
statusIds: [],
|
||||
createdByIds: [],
|
||||
paidById: [],
|
||||
ExpenseTypeIds: [],
|
||||
isTransactionDate: true,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
};
|
||||
|
||||
|
||||
@ -143,7 +143,11 @@ const SkeletonCell = ({
|
||||
/>
|
||||
);
|
||||
|
||||
export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
|
||||
export const ExpenseTableSkeleton = ({
|
||||
groups = 3,
|
||||
rowsPerGroup = 3,
|
||||
headers,
|
||||
}) => {
|
||||
return (
|
||||
<div className="card px-2">
|
||||
<table
|
||||
@ -153,17 +157,11 @@ export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="d-none d-sm-table-cell">
|
||||
<div className="text-start ms-5">Expense Type</div>
|
||||
</th>
|
||||
<th className="d-none d-sm-table-cell">
|
||||
<div className="text-start ms-5">Payment Mode</div>
|
||||
</th>
|
||||
<th className="d-none d-sm-table-cell">Submitted By</th>
|
||||
<th className="d-none d-sm-table-cell">Submitted</th>
|
||||
<th className="d-none d-md-table-cell">Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
{headers.map((header) => (
|
||||
<th key={header} className="d-none d-sm-table-cell">
|
||||
<div className="text-start ms-5">{header}</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -204,7 +202,7 @@ export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
|
||||
<SkeletonCell width="80px" height={16} />
|
||||
</div>
|
||||
</td>
|
||||
{/* Submitted */}
|
||||
{/* Submitted */}
|
||||
<td className="d-none d-md-table-cell text-end">
|
||||
<SkeletonCell width="70px" height={16} />
|
||||
</td>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useState,useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
|
||||
|
||||
import Timeline from "../common/TimeLine";
|
||||
import moment from "moment";
|
||||
import { getColorNameFromHex } from "../../utils/appUtils";
|
||||
const ExpenseStatusLogs = ({ data }) => {
|
||||
const [visibleCount, setVisibleCount] = useState(4);
|
||||
|
||||
@ -13,56 +14,40 @@ const ExpenseStatusLogs = ({ data }) => {
|
||||
);
|
||||
}, [data?.expenseLogs]);
|
||||
|
||||
const logsToShow = sortedLogs.slice(0, visibleCount);
|
||||
const logsToShow = useMemo(
|
||||
() => sortedLogs.slice(0, visibleCount),
|
||||
[sortedLogs, visibleCount]
|
||||
);
|
||||
console.log(logsToShow);
|
||||
const timelineData = useMemo(() => {
|
||||
return logsToShow.map((log, index) => ({
|
||||
id: index + 1,
|
||||
title: log.action || "Status Updated",
|
||||
description: log.comment || "",
|
||||
timeAgo: log.updateAt,
|
||||
color: getColorNameFromHex(log.nextStatus?.color) || "primary",
|
||||
users: log.updatedBy
|
||||
? [
|
||||
{
|
||||
firstName: log.updatedBy.firstName || "",
|
||||
lastName: log?.updatedBy?.lastName || "",
|
||||
role: log.updatedBy.jobRoleName || "",
|
||||
avatar: log.updatedBy.photo,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}));
|
||||
}, [logsToShow]);
|
||||
|
||||
const handleShowMore = () => {
|
||||
setVisibleCount((prev) => prev + 4);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row g-2">
|
||||
{logsToShow.map((log) => (
|
||||
<div key={log.id} className="col-12 d-flex align-items-start mb-1">
|
||||
<Avatar
|
||||
size="xs"
|
||||
firstName={log.updatedBy.firstName}
|
||||
lastName={log.updatedBy.lastName}
|
||||
/>
|
||||
|
||||
<div className="flex-grow-1">
|
||||
<div className="text-start">
|
||||
<div className="flex">
|
||||
<span>{`${log.updatedBy.firstName} ${log.updatedBy.lastName}`}</span>
|
||||
<small className="text-secondary text-tiny ms-2">
|
||||
<em>{log.action}</em>
|
||||
</small>
|
||||
<span className="text-tiny text-secondary d-block" >
|
||||
{formatUTCToLocalTime(log.updateAt,true)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center text-muted small mt-1">
|
||||
<span>{log.comment}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{sortedLogs.length > visibleCount && (
|
||||
<div className="text-center my-1">
|
||||
<button
|
||||
className="btn btn-xs btn-outline-primary"
|
||||
onClick={handleShowMore}
|
||||
>
|
||||
Show More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className="page-min-h overflow-auto py-1">
|
||||
<Timeline items={timelineData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ExpenseStatusLogs;
|
||||
|
||||
95
src/components/Expenses/Filelist.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
|
||||
import Tooltip from "../common/Tooltip";
|
||||
|
||||
const Filelist = ({ files, removeFile, expenseToEdit }) => {
|
||||
return (
|
||||
<div className="d-flex flex-wrap gap-2 my-1">
|
||||
{files
|
||||
.filter((file) => {
|
||||
if (expenseToEdit) {
|
||||
return file.isActive;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((file, idx) => (
|
||||
<div className="col-12 col-sm-6 col-md-4 mb-2" key={idx}>
|
||||
<div className="d-flex align-items-center justify-content-between bg-white border rounded p-1">
|
||||
{/* File icon and info */}
|
||||
<div className="d-flex align-items-center flex-grow-1 gap-2 overflow-hidden">
|
||||
<i
|
||||
className={`bx ${getIconByFileType(
|
||||
file?.contentType
|
||||
)} fs-3 text-primary`}
|
||||
style={{ minWidth: "30px" }}
|
||||
></i>
|
||||
|
||||
<div className="d-flex flex-column text-truncate">
|
||||
<span className="fw-semibold small text-truncate">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-body-secondary small">
|
||||
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete icon */}
|
||||
<Tooltip text="Remove file">
|
||||
<i
|
||||
className="bx bx-sm bx-trash text-danger fs-4 cursor-pointer ms-2"
|
||||
role="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeFile(expenseToEdit ? file.documentId : idx);
|
||||
}}
|
||||
></i>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Filelist;
|
||||
export const FilelistView = ({ files, viewFile }) => {
|
||||
return (
|
||||
<div className="d-flex flex-wrap gap-2 mt-2">
|
||||
{files?.map((file, idx) => (
|
||||
<div className=" bg-white " key={idx}>
|
||||
<div className="row align-items-center">
|
||||
{/* File icon and info */}
|
||||
<div className="col-12 d-flex align-items-center gap-2">
|
||||
<i
|
||||
className={`bx ${getIconByFileType(file?.fileName)} fs-3`}
|
||||
></i>
|
||||
|
||||
<div
|
||||
className="d-flex flex-column text-truncate"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
viewFile({
|
||||
IsOpen: true,
|
||||
Image: file.preSignedUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="fw-medium small text-truncate">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-body-secondary small">
|
||||
<Tooltip text={"Click on file"}>
|
||||
{" "}
|
||||
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -3,12 +3,12 @@ import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { useCurrencies, useProjectName } from "../../hooks/useProjects";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||
import useMaster, {
|
||||
useExpenseCategory,
|
||||
useExpenseStatus,
|
||||
useExpenseType,
|
||||
usePaymentMode,
|
||||
} from "../../hooks/masterHook/useMaster";
|
||||
import {
|
||||
@ -28,6 +28,9 @@ import moment from "moment";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import ErrorPage from "../../pages/ErrorPage";
|
||||
import Label from "../common/Label";
|
||||
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||
import Filelist from "./Filelist";
|
||||
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
||||
|
||||
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
const {
|
||||
@ -38,11 +41,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
const [ExpenseType, setExpenseType] = useState();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
ExpenseTypes,
|
||||
ExpenseCategories,
|
||||
loading: ExpenseLoading,
|
||||
error: ExpenseError,
|
||||
} = useExpenseType();
|
||||
const schema = ExpenseSchema(ExpenseTypes);
|
||||
} = useExpenseCategory();
|
||||
const schema = ExpenseSchema(ExpenseCategories);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -57,14 +60,18 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
});
|
||||
|
||||
const selectedproject = watch("projectId");
|
||||
|
||||
|
||||
const {
|
||||
projectNames,
|
||||
loading: projectLoading,
|
||||
error,
|
||||
isError: isProjectError,
|
||||
} = useProjectName();
|
||||
|
||||
const {
|
||||
data: currencies,
|
||||
isLoading: currencyLoading,
|
||||
error: currencyError,
|
||||
} = useCurrencies();
|
||||
const {
|
||||
PaymentModes,
|
||||
loading: PaymentModeLoading,
|
||||
@ -80,6 +87,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
isLoading: EmpLoading,
|
||||
isError: isEmployeeError,
|
||||
} = useEmployeesNameByProject(selectedproject);
|
||||
|
||||
const files = watch("billAttachments");
|
||||
const onFileChange = async (e) => {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
@ -142,11 +150,10 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (expenseToEdit && data ) {
|
||||
|
||||
if (expenseToEdit && data) {
|
||||
reset({
|
||||
projectId: data.project.id || "",
|
||||
expensesTypeId: data.expensesType.id || "",
|
||||
expenseCategoryId: data.expenseType.id || "",
|
||||
paymentModeId: data.paymentMode.id || "",
|
||||
paidById: data.paidBy.id || "",
|
||||
transactionDate: data.transactionDate?.slice(0, 10) || "",
|
||||
@ -156,7 +163,8 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
supplerName: data.supplerName || "",
|
||||
amount: data.amount || "",
|
||||
noOfPersons: data.noOfPersons || "",
|
||||
gstNumber:data.gstNumber || "",
|
||||
gstNumber: data.gstNumber || "",
|
||||
currencyId: data.currencyId || DEFAULT_CURRENCY,
|
||||
billAttachments: data.documents
|
||||
? data.documents.map((doc) => ({
|
||||
fileName: doc.fileName,
|
||||
@ -183,8 +191,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
const onSubmit = (fromdata) => {
|
||||
let payload = {
|
||||
...fromdata,
|
||||
transactionDate: localToUtc(fromdata.transactionDate)
|
||||
|
||||
transactionDate: localToUtc(fromdata.transactionDate),
|
||||
};
|
||||
if (expenseToEdit) {
|
||||
const editPayload = { ...payload, id: data.id };
|
||||
@ -193,10 +200,12 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
CreateExpense(payload);
|
||||
}
|
||||
};
|
||||
const ExpenseTypeId = watch("expensesTypeId");
|
||||
const ExpenseTypeId = watch("expensesCategoryId");
|
||||
|
||||
useEffect(() => {
|
||||
setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId));
|
||||
setExpenseType(
|
||||
ExpenseCategories?.find((type) => type.id === ExpenseTypeId)
|
||||
);
|
||||
}, [ExpenseTypeId]);
|
||||
|
||||
const handleClose = () => {
|
||||
@ -206,7 +215,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
if (StatusLoadding || projectLoading || ExpenseLoading || isLoading)
|
||||
return <ExpenseSkeleton />;
|
||||
|
||||
|
||||
return (
|
||||
<div className="container p-3">
|
||||
<h5 className="m-0">
|
||||
@ -215,7 +223,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label className="form-label" required>Select Project</Label>
|
||||
<Label className="form-label" required>
|
||||
Select Project
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
@ -237,13 +247,13 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="expensesTypeId" className="form-label" required>
|
||||
Expense Type
|
||||
<Label htmlFor="expenseCategoryId" className="form-label" required>
|
||||
Expense Category
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expensesTypeId"
|
||||
{...register("expensesTypeId")}
|
||||
id="expenseCategoryId"
|
||||
{...register("expenseCategoryId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Type
|
||||
@ -251,16 +261,16 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
{ExpenseLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
ExpenseTypes?.map((expense) => (
|
||||
ExpenseCategories?.map((expense) => (
|
||||
<option key={expense.id} value={expense.id}>
|
||||
{expense.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.expensesTypeId && (
|
||||
{errors.expensesCategoryId && (
|
||||
<small className="danger-text">
|
||||
{errors.expensesTypeId.message}
|
||||
{errors.expensesCategoryId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
@ -295,33 +305,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="paidById" className="form-label" required>
|
||||
Paid By
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="paymentModeId"
|
||||
{...register("paidById")}
|
||||
disabled={!selectedproject}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Person
|
||||
</option>
|
||||
{EmpLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
employees?.map((emp) => (
|
||||
<option key={emp.id} value={emp.id}>
|
||||
{`${emp.firstName} ${emp.lastName} `}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.paidById && (
|
||||
<small className="danger-text">{errors.paidById.message}</small>
|
||||
)}
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<Label className="form-label" required>Paid By </Label>
|
||||
<EmployeeSearchInput
|
||||
control={control}
|
||||
name="paidById"
|
||||
projectId={null}
|
||||
forAll={expenseToEdit ? true : false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -330,7 +321,12 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
<Label htmlFor="transactionDate" className="form-label" required>
|
||||
Transaction Date
|
||||
</Label>
|
||||
<DatePicker name="transactionDate" control={control} maxDate={new Date()}/>
|
||||
<DatePicker
|
||||
name="transactionDate"
|
||||
className="w-100"
|
||||
control={control}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
|
||||
{errors.transactionDate && (
|
||||
<small className="danger-text">
|
||||
@ -409,9 +405,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="statusId" className="form-label ">
|
||||
GST Number
|
||||
GST Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -421,9 +417,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
{...register("gstNumber")}
|
||||
/>
|
||||
{errors.gstNumber && (
|
||||
<small className="danger-text">
|
||||
{errors.gstNumber.message}
|
||||
</small>
|
||||
<small className="danger-text">{errors.gstNumber.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -445,10 +439,38 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6 text-start">
|
||||
<Label htmlFor="currencyId" className="form-label" required>
|
||||
Select Currency
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="currencyId"
|
||||
{...register("currencyId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Currency
|
||||
</option>
|
||||
{currencyLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
currencies?.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{`${currency.currencyName} (${currency.symbol}) `}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.currencyId && (
|
||||
<small className="danger-text">{errors.currencyId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="description" className="form-label" required>Description</Label>
|
||||
<Label htmlFor="description" className="form-label" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
@ -465,7 +487,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label className="form-label" required>Upload Bill </Label>
|
||||
<Label className="form-label" required>
|
||||
Upload Bill{" "}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
@ -497,40 +521,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</small>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<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>
|
||||
<Filelist
|
||||
files={files}
|
||||
removeFile={removeFile}
|
||||
expenseToEdit={expenseToEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Array.isArray(errors.billAttachments) &&
|
||||
@ -549,7 +544,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
{" "}
|
||||
<button
|
||||
<button
|
||||
type="reset"
|
||||
disabled={isPending || createPending}
|
||||
onClick={handleClose}
|
||||
@ -568,7 +563,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -1,27 +1,53 @@
|
||||
import { useState } from 'react';
|
||||
import { useState } from "react";
|
||||
|
||||
const PreviewDocument = ({ imageUrl }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [rotation, setRotation] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: "50vh" }}>
|
||||
<>
|
||||
<div className="d-flex justify-content-start">
|
||||
<i
|
||||
className="bx bx-rotate-right cursor-pointer"
|
||||
onClick={() => setRotation((prev) => prev + 90)}
|
||||
></i>
|
||||
</div>
|
||||
<div
|
||||
className="position-relative d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ minHeight: "80vh" }}
|
||||
>
|
||||
|
||||
{loading && (
|
||||
<div className="text-secondary text-center mb-2">
|
||||
Loading...
|
||||
</div>
|
||||
<div className="text-secondary text-center mb-2">Loading...</div>
|
||||
)}
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Full View"
|
||||
className="img-fluid"
|
||||
style={{
|
||||
maxHeight: "100vh",
|
||||
objectFit: "contain",
|
||||
display: loading ? "none" : "block",
|
||||
}}
|
||||
onLoad={() => setLoading(false)}
|
||||
/>
|
||||
|
||||
<div className="mb-3 d-flex justify-content-center align-items-center">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Full View"
|
||||
className="img-fluid"
|
||||
style={{
|
||||
maxHeight: "80vh",
|
||||
objectFit: "contain",
|
||||
display: loading ? "none" : "block",
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
transition: "transform 0.3s ease",
|
||||
}}
|
||||
onLoad={() => setLoading(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="position-absolute bottom-0 start-0 justify-content-center gap-2">
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => setRotation(0)}
|
||||
title="Reset Rotation"
|
||||
>
|
||||
<i className="bx bx-reset"></i> Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -9,7 +9,13 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||
import { getColorNameFromHex, getIconByFileType } from "../../utils/appUtils";
|
||||
import {
|
||||
formatCurrency,
|
||||
formatFigure,
|
||||
getColorNameFromHex,
|
||||
getIconByFileType,
|
||||
localToUtc,
|
||||
} from "../../utils/appUtils";
|
||||
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import {
|
||||
@ -38,7 +44,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||
const [imageLoaded, setImageLoaded] = useState({});
|
||||
const { setDocumentView } = useExpenseContext();
|
||||
const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({});
|
||||
const ActionSchema = ExpenseActionScheam(IsPaymentProcess,data?.createdAt) ?? z.object({});
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
@ -91,9 +97,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
const onSubmit = (formData) => {
|
||||
const Payload = {
|
||||
...formData,
|
||||
reimburseDate: moment
|
||||
.utc(formData.reimburseDate, "DD-MM-YYYY")
|
||||
.toISOString(),
|
||||
reimburseDate:localToUtc(formData.reimburseDate),
|
||||
expenseId: ExpenseId,
|
||||
comment: formData.comment,
|
||||
};
|
||||
@ -105,361 +109,430 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
const handleImageLoad = (id) => {
|
||||
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
||||
};
|
||||
|
||||
console.log(errors)
|
||||
return (
|
||||
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row mb-3">
|
||||
<div className="col-12 mb-3">
|
||||
<h5 className="fw-semibold">Expense Details</h5>
|
||||
<hr />
|
||||
</div>
|
||||
<div className="text-start mb-2">
|
||||
<div className="text-muted">{data?.description}</div>
|
||||
</div>
|
||||
{/* Row 1 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Transaction Date :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.transactionDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Expense Type :
|
||||
</label>
|
||||
<div className="text-muted">{data?.expensesType?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mb-1">
|
||||
<h5 className="fw-semibold m-0">Expense Details</h5>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Supplier :
|
||||
</label>
|
||||
<div className="text-muted">{data?.supplerName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Amount :
|
||||
</label>
|
||||
<div className="text-muted">₹ {data.amount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Payment Mode :
|
||||
</label>
|
||||
<div className="text-muted">{data?.paymentMode?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
{data?.gstNumber && (
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
<div className="row mb-1 ">
|
||||
<div className="col-12 col-lg-7 col-xl-7 mb-3">
|
||||
<div className="row">
|
||||
<div className="col-12 d-flex justify-content-between text-start fw-semibold my-2">
|
||||
<span>{data?.expenseUId}</span>
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||
}`}
|
||||
t
|
||||
>
|
||||
GST Number :
|
||||
</label>
|
||||
<div className="text-muted">{data?.gstNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 4 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Status :
|
||||
</label>
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||
}`}
|
||||
>
|
||||
{data?.status?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Pre-Approved :
|
||||
</label>
|
||||
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted">{data?.project?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created At :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 6 */}
|
||||
{data.createdBy && (
|
||||
<div className="col-md-6 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created By :
|
||||
</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data.createdBy?.firstName}
|
||||
lastName={data.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data.createdBy?.firstName ?? ""} ${
|
||||
data.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-md-6 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Paid By:
|
||||
</label>
|
||||
<div className="d-flex align-items-center ">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data.paidBy?.firstName}
|
||||
lastName={data.paidBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data.paidBy?.firstName ?? ""} ${
|
||||
data.paidBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
{data?.status?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label me-2 mb-2 fw-semibold">Attachment :</label>
|
||||
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{data?.documents?.map((doc) => {
|
||||
const isImage = doc.contentType?.includes("image");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.documentId}
|
||||
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
|
||||
style={{
|
||||
width: "80px",
|
||||
cursor: isImage ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isImage) {
|
||||
setDocumentView({
|
||||
IsOpen: true,
|
||||
Image: doc.preSignedUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={`bx ${getIconByFileType(doc.contentType)}`}
|
||||
style={{ fontSize: "30px" }}
|
||||
></i>
|
||||
<small
|
||||
className="text-center text-tiny text-truncate w-100"
|
||||
title={doc.fileName}
|
||||
{/* Row 1 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
{doc.fileName}
|
||||
</small>
|
||||
Transaction Date :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.transactionDate)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.expensesReimburse && (
|
||||
<div className="row text-start mt-2">
|
||||
<div className="col-md-6 mb-sm-0 mb-2">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Transaction ID :
|
||||
</label>
|
||||
{data.expensesReimburse.reimburseTransactionId || "N/A"}
|
||||
</div>
|
||||
<div className="col-md-6 ">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Reimburse Date :
|
||||
</label>
|
||||
{formatUTCToLocalTime(data.expensesReimburse.reimburseDate)}
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Expense Category :
|
||||
</label>
|
||||
<div className="text-muted">{data?.expenseCategory?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.expensesReimburse && (
|
||||
<>
|
||||
<div className="col-md-6 d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Reimburse By :
|
||||
{/* Row 2 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Supplier :
|
||||
</label>
|
||||
<div className="text-muted">{data?.supplerName}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Amount :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{" "}
|
||||
{formatFigure(data?.amount, {
|
||||
type: "currency",
|
||||
currency: data?.currency?.currencyCode ?? "INR",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Payment Mode :
|
||||
</label>
|
||||
<div className="text-muted">{data?.paymentMode?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data?.gstNumber && (
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
GST Number :
|
||||
</label>
|
||||
<div className="text-muted">{data?.gstNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Pre-Approved :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{data.preApproved ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 5 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted">{data?.project?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created At :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Created & Paid By */}
|
||||
{data.createdBy && (
|
||||
<div className="col-md-6 text-start mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created By :
|
||||
</label>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-1"
|
||||
firstName={data.createdBy?.firstName}
|
||||
lastName={data.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data.createdBy?.firstName ?? ""} ${
|
||||
data.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-md-6 text-start mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Paid By :
|
||||
</label>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-1"
|
||||
firstName={data?.expensesReimburse?.reimburseBy?.firstName}
|
||||
lastName={data?.expensesReimburse?.reimburseBy?.lastName}
|
||||
firstName={data.paidBy?.firstName}
|
||||
lastName={data.paidBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data?.expensesReimburse?.reimburseBy?.firstName} ${data?.expensesReimburse?.reimburseBy?.lastName}`.trim()}
|
||||
{`${data.paidBy?.firstName ?? ""} ${
|
||||
data.paidBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<hr className="divider my-1 border-2 divider-primary my-2" />
|
||||
</div>
|
||||
|
||||
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
|
||||
<>
|
||||
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Transaction Id </label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("reimburseTransactionId")}
|
||||
/>
|
||||
{errors.reimburseTransactionId && (
|
||||
<small className="danger-text">
|
||||
{errors.reimburseTransactionId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Transaction Date </label>
|
||||
<DatePicker
|
||||
name="reimburseDate"
|
||||
control={control}
|
||||
minDate={data?.transactionDate}
|
||||
/>
|
||||
{errors.reimburseDate && (
|
||||
<small className="danger-text">
|
||||
{errors.reimburseDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Reimburse By </label>
|
||||
<EmployeeSearchInput
|
||||
control={control}
|
||||
name="reimburseById"
|
||||
projectId={null}
|
||||
/>
|
||||
{/* Description */}
|
||||
<div className="col-12 text-start mb-2">
|
||||
<label className="fw-semibold form-label">Description : </label>
|
||||
<div className="text-muted">{data?.description}</div>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
<div className="col-12 text-start mb-2">
|
||||
<label className="form-label me-2 mb-2 fw-semibold">
|
||||
Attachment :
|
||||
</label>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{data?.documents?.map((doc) => {
|
||||
const isImage = doc.contentType?.includes("image");
|
||||
return (
|
||||
<div
|
||||
key={doc.documentId}
|
||||
className="d-flex align-items-center cusor-pointer"
|
||||
onClick={() => {
|
||||
if (isImage) {
|
||||
setDocumentView({
|
||||
IsOpen: true,
|
||||
Image: doc.preSignedUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={`bx ${getIconByFileType(doc.contentType)}`}
|
||||
style={{ fontSize: "30px" }}
|
||||
></i>
|
||||
<small
|
||||
className="text-center text-tiny text-truncate w-100"
|
||||
title={doc.fileName}
|
||||
>
|
||||
{doc.fileName}
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-12 mb-3 text-start">
|
||||
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
|
||||
(IsRejectedExpense && isCreatedBy)) && (
|
||||
<>
|
||||
<Label className="form-label me-2 mb-0" required>Comment</Label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
rows="2"
|
||||
/>
|
||||
{errors.comment && (
|
||||
<small className="danger-text">
|
||||
{errors.comment.message}
|
||||
</small>
|
||||
|
||||
{data.expensesReimburse && (
|
||||
<div className="row text-start mt-2">
|
||||
<div className="col-md-6 mb-sm-0 mb-2">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Transaction ID :
|
||||
</label>
|
||||
{data.expensesReimburse.reimburseTransactionId || "N/A"}
|
||||
</div>
|
||||
<div className="col-md-6 ">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Reimburse Date :
|
||||
</label>
|
||||
{formatUTCToLocalTime(data.expensesReimburse.reimburseDate)}
|
||||
</div>
|
||||
|
||||
{data.expensesReimburse && (
|
||||
<>
|
||||
<div className="col-md-6 d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Reimburse By :
|
||||
</label>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-1"
|
||||
firstName={
|
||||
data?.expensesReimburse?.reimburseBy?.firstName
|
||||
}
|
||||
lastName={
|
||||
data?.expensesReimburse?.reimburseBy?.lastName
|
||||
}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data?.expensesReimburse?.reimburseBy?.firstName} ${data?.expensesReimburse?.reimburseBy?.lastName}`.trim()}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* <hr className="divider my-1 border-2 divider-primary my-2" /> */}
|
||||
|
||||
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
|
||||
<>
|
||||
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
|
||||
<div className="row ">
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<Label className="form-label" required>Transaction Id </Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("reimburseTransactionId")}
|
||||
/>
|
||||
{errors.reimburseTransactionId && (
|
||||
<small className="danger-text">
|
||||
{errors.reimburseTransactionId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start mb-1">
|
||||
<Label className="form-label" required>Transaction Date </Label>
|
||||
<DatePicker
|
||||
name="reimburseDate"
|
||||
control={control}
|
||||
minDate={data?.transactionDate}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
{errors.reimburseDate && (
|
||||
<small className="danger-text">
|
||||
{errors.reimburseDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start mb-1">
|
||||
<Label className="form-label" required>
|
||||
Reimburse By{" "}
|
||||
</Label>
|
||||
<EmployeeSearchInput
|
||||
control={control}
|
||||
name="reimburseById"
|
||||
projectId={null}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<Label className="form-label" >
|
||||
TDS Percentage
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("tdsPercentage")}
|
||||
/>
|
||||
{errors.tdsPercentage && (
|
||||
<small className="danger-text">
|
||||
{errors.tdsPercentage.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<Label className="form-label" required>
|
||||
Base Amount
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("baseAmount")}
|
||||
/>
|
||||
{errors.baseAmount && (
|
||||
<small className="danger-text">
|
||||
{errors.baseAmount.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<Label className="form-label" required>
|
||||
Tax Amount
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("taxAmount")}
|
||||
/>
|
||||
{errors.taxAmount && (
|
||||
<small className="danger-text">
|
||||
{errors.taxAmount.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-12 mb-3 text-start mt-1">
|
||||
{((nextStatusWithPermission.length > 0 &&
|
||||
!IsRejectedExpense) ||
|
||||
(IsRejectedExpense && isCreatedBy)) && (
|
||||
<>
|
||||
<Label className="form-label me-2 mb-0" required>
|
||||
Comment
|
||||
</Label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
rows="2"
|
||||
/>
|
||||
{errors.comment && (
|
||||
<small className="danger-text">
|
||||
{errors.comment.message}
|
||||
</small>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{nextStatusWithPermission?.length > 0 &&
|
||||
(!IsRejectedExpense || isCreatedBy) && (
|
||||
<div className="text-end flex-wrap gap-2 my-2 mt-3">
|
||||
{nextStatusWithPermission.map((status, index) => (
|
||||
<button
|
||||
key={status.id || index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClickedStatusId(status.id);
|
||||
setValue("statusId", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
disabled={isPending || isFetching}
|
||||
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
|
||||
>
|
||||
{isPending && clickedStatusId === status.id
|
||||
? "Please Wait..."
|
||||
: status.displayName || status.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{nextStatusWithPermission?.length > 0 &&
|
||||
(!IsRejectedExpense || isCreatedBy) && (
|
||||
<div className="text-end flex-wrap gap-2 my-2 mt-3">
|
||||
{nextStatusWithPermission.map((status, index) => (
|
||||
<button
|
||||
key={status.id || index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClickedStatusId(status.id);
|
||||
setValue("statusId", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
disabled={isPending || isFetching}
|
||||
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
|
||||
>
|
||||
{isPending && clickedStatusId === status.id
|
||||
? "Please Wait..."
|
||||
: status.displayName || status.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ExpenseStatusLogs data={data} />
|
||||
<div className="col-12 col-lg-5 col-xl-5">
|
||||
<div className="d-flex align-items-center text-secondary mb-4">
|
||||
<i className="bx bx-time-five me-2"></i>{" "}
|
||||
<p className=" m-0">TimeLine</p>
|
||||
</div>
|
||||
<ExpenseStatusLogs data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback, useEffect, useState, useMemo } from "react";
|
||||
import getGreetingMessage from "../../utils/greetingHandler";
|
||||
import {
|
||||
cacheData,
|
||||
@ -14,121 +15,104 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import Avatar from "../../components/common/Avatar";
|
||||
import { useChangePassword } from "../Context/ChangePasswordContext";
|
||||
import { useProjects } from "../../hooks/useProjects";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import eventBus from "../../services/eventBus";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { MANAGE_PROJECT } from "../../utils/constants";
|
||||
import { ALLOW_PROJECTSTATUS_ID, MANAGE_PROJECT, UUID_REGEX } from "../../utils/constants";
|
||||
import { useAuthModal, useLogout } from "../../hooks/useAuth";
|
||||
|
||||
const Header = () => {
|
||||
const { profile } = useProfile();
|
||||
const { data: masterData } = useMaster();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const { data, loading } = useMaster();
|
||||
const navigate = useNavigate();
|
||||
const {onOpen} = useAuthModal()
|
||||
|
||||
const { mutate: logout, isPending: logouting } = useLogout();
|
||||
const { onOpen } = useAuthModal();
|
||||
const { openChangePassword } = useChangePassword();
|
||||
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
|
||||
const { mutate : logout,isPending:logouting} = useLogout()
|
||||
|
||||
const isDashboardPath =
|
||||
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
|
||||
const isProjectPath = /^\/projects$/.test(location.pathname);
|
||||
const pathname = location.pathname;
|
||||
|
||||
const showProjectDropdown = (pathname) => {
|
||||
const isDirectoryPath = /^\/directory$/.test(pathname);
|
||||
// ======= MEMO CHECKS =======
|
||||
|
||||
// const isProfilePage = /^\/employee$/.test(location.pathname);
|
||||
const isProfilePage =
|
||||
/^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
||||
pathname
|
||||
);
|
||||
const isExpensePage = /^\/expenses$/.test(pathname);
|
||||
const isDashboardPath = pathname === "/" || pathname === "/dashboard";
|
||||
const isProjectPath = pathname === "/projects";
|
||||
const isDirectory = pathname === "/directory";
|
||||
const isEmployeeList = pathname === "/employees";
|
||||
const isEmployeeProfile = UUID_REGEX.test(pathname);
|
||||
const isMasters = pathname === "/masters";
|
||||
const isExpensePath = pathname.startsWith("/expenses");
|
||||
|
||||
return !(isDirectoryPath || isProfilePage || isExpensePage);
|
||||
};
|
||||
const allowedProjectStatusIds = [
|
||||
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
|
||||
"cdad86aa-8a56-4ff4-b633-9c629057dfef",
|
||||
"b74da4c2-d07e-46f2-9919-e75e49b12731",
|
||||
];
|
||||
const hideDropPaths =
|
||||
isDirectory || isEmployeeList || isMasters || isEmployeeProfile || isExpensePath;
|
||||
|
||||
const getRole = (roles, joRoleId) => {
|
||||
if (!Array.isArray(roles)) return "User";
|
||||
let role = roles.find((role) => role.id === joRoleId);
|
||||
return role ? role.name : "User";
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleProfilePage = () => {
|
||||
navigate(`/employee/${profile?.employeeInfo?.id}`);
|
||||
};
|
||||
const showProjectDropdown = !hideDropPaths;
|
||||
|
||||
// ===== Project Names & Selected Project =====
|
||||
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
||||
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
const projectsForDropdown = isDashboardPath
|
||||
? projectNames
|
||||
: projectNames?.filter((project) =>
|
||||
allowedProjectStatusIds.includes(project.projectStatusId)
|
||||
);
|
||||
const projectsForDropdown = useMemo(
|
||||
() =>
|
||||
isDashboardPath
|
||||
? projectNames
|
||||
: projectNames?.filter((project) =>
|
||||
ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId)
|
||||
),
|
||||
[projectNames, isDashboardPath]
|
||||
);
|
||||
|
||||
let currentProjectDisplayName;
|
||||
if (projectLoading) {
|
||||
currentProjectDisplayName = "Loading...";
|
||||
} else if (!projectNames || projectNames.length === 0) {
|
||||
currentProjectDisplayName = "No Projects Assigned";
|
||||
} else if (projectNames.length === 1) {
|
||||
currentProjectDisplayName = projectNames[0].name;
|
||||
} else {
|
||||
if (selectedProject === null) {
|
||||
currentProjectDisplayName = "All Projects";
|
||||
} else {
|
||||
const selectedProjectObj = projectNames.find(
|
||||
(p) => p?.id === selectedProject
|
||||
);
|
||||
currentProjectDisplayName = selectedProjectObj
|
||||
? selectedProjectObj.name
|
||||
: "All Projects";
|
||||
}
|
||||
}
|
||||
const currentProjectDisplayName = useMemo(() => {
|
||||
if (projectLoading) return "Loading...";
|
||||
if (!projectNames?.length) return "No Projects Assigned";
|
||||
if (projectNames.length === 1) return projectNames[0].name;
|
||||
if (selectedProject === null) return "All Projects";
|
||||
const selectedObj = projectNames.find((p) => p.id === selectedProject);
|
||||
return selectedObj
|
||||
? selectedObj.name
|
||||
: projectNames[0]?.name || "No Projects Assigned";
|
||||
}, [projectLoading, projectNames, selectedProject]);
|
||||
|
||||
const { openChangePassword } = useChangePassword();
|
||||
// ===== Role Helper =====
|
||||
const getRole = (roles, joRoleId) => {
|
||||
if (!Array.isArray(roles)) return "User";
|
||||
return roles.find((r) => r.id === joRoleId)?.name || "User";
|
||||
};
|
||||
|
||||
// ===== Navigate to Profile =====
|
||||
const handleProfilePage = () =>
|
||||
navigate(`/employee/${profile?.employeeInfo?.id}`);
|
||||
|
||||
// ===== Set default project on load =====
|
||||
useEffect(() => {
|
||||
if (
|
||||
projectNames &&
|
||||
projectNames.length > 0 &&
|
||||
projectNames?.length &&
|
||||
selectedProject === undefined &&
|
||||
!getCachedData("hasReceived")
|
||||
) {
|
||||
if (projectNames.length === 1) {
|
||||
dispatch(setProjectId(projectNames[0]?.id || null));
|
||||
dispatch(setProjectId(projectNames[0].id || null));
|
||||
} else {
|
||||
if (isDashboardPath) {
|
||||
dispatch(setProjectId(null));
|
||||
} else {
|
||||
const firstAllowedProject = projectNames.find((project) =>
|
||||
allowedProjectStatusIds.includes(project.projectStatusId)
|
||||
const firstAllowed = projectNames.find((project) =>
|
||||
ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId)
|
||||
);
|
||||
dispatch(setProjectId(firstAllowedProject?.id || null));
|
||||
dispatch(setProjectId(firstAllowed?.id || null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [projectNames, selectedProject, dispatch, isDashboardPath]);
|
||||
|
||||
// ===== Event Handlers =====
|
||||
const handler = useCallback(
|
||||
async (data) => {
|
||||
if (!HasManageProjectPermission) {
|
||||
await fetchData();
|
||||
const projectExist = data.projectIds.some(
|
||||
(item) => item === selectedProject
|
||||
);
|
||||
if (projectExist) {
|
||||
if (data.projectIds?.includes(selectedProject)) {
|
||||
cacheData("hasReceived", false);
|
||||
}
|
||||
}
|
||||
@ -138,14 +122,15 @@ const Header = () => {
|
||||
|
||||
const newProjectHandler = useCallback(
|
||||
async (msg) => {
|
||||
if (HasManageProjectPermission && msg.keyword === "Create_Project") {
|
||||
await fetchData();
|
||||
} else if (projectNames?.some((item) => item.id === msg.response.id)) {
|
||||
if (
|
||||
msg.keyword === "Create_Project" ||
|
||||
projectNames?.some((p) => p.id === msg.response?.id)
|
||||
) {
|
||||
await fetchData();
|
||||
cacheData("hasReceived", false);
|
||||
}
|
||||
cacheData("hasReceived", false);
|
||||
},
|
||||
[HasManageProjectPermission, projectNames, fetchData]
|
||||
[projectNames, fetchData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -162,10 +147,10 @@ const Header = () => {
|
||||
};
|
||||
}, [handler, newProjectHandler]);
|
||||
|
||||
const handleProjectChange = (project) => {
|
||||
dispatch(setProjectId(project));
|
||||
|
||||
if (isProjectPath && project !== null) {
|
||||
// ===== Project Change =====
|
||||
const handleProjectChange = (projectId) => {
|
||||
dispatch(setProjectId(projectId));
|
||||
if (isProjectPath && projectId !== null) {
|
||||
navigate("/projects/details");
|
||||
}
|
||||
};
|
||||
@ -189,7 +174,7 @@ const Header = () => {
|
||||
className="navbar-nav-right d-flex align-items-center justify-content-between"
|
||||
id="navbar-collapse"
|
||||
>
|
||||
{showProjectDropdown(location.pathname) && (
|
||||
{showProjectDropdown && (
|
||||
<div className="align-items-center">
|
||||
<i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>
|
||||
<div className="btn-group">
|
||||
@ -215,16 +200,14 @@ const Header = () => {
|
||||
className="dropdown-menu"
|
||||
style={{ overflow: "auto", maxHeight: "300px" }}
|
||||
>
|
||||
{isDashboardPath && (
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => handleProjectChange(null)}
|
||||
>
|
||||
All Projects
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => handleProjectChange(null)}
|
||||
>All Project</button>
|
||||
</li>
|
||||
|
||||
{[...projectsForDropdown]
|
||||
.sort((a, b) => a?.name?.localeCompare(b.name))
|
||||
.map((project) => (
|
||||
@ -249,112 +232,9 @@ const Header = () => {
|
||||
)}
|
||||
|
||||
<ul className="navbar-nav flex-row align-items-center ms-md-auto">
|
||||
<li className="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0">
|
||||
<a
|
||||
className="nav-link dropdown-toggle hide-arrow"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="icon-base bx bx-grid-alt icon-md"></i>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end p-0">
|
||||
<div className="dropdown-menu-header border-bottom">
|
||||
<div className="dropdown-header d-flex align-items-center py-3">
|
||||
<h6 className="mb-0 me-auto">Shortcuts</h6>
|
||||
<a
|
||||
className="dropdown-shortcuts-add py-2 cusror-pointer"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
aria-label="Add shortcuts"
|
||||
data-bs-original-title="Add shortcuts"
|
||||
>
|
||||
<i className="icon-base bx bx-plus-circle text-heading"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropdown-shortcuts-list scrollable-container ps">
|
||||
<div className="row row-bordered overflow-visible g-0">
|
||||
<div className="dropdown-shortcuts-item col">
|
||||
<a
|
||||
onClick={() => navigate(`/dashboard`)}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||
<i className="icon-base bx bx-home icon-26px text-heading"></i>
|
||||
</span>
|
||||
Dashboard
|
||||
</a>
|
||||
<small>User Dashboard</small>
|
||||
</div>
|
||||
<div className="dropdown-shortcuts-item col">
|
||||
<a
|
||||
onClick={() => navigate(`/projects`)}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||
<i className="icon-base bx bx-building-house icon-26px text-heading"></i>
|
||||
</span>
|
||||
Projects
|
||||
</a>
|
||||
<small>Projects List</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row-bordered overflow-visible g-0">
|
||||
<div className="dropdown-shortcuts-item col">
|
||||
<a
|
||||
onClick={() => navigate(`/employees`)}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||
<i className="icon-base bx bxs-user-account icon-26px text-heading"></i>
|
||||
</span>
|
||||
Employees
|
||||
</a>
|
||||
<small>Manage Employees</small>
|
||||
</div>
|
||||
<div className="dropdown-shortcuts-item col">
|
||||
<a
|
||||
onClick={() => navigate(`/activities/attendance`)}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||
<i className="icon-base bx bx-user-check icon-26px text-heading"></i>
|
||||
</span>
|
||||
Attendance
|
||||
</a>
|
||||
<small>Manage Attendance</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row-bordered overflow-visible g-0">
|
||||
<div className="dropdown-shortcuts-item col">
|
||||
<a
|
||||
onClick={() => navigate(`/activities/task`)}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||
<i className="icon-base bx bxs-wrench icon-26px text-heading"></i>
|
||||
</span>
|
||||
Allocate Work
|
||||
</a>
|
||||
<small>Work Allocations</small>
|
||||
</div>
|
||||
<div className="dropdown-shortcuts-item col">
|
||||
<a
|
||||
onClick={() => navigate(`/activities/records`)}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="dropdown-shortcuts-icon rounded-circle mb-3">
|
||||
<i className="icon-base bx bx-list-ul icon-26px text-heading"></i>
|
||||
</span>
|
||||
Daily Work Log
|
||||
</a>
|
||||
<small>Daily Work Allocations</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/* {HasManageProjectPermission && ( */}
|
||||
|
||||
{/* )} */}
|
||||
<li className="nav-item navbar-dropdown dropdown-user dropdown">
|
||||
<a
|
||||
aria-label="dropdown profile avatar"
|
||||
@ -386,7 +266,7 @@ const Header = () => {
|
||||
{profile?.employeeInfo?.firstName}
|
||||
</span>
|
||||
<small className="text-muted">
|
||||
{getRole(data, profile?.employeeInfo?.joRoleId)}
|
||||
{getRole(masterData, profile?.employeeInfo?.joRoleId)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -395,15 +275,13 @@ const Header = () => {
|
||||
<li>
|
||||
<div className="dropdown-divider"></div>
|
||||
</li>
|
||||
<li onClick={()=>onOpen()}>
|
||||
{/* <li onClick={() => onOpen()}>
|
||||
{" "}
|
||||
<a
|
||||
className="dropdown-item cusor-pointer"
|
||||
>
|
||||
<a className="dropdown-item cusor-pointer">
|
||||
<i className="bx bx-transfer-alt me-2"></i>
|
||||
<span className="align-middle">Switch Workspace</span>
|
||||
</a>
|
||||
</li>
|
||||
</li> */}
|
||||
<li onClick={handleProfilePage}>
|
||||
<a
|
||||
aria-label="go to profile"
|
||||
@ -433,7 +311,6 @@ const Header = () => {
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<div className="dropdown-divider"></div>
|
||||
</li>
|
||||
@ -441,10 +318,17 @@ const Header = () => {
|
||||
<a
|
||||
aria-label="click to log out"
|
||||
className="dropdown-item cusor-pointer"
|
||||
onClick={()=>logout()}
|
||||
onClick={() => logout()}
|
||||
>
|
||||
{logouting ? "Please Wait":<> <i className="bx bx-log-out me-2"></i>
|
||||
<span className="align-middle">SignOut</span></>}
|
||||
{logouting ? (
|
||||
"Please Wait"
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<i className="bx bx-log-out me-2"></i>
|
||||
<span className="align-middle">SignOut</span>
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@ -454,4 +338,4 @@ const Header = () => {
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
247
src/components/PaymentRequest/MakeExpense.jsx
Normal file
@ -0,0 +1,247 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
DefaultRequestedExpense,
|
||||
RequestedExpenseSchema,
|
||||
} from "./PaymentRequestSchema";
|
||||
import Label from "../common/Label";
|
||||
import { usePaymentMode } from "../../hooks/masterHook/useMaster";
|
||||
import { useCreatePaymentRequestExpense, useCreateRecurringExpense } from "../../hooks/useExpense";
|
||||
import Filelist from "../Expenses/Filelist";
|
||||
import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage";
|
||||
|
||||
const MakeExpense = ({ onClose }) => {
|
||||
const {isExpenseGenerate} = usePaymentRequestContext()
|
||||
const {
|
||||
PaymentModes,
|
||||
loading: PaymentModeLoading,
|
||||
error: PaymentModeError,
|
||||
} = usePaymentMode();
|
||||
|
||||
const {
|
||||
setValue,
|
||||
register,
|
||||
watch,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(RequestedExpenseSchema),
|
||||
defaultValues: DefaultRequestedExpense,
|
||||
});
|
||||
const files = watch("billAttachments");
|
||||
const onFileChange = async (e) => {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
const existingFiles = watch("billAttachments") || [];
|
||||
|
||||
const parsedFiles = await Promise.all(
|
||||
newFiles.map(async (file) => {
|
||||
const base64Data = await toBase64(file);
|
||||
return {
|
||||
fileName: file.name,
|
||||
base64Data,
|
||||
contentType: file.type,
|
||||
fileSize: file.size,
|
||||
description: "",
|
||||
isActive: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const combinedFiles = [
|
||||
...existingFiles,
|
||||
...parsedFiles.filter(
|
||||
(newFile) =>
|
||||
!existingFiles.some(
|
||||
(f) =>
|
||||
f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
setValue("billAttachments", combinedFiles, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
const toBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result.split(",")[1]);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
const removeFile = (index) => {
|
||||
debugger
|
||||
const newFiles = files.filter((_, i) => i !== index);
|
||||
setValue("billAttachments", newFiles, { shouldValidate: true });
|
||||
};
|
||||
|
||||
const { mutate: CreatedExpense, isPending } = useCreatePaymentRequestExpense(
|
||||
() => {
|
||||
handleClose();
|
||||
}
|
||||
);
|
||||
const onSubmit = (formData) => {
|
||||
let payload = {
|
||||
...formData,
|
||||
paymentRequestId:isExpenseGenerate?.requestId
|
||||
}
|
||||
CreatedExpense(payload)
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div className="row px-2 py-3">
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
<h5 className="m-0">
|
||||
Create New Expense
|
||||
</h5>
|
||||
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6 mb-2">
|
||||
<Label htmlFor="paymentModeId" className="form-label" required>
|
||||
Payment Mode
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="paymentModeId"
|
||||
{...register("paymentModeId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Mode
|
||||
</option>
|
||||
{PaymentModeLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
PaymentModes?.map((payment) => (
|
||||
<option key={payment.id} value={payment.id}>
|
||||
{payment.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.paymentModeId && (
|
||||
<small className="danger-text">
|
||||
{errors.paymentModeId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="statusId" className="form-label ">
|
||||
GST Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="gstNumber"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
{...register("gstNumber")}
|
||||
/>
|
||||
{errors.gstNumber && (
|
||||
<small className="danger-text">{errors.gstNumber.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 text-start">
|
||||
<Label htmlFor="location" className="form-label" required>
|
||||
Location
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
className="form-control form-control-sm"
|
||||
{...register("location")}
|
||||
/>
|
||||
{errors.location && (
|
||||
<small className="danger-text">{errors.location.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label className="form-label" required>
|
||||
Upload Bill{" "}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("billAttachments").click()}
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"> </i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
<small className="text-muted">(PDF, JPG, PNG, max 5MB)</small>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="billAttachments"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
{...register("billAttachments")}
|
||||
onChange={(e) => {
|
||||
onFileChange(e);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.billAttachments && (
|
||||
<small className="danger-text">
|
||||
{errors.billAttachments.message}
|
||||
</small>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<Filelist
|
||||
files={files}
|
||||
removeFile={removeFile}
|
||||
expenseToEdit={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Array.isArray(errors.billAttachments) &&
|
||||
errors.billAttachments.map((fileError, index) => (
|
||||
<div key={index} className="danger-text small mt-1">
|
||||
{
|
||||
(fileError?.fileSize?.message ||
|
||||
fileError?.contentType?.message ||
|
||||
fileError?.base64Data?.message,
|
||||
fileError?.documentId?.message)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
{" "}
|
||||
<button
|
||||
type="reset"
|
||||
disabled={isPending}
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MakeExpense;
|
||||
539
src/components/PaymentRequest/ManagePaymentRequest.jsx
Normal file
@ -0,0 +1,539 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useCurrencies, useProjectName } from "../../hooks/useProjects";
|
||||
import Label from "../common/Label";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import {
|
||||
useCreatePaymentRequest,
|
||||
usePayee,
|
||||
usePaymentRequestDetail,
|
||||
useUpdatePaymentRequest,
|
||||
} from "../../hooks/useExpense";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||
import {
|
||||
defaultPaymentRequest,
|
||||
PaymentRequestSchema,
|
||||
} from "./PaymentRequestSchema";
|
||||
import { INR_CURRENCY_CODE } from "../../utils/constants";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import Filelist from "../Expenses/Filelist";
|
||||
import InputSuggestions from "../common/InputSuggestion";
|
||||
|
||||
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error: requestError,
|
||||
} = usePaymentRequestDetail(requestToEdit);
|
||||
|
||||
const {
|
||||
projectNames,
|
||||
loading: projectLoading,
|
||||
error,
|
||||
isError: isProjectError,
|
||||
} = useProjectName();
|
||||
|
||||
const {
|
||||
data: currencyData,
|
||||
isLoading: currencyLoading,
|
||||
isError: currencyError,
|
||||
} = useCurrencies();
|
||||
|
||||
const {
|
||||
ExpenseCategories,
|
||||
loading: ExpenseLoading,
|
||||
error: ExpenseError,
|
||||
} = useExpenseCategory();
|
||||
|
||||
|
||||
const {
|
||||
data: Payees,
|
||||
isLoading: isPayeeLoaing,
|
||||
isError: isPayeeError,
|
||||
error: payeeError,
|
||||
} = usePayee();
|
||||
const schema = PaymentRequestSchema(ExpenseCategories);
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaultPaymentRequest,
|
||||
});
|
||||
|
||||
const [isItself, setisItself] = useState(false);
|
||||
|
||||
const files = watch("billAttachments");
|
||||
const onFileChange = async (e) => {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
const existingFiles = watch("billAttachments") || [];
|
||||
|
||||
const parsedFiles = await Promise.all(
|
||||
newFiles.map(async (file) => {
|
||||
const base64Data = await toBase64(file);
|
||||
return {
|
||||
fileName: file.name,
|
||||
base64Data,
|
||||
contentType: file.type,
|
||||
fileSize: file.size,
|
||||
description: "",
|
||||
isActive: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const combinedFiles = [
|
||||
...existingFiles,
|
||||
...parsedFiles.filter(
|
||||
(newFile) =>
|
||||
!existingFiles.some(
|
||||
(f) =>
|
||||
f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
setValue("billAttachments", combinedFiles, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
const toBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result.split(",")[1]);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
const removeFile = (index) => {
|
||||
if (requestToEdit) {
|
||||
const newFiles = files.map((file, i) => {
|
||||
if (file.documentId !== index) return file;
|
||||
return {
|
||||
...file,
|
||||
isActive: false,
|
||||
};
|
||||
});
|
||||
setValue("billAttachments", newFiles, { shouldValidate: true });
|
||||
} else {
|
||||
const newFiles = files.filter((_, i) => i !== index);
|
||||
setValue("billAttachments", newFiles, { shouldValidate: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const { mutate: CreatePaymentRequest, isPending: createPending } =
|
||||
useCreatePaymentRequest(() => {
|
||||
handleClose();
|
||||
});
|
||||
const { mutate: PaymentRequestUpdate, isPending } = useUpdatePaymentRequest(
|
||||
() => handleClose()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestToEdit && data) {
|
||||
reset({
|
||||
title: data.title || "",
|
||||
description: data.description || "",
|
||||
payee: data.payee || "",
|
||||
currencyId: data.currency.id || "",
|
||||
amount: data.amount || "",
|
||||
dueDate: data.dueDate?.slice(0, 10) || "",
|
||||
projectId: data.project.id || "",
|
||||
expenseCategoryId: data.expenseCategory.id || "",
|
||||
isAdvancePayment: data.isAdvancePayment || false,
|
||||
billAttachments: data.attachments
|
||||
? data?.attachments?.map((doc) => ({
|
||||
fileName: doc.fileName,
|
||||
base64Data: null,
|
||||
contentType: doc.contentType,
|
||||
documentId: doc.id,
|
||||
fileSize: 0,
|
||||
description: "",
|
||||
preSignedUrl: doc.preSignedUrl,
|
||||
isActive: doc.isActive ?? true,
|
||||
}))
|
||||
: [],
|
||||
});
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestToEdit && currencyData && currencyData.length > 0) {
|
||||
const inrCurrency = currencyData.find((c) => c.id === INR_CURRENCY_CODE);
|
||||
if (inrCurrency) {
|
||||
setValue("currencyId", INR_CURRENCY_CODE, { shouldValidate: true });
|
||||
}
|
||||
}
|
||||
}, [currencyData, requestToEdit, setValue]);
|
||||
|
||||
const onSubmit = (fromdata) => {
|
||||
let payload = {
|
||||
...fromdata,
|
||||
dueDate: localToUtc(fromdata.dueDate),
|
||||
payee: isItself ? `${profile?.employeeInfo?.firstName} ${profile?.employeeInfo?.lastName}` : fromdata.payee,
|
||||
};
|
||||
if (requestToEdit) {
|
||||
const editPayload = {
|
||||
...payload,
|
||||
id: data.id,
|
||||
payee: isItself ? `${profile?.employeeInfo?.firstName} ${profile?.employeeInfo?.lastName}` : fromdata.payee,
|
||||
};
|
||||
PaymentRequestUpdate({ id: data.id, payload: editPayload });
|
||||
} else {
|
||||
CreatePaymentRequest(payload);
|
||||
}
|
||||
};
|
||||
const handleSetItSelf = (e) => {
|
||||
setisItself(e.target.value);
|
||||
let name = `${profile?.employeeInfo.firstName} ${profile?.employeeInfo.lastName}`
|
||||
|
||||
setValue(
|
||||
"payee",
|
||||
name
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container p-3">
|
||||
<h5 className="m-0">
|
||||
{requestToEdit ? "Update Payment Request " : "Create Payment Request"}
|
||||
</h5>
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Project and Category */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label className="form-label" required>
|
||||
Select Project
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
>
|
||||
<option value="">Select Project</option>
|
||||
{projectLoading ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
projectNames?.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.projectId && (
|
||||
<small className="danger-text">{errors.projectId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="expenseCategoryId" className="form-label" required>
|
||||
Expense Category
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expenseCategoryId"
|
||||
{...register("expenseCategoryId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Category
|
||||
</option>
|
||||
{ExpenseLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
ExpenseCategories?.map((expense) => (
|
||||
<option key={expense.id} value={expense.id}>
|
||||
{expense.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.expenseCategoryId && (
|
||||
<small className="danger-text">
|
||||
{errors.expenseCategoryId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and Advance Payment */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="title" className="form-label" required>
|
||||
Title
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
className="form-control form-control-sm"
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors.title && (
|
||||
<small className="danger-text">{errors.title.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 ">
|
||||
<Label htmlFor="isAdvance" className="form-label">
|
||||
Is Advance Payment
|
||||
</Label>
|
||||
|
||||
<Controller
|
||||
name="isAdvancePayment"
|
||||
control={control}
|
||||
defaultValue={defaultPaymentRequest.isAdvancePayment ?? false}
|
||||
render={({ field }) => (
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<div className="form-check d-flex flex-row m-0 gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="isAdvancePayment"
|
||||
className="form-check-input m-0"
|
||||
// mark checked when the controlled value is true
|
||||
checked={field.value === true}
|
||||
onChange={() => field.onChange(true)} // send boolean true
|
||||
/>
|
||||
<Label
|
||||
className="form-check-label"
|
||||
>
|
||||
Yes
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="form-check d-flex flex-row m-0 gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="isVariableFalse"
|
||||
className="form-check-input m-0"
|
||||
checked={field.value === false}
|
||||
onChange={() => field.onChange(false)} // send boolean false
|
||||
/>
|
||||
<Label
|
||||
className="form-check-label"
|
||||
>
|
||||
No
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{errors.isVariable && (
|
||||
<small className="danger-text">{errors.isVariable.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date and Amount */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="dueDate" className="form-label" required>
|
||||
Due Date
|
||||
</Label>
|
||||
<DatePicker
|
||||
name="dueDate"
|
||||
control={control}
|
||||
minDate={new Date()}
|
||||
className="w-100"
|
||||
/>
|
||||
|
||||
{errors.dueDate && (
|
||||
<small className="danger-text">{errors.dueDate.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.amount && (
|
||||
<small className="danger-text">{errors.amount.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payee and Currency */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="payee" className="form-label" required>
|
||||
Payee (Supplier Name/Transporter Name/Other)
|
||||
</Label>
|
||||
<InputSuggestions
|
||||
organizationList={Payees}
|
||||
value={watch("payee") || ""}
|
||||
onChange={(val) =>
|
||||
setValue("payee", val, { shouldValidate: true })
|
||||
}
|
||||
error={errors.payee?.message}
|
||||
/>
|
||||
|
||||
|
||||
{/* Checkbox below input */}
|
||||
<div className="form-check mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="sameAsSupplier"
|
||||
className="form-check-input"
|
||||
value={isItself}
|
||||
onChange={handleSetItSelf}
|
||||
/>
|
||||
<Label htmlFor="sameAsSupplier" className="form-check-label">
|
||||
It self
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="currencyId" className="form-label" required>
|
||||
Currency
|
||||
</Label>
|
||||
<select
|
||||
id="currencyId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("currencyId")}
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
|
||||
{currencyLoading && <option>Loading...</option>}
|
||||
|
||||
{!currencyLoading &&
|
||||
!currencyError &&
|
||||
currencyData?.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{`${currency.currencyName} (${currency.symbol})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.currencyId && (
|
||||
<small className="danger-text">{errors.currencyId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="description" className="form-label" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
{...register("description")}
|
||||
rows="2"
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<small className="danger-text">
|
||||
{errors.description.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload Document */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label className="form-label">Upload Bill </Label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("billAttachments").click()}
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"> </i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
<small className="text-muted">(PDF, JPG, PNG, max 5MB)</small>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="billAttachments"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
{...register("billAttachments")}
|
||||
onChange={(e) => {
|
||||
onFileChange(e);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.billAttachments && (
|
||||
<small className="danger-text">
|
||||
{errors.billAttachments.message}
|
||||
</small>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<Filelist
|
||||
files={files}
|
||||
removeFile={removeFile}
|
||||
expenseToEdit={requestToEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Array.isArray(errors.billAttachments) &&
|
||||
errors.billAttachments.map((fileError, index) => (
|
||||
<div key={index} className="danger-text small mt-1">
|
||||
{
|
||||
(fileError?.fileSize?.message ||
|
||||
fileError?.contentType?.message ||
|
||||
fileError?.base64Data?.message,
|
||||
fileError?.documentId?.message)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
<button
|
||||
type="reset"
|
||||
disabled={createPending || isPending}
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
disabled={createPending || isPending}
|
||||
>
|
||||
{createPending || isPending
|
||||
? "Please Wait..."
|
||||
: requestToEdit
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManagePaymentRequest;
|
||||
202
src/components/PaymentRequest/PaymentRequestFilterPanel.jsx
Normal file
@ -0,0 +1,202 @@
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "./PaymentRequestSchema";
|
||||
|
||||
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { useExpenseStatus } from "../../hooks/masterHook/useMaster";
|
||||
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
||||
import { useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
import { usePaymentRequestFilter } from "../../hooks/useExpense";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
const { status } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const selectedProjectId = useSelector(
|
||||
(store) => store.localVariables.projectId
|
||||
);
|
||||
const { data, isLoading, isError, error, isFetching, isFetched } =
|
||||
usePaymentRequestFilter();
|
||||
|
||||
const groupByList = useMemo(() => {
|
||||
return [
|
||||
{ id: "projects", name: "Project" },
|
||||
{ id: "status", name: "Status" },
|
||||
{ id: "createdBy", name: "Submitted By" },
|
||||
{ id: "currency", name: "Currency" },
|
||||
{ id: "expensesCategory", name: "Expense Category" },
|
||||
{ id: "payees", name: "Payee" },
|
||||
{ id: "date", name: "Due Date" },
|
||||
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, []);
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(SearchPaymentRequestSchema),
|
||||
defaultValues: defaultPaymentRequestFilter,
|
||||
});
|
||||
|
||||
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||
const isTransactionDate = watch("isTransactionDate");
|
||||
|
||||
const closePanel = () => {
|
||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleGroupChange = (e) => {
|
||||
const group = groupByList.find((g) => g.id === e.target.value);
|
||||
if (group) setSelectedGroup(group);
|
||||
};
|
||||
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
onApply({
|
||||
...formData,
|
||||
startDate: moment.utc(formData.startDate, "DD-MM-YYYY").toISOString(),
|
||||
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
|
||||
});
|
||||
handleGroupBy(selectedGroup.id);
|
||||
// closePanel();
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
reset(defaultPaymentRequestFilter);
|
||||
setResetKey((prev) => prev + 1);
|
||||
onApply(defaultPaymentRequestFilter);
|
||||
if (status) {
|
||||
navigate("/expenses", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
closePanel();
|
||||
}, [location]);
|
||||
|
||||
const [appliedStatusId, setAppliedStatusId] = useState(null);
|
||||
|
||||
if (isError && isFetched)
|
||||
return <div>Something went wrong Here- {error.message} </div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||
<div className="mb-3 w-100">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<label className="form-label me-2">Filter By:</label>
|
||||
</div>
|
||||
<DateRangePicker1
|
||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||
startField="startDate"
|
||||
endField="endDate"
|
||||
className="w-100"
|
||||
resetSignal={resetKey}
|
||||
defaultRange={false}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row g-2">
|
||||
<SelectMultiple
|
||||
name="projectIds"
|
||||
label="Projects :"
|
||||
options={data?.projects}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="createdByIds"
|
||||
label="Submitted By :"
|
||||
options={data?.createdBy}
|
||||
labelKey={(item) => item.name}
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="payees"
|
||||
label="Payee :"
|
||||
options={data?.payees}
|
||||
labelKey={(item) => item.name}
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="expenseCategoryIds"
|
||||
label="Category :"
|
||||
options={data?.expensesCategory}
|
||||
labelKey={(item) => item.name}
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="currencyIds"
|
||||
label="Currency :"
|
||||
options={data?.currency}
|
||||
labelKey={(item) => item.name}
|
||||
valueKey="id"
|
||||
/>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Status :</label>
|
||||
<div className="row flex-wrap">
|
||||
{data?.status
|
||||
?.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((status) => (
|
||||
<div className="col-6" key={status.id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="statusIds"
|
||||
render={({ field: { value = [], onChange } }) => (
|
||||
<div className="d-flex align-items-center me-3 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
value={status.id}
|
||||
checked={value.includes(status.id)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
onChange(
|
||||
checked
|
||||
? [...value, status.id]
|
||||
: value.filter((v) => v !== status.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label className="ms-2 mb-0">{status.name}</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end py-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-label-secondary btn-sm"
|
||||
onClick={onClear}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-sm">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentRequestFilterPanel;
|
||||
390
src/components/PaymentRequest/PaymentRequestList.jsx
Normal file
@ -0,0 +1,390 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
EXPENSE_DRAFT,
|
||||
EXPENSE_REJECTEDBY,
|
||||
ITEMS_PER_PAGE,
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
formatCurrency,
|
||||
getColorNameFromHex,
|
||||
useDebounce,
|
||||
} from "../../utils/appUtils";
|
||||
import { usePaymentRequestList } from "../../hooks/useExpense";
|
||||
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Avatar from "../../components/common/Avatar";
|
||||
import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage";
|
||||
import { ExpenseTableSkeleton } from "../Expenses/ExpenseSkeleton";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import Error from "../common/Error";
|
||||
|
||||
const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
|
||||
const { setManageRequest, setVieRequest } = usePaymentRequestContext();
|
||||
const navigate = useNavigate();
|
||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
const SelfId = useSelector(
|
||||
(store) => store?.globalVariables?.loginUser?.employeeInfo?.id
|
||||
);
|
||||
const groupByField = (items, field) => {
|
||||
return items.reduce((acc, item) => {
|
||||
let key;
|
||||
let displayField;
|
||||
|
||||
switch (field) {
|
||||
case "transactionDate":
|
||||
key = item?.transactionDate?.split("T")[0];
|
||||
displayField = "Transaction Date";
|
||||
break;
|
||||
case "status":
|
||||
key = item?.status?.displayName || "Unknown";
|
||||
displayField = "Status";
|
||||
break;
|
||||
case "submittedBy":
|
||||
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
||||
}`.trim();
|
||||
displayField = "Submitted By";
|
||||
break;
|
||||
case "project":
|
||||
key = item?.project?.name || "Unknown Project";
|
||||
displayField = "Project";
|
||||
break;
|
||||
case "paymentMode":
|
||||
key = item?.paymentMode?.name || "Unknown Mode";
|
||||
displayField = "Payment Mode";
|
||||
break;
|
||||
case "expensesType":
|
||||
key = item?.expensesType?.name || "Unknown Type";
|
||||
displayField = "Expense Category";
|
||||
break;
|
||||
case "createdAt":
|
||||
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
||||
displayField = "Created Date";
|
||||
break;
|
||||
default:
|
||||
key = "Others";
|
||||
displayField = "Others";
|
||||
}
|
||||
|
||||
const groupKey = `${field}_${key}`; // unique key for object property
|
||||
if (!acc[groupKey]) {
|
||||
acc[groupKey] = { key, displayField, items: [] };
|
||||
}
|
||||
|
||||
acc[groupKey].items.push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const paymentRequestColumns = [
|
||||
{
|
||||
key: "paymentRequestUID",
|
||||
label: "Request ID",
|
||||
align: "text-start mx-2",
|
||||
getValue: (e) => e.paymentRequestUID || "N/A",
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
label: "Request Title",
|
||||
align: "text-start",
|
||||
getValue: (e) => e.title || "N/A",
|
||||
},
|
||||
// { key: "payee", label: "Payee", align: "text-start" },
|
||||
{
|
||||
key: "SubmittedBy",
|
||||
label: "Submitted By",
|
||||
align: "text-start",
|
||||
getValue: (e) =>
|
||||
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A",
|
||||
customRender: (e) => (
|
||||
<div
|
||||
className="d-flex align-items-center cursor-pointer"
|
||||
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}
|
||||
>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={e.createdBy?.firstName}
|
||||
lastName={e.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-truncate">
|
||||
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "Submitted On",
|
||||
align: "text-start",
|
||||
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
||||
},
|
||||
{
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
align: "text-end",
|
||||
getValue: (e) =>
|
||||
e?.amount
|
||||
? `${e?.currency?.symbol ? e.currency.symbol + " " : ""}${e.amount.toLocaleString()}`
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
key: "expenseStatus",
|
||||
label: "Status",
|
||||
align: "text-center",
|
||||
getValue: (e) => (
|
||||
<span
|
||||
className={`badge bg-label-${getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
|
||||
}`}
|
||||
>
|
||||
{e?.expenseStatus?.name || "Unknown"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
const { data, isLoading, isError, error, isRefetching, refetch } =
|
||||
usePaymentRequestList(
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
filters,
|
||||
true,
|
||||
debouncedSearch
|
||||
);
|
||||
|
||||
const paymentRequestData = data?.data || [];
|
||||
const totalPages = data?.data?.totalPages || 1;
|
||||
|
||||
if (isError) {
|
||||
return <Error error={error} isFeteching={isRefetching} refetch={refetch} />;
|
||||
}
|
||||
const header = [
|
||||
"Request ID",
|
||||
"Request Title",
|
||||
"Submitted By",
|
||||
"Submitted On",
|
||||
"Amount",
|
||||
"Status",
|
||||
"Action",
|
||||
];
|
||||
if (isLoading) return <ExpenseTableSkeleton headers={header} />;
|
||||
|
||||
const grouped = groupBy
|
||||
? Object.fromEntries(
|
||||
Object.entries(groupByField(data?.data ?? [], groupBy)).sort(([keyA], [keyB]) =>
|
||||
keyA.localeCompare(keyB)
|
||||
)
|
||||
)
|
||||
: { All: data?.data ?? [] };
|
||||
|
||||
|
||||
const IsGroupedByDate = [
|
||||
{ key: "transactionDate", displayField: "Transaction Date" },
|
||||
{ key: "createdAt", displayField: "created Date" },
|
||||
]?.includes(groupBy);
|
||||
|
||||
|
||||
const canEditExpense = (paymentRequest) => {
|
||||
return (
|
||||
(paymentRequest?.expenseStatus?.id === EXPENSE_DRAFT ||
|
||||
EXPENSE_REJECTEDBY.includes(paymentRequest?.expenseStatus.id)) &&
|
||||
paymentRequest?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
const canDetetExpense = (request) => {
|
||||
return (
|
||||
request?.expenseStatus?.id === EXPENSE_DRAFT &&
|
||||
request?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setDeletingId(id);
|
||||
DeleteExpense(
|
||||
{ id },
|
||||
{
|
||||
onSettled: () => {
|
||||
setDeletingId(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Expense"
|
||||
message="Under the woring?"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
// loading={isPending}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
)}
|
||||
<div className="card page-min-h table-responsive px-sm-4">
|
||||
<div className="card-datatable" id="payment-request-table">
|
||||
<table className="table border-top dataTable text-nowrap align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
{paymentRequestColumns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{Object.keys(grouped).length > 0 ? (
|
||||
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||
<React.Fragment key={key}>
|
||||
<tr className="tr-group text-dark">
|
||||
<td colSpan={8} className="text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
{" "}
|
||||
<small className="fs-6 py-1 ms-1">
|
||||
{displayField} :{" "}
|
||||
</small>{" "}
|
||||
<small className="fs-6 ms-3">
|
||||
{IsGroupedByDate ? formatUTCToLocalTime(key) : key}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{items?.map((paymentRequest) => (
|
||||
<tr key={paymentRequest.id}>
|
||||
{paymentRequestColumns.map(
|
||||
(col) =>
|
||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`d-table-cell ${col.align ?? ""}`}
|
||||
>
|
||||
{col?.customRender
|
||||
? col?.customRender(paymentRequest)
|
||||
: col?.getValue(paymentRequest)}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
<td className="sticky-action-column bg-white">
|
||||
<div className="d-flex flex-row gap-2">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
setVieRequest({
|
||||
requestId: paymentRequest.id,
|
||||
view: true,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
{canEditExpense(paymentRequest) && (
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded text-muted p-0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||
<li
|
||||
onClick={() =>
|
||||
setManageRequest({
|
||||
IsOpen: true,
|
||||
RequestId: paymentRequest.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||
<span className="align-left ">
|
||||
Modify
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{canDetetExpense(paymentRequest) && (
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(paymentRequest.id);
|
||||
}}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||
<span className="align-left">
|
||||
Delete
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center border-0 ">
|
||||
<div className="py-8">
|
||||
<p>No Request Found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex justify-content-end py-3 pe-3">
|
||||
<nav>
|
||||
<ul className="pagination mb-0">
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentRequestList;
|
||||
180
src/components/PaymentRequest/PaymentRequestSchema.js
Normal file
@ -0,0 +1,180 @@
|
||||
import { boolean, z } from "zod";
|
||||
import { INR_CURRENCY_CODE } from "../../utils/constants";
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ALLOWED_TYPES = [
|
||||
"application/pdf",
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
];
|
||||
export const PaymentRequestSchema = (expenseTypes, isItself) => {
|
||||
return z.object({
|
||||
title: z.string().min(1, { message: "Project is required" }),
|
||||
projectId: z.string().min(1, { message: "Project is required" }),
|
||||
expenseCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Expense Category is required" }),
|
||||
currencyId: z.string().min(1, { message: "Currency is required" }),
|
||||
dueDate: z.string().min(1, { message: "Date is required" }),
|
||||
description: z.string().min(1, { message: "Description is required" }),
|
||||
payee: z.string().min(1, { message: "Supplier name is required" }),
|
||||
isAdvancePayment: z.boolean().optional().default(false),
|
||||
amount: z.coerce
|
||||
.number({
|
||||
invalid_type_error: "Amount is required and must be a number",
|
||||
})
|
||||
.min(1, "Amount must be Enter")
|
||||
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
|
||||
message: "Amount must have at most 2 decimal places",
|
||||
}),
|
||||
|
||||
billAttachments: z.array(
|
||||
z.object({
|
||||
fileName: z.string().min(1, { message: "Filename is required" }),
|
||||
base64Data: z.string().nullable(),
|
||||
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
|
||||
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
||||
}),
|
||||
documentId: z.string().optional(),
|
||||
fileSize: z.number().max(MAX_FILE_SIZE, {
|
||||
message: "File size must be less than or equal to 5MB",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
})
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultPaymentRequest = {
|
||||
title: "",
|
||||
description: "",
|
||||
payee: "",
|
||||
currencyId: "",
|
||||
amount: "",
|
||||
dueDate: "",
|
||||
projectId: "",
|
||||
expenseCategoryId: "",
|
||||
isAdvancePayment: false,
|
||||
billAttachments: [],
|
||||
};
|
||||
|
||||
export const SearchPaymentRequestSchema = z.object({
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
statusIds: z.array(z.string()).optional(),
|
||||
createdByIds: z.array(z.string()).optional(),
|
||||
currencyIds: z.array(z.string()).optional(),
|
||||
expenseCategoryIds: z.array(z.string()).optional(),
|
||||
payees: z.array(z.string()).optional(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
});
|
||||
|
||||
export const defaultPaymentRequestFilter = {
|
||||
projectIds: [],
|
||||
statusIds: [],
|
||||
createdByIds: [],
|
||||
currencyIds: [],
|
||||
expenseCategoryIds: [],
|
||||
payees: [],
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
};
|
||||
|
||||
export const PaymentRequestActionScheam = (
|
||||
isTransaction = false,
|
||||
transactionDate
|
||||
) => {
|
||||
return z
|
||||
.object({
|
||||
comment: z.string().min(1, { message: "Please leave comment" }),
|
||||
statusId: z.string().min(1, { message: "Please select a status" }),
|
||||
paidTransactionId: z.string().nullable().optional(),
|
||||
paidAt: z.string().nullable().optional(),
|
||||
paidById: z.string().nullable().optional(),
|
||||
tdsPercentage: z.string().nullable().optional(),
|
||||
baseAmount: z.string().nullable().optional(),
|
||||
taxAmount: z.string().nullable().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (isTransaction) {
|
||||
if (!data.paidTransactionId?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["paidTransactionId"],
|
||||
message: "Transaction ID is required",
|
||||
});
|
||||
}
|
||||
if (!data.paidAt) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["paidAt"],
|
||||
message: "Transacion Date is required",
|
||||
});
|
||||
}
|
||||
if (!data.paidById) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["paidById"],
|
||||
message: "Paid By is required",
|
||||
});
|
||||
}
|
||||
if (!data.baseAmount) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["baseAmount"],
|
||||
message: "Base Amount i required",
|
||||
});
|
||||
}
|
||||
if (!data.taxAmount) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["taxAmount"],
|
||||
message: "Tax is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultPRActionValues = {
|
||||
comment: "",
|
||||
statusId: "",
|
||||
paidTransactionId: null,
|
||||
paidAt: null,
|
||||
paidById: null,
|
||||
tdsPercentage: "0",
|
||||
baseAmount: null,
|
||||
taxAmount: null,
|
||||
};
|
||||
|
||||
export const RequestedExpenseSchema = z.object({
|
||||
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
||||
location: z.string().min(1, { message: "Location is required" }),
|
||||
gstNumber: z.string().optional(),
|
||||
billAttachments: z
|
||||
.array(
|
||||
z.object({
|
||||
fileName: z.string().min(1, { message: "Filename is required" }),
|
||||
base64Data: z.string().nullable(),
|
||||
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
|
||||
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
||||
}),
|
||||
documentId: z.string().optional(),
|
||||
fileSize: z.number().max(MAX_FILE_SIZE, {
|
||||
message: "File size must be less than or equal to 5MB",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
})
|
||||
)
|
||||
.nonempty({ message: "At least one file attachment is required" }),
|
||||
});
|
||||
|
||||
export const DefaultRequestedExpense = {
|
||||
paymentModeId: "",
|
||||
location: "",
|
||||
gstNumber: "",
|
||||
// amount:"",
|
||||
billAttachments: [],
|
||||
};
|
||||
89
src/components/PaymentRequest/PaymentStatusLogs.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Timeline from "../common/TimeLine";
|
||||
import moment from "moment";
|
||||
import { getColorNameFromHex } from "../../utils/appUtils";
|
||||
const PaymentStatusLogs = ({ data }) => {
|
||||
|
||||
const sortedLogs = useMemo(() => {
|
||||
if (!data?.updateLogs) return [];
|
||||
return [...data.updateLogs].sort(
|
||||
(a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||
);
|
||||
}, [data?.updateLogs]);
|
||||
|
||||
|
||||
|
||||
const timelineData = useMemo(() => {
|
||||
return sortedLogs.map((log, index) => ({
|
||||
id: log.id,
|
||||
title: log.nextStatus?.name || "Status Updated",
|
||||
description: log.nextStatus?.description || "",
|
||||
timeAgo: log.updatedAt,
|
||||
color: getColorNameFromHex(log.nextStatus?.color) || "primary",
|
||||
userComment:log.comment,
|
||||
users: log.updatedBy
|
||||
? [
|
||||
{
|
||||
firstName: log.updatedBy.firstName || "",
|
||||
lastName: log?.updatedBy?.lastName || "",
|
||||
role: log.updatedBy.jobRoleName || "",
|
||||
avatar: log.updatedBy.photo,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}));
|
||||
}, [sortedLogs]);
|
||||
|
||||
const handleShowMore = () => {
|
||||
setVisibleCount((prev) => prev + 4);
|
||||
};
|
||||
return (
|
||||
<div className="page-min-h overflow-auto h-56" >
|
||||
{/* <div className="row g-2">
|
||||
{logsToShow.map((log) => (
|
||||
<div key={log.id} className="col-12 d-flex align-items-start mb-1">
|
||||
<Avatar
|
||||
size="xs"
|
||||
firstName={log.updatedBy.firstName}
|
||||
lastName={log.updatedBy.lastName}
|
||||
/>
|
||||
|
||||
<div className="flex-grow-1">
|
||||
<div className="text-start">
|
||||
<div className="flex">
|
||||
<span>{`${log.updatedBy.firstName} ${log.updatedBy.lastName}`}</span>
|
||||
<small className="text-secondary text-tiny ms-2">
|
||||
<em>{log.action}</em>
|
||||
</small>
|
||||
<span className="text-tiny text-secondary d-block">
|
||||
{formatUTCToLocalTime(log.updateAt, true)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center text-muted small mt-1">
|
||||
<span>{log.comment}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{sortedLogs.length > visibleCount && (
|
||||
<div className="text-center my-1">
|
||||
<button
|
||||
className="btn btn-xs btn-outline-primary"
|
||||
onClick={handleShowMore}
|
||||
>
|
||||
Show More
|
||||
</button>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<Timeline items={timelineData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentStatusLogs;
|
||||
549
src/components/PaymentRequest/ViewPaymentRequest.jsx
Normal file
@ -0,0 +1,549 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
useActionOnExpense,
|
||||
useActionOnPaymentRequest,
|
||||
usePaymentRequestDetail,
|
||||
} from "../../hooks/useExpense";
|
||||
import {
|
||||
formatCurrency,
|
||||
formatFigure,
|
||||
getColorNameFromHex,
|
||||
getIconByFileType,
|
||||
localToUtc,
|
||||
} from "../../utils/appUtils";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Avatar from "../common/Avatar";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||
import Error from "../common/Error";
|
||||
import {
|
||||
defaultActionValues,
|
||||
ExpenseActionScheam,
|
||||
} from "../Expenses/ExpenseSchema";
|
||||
import { ExpenseDetailsSkeleton } from "../Expenses/ExpenseSkeleton";
|
||||
import ExpenseStatusLogs from "../Expenses/ExpenseStatusLogs";
|
||||
import { useSelector } from "react-redux";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import {
|
||||
EXPENSE_REJECTEDBY,
|
||||
PROCESS_EXPENSE,
|
||||
REVIEW_EXPENSE,
|
||||
} from "../../utils/constants";
|
||||
import Label from "../common/Label";
|
||||
import { FilelistView } from "../Expenses/Filelist";
|
||||
import PaymentStatusLogs from "./PaymentStatusLogs";
|
||||
import { defaultPRActionValues, PaymentRequestActionScheam } from "./PaymentRequestSchema";
|
||||
|
||||
const ViewPaymentRequest = ({ requestId }) => {
|
||||
const { data, isLoading, isError, error, isFetching } =
|
||||
usePaymentRequestDetail(requestId);
|
||||
const [IsPaymentProcess, setIsPaymentProcess] = useState(false);
|
||||
const [clickedStatusId, setClickedStatusId] = useState(null);
|
||||
|
||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||
const [imageLoaded, setImageLoaded] = useState({});
|
||||
const { setDocumentView, setModalSize, setVieRequest, setIsExpenseGenerate } =
|
||||
usePaymentRequestContext();
|
||||
const ActionSchema =
|
||||
PaymentRequestActionScheam(IsPaymentProcess, data?.createdAt) ?? z.object({});
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(ActionSchema),
|
||||
defaultValues: defaultPRActionValues,
|
||||
});
|
||||
|
||||
const userPermissions = useSelector(
|
||||
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
|
||||
);
|
||||
const CurrentUser = useSelector(
|
||||
(state) => state?.globalVariables?.loginUser?.employeeInfo
|
||||
);
|
||||
|
||||
const nextStatusWithPermission = useMemo(() => {
|
||||
if (!Array.isArray(data?.nextStatus)) return [];
|
||||
|
||||
return data.nextStatus.filter((status) => {
|
||||
const permissionIds = Array.isArray(status?.permissionIds)
|
||||
? status.permissionIds
|
||||
: [];
|
||||
|
||||
if (permissionIds.length === 0) return true;
|
||||
if (permissionIds.includes(PROCESS_EXPENSE)) {
|
||||
setIsPaymentProcess(true);
|
||||
}
|
||||
return permissionIds.some((id) => userPermissions.includes(id));
|
||||
});
|
||||
}, [data, userPermissions]);
|
||||
|
||||
const isRejectedRequest = useMemo(() => {
|
||||
return EXPENSE_REJECTEDBY.includes(data?.status?.id);
|
||||
}, [data]);
|
||||
|
||||
const isCreatedBy = useMemo(() => {
|
||||
return data?.createdBy?.id === CurrentUser?.id;
|
||||
}, [data, CurrentUser]);
|
||||
|
||||
const { mutate: MakeAction, isPending } = useActionOnPaymentRequest(() => {
|
||||
setClickedStatusId(null);
|
||||
reset();
|
||||
});
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const Payload = {
|
||||
...formData,
|
||||
paidAt: localToUtc(formData.paidAt),
|
||||
paymentRequestId: data.id,
|
||||
comment: formData.comment,
|
||||
};
|
||||
MakeAction(Payload);
|
||||
};
|
||||
|
||||
if (isLoading) return <ExpenseDetailsSkeleton />;
|
||||
if (isError) return <Error error={error} />;
|
||||
const handleImageLoad = (id) => {
|
||||
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
||||
};
|
||||
const handleExpense = () => {
|
||||
setIsExpenseGenerate({ IsOpen: true, requestId: requestId });
|
||||
setVieRequest({ IsOpen: true, requestId: requestId });
|
||||
};
|
||||
return (
|
||||
<form
|
||||
className="container px-3 py-2 py-md-0"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="col-12 mb-2 text-center ">
|
||||
<h5 className="fw-semibold m-0">Payment Request Details</h5>
|
||||
<hr />
|
||||
</div>
|
||||
<div className="row mb-1">
|
||||
<div className="col-12 col-sm-6 col-md-7">
|
||||
<div className="row">
|
||||
<div className="col-12 text-start fw-semibold mb-2">
|
||||
{data?.paymentRequestUID}
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-block d-md-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Project Name:
|
||||
</label>
|
||||
<div className="text-muted">{data?.project?.name || "—"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Due Date :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.dueDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Expense Category :
|
||||
</label>
|
||||
<div className="text-muted">{data?.expenseCategory?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Supplier :
|
||||
</label>
|
||||
<div className="text-muted">{data?.payee}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Amount :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatFigure(data?.amount, {
|
||||
type: "currency",
|
||||
currency: data?.currency?.currencyCode,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
{/* <div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Payment Mode :
|
||||
</label>
|
||||
<div className="text-muted">{data?.paymentMode?.name}</div>
|
||||
</div>
|
||||
</div> */}
|
||||
{data?.gstNumber && (
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
GST Number :
|
||||
</label>
|
||||
<div className="text-muted">{data?.gstNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 4 */}
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Status :
|
||||
</label>
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
getColorNameFromHex(data?.expenseStatus?.color) ||
|
||||
"secondary"
|
||||
}`}
|
||||
>
|
||||
{data?.expenseStatus?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Pre-Approved :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{data?.preApproved ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted">{data?.project?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created At :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 6 */}
|
||||
{data?.createdBy && (
|
||||
<div className="col-md-6 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Created By :
|
||||
</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data?.createdBy?.firstName}
|
||||
lastName={data?.createdBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data?.createdBy?.firstName ?? ""} ${
|
||||
data?.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.paidBy && (
|
||||
<div className="col-md-6 text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
<label
|
||||
className="form-label me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Paid By :
|
||||
</label>
|
||||
<div className="d-flex align-items-center ">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={data?.paidBy?.firstName}
|
||||
lastName={data?.paidBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data?.paidBy?.firstName ?? ""} ${
|
||||
data?.paidBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-start my-1">
|
||||
<label className="fw-semibold form-label">Description : </label>
|
||||
<div className="text-muted">{data?.description}</div>
|
||||
</div>
|
||||
<div className="col-6 text-start">
|
||||
<label className="form-label me-2 mb-2 fw-semibold">
|
||||
Attachment :
|
||||
</label>
|
||||
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{data?.attachments?.length > 0 ? (
|
||||
<FilelistView
|
||||
files={data?.attachments}
|
||||
viewFile={setDocumentView}
|
||||
/>
|
||||
) : (
|
||||
<p className="m-0 text-secondary">No Attachment</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data?.paidTransactionId && (
|
||||
<div className="row text-start mt-2">
|
||||
<div className="col-md-6 mb-sm-0 mb-2">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Transaction ID :
|
||||
</label>
|
||||
{data?.paidTransactionId}
|
||||
</div>
|
||||
<div className="col-md-6 ">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Transaction Date :
|
||||
</label>
|
||||
{formatUTCToLocalTime(data?.paidAt)}
|
||||
</div>
|
||||
|
||||
{data?.paidBy && (
|
||||
<>
|
||||
<div className="col-md-6 d-flex align-items-center">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Paid By :
|
||||
</label>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0 me-1"
|
||||
firstName={data?.paidBy?.firstName}
|
||||
lastName={data?.paidBy?.lastName}
|
||||
/>
|
||||
<span className="text-muted">
|
||||
{`${data?.paidBy?.firstName} ${data?.paidBy?.lastName}`.trim()}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(data?.nextStatus) && data?.nextStatus.length > 0 ? (
|
||||
<>
|
||||
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Transaction Id </label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("paidTransactionId")}
|
||||
/>
|
||||
{errors.paidTransactionId && (
|
||||
<small className="danger-text">
|
||||
{errors.paidTransactionId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start mb-1">
|
||||
<label className="form-label">Transaction Date </label>
|
||||
<DatePicker className="w-100"
|
||||
name="paidAt"
|
||||
control={control}
|
||||
minDate={data?.createdAt}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
{errors.paidAt && (
|
||||
<small className="danger-text">
|
||||
{errors.paidAt.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<label className="form-label">Paid By </label>
|
||||
<EmployeeSearchInput
|
||||
control={control}
|
||||
name="paidById"
|
||||
projectId={null}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start mb-1">
|
||||
<Label className="form-label">TDS Percentage</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("tdsPercentage")}
|
||||
/>
|
||||
{errors.tdsPercentage && (
|
||||
<small className="danger-text">
|
||||
{errors.tdsPercentage.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start mb-1">
|
||||
<Label className="form-label" required>
|
||||
Base Amount
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("baseAmount")}
|
||||
/>
|
||||
{errors.baseAmount && (
|
||||
<small className="danger-text">
|
||||
{errors.baseAmount.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 text-start mb-1">
|
||||
<Label className="form-label" required>
|
||||
Tax Amount
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("taxAmount")}
|
||||
/>
|
||||
{errors.taxAmount && (
|
||||
<small className="danger-text">
|
||||
{errors.taxAmount.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-12 mb-3 text-start">
|
||||
{((nextStatusWithPermission.length > 0 &&
|
||||
!isRejectedRequest) ||
|
||||
(isRejectedRequest && isCreatedBy)) && (
|
||||
<>
|
||||
<Label className="form-label me-2 mb-0" required>
|
||||
Comment
|
||||
</Label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
rows="2"
|
||||
/>
|
||||
{errors.comment && (
|
||||
<small className="danger-text">
|
||||
{errors.comment.message}
|
||||
</small>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{nextStatusWithPermission?.length > 0 &&
|
||||
(!isRejectedRequest || isCreatedBy) && (
|
||||
<div className="text-end flex-wrap gap-2 my-2 mt-3">
|
||||
{nextStatusWithPermission?.map((status, index) => (
|
||||
<button
|
||||
key={status.id || index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClickedStatusId(status.id);
|
||||
setValue("statusId", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
disabled={isPending || isFetching}
|
||||
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
|
||||
>
|
||||
{isPending && clickedStatusId === status.id
|
||||
? "Please Wait..."
|
||||
: status.displayName || status.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (!data?.isExpenseCreated && ! data?.isAdvancePayment) ? (
|
||||
<div className="text-end flex-wrap gap-2 my-2 mt-3">
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={handleExpense}
|
||||
>
|
||||
Make Expense
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className=" col-sm-12 my-md-0 border-top border-md-none col-md-5">
|
||||
<div className="d-flex mb-2 py-1">
|
||||
<i className="bx bx-time-five me-2 "></i>{" "}
|
||||
<p className="fw-medium">TimeLine</p>
|
||||
</div>
|
||||
<PaymentStatusLogs data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export default ViewPaymentRequest;
|
||||
@ -42,7 +42,7 @@ const AboutProject = () => {
|
||||
{IsOpenModal && (
|
||||
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
|
||||
<ManageProjectInfo
|
||||
project={projects_Details}
|
||||
project={projects_Details?.id}
|
||||
handleSubmitForm={handleFormSubmit}
|
||||
onClose={() => setIsOpenModal(false)}
|
||||
isPending={isPending}
|
||||
|
||||
@ -5,7 +5,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import Label from "../common/Label";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { useCreateProject, useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
|
||||
import {
|
||||
useCreateProject,
|
||||
useProjectDetails,
|
||||
useUpdateProject,
|
||||
} from "../../hooks/useProjects";
|
||||
|
||||
import {
|
||||
DEFAULT_EMPTY_STATUS_ID,
|
||||
@ -17,6 +21,8 @@ import {
|
||||
useOrganizationsList,
|
||||
} from "../../hooks/useOrganization";
|
||||
import { localToUtc } from "../../utils/appUtils";
|
||||
import Modal from "../common/Modal";
|
||||
import { useModal } from "../../hooks/useAuth";
|
||||
|
||||
const currentDate = new Date().toLocaleDateString("en-CA");
|
||||
const formatDate = (date) => {
|
||||
@ -32,18 +38,24 @@ const formatDate = (date) => {
|
||||
const ManageProjectInfo = ({ project, onClose }) => {
|
||||
const [addressLength, setAddressLength] = useState(0);
|
||||
const maxAddressLength = 500;
|
||||
const { onOpen, startStep, flowType } = useOrganizationModal();
|
||||
const { onOpen, startStep, flowType } = useModal("ManageProject");
|
||||
|
||||
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
||||
|
||||
const { projects_Details, loading } = useProjectDetails(project);
|
||||
const { data, isLoading, isError, error } = useOrganizationsList(
|
||||
ITEMS_PER_PAGE,
|
||||
1,
|
||||
true
|
||||
// const { data, isLoading, isError, error } = useOrganizationsList(
|
||||
// ITEMS_PER_PAGE,
|
||||
// 1,
|
||||
// true
|
||||
// );
|
||||
const { mutate: UpdateProject, isPending } = useUpdateProject(() => {
|
||||
onClose?.();
|
||||
});
|
||||
const { mutate: CeateProject, isPending: isCreating } = useCreateProject(
|
||||
() => {
|
||||
onClose?.();
|
||||
}
|
||||
);
|
||||
const { mutate: UpdateProject, isPending } = useUpdateProject(() => {onClose?.()});
|
||||
const {mutate:CeateProject,isPending:isCreating} = useCreateProject(()=>{onClose?.()})
|
||||
|
||||
const {
|
||||
register,
|
||||
@ -70,11 +82,11 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
projectStatusId:
|
||||
String(projects_Details?.projectStatus?.id) ||
|
||||
DEFAULT_EMPTY_STATUS_IDF,
|
||||
promoterId: projects_Details?.promoter?.id || "",
|
||||
pmcId: projects_Details?.pmc?.id || "",
|
||||
// promoterId: projects_Details?.promoter?.id || "", // hide temp. for version 1
|
||||
// pmcId: projects_Details?.pmc?.id || "",
|
||||
});
|
||||
setAddressLength(projects_Details?.projectAddress?.length || 0);
|
||||
}, [project, projects_Details, reset,data]);
|
||||
}, [project, projects_Details, reset]);
|
||||
|
||||
const onSubmitForm = (formData) => {
|
||||
if (project) {
|
||||
@ -85,13 +97,13 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
id: project,
|
||||
};
|
||||
UpdateProject({ projectId: project, payload: payload });
|
||||
}else{
|
||||
let payload = {
|
||||
} else {
|
||||
let payload = {
|
||||
...formData,
|
||||
startDate: localToUtc(formData.startDate),
|
||||
endDate: localToUtc(formData.endDate),
|
||||
};
|
||||
CeateProject(payload)
|
||||
CeateProject(payload);
|
||||
}
|
||||
};
|
||||
|
||||
@ -104,7 +116,6 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
onOpen({ startStep: 2, flowType: "default" });
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-sm-2 p-2">
|
||||
<div className="text-center mb-2">
|
||||
@ -254,7 +265,7 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 ">
|
||||
{/* <div className="col-12 ">
|
||||
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||
Promoter
|
||||
</label>
|
||||
@ -330,7 +341,7 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
<small className="cursor-pointer" onClick={handleOrganizaioFinder}>
|
||||
<i className="bx bx-plus-circle text-primary"></i>
|
||||
</small>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<Label htmlFor="projectAddress" required>
|
||||
@ -376,7 +387,11 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isPending || isCreating}
|
||||
>
|
||||
{isPending||isCreating ? "Please Wait..." : project ? "Update" : "Submit"}
|
||||
{isPending || isCreating
|
||||
? "Please Wait..."
|
||||
: project
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -385,3 +400,15 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
||||
};
|
||||
|
||||
export default ManageProjectInfo;
|
||||
|
||||
export const ProjectModal = () => {
|
||||
const { isOpen, data, closeModal } = useProjectModal();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
body={<ManageProjectInfo project={data} onClose={closeModal} />}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -35,6 +35,7 @@ const ProjectCard = ({ project }) => {
|
||||
|
||||
const handleViewProject = () => {
|
||||
dispatch(setProjectId(project.id));
|
||||
localStorage.setItem("lastActiveProjectTab","profile")
|
||||
navigate(`/projects/details`);
|
||||
};
|
||||
const handleViewActivities = () => {
|
||||
@ -54,7 +55,7 @@ const ProjectCard = ({ project }) => {
|
||||
style={{ fontSize: "xx-large" }}
|
||||
></i>
|
||||
</div>
|
||||
<div className="me-2">
|
||||
<div className="me-2 text-start">
|
||||
<h5
|
||||
className="mb-0 stretched-link text-heading text-start"
|
||||
onClick={handleViewProject}
|
||||
@ -66,7 +67,7 @@ const ProjectCard = ({ project }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`ms-auto ${!ManageProject && "d-none"}`}>
|
||||
<div className={`ms-auto `}>
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
@ -106,12 +107,6 @@ const ProjectCard = ({ project }) => {
|
||||
<span className="align-left">Modify</span>
|
||||
</a>
|
||||
</li>
|
||||
<li onClick={handleViewActivities}>
|
||||
<a className="dropdown-item">
|
||||
<i className="bx bx-task me-2"></i>
|
||||
<span className="align-left">Activities</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,70 +1,62 @@
|
||||
import React from 'react'
|
||||
import { useProjects } from '../../hooks/useProjects'
|
||||
import Loader from '../common/Loader'
|
||||
import ProjectCard from './ProjectCard'
|
||||
|
||||
const ProjectCardView = ({currentItems,setCurrentPage,totalPages }) => {
|
||||
|
||||
import React from "react";
|
||||
import { useProjects } from "../../hooks/useProjects";
|
||||
import Loader from "../common/Loader";
|
||||
import ProjectCard from "./ProjectCard";
|
||||
|
||||
const ProjectCardView = ({ currentItems, setCurrentPage, totalPages }) => {
|
||||
return (
|
||||
<div className="row page-min-h">
|
||||
{currentItems.length === 0 && (
|
||||
<p className="text-center text-muted">No projects found.</p>
|
||||
)}
|
||||
|
||||
<div className="row page-min-h">
|
||||
{currentItems.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
))}
|
||||
|
||||
{ currentItems.length === 0 && (
|
||||
<p className="text-center text-muted">No projects found.</p>
|
||||
)}
|
||||
|
||||
{currentItems.map((project) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
{ totalPages > 1 && (
|
||||
<nav>
|
||||
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{totalPages > 1 && (
|
||||
<nav>
|
||||
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, i) => (
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages && "disabled"
|
||||
}`}
|
||||
key={i}
|
||||
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() =>
|
||||
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||
}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
»
|
||||
{i + 1}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${
|
||||
currentPage === totalPages && "disabled"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() =>
|
||||
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||
}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectCardView
|
||||
export default ProjectCardView;
|
||||
|
||||
@ -26,9 +26,8 @@ const ProjectListView = ({
|
||||
const navigate = useNavigate();
|
||||
const { setMangeProject } = useProjectContext();
|
||||
// const { data, isLoading, isError, error } = useProjects();
|
||||
|
||||
// check Permissions
|
||||
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||
// const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||
|
||||
const projectColumns = [
|
||||
{
|
||||
@ -125,154 +124,156 @@ const ProjectListView = ({
|
||||
},
|
||||
];
|
||||
|
||||
const handleViewActivities = (project) => {
|
||||
dispatch(setProjectId(project));
|
||||
navigate(`/activities/records?project=${project}`);
|
||||
};
|
||||
// const handleViewActivities = (project) => {
|
||||
// dispatch(setProjectId(project));
|
||||
// navigate(`/activities/records?project=${project}`);
|
||||
// };
|
||||
|
||||
const handleMoveDetails = (project) => {
|
||||
dispatch(setProjectId(project));
|
||||
localStorage.setItem("lastActiveProjectTab", "profile")
|
||||
navigate("/projects/details");
|
||||
};
|
||||
return (
|
||||
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||
<table className="table table-hover align-middle m-0">
|
||||
<thead className="border-bottom">
|
||||
<tr>
|
||||
{projectColumns.map((col) => (
|
||||
<th key={col.key} colSpan={col.colSpan} className={col.className}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-center py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentItems?.map((project) => (
|
||||
<tr key={project.id}>
|
||||
<div className="table-responsive text-nowrap">
|
||||
<table className="table table-hover align-middle m-0">
|
||||
<thead className="border-bottom">
|
||||
<tr>
|
||||
{projectColumns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
colSpan={col.colSpan}
|
||||
className={`${col.className} py-5`}
|
||||
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||
>
|
||||
{col.getValue
|
||||
? col.getValue(project)
|
||||
: project[col.key] || "N/A"}
|
||||
</td>
|
||||
<th key={col.key} colSpan={col.colSpan} className={col.className}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<td
|
||||
className={`mx-2 ${
|
||||
canManageProject ? "d-sm-table-cell" : "d-none"
|
||||
}`}
|
||||
>
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
<th className="text-center py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentItems?.map((project) => (
|
||||
<tr key={project.id}>
|
||||
{projectColumns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
colSpan={col.colSpan}
|
||||
className={`${col.className} py-5`}
|
||||
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a
|
||||
aria-label="click to View details"
|
||||
className="dropdown-item cursor-pointer"
|
||||
>
|
||||
<i className="bx bx-detail me-2"></i>
|
||||
<span className="align-left">View details</span>
|
||||
</a>
|
||||
</li>
|
||||
{col.getValue
|
||||
? col.getValue(project)
|
||||
: project[col.key] || "N/A"}
|
||||
</td>
|
||||
))}
|
||||
<td className={`mx-2 ${"d-sm-table-cell"}`}>
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
<li onClick={() => handleMoveDetails(project.id)}>
|
||||
<a
|
||||
aria-label="click to View details"
|
||||
className="dropdown-item cursor-pointer"
|
||||
>
|
||||
<i className="bx bx-detail me-2"></i>
|
||||
<span className="align-left">View details</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() =>
|
||||
setMangeProject({
|
||||
isOpen: true,
|
||||
Project: project.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-pencil me-2"></i>
|
||||
<span className="align-left">Modify</span>
|
||||
</a>
|
||||
</li>
|
||||
<li onClick={() => handleViewActivities(project.id)}>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() =>
|
||||
setMangeProject({
|
||||
isOpen: true,
|
||||
Project: project.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-pencil me-2"></i>
|
||||
<span className="align-left">Modify</span>
|
||||
</a>
|
||||
</li>
|
||||
{/* <li onClick={() => handleViewActivities(project.id)}>
|
||||
<a className="dropdown-item cursor-pointer">
|
||||
<i className="bx bx-task me-2"></i>
|
||||
<span className="align-left">Activities</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{isLoading && (
|
||||
<div className="py-4">
|
||||
{" "}
|
||||
{isLoading && <p className="text-center">Loading...</p>}
|
||||
{!isLoading && filteredProjects.length === 0 && (
|
||||
{isLoading && (
|
||||
<div className="py-4">
|
||||
{" "}
|
||||
{isLoading && <p className="text-center">Loading...</p>}
|
||||
{!isLoading && filteredProjects.length === 0 && (
|
||||
<p className="text-center text-muted">No projects found.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && currentItems.length === 0 && (
|
||||
<div className="py-6">
|
||||
<p className="text-center text-muted">No projects found.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && currentItems.length === 0 && (
|
||||
<div className="py-6">
|
||||
<p className="text-center text-muted">No projects found.</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && totalPages > 1 && (
|
||||
<nav>
|
||||
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, i) => (
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && totalPages > 1 && (
|
||||
<nav>
|
||||
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
key={i}
|
||||
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||
className={`page-item ${currentPage === totalPages && "disabled"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
onClick={() =>
|
||||
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||
}
|
||||
>
|
||||
{i + 1}
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${
|
||||
currentPage === totalPages && "disabled"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() =>
|
||||
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||
}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -27,13 +27,13 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
|
||||
const ProjectTab = [
|
||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||
{ key: "teams", icon: "bx bx-group", label: "Teams" },
|
||||
{
|
||||
key: "infra",
|
||||
icon: "bx bx-grid-alt",
|
||||
label: "Infrastructure",
|
||||
hidden: !(HasViewInfraStructure || HasManageInfra || HasManageTask),
|
||||
},
|
||||
// { key: "teams", icon: "bx bx-group", label: "Teams" },
|
||||
// {
|
||||
// key: "infra",
|
||||
// icon: "bx bx-grid-alt",
|
||||
// label: "Infrastructure",
|
||||
// hidden: !(HasViewInfraStructure || HasManageInfra || HasManageTask),
|
||||
// },
|
||||
{
|
||||
key: "directory",
|
||||
icon: "bx bxs-contact",
|
||||
@ -41,8 +41,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
hidden: !(DirAdmin || DireManager || DirUser),
|
||||
},
|
||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
||||
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
|
||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
||||
// { key: "organization", icon: "bx bx-buildings", label: "Organization"},
|
||||
// { key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
||||
];
|
||||
return (
|
||||
<div className="nav-align-top">
|
||||
|
||||
@ -11,8 +11,8 @@ export const projectDefault = {
|
||||
startDate: currentDate.toISOString().split("T")[0],
|
||||
endDate: currentDate.toISOString().split("T")[0],
|
||||
projectStatusId: DEFAULT_EMPTY_STATUS_ID,
|
||||
promoterId: "",
|
||||
pmcId: "",
|
||||
// promoterId: "",
|
||||
// pmcId: "",
|
||||
};
|
||||
|
||||
|
||||
@ -39,8 +39,8 @@ export const projectSchema = z
|
||||
.min(1, { message: "End Date is required" })
|
||||
.default(projectDefault),
|
||||
projectStatusId: z.string().min(1, { message: "Status is required" }),
|
||||
promoterId: z.string().min(1, { message: "Promoter is required" }),
|
||||
pmcId: z.string().min(1, { message: "PMC is required" }),
|
||||
// promoterId: z.string().min(1, { message: "Promoter is required" }),
|
||||
// pmcId: z.string().min(1, { message: "PMC is required" }),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
|
||||
464
src/components/RecurringExpense/ManageRecurringExpense.jsx
Normal file
@ -0,0 +1,464 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Label from '../common/Label';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useExpenseCategory, useRecurringStatus } from '../../hooks/masterHook/useMaster';
|
||||
import DatePicker from '../common/DatePicker';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { defaultRecurringExpense, PaymentRecurringExpense } from './RecurringExpenseSchema';
|
||||
import { FREQUENCY_FOR_RECURRING, INR_CURRENCY_CODE } from '../../utils/constants';
|
||||
import { useCurrencies, useProjectName } from '../../hooks/useProjects';
|
||||
import { useCreateRecurringExpense, usePayee, useRecurringExpenseDetail, useUpdateRecurringExpense } from '../../hooks/useExpense';
|
||||
import InputSuggestions from '../common/InputSuggestion';
|
||||
import MultiEmployeeSearchInput from '../common/MultiEmployeeSearchInput';
|
||||
|
||||
function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error: requestError,
|
||||
} = useRecurringExpenseDetail(requestToEdit);
|
||||
//APIs
|
||||
const { projectNames, loading: projectLoading, error, isError: isProjectError, } = useProjectName();
|
||||
const { data: currencyData, isLoading: currencyLoading, isError: currencyError } = useCurrencies();
|
||||
const { data: statusData, isLoading: statusLoading, isError: statusError } = useRecurringStatus();
|
||||
const { data: Payees, isLoading: isPayeeLoaing, isError: isPayeeError, error: payeeError } = usePayee()
|
||||
const { ExpenseCategories, loading: ExpenseLoading, error: ExpenseError } = useExpenseCategory();
|
||||
|
||||
const schema = PaymentRecurringExpense();
|
||||
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaultRecurringExpense,
|
||||
});
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const { mutate: CreateRecurringExpense, isPending: createPending } = useCreateRecurringExpense(
|
||||
() => {
|
||||
handleClose();
|
||||
}
|
||||
);
|
||||
const { mutate: RecurringExpenseUpdate, isPending } = useUpdateRecurringExpense(() =>
|
||||
handleClose()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestToEdit && data) {
|
||||
reset({
|
||||
title: data.title || "",
|
||||
description: data.description || "",
|
||||
payee: data.payee || "",
|
||||
notifyTo: data.notifyTo || "",
|
||||
currencyId: data.currency.id || "",
|
||||
amount: data.amount || "",
|
||||
strikeDate: data.strikeDate?.slice(0, 10) || "",
|
||||
projectId: data.project.id || "",
|
||||
paymentBufferDays: data.paymentBufferDays || "",
|
||||
numberOfIteration: data.numberOfIteration || "",
|
||||
expenseCategoryId: data.expenseCategory.id || "",
|
||||
statusId: data.status?.id || "",
|
||||
frequency: data.frequency || "",
|
||||
isVariable: data.isVariable || false,
|
||||
|
||||
});
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestToEdit && currencyData && currencyData.length > 0) {
|
||||
const inrCurrency = currencyData.find(
|
||||
(c) => c.id === INR_CURRENCY_CODE
|
||||
);
|
||||
if (inrCurrency) {
|
||||
setValue("currencyId", INR_CURRENCY_CODE, { shouldValidate: true });
|
||||
}
|
||||
}
|
||||
}, [currencyData, requestToEdit, setValue]);
|
||||
|
||||
const onSubmit = (fromdata) => {
|
||||
let payload = {
|
||||
...fromdata,
|
||||
strikeDate: fromdata.strikeDate ? new Date(fromdata.strikeDate).toISOString() : null,
|
||||
};
|
||||
if (requestToEdit) {
|
||||
const editPayload = { ...payload, id: data.id };
|
||||
RecurringExpenseUpdate({ id: data.id, payload: editPayload });
|
||||
} else {
|
||||
CreateRecurringExpense(payload);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container p-3">
|
||||
<h5 className="m-0">
|
||||
{requestToEdit ? "Update Expense Recurring " : "Create Expense Recurring"}
|
||||
</h5>
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Project and Category */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label className="form-label" required>
|
||||
Select Project
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
>
|
||||
<option value="">Select Project</option>
|
||||
{projectLoading ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
projectNames?.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.projectId && (
|
||||
<small className="danger-text">{errors.projectId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="expenseCategoryId" className="form-label" required>
|
||||
Expense Category
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expenseCategoryId"
|
||||
{...register("expenseCategoryId")}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Category
|
||||
</option>
|
||||
{ExpenseLoading ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
ExpenseCategories?.map((expense) => (
|
||||
<option key={expense.id} value={expense.id}>
|
||||
{expense.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.expenseCategoryId && (
|
||||
<small className="danger-text">
|
||||
{errors.expenseCategoryId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and Is Variable */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="title" className="form-label" required>
|
||||
Title
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
className="form-control form-control-sm"
|
||||
{...register("title")}
|
||||
placeholder="Enter title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<small className="danger-text">
|
||||
{errors.title.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <div className="col-md-6">
|
||||
<Label htmlFor="isVariable" className="form-label" required>
|
||||
Is Variable
|
||||
</Label>
|
||||
<select
|
||||
id="isVariable"
|
||||
className="form-select form-select-sm"
|
||||
{...register("isVariable", {
|
||||
setValueAs: (v) => v === "true" ? true : v === "false" ? false : false,
|
||||
})}
|
||||
>
|
||||
<option value="false">False</option>
|
||||
<option value="true">True</option>
|
||||
</select>
|
||||
{errors.isVariable && (
|
||||
<small className="danger-text">{errors.isVariable.message}</small>
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
<div className="col-md-6 mt-2">
|
||||
<Label htmlFor="isVariable" className="form-label" required>
|
||||
Payment Type
|
||||
</Label>
|
||||
|
||||
<Controller
|
||||
name="isVariable"
|
||||
control={control}
|
||||
defaultValue={defaultRecurringExpense.isVariable ?? false}
|
||||
render={({ field }) => (
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
id="isVariableTrue"
|
||||
className="form-check-input"
|
||||
checked={field.value === true}
|
||||
onChange={() => field.onChange(true)}
|
||||
/>
|
||||
<Label htmlFor="isVariableTrue" className="form-check-label">
|
||||
Is Variable
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
id="isVariableFalse"
|
||||
className="form-check-input"
|
||||
checked={field.value === false}
|
||||
onChange={() => field.onChange(false)}
|
||||
/>
|
||||
<Label htmlFor="isVariableFalse" className="form-check-label">
|
||||
Fixed
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.isVariable && (
|
||||
<small className="danger-text">{errors.isVariable.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date and Amount */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="strikeDate" className="form-label" required>
|
||||
Strike Date
|
||||
</Label>
|
||||
<DatePicker
|
||||
name="strikeDate"
|
||||
control={control}
|
||||
minDate={new Date()}
|
||||
className='w-100'
|
||||
/>
|
||||
{errors.strikeDate && (
|
||||
<small className="danger-text">
|
||||
{errors.strikeDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
placeholder="Enter amount"
|
||||
/>
|
||||
{errors.amount && (
|
||||
<small className="danger-text">{errors.amount.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payee and Currency */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="payee" className="form-label" required>
|
||||
Payee (Supplier Name/Transporter Name/Other)
|
||||
</Label>
|
||||
<InputSuggestions
|
||||
organizationList={Payees}
|
||||
value={watch("payee") || ""}
|
||||
onChange={(val) =>
|
||||
setValue("payee", val, { shouldValidate: true })
|
||||
}
|
||||
error={errors.payee?.message}
|
||||
placeholder="Select or enter payee"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="currencyId" className="form-label" required>
|
||||
Currency
|
||||
</Label>
|
||||
<select
|
||||
id="currencyId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("currencyId")}
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
|
||||
{currencyLoading && <option>Loading...</option>}
|
||||
|
||||
{!currencyLoading &&
|
||||
!currencyError &&
|
||||
currencyData?.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{`${currency.currencyName} (${currency.symbol})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.currencyId && (
|
||||
<small className="danger-text">{errors.currencyId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Frequency To and Status Id */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="frequency" className="form-label" required>
|
||||
Frequency
|
||||
</Label>
|
||||
<select
|
||||
id="frequency"
|
||||
className="form-select form-select-sm"
|
||||
{...register("frequency", { valueAsNumber: true })}
|
||||
>
|
||||
<option value="">Select Frequency</option>
|
||||
{Object.entries(FREQUENCY_FOR_RECURRING).map(([key, label]) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.frequency && (
|
||||
<small className="danger-text">{errors.frequency.message}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="statusId" className="form-label" required>
|
||||
Status
|
||||
</Label>
|
||||
<select
|
||||
id="statusId"
|
||||
className="form-select form-select-sm"
|
||||
{...register("statusId")}
|
||||
>
|
||||
<option value="">Select Status</option>
|
||||
{statusLoading && <option>Loading...</option>}
|
||||
{!statusLoading && !statusError && statusData?.map((status) => (
|
||||
<option key={status.id} value={status.id}>
|
||||
{status.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.statusId && (
|
||||
<small className="danger-text">{errors.statusId.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Buffer Days and Number of Iteration */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="paymentBufferDays" className="form-label" required>
|
||||
Payment Buffer Days
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="paymentBufferDays"
|
||||
className="form-control form-control-sm"
|
||||
min="0"
|
||||
step="1"
|
||||
{...register("paymentBufferDays", { valueAsNumber: true })}
|
||||
placeholder="Enter payment buffer days"
|
||||
/>
|
||||
{errors.paymentBufferDays && (
|
||||
<small className="danger-text">{errors.paymentBufferDays.message}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="numberOfIteration" className="form-label" required>
|
||||
Number of Iteration
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="numberOfIteration"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="1"
|
||||
{...register("numberOfIteration", { valueAsNumber: true })}
|
||||
placeholder="Enter number of iterations"
|
||||
/>
|
||||
{errors.numberOfIteration && (
|
||||
<small className="danger-text">{errors.numberOfIteration.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<Label htmlFor="notifyTo" className="form-label" required>
|
||||
Notify Employees
|
||||
</Label>
|
||||
|
||||
<MultiEmployeeSearchInput
|
||||
control={control}
|
||||
name="notifyTo"
|
||||
projectId={watch("projectId")}
|
||||
placeholder="Select Employees"
|
||||
forAll={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="description" className="form-label" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
{...register("description")}
|
||||
rows="2"
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<small className="danger-text">
|
||||
{errors.description.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
<button
|
||||
type="reset"
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
>
|
||||
{createPending || isPending ? "Please wait...." : requestToEdit ? "Update" : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManageRecurringExpense
|
||||
|
||||
302
src/components/RecurringExpense/RecurringExpenseList.jsx
Normal file
@ -0,0 +1,302 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
EXPENSE_DRAFT,
|
||||
EXPENSE_REJECTEDBY,
|
||||
FREQUENCY_FOR_RECURRING,
|
||||
ITEMS_PER_PAGE,
|
||||
PAYEE_RECURRING_EXPENSE,
|
||||
} from "../../utils/constants";
|
||||
import { formatCurrency, useDebounce } from "../../utils/appUtils";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { ExpenseTableSkeleton } from "../Expenses/ExpenseSkeleton";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import Error from "../common/Error";
|
||||
import { useRecurringExpenseContext } from "../../pages/RecurringExpense/RecurringExpensePage";
|
||||
import { useRecurringExpenseList } from "../../hooks/useExpense";
|
||||
|
||||
const RecurringExpenseList = ({ search, filterStatuses }) => {
|
||||
const { setManageRequest, setVieRequest, setViewRecurring } = useRecurringExpenseContext();
|
||||
const navigate = useNavigate();
|
||||
const [IsDeleteModalOpen, setIsDeleteModalOpen,] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
|
||||
const SelfId = useSelector(
|
||||
(store) => store?.globalVariables?.loginUser?.employeeInfo?.id
|
||||
);
|
||||
|
||||
const statusColorMap = {
|
||||
"da462422-13b2-45cc-a175-910a225f6fc8": "primary", // Active
|
||||
"306856fb-5655-42eb-bf8b-808bb5e84725": "success", // Completed
|
||||
"3ec864d2-8bf5-42fb-ba70-5090301dd816": "danger", // De-Activated
|
||||
"8bfc9346-e092-4a80-acbf-515ae1ef6868": "warning", // Paused
|
||||
};
|
||||
|
||||
const recurringExpenseColumns = [
|
||||
{
|
||||
key: "expenseCategory",
|
||||
label: "Category",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.expenseCategory?.name || "N/A",
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
label: "Title",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.title || "N/A",
|
||||
},
|
||||
{
|
||||
key: "payee",
|
||||
label: "Payee",
|
||||
align: "text-start",
|
||||
getValue: (e) => e?.payee || "N/A",
|
||||
},
|
||||
{
|
||||
key: "frequency",
|
||||
label: "Frequency",
|
||||
align: "text-start",
|
||||
getValue: (e) =>
|
||||
e?.frequency !== undefined && e?.frequency !== null
|
||||
? FREQUENCY_FOR_RECURRING[e.frequency] || "N/A"
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
align: "text-end",
|
||||
getValue: (e) =>
|
||||
e?.amount
|
||||
? `${e?.currency?.symbol ? e.currency.symbol + " " : ""}${e.amount.toLocaleString()}`
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "Next Generation Date",
|
||||
align: "text-center",
|
||||
getValue: (e) =>
|
||||
e?.createdAt ? formatUTCToLocalTime(e.createdAt) : "N/A",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
align: "text-center",
|
||||
getValue: (e) => {
|
||||
const color = statusColorMap[e?.status?.id] || "secondary";
|
||||
const label = PAYEE_RECURRING_EXPENSE.find(
|
||||
(s) => s.id === e?.status?.id
|
||||
)?.label;
|
||||
return (
|
||||
<span className={`badge bg-label-${color}`}>
|
||||
{label || e?.status?.name || "N/A"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
const { data, isLoading, isError, error, isRefetching, refetch } =
|
||||
useRecurringExpenseList(
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
{},
|
||||
true,
|
||||
debouncedSearch
|
||||
);
|
||||
|
||||
const recurringExpenseData = data?.data || [];
|
||||
const totalPages = data?.totalPages || 1;
|
||||
|
||||
if (isError) {
|
||||
return <Error error={error} isFeteching={isRefetching} refetch={refetch} />;
|
||||
}
|
||||
|
||||
const header = [
|
||||
"Category",
|
||||
"Title",
|
||||
"Amount",
|
||||
"Payee",
|
||||
"Frequency",
|
||||
"Next Generation",
|
||||
"Status",
|
||||
"Action",
|
||||
];
|
||||
|
||||
if (isLoading) return <ExpenseTableSkeleton headers={header} />;
|
||||
|
||||
const canEditExpense = (recurringExpense) => {
|
||||
// return (
|
||||
// (recurringExpense?.expenseStatus?.id === EXPENSE_DRAFT ||
|
||||
// EXPENSE_REJECTEDBY.includes(recurringExpense?.expenseStatus.id)) &&
|
||||
// recurringExpense?.createdBy?.id === SelfId
|
||||
// );
|
||||
};
|
||||
|
||||
const canDeleteExpense = (request) => {
|
||||
return (
|
||||
request?.expenseStatus?.id === EXPENSE_DRAFT &&
|
||||
request?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
|
||||
const filteredData = recurringExpenseData.filter((item) =>
|
||||
filterStatuses.includes(item?.status?.id)
|
||||
);
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setDeletingId(id);
|
||||
DeleteExpense(
|
||||
{ id },
|
||||
{
|
||||
onSettled: () => {
|
||||
setDeletingId(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Recurring Expense"
|
||||
message="Under the working"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="card page-min-h table-responsive px-sm-4">
|
||||
<div className="card-datatable" id="payment-request-table">
|
||||
<table className="table border-top dataTable text-nowrap align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
{recurringExpenseColumns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((recurringExpense) => (
|
||||
<tr
|
||||
key={recurringExpense.id}
|
||||
className="align-middle"
|
||||
style={{ height: "50px" }}
|
||||
>
|
||||
{recurringExpenseColumns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`d-table-cell ${col.align ?? ""} py-3`}
|
||||
>
|
||||
{col?.customRender
|
||||
? col?.customRender(recurringExpense)
|
||||
: col?.getValue(recurringExpense)}
|
||||
</td>
|
||||
))}
|
||||
<td className="sticky-action-column bg-white">
|
||||
<div className="d-flex flex-row gap-2 gap-0">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
setViewRecurring({
|
||||
recurringId: recurringExpense?.id,
|
||||
view: true,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||
<li
|
||||
onClick={() =>
|
||||
setManageRequest({
|
||||
IsOpen: true,
|
||||
RecurringId: recurringExpense?.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||
Modify
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(recurringExpense.id);
|
||||
}}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={recurringExpenseColumns.length + 1}
|
||||
className="text-center border-0 py-8"
|
||||
>
|
||||
<p>No Recurring Expense Found</p>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex justify-content-end py-3 pe-3">
|
||||
<nav>
|
||||
<ul className="pagination mb-0">
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecurringExpenseList;
|
||||
112
src/components/RecurringExpense/RecurringExpenseSchema.js
Normal file
@ -0,0 +1,112 @@
|
||||
import { boolean, z } from "zod";
|
||||
import { INR_CURRENCY_CODE } from "../../utils/constants";
|
||||
|
||||
export const PaymentRecurringExpense = (expenseTypes) => {
|
||||
return z.object({
|
||||
title: z.string().min(1, { message: "Title is required" }).transform((val) => val.trim()),
|
||||
description: z.string().min(1, { message: "Description is required" }).transform((val) => val.trim()),
|
||||
payee: z.string().min(1, { message: "Payee name is required" }).transform((val) => val.trim()),
|
||||
notifyTo: z.string().min(1, { message: "Notification e-mail is required" }).transform((val) => val.trim()),
|
||||
currencyId: z
|
||||
.string()
|
||||
.min(1, { message: "Currency is required" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
amount: z
|
||||
.number({
|
||||
required_error: "Amount is required",
|
||||
invalid_type_error: "Amount must be a number",
|
||||
})
|
||||
.min(1, { message: "Amount must be greater than 0" })
|
||||
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
|
||||
message: "Amount must have at most 2 decimal places",
|
||||
}),
|
||||
|
||||
strikeDate: z
|
||||
.string()
|
||||
.min(1, { message: "Date is required" })
|
||||
.refine((val) => !isNaN(Date.parse(val)), {
|
||||
message: "Invalid date format",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
projectId: z
|
||||
.string()
|
||||
.min(1, { message: "Project is required" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
paymentBufferDays: z
|
||||
.number({
|
||||
required_error: "Buffer days is required",
|
||||
invalid_type_error: "Buffer days must be a number",
|
||||
})
|
||||
.min(0, { message: "Buffer days cannot be negative" }),
|
||||
|
||||
numberOfIteration: z
|
||||
.number({
|
||||
required_error: "Iteration is required",
|
||||
invalid_type_error: "Iteration must be a number",
|
||||
})
|
||||
.min(1, { message: "Iteration must be at least 1" }),
|
||||
|
||||
expenseCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Expense Category is required" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
statusId: z
|
||||
.string()
|
||||
.min(1, { message: "Please select a status" })
|
||||
.transform((val) => val.trim()),
|
||||
|
||||
frequency: z
|
||||
.number({
|
||||
required_error: "Frequency is required",
|
||||
invalid_type_error: "Frequency must be a number",
|
||||
})
|
||||
.refine((val) => [0, 1, 2, 3, 4, 5].includes(val), {
|
||||
message: "Invalid frequency selected",
|
||||
}),
|
||||
|
||||
isVariable: z.boolean().optional(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const defaultRecurringExpense = {
|
||||
title: "",
|
||||
description: "",
|
||||
payee: "",
|
||||
notifyTo: "",
|
||||
currencyId: "",
|
||||
amount: 0,
|
||||
strikeDate: "",
|
||||
projectId: "",
|
||||
paymentBufferDays: 0,
|
||||
numberOfIteration: 1,
|
||||
expenseCategoryId: "",
|
||||
statusId: "",
|
||||
frequency: 1,
|
||||
isVariable: true,
|
||||
};
|
||||
|
||||
|
||||
export const SearchRecurringExpenseSchema = z.object({
|
||||
title: z.array(z.string()).optional(),
|
||||
description: z.array(z.string()).optional(),
|
||||
payee: z.array(z.string()).optional(),
|
||||
notifyTo: z.array(z.string()).optional(),
|
||||
currencyId: z.array(z.string()).optional(),
|
||||
amount: z.array(z.string()).optional(),
|
||||
strikeDate: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
paymentBufferDays: z.string().optional(),
|
||||
numberOfIteration: z.string().optional(),
|
||||
expenseCategoryId: z.string().optional(),
|
||||
statusId: z.string().optional(),
|
||||
frequency: z.string().optional(),
|
||||
isVariable: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
342
src/components/RecurringExpense/RecurringRexpenseList.jsx
Normal file
@ -0,0 +1,342 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
EXPENSE_DRAFT,
|
||||
EXPENSE_REJECTEDBY,
|
||||
ITEMS_PER_PAGE,
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
formatCurrency,
|
||||
getColorNameFromHex,
|
||||
useDebounce,
|
||||
} from "../../utils/appUtils";
|
||||
import { usePaymentRequestList } from "../../hooks/useExpense";
|
||||
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Avatar from "../../components/common/Avatar";
|
||||
import { ExpenseTableSkeleton } from "../Expenses/ExpenseSkeleton";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import Error from "../common/Error";
|
||||
import { useRecurringExpenseContext } from "../../pages/RecurringExpense/RecurringExpensePage";
|
||||
|
||||
const RecurringExpenseList = ({ filters, groupBy = "submittedBy", search }) => {
|
||||
const { setManageRequest, setVieRequest } = useRecurringExpenseContext();
|
||||
const navigate = useNavigate();
|
||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
const SelfId = useSelector(
|
||||
(store) => store?.globalVariables?.loginUser?.employeeInfo?.id
|
||||
);
|
||||
const groupByField = (items, field) => {
|
||||
return items.reduce((acc, item) => {
|
||||
let key;
|
||||
let displayField;
|
||||
|
||||
switch (field) {
|
||||
case "transactionDate":
|
||||
key = item?.transactionDate?.split("T")[0];
|
||||
displayField = "Transaction Date";
|
||||
break;
|
||||
case "status":
|
||||
key = item?.status?.displayName || "Unknown";
|
||||
displayField = "Status";
|
||||
break;
|
||||
case "submittedBy":
|
||||
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
||||
}`.trim();
|
||||
displayField = "Submitted By";
|
||||
break;
|
||||
case "project":
|
||||
key = item?.project?.name || "Unknown Project";
|
||||
displayField = "Project";
|
||||
break;
|
||||
case "paymentMode":
|
||||
key = item?.paymentMode?.name || "Unknown Mode";
|
||||
displayField = "Payment Mode";
|
||||
break;
|
||||
case "expensesType":
|
||||
key = item?.expensesType?.name || "Unknown Type";
|
||||
displayField = "Expense Category";
|
||||
break;
|
||||
case "createdAt":
|
||||
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
||||
displayField = "Created Date";
|
||||
break;
|
||||
default:
|
||||
key = "Others";
|
||||
displayField = "Others";
|
||||
}
|
||||
|
||||
const groupKey = `${field}_${key}`; // unique key for object property
|
||||
if (!acc[groupKey]) {
|
||||
acc[groupKey] = { key, displayField, items: [] };
|
||||
}
|
||||
|
||||
acc[groupKey].items.push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const paymentRequestColumns = [
|
||||
{
|
||||
key: "paymentRequestUID",
|
||||
label: "Template Name",
|
||||
align: "text-start mx-2",
|
||||
getValue: (e) => e.paymentRequestUID || "N/A",
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
label: "Frequency",
|
||||
align: "text-start",
|
||||
getValue: (e) => e.title || "N/A",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "Next Generation Date",
|
||||
align: "text-start",
|
||||
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "Status",
|
||||
align: "text-start",
|
||||
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
const { data, isLoading, isError, error, isRefetching, refetch } =
|
||||
usePaymentRequestList(
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
filters,
|
||||
true,
|
||||
debouncedSearch
|
||||
);
|
||||
|
||||
const paymentRequestData = data?.data || [];
|
||||
const totalPages = data?.data?.totalPages || 1;
|
||||
|
||||
if (isError) {
|
||||
return <Error error={error} isFeteching={isRefetching} refetch={refetch} />;
|
||||
}
|
||||
const header = [
|
||||
"Request ID",
|
||||
"Request Title",
|
||||
"Submitted By",
|
||||
"Submitted On",
|
||||
"Amount",
|
||||
"Status",
|
||||
"Action",
|
||||
];
|
||||
if (isLoading) return <ExpenseTableSkeleton headers={header} />;
|
||||
|
||||
const grouped = groupBy
|
||||
? groupByField(data?.data ?? [], groupBy)
|
||||
: { All: data?.data ?? [] };
|
||||
const IsGroupedByDate = [
|
||||
{ key: "transactionDate", displayField: "Transaction Date" },
|
||||
{ key: "createdAt", displayField: "created Date" },
|
||||
]?.includes(groupBy);
|
||||
|
||||
const canEditExpense = (paymentRequest) => {
|
||||
return (
|
||||
(paymentRequest?.expenseStatus?.id === EXPENSE_DRAFT ||
|
||||
EXPENSE_REJECTEDBY.includes(paymentRequest?.expenseStatus.id)) &&
|
||||
paymentRequest?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
const canDetetExpense = (request) => {
|
||||
return (
|
||||
request?.expenseStatus?.id === EXPENSE_DRAFT &&
|
||||
request?.createdBy?.id === SelfId
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setDeletingId(id);
|
||||
DeleteExpense(
|
||||
{ id },
|
||||
{
|
||||
onSettled: () => {
|
||||
setDeletingId(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Expense"
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
// loading={isPending}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
)}
|
||||
<div className="card page-min-h table-responsive px-sm-4">
|
||||
<div className="card-datatable" id="payment-request-table">
|
||||
<table className="table border-top dataTable text-nowrap align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
{paymentRequestColumns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{Object.keys(grouped).length > 0 ? (
|
||||
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||
<React.Fragment key={key}>
|
||||
<tr className="tr-group text-dark">
|
||||
<td colSpan={8} className="text-start">
|
||||
<div className="d-flex align-items-center">
|
||||
{" "}
|
||||
<small className="fs-6 py-1">
|
||||
{displayField} :{" "}
|
||||
</small>{" "}
|
||||
<small className="fs-6 ms-3">
|
||||
{IsGroupedByDate ? formatUTCToLocalTime(key) : key}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{items?.map((paymentRequest) => (
|
||||
<tr key={paymentRequest.id}>
|
||||
{paymentRequestColumns.map(
|
||||
(col) =>
|
||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`d-table-cell ${col.align ?? ""}`}
|
||||
>
|
||||
{col?.customRender
|
||||
? col?.customRender(paymentRequest)
|
||||
: col?.getValue(paymentRequest)}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
<td className="sticky-action-column bg-white">
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
setVieRequest({
|
||||
requestId: paymentRequest.id,
|
||||
view: true,
|
||||
})
|
||||
}
|
||||
></i>
|
||||
{canEditExpense(paymentRequest) && (
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded text-muted p-0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||
<li
|
||||
onClick={() =>
|
||||
setManageRequest({
|
||||
IsOpen: true,
|
||||
RequestId: paymentRequest.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||
<span className="align-left ">
|
||||
Modify
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{canDetetExpense(paymentRequest) && (
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(paymentRequest.id);
|
||||
}}
|
||||
>
|
||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||
<span className="align-left">
|
||||
Delete
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center border-0 ">
|
||||
<div className="py-8">
|
||||
<p>No Request Found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex justify-content-end py-3 pe-3">
|
||||
<nav>
|
||||
<ul className="pagination mb-0">
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => setCurrentPage(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecurringExpenseList;
|
||||
14
src/components/RecurringExpense/ViewRecurringExpense.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const ViewRecurringExpense = () => {
|
||||
return (
|
||||
<div>
|
||||
<h5>Detail Recurring</h5>
|
||||
<div className='d-flex justify-content-center align-items-center'>
|
||||
<p>Under the Working!</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewRecurringExpense
|
||||
@ -8,12 +8,14 @@ import { orgSize, reference } from "../../utils/constants";
|
||||
import moment from "moment";
|
||||
import { useGlobalServices } from "../../hooks/masterHook/useMaster";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
||||
const { data, isError, isLoading: industryLoading } = useIndustries();
|
||||
const [logoPreview, setLogoPreview] = useState(null);
|
||||
const [logoName, setLogoName] = useState("");
|
||||
const { data: services, isLoading: serviceLoading } = useGlobalServices();
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@ -29,8 +31,8 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
||||
error,
|
||||
isPending,
|
||||
} = useCreateTenant(() => {
|
||||
debugger
|
||||
onNext()
|
||||
// onNext()
|
||||
navigate("/tenants");
|
||||
});
|
||||
|
||||
const handleNext = async () => {
|
||||
@ -134,7 +136,6 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
||||
control={control}
|
||||
placeholder="DD-MM-YYYY"
|
||||
maxDate={new Date()}
|
||||
className={errors.onBoardingDate ? "is-invalid" : ""}
|
||||
/>
|
||||
{errors.onBoardingDate && (
|
||||
<div className="invalid-feedback">
|
||||
|
||||
266
src/components/collections/AddPayment.jsx
Normal file
@ -0,0 +1,266 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { defaultPayment, paymentSchema } from "./collectionSchema";
|
||||
import Label from "../common/Label";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { formatDate } from "date-fns";
|
||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||
import { useAddPayment, useCollection } from "../../hooks/useCollections";
|
||||
import { formatFigure, localToUtc } from "../../utils/appUtils";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { PaymentHistorySkeleton } from "./CollectionSkeleton";
|
||||
import { usePaymentAjustmentHead } from "../../hooks/masterHook/useMaster";
|
||||
|
||||
const AddPayment = ({ onClose }) => {
|
||||
const { addPayment } = useCollectionContext();
|
||||
const { data, isLoading, isError, error } = useCollection(
|
||||
addPayment?.invoiceId
|
||||
);
|
||||
const {
|
||||
data: paymentTypes,
|
||||
isLoading: isPaymentTypeLoading,
|
||||
isError: isPaymentTypeError,
|
||||
error: paymentError,
|
||||
} = usePaymentAjustmentHead(true);
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(paymentSchema),
|
||||
defaultValues: defaultPayment,
|
||||
});
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
const { mutate: AddPayment, isPending } = useAddPayment(() => {
|
||||
handleClose();
|
||||
});
|
||||
const onSubmit = (formData) => {
|
||||
const payload = {
|
||||
...formData,
|
||||
paymentReceivedDate: localToUtc(formData.paymentReceivedDate),
|
||||
invoiceId: addPayment.invoiceId,
|
||||
};
|
||||
AddPayment(payload);
|
||||
};
|
||||
const handleClose = (formData) => {
|
||||
reset(defaultPayment);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container pb-3">
|
||||
<div className="text-black fs-5 mb-2">Add Payment</div>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
||||
<div className="row px-md-1 px-0">
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>TransanctionId</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("transactionId")}
|
||||
/>
|
||||
{errors.transactionId && (
|
||||
<small className="danger-text">
|
||||
{errors.transactionId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Transaction Date </Label>
|
||||
<DatePicker
|
||||
name="paymentReceivedDate"
|
||||
control={control}
|
||||
minDate={
|
||||
data?.clientSubmitedDate
|
||||
? new Date(
|
||||
new Date(data?.clientSubmitedDate).setDate(
|
||||
new Date(data?.clientSubmitedDate).getDate() + 1
|
||||
)
|
||||
)
|
||||
: null
|
||||
}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
{errors.paymentReceivedDate && (
|
||||
<small className="danger-text">
|
||||
{errors.paymentReceivedDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label
|
||||
htmlFor="paymentAdjustmentHeadId"
|
||||
className="form-label"
|
||||
required
|
||||
>
|
||||
Payment Adjustment Head
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm "
|
||||
{...register("paymentAdjustmentHeadId")}
|
||||
>
|
||||
{isPaymentTypeLoading ? (
|
||||
<option>Loading..</option>
|
||||
) : (
|
||||
<>
|
||||
<option value="">Select Payment Head</option>
|
||||
{paymentTypes?.data
|
||||
?.sort((a, b) => a.name.localeCompare(b.name))
|
||||
?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
{errors.paymentAdjustmentHeadId && (
|
||||
<small className="danger-text">
|
||||
{errors.paymentAdjustmentHeadId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.amount && (
|
||||
<small className="danger-text">{errors.amount.message}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 mb-2">
|
||||
<Label htmlFor="comment" className="form-label" required>
|
||||
Comment
|
||||
</Label>
|
||||
<textarea
|
||||
id="comment"
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
/>
|
||||
{errors.comment && (
|
||||
<small className="danger-text">{errors.comment.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
{" "}
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
onClose();
|
||||
}}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
{isLoading ? (
|
||||
<PaymentHistorySkeleton />
|
||||
) : (
|
||||
data?.receivedInvoicePayments?.length > 0 && (
|
||||
<div className="mt-1 text-start">
|
||||
<div className="mb-2 text-secondry fs-6">
|
||||
<i className="bx bx-history bx-sm me-1"></i>History
|
||||
</div>
|
||||
|
||||
<div className="row text-start mx-2">
|
||||
{data.receivedInvoicePayments
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.map((payment, index) => (
|
||||
<div className="col-12 mb-2" key={payment.id}>
|
||||
<div className=" p-2 border-start border-warning">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 d-flex justify-content-between align-items-center ">
|
||||
<div>
|
||||
<small className="fw-semibold me-1">
|
||||
Transaction Date:
|
||||
</small>{" "}
|
||||
{formatUTCToLocalTime(payment.paymentReceivedDate)}
|
||||
</div>
|
||||
<span className="fs-semibold d-block d-md-none">
|
||||
{formatFigure(payment.amount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-0 d-flex align-items-center m-0">
|
||||
<small className="fw-semibold me-2">
|
||||
Updated By:
|
||||
</small>{" "}
|
||||
<Avatar
|
||||
size="xs"
|
||||
firstName={payment?.createdBy?.firstName}
|
||||
lastName={payment?.createdBy?.lastName}
|
||||
/>{" "}
|
||||
{payment?.createdBy?.firstName}{" "}
|
||||
{payment.createdBy?.lastName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-1">
|
||||
<small className="fw-semibold">
|
||||
Transaction ID:
|
||||
</small>{" "}
|
||||
{payment.transactionId}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 ">
|
||||
<div className="d-flex justify-content-between">
|
||||
<span>{payment?.paymentAdjustmentHead?.name}</span>
|
||||
<span className="fs-semibold d-none d-md-block">
|
||||
{formatFigure(payment.amount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-tiny m-0 mt-1">
|
||||
{payment?.comment}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPayment;
|
||||