Compare commits
227 Commits
main
...
upgrade_Ex
| Author | SHA1 | Date | |
|---|---|---|---|
| 081fea51d3 | |||
| 26fd5628a0 | |||
| 608741f2a1 | |||
| f852684344 | |||
| e1455c9ef8 | |||
| 8e38f52e72 | |||
| d18eabf6f5 | |||
| ec38e8f9e0 | |||
| e962e02fbc | |||
| 4cbac98986 | |||
| 2e46e3235c | |||
| ba42cede6a | |||
| 523ef6775f | |||
| 096a4acfff | |||
| 03207a3318 | |||
| 8f83d91d25 | |||
| bb1fb2a696 | |||
| 6b56d04069 | |||
| a161f87b82 | |||
| f5318c0a8d | |||
| c0868d90ed | |||
| 6adad5718a | |||
| 5fea95f006 | |||
| 5f766b4028 | |||
| 9c2645d1d3 | |||
| ade877d633 | |||
| 492f483f5e | |||
| f69faccd62 | |||
| 25a599e614 | |||
| 71959558cd | |||
| e40ff68209 | |||
| 2a0c10fa71 | |||
| 161f14d247 | |||
| da32f9c8ff | |||
| 72e5cf0bbe | |||
| 1278e32da9 | |||
| 28f15f649f | |||
| 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="" />
|
<meta name="description" content="" />
|
||||||
|
|
||||||
|
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="/img/favicon.ico" />
|
<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/default.css" />
|
||||||
<link rel="stylesheet" href="/assets/css/skeleton.css" />
|
<link rel="stylesheet" href="/assets/css/skeleton.css" />
|
||||||
<link rel="stylesheet" href="/assets/css/hover-utility.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" />
|
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||||
|
|
||||||
@ -42,6 +46,8 @@
|
|||||||
<link rel="stylesheet" href="/assets/vendor/libs/bs-stepper/bs-stepper.css" />
|
<link rel="stylesheet" href="/assets/vendor/libs/bs-stepper/bs-stepper.css" />
|
||||||
<link rel="stylesheet" href="/assets/vendor/libs/bootstrap-select/bootstrap-select.css" />
|
<link rel="stylesheet" href="/assets/vendor/libs/bootstrap-select/bootstrap-select.css" />
|
||||||
<link rel="stylesheet" href="/assets/vendor/libs/select2/select2.css" />
|
<link rel="stylesheet" href="/assets/vendor/libs/select2/select2.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.js" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" />
|
<link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" />
|
||||||
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />
|
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />
|
||||||
@ -105,6 +111,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/@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.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 -->
|
<!-- Flatpickr JS -->
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -10,3 +10,128 @@
|
|||||||
.table_header_border {
|
.table_header_border {
|
||||||
border-bottom:2px solid var(--bs-table-border-color) ;
|
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
@ -32574,3 +32574,6 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
|
|||||||
.text-red{
|
.text-red{
|
||||||
color:var(--bs-red)
|
color:var(--bs-red)
|
||||||
}
|
}
|
||||||
|
.bg-gray {
|
||||||
|
background:var(--bs-body-color)
|
||||||
|
}
|
||||||
879
public/assets/vendor/libs/tagify/tagify.css
vendored
Normal file
@ -0,0 +1,879 @@
|
|||||||
|
@charset "UTF-8";
|
||||||
|
:root {
|
||||||
|
--tagify-dd-color-primary: rgb(53,149,246);
|
||||||
|
--tagify-dd-bg-color: white;
|
||||||
|
--tagify-dd-item-pad: .3em .5em;
|
||||||
|
--tagify-dd-max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagify {
|
||||||
|
--tags-disabled-bg: #F1F1F1;
|
||||||
|
--tags-border-color: #DDD;
|
||||||
|
--tags-hover-border-color: #CCC;
|
||||||
|
--tags-focus-border-color: #3595f6;
|
||||||
|
--tag-border-radius: 3px;
|
||||||
|
--tag-bg: rgba(167, 172, 178, 0.5);
|
||||||
|
--tag-hover: #D3E2E2;
|
||||||
|
--tag-text-color: black;
|
||||||
|
--tag-text-color--edit: black;
|
||||||
|
--tag-pad: 0.3em 0.5em;
|
||||||
|
--tag-inset-shadow-size: 2em;
|
||||||
|
--tag-invalid-color: #ff3e1d;
|
||||||
|
--tag-invalid-bg: rgba(255, 62, 29, 0.5);
|
||||||
|
--tag--min-width: 1ch;
|
||||||
|
--tag--max-width: auto;
|
||||||
|
--tag-hide-transition: 0.3s;
|
||||||
|
--tag-remove-bg: rgba(255, 62, 29, 0.3);
|
||||||
|
--tag-remove-btn-color: #7a838b;
|
||||||
|
--tag-remove-btn-bg: none;
|
||||||
|
--tag-remove-btn-bg--hover: #ff2804;
|
||||||
|
--input-color: inherit;
|
||||||
|
--placeholder-color: rgba(0, 0, 0, 0.4);
|
||||||
|
--placeholder-color-focus: rgba(0, 0, 0, 0.25);
|
||||||
|
--loader-size: .8em;
|
||||||
|
--readonly-striped: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border: 1px solid var(--tags-border-color);
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
cursor: text;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: 0.1s;
|
||||||
|
}
|
||||||
|
@keyframes tags--bump {
|
||||||
|
30% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes rotateLoader {
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagify:hover:not(.tagify--focus):not(.tagify--invalid) {
|
||||||
|
--tags-border-color: var(--tags-hover-border-color);
|
||||||
|
}
|
||||||
|
.tagify[disabled] {
|
||||||
|
background: var(--tags-disabled-bg);
|
||||||
|
filter: saturate(0);
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tagify[readonly].tagify--select, .tagify[disabled].tagify--select {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tagify[readonly]:not(.tagify--mix):not(.tagify--select), .tagify[disabled]:not(.tagify--mix):not(.tagify--select) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.tagify[readonly]:not(.tagify--mix):not(.tagify--select) > .tagify__input, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) > .tagify__input {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div {
|
||||||
|
padding: var(--tag-pad);
|
||||||
|
}
|
||||||
|
.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before {
|
||||||
|
animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused;
|
||||||
|
}
|
||||||
|
@keyframes readonlyStyles {
|
||||||
|
0% {
|
||||||
|
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
|
||||||
|
box-shadow: none;
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagify[readonly] .tagify__tag__removeBtn, .tagify[disabled] .tagify__tag__removeBtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify--loading .tagify__input > br:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify--loading .tagify__input::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
.tagify--loading .tagify__input::after {
|
||||||
|
content: "";
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 1;
|
||||||
|
width: 0.7em;
|
||||||
|
height: 0.7em;
|
||||||
|
width: var(--loader-size);
|
||||||
|
height: var(--loader-size);
|
||||||
|
min-width: 0;
|
||||||
|
border: 3px solid;
|
||||||
|
border-color: #EEE #BBB #888 transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: rotateLoader 0.4s infinite linear;
|
||||||
|
content: "" !important;
|
||||||
|
margin: -2px 0 -2px 0.5em;
|
||||||
|
}
|
||||||
|
.tagify--loading .tagify__input:empty::after {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.tagify + input,
|
||||||
|
.tagify + textarea {
|
||||||
|
position: absolute !important;
|
||||||
|
left: -9999em !important;
|
||||||
|
transform: scale(0) !important;
|
||||||
|
}
|
||||||
|
.tagify__tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: calc(var(--tag--max-width) - 10px);
|
||||||
|
margin-inline: 5px 0;
|
||||||
|
margin-block: 5px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
outline: none;
|
||||||
|
line-height: normal;
|
||||||
|
cursor: default;
|
||||||
|
transition: 0.13s ease-out;
|
||||||
|
}
|
||||||
|
.tagify__tag > div {
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: var(--tag-pad);
|
||||||
|
color: var(--tag-text-color);
|
||||||
|
line-height: inherit;
|
||||||
|
border-radius: var(--tag-border-radius);
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: 0.13s ease-out;
|
||||||
|
}
|
||||||
|
.tagify__tag > div > * {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
min-width: var(--tag--min-width);
|
||||||
|
max-width: var(--tag--max-width);
|
||||||
|
transition: 0.8s ease, 0.1s color;
|
||||||
|
}
|
||||||
|
.tagify__tag > div > *[contenteditable] {
|
||||||
|
outline: none;
|
||||||
|
user-select: text;
|
||||||
|
cursor: text;
|
||||||
|
margin: -2px;
|
||||||
|
padding: 2px;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
.tagify__tag > div::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-radius: inherit;
|
||||||
|
inset: var(--tag-bg-inset, 0);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: 120ms ease;
|
||||||
|
animation: tags--bump 0.3s ease-out 1;
|
||||||
|
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset;
|
||||||
|
}
|
||||||
|
.tagify__tag:hover:not([readonly]) div::before, .tagify__tag:focus div::before {
|
||||||
|
--tag-bg-inset: -2.5px;
|
||||||
|
--tag-bg: var(--tag-hover);
|
||||||
|
}
|
||||||
|
.tagify__tag--loading {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tagify__tag--loading .tagify__tag__removeBtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify__tag--loading::after {
|
||||||
|
--loader-size: .4em;
|
||||||
|
content: "";
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 1;
|
||||||
|
width: 0.7em;
|
||||||
|
height: 0.7em;
|
||||||
|
width: var(--loader-size);
|
||||||
|
height: var(--loader-size);
|
||||||
|
min-width: 0;
|
||||||
|
border: 3px solid;
|
||||||
|
border-color: #EEE #BBB #888 transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: rotateLoader 0.4s infinite linear;
|
||||||
|
margin: 0 0.5em 0 -0.1em;
|
||||||
|
}
|
||||||
|
.tagify__tag--flash div::before {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.tagify__tag--hide {
|
||||||
|
width: 0 !important;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
transition: var(--tag-hide-transition);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tagify__tag--hide > div > * {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tagify__tag.tagify--noAnim > div::before {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div > span {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before {
|
||||||
|
--tag-bg: var(--tag-invalid-bg);
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
.tagify__tag[readonly] .tagify__tag__removeBtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify__tag[readonly] > div::before {
|
||||||
|
animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused;
|
||||||
|
}
|
||||||
|
@keyframes readonlyStyles {
|
||||||
|
0% {
|
||||||
|
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
|
||||||
|
box-shadow: none;
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagify__tag--editable > div {
|
||||||
|
color: var(--tag-text-color--edit);
|
||||||
|
}
|
||||||
|
.tagify__tag--editable > div::before {
|
||||||
|
box-shadow: 0 0 0 2px var(--tag-hover) inset !important;
|
||||||
|
}
|
||||||
|
.tagify__tag--editable > .tagify__tag__removeBtn {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tagify__tag--editable > .tagify__tag__removeBtn::after {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%) translateX(5px);
|
||||||
|
}
|
||||||
|
.tagify__tag--editable.tagify--invalid > div::before {
|
||||||
|
box-shadow: 0 0 0 2px var(--tag-invalid-color) inset !important;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn {
|
||||||
|
order: 5;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 14px/1 Arial;
|
||||||
|
background: var(--tag-remove-btn-bg);
|
||||||
|
color: var(--tag-remove-btn-color);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-inline: auto 4.6666666667px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn::after {
|
||||||
|
content: "×";
|
||||||
|
transition: 0.3s, color 0s;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn:hover {
|
||||||
|
color: white;
|
||||||
|
background: var(--tag-remove-btn-bg--hover);
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn:hover + div > span {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn:hover + div::before {
|
||||||
|
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg, rgba(255, 62, 29, 0.3)) inset !important;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.tagify:not(.tagify--mix) .tagify__input br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify:not(.tagify--mix) .tagify__input * {
|
||||||
|
display: inline;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tagify__input {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 110px;
|
||||||
|
margin: 5px;
|
||||||
|
padding: var(--tag-pad);
|
||||||
|
line-height: normal;
|
||||||
|
position: relative;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--input-color);
|
||||||
|
box-sizing: inherit;
|
||||||
|
/* Seems firefox newer versions don't need this any more
|
||||||
|
@supports ( -moz-appearance:none ){
|
||||||
|
&::before{
|
||||||
|
line-height: inherit;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
@-moz-document url-prefix() {}
|
||||||
|
.tagify__input:empty::before {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.tagify__input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.tagify__input:focus::before {
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translatex(6px);
|
||||||
|
/* ALL MS BROWSERS: hide placeholder (on focus) otherwise the caret is placed after it, which is weird */
|
||||||
|
/* IE Edge 12+ CSS styles go here */
|
||||||
|
}
|
||||||
|
@supports (-ms-ime-align: auto) {
|
||||||
|
.tagify__input:focus::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagify__input:focus:empty::before {
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
color: var(--placeholder-color-focus);
|
||||||
|
}
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
.tagify__input:focus:empty::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagify__input::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
height: 1em;
|
||||||
|
line-height: 1em;
|
||||||
|
margin: auto 0;
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--placeholder-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.tagify__input::after {
|
||||||
|
content: attr(data-suggest);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: absolute;
|
||||||
|
min-width: calc(100% - 1.5em);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre; /* allows spaces at the beginning */
|
||||||
|
color: var(--tag-text-color);
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
.tagify__input .tagify__tag {
|
||||||
|
margin: 0 1px;
|
||||||
|
}
|
||||||
|
.tagify--mix {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tagify--mix .tagify__input {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tagify--mix .tagify__input::before {
|
||||||
|
height: auto;
|
||||||
|
display: none;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.tagify--mix .tagify__input::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
.tagify--select::after {
|
||||||
|
content: ">";
|
||||||
|
opacity: 0.5;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
font: 16px monospace;
|
||||||
|
line-height: 8px;
|
||||||
|
height: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg);
|
||||||
|
transition: 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.tagify--select[aria-expanded=true]::after {
|
||||||
|
transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2);
|
||||||
|
}
|
||||||
|
.tagify--select .tagify__tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 1.8em;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.tagify--select .tagify__tag div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify--select .tagify__input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.tagify--empty .tagify__input::before {
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.tagify--mix .tagify--empty .tagify__input::before {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.tagify--focus {
|
||||||
|
--tags-border-color: var(--tags-focus-border-color);
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
.tagify--invalid {
|
||||||
|
--tags-border-color: #ff3e1d;
|
||||||
|
}
|
||||||
|
.tagify__dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-top: 1px solid var(--tagify-dd-color-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.tagify__dropdown[dir=rtl] {
|
||||||
|
transform: translate(-100%, -1px);
|
||||||
|
}
|
||||||
|
.tagify__dropdown[placement=top] {
|
||||||
|
margin-top: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
.tagify__dropdown[placement=top] .tagify__dropdown__wrapper {
|
||||||
|
border-top-width: 1.1px;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
.tagify__dropdown[position=text] {
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), 0.1);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.tagify__dropdown[position=text] .tagify__dropdown__wrapper {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
.tagify__dropdown__wrapper {
|
||||||
|
max-height: var(--tagify-dd-max-height);
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: var(--tagify-dd-bg-color);
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: var(--tagify-dd-color-primary);
|
||||||
|
border-bottom-width: 1.5px;
|
||||||
|
border-top-width: 0;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: 0.3s cubic-bezier(0.5, 0, 0.3, 1), transform 0.15s;
|
||||||
|
animation: dd-wrapper-show 0s 0.3s forwards;
|
||||||
|
}
|
||||||
|
@keyframes dd-wrapper-show {
|
||||||
|
to {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagify__dropdown__header:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify__dropdown__footer {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding: var(--tagify-dd-item-pad);
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.tagify__dropdown__footer:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify__dropdown--initial .tagify__dropdown__wrapper {
|
||||||
|
max-height: 20px;
|
||||||
|
transform: translateY(-1em);
|
||||||
|
}
|
||||||
|
.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper {
|
||||||
|
transform: translateY(2em);
|
||||||
|
}
|
||||||
|
.tagify__dropdown__item {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--tagify-dd-item-pad);
|
||||||
|
margin: 1px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
max-height: 60px;
|
||||||
|
max-width: 100%;
|
||||||
|
/* custom hidden transition effect is needed for horizontal-layout suggestions */
|
||||||
|
}
|
||||||
|
.tagify__dropdown__item--active {
|
||||||
|
background: var(--tagify-dd-color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.tagify__dropdown__item:active {
|
||||||
|
filter: brightness(105%);
|
||||||
|
}
|
||||||
|
.tagify__dropdown__item--hidden {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
transition: var(--tagify-dd-item--hidden-duration, 0.3s) !important;
|
||||||
|
}
|
||||||
|
.tagify__dropdown__item--hidden > * {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggestions items */
|
||||||
|
.tagify__dropdown.users-list {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.tagify__dropdown.users-list .addAll {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.tagify__dropdown.users-list .tagify__dropdown__item {
|
||||||
|
padding: 0.5em 0.7em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0 1em;
|
||||||
|
grid-template-areas: "avatar name" "avatar email";
|
||||||
|
}
|
||||||
|
.tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
|
||||||
|
grid-area: avatar;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: 0.1s ease-out;
|
||||||
|
}
|
||||||
|
.tagify__dropdown.users-list img {
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.tagify__dropdown.users-list strong {
|
||||||
|
grid-area: name;
|
||||||
|
width: 100%;
|
||||||
|
align-self: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.tagify__dropdown.users-list span {
|
||||||
|
grid-area: email;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags items */
|
||||||
|
.tagify__tag {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tagify__tag .tagify__tag__avatar-wrap {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
white-space: normal;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 5px;
|
||||||
|
transition: 0.12s ease-out;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.tagify__tag img {
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir=rtl] .tagify__tag .tagify__tag__avatar-wrap {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-style .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
|
||||||
|
background: #f5f5f9;
|
||||||
|
}
|
||||||
|
.light-style .tagify__tag .tagify__tag__avatar-wrap {
|
||||||
|
background: #f5f5f9;
|
||||||
|
}
|
||||||
|
.light-style .tagify__dropdown.users-list .addAll {
|
||||||
|
border-bottom: 1px solid #e4e6e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-style .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
|
||||||
|
background: #232333;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__tag .tagify__tag__avatar-wrap {
|
||||||
|
background: #232333;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__dropdown.users-list .addAll {
|
||||||
|
border-bottom: 1px solid #4e4f6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-inline .tagify__dropdown__wrapper {
|
||||||
|
padding: 0 0.4375rem 0.4375rem 0.4375rem;
|
||||||
|
}
|
||||||
|
.tags-inline .tagify__dropdown__item {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
margin: 0.4375rem 0.4375rem 0 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir=rtl] .tags-inline .tagify__dropdown__item {
|
||||||
|
margin: 0.4375rem 0 0 0.4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-style .tags-inline .tagify__dropdown__item {
|
||||||
|
border: 1px solid #e4e6e8;
|
||||||
|
color: #646e78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-style .tags-inline .tagify__dropdown__item {
|
||||||
|
border: 1px solid #4e4f6c;
|
||||||
|
color: #b2b2c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagify-email-list {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
/* Do not show the "remove tag" (x) button when only a single tag remains */
|
||||||
|
}
|
||||||
|
.tagify-email-list.tagify {
|
||||||
|
padding: 0 !important;
|
||||||
|
padding-bottom: calc(0.4375rem - var(--bs-border-width)) !important;
|
||||||
|
}
|
||||||
|
.tagify-email-list.tagify {
|
||||||
|
padding: 0 !important;
|
||||||
|
padding-bottom: calc(0.4375rem - var(--bs-border-width)) !important;
|
||||||
|
}
|
||||||
|
.tagify-email-list.tagify.tagify--focus {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__tag {
|
||||||
|
margin: 0;
|
||||||
|
margin-inline-start: 0 !important;
|
||||||
|
margin-inline-end: 0.625rem !important;
|
||||||
|
margin-bottom: 0.4375rem !important;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__tag > div {
|
||||||
|
padding: 0.21875rem 0.4375rem !important;
|
||||||
|
padding-inline: 0.875rem !important;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__tag:only-of-type > div {
|
||||||
|
padding-inline: 0.4375rem !important;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__tag:only-of-type .tagify__tag__removeBtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__tag__removeBtn {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-6px) scale(0.5);
|
||||||
|
margin-left: -3ch;
|
||||||
|
transition: 0.12s;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__tag:hover .tagify__tag__removeBtn {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
margin-left: -1ch;
|
||||||
|
}
|
||||||
|
.tagify-email-list .tagify__input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagify__tag > div {
|
||||||
|
border-radius: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir=rtl] .tagify-email-list .tagify__tag {
|
||||||
|
margin: 0 0.4375rem 0.4375rem 0;
|
||||||
|
}
|
||||||
|
[dir=rtl] .tagify-email-list .tagify__tag:hover .tagify__tag__removeBtn {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -1ch;
|
||||||
|
}
|
||||||
|
[dir=rtl] .tagify-email-list .tagify__tag__removeBtn {
|
||||||
|
transform: translateX(6px) scale(0.5);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -3ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-style .tagify-email-list .tagify__tag--editable:not(.tagify--invalid) > div::before {
|
||||||
|
box-shadow: 0 0 0 2px #e4e6e8 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-style .tagify-email-list .tagify__tag--editable:not(.tagify--invalid) > div::before {
|
||||||
|
box-shadow: 0 0 0 2px #4e4f6c inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagify.form-control {
|
||||||
|
transition: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
/* padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.4231rem !important; */
|
||||||
|
padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.2rem !important;
|
||||||
|
}
|
||||||
|
.fv-plugins-bootstrap5-row-invalid .tagify.form-control {
|
||||||
|
padding: 0 calc(0.4375rem - var(--bs-border-width)) calc(0.4375rem - 2px) !important;
|
||||||
|
}
|
||||||
|
.tagify.tagify--focus, .tagify.form-control:focus {
|
||||||
|
padding: 0 calc(0.4375rem - var(--bs-border-width)) 0.3606rem !important;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.tagify__tag, .tagify__input {
|
||||||
|
margin: 0.1875rem 0.625rem 0 0 !important;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.tagify__input {
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
.tagify__input:empty::before {
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
.tagify__tag > div {
|
||||||
|
line-height: 1.5rem;
|
||||||
|
padding: 0 0 0 0.4375rem;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn {
|
||||||
|
margin-right: 0.1375rem;
|
||||||
|
margin-left: 0.21875rem;
|
||||||
|
font-family: "boxicons";
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn:hover {
|
||||||
|
background: none;
|
||||||
|
color: #ff2804 !important;
|
||||||
|
}
|
||||||
|
.tagify__tag__removeBtn::after {
|
||||||
|
content: "\ef06";
|
||||||
|
}
|
||||||
|
.tagify__tag:hover:not([readonly]) div::before, .tagify__tag:focus div::before {
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
.tagify__dropdown {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.tagify[readonly]:not(.tagify--mix) .tagify__tag > div {
|
||||||
|
padding: 0 0.4375rem 0 0.4375rem !important;
|
||||||
|
}
|
||||||
|
.tagify__input {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.tagify__tag-text {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagify.form-control {
|
||||||
|
padding-top: 0.1412rem !important;
|
||||||
|
}
|
||||||
|
.tagify.tagify--focus, .tagify.form-control:focus {
|
||||||
|
padding-top: calc(0.1412rem - 1px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagify__tag__removeBtn {
|
||||||
|
margin-inline-end: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir=rtl] .tagify__tag, [dir=rtl] .tagify__input {
|
||||||
|
margin: 0.4375rem 0 0 0.4375rem;
|
||||||
|
}
|
||||||
|
[dir=rtl] .tagify + input,
|
||||||
|
[dir=rtl] .tagify + textarea {
|
||||||
|
left: 0;
|
||||||
|
right: -9999em !important;
|
||||||
|
}
|
||||||
|
[dir=rtl] .tagify__tag > div {
|
||||||
|
padding: 0 0.6875rem 0 0;
|
||||||
|
}
|
||||||
|
[dir=rtl] .tagify__tag__removeBtn {
|
||||||
|
margin-left: 0.4375rem;
|
||||||
|
margin-right: 0.21875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-style .tagify__tag > div::before {
|
||||||
|
box-shadow: 0 0 0 1.3em rgba(34, 48, 62, 0.08) inset;
|
||||||
|
}
|
||||||
|
.light-style .tagify__tag .tagify__tag-text {
|
||||||
|
color: #384551;
|
||||||
|
}
|
||||||
|
.light-style .tagify__tag:hover:not([readonly]) div::before, .light-style .tagify__tag:focus div::before {
|
||||||
|
box-shadow: 0 0 0 1.3em rgba(34, 48, 62, 0.12) inset;
|
||||||
|
}
|
||||||
|
.light-style .tagify__tag__removeBtn {
|
||||||
|
color: #7a838b;
|
||||||
|
}
|
||||||
|
.light-style .tagify__tag__removeBtn:hover + div::before {
|
||||||
|
background: rgba(255, 62, 29, 0.3);
|
||||||
|
}
|
||||||
|
.light-style .tagify:hover:not([readonly]) {
|
||||||
|
border-color: #ced1d5;
|
||||||
|
}
|
||||||
|
.light-style .tagify__input::before {
|
||||||
|
color: #a7acb2 !important;
|
||||||
|
}
|
||||||
|
.light-style .tagify__dropdown {
|
||||||
|
box-shadow: 0 0.25rem 0.75rem 0 rgba(34, 48, 62, 0.14);
|
||||||
|
border-top-color: #e4e6e8;
|
||||||
|
}
|
||||||
|
.light-style .tagify__dropdown__wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #e4e6e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-style .tagify__tag > div::before {
|
||||||
|
box-shadow: 0 0 0 1.3em rgba(230, 230, 241, 0.08) inset;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__tag > div .tagify__tag-text {
|
||||||
|
color: #d5d5e2;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__tag:hover:not([readonly]) div::before, .dark-style .tagify__tag:focus div::before {
|
||||||
|
box-shadow: 0 0 0 1.3em rgba(230, 230, 241, 0.12) inset;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__tag__removeBtn {
|
||||||
|
color: #a1a1b5;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__tag__removeBtn:hover + div::before {
|
||||||
|
background: rgba(255, 62, 29, 0.3);
|
||||||
|
}
|
||||||
|
.dark-style .tagify:hover:not([readonly]) {
|
||||||
|
border-color: #5f607b;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__input::before {
|
||||||
|
color: #7e7f96 !important;
|
||||||
|
}
|
||||||
|
.dark-style .tagify[readonly]:not(.tagify--mix) .tagify__tag > div::before {
|
||||||
|
background: linear-gradient(45deg, #5f607b 25%, transparent 25%, transparent 50%, #5f607b 50%, #5f607b 75%, transparent 75%, transparent) 0/5px 5px;
|
||||||
|
}
|
||||||
|
.dark-style .tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before {
|
||||||
|
animation: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__dropdown {
|
||||||
|
box-shadow: 0 0.25rem 0.75rem 0 rgba(20, 20, 29, 0.24);
|
||||||
|
border-top-color: #4e4f6c;
|
||||||
|
}
|
||||||
|
.dark-style .tagify__dropdown__wrapper {
|
||||||
|
box-shadow: 0 0.25rem 0.75rem 0 rgba(20, 20, 29, 0.24);
|
||||||
|
background: #2b2c40;
|
||||||
|
border-color: #4e4f6c;
|
||||||
|
}
|
||||||
120
public/assets/vendor/libs/tagify/tagify.js
vendored
Normal file
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 { queryClient } from "./layouts/AuthLayout";
|
||||||
import ModalProvider from "./ModalProvider";
|
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 = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useOrganizationModal } from "./hooks/useOrganization";
|
import { useOrganizationModal } from "./hooks/useOrganization";
|
||||||
import OrganizationModal from "./components/Organization/OrganizationModal";
|
import OrganizationModal from "./components/Organization/OrganizationModal";
|
||||||
import { useAuthModal } from "./hooks/useAuth";
|
import { useAuthModal, useModal } from "./hooks/useAuth";
|
||||||
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
||||||
|
import { ProjectModal } from "./components/Project/ManageProjectInfo";
|
||||||
|
|
||||||
const ModalProvider = () => {
|
const ModalProvider = () => {
|
||||||
const { isOpen, onClose } = useOrganizationModal();
|
const { isOpen, onClose } = useOrganizationModal();
|
||||||
const { isOpen: isAuthOpen } = useAuthModal();
|
const { isOpen: isAuthOpen } = useAuthModal();
|
||||||
|
const {isOpen:isOpenProject} = useModal("ManageProject")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && <OrganizationModal />}
|
{isOpen && <OrganizationModal />}
|
||||||
{isAuthOpen && <SwitchTenant />}
|
{isAuthOpen && <SwitchTenant />}
|
||||||
|
{isOpenProject && <ProjectModal/>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -123,15 +123,19 @@ const AttendLogs = ({ Id }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<div className="text-start">
|
<div className="mb-3">
|
||||||
|
<h5 className="mb-4">Attendance Logs</h5>
|
||||||
{logs && !loading && (
|
{logs && !loading && (
|
||||||
<p>
|
<p className="mb-0 text-start">
|
||||||
Attendance logs for{" "}
|
Showing logs for{" "}
|
||||||
{logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName}{" "}
|
<strong>
|
||||||
on {formatUTCToLocalTime(logs[0]?.activityTime)}
|
{logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName}
|
||||||
|
</strong>{" "}
|
||||||
|
on <strong>{formatUTCToLocalTime(logs[0]?.activityTime)}</strong>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <p>Loading..</p>}
|
{loading && <p>Loading..</p>}
|
||||||
{logs && logs.length > 0 && (
|
{logs && logs.length > 0 && (
|
||||||
<>
|
<>
|
||||||
@ -142,9 +146,9 @@ const AttendLogs = ({ Id }) => {
|
|||||||
<table className="table table-sm mb-0">
|
<table className="table table-sm mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Activity</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
<th>Activity</th>
|
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Recored By</th>
|
<th>Recored By</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
@ -156,11 +160,16 @@ const AttendLogs = ({ Id }) => {
|
|||||||
.sort((a, b) => b.id - a.id)
|
.sort((a, b) => b.id - a.id)
|
||||||
.map((log, index) => (
|
.map((log, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{formatUTCToLocalTime(log.activityTime)}</td>
|
|
||||||
<td>{convertShortTime(log.activityTime)}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{whichActivityPerform(log.activity, log.activityTime)}
|
{whichActivityPerform(log.activity, log.activityTime)}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="py-2">
|
||||||
|
{formatUTCToLocalTime(log.activityTime)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{convertShortTime(log.activityTime)}</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{log?.latitude != 0 ? (
|
{log?.latitude != 0 ? (
|
||||||
<i
|
<i
|
||||||
@ -179,9 +188,8 @@ const AttendLogs = ({ Id }) => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-wrap">
|
<td className="text-wrap">
|
||||||
{`${log?.updatedByEmployee?.firstName ?? ""} ${
|
{`${log?.updatedByEmployee?.firstName ?? ""} ${log?.updatedByEmployee?.lastName ?? ""
|
||||||
log?.updatedByEmployee?.lastName ?? ""
|
}`}
|
||||||
}`}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="text-wrap" colSpan={3}>
|
<td className="text-wrap" colSpan={3}>
|
||||||
{log?.comment?.length > 50
|
{log?.comment?.length > 50
|
||||||
|
|||||||
@ -11,8 +11,18 @@ import { useSelector } from "react-redux";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
|
|
||||||
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, includeInactive, date }) => {
|
const Attendance = ({
|
||||||
|
getRole,
|
||||||
|
handleModalData,
|
||||||
|
searchTerm,
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
includeInactive,
|
||||||
|
date,
|
||||||
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -23,12 +33,12 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
|||||||
attendance,
|
attendance,
|
||||||
loading: attLoading,
|
loading: attLoading,
|
||||||
recall: attrecall,
|
recall: attrecall,
|
||||||
isFetching
|
isFetching,
|
||||||
} = useAttendance(selectedProject, organizationId, includeInactive, date);
|
} = useAttendance(selectedProject, organizationId, includeInactive, date);
|
||||||
const filteredAttendance = ShowPending
|
const filteredAttendance = ShowPending
|
||||||
? attendance?.filter(
|
? attendance?.filter(
|
||||||
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
||||||
)
|
)
|
||||||
: attendance;
|
: attendance;
|
||||||
|
|
||||||
const attendanceList = Array.isArray(filteredAttendance)
|
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
|
// Reset pagination when the filter or search term changes
|
||||||
useEffect(() => {
|
useEffect(() => {}, [finalFilteredData]);
|
||||||
}, [finalFilteredData]);
|
|
||||||
|
|
||||||
|
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
(msg) => {
|
(msg) => {
|
||||||
if (selectedProject == msg.projectId) {
|
if (selectedProject == msg.projectId) {
|
||||||
queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
|
queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
|
||||||
if (!oldData) {
|
if (!oldData) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] })
|
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||||
};
|
}
|
||||||
return oldData.map((record) =>
|
return oldData.map((record) =>
|
||||||
record.employeeId === msg.response.employeeId ? { ...record, ...msg.response } : record
|
record.employeeId === msg.response.employeeId
|
||||||
|
? { ...record, ...msg.response }
|
||||||
|
: record
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -110,175 +120,143 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div>
|
||||||
className="table-responsive text-nowrap h-100"
|
<div className="table-responsive text-nowrap ">
|
||||||
style={{ minHeight: "200px" }} // Ensures fixed height
|
<div className="d-flex justify-content-between align-items-center py-2">
|
||||||
>
|
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
||||||
<div className="d-flex text-start align-items-center py-2">
|
<div className="form-check form-switch text-start m-0 ms-5">
|
||||||
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
<input
|
||||||
<div className="form-check form-switch text-start m-0 ms-5">
|
type="checkbox"
|
||||||
<input
|
className="form-check-input"
|
||||||
type="checkbox"
|
role="switch"
|
||||||
className="form-check-input"
|
id="inactiveEmployeesCheckbox"
|
||||||
role="switch"
|
disabled={isFetching}
|
||||||
id="inactiveEmployeesCheckbox"
|
checked={ShowPending}
|
||||||
disabled={isFetching}
|
onChange={(e) => setShowPending(e.target.checked)}
|
||||||
checked={ShowPending}
|
/>
|
||||||
onChange={(e) => setShowPending(e.target.checked)}
|
<label className="form-check-label ms-0">Show Pending</label>
|
||||||
/>
|
</div>
|
||||||
<label className="form-check-label ms-0">Show Pending</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{attLoading ? (
|
||||||
{attLoading ? (
|
<div
|
||||||
<div>Loading...</div>
|
className="d-flex justify-content-center align-items-center"
|
||||||
) : currentItems?.length > 0 ? (
|
style={{ minHeight: "70vh" }}
|
||||||
<>
|
>
|
||||||
<table className="table ">
|
<SpinnerLoader />
|
||||||
<thead>
|
</div>
|
||||||
<tr className="border-top-1">
|
) : currentItems?.length > 0 ? (
|
||||||
<th colSpan={2}>Name</th>
|
<>
|
||||||
<th>Role</th>
|
<table className="table table-hover ">
|
||||||
<th>Organization</th>
|
<thead>
|
||||||
<th>
|
<tr className="border-top-1">
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i>
|
<th colSpan={2}>Name</th>
|
||||||
Check-In
|
<th className="text-start actions-col text-center">Role</th>
|
||||||
</th>
|
{/* <th>Organization</th> */}
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
<i className="bx bxs-down-arrow-alt text-success"></i>
|
||||||
</th>
|
Check-In
|
||||||
<th>Actions</th>
|
</th>
|
||||||
</tr>
|
<th>
|
||||||
</thead>
|
<i className="bx bxs-up-arrow-alt text-danger"></i>
|
||||||
<tbody className="table-border-bottom-0 ">
|
Check-Out
|
||||||
{currentItems &&
|
</th>
|
||||||
currentItems
|
<th className="actions-col">Actions</th>
|
||||||
.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>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
</thead>
|
||||||
</tbody>
|
<tbody className="table-border-bottom-0 ">
|
||||||
</table>
|
{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 && (
|
<td>
|
||||||
<nav aria-label="Page ">
|
{item.checkInTime
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
? convertShortTime(item.checkInTime)
|
||||||
<li
|
: "--"}
|
||||||
className={`page-item ${currentPage === 1 ? "disabled" : ""
|
</td>
|
||||||
}`}
|
<td>
|
||||||
>
|
{item.checkOutTime
|
||||||
<button
|
? convertShortTime(item.checkOutTime)
|
||||||
className="page-link btn-xs"
|
: "--"}
|
||||||
onClick={() => paginate(currentPage - 1)}
|
</td>
|
||||||
>
|
|
||||||
«
|
<td className="text-center actions-col">
|
||||||
</button>
|
<RenderAttendanceStatus
|
||||||
</li>
|
attendanceData={item}
|
||||||
{[...Array(totalPages)].map((_, index) => (
|
handleModalData={handleModalData}
|
||||||
<li
|
Tab={1}
|
||||||
key={index}
|
currentDate={null}
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
/>
|
||||||
}`}
|
</td>
|
||||||
>
|
</tr>
|
||||||
<button
|
))}
|
||||||
className="page-link "
|
{!attendance && (
|
||||||
onClick={() => paginate(index + 1)}
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={7}
|
||||||
|
className="text-center text-secondary"
|
||||||
|
style={{ height: "200px" }}
|
||||||
>
|
>
|
||||||
{index + 1}
|
No employees assigned to the project!
|
||||||
</button>
|
</td>
|
||||||
</li>
|
</tr>
|
||||||
))}
|
)}
|
||||||
<li
|
</tbody>
|
||||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
</table>
|
||||||
}`}
|
</>
|
||||||
>
|
) : (
|
||||||
<button
|
<div
|
||||||
className="page-link "
|
className="d-flex justify-content-center align-items-center text-muted"
|
||||||
onClick={() => paginate(currentPage + 1)}
|
style={{ height: "200px" }}
|
||||||
>
|
>
|
||||||
»
|
{searchTerm
|
||||||
</button>
|
? "No results found for your search."
|
||||||
</li>
|
: attendanceList.length === 0
|
||||||
</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."
|
? "No employees assigned to the project."
|
||||||
: "No pending records available."}
|
: "No pending records available."}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,16 +1,23 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../common/Avatar";
|
||||||
import { convertShortTime } from "../../utils/dateUtils";
|
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import RenderAttendanceStatus from "./RenderAttendanceStatus";
|
import RenderAttendanceStatus from "./RenderAttendanceStatus";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import DateRangePicker from "../common/DateRangePicker";
|
import DateRangePicker from "../common/DateRangePicker";
|
||||||
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
|
import {
|
||||||
|
clearCacheKey,
|
||||||
|
getCachedData,
|
||||||
|
useSelectedProject,
|
||||||
|
} from "../../slices/apiDataManager";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import AttendanceRepository from "../../repositories/AttendanceRepository";
|
import AttendanceRepository from "../../repositories/AttendanceRepository";
|
||||||
import { useAttendancesLogs } from "../../hooks/useAttendance";
|
import { useAttendancesLogs } from "../../hooks/useAttendance";
|
||||||
import { queryClient } from "../../layouts/AuthLayout";
|
import { queryClient } from "../../layouts/AuthLayout";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
|
|
||||||
const usePagination = (data, itemsPerPage) => {
|
const usePagination = (data, itemsPerPage) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@ -33,18 +40,16 @@ const usePagination = (data, itemsPerPage) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||||
// const selectedProject = useSelector(
|
|
||||||
// (store) => store.localVariables.projectId
|
|
||||||
// );
|
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showPending, setShowPending] = useState(false)
|
const [showPending, setShowPending] = useState(false);
|
||||||
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [processedData, setProcessedData] = useState([]);
|
const [processedData, setProcessedData] = useState([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
@ -84,59 +89,64 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
dateRange.endDate,
|
dateRange.endDate,
|
||||||
organizationId
|
organizationId
|
||||||
);
|
);
|
||||||
const filtering = (data) => {
|
const filtering = useCallback(
|
||||||
const filteredData = showPending
|
(dataToFilter) => {
|
||||||
? data.filter((item) => item.checkOutTime === null)
|
const filteredData = showPending
|
||||||
: data;
|
? dataToFilter.filter((item) => item.checkOutTime === null)
|
||||||
|
: dataToFilter;
|
||||||
|
|
||||||
const group1 = filteredData
|
const group1 = filteredData
|
||||||
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
|
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
|
||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
const group2 = filteredData
|
const group2 = filteredData
|
||||||
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
|
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
|
||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
const group3 = filteredData
|
const group3 = filteredData
|
||||||
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
|
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
|
||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
const group4 = filteredData.filter(
|
const group4 = filteredData.filter(
|
||||||
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
|
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
|
||||||
);
|
);
|
||||||
const group5 = filteredData
|
const group5 = filteredData
|
||||||
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
|
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
|
||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
const group6 = filteredData
|
const group6 = filteredData
|
||||||
.filter((d) => d.activity === 5)
|
.filter((d) => d.activity === 5)
|
||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
|
|
||||||
const sortedList = [
|
const sortedList = [
|
||||||
...group1,
|
...group1,
|
||||||
...group2,
|
...group2,
|
||||||
...group3,
|
...group3,
|
||||||
...group4,
|
...group4,
|
||||||
...group5,
|
...group5,
|
||||||
...group6,
|
...group6,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Group by date
|
// Group by date
|
||||||
const groupedByDate = sortedList.reduce((acc, item) => {
|
const groupedByDate = sortedList.reduce((acc, item) => {
|
||||||
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
||||||
if (date) {
|
if (date) {
|
||||||
acc[date] = acc[date] || [];
|
acc[date] = acc[date] || [];
|
||||||
acc[date].push(item);
|
acc[date].push(item);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const sortedDates = Object.keys(groupedByDate).sort(
|
const sortedDates = Object.keys(groupedByDate).sort(
|
||||||
(a, b) => new Date(b) - new Date(a)
|
(a, b) => new Date(b) - new Date(a)
|
||||||
);
|
);
|
||||||
|
|
||||||
const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
|
const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
|
||||||
setProcessedData(finalData);
|
setProcessedData(finalData);
|
||||||
};
|
},
|
||||||
|
[showPending]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
filtering(data);
|
if (data?.length) {
|
||||||
|
filtering(data);
|
||||||
|
}
|
||||||
}, [data, showPending]);
|
}, [data, showPending]);
|
||||||
|
|
||||||
// New useEffect to handle search filtering
|
// New useEffect to handle search filtering
|
||||||
@ -151,33 +161,6 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
});
|
});
|
||||||
}, [processedData, searchTerm]);
|
}, [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 {
|
const {
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
@ -235,7 +218,7 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
// })
|
// })
|
||||||
// );
|
// );
|
||||||
|
|
||||||
refetch()
|
refetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectedProject, dateRange, data, refetch]
|
[selectedProject, dateRange, data, refetch]
|
||||||
@ -249,50 +232,42 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dataTables_length text-start py-2 d-flex justify-content-between"
|
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
||||||
id="DataTables_Table_0_length"
|
id="DataTables_Table_0_length"
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-center my-0 ">
|
<div className=" col-12">
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
onRangeChange={setDateRange}
|
onRangeChange={setDateRange}
|
||||||
defaultStartDate={yesterday}
|
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>
|
</div>
|
||||||
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
|
<div className="table-responsive text-nowrap ">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
<div
|
||||||
<p className="text-secondary">Loading...</p>
|
className="d-flex justify-content-center align-items-center"
|
||||||
|
style={{ minHeight: "70vh" }}
|
||||||
|
>
|
||||||
|
<SpinnerLoader/>
|
||||||
</div>
|
</div>
|
||||||
) : filteredSearchData?.length > 0 ? (
|
) : filteredSearchData?.length > 0 ? (
|
||||||
<table className="table mb-0">
|
<table className="table mb-0 table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border-top-1" colSpan={2}>
|
<th className="border-top-1" colSpan={2}>
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="border-top-1">Date</th>
|
<th className="border-top-1">Date</th>
|
||||||
<th>Organization</th>
|
{/* <th>Organization</th> */}
|
||||||
|
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i> Check-In
|
<i className="bx bxs-down-arrow-alt text-success"></i>{" "}
|
||||||
|
Check-In
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
|
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
|
||||||
</th>
|
</th>
|
||||||
<th>Action</th>
|
<th className="actions-col">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -303,9 +278,9 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
const previousAttendance = arr[index - 1];
|
const previousAttendance = arr[index - 1];
|
||||||
const previousDate = previousAttendance
|
const previousDate = previousAttendance
|
||||||
? moment(
|
? moment(
|
||||||
previousAttendance.checkInTime ||
|
previousAttendance.checkInTime ||
|
||||||
previousAttendance.checkOutTime
|
previousAttendance.checkOutTime
|
||||||
).format("YYYY-MM-DD")
|
).format("YYYY-MM-DD")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!previousDate || currentDate !== previousDate) {
|
if (!previousDate || currentDate !== previousDate) {
|
||||||
@ -315,8 +290,8 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
className="table-row-header"
|
className="table-row-header"
|
||||||
>
|
>
|
||||||
<td colSpan={8} className="text-start">
|
<td colSpan={8} className="text-start">
|
||||||
<strong>
|
<strong className="d-inline-block my-1 ms-2">
|
||||||
{moment(currentDate).format("DD-MM-YYYY")}
|
{formatUTCToLocalTime(currentDate)}
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -331,7 +306,14 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
lastName={attendance.lastName}
|
lastName={attendance.lastName}
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<a href="#" className="text-heading text-truncate">
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/employee/${attendance.employeeId}?for=attendance`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="text-heading text-truncate cursor-pointer"
|
||||||
|
>
|
||||||
<span className="fw-normal">
|
<span className="fw-normal">
|
||||||
{attendance.firstName} {attendance.lastName}
|
{attendance.firstName} {attendance.lastName}
|
||||||
</span>
|
</span>
|
||||||
@ -344,14 +326,14 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
attendance.checkInTime || attendance.checkOutTime
|
attendance.checkInTime || attendance.checkOutTime
|
||||||
).format("DD-MMM-YYYY")}
|
).format("DD-MMM-YYYY")}
|
||||||
</td>
|
</td>
|
||||||
<td>{attendance.organizationName || "--"}</td>
|
{/* <td>{attendance.organizationName || "--"}</td> */}
|
||||||
<td>{convertShortTime(attendance.checkInTime)}</td>
|
<td>{convertShortTime(attendance.checkInTime)}</td>
|
||||||
<td>
|
<td>
|
||||||
{attendance.checkOutTime
|
{attendance.checkOutTime
|
||||||
? convertShortTime(attendance.checkOutTime)
|
? convertShortTime(attendance.checkOutTime)
|
||||||
: "--"}
|
: "--"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center">
|
<td className="text-center actions-col">
|
||||||
<RenderAttendanceStatus
|
<RenderAttendanceStatus
|
||||||
attendanceData={attendance}
|
attendanceData={attendance}
|
||||||
handleModalData={handleModalData}
|
handleModalData={handleModalData}
|
||||||
@ -366,7 +348,14 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
||||||
@ -378,45 +367,11 @@ const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filteredSearchData.length > ITEMS_PER_PAGE && (
|
{filteredSearchData.length > ITEMS_PER_PAGE && (
|
||||||
<nav aria-label="Page ">
|
<Pagination
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
currentPage={currentPage}
|
||||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
totalPages={totalPages}
|
||||||
<button
|
onPageChange={paginate}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -9,6 +9,7 @@ import showToast from "../../services/toastService";
|
|||||||
import { checkIfCurrentDate } from "../../utils/dateUtils";
|
import { checkIfCurrentDate } from "../../utils/dateUtils";
|
||||||
import { useMarkAttendance } from "../../hooks/useAttendance";
|
import { useMarkAttendance } from "../../hooks/useAttendance";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
|
|
||||||
const createSchema = (modeldata) => {
|
const createSchema = (modeldata) => {
|
||||||
return z
|
return z
|
||||||
@ -19,31 +20,36 @@ const createSchema = (modeldata) => {
|
|||||||
.max(200, "Description should be less than 200 characters")
|
.max(200, "Description should be less than 200 characters")
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.refine((data) => {
|
.refine(
|
||||||
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
|
(data) => {
|
||||||
const checkIn = new Date(modeldata.checkInTime);
|
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
|
||||||
const [time, modifier] = data.markTime.split(" ");
|
const checkIn = new Date(modeldata.checkInTime);
|
||||||
const [hourStr, minuteStr] = time.split(":");
|
const [time, modifier] = data.markTime.split(" ");
|
||||||
let hour = parseInt(hourStr, 10);
|
const [hourStr, minuteStr] = time.split(":");
|
||||||
const minute = parseInt(minuteStr, 10);
|
let hour = parseInt(hourStr, 10);
|
||||||
|
const minute = parseInt(minuteStr, 10);
|
||||||
|
|
||||||
if (modifier === "PM" && hour !== 12) hour += 12;
|
if (modifier === "PM" && hour !== 12) hour += 12;
|
||||||
if (modifier === "AM" && hour === 12) hour = 0;
|
if (modifier === "AM" && hour === 12) hour = 0;
|
||||||
|
|
||||||
const checkOut = new Date(checkIn);
|
const checkOut = new Date(checkIn);
|
||||||
checkOut.setHours(hour, minute, 0, 0);
|
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 CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||||
|
const [currentProject, setCurrentProject] = useState(null);
|
||||||
const projectId = useSelectedProject();
|
const projectId = useSelectedProject();
|
||||||
|
const { projectNames, loading } = useProjectName();
|
||||||
const { mutate: MarkAttendance } = useMarkAttendance();
|
const { mutate: MarkAttendance } = useMarkAttendance();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const coords = usePositionTracker();
|
const coords = usePositionTracker();
|
||||||
@ -95,17 +101,24 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
|||||||
closeModal();
|
closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId && projectNames) {
|
||||||
|
setCurrentProject(
|
||||||
|
projectNames?.find((project) => project.id === projectId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [projectNames, projectId, loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row p-2" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="col-12 d-flex justify-content-center">
|
<div className="col-12 d-flex justify-content-center mt-2">
|
||||||
<label className="fs-5 text-dark text-center">
|
<label className="fs-5 text-dark text-center">
|
||||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||||
? "Check-out :"
|
? `Check out for ${currentProject?.name}`
|
||||||
: "Check-in :"}
|
: `Check In for ${currentProject?.name}`}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="col-6 col-md-6 text-start">
|
<div className="col-6 col-md-6 text-start">
|
||||||
<label className="form-label" htmlFor="checkInDate">
|
<label className="form-label" htmlFor="checkInDate">
|
||||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||||
@ -207,7 +220,7 @@ export const Regularization = ({ modeldata, closeModal, handleSubmitForm }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row " onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="col-12 col-md-12">
|
<div className="col-12 col-md-12">
|
||||||
<p>Regularize Attendance</p>
|
<p>Regularize Attendance</p>
|
||||||
<label className="form-label" htmlFor="description">
|
<label className="form-label" htmlFor="description">
|
||||||
|
|||||||
@ -9,19 +9,34 @@ import usePagination from "../../hooks/usePagination";
|
|||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
|
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
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();
|
const queryClient = useQueryClient();
|
||||||
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const [regularizesList, setregularizedList] = useState([]);
|
const [regularizesList, setregularizedList] = useState([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
const { regularizes, loading, error, refetch } =
|
const { regularizes, loading, error, refetch } =
|
||||||
useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
|
useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
|
||||||
|
|
||||||
useEffect(() => {
|
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]);
|
}, [regularizes]);
|
||||||
|
|
||||||
|
|
||||||
const sortByName = (a, b) => {
|
const sortByName = (a, b) => {
|
||||||
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
|
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
|
||||||
const nameB = b.firstName.toLowerCase() + b.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);
|
return () => eventBus.off("employee", employeeHandler);
|
||||||
}, [employeeHandler]);
|
}, [employeeHandler]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
|
<div>
|
||||||
{loading ? (
|
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
|
||||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
{loading ? (
|
||||||
<p className="text-secondary">Loading...</p>
|
<div
|
||||||
</div>
|
className="d-flex justify-content-center align-items-center"
|
||||||
) : currentItems?.length > 0 ? (
|
style={{ minHeight: "70vh" }}
|
||||||
<table className="table mb-0">
|
>
|
||||||
<thead>
|
<SpinnerLoader/>
|
||||||
<tr>
|
</div>
|
||||||
<th colSpan={2}>Name</th>
|
) : currentItems?.length > 0 ? (
|
||||||
<th>Date</th>
|
<table className="table mb-0 table-hover">
|
||||||
<th>Organization</th>
|
<thead>
|
||||||
<th>
|
<tr>
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
<th colSpan={2}>Name</th>
|
||||||
</th>
|
<th>Date</th>
|
||||||
<th>
|
{/* <th>Organization</th> */}
|
||||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
<th>
|
||||||
</th>
|
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
||||||
<th>Action</th>
|
</th>
|
||||||
</tr>
|
<th>
|
||||||
</thead>
|
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||||
<tbody>
|
</th>
|
||||||
{currentItems?.map((att, index) => (
|
|
||||||
<tr key={index}>
|
<th>Request By</th>
|
||||||
<td colSpan={2}>
|
<th>Requested At</th>
|
||||||
<div className="d-flex justify-content-start align-items-center">
|
<th className="actions-col">Action</th>
|
||||||
<Avatar
|
</tr>
|
||||||
firstName={att.firstName}
|
</thead>
|
||||||
lastName={att.lastName}
|
<tbody>
|
||||||
/>
|
{currentItems?.map((att, index) => (
|
||||||
<div className="d-flex flex-column">
|
<tr key={index}>
|
||||||
<a href="#" className="text-heading text-truncate">
|
<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">
|
<span className="fw-normal">
|
||||||
{att.firstName} {att.lastName}
|
{att.firstName} {att.lastName}
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
|
||||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
|
||||||
|
|
||||||
<td>{att.organizationName || "--"}</td>
|
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||||
|
|
||||||
<td>{convertShortTime(att.checkInTime)}</td>
|
{/* <td>{att.organizationName || "--"}</td> */}
|
||||||
<td>
|
|
||||||
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
|
||||||
</td>
|
|
||||||
<td className="text-center ">
|
|
||||||
<RegularizationActions
|
|
||||||
attendanceData={att}
|
|
||||||
handleRequest={handleRequest}
|
|
||||||
refresh={refetch}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
) : (
|
<td>{convertShortTime(att.checkInTime)}</td>
|
||||||
<div
|
<td>
|
||||||
className="d-flex justify-content-center align-items-center"
|
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
||||||
style={{ height: "200px" }}
|
</td>
|
||||||
>
|
<td>
|
||||||
<span className="text-secondary">
|
{att.requestedBy
|
||||||
{searchTerm
|
? `${att.requestedBy?.firstName} ${att.requestedBy?.lastName}`
|
||||||
? "No results found for your search."
|
: "--"}
|
||||||
: "No Requests Found !"}
|
</td>
|
||||||
</span>
|
<td>
|
||||||
</div>
|
{att.requestedAt
|
||||||
)}
|
? moment(att.requestedAt).format("DD-MMM-YYYY")
|
||||||
{!loading && totalPages > 1 && (
|
: "--"}
|
||||||
<nav aria-label="Page ">
|
</td>
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
|
<td className="text-center ">
|
||||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
<RegularizationActions
|
||||||
<button
|
attendanceData={att}
|
||||||
className="page-link btn-xs"
|
handleRequest={handleRequest}
|
||||||
onClick={() => paginate(currentPage - 1)}
|
refresh={refetch}
|
||||||
>
|
/>
|
||||||
«
|
</td>
|
||||||
</button>
|
</tr>
|
||||||
</li>
|
))}
|
||||||
{[...Array(totalPages)].map((_, index) => (
|
</tbody>
|
||||||
<li
|
</table>
|
||||||
key={index}
|
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
) : (
|
||||||
}`}
|
<div
|
||||||
>
|
className="d-flex justify-content-center align-items-center"
|
||||||
<button
|
style={{ height: "200px" }}
|
||||||
className="page-link "
|
>
|
||||||
onClick={() => paginate(index + 1)}
|
<span className="text-secondary">
|
||||||
>
|
{searchTerm
|
||||||
{index + 1}
|
? "No results found for your search."
|
||||||
</button>
|
: "No Requests Found !"}
|
||||||
</li>
|
</span>
|
||||||
))}
|
</div>
|
||||||
<li
|
)}
|
||||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
</div>
|
||||||
}`}
|
{!loading && totalPages > 1 && (
|
||||||
>
|
<Pagination
|
||||||
<button
|
currentPage={currentPage}
|
||||||
className="page-link "
|
totalPages={totalPages}
|
||||||
onClick={() => paginate(currentPage + 1)}
|
onPageChange={paginate}
|
||||||
>
|
/>
|
||||||
»
|
)}
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</div>
|
</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 ApexChart from "../Charts/Circle";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
|
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
|
||||||
import { useSelectedProject } from "../../hooks/useSelectedProject"; // ✅ your custom hook
|
import { useSelectedProject } from "../../hooks/useSelectedProject";
|
||||||
|
|
||||||
const Attendance = () => {
|
const Attendance = () => {
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
const today = new Date().toISOString().split("T")[0];
|
||||||
const [selectedDate, setSelectedDate] = useState(today);
|
const [selectedDate, setSelectedDate] = useState(today);
|
||||||
|
|
||||||
// central project selection hook
|
|
||||||
const selectedProjectId = useSelectedProject()
|
const selectedProjectId = useSelectedProject()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -31,7 +30,7 @@ const selectedProjectId = useSelectedProject()
|
|||||||
<div className="card-header mb-1 pb-0">
|
<div className="card-header mb-1 pb-0">
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
||||||
<div className="card-title mb-0 text-start">
|
<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>
|
<p className="card-subtitle">Daily Attendance Data</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,7 +135,6 @@ const selectedProjectId = useSelectedProject()
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Details */}
|
|
||||||
{AttendanceData?.activeTab === "Details" && (
|
{AttendanceData?.activeTab === "Details" && (
|
||||||
<div className="table-responsive" style={{ maxHeight: "300px" }}>
|
<div className="table-responsive" style={{ maxHeight: "300px" }}>
|
||||||
<table className="table table-hover mb-0 text-start">
|
<table className="table table-hover mb-0 text-start">
|
||||||
|
|||||||
@ -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;
|
|
||||||
227
src/components/Dashboard/AttendanceOverview.jsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import ReactApexChart from "react-apexcharts";
|
||||||
|
import moment from "moment";
|
||||||
|
import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data";
|
||||||
|
import flatColors from "../Charts/flatColor";
|
||||||
|
import ChartSkeleton from "../Charts/Skelton";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
|
|
||||||
|
const formatDate_DayMonth = (dateStr) => moment(dateStr).format("DD MMM YY");
|
||||||
|
|
||||||
|
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 || [];
|
||||||
|
|
||||||
|
// Prepare data for chart and table
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Chart data
|
||||||
|
const chartSeries = roles.map((role) => ({
|
||||||
|
name: role,
|
||||||
|
data: tableData.map((row) => row[role]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Chart options
|
||||||
|
const chartOptions = {
|
||||||
|
chart: {
|
||||||
|
type: "bar",
|
||||||
|
stacked: true, // make false if you want side-by-side bars
|
||||||
|
height: 400,
|
||||||
|
toolbar: { show: false },
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
borderRadius: 4,
|
||||||
|
columnWidth: "55%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: tableData.map((row) => row.date),
|
||||||
|
labels: {
|
||||||
|
rotate: -45,
|
||||||
|
style: { fontSize: "12px" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
axisBorder: { show: true, color: "#78909C" },
|
||||||
|
axisTicks: { show: true, color: "#78909C" },
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: "bottom",
|
||||||
|
horizontalAlign: "center",
|
||||||
|
fontSize: "12px",
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
borderColor: "#e0e0e0",
|
||||||
|
strokeDashArray: 3,
|
||||||
|
},
|
||||||
|
fill: { opacity: 1 },
|
||||||
|
colors: roles.map((_, i) => flatColors[i % flatColors.length]),
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: (val) => `${val} present`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-4 rounded shadow d-flex flex-column h-100">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="row mb-3 align-items-center">
|
||||||
|
<div className="col-md-6 text-start mb-6">
|
||||||
|
<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">
|
||||||
|
{/* Day range dropdown */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* View toggle buttons */}
|
||||||
|
<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 */}
|
||||||
|
<div className="flex-grow-1 d-flex align-items-center justify-content-center ">
|
||||||
|
{/* {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>
|
||||||
|
) : !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" ? ( */}
|
||||||
|
{isLoading ? (
|
||||||
|
<SpinnerLoader />
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-danger">{error}</p>
|
||||||
|
) : attendanceOverviewData.length === 0 ||
|
||||||
|
attendanceOverviewData.every((item) => item.present === 0) ? (
|
||||||
|
<div className="text-center text-dark">No data found</div>
|
||||||
|
) : view === "chart" ? (
|
||||||
|
<div className="w-100">
|
||||||
|
<ReactApexChart
|
||||||
|
options={chartOptions}
|
||||||
|
series={chartSeries}
|
||||||
|
type="bar"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</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" }}>
|
||||||
|
{moment(date, "DD MMM YY").format("DD MMM")}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{roles.map((role) => (
|
||||||
|
<tr key={role}>
|
||||||
|
<td className="fw-medium text-start">{role}</td>
|
||||||
|
{tableData.map((row, idx) => {
|
||||||
|
const value = row[role];
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={idx}
|
||||||
|
style={value > 0 ? { backgroundColor: "#d5d5d5" } : {}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttendanceOverview;
|
||||||
@ -1,69 +1,76 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
// import {
|
||||||
useDashboardProjectsCardData,
|
// useDashboardProjectsCardData,
|
||||||
useDashboardTeamsCardData,
|
// useDashboardTeamsCardData,
|
||||||
useDashboardTasksCardData,
|
// useDashboardTasksCardData,
|
||||||
useAttendanceOverviewData
|
// useAttendanceOverviewData
|
||||||
} from "../../hooks/useDashboard_Data";
|
// } from "../../hooks/useDashboard_Data";
|
||||||
|
|
||||||
import Projects from "./Projects";
|
// import Projects from "./Projects";
|
||||||
import Teams from "./Teams";
|
// import Teams from "./Teams";
|
||||||
import TasksCard from "./Tasks";
|
// import TasksCard from "./Tasks";
|
||||||
import ProjectCompletionChart from "./ProjectCompletionChart";
|
// import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||||
import ProjectProgressChart from "./ProjectProgressChart";
|
// import ProjectProgressChart from "./ProjectProgressChart";
|
||||||
import ProjectOverview from "../Project/ProjectOverview";
|
// import ProjectOverview from "../Project/ProjectOverview";
|
||||||
import AttendanceOverview from "./AttendanceChart";
|
import AttendanceOverview from "./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 Dashboard = () => {
|
||||||
const { projectsCardData } = useDashboardProjectsCardData();
|
// const { projectsCardData } = useDashboardProjectsCardData();
|
||||||
const { teamsCardData } = useDashboardTeamsCardData();
|
// const { teamsCardData } = useDashboardTeamsCardData();
|
||||||
const { tasksCardData } = useDashboardTasksCardData();
|
// const { tasksCardData } = useDashboardTasksCardData();
|
||||||
|
|
||||||
// Get the selected project ID from Redux store
|
// Get the selected project ID from Redux store
|
||||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||||
const isAllProjectsSelected = projectId === null;
|
const isAllProjectsSelected = projectId === null;
|
||||||
|
|
||||||
return (
|
const isViewExpense = useHasAnyPermission(
|
||||||
<div className="container-fluid mt-5">
|
VIEW_ALL_EXPNESE,
|
||||||
<div className="row gy-4">
|
APPROVE_EXPENSE,
|
||||||
{isAllProjectsSelected && (
|
EXPENSE_MANAGE
|
||||||
<div className="col-sm-6 col-lg-4">
|
);
|
||||||
<Projects projectsCardData={projectsCardData} />
|
return (
|
||||||
</div>
|
<div className="container-fluid py-5">
|
||||||
)}
|
{isViewExpense && (
|
||||||
|
<div className="row mb-6 g-6">
|
||||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
<div className="col-12 col-xl-8">
|
||||||
<Teams teamsCardData={teamsCardData} />
|
<div className="card h-100">
|
||||||
</div>
|
<ExpenseAnalysis />
|
||||||
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-xl-4 col-md-6">
|
||||||
|
<div className="card h-100">
|
||||||
|
<ExpenseStatus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
{!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;
|
||||||
175
src/components/Dashboard/ExpenseAnalysis.jsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
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";
|
||||||
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
|
|
||||||
|
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={{ minHeight: "50vh" }}
|
||||||
|
>
|
||||||
|
<SpinnerLoader />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && report.length === 0 && (
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-center align-items-center text-muted"
|
||||||
|
style={{ height: "300px" }}
|
||||||
|
>
|
||||||
|
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;
|
||||||
165
src/components/Dashboard/ExpenseByProject.jsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
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";
|
||||||
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
|
|
||||||
|
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 = ExpenseCategories.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 h-100 rounded ">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3 mt-1">
|
||||||
|
<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 key={cat.id}>
|
||||||
|
<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 ? (
|
||||||
|
<div className="d-flex justify-content-center align-items-center py-5">
|
||||||
|
<SpinnerLoader />
|
||||||
|
</div>
|
||||||
|
) : !expenseApiData || expenseApiData.length === 0 ? (
|
||||||
|
<div className="text-center text-muted py-12">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]);
|
}, [selectedBucket, employeesList]);
|
||||||
|
|
||||||
const { mutate: AssignEmployee, isPending } = useAssignEmpToBucket(() =>
|
const { mutate: AssignEmployee, isPending } = useAssignEmpToBucket(() =>{
|
||||||
|
setSelectedEmployees([])
|
||||||
handleClose()
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
<div className="d-flex justify-content-between align-items-center mt-2 h-25" >
|
||||||
<p className="m-0 fw-normal">Add Employee</p>
|
<p className="m-0 fw-normal">Add Employee</p>
|
||||||
<div className="px-1">
|
<div className="px-1">
|
||||||
<input
|
<input
|
||||||
@ -87,7 +87,7 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="table-responsive px-1 my-1 px-sm-0">
|
<div className="table-responsive px-1 my-1 px-sm-0" style={{maxHeight:'200px'}}>
|
||||||
<table className="table align-middle mb-0">
|
<table className="table align-middle mb-0">
|
||||||
<thead className="table-light">
|
<thead className="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import Pagination from "../common/Pagination";
|
|||||||
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
|
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
|
||||||
import { useActiveInActiveContact } from "../../hooks/useDirectory";
|
import { useActiveInActiveContact } from "../../hooks/useDirectory";
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
import ConfirmModal from "../common/ConfirmModal";
|
||||||
|
import Loader from "../common/Loader";
|
||||||
|
|
||||||
const ListViewContact = ({ data, Pagination }) => {
|
const ListViewContact = ({ data, Pagination, isLoading }) => {
|
||||||
const { showActive, setManageContact, setContactOpen } =
|
const { showActive, setManageContact, setContactOpen } =
|
||||||
useDirectoryContext();
|
useDirectoryContext();
|
||||||
const [deleteContact, setDeleteContact] = useState({
|
const [deleteContact, setDeleteContact] = useState({
|
||||||
@ -85,6 +86,8 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
ActiveInActive({ contactId: contactId, contactStatus: !showActive });
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@ -97,104 +100,96 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
paramData={deleteContact.contactId}
|
paramData={deleteContact.contactId}
|
||||||
isOpen={deleteContact.Open}
|
isOpen={deleteContact.Open}
|
||||||
/>
|
/>
|
||||||
<div className="card ">
|
<div className="card page-min-h">
|
||||||
<div
|
<div
|
||||||
className="card-datatable table-responsive"
|
className="card-datatable table-responsive"
|
||||||
id="horizontal-example"
|
id="horizontal-example"
|
||||||
>
|
>
|
||||||
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
|
||||||
<table className="table dataTable text-nowrap">
|
{data && (
|
||||||
<thead>
|
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||||
<tr className="table_header_border">
|
<table className="table dataTable text-nowrap">
|
||||||
{contactList?.map((col) => (
|
<thead>
|
||||||
<th key={col.key} className={col.align}>
|
<tr className="table_header_border">
|
||||||
{col.label}
|
{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>
|
||||||
))}
|
</tr>
|
||||||
<th className="sticky-action-column bg-white text-center">
|
</thead>
|
||||||
Action
|
<tbody>
|
||||||
</th>
|
{Array.isArray(data) && data.length > 0 && (
|
||||||
</tr>
|
data.map((row, i) => (
|
||||||
</thead>
|
<tr
|
||||||
<tbody >
|
key={i}
|
||||||
{Array.isArray(data) && data.length > 0 ? (
|
style={{
|
||||||
data.map((row, i) => (
|
background: `${!showActive ? "#f8f6f6" : ""}`,
|
||||||
<tr
|
}}
|
||||||
key={i}
|
>
|
||||||
style={{ background: `${!showActive ? "#f8f6f6" : ""}` }}
|
{contactList.map((col) => (
|
||||||
>
|
<td key={col.key} className={col.align}>
|
||||||
{contactList.map((col) => (
|
{col.getValue(row)}
|
||||||
<td key={col.key} className={col.align}>
|
</td>
|
||||||
{col.getValue(row)}
|
))}
|
||||||
</td>
|
<td className="text-center">
|
||||||
))}
|
{showActive ? (
|
||||||
<td className="text-center">
|
<div className="d-flex justify-content-center gap-2">
|
||||||
{showActive ? (
|
<i
|
||||||
<div className="d-flex justify-content-center gap-2">
|
className="bx bx-show text-primary cursor-pointer"
|
||||||
<i
|
onClick={() =>
|
||||||
className="bx bx-show text-primary cursor-pointer"
|
setContactOpen({ contact: row, Open: true })
|
||||||
onClick={() =>
|
}
|
||||||
setContactOpen({ contact: row, Open: true })
|
></i>
|
||||||
}
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<i
|
<i
|
||||||
className="bx bx-edit text-secondary cursor-pointer"
|
className="bx bx-edit text-secondary cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setManageContact({
|
setManageContact({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
contactId: row.id,
|
contactId: row.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
|
<i
|
||||||
|
className="bx bx-trash text-danger cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setDeleteContact({
|
||||||
|
contactId: row.id,
|
||||||
|
Open: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<i
|
<i
|
||||||
className="bx bx-trash text-danger cursor-pointer"
|
className={`bx ${isPending && activeContact === row.id
|
||||||
onClick={() =>
|
|
||||||
setDeleteContact({
|
|
||||||
contactId: row.id,
|
|
||||||
Open: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<i
|
|
||||||
className={`bx ${
|
|
||||||
isPending && activeContact === row.id
|
|
||||||
? "bx-loader-alt bx-spin"
|
? "bx-loader-alt bx-spin"
|
||||||
: "bx-recycle"
|
: "bx-recycle"
|
||||||
} me-1 text-primary cursor-pointer`}
|
} me-1 text-primary cursor-pointer`}
|
||||||
title="Restore"
|
title="Restore"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveContact(row.id);
|
setActiveContact(row.id);
|
||||||
handleActiveInactive(row.id);
|
handleActiveInactive(row.id);
|
||||||
}}
|
}}
|
||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
) : (
|
)}
|
||||||
<tr style={{ height: "200px" }}>
|
</tbody>
|
||||||
<td
|
</table>
|
||||||
colSpan={contactList.length + 1}
|
</div>
|
||||||
className="text-center align-middle"
|
)}
|
||||||
>
|
|
||||||
No contacts found
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{Pagination && (
|
|
||||||
<div className="d-flex justify-content-start">
|
|
||||||
{Pagination}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{Pagination && (
|
||||||
|
<div className="d-flex justify-content-start">{Pagination}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import Label from "../common/Label";
|
|||||||
const ManageContact = ({ contactId, closeModal }) => {
|
const ManageContact = ({ contactId, closeModal }) => {
|
||||||
// fetch master data
|
// fetch master data
|
||||||
const { buckets, loading: bucketsLoaging } = useBuckets();
|
const { buckets, loading: bucketsLoaging } = useBuckets();
|
||||||
const { projects, loading: projectLoading } = useProjects();
|
const { data: projects, loading: projectLoading } = useProjects();
|
||||||
const { contactCategory, loading: contactCategoryLoading } =
|
const { contactCategory, loading: contactCategoryLoading } =
|
||||||
useContactCategory();
|
useContactCategory();
|
||||||
const { organizationList } = useOrganization();
|
const { organizationList } = useOrganization();
|
||||||
@ -205,13 +205,14 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
<Label htmlFor={"organization"} required>
|
<Label htmlFor={"organization"} required>
|
||||||
Organization
|
Organization
|
||||||
</Label>
|
</Label>
|
||||||
<InputSuggestions
|
<InputSuggestions
|
||||||
organizationList={organizationList}
|
organizationList={organizationList}
|
||||||
value={watch("organization") || ""}
|
value={watch("organization") || ""}
|
||||||
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
|
onChange={(val) =>
|
||||||
error={errors.organization?.message}
|
setValue("organization", val, { shouldValidate: true })
|
||||||
/>
|
}
|
||||||
|
error={errors.organization?.message}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -394,6 +395,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
IsLoading={projectLoading}
|
IsLoading={projectLoading}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{errors.projectIds && (
|
{errors.projectIds && (
|
||||||
<small className="danger-text">{errors.projectIds.message}</small>
|
<small className="danger-text">{errors.projectIds.message}</small>
|
||||||
@ -408,6 +410,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
label="Tags"
|
label="Tags"
|
||||||
options={contactTags}
|
options={contactTags}
|
||||||
isRequired={true}
|
isRequired={true}
|
||||||
|
require
|
||||||
/>
|
/>
|
||||||
{errors.tags && (
|
{errors.tags && (
|
||||||
<small className="danger-text">{errors.tags.message}</small>
|
<small className="danger-text">{errors.tags.message}</small>
|
||||||
@ -417,7 +420,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
{/* Buckets */}
|
{/* Buckets */}
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-12 mt-1 text-start">
|
<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">
|
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
||||||
{bucketsLoaging && <p>Loading...</p>}
|
{bucketsLoaging && <p>Loading...</p>}
|
||||||
{buckets?.map((item) => (
|
{buckets?.map((item) => (
|
||||||
@ -450,7 +453,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Address + Description */}
|
{/* Address + Description */}
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start mb-2">
|
||||||
<label className="form-label">Address</label>
|
<label className="form-label">Address</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@ -459,7 +462,7 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start">
|
||||||
<label className="form-label">Description</label>
|
<Label required>Description</Label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
rows="2"
|
rows="2"
|
||||||
@ -479,10 +482,13 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</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"}
|
{isPending ? "Please Wait..." : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</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 { useDocumentFilterEntities } from "../../hooks/useDocument";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -9,198 +9,240 @@ import {
|
|||||||
import { DateRangePicker1 } from "../common/DateRangePicker";
|
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
const DocumentFilterPanel = forwardRef(
|
||||||
const [resetKey, setResetKey] = useState(0);
|
({ entityTypeId, onApply, setFilterdata }, ref) => {
|
||||||
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
const { status } = useParams();
|
||||||
|
|
||||||
const { data, isError, isLoading, error } =
|
const { data, isError, isLoading, error } =
|
||||||
useDocumentFilterEntities(entityTypeId);
|
useDocumentFilterEntities(entityTypeId);
|
||||||
|
|
||||||
const methods = useForm({
|
useEffect(() => {
|
||||||
resolver: zodResolver(DocumentFilterSchema),
|
return () => {
|
||||||
defaultValues: DocumentFilterDefaultValues,
|
closePanel();
|
||||||
});
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { handleSubmit, reset, setValue, watch } = methods;
|
//changes
|
||||||
|
|
||||||
// Watch values from form
|
const dynamicDocumentFilterDefaultValues = useMemo(() => {
|
||||||
const isUploadedAt = watch("isUploadedAt");
|
return {
|
||||||
const isVerified = watch("isVerified");
|
...DocumentFilterDefaultValues,
|
||||||
|
uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [],
|
||||||
|
documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [],
|
||||||
|
documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [],
|
||||||
|
documentTagIds: DocumentFilterDefaultValues.documentTagIds || [],
|
||||||
|
startDate: DocumentFilterDefaultValues.startDate,
|
||||||
|
endDate: DocumentFilterDefaultValues.endDate,
|
||||||
|
};
|
||||||
|
|
||||||
// Close the offcanvas (bootstrap specific)
|
}, [status]);
|
||||||
const closePanel = () => {
|
|
||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (values) => {
|
const methods = useForm({
|
||||||
onApply({
|
resolver: zodResolver(DocumentFilterSchema),
|
||||||
...values,
|
defaultValues: dynamicDocumentFilterDefaultValues,
|
||||||
startDate: values.startDate
|
|
||||||
? moment.utc(values.startDate, "DD-MM-YYYY").toISOString()
|
|
||||||
: null,
|
|
||||||
endDate: values.endDate
|
|
||||||
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
|
|
||||||
: null,
|
|
||||||
});
|
});
|
||||||
closePanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClear = () => {
|
const { handleSubmit, reset, setValue, watch } = methods;
|
||||||
reset(DocumentFilterDefaultValues);
|
|
||||||
setResetKey((prev) => prev + 1);
|
|
||||||
onApply(DocumentFilterDefaultValues);
|
|
||||||
closePanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
// Watch values from form
|
||||||
if (isError)
|
const isUploadedAt = watch("isUploadedAt");
|
||||||
return <div>Error: {error?.message || "Something went wrong!"}</div>;
|
const isVerified = watch("isVerified");
|
||||||
|
|
||||||
const {
|
// Close the offcanvas (bootstrap specific)
|
||||||
uploadedBy = [],
|
const closePanel = () => {
|
||||||
documentCategory = [],
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
documentType = [],
|
};
|
||||||
documentTag = [],
|
|
||||||
} = data?.data || {};
|
|
||||||
|
|
||||||
return (
|
useImperativeHandle(ref, () => ({
|
||||||
<FormProvider {...methods}>
|
resetFieldValue: (name, value) => {
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
if (value !== undefined) {
|
||||||
{/* Date Range Section */}
|
setValue(name, value);
|
||||||
<div className="mb-2">
|
} else {
|
||||||
<div className="text-start d-flex align-items-center my-1">
|
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
|
||||||
<label className="form-label me-2 my-0">Choose Date:</label>
|
}
|
||||||
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
|
},
|
||||||
<button
|
getValues: methods.getValues, // optional, to read current filter state
|
||||||
type="button"
|
}));
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
|
||||||
isUploadedAt ? "active btn-secondary text-white" : ""
|
//changes
|
||||||
}`}
|
useEffect(() => {
|
||||||
onClick={() => setValue("isUploadedAt", true)}
|
if (data && setFilterdata) {
|
||||||
>
|
setFilterdata(data);
|
||||||
Uploaded On
|
}
|
||||||
</button>
|
}, [data, setFilterdata]);
|
||||||
<button
|
|
||||||
type="button"
|
const onSubmit = (values) => {
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
onApply({
|
||||||
!isUploadedAt ? "active btn-secondary text-white" : ""
|
...values,
|
||||||
}`}
|
startDate: values.startDate
|
||||||
onClick={() => setValue("isUploadedAt", false)}
|
? moment.utc(values.startDate, "DD-MM-YYYY").toISOString()
|
||||||
>
|
: null,
|
||||||
Updated On
|
endDate: values.endDate
|
||||||
</button>
|
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
// closePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClear = () => {
|
||||||
|
reset(DocumentFilterDefaultValues);
|
||||||
|
setResetKey((prev) => prev + 1);
|
||||||
|
onApply(DocumentFilterDefaultValues);
|
||||||
|
// closePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (isError)
|
||||||
|
return <div>Error: {error?.message || "Something went wrong!"}</div>;
|
||||||
|
|
||||||
|
const {
|
||||||
|
uploadedBy = [],
|
||||||
|
documentCategory = [],
|
||||||
|
documentType = [],
|
||||||
|
documentTag = [],
|
||||||
|
} = data?.data || {};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{/* Date Range Section */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="text-start d-flex align-items-center my-1">
|
||||||
|
<label className="form-label me-2 my-0">Choose Date:</label>
|
||||||
|
<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" : ""
|
||||||
|
}`}
|
||||||
|
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" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setValue("isUploadedAt", false)}
|
||||||
|
>
|
||||||
|
Updated On
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DateRangePicker1
|
||||||
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
|
startField="startDate"
|
||||||
|
endField="endDate"
|
||||||
|
defaultRange={false}
|
||||||
|
resetSignal={resetKey}
|
||||||
|
maxDate={new Date()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dropdown Filters */}
|
||||||
|
<div className="row g-2 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="uploadedByIds"
|
||||||
|
label="Uploaded By:"
|
||||||
|
options={uploadedBy}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="documentCategoryIds"
|
||||||
|
label="Document Category:"
|
||||||
|
options={documentCategory}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="documentTypeIds"
|
||||||
|
label="Document Type:"
|
||||||
|
options={documentType}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="documentTagIds"
|
||||||
|
label="Tags:"
|
||||||
|
options={documentTag}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Filter */}
|
||||||
|
<div className="text-start my-2">
|
||||||
|
<label className="form-label d-block mb-2">Choose Status:</label>
|
||||||
|
<div className="d-flex gap-4">
|
||||||
|
<label className="switch switch-sm">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="switch-input"
|
||||||
|
name="isVerified"
|
||||||
|
checked={isVerified === null}
|
||||||
|
onChange={() => setValue("isVerified", null)}
|
||||||
|
/>
|
||||||
|
<span className="switch-toggle-slider">
|
||||||
|
<span className="switch-on"></span>
|
||||||
|
<span className="switch-off"></span>
|
||||||
|
</span>
|
||||||
|
<span className="switch-label">All</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="switch switch-sm">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="switch-input"
|
||||||
|
name="isVerified"
|
||||||
|
checked={isVerified === true}
|
||||||
|
onChange={() => setValue("isVerified", true)}
|
||||||
|
/>
|
||||||
|
<span className="switch-toggle-slider">
|
||||||
|
<span className="switch-on"></span>
|
||||||
|
<span className="switch-off"></span>
|
||||||
|
</span>
|
||||||
|
<span className="switch-label">Verified</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="switch switch-sm">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="switch-input"
|
||||||
|
name="isVerified"
|
||||||
|
checked={isVerified === false}
|
||||||
|
onChange={() => setValue("isVerified", false)}
|
||||||
|
/>
|
||||||
|
<span className="switch-toggle-slider">
|
||||||
|
<span className="switch-on"></span>
|
||||||
|
<span className="switch-off"></span>
|
||||||
|
</span>
|
||||||
|
<span className="switch-label">Rejected</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DateRangePicker1
|
{/* Footer Buttons */}
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
startField="startDate"
|
<button
|
||||||
endField="endDate"
|
type="button"
|
||||||
defaultRange={false}
|
className="btn btn-label-secondary btn-sm"
|
||||||
resetSignal={resetKey}
|
onClick={onClear}
|
||||||
maxDate={new Date()}
|
>
|
||||||
/>
|
Clear
|
||||||
</div>
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary btn-sm">
|
||||||
{/* Dropdown Filters */}
|
Apply
|
||||||
<div className="row g-2 text-start">
|
</button>
|
||||||
<SelectMultiple
|
|
||||||
name="uploadedByIds"
|
|
||||||
label="Uploaded By:"
|
|
||||||
options={uploadedBy}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="documentCategoryIds"
|
|
||||||
label="Document Category:"
|
|
||||||
options={documentCategory}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="documentTypeIds"
|
|
||||||
label="Document Type:"
|
|
||||||
options={documentType}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="documentTagIds"
|
|
||||||
label="Tags:"
|
|
||||||
options={documentTag}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Filter */}
|
|
||||||
<div className="text-start my-2">
|
|
||||||
<label className="form-label d-block mb-2">Choose Status:</label>
|
|
||||||
<div className="d-flex gap-4">
|
|
||||||
<label className="switch switch-sm">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
className="switch-input"
|
|
||||||
name="isVerified"
|
|
||||||
checked={isVerified === null}
|
|
||||||
onChange={() => setValue("isVerified", null)}
|
|
||||||
/>
|
|
||||||
<span className="switch-toggle-slider">
|
|
||||||
<span className="switch-on"></span>
|
|
||||||
<span className="switch-off"></span>
|
|
||||||
</span>
|
|
||||||
<span className="switch-label">All</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="switch switch-sm">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
className="switch-input"
|
|
||||||
name="isVerified"
|
|
||||||
checked={isVerified === true}
|
|
||||||
onChange={() => setValue("isVerified", true)}
|
|
||||||
/>
|
|
||||||
<span className="switch-toggle-slider">
|
|
||||||
<span className="switch-on"></span>
|
|
||||||
<span className="switch-off"></span>
|
|
||||||
</span>
|
|
||||||
<span className="switch-label">Verified</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="switch switch-sm">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
className="switch-input"
|
|
||||||
name="isVerified"
|
|
||||||
checked={isVerified === false}
|
|
||||||
onChange={() => setValue("isVerified", false)}
|
|
||||||
/>
|
|
||||||
<span className="switch-toggle-slider">
|
|
||||||
<span className="switch-on"></span>
|
|
||||||
<span className="switch-off"></span>
|
|
||||||
</span>
|
|
||||||
<span className="switch-label">Rejected</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
</FormProvider>
|
||||||
{/* Footer Buttons */}
|
);
|
||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
});
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-label-secondary btn-xs"
|
|
||||||
onClick={onClear}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="btn btn-primary btn-xs">
|
|
||||||
Apply
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DocumentFilterPanel;
|
export default DocumentFilterPanel;
|
||||||
|
|||||||
@ -17,54 +17,56 @@ const SkeletonCell = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DocumentTableSkeleton = ({ rows = 5 }) => {
|
export const DocumentTableSkeleton = ({ rows = 10 }) => {
|
||||||
return (
|
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">
|
<tbody>
|
||||||
<thead>
|
{[...Array(rows)].map((_, idx) => (
|
||||||
<tr>
|
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
|
||||||
<th className="text-start">Name</th>
|
{/* Name */}
|
||||||
<th className="text-start">Document Type</th>
|
<td className="text-start">
|
||||||
<th className="text-start">Uploaded By</th>
|
<SkeletonCell width="120px" height={16} />
|
||||||
<th className="text-center">Uploaded on</th>
|
</td>
|
||||||
<th className="text-center">Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
{/* Document Type */}
|
||||||
{[...Array(rows)].map((_, idx) => (
|
<td className="text-start">
|
||||||
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
|
<SkeletonCell width="100px" height={16} />
|
||||||
{/* Name */}
|
</td>
|
||||||
<td className="text-start">
|
|
||||||
<SkeletonCell width="120px" height={16} />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Document Type */}
|
{/* Uploaded By (Avatar + Name) */}
|
||||||
<td className="text-start">
|
<td className="text-start">
|
||||||
<SkeletonCell width="100px" height={16} />
|
<div className="d-flex align-items-center gap-2">
|
||||||
</td>
|
<SkeletonCell
|
||||||
|
width="30px"
|
||||||
{/* Uploaded By (Avatar + Name) */}
|
height={30}
|
||||||
<td className="text-start">
|
className="rounded-circle"
|
||||||
<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">
|
|
||||||
<SkeletonCell width="80px" height={16} />
|
<SkeletonCell width="80px" height={16} />
|
||||||
</td>
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
{/* Status */}
|
{/* Uploaded on */}
|
||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
<SkeletonCell width="70px" height={20} className="rounded" />
|
<SkeletonCell width="80px" height={16} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
{/* 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 GlobalModel from "../common/GlobalModel";
|
||||||
import NewDocument from "./ManageDocument";
|
import NewDocument from "./ManageDocument";
|
||||||
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
|
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
|
||||||
@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument";
|
|||||||
import DocumentViewerModal from "./DocumentViewerModal";
|
import DocumentViewerModal from "./DocumentViewerModal";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
|
import DocumentFilterChips from "./DocumentFilterChips";
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
export const DocumentContext = createContext();
|
export const DocumentContext = createContext();
|
||||||
@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
const [isSelf, setIsSelf] = useState(false);
|
const [isSelf, setIsSelf] = useState(false);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [isActive, setIsActive] = useState(true);
|
const [isActive, setIsActive] = useState(true);
|
||||||
const [filters, setFilter] = useState();
|
const [filters, setFilter] = useState(DocumentFilterDefaultValues);
|
||||||
const [isRefetching, setIsRefetching] = useState(false);
|
const [isRefetching, setIsRefetching] = useState(false);
|
||||||
const [refetchFn, setRefetchFn] = useState(null);
|
const [refetchFn, setRefetchFn] = useState(null);
|
||||||
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
|
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
|
||||||
const { employeeId } = useParams();
|
const { employeeId } = useParams();
|
||||||
const [OpenDocument, setOpenDocument] = useState(false);
|
const [OpenDocument, setOpenDocument] = useState(false);
|
||||||
|
const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues);
|
||||||
|
const updatedRef = useRef();
|
||||||
const [ManageDoc, setManageDoc] = useState({
|
const [ManageDoc, setManageDoc] = useState({
|
||||||
document: null,
|
document: null,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
setShowTrigger(true);
|
setShowTrigger(true);
|
||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Document Filters",
|
"Document Filters",
|
||||||
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
|
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} setFilterdata={setFilterdata} ref={updatedRef} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
setDocumentEntity(Document_Entity);
|
setDocumentEntity(Document_Entity);
|
||||||
}
|
}
|
||||||
}, [Document_Entity]);
|
}, [Document_Entity]);
|
||||||
|
|
||||||
|
|
||||||
|
const removeFilterChip = (key, id) => {
|
||||||
|
const updatedFilters = { ...filters };
|
||||||
|
if (Array.isArray(updatedFilters[key])) {
|
||||||
|
updatedFilters[key] = updatedFilters[key].filter((v) => v !== id);
|
||||||
|
updatedRef.current?.resetFieldValue(key,updatedFilters[key]);
|
||||||
|
}
|
||||||
|
else if (key === "dateRange") {
|
||||||
|
updatedFilters.startDate = null;
|
||||||
|
updatedFilters.endDate = null;
|
||||||
|
updatedRef.current?.resetFieldValue("startDate",null);
|
||||||
|
updatedRef.current?.resetFieldValue("endDate",null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updatedFilters[key] = null;
|
||||||
|
}
|
||||||
|
setFilter(updatedFilters);
|
||||||
|
return updatedFilters;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentContext.Provider value={contextValues}>
|
<DocumentContext.Provider value={contextValues}>
|
||||||
<div className="mt-5">
|
<div className="mt-2">
|
||||||
<div className="card page-min-h d-flex p-2">
|
<div className="card page-min-h d-flex p-5">
|
||||||
|
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
|
<div className="d-flex flex-row gap-2 col-12 col-md-8 col-lg-4 mb-md-0 align-items-center mb-2">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
{" "}
|
{" "}
|
||||||
<input
|
<input
|
||||||
@ -149,7 +174,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-6 col-md-6 col-lg-8 text-end">
|
<div className="col-12 col-md-6 col-lg-8 text-end">
|
||||||
{(isSelf || canUploadDocument) && (
|
{(isSelf || canUploadDocument) && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary me-3"
|
className="btn btn-sm btn-primary me-3"
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const DocumentsList = ({
|
|||||||
setIsRefetching(isFetching);
|
setIsRefetching(isFetching);
|
||||||
}, [isFetching, setIsRefetching]);
|
}, [isFetching, setIsRefetching]);
|
||||||
|
|
||||||
const { setManageDoc, setViewDoc } = useDocumentContext();
|
const { setManageDoc, setViewDoc,removeFilterChip } = useDocumentContext();
|
||||||
const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument();
|
const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument();
|
||||||
|
|
||||||
const paginate = (page) => {
|
const paginate = (page) => {
|
||||||
@ -82,9 +82,9 @@ const DocumentsList = ({
|
|||||||
if (isLoading || isFetching) return <DocumentTableSkeleton />;
|
if (isLoading || isFetching) return <DocumentTableSkeleton />;
|
||||||
if (isError)
|
if (isError)
|
||||||
return <div>Error: {error?.message || "Something went wrong"}</div>;
|
return <div>Error: {error?.message || "Something went wrong"}</div>;
|
||||||
if (isInitialEmpty) return <div>No documents found yet.</div>;
|
if (isInitialEmpty) return <div className="py-12 my-12">No documents found yet.</div>;
|
||||||
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
|
if (isSearchEmpty) return <div className="py-12 my-12">No results found for "{debouncedSearch}"</div>;
|
||||||
if (isFilterEmpty) return <div>No documents match your filter.</div>;
|
if (isFilterEmpty) return <div className="py-12 my-12">No documents match your filter.</div>;
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
ActiveInActive(
|
ActiveInActive(
|
||||||
@ -180,10 +180,10 @@ const DocumentsList = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="table-responsive">
|
<div className="table-responsive p-2">
|
||||||
<table className="table border-top dataTable text-nowrap">
|
<table className="table border-top dataTable text-nowrap">
|
||||||
<thead>
|
<thead className="">
|
||||||
<tr className="shadow-sm">
|
<tr className="py-2 ">
|
||||||
{DocumentColumns.map((col) => (
|
{DocumentColumns.map((col) => (
|
||||||
<th key={col.key} className={`sorting ${col.align}`}>
|
<th key={col.key} className={`sorting ${col.align}`}>
|
||||||
{col.label}
|
{col.label}
|
||||||
|
|||||||
@ -99,7 +99,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
if (
|
if (
|
||||||
!existing ||
|
!existing ||
|
||||||
new Date(rec.checkInTime || rec.checkOutTime) >
|
new Date(rec.checkInTime || rec.checkOutTime) >
|
||||||
new Date(existing.checkInTime || existing.checkOutTime)
|
new Date(existing.checkInTime || existing.checkOutTime)
|
||||||
) {
|
) {
|
||||||
uniqueMap.set(key, rec);
|
uniqueMap.set(key, rec);
|
||||||
}
|
}
|
||||||
@ -123,7 +123,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
};
|
};
|
||||||
const closeModal = () => setIsModalOpen(false);
|
const closeModal = () => setIsModalOpen(false);
|
||||||
|
|
||||||
const onSubmit = (formData) => {};
|
const onSubmit = (formData) => { };
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
@ -131,7 +131,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
<AttendLogs Id={attendanceId} />
|
<AttendLogs Id={attendanceId} />
|
||||||
</GlobalModel>
|
</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
|
<div
|
||||||
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
||||||
id="DataTables_Table_0_length"
|
id="DataTables_Table_0_length"
|
||||||
@ -153,19 +153,21 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
</>
|
</>
|
||||||
</div>
|
</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">
|
<div className="table-responsive text-nowrap">
|
||||||
{!loading && data.length === 0 && <span>No employee logs</span>}
|
{!loading && data.length === 0 && (
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-center align-items-center"
|
||||||
|
style={{ minHeight: "40vh" }}
|
||||||
|
>
|
||||||
|
<p className="text-secondary mb-0">
|
||||||
|
No data for this date range. Please choose another.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{isError && <div className="text-center">{error.message}</div>}
|
{isError && <div className="text-center">{error.message}</div>}
|
||||||
{loading && !data && <div className="text-center">Loading...</div>}
|
{loading && !data && <div className="text-center">Loading...</div>}
|
||||||
{data && data.length > 0 && (
|
{data && data.length > 0 && (
|
||||||
@ -250,9 +252,8 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
{[...Array(totalPages)].map((_, index) => (
|
{[...Array(totalPages)].map((_, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={`page-item ${
|
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||||
currentPage === index + 1 ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link "
|
className="page-link "
|
||||||
@ -263,9 +264,8 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li
|
<li
|
||||||
className={`page-item ${
|
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||||
currentPage === totalPages ? "disabled" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link "
|
className="page-link "
|
||||||
|
|||||||
@ -12,11 +12,11 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="row">
|
<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>
|
<EmpOverview profile={profile}></EmpOverview>
|
||||||
</div>
|
</div>
|
||||||
<div className="col col-sm-6 pt-5">
|
{/* <div className="col col-sm-6 pt-5">
|
||||||
<div className="card ">
|
<div className="card ">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<small className="card-text text-uppercase text-body-secondary small text-start d-block">
|
<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"
|
className="d-flex mb-4 align-items-start flex-wrap"
|
||||||
key={project.id}
|
key={project.id}
|
||||||
>
|
>
|
||||||
{/* Project Info */}
|
|
||||||
<div className="flex-grow-1">
|
<div className="flex-grow-1">
|
||||||
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
@ -70,7 +69,6 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dates */}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -79,7 +77,7 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const EmployeeNav = ({ onPillClick, activePill }) => {
|
|||||||
icon: "bx bx-file",
|
icon: "bx bx-file",
|
||||||
label: "Documents",
|
label: "Documents",
|
||||||
},
|
},
|
||||||
{ key: "activities", icon: "bx bx-grid-alt", label: "Activities" },
|
// { key: "activities", icon: "bx bx-grid-alt", label: "Activities" },
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return (
|
return (
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export const employeeSchema =
|
|||||||
.min(1, { message: "Phone Number is required" })
|
.min(1, { message: "Phone Number is required" })
|
||||||
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
||||||
jobRoleId: z.string().min(1, { message: "Role is required" }),
|
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),
|
hasApplicationAccess:z.boolean().default(false),
|
||||||
}).refine((data) => {
|
}).refine((data) => {
|
||||||
if (data.hasApplicationAccess) {
|
if (data.hasApplicationAccess) {
|
||||||
@ -119,6 +119,6 @@ export const defatEmployeeObj = {
|
|||||||
permanentAddress: "",
|
permanentAddress: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
jobRoleId: null,
|
jobRoleId: null,
|
||||||
organizationId:"",
|
// organizationId:"",
|
||||||
hasApplicationAccess:false
|
hasApplicationAccess:false
|
||||||
}
|
}
|
||||||
@ -15,18 +15,18 @@ import {
|
|||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
import { defatEmployeeObj, employeeSchema } from "./EmployeeSchema";
|
import { defatEmployeeObj, employeeSchema } from "./EmployeeSchema";
|
||||||
import { useOrganizationsList } from "../../hooks/useOrganization";
|
// import { useOrganizationsList } from "../../hooks/useOrganization";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
|
||||||
const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
|
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
|
||||||
const {
|
// const {
|
||||||
data: organzationList,
|
// data: organzationList,
|
||||||
isLoading,
|
// isLoading,
|
||||||
isError,
|
// isError,
|
||||||
error: EempError,
|
// error: EempError,
|
||||||
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
// } = useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
||||||
const {
|
const {
|
||||||
employee,
|
employee,
|
||||||
error,
|
error,
|
||||||
@ -113,7 +113,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
permanentAddress: currentEmployee.permanentAddress || "",
|
permanentAddress: currentEmployee.permanentAddress || "",
|
||||||
phoneNumber: currentEmployee.phoneNumber || "",
|
phoneNumber: currentEmployee.phoneNumber || "",
|
||||||
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
|
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
|
||||||
organizationId: currentEmployee.organizationId || "",
|
// organizationId: currentEmployee.organizationId || "", // Hide temp. for version 1
|
||||||
hasApplicationAccess: currentEmployee.hasApplicationAccess || false,
|
hasApplicationAccess: currentEmployee.hasApplicationAccess || false,
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
@ -413,9 +413,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* -------------- */}
|
|
||||||
<div className="row mb-3">
|
<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>
|
<Label className="form-text text-start" required>
|
||||||
Organization
|
Organization
|
||||||
</Label>
|
</Label>
|
||||||
@ -446,9 +447,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
{errors.organizationId.message}
|
{errors.organizationId.message}
|
||||||
</div>
|
</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">
|
<label className="form-check-label d-flex align-items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -584,15 +586,14 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row text-end">
|
<div className="d-flex flex-row gap-3 justify-content-end">
|
||||||
<div className="col-sm-12">
|
|
||||||
<button
|
<button
|
||||||
aria-label="manage employee"
|
aria-label="manage employee"
|
||||||
type="reset"
|
|
||||||
className="btn btn-sm btn-label-secondary me-2"
|
className="btn btn-sm btn-label-secondary me-2"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
|
onClick={()=>onClosed()}
|
||||||
>
|
>
|
||||||
Clear
|
Close
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="manage employee"
|
aria-label="manage employee"
|
||||||
@ -600,9 +601,8 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isPending ? "Please Wait..." : employeeId ? "Update" : "Create"}
|
{isPending ? "Please Wait..." : employeeId ? "Update" : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -14,8 +14,7 @@ const formSchema = z.object({
|
|||||||
selectedRole: z.record(z.boolean()),
|
selectedRole: z.record(z.boolean()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ManageRole = ( {employeeId, onClosed} ) =>
|
const ManageRole = ({ employeeId, onClosed }) => {
|
||||||
{
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const formStateRef = useRef({});
|
const formStateRef = useRef({});
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ const ManageRole = ( {employeeId, onClosed} ) =>
|
|||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
updateRoles,
|
updateRoles,
|
||||||
isPending : isLoading,
|
isPending: isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useUpdateEmployeeRoles({
|
} = useUpdateEmployeeRoles({
|
||||||
@ -112,56 +111,57 @@ const ManageRole = ( {employeeId, onClosed} ) =>
|
|||||||
const isLoadingData = roleLoading || empLoading;
|
const isLoadingData = roleLoading || empLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="text-start mb-3">
|
<div className="text-center mb-3">
|
||||||
<h5 className="lead">Select Roles :</h5>
|
<h5 className="lead">Select Roles :</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoadingData ? (
|
{isLoadingData ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="d-flex flex-wrap justify-content-between pb-4"
|
className="d-flex flex-wrap justify-content-between pb-4"
|
||||||
style={{ maxHeight: "70vh", overflowY: "auto" }}
|
style={{ maxHeight: "70vh", overflowY: "auto" }}
|
||||||
>
|
>
|
||||||
{roles.map((role) => (
|
{roles.slice()
|
||||||
<div className="col-md-6 col-lg-4 mb-3" key={role.id}>
|
.sort((a, b) => a.role.localeCompare(b.role)).map((role) => (
|
||||||
<div className="form-check ms-2 text-start">
|
<div className="col-md-6 col-lg-4 mb-3" key={role.id}>
|
||||||
<input
|
<div className="form-check ms-2 text-start">
|
||||||
className="form-check-input"
|
<input
|
||||||
type="checkbox"
|
className="form-check-input"
|
||||||
id={role.id}
|
type="checkbox"
|
||||||
{...register(`selectedRole.${role.id}`)}
|
id={role.id}
|
||||||
/>
|
{...register(`selectedRole.${role.id}`)}
|
||||||
<label className="form-check-label" htmlFor={role.id}>
|
/>
|
||||||
<small>{role.role || "--"}</small>
|
<label className="form-check-label" htmlFor={role.id}>
|
||||||
</label>
|
<small>{role.role || "--"}</small>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</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>
|
</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;
|
||||||
90
src/components/Expenses/ExpenseFilterChips.jsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||||
|
const filterChips = useMemo(() => {
|
||||||
|
if (!filterData?.projects?.length) return [];
|
||||||
|
|
||||||
|
const chips = [];
|
||||||
|
const projectExists = filters.projectIds?.every((pid) =>
|
||||||
|
filterData.projects.some((p) => p.id === pid)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!projectExists) return [];
|
||||||
|
|
||||||
|
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%" }}
|
||||||
|
>
|
||||||
|
<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 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 { FormProvider, useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||||
@ -13,9 +13,11 @@ import { useSelector } from "react-redux";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useExpenseFilter } from "../../hooks/useExpense";
|
import { useExpenseFilter } from "../../hooks/useExpense";
|
||||||
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => {
|
||||||
|
const { status } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector(
|
||||||
(store) => store.localVariables.projectId
|
(store) => store.localVariables.projectId
|
||||||
);
|
);
|
||||||
@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
{ id: "submittedBy", name: "Submitted By" },
|
{ id: "submittedBy", name: "Submitted By" },
|
||||||
{ id: "project", name: "Project" },
|
{ id: "project", name: "Project" },
|
||||||
{ id: "paymentMode", name: "Payment Mode" },
|
{ id: "paymentMode", name: "Payment Mode" },
|
||||||
{ id: "expensesType", name: "Expense Type" },
|
{ id: "expensesType", name: "Expense Category" },
|
||||||
{ id: "createdAt", name: "Submitted Date" },
|
{ id: "createdAt", name: "Submitted Date" },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
||||||
const [resetKey, setResetKey] = useState(0);
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
|
const dynamicDefaultFilter = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...defaultFilter,
|
||||||
|
statusIds: status ? [status] : defaultFilter.statusIds || [],
|
||||||
|
projectIds: defaultFilter.projectIds || [],
|
||||||
|
createdByIds: defaultFilter.createdByIds || [],
|
||||||
|
paidById: defaultFilter.paidById || [],
|
||||||
|
ExpenseCategoryIds: defaultFilter.ExpenseCategoryIds || [],
|
||||||
|
isTransactionDate: defaultFilter.isTransactionDate ?? true,
|
||||||
|
startDate: defaultFilter.startDate,
|
||||||
|
endDate: defaultFilter.endDate,
|
||||||
|
};
|
||||||
|
}, [status, selectedProjectId]);
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(SearchSchema),
|
resolver: zodResolver(SearchSchema),
|
||||||
defaultValues: defaultFilter,
|
defaultValues: dynamicDefaultFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control, handleSubmit, reset, setValue, watch } = methods;
|
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||||
@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Change here
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && setFilterdata) {
|
||||||
|
setFilterdata(data);
|
||||||
|
}
|
||||||
|
}, [data, setFilterdata]);
|
||||||
|
|
||||||
const handleGroupChange = (e) => {
|
const handleGroupChange = (e) => {
|
||||||
const group = groupByList.find((g) => g.id === e.target.value);
|
const group = groupByList.find((g) => g.id === e.target.value);
|
||||||
if (group) setSelectedGroup(group);
|
if (group) setSelectedGroup(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
resetFieldValue: (name, value) => {
|
||||||
|
// Reset specific field
|
||||||
|
if (value !== undefined) {
|
||||||
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getValues: methods.getValues, // optional, to read current filter state
|
||||||
|
}));
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
onApply({
|
onApply({
|
||||||
...formData,
|
...formData,
|
||||||
@ -61,7 +96,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
|
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
|
||||||
});
|
});
|
||||||
handleGroupBy(selectedGroup.id);
|
handleGroupBy(selectedGroup.id);
|
||||||
closePanel();
|
// closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
@ -70,18 +105,66 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
setSelectedGroup(groupByList[0]);
|
setSelectedGroup(groupByList[0]);
|
||||||
onApply(defaultFilter);
|
onApply(defaultFilter);
|
||||||
handleGroupBy(groupByList[0].id);
|
handleGroupBy(groupByList[0].id);
|
||||||
closePanel();
|
// closePanel();
|
||||||
|
if (status) {
|
||||||
|
navigate("/expenses", { replace: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close popup when navigating to another component
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
closePanel();
|
closePanel();
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
|
const [appliedStatusId, setAppliedStatusId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!status || !data) return;
|
||||||
|
|
||||||
|
const projectExists = selectedProjectId
|
||||||
|
? data?.projects?.some((p) => p.id === selectedProjectId)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!projectExists) {
|
||||||
|
console.warn("Project not found, skipping ExpenseStatus filter.");
|
||||||
|
return; // stop processing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== appliedStatusId) {
|
||||||
|
const filterWithStatus = {
|
||||||
|
...dynamicDefaultFilter,
|
||||||
|
projectIds: [selectedProjectId],
|
||||||
|
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 (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
||||||
if (isError && isFetched)
|
if (isError && isFetched)
|
||||||
return <div>Something went wrong Here- {error.message} </div>;
|
return <div>Something went wrong Here- {error.message} </div>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
@ -92,31 +175,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
isTransactionDate ? "active btn-primary text-white" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setValue("isTransactionDate", true)}
|
onClick={() => setValue("isTransactionDate", true)}
|
||||||
>
|
>
|
||||||
Transaction Date
|
Transaction Date
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
!isTransactionDate ? "active btn-primary text-white" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setValue("isTransactionDate", false)}
|
onClick={() => setValue("isTransactionDate", false)}
|
||||||
>
|
>
|
||||||
Submitted Date
|
Submitted Date
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label className="fw-semibold">Choose Date Range:</label>
|
|
||||||
<DateRangePicker1
|
<DateRangePicker1
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
startField="startDate"
|
startField="startDate"
|
||||||
endField="endDate"
|
endField="endDate"
|
||||||
resetSignal={resetKey}
|
resetSignal={resetKey}
|
||||||
defaultRange={false}
|
defaultRange={false}
|
||||||
|
maxDate={new Date()}
|
||||||
|
className="w-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -142,6 +224,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
labelKey={(item) => item.name}
|
labelKey={(item) => item.name}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
/>
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="ExpenseCategoryIds"
|
||||||
|
label="Category :"
|
||||||
|
options={data.expenseCategory}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">Status :</label>
|
<label className="form-label">Status :</label>
|
||||||
@ -213,6 +302,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ExpenseFilterPanel;
|
export default ExpenseFilterPanel;
|
||||||
@ -10,20 +10,34 @@ import {
|
|||||||
EXPENSE_REJECTEDBY,
|
EXPENSE_REJECTEDBY,
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils";
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
formatFigure,
|
||||||
|
getColorNameFromHex,
|
||||||
|
useDebounce,
|
||||||
|
} from "../../utils/appUtils";
|
||||||
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
import ConfirmModal from "../common/ConfirmModal";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import ExpenseFilterChips from "./ExpenseFilterChips";
|
||||||
|
import { defaultFilter } from "./ExpenseSchema";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||||
const [deletingId, setDeletingId] = useState(null);
|
const [deletingId, setDeletingId] = useState(null);
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const { setViewExpense, setManageExpenseModal } = useExpenseContext();
|
const {
|
||||||
|
setViewExpense,
|
||||||
|
setManageExpenseModal,
|
||||||
|
filterData,
|
||||||
|
removeFilterChip,
|
||||||
|
} = useExpenseContext();
|
||||||
const IsExpenseEditable = useHasUserPermission();
|
const IsExpenseEditable = useHasUserPermission();
|
||||||
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const debouncedSearch = useDebounce(searchText, 500);
|
const debouncedSearch = useDebounce(searchText, 500);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
||||||
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
||||||
@ -59,44 +73,65 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
const groupByField = (items, field) => {
|
const groupByField = (items, field) => {
|
||||||
return items.reduce((acc, item) => {
|
return items.reduce((acc, item) => {
|
||||||
let key;
|
let key;
|
||||||
|
let displayField;
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case "transactionDate":
|
case "transactionDate":
|
||||||
key = item.transactionDate?.split("T")[0];
|
key = item?.transactionDate?.split("T")[0];
|
||||||
|
displayField = "Transaction Date";
|
||||||
break;
|
break;
|
||||||
case "status":
|
case "status":
|
||||||
key = item.status?.displayName || "Unknown";
|
key = item?.status?.displayName || "Unknown";
|
||||||
|
displayField = "Status";
|
||||||
break;
|
break;
|
||||||
case "submittedBy":
|
case "submittedBy":
|
||||||
key = `${item.createdBy?.firstName ?? ""} ${
|
key = `${item?.createdBy?.firstName ?? ""} ${
|
||||||
item.createdBy?.lastName ?? ""
|
item.createdBy?.lastName ?? ""
|
||||||
}`.trim();
|
}`.trim();
|
||||||
|
displayField = "Submitted By";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
key = item.project?.name || "Unknown Project";
|
key = item?.project?.name || "Unknown Project";
|
||||||
|
displayField = "Project";
|
||||||
break;
|
break;
|
||||||
case "paymentMode":
|
case "paymentMode":
|
||||||
key = item.paymentMode?.name || "Unknown Mode";
|
key = item?.paymentMode?.name || "Unknown Mode";
|
||||||
|
displayField = "Payment Mode";
|
||||||
break;
|
break;
|
||||||
case "expensesType":
|
case "expenseCategory":
|
||||||
key = item.expensesType?.name || "Unknown Type";
|
key = item?.expenseCategory?.name || "Unknown Type";
|
||||||
|
displayField = "Expense Category";
|
||||||
break;
|
break;
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
key = item.createdAt?.split("T")[0] || "Unknown Type";
|
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
||||||
|
displayField = "Created Date";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
key = "Others";
|
key = "Others";
|
||||||
|
displayField = "Others";
|
||||||
}
|
}
|
||||||
if (!acc[key]) acc[key] = [];
|
|
||||||
acc[key].push(item);
|
const groupKey = `${field}_${key}`; // unique key for object property
|
||||||
|
if (!acc[groupKey]) {
|
||||||
|
acc[groupKey] = { key, displayField, items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[groupKey].items.push(item);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const expenseColumns = [
|
const expenseColumns = [
|
||||||
{
|
{
|
||||||
key: "expensesType",
|
key: "expenseUId",
|
||||||
label: "Expense Type",
|
label: "Expense Id",
|
||||||
getValue: (e) => e.expensesType?.name || "N/A",
|
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",
|
align: "text-start",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -114,7 +149,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
e.createdBy?.lastName ?? ""
|
e.createdBy?.lastName ?? ""
|
||||||
}`.trim() || "N/A",
|
}`.trim() || "N/A",
|
||||||
customRender: (e) => (
|
customRender: (e) => (
|
||||||
<div className="d-flex align-items-center">
|
<div
|
||||||
|
className="d-flex align-items-center cursor-pointer"
|
||||||
|
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="xs"
|
size="xs"
|
||||||
classAvatar="m-0"
|
classAvatar="m-0"
|
||||||
@ -140,7 +178,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
label: "Amount",
|
label: "Amount",
|
||||||
getValue: (e) => (
|
getValue: (e) => (
|
||||||
<>
|
<>
|
||||||
<i className="bx bx-rupee b-xs"></i> {e?.amount}
|
{" "}
|
||||||
|
{formatFigure(e?.amount, {
|
||||||
|
type: "currency",
|
||||||
|
currency: e?.currency?.currencyCode,
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
isAlwaysVisible: true,
|
isAlwaysVisible: true,
|
||||||
@ -161,25 +203,38 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const headers = [
|
||||||
if (isInitialLoading) return <ExpenseTableSkeleton />;
|
"Expense Category",
|
||||||
if (isError) return <div>{error.message}</div>;
|
"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
|
const grouped = groupBy
|
||||||
? groupByField(data?.data ?? [], groupBy)
|
? groupByField(data?.data ?? [], groupBy)
|
||||||
: { All: data?.data ?? [] };
|
: { All: data?.data ?? [] };
|
||||||
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy);
|
const IsGroupedByDate = [
|
||||||
|
{ key: "transactionDate", displayField: "Transaction Date" },
|
||||||
|
{ key: "createdAt", displayField: "created Date" },
|
||||||
|
]?.includes(groupBy);
|
||||||
|
|
||||||
const canEditExpense = (expense) => {
|
const canEditExpense = (expense) => {
|
||||||
return (
|
return (
|
||||||
(expense.status.id === EXPENSE_DRAFT ||
|
(expense?.status?.id === EXPENSE_DRAFT ||
|
||||||
EXPENSE_REJECTEDBY.includes(expense.status.id)) &&
|
EXPENSE_REJECTEDBY.includes(expense?.status?.id)) &&
|
||||||
expense.createdBy?.id === SelfId
|
expense?.createdBy?.id === SelfId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canDetetExpense = (expense) => {
|
const canDetetExpense = (expense) => {
|
||||||
return (
|
return (
|
||||||
expense.status.id === EXPENSE_DRAFT && expense.createdBy.id === SelfId
|
expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -198,9 +253,16 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card px-0 px-sm-4">
|
<div className="card page-min-h px-sm-4 table-responsive">
|
||||||
|
{/* Filter Chips */}
|
||||||
|
<ExpenseFilterChips
|
||||||
|
filters={filters}
|
||||||
|
filterData={filterData}
|
||||||
|
removeFilterChip={removeFilterChip}
|
||||||
|
groupBy={groupBy}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="card-datatable table-responsive "
|
className="card-datatable "
|
||||||
id="horizontal-example"
|
id="horizontal-example"
|
||||||
>
|
>
|
||||||
<div className="dataTables_wrapper no-footer px-2 ">
|
<div className="dataTables_wrapper no-footer px-2 ">
|
||||||
@ -212,10 +274,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||||
<th
|
<th
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className={`sorting d-table-cell`}
|
className={`sorting d-table-cell `}
|
||||||
aria-sort="descending"
|
aria-sort="descending"
|
||||||
>
|
>
|
||||||
<div className={`${col.align}`}>{col.label}</div>
|
<div className={`${col.align} `}>{col.label}</div>
|
||||||
</th>
|
</th>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@ -226,34 +288,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.keys(grouped).length > 0 ? (
|
{Object.keys(grouped).length > 0 ? (
|
||||||
Object.entries(grouped).map(([group, expenses]) => (
|
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||||
<React.Fragment key={group}>
|
<React.Fragment key={key}>
|
||||||
<tr className="tr-group text-dark">
|
<tr className="tr-group text-dark">
|
||||||
<td colSpan={8} className="text-start">
|
<td colSpan={8} className="text-start">
|
||||||
<strong>
|
<div className="d-flex align-items-center px-2">
|
||||||
{IsGroupedByDate
|
{" "}
|
||||||
? formatUTCToLocalTime(group)
|
<small className="fs-6 py-1">
|
||||||
: group}
|
{displayField} :{" "}
|
||||||
</strong>
|
</small>{" "}
|
||||||
|
<small className="fs-6 ms-3">
|
||||||
|
{IsGroupedByDate
|
||||||
|
? formatUTCToLocalTime(key)
|
||||||
|
: key}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expenses.map((expense) => (
|
{items?.map((expense) => (
|
||||||
<tr key={expense.id}>
|
<tr key={expense.id}>
|
||||||
{expenseColumns.map(
|
{expenseColumns.map(
|
||||||
(col) =>
|
(col) =>
|
||||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||||
<td
|
<td
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className={`d-table-cell ${col.align ?? ""}`}
|
className={`d-table-cell ml-2 ${
|
||||||
|
col.align ?? ""
|
||||||
|
} `}
|
||||||
>
|
>
|
||||||
{col.customRender
|
<div
|
||||||
? col.customRender(expense)
|
className={`d-flex px-2 ${
|
||||||
: col.getValue(expense)}
|
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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<td className="sticky-action-column bg-white">
|
<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
|
<i
|
||||||
className="bx bx-show text-primary cursor-pointer"
|
className="bx bx-show text-primary cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -263,27 +352,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
></i>
|
></i>
|
||||||
{canEditExpense(expense) && (
|
{canDetetExpense(expense) &&
|
||||||
<i
|
canEditExpense(expense) && (
|
||||||
className="bx bx-edit text-secondary cursor-pointer"
|
<div className="dropdown z-2">
|
||||||
onClick={() =>
|
<button
|
||||||
setManageExpenseModal({
|
type="button"
|
||||||
IsOpen: true,
|
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||||
expenseId: expense.id,
|
data-bs-toggle="dropdown"
|
||||||
})
|
aria-expanded="false"
|
||||||
}
|
>
|
||||||
></i>
|
<i
|
||||||
)}
|
className="bx bx-dots-vertical-rounded text-muted p-0"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
|
data-bs-custom-class="tooltip-dark"
|
||||||
|
title="More Action"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||||
|
{canDetetExpense(expense) && (
|
||||||
|
<li
|
||||||
|
onClick={() =>
|
||||||
|
setManageExpenseModal({
|
||||||
|
IsOpen: true,
|
||||||
|
expenseId: expense.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
|
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||||
|
<span className="align-left ">
|
||||||
|
Modify
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
{canDetetExpense(expense) && (
|
{canDetetExpense(expense) && (
|
||||||
<i
|
<li
|
||||||
className="bx bx-trash text-danger cursor-pointer"
|
onClick={() => {
|
||||||
onClick={() => {
|
setIsDeleteModalOpen(true);
|
||||||
setIsDeleteModalOpen(true);
|
setDeletingId(expense.id);
|
||||||
setDeletingId(expense.id);
|
}}
|
||||||
}}
|
>
|
||||||
></i>
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
)}
|
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||||
|
<span className="align-left">
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -292,22 +415,24 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="text-center py-4">
|
<td colSpan={8} className="text-center border-0 ">
|
||||||
No Expense Found
|
<div className="py-8">
|
||||||
|
<p>No Expense Found</p>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{data?.data?.length > 0 && (
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={data.totalPages}
|
|
||||||
onPageChange={paginate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{data?.data?.length > 0 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
|
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
const ALLOWED_TYPES = [
|
const ALLOWED_TYPES = [
|
||||||
@ -8,24 +10,25 @@ const ALLOWED_TYPES = [
|
|||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ExpenseSchema = (expenseTypes) => {
|
export const ExpenseSchema = (ExpenseCategories) => {
|
||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
projectId: z.string().min(1, { message: "Project is required" }),
|
projectId: z.string().min(1, { message: "Project is required" }),
|
||||||
expensesTypeId: z
|
expenseCategoryId: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "Expense type is required" }),
|
.min(1, { message: "Expense type is required" }),
|
||||||
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
|
||||||
paidById: z.string().min(1, { message: "Employee name is required" }),
|
paidById: z.string().min(1, { message: "Employee name is required" }),
|
||||||
transactionDate: z
|
transactionDate: z.string().min(1, { message: "Date is required" }),
|
||||||
.string()
|
|
||||||
.min(1, { message: "Date is required" })
|
|
||||||
,
|
|
||||||
transactionId: z.string().optional(),
|
transactionId: z.string().optional(),
|
||||||
description: z.string().min(1, { message: "Description is required" }),
|
description: z.string().min(1, { message: "Description is required" }),
|
||||||
location: z.string().min(1, { message: "Location is required" }),
|
location: z.string().min(1, { message: "Location is required" }),
|
||||||
supplerName: z.string().min(1, { message: "Supplier name 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
|
amount: z.coerce
|
||||||
.number({
|
.number({
|
||||||
invalid_type_error: "Amount is required and must be a 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" }),
|
.nonempty({ message: "At least one file attachment is required" }),
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@ -68,9 +69,14 @@ export const ExpenseSchema = (expenseTypes) => {
|
|||||||
path: ["paidById"],
|
path: ["paidById"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId);
|
const ExpenseCategory = ExpenseCategories.find(
|
||||||
if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) {
|
(et) => et.id === data.expenseCategoryId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
ExpenseCategory?.noOfPersonsRequired &&
|
||||||
|
(!data.noOfPersons || data.noOfPersons < 1)
|
||||||
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "No. of Persons is required and must be at least 1",
|
message: "No. of Persons is required and must be at least 1",
|
||||||
@ -82,7 +88,7 @@ export const ExpenseSchema = (expenseTypes) => {
|
|||||||
|
|
||||||
export const defaultExpense = {
|
export const defaultExpense = {
|
||||||
projectId: "",
|
projectId: "",
|
||||||
expensesTypeId: "",
|
expenseCategoryId: "",
|
||||||
paymentModeId: "",
|
paymentModeId: "",
|
||||||
paidById: "",
|
paidById: "",
|
||||||
transactionDate: "",
|
transactionDate: "",
|
||||||
@ -92,12 +98,15 @@ export const defaultExpense = {
|
|||||||
supplerName: "",
|
supplerName: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
noOfPersons: "",
|
noOfPersons: "",
|
||||||
gstNumber:"",
|
gstNumber: "",
|
||||||
|
currencyId: DEFAULT_CURRENCY,
|
||||||
billAttachments: [],
|
billAttachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ExpenseActionScheam = (
|
||||||
export const ExpenseActionScheam = (isReimbursement = false) => {
|
isReimbursement = false,
|
||||||
|
transactionDate
|
||||||
|
) => {
|
||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
comment: z.string().min(1, { message: "Please leave comment" }),
|
comment: z.string().min(1, { message: "Please leave comment" }),
|
||||||
@ -105,6 +114,9 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
|
|||||||
reimburseTransactionId: z.string().nullable().optional(),
|
reimburseTransactionId: z.string().nullable().optional(),
|
||||||
reimburseDate: z.string().nullable().optional(),
|
reimburseDate: z.string().nullable().optional(),
|
||||||
reimburseById: 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) => {
|
.superRefine((data, ctx) => {
|
||||||
if (isReimbursement) {
|
if (isReimbursement) {
|
||||||
@ -122,6 +134,7 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
|
|||||||
message: "Reimburse Date is required",
|
message: "Reimburse Date is required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.reimburseById) {
|
if (!data.reimburseById) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@ -129,26 +142,42 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
|
|||||||
message: "Reimburse By is required",
|
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: "",
|
comment: "",
|
||||||
statusId: "",
|
statusId: "",
|
||||||
|
|
||||||
reimburseTransactionId: null,
|
reimburseTransactionId: null,
|
||||||
reimburseDate: null,
|
reimburseDate: null,
|
||||||
reimburseById: null,
|
reimburseById: null,
|
||||||
|
tdsPercentage: null,
|
||||||
|
baseAmount:null,
|
||||||
|
taxAmount: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const SearchSchema = z.object({
|
export const SearchSchema = z.object({
|
||||||
projectIds: z.array(z.string()).optional(),
|
projectIds: z.array(z.string()).optional(),
|
||||||
statusIds: z.array(z.string()).optional(),
|
statusIds: z.array(z.string()).optional(),
|
||||||
createdByIds: z.array(z.string()).optional(),
|
createdByIds: z.array(z.string()).optional(),
|
||||||
paidById: z.array(z.string()).optional(),
|
paidById: z.array(z.string()).optional(),
|
||||||
|
ExpenseCategoryIds: z.array(z.string()).optional(),
|
||||||
startDate: z.string().optional(),
|
startDate: z.string().optional(),
|
||||||
endDate: z.string().optional(),
|
endDate: z.string().optional(),
|
||||||
isTransactionDate: z.boolean().default(true),
|
isTransactionDate: z.boolean().default(true),
|
||||||
@ -159,8 +188,8 @@ export const defaultFilter = {
|
|||||||
statusIds: [],
|
statusIds: [],
|
||||||
createdByIds: [],
|
createdByIds: [],
|
||||||
paidById: [],
|
paidById: [],
|
||||||
|
ExpenseCategoryIds: [],
|
||||||
isTransactionDate: true,
|
isTransactionDate: true,
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: 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 (
|
return (
|
||||||
<div className="card px-2">
|
<div className="card px-2">
|
||||||
<table
|
<table
|
||||||
@ -153,17 +157,11 @@ export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
|
|||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="d-none d-sm-table-cell">
|
{headers.map((header) => (
|
||||||
<div className="text-start ms-5">Expense Type</div>
|
<th key={header} className="d-none d-sm-table-cell">
|
||||||
</th>
|
<div className="text-start ms-5">{header}</div>
|
||||||
<th className="d-none d-sm-table-cell">
|
</th>
|
||||||
<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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -204,7 +202,7 @@ export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
|
|||||||
<SkeletonCell width="80px" height={16} />
|
<SkeletonCell width="80px" height={16} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{/* Submitted */}
|
{/* Submitted */}
|
||||||
<td className="d-none d-md-table-cell text-end">
|
<td className="d-none d-md-table-cell text-end">
|
||||||
<SkeletonCell width="70px" height={16} />
|
<SkeletonCell width="70px" height={16} />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useState,useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../common/Avatar";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
|
import Timeline from "../common/TimeLine";
|
||||||
|
import moment from "moment";
|
||||||
|
import { getColorNameFromHex } from "../../utils/appUtils";
|
||||||
const ExpenseStatusLogs = ({ data }) => {
|
const ExpenseStatusLogs = ({ data }) => {
|
||||||
const [visibleCount, setVisibleCount] = useState(4);
|
|
||||||
|
|
||||||
const sortedLogs = useMemo(() => {
|
const sortedLogs = useMemo(() => {
|
||||||
if (!data?.expenseLogs) return [];
|
if (!data?.expenseLogs) return [];
|
||||||
@ -13,56 +14,35 @@ const ExpenseStatusLogs = ({ data }) => {
|
|||||||
);
|
);
|
||||||
}, [data?.expenseLogs]);
|
}, [data?.expenseLogs]);
|
||||||
|
|
||||||
const logsToShow = sortedLogs.slice(0, visibleCount);
|
const timelineData = useMemo(() => {
|
||||||
|
return sortedLogs.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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
}, [sortedLogs]);
|
||||||
|
|
||||||
const handleShowMore = () => {
|
const handleShowMore = () => {
|
||||||
setVisibleCount((prev) => prev + 4);
|
setVisibleCount((prev) => prev + 4);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="page-min-h overflow-auto py-1">
|
||||||
<div className="row g-2">
|
<Timeline items={timelineData} />
|
||||||
{logsToShow.map((log) => (
|
</div>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default ExpenseStatusLogs;
|
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 { useForm } from "react-hook-form";
|
||||||
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
||||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useCurrencies, useProjectName } from "../../hooks/useProjects";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import useMaster, {
|
import useMaster, {
|
||||||
|
useExpenseCategory,
|
||||||
useExpenseStatus,
|
useExpenseStatus,
|
||||||
useExpenseType,
|
|
||||||
usePaymentMode,
|
usePaymentMode,
|
||||||
} from "../../hooks/masterHook/useMaster";
|
} from "../../hooks/masterHook/useMaster";
|
||||||
import {
|
import {
|
||||||
@ -28,6 +28,9 @@ import moment from "moment";
|
|||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
import ErrorPage from "../../pages/ErrorPage";
|
import ErrorPage from "../../pages/ErrorPage";
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
|
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||||
|
import Filelist from "./Filelist";
|
||||||
|
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
||||||
|
|
||||||
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||||
const {
|
const {
|
||||||
@ -38,11 +41,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
const [ExpenseType, setExpenseType] = useState();
|
const [ExpenseType, setExpenseType] = useState();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
ExpenseTypes,
|
ExpenseCategories,
|
||||||
loading: ExpenseLoading,
|
loading: ExpenseLoading,
|
||||||
error: ExpenseError,
|
error: ExpenseError,
|
||||||
} = useExpenseType();
|
} = useExpenseCategory();
|
||||||
const schema = ExpenseSchema(ExpenseTypes);
|
const schema = ExpenseSchema(ExpenseCategories);
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -64,7 +67,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
error,
|
error,
|
||||||
isError: isProjectError,
|
isError: isProjectError,
|
||||||
} = useProjectName();
|
} = useProjectName();
|
||||||
|
const {
|
||||||
|
data: currencies,
|
||||||
|
isLoading: currencyLoading,
|
||||||
|
error: currencyError,
|
||||||
|
} = useCurrencies();
|
||||||
const {
|
const {
|
||||||
PaymentModes,
|
PaymentModes,
|
||||||
loading: PaymentModeLoading,
|
loading: PaymentModeLoading,
|
||||||
@ -80,6 +87,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
isLoading: EmpLoading,
|
isLoading: EmpLoading,
|
||||||
isError: isEmployeeError,
|
isError: isEmployeeError,
|
||||||
} = useEmployeesNameByProject(selectedproject);
|
} = useEmployeesNameByProject(selectedproject);
|
||||||
|
|
||||||
const files = watch("billAttachments");
|
const files = watch("billAttachments");
|
||||||
const onFileChange = async (e) => {
|
const onFileChange = async (e) => {
|
||||||
const newFiles = Array.from(e.target.files);
|
const newFiles = Array.from(e.target.files);
|
||||||
@ -142,11 +150,10 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expenseToEdit && data ) {
|
if (expenseToEdit && data) {
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
projectId: data.project.id || "",
|
projectId: data.project.id || "",
|
||||||
expensesTypeId: data.expensesType.id || "",
|
expenseCategoryId: data?.expenseCategory?.id || "",
|
||||||
paymentModeId: data.paymentMode.id || "",
|
paymentModeId: data.paymentMode.id || "",
|
||||||
paidById: data.paidBy.id || "",
|
paidById: data.paidBy.id || "",
|
||||||
transactionDate: data.transactionDate?.slice(0, 10) || "",
|
transactionDate: data.transactionDate?.slice(0, 10) || "",
|
||||||
@ -156,7 +163,8 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
supplerName: data.supplerName || "",
|
supplerName: data.supplerName || "",
|
||||||
amount: data.amount || "",
|
amount: data.amount || "",
|
||||||
noOfPersons: data.noOfPersons || "",
|
noOfPersons: data.noOfPersons || "",
|
||||||
gstNumber:data.gstNumber || "",
|
gstNumber: data.gstNumber || "",
|
||||||
|
currencyId: data.currencyId || DEFAULT_CURRENCY,
|
||||||
billAttachments: data.documents
|
billAttachments: data.documents
|
||||||
? data.documents.map((doc) => ({
|
? data.documents.map((doc) => ({
|
||||||
fileName: doc.fileName,
|
fileName: doc.fileName,
|
||||||
@ -183,8 +191,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
const onSubmit = (fromdata) => {
|
const onSubmit = (fromdata) => {
|
||||||
let payload = {
|
let payload = {
|
||||||
...fromdata,
|
...fromdata,
|
||||||
transactionDate: localToUtc(fromdata.transactionDate)
|
transactionDate: localToUtc(fromdata.transactionDate),
|
||||||
|
|
||||||
};
|
};
|
||||||
if (expenseToEdit) {
|
if (expenseToEdit) {
|
||||||
const editPayload = { ...payload, id: data.id };
|
const editPayload = { ...payload, id: data.id };
|
||||||
@ -193,10 +200,12 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
CreateExpense(payload);
|
CreateExpense(payload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const ExpenseTypeId = watch("expensesTypeId");
|
const ExpenseTypeId = watch("expenseCategoryId");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId));
|
setExpenseType(
|
||||||
|
ExpenseCategories?.find((type) => type.id === ExpenseTypeId)
|
||||||
|
);
|
||||||
}, [ExpenseTypeId]);
|
}, [ExpenseTypeId]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -206,7 +215,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
if (StatusLoadding || projectLoading || ExpenseLoading || isLoading)
|
if (StatusLoadding || projectLoading || ExpenseLoading || isLoading)
|
||||||
return <ExpenseSkeleton />;
|
return <ExpenseSkeleton />;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container p-3">
|
<div className="container p-3">
|
||||||
<h5 className="m-0">
|
<h5 className="m-0">
|
||||||
@ -215,7 +223,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label className="form-label" required>Select Project</Label>
|
<Label className="form-label" required>
|
||||||
|
Select Project
|
||||||
|
</Label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
{...register("projectId")}
|
{...register("projectId")}
|
||||||
@ -237,13 +247,13 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label htmlFor="expensesTypeId" className="form-label" required>
|
<Label htmlFor="expenseCategoryId" className="form-label" required>
|
||||||
Expense Type
|
Expense Category
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
id="expensesTypeId"
|
id="expenseCategoryId"
|
||||||
{...register("expensesTypeId")}
|
{...register("expenseCategoryId")}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
Select Type
|
Select Type
|
||||||
@ -251,16 +261,16 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
{ExpenseLoading ? (
|
{ExpenseLoading ? (
|
||||||
<option disabled>Loading...</option>
|
<option disabled>Loading...</option>
|
||||||
) : (
|
) : (
|
||||||
ExpenseTypes?.map((expense) => (
|
ExpenseCategories?.map((expense) => (
|
||||||
<option key={expense.id} value={expense.id}>
|
<option key={expense.id} value={expense.id}>
|
||||||
{expense.name}
|
{expense.name}
|
||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
{errors.expensesTypeId && (
|
{errors.expensesCategoryId && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
{errors.expensesTypeId.message}
|
{errors.expensesCategoryId.message}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -295,33 +305,16 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-md-6 text-start">
|
||||||
<div className="col-md-6">
|
<Label className="form-label" required>
|
||||||
<Label htmlFor="paidById" className="form-label" required>
|
Paid By{" "}
|
||||||
Paid By
|
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<EmployeeSearchInput
|
||||||
className="form-select form-select-sm"
|
control={control}
|
||||||
id="paymentModeId"
|
name="paidById"
|
||||||
{...register("paidById")}
|
projectId={null}
|
||||||
disabled={!selectedproject}
|
forAll={expenseToEdit ? true : false}
|
||||||
>
|
/>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -330,7 +323,12 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<Label htmlFor="transactionDate" className="form-label" required>
|
<Label htmlFor="transactionDate" className="form-label" required>
|
||||||
Transaction Date
|
Transaction Date
|
||||||
</Label>
|
</Label>
|
||||||
<DatePicker name="transactionDate" control={control} maxDate={new Date()}/>
|
<DatePicker
|
||||||
|
name="transactionDate"
|
||||||
|
className="w-100"
|
||||||
|
control={control}
|
||||||
|
maxDate={new Date()}
|
||||||
|
/>
|
||||||
|
|
||||||
{errors.transactionDate && (
|
{errors.transactionDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -409,9 +407,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label htmlFor="statusId" className="form-label ">
|
<label htmlFor="statusId" className="form-label ">
|
||||||
GST Number
|
GST Number
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -421,34 +419,62 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
{...register("gstNumber")}
|
{...register("gstNumber")}
|
||||||
/>
|
/>
|
||||||
{errors.gstNumber && (
|
{errors.gstNumber && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">{errors.gstNumber.message}</small>
|
||||||
{errors.gstNumber.message}
|
|
||||||
</small>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ExpenseType?.noOfPersonsRequired && (
|
|
||||||
<div className="col-md-6 mt-2 text-start">
|
|
||||||
<label className="form-label ">No. of Persons</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="noOfPersons"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("noOfPersons")}
|
|
||||||
inputMode="numeric"
|
|
||||||
/>
|
|
||||||
{errors.noOfPersons && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.noOfPersons.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<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>
|
||||||
|
{ExpenseType?.noOfPersonsRequired && (
|
||||||
|
<div className="col-md-6 text-start">
|
||||||
|
<Label className="form-label" required>No. of Persons</Label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="noOfPersons"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("noOfPersons")}
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
{errors.noOfPersons && (
|
||||||
|
<small className="danger-text">
|
||||||
|
{errors.noOfPersons.message}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<Label htmlFor="description" className="form-label" required>Description</Label>
|
<Label htmlFor="description" className="form-label" required>
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@ -465,7 +491,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
|
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<Label className="form-label" required>Upload Bill </Label>
|
<Label className="form-label" required>
|
||||||
|
Upload Bill{" "}
|
||||||
|
</Label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||||
@ -497,40 +525,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="d-block">
|
<Filelist
|
||||||
{files
|
files={files}
|
||||||
.filter((file) => {
|
removeFile={removeFile}
|
||||||
if (expenseToEdit) {
|
expenseToEdit={expenseToEdit}
|
||||||
return file.isActive;
|
/>
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((file, idx) => (
|
|
||||||
<a
|
|
||||||
key={idx}
|
|
||||||
className="d-flex justify-content-between text-start p-1"
|
|
||||||
href={file.preSignedUrl || "#"}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span className="mb-0 text-secondary small d-block">
|
|
||||||
{file.fileName}
|
|
||||||
</span>
|
|
||||||
<span className="text-body-secondary small d-block">
|
|
||||||
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
removeFile(expenseToEdit ? file.documentId : idx);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{Array.isArray(errors.billAttachments) &&
|
{Array.isArray(errors.billAttachments) &&
|
||||||
@ -549,7 +548,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
|
|
||||||
<div className="d-flex justify-content-end gap-3">
|
<div className="d-flex justify-content-end gap-3">
|
||||||
{" "}
|
{" "}
|
||||||
<button
|
<button
|
||||||
type="reset"
|
type="reset"
|
||||||
disabled={isPending || createPending}
|
disabled={isPending || createPending}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
@ -566,9 +565,8 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
? "Please Wait..."
|
? "Please Wait..."
|
||||||
: expenseToEdit
|
: expenseToEdit
|
||||||
? "Update"
|
? "Update"
|
||||||
: "Submit"}
|
: "Save as Draft"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,27 +1,53 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
|
|
||||||
const PreviewDocument = ({ imageUrl }) => {
|
const PreviewDocument = ({ imageUrl }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [rotation, setRotation] = useState(0);
|
||||||
|
|
||||||
return (
|
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 && (
|
{loading && (
|
||||||
<div className="text-secondary text-center mb-2">
|
<div className="text-secondary text-center mb-2">Loading...</div>
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<img
|
|
||||||
src={imageUrl}
|
<div className="mb-3 d-flex justify-content-center align-items-center">
|
||||||
alt="Full View"
|
<img
|
||||||
className="img-fluid"
|
src={imageUrl}
|
||||||
style={{
|
alt="Full View"
|
||||||
maxHeight: "100vh",
|
className="img-fluid"
|
||||||
objectFit: "contain",
|
style={{
|
||||||
display: loading ? "none" : "block",
|
maxHeight: "80vh",
|
||||||
}}
|
objectFit: "contain",
|
||||||
onLoad={() => setLoading(false)}
|
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>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,13 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
||||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||||
import { getColorNameFromHex, getIconByFileType } from "../../utils/appUtils";
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
formatFigure,
|
||||||
|
getColorNameFromHex,
|
||||||
|
getIconByFileType,
|
||||||
|
localToUtc,
|
||||||
|
} from "../../utils/appUtils";
|
||||||
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import {
|
import {
|
||||||
@ -38,7 +44,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||||
const [imageLoaded, setImageLoaded] = useState({});
|
const [imageLoaded, setImageLoaded] = useState({});
|
||||||
const { setDocumentView } = useExpenseContext();
|
const { setDocumentView } = useExpenseContext();
|
||||||
const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({});
|
const ActionSchema = ExpenseActionScheam(IsPaymentProcess,data?.createdAt) ?? z.object({});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -91,9 +97,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
const Payload = {
|
const Payload = {
|
||||||
...formData,
|
...formData,
|
||||||
reimburseDate: moment
|
reimburseDate:localToUtc(formData.reimburseDate),
|
||||||
.utc(formData.reimburseDate, "DD-MM-YYYY")
|
|
||||||
.toISOString(),
|
|
||||||
expenseId: ExpenseId,
|
expenseId: ExpenseId,
|
||||||
comment: formData.comment,
|
comment: formData.comment,
|
||||||
};
|
};
|
||||||
@ -105,361 +109,430 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
const handleImageLoad = (id) => {
|
const handleImageLoad = (id) => {
|
||||||
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
||||||
};
|
};
|
||||||
|
console.log(errors)
|
||||||
return (
|
return (
|
||||||
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row mb-3">
|
<div className="col-12 mb-1">
|
||||||
<div className="col-12 mb-3">
|
<h5 className="fw-semibold m-0">Expense Details</h5>
|
||||||
<h5 className="fw-semibold">Expense Details</h5>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Row 2 */}
|
<div className="row mb-1 ">
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-12 col-lg-7 col-xl-7 mb-3">
|
||||||
<div className="d-flex">
|
<div className="row">
|
||||||
<label
|
<div className="col-12 d-flex justify-content-between text-start fw-semibold my-2">
|
||||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
<span>{data?.expenseUId}</span>
|
||||||
style={{ minWidth: "130px" }}
|
<span
|
||||||
>
|
className={`badge bg-label-${
|
||||||
Supplier :
|
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||||
</label>
|
}`}
|
||||||
<div className="text-muted">{data?.supplerName}</div>
|
t
|
||||||
</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" }}
|
|
||||||
>
|
>
|
||||||
GST Number :
|
{data?.status?.name}
|
||||||
</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"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
{/* Row 1 */}
|
||||||
<label className="form-label me-2 mb-2 fw-semibold">Attachment :</label>
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
<div className="d-flex flex-wrap gap-2">
|
<label
|
||||||
{data?.documents?.map((doc) => {
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
const isImage = doc.contentType?.includes("image");
|
style={{ minWidth: "130px" }}
|
||||||
|
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
{doc.fileName}
|
Transaction Date :
|
||||||
</small>
|
</label>
|
||||||
|
<div className="text-muted">
|
||||||
|
{formatUTCToLocalTime(data?.transactionDate)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.expensesReimburse && (
|
<div className="col-md-6 mb-3">
|
||||||
<div className="row text-start mt-2">
|
<div className="d-flex">
|
||||||
<div className="col-md-6 mb-sm-0 mb-2">
|
<label
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
Transaction ID :
|
style={{ minWidth: "130px" }}
|
||||||
</label>
|
>
|
||||||
{data.expensesReimburse.reimburseTransactionId || "N/A"}
|
Expense Category :
|
||||||
</div>
|
</label>
|
||||||
<div className="col-md-6 ">
|
<div className="text-muted">{data?.expenseCategory?.name}</div>
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
</div>
|
||||||
Reimburse Date :
|
</div>
|
||||||
</label>
|
|
||||||
{formatUTCToLocalTime(data.expensesReimburse.reimburseDate)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.expensesReimburse && (
|
{/* Row 2 */}
|
||||||
<>
|
<div className="col-md-6 mb-3">
|
||||||
<div className="col-md-6 d-flex align-items-center">
|
<div className="d-flex">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold">
|
<label
|
||||||
Reimburse By :
|
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>
|
</label>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="xs"
|
size="xs"
|
||||||
classAvatar="m-0 me-1"
|
classAvatar="m-0 me-1"
|
||||||
firstName={data?.expensesReimburse?.reimburseBy?.firstName}
|
firstName={data.paidBy?.firstName}
|
||||||
lastName={data?.expensesReimburse?.reimburseBy?.lastName}
|
lastName={data.paidBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
{`${data?.expensesReimburse?.reimburseBy?.firstName} ${data?.expensesReimburse?.reimburseBy?.lastName}`.trim()}
|
{`${data.paidBy?.firstName ?? ""} ${
|
||||||
|
data.paidBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<hr className="divider my-1 border-2 divider-primary my-2" />
|
|
||||||
|
|
||||||
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
|
{/* Description */}
|
||||||
<>
|
<div className="col-12 text-start mb-2">
|
||||||
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
|
<label className="fw-semibold form-label">Description : </label>
|
||||||
<div className="row">
|
<div className="text-muted">{data?.description}</div>
|
||||||
<div className="col-12 col-md-6 text-start">
|
</div>
|
||||||
<label className="form-label">Transaction Id </label>
|
|
||||||
<input
|
{/* Attachments */}
|
||||||
type="text"
|
<div className="col-12 text-start mb-2">
|
||||||
className="form-control form-control-sm"
|
<label className="form-label me-2 mb-2 fw-semibold">
|
||||||
{...register("reimburseTransactionId")}
|
Attachment :
|
||||||
/>
|
</label>
|
||||||
{errors.reimburseTransactionId && (
|
<div className="d-flex flex-wrap gap-2">
|
||||||
<small className="danger-text">
|
{data?.documents?.map((doc) => {
|
||||||
{errors.reimburseTransactionId.message}
|
const isImage = doc.contentType?.includes("image");
|
||||||
</small>
|
return (
|
||||||
)}
|
<div
|
||||||
</div>
|
key={doc.documentId}
|
||||||
<div className="col-12 col-md-6 text-start">
|
className="d-flex align-items-center cusor-pointer"
|
||||||
<label className="form-label">Transaction Date </label>
|
onClick={() => {
|
||||||
<DatePicker
|
if (isImage) {
|
||||||
name="reimburseDate"
|
setDocumentView({
|
||||||
control={control}
|
IsOpen: true,
|
||||||
minDate={data?.transactionDate}
|
Image: doc.preSignedUrl,
|
||||||
/>
|
});
|
||||||
{errors.reimburseDate && (
|
}
|
||||||
<small className="danger-text">
|
}}
|
||||||
{errors.reimburseDate.message}
|
>
|
||||||
</small>
|
<i
|
||||||
)}
|
className={`bx ${getIconByFileType(doc.contentType)}`}
|
||||||
</div>
|
style={{ fontSize: "30px" }}
|
||||||
<div className="col-12 col-md-6 text-start">
|
></i>
|
||||||
<label className="form-label">Reimburse By </label>
|
<small
|
||||||
<EmployeeSearchInput
|
className="text-center text-tiny text-truncate w-100"
|
||||||
control={control}
|
title={doc.fileName}
|
||||||
name="reimburseById"
|
>
|
||||||
projectId={null}
|
{doc.fileName}
|
||||||
/>
|
</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="col-12 mb-3 text-start">
|
{data.expensesReimburse && (
|
||||||
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
|
<div className="row text-start mt-2">
|
||||||
(IsRejectedExpense && isCreatedBy)) && (
|
<div className="col-md-6 mb-sm-0 mb-2">
|
||||||
<>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
<Label className="form-label me-2 mb-0" required>Comment</Label>
|
Transaction ID :
|
||||||
<textarea
|
</label>
|
||||||
className="form-control form-control-sm"
|
{data.expensesReimburse.reimburseTransactionId || "N/A"}
|
||||||
{...register("comment")}
|
</div>
|
||||||
rows="2"
|
<div className="col-md-6 ">
|
||||||
/>
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
{errors.comment && (
|
Reimburse Date :
|
||||||
<small className="danger-text">
|
</label>
|
||||||
{errors.comment.message}
|
{formatUTCToLocalTime(data.expensesReimburse.reimburseDate)}
|
||||||
</small>
|
</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 className="w-100"
|
||||||
|
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>
|
||||||
</>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback, useEffect, useState, useMemo } from "react";
|
||||||
import getGreetingMessage from "../../utils/greetingHandler";
|
import getGreetingMessage from "../../utils/greetingHandler";
|
||||||
import {
|
import {
|
||||||
cacheData,
|
cacheData,
|
||||||
@ -14,121 +15,104 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|||||||
import Avatar from "../../components/common/Avatar";
|
import Avatar from "../../components/common/Avatar";
|
||||||
import { useChangePassword } from "../Context/ChangePasswordContext";
|
import { useChangePassword } from "../Context/ChangePasswordContext";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
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";
|
import { useAuthModal, useLogout } from "../../hooks/useAuth";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { profile } = useProfile();
|
const { profile } = useProfile();
|
||||||
|
const { data: masterData } = useMaster();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { data, loading } = useMaster();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {onOpen} = useAuthModal()
|
|
||||||
|
const { mutate: logout, isPending: logouting } = useLogout();
|
||||||
|
const { onOpen } = useAuthModal();
|
||||||
|
const { openChangePassword } = useChangePassword();
|
||||||
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
|
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
|
||||||
const { mutate : logout,isPending:logouting} = useLogout()
|
|
||||||
|
|
||||||
const isDashboardPath =
|
const pathname = location.pathname;
|
||||||
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
|
|
||||||
const isProjectPath = /^\/projects$/.test(location.pathname);
|
|
||||||
|
|
||||||
const showProjectDropdown = (pathname) => {
|
// ======= MEMO CHECKS =======
|
||||||
const isDirectoryPath = /^\/directory$/.test(pathname);
|
|
||||||
|
|
||||||
// const isProfilePage = /^\/employee$/.test(location.pathname);
|
const isDashboardPath = pathname === "/" || pathname === "/dashboard";
|
||||||
const isProfilePage =
|
const isProjectPath = pathname === "/projects";
|
||||||
/^\/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(
|
const isDirectory = pathname === "/directory";
|
||||||
pathname
|
const isEmployeeList = pathname === "/employees";
|
||||||
);
|
const isEmployeeProfile = UUID_REGEX.test(pathname);
|
||||||
const isExpensePage = /^\/expenses$/.test(pathname);
|
const isMasters = pathname === "/masters";
|
||||||
|
const isExpensePath = pathname.startsWith("/expenses");
|
||||||
|
|
||||||
return !(isDirectoryPath || isProfilePage || isExpensePage);
|
const hideDropPaths =
|
||||||
};
|
isDirectory || isEmployeeList || isMasters || isEmployeeProfile || isExpensePath;
|
||||||
const allowedProjectStatusIds = [
|
|
||||||
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
|
|
||||||
"cdad86aa-8a56-4ff4-b633-9c629057dfef",
|
|
||||||
"b74da4c2-d07e-46f2-9919-e75e49b12731",
|
|
||||||
];
|
|
||||||
|
|
||||||
const getRole = (roles, joRoleId) => {
|
const showProjectDropdown = !hideDropPaths;
|
||||||
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}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// ===== Project Names & Selected Project =====
|
||||||
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
||||||
|
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
|
|
||||||
const projectsForDropdown = isDashboardPath
|
const projectsForDropdown = useMemo(
|
||||||
? projectNames
|
() =>
|
||||||
: projectNames?.filter((project) =>
|
isDashboardPath
|
||||||
allowedProjectStatusIds.includes(project.projectStatusId)
|
? projectNames
|
||||||
);
|
: projectNames?.filter((project) =>
|
||||||
|
ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId)
|
||||||
|
),
|
||||||
|
[projectNames, isDashboardPath]
|
||||||
|
);
|
||||||
|
|
||||||
let currentProjectDisplayName;
|
const currentProjectDisplayName = useMemo(() => {
|
||||||
if (projectLoading) {
|
if (projectLoading) return "Loading...";
|
||||||
currentProjectDisplayName = "Loading...";
|
if (!projectNames?.length) return "No Projects Assigned";
|
||||||
} else if (!projectNames || projectNames.length === 0) {
|
if (projectNames.length === 1) return projectNames[0].name;
|
||||||
currentProjectDisplayName = "No Projects Assigned";
|
if (selectedProject === null) return "All Projects";
|
||||||
} else if (projectNames.length === 1) {
|
const selectedObj = projectNames.find((p) => p.id === selectedProject);
|
||||||
currentProjectDisplayName = projectNames[0].name;
|
return selectedObj
|
||||||
} else {
|
? selectedObj.name
|
||||||
if (selectedProject === null) {
|
: projectNames[0]?.name || "No Projects Assigned";
|
||||||
currentProjectDisplayName = "All Projects";
|
}, [projectLoading, projectNames, selectedProject]);
|
||||||
} else {
|
|
||||||
const selectedProjectObj = projectNames.find(
|
|
||||||
(p) => p?.id === selectedProject
|
|
||||||
);
|
|
||||||
currentProjectDisplayName = selectedProjectObj
|
|
||||||
? selectedProjectObj.name
|
|
||||||
: "All Projects";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
projectNames &&
|
projectNames?.length &&
|
||||||
projectNames.length > 0 &&
|
|
||||||
selectedProject === undefined &&
|
selectedProject === undefined &&
|
||||||
!getCachedData("hasReceived")
|
!getCachedData("hasReceived")
|
||||||
) {
|
) {
|
||||||
if (projectNames.length === 1) {
|
if (projectNames.length === 1) {
|
||||||
dispatch(setProjectId(projectNames[0]?.id || null));
|
dispatch(setProjectId(projectNames[0].id || null));
|
||||||
} else {
|
} else {
|
||||||
if (isDashboardPath) {
|
if (isDashboardPath) {
|
||||||
dispatch(setProjectId(null));
|
dispatch(setProjectId(null));
|
||||||
} else {
|
} else {
|
||||||
const firstAllowedProject = projectNames.find((project) =>
|
const firstAllowed = projectNames.find((project) =>
|
||||||
allowedProjectStatusIds.includes(project.projectStatusId)
|
ALLOW_PROJECTSTATUS_ID.includes(project.projectStatusId)
|
||||||
);
|
);
|
||||||
dispatch(setProjectId(firstAllowedProject?.id || null));
|
dispatch(setProjectId(firstAllowed?.id || null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [projectNames, selectedProject, dispatch, isDashboardPath]);
|
}, [projectNames, selectedProject, dispatch, isDashboardPath]);
|
||||||
|
|
||||||
|
// ===== Event Handlers =====
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
if (!HasManageProjectPermission) {
|
if (!HasManageProjectPermission) {
|
||||||
await fetchData();
|
await fetchData();
|
||||||
const projectExist = data.projectIds.some(
|
if (data.projectIds?.includes(selectedProject)) {
|
||||||
(item) => item === selectedProject
|
|
||||||
);
|
|
||||||
if (projectExist) {
|
|
||||||
cacheData("hasReceived", false);
|
cacheData("hasReceived", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,14 +122,15 @@ const Header = () => {
|
|||||||
|
|
||||||
const newProjectHandler = useCallback(
|
const newProjectHandler = useCallback(
|
||||||
async (msg) => {
|
async (msg) => {
|
||||||
if (HasManageProjectPermission && msg.keyword === "Create_Project") {
|
if (
|
||||||
await fetchData();
|
msg.keyword === "Create_Project" ||
|
||||||
} else if (projectNames?.some((item) => item.id === msg.response.id)) {
|
projectNames?.some((p) => p.id === msg.response?.id)
|
||||||
|
) {
|
||||||
await fetchData();
|
await fetchData();
|
||||||
|
cacheData("hasReceived", false);
|
||||||
}
|
}
|
||||||
cacheData("hasReceived", false);
|
|
||||||
},
|
},
|
||||||
[HasManageProjectPermission, projectNames, fetchData]
|
[projectNames, fetchData]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -162,10 +147,10 @@ const Header = () => {
|
|||||||
};
|
};
|
||||||
}, [handler, newProjectHandler]);
|
}, [handler, newProjectHandler]);
|
||||||
|
|
||||||
const handleProjectChange = (project) => {
|
// ===== Project Change =====
|
||||||
dispatch(setProjectId(project));
|
const handleProjectChange = (projectId) => {
|
||||||
|
dispatch(setProjectId(projectId));
|
||||||
if (isProjectPath && project !== null) {
|
if (isProjectPath && projectId !== null) {
|
||||||
navigate("/projects/details");
|
navigate("/projects/details");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -189,7 +174,7 @@ const Header = () => {
|
|||||||
className="navbar-nav-right d-flex align-items-center justify-content-between"
|
className="navbar-nav-right d-flex align-items-center justify-content-between"
|
||||||
id="navbar-collapse"
|
id="navbar-collapse"
|
||||||
>
|
>
|
||||||
{showProjectDropdown(location.pathname) && (
|
{showProjectDropdown && (
|
||||||
<div className="align-items-center">
|
<div className="align-items-center">
|
||||||
<i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>
|
<i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
@ -215,16 +200,14 @@ const Header = () => {
|
|||||||
className="dropdown-menu"
|
className="dropdown-menu"
|
||||||
style={{ overflow: "auto", maxHeight: "300px" }}
|
style={{ overflow: "auto", maxHeight: "300px" }}
|
||||||
>
|
>
|
||||||
{isDashboardPath && (
|
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => handleProjectChange(null)}
|
onClick={() => handleProjectChange(null)}
|
||||||
>
|
>All Project</button>
|
||||||
All Projects
|
</li>
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{[...projectsForDropdown]
|
{[...projectsForDropdown]
|
||||||
.sort((a, b) => a?.name?.localeCompare(b.name))
|
.sort((a, b) => a?.name?.localeCompare(b.name))
|
||||||
.map((project) => (
|
.map((project) => (
|
||||||
@ -249,112 +232,9 @@ const Header = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ul className="navbar-nav flex-row align-items-center ms-md-auto">
|
<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">
|
{/* {HasManageProjectPermission && ( */}
|
||||||
<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>
|
|
||||||
<li className="nav-item navbar-dropdown dropdown-user dropdown">
|
<li className="nav-item navbar-dropdown dropdown-user dropdown">
|
||||||
<a
|
<a
|
||||||
aria-label="dropdown profile avatar"
|
aria-label="dropdown profile avatar"
|
||||||
@ -386,7 +266,7 @@ const Header = () => {
|
|||||||
{profile?.employeeInfo?.firstName}
|
{profile?.employeeInfo?.firstName}
|
||||||
</span>
|
</span>
|
||||||
<small className="text-muted">
|
<small className="text-muted">
|
||||||
{getRole(data, profile?.employeeInfo?.joRoleId)}
|
{getRole(masterData, profile?.employeeInfo?.joRoleId)}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -395,15 +275,13 @@ const Header = () => {
|
|||||||
<li>
|
<li>
|
||||||
<div className="dropdown-divider"></div>
|
<div className="dropdown-divider"></div>
|
||||||
</li>
|
</li>
|
||||||
<li onClick={()=>onOpen()}>
|
{/* <li onClick={() => onOpen()}>
|
||||||
{" "}
|
{" "}
|
||||||
<a
|
<a className="dropdown-item cusor-pointer">
|
||||||
className="dropdown-item cusor-pointer"
|
|
||||||
>
|
|
||||||
<i className="bx bx-transfer-alt me-2"></i>
|
<i className="bx bx-transfer-alt me-2"></i>
|
||||||
<span className="align-middle">Switch Workspace</span>
|
<span className="align-middle">Switch Workspace</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li> */}
|
||||||
<li onClick={handleProfilePage}>
|
<li onClick={handleProfilePage}>
|
||||||
<a
|
<a
|
||||||
aria-label="go to profile"
|
aria-label="go to profile"
|
||||||
@ -433,7 +311,6 @@ const Header = () => {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<div className="dropdown-divider"></div>
|
<div className="dropdown-divider"></div>
|
||||||
</li>
|
</li>
|
||||||
@ -441,10 +318,17 @@ const Header = () => {
|
|||||||
<a
|
<a
|
||||||
aria-label="click to log out"
|
aria-label="click to log out"
|
||||||
className="dropdown-item cusor-pointer"
|
className="dropdown-item cusor-pointer"
|
||||||
onClick={()=>logout()}
|
onClick={() => logout()}
|
||||||
>
|
>
|
||||||
{logouting ? "Please Wait":<> <i className="bx bx-log-out me-2"></i>
|
{logouting ? (
|
||||||
<span className="align-middle">SignOut</span></>}
|
"Please Wait"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<i className="bx bx-log-out me-2"></i>
|
||||||
|
<span className="align-middle">SignOut</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
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;
|
||||||
530
src/components/PaymentRequest/ManagePaymentRequest.jsx
Normal file
@ -0,0 +1,530 @@
|
|||||||
|
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 Filelist from "../Expenses/Filelist";
|
||||||
|
import InputSuggestions from "../common/InputSuggestion";
|
||||||
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
|
|
||||||
|
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error: requestError,
|
||||||
|
} = usePaymentRequestDetail(requestToEdit);
|
||||||
|
|
||||||
|
const { profile } = useProfile();
|
||||||
|
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?.expenseCategory}
|
||||||
|
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;
|
||||||
380
src/components/PaymentRequest/PaymentRequestList.jsx
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
EXPENSE_DRAFT,
|
||||||
|
EXPENSE_REJECTEDBY,
|
||||||
|
ITEMS_PER_PAGE,
|
||||||
|
} from "../../utils/constants";
|
||||||
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
formatFigure,
|
||||||
|
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";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
|
||||||
|
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) =>
|
||||||
|
formatFigure(e?.amount, {
|
||||||
|
type: "currency",
|
||||||
|
currency: e?.currency?.currencyCode,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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 paginate = (page) => {
|
||||||
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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 */}
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data?.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
</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: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
558
src/components/PaymentRequest/ViewPaymentRequest.jsx
Normal file
@ -0,0 +1,558 @@
|
|||||||
|
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_PROCESSED,
|
||||||
|
EXPENSE_REJECTEDBY,
|
||||||
|
EXPENSE_STATUS,
|
||||||
|
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 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_HEADING = {
|
||||||
|
[EXPENSE_STATUS.daft]: "Initiation",
|
||||||
|
[EXPENSE_STATUS.review_pending]: "Review & Validation",
|
||||||
|
[EXPENSE_STATUS.approve_pending]: "Approval",
|
||||||
|
[EXPENSE_STATUS.payment_pending]: " Processing & Disbursement",
|
||||||
|
};
|
||||||
|
|
||||||
|
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 - {STATUS_HEADING[data?.expenseStatus?.id] || "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}
|
||||||
|
/>
|
||||||
|
{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>
|
||||||
|
<inputj
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("baseAmount", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
{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="number"
|
||||||
|
step="0.01"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("taxAmount", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
{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) && (data?.expenseStatus.id === EXPENSE_PROCESSED) ? (
|
||||||
|
<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 && (
|
{IsOpenModal && (
|
||||||
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
|
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
|
||||||
<ManageProjectInfo
|
<ManageProjectInfo
|
||||||
project={projects_Details}
|
project={projects_Details?.id}
|
||||||
handleSubmitForm={handleFormSubmit}
|
handleSubmitForm={handleFormSubmit}
|
||||||
onClose={() => setIsOpenModal(false)}
|
onClose={() => setIsOpenModal(false)}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
|
|||||||
@ -5,7 +5,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
|
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
import { useCreateProject, useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
|
import {
|
||||||
|
useCreateProject,
|
||||||
|
useProjectDetails,
|
||||||
|
useUpdateProject,
|
||||||
|
} from "../../hooks/useProjects";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_EMPTY_STATUS_ID,
|
DEFAULT_EMPTY_STATUS_ID,
|
||||||
@ -17,6 +21,8 @@ import {
|
|||||||
useOrganizationsList,
|
useOrganizationsList,
|
||||||
} from "../../hooks/useOrganization";
|
} from "../../hooks/useOrganization";
|
||||||
import { localToUtc } from "../../utils/appUtils";
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
|
import Modal from "../common/Modal";
|
||||||
|
import { useModal } from "../../hooks/useAuth";
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleDateString("en-CA");
|
const currentDate = new Date().toLocaleDateString("en-CA");
|
||||||
const formatDate = (date) => {
|
const formatDate = (date) => {
|
||||||
@ -32,18 +38,24 @@ const formatDate = (date) => {
|
|||||||
const ManageProjectInfo = ({ project, onClose }) => {
|
const ManageProjectInfo = ({ project, onClose }) => {
|
||||||
const [addressLength, setAddressLength] = useState(0);
|
const [addressLength, setAddressLength] = useState(0);
|
||||||
const maxAddressLength = 500;
|
const maxAddressLength = 500;
|
||||||
const { onOpen, startStep, flowType } = useOrganizationModal();
|
const { onOpen, startStep, flowType } = useModal("ManageProject");
|
||||||
|
|
||||||
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
||||||
|
|
||||||
const { projects_Details, loading } = useProjectDetails(project);
|
const { projects_Details, loading } = useProjectDetails(project);
|
||||||
const { data, isLoading, isError, error } = useOrganizationsList(
|
// const { data, isLoading, isError, error } = useOrganizationsList(
|
||||||
ITEMS_PER_PAGE,
|
// ITEMS_PER_PAGE,
|
||||||
1,
|
// 1,
|
||||||
true
|
// 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 {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -70,11 +82,11 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
projectStatusId:
|
projectStatusId:
|
||||||
String(projects_Details?.projectStatus?.id) ||
|
String(projects_Details?.projectStatus?.id) ||
|
||||||
DEFAULT_EMPTY_STATUS_IDF,
|
DEFAULT_EMPTY_STATUS_IDF,
|
||||||
promoterId: projects_Details?.promoter?.id || "",
|
// promoterId: projects_Details?.promoter?.id || "", // hide temp. for version 1
|
||||||
pmcId: projects_Details?.pmc?.id || "",
|
// pmcId: projects_Details?.pmc?.id || "",
|
||||||
});
|
});
|
||||||
setAddressLength(projects_Details?.projectAddress?.length || 0);
|
setAddressLength(projects_Details?.projectAddress?.length || 0);
|
||||||
}, [project, projects_Details, reset,data]);
|
}, [project, projects_Details, reset]);
|
||||||
|
|
||||||
const onSubmitForm = (formData) => {
|
const onSubmitForm = (formData) => {
|
||||||
if (project) {
|
if (project) {
|
||||||
@ -85,13 +97,13 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
id: project,
|
id: project,
|
||||||
};
|
};
|
||||||
UpdateProject({ projectId: project, payload: payload });
|
UpdateProject({ projectId: project, payload: payload });
|
||||||
}else{
|
} else {
|
||||||
let payload = {
|
let payload = {
|
||||||
...formData,
|
...formData,
|
||||||
startDate: localToUtc(formData.startDate),
|
startDate: localToUtc(formData.startDate),
|
||||||
endDate: localToUtc(formData.endDate),
|
endDate: localToUtc(formData.endDate),
|
||||||
};
|
};
|
||||||
CeateProject(payload)
|
CeateProject(payload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,7 +116,6 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
onOpen({ startStep: 2, flowType: "default" });
|
onOpen({ startStep: 2, flowType: "default" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-sm-2 p-2">
|
<div className="p-sm-2 p-2">
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
@ -254,7 +265,7 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 ">
|
{/* <div className="col-12 ">
|
||||||
<label className="form-label" htmlFor="modalEditUserStatus">
|
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||||
Promoter
|
Promoter
|
||||||
</label>
|
</label>
|
||||||
@ -330,7 +341,7 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
<small className="cursor-pointer" onClick={handleOrganizaioFinder}>
|
<small className="cursor-pointer" onClick={handleOrganizaioFinder}>
|
||||||
<i className="bx bx-plus-circle text-primary"></i>
|
<i className="bx bx-plus-circle text-primary"></i>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="col-12 col-md-12">
|
<div className="col-12 col-md-12">
|
||||||
<Label htmlFor="projectAddress" required>
|
<Label htmlFor="projectAddress" required>
|
||||||
@ -376,7 +387,11 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
disabled={isPending || isCreating}
|
disabled={isPending || isCreating}
|
||||||
>
|
>
|
||||||
{isPending||isCreating ? "Please Wait..." : project ? "Update" : "Submit"}
|
{isPending || isCreating
|
||||||
|
? "Please Wait..."
|
||||||
|
: project
|
||||||
|
? "Update"
|
||||||
|
: "Submit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -385,3 +400,15 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ManageProjectInfo;
|
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 = () => {
|
const handleViewProject = () => {
|
||||||
dispatch(setProjectId(project.id));
|
dispatch(setProjectId(project.id));
|
||||||
|
localStorage.setItem("lastActiveProjectTab","profile")
|
||||||
navigate(`/projects/details`);
|
navigate(`/projects/details`);
|
||||||
};
|
};
|
||||||
const handleViewActivities = () => {
|
const handleViewActivities = () => {
|
||||||
@ -54,7 +55,7 @@ const ProjectCard = ({ project }) => {
|
|||||||
style={{ fontSize: "xx-large" }}
|
style={{ fontSize: "xx-large" }}
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div className="me-2">
|
<div className="me-2 text-start">
|
||||||
<h5
|
<h5
|
||||||
className="mb-0 stretched-link text-heading text-start"
|
className="mb-0 stretched-link text-heading text-start"
|
||||||
onClick={handleViewProject}
|
onClick={handleViewProject}
|
||||||
@ -66,7 +67,7 @@ const ProjectCard = ({ project }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`ms-auto ${!ManageProject && "d-none"}`}>
|
<div className={`ms-auto `}>
|
||||||
<div className="dropdown z-2">
|
<div className="dropdown z-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -106,12 +107,6 @@ const ProjectCard = ({ project }) => {
|
|||||||
<span className="align-left">Modify</span>
|
<span className="align-left">Modify</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,70 +1,62 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { useProjects } from '../../hooks/useProjects'
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import Loader from '../common/Loader'
|
import Loader from "../common/Loader";
|
||||||
import ProjectCard from './ProjectCard'
|
import ProjectCard from "./ProjectCard";
|
||||||
|
|
||||||
const ProjectCardView = ({currentItems,setCurrentPage,totalPages }) => {
|
|
||||||
|
|
||||||
|
|
||||||
|
const ProjectCardView = ({ currentItems, setCurrentPage, totalPages }) => {
|
||||||
return (
|
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 && (
|
{totalPages > 1 && (
|
||||||
<p className="text-center text-muted">No projects found.</p>
|
<nav>
|
||||||
)}
|
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||||
|
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||||
{currentItems.map((project) => (
|
<button
|
||||||
<ProjectCard
|
className="page-link"
|
||||||
key={project.id}
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
project={project}
|
>
|
||||||
/>
|
«
|
||||||
))}
|
</button>
|
||||||
|
</li>
|
||||||
|
{[...Array(totalPages)].map((_, i) => (
|
||||||
{ 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
|
<li
|
||||||
className={`page-item ${currentPage === totalPages && "disabled"
|
key={i}
|
||||||
}`}
|
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
onClick={() =>
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
»
|
{i + 1}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
))}
|
||||||
</nav>
|
<li
|
||||||
)}
|
className={`page-item ${
|
||||||
</div>
|
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 navigate = useNavigate();
|
||||||
const { setMangeProject } = useProjectContext();
|
const { setMangeProject } = useProjectContext();
|
||||||
// const { data, isLoading, isError, error } = useProjects();
|
// const { data, isLoading, isError, error } = useProjects();
|
||||||
|
|
||||||
// check Permissions
|
// check Permissions
|
||||||
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
// const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
|
|
||||||
const projectColumns = [
|
const projectColumns = [
|
||||||
{
|
{
|
||||||
@ -125,154 +124,156 @@ const ProjectListView = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleViewActivities = (project) => {
|
// const handleViewActivities = (project) => {
|
||||||
dispatch(setProjectId(project));
|
// dispatch(setProjectId(project));
|
||||||
navigate(`/activities/records?project=${project}`);
|
// navigate(`/activities/records?project=${project}`);
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
const handleMoveDetails = (project) => {
|
||||||
|
dispatch(setProjectId(project));
|
||||||
|
localStorage.setItem("lastActiveProjectTab", "profile")
|
||||||
|
navigate("/projects/details");
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="card page-min-h py-4 px-6 shadow-sm">
|
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||||
<table className="table table-hover align-middle m-0">
|
<div className="table-responsive text-nowrap">
|
||||||
<thead className="border-bottom">
|
<table className="table table-hover align-middle m-0">
|
||||||
<tr>
|
<thead className="border-bottom">
|
||||||
{projectColumns.map((col) => (
|
<tr>
|
||||||
<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}>
|
|
||||||
{projectColumns.map((col) => (
|
{projectColumns.map((col) => (
|
||||||
<td
|
<th key={col.key} colSpan={col.colSpan} className={col.className}>
|
||||||
key={col.key}
|
{col.label}
|
||||||
colSpan={col.colSpan}
|
</th>
|
||||||
className={`${col.className} py-5`}
|
|
||||||
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
|
||||||
>
|
|
||||||
{col.getValue
|
|
||||||
? col.getValue(project)
|
|
||||||
: project[col.key] || "N/A"}
|
|
||||||
</td>
|
|
||||||
))}
|
))}
|
||||||
<td
|
<th className="text-center py-3">Action</th>
|
||||||
className={`mx-2 ${
|
</tr>
|
||||||
canManageProject ? "d-sm-table-cell" : "d-none"
|
</thead>
|
||||||
}`}
|
<tbody>
|
||||||
>
|
{currentItems?.map((project) => (
|
||||||
<div className="dropdown z-2">
|
<tr key={project.id}>
|
||||||
<button
|
{projectColumns.map((col) => (
|
||||||
type="button"
|
<td
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
key={col.key}
|
||||||
data-bs-toggle="dropdown"
|
colSpan={col.colSpan}
|
||||||
aria-expanded="false"
|
className={`${col.className} py-5`}
|
||||||
|
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||||
>
|
>
|
||||||
<i
|
{col.getValue
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
? col.getValue(project)
|
||||||
data-bs-toggle="tooltip"
|
: project[col.key] || "N/A"}
|
||||||
data-bs-offset="0,8"
|
</td>
|
||||||
data-bs-placement="top"
|
))}
|
||||||
data-bs-custom-class="tooltip-dark"
|
<td className={`mx-2 ${"d-sm-table-cell"}`}>
|
||||||
title="More Action"
|
<div className="dropdown z-2">
|
||||||
></i>
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||||
<li>
|
data-bs-toggle="dropdown"
|
||||||
<a
|
aria-expanded="false"
|
||||||
aria-label="click to View details"
|
>
|
||||||
className="dropdown-item cursor-pointer"
|
<i
|
||||||
>
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
<i className="bx bx-detail me-2"></i>
|
data-bs-toggle="tooltip"
|
||||||
<span className="align-left">View details</span>
|
data-bs-offset="0,8"
|
||||||
</a>
|
data-bs-placement="top"
|
||||||
</li>
|
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>
|
<li>
|
||||||
<a
|
<a
|
||||||
className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setMangeProject({
|
setMangeProject({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
Project: project.id,
|
Project: project.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-pencil me-2"></i>
|
<i className="bx bx-pencil me-2"></i>
|
||||||
<span className="align-left">Modify</span>
|
<span className="align-left">Modify</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li onClick={() => handleViewActivities(project.id)}>
|
{/* <li onClick={() => handleViewActivities(project.id)}>
|
||||||
<a className="dropdown-item cursor-pointer">
|
<a className="dropdown-item cursor-pointer">
|
||||||
<i className="bx bx-task me-2"></i>
|
<i className="bx bx-task me-2"></i>
|
||||||
<span className="align-left">Activities</span>
|
<span className="align-left">Activities</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li> */}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
{" "}
|
{" "}
|
||||||
{isLoading && <p className="text-center">Loading...</p>}
|
{isLoading && <p className="text-center">Loading...</p>}
|
||||||
{!isLoading && filteredProjects.length === 0 && (
|
{!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>
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{!isLoading && totalPages > 1 && (
|
||||||
{!isLoading && currentItems.length === 0 && (
|
<nav>
|
||||||
<div className="py-6">
|
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||||
<p className="text-center text-muted">No projects found.</p>
|
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||||
</div>
|
<button
|
||||||
)}
|
className="page-link"
|
||||||
{!isLoading && totalPages > 1 && (
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
<nav>
|
>
|
||||||
<ul className="pagination pagination-sm justify-content-end py-2">
|
«
|
||||||
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
</button>
|
||||||
<button
|
</li>
|
||||||
className="page-link"
|
{[...Array(totalPages)].map((_, i) => (
|
||||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
<li
|
||||||
>
|
key={i}
|
||||||
«
|
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||||
</button>
|
>
|
||||||
</li>
|
<button
|
||||||
{[...Array(totalPages)].map((_, i) => (
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
<li
|
<li
|
||||||
key={i}
|
className={`page-item ${currentPage === totalPages && "disabled"
|
||||||
className={`page-item ${currentPage === i + 1 && "active"}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() =>
|
||||||
|
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{i + 1}
|
»
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
</ul>
|
||||||
<li
|
</nav>
|
||||||
className={`page-item ${
|
)}
|
||||||
currentPage === totalPages && "disabled"
|
</div>
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,13 +27,13 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
|
|
||||||
const ProjectTab = [
|
const ProjectTab = [
|
||||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||||
{ key: "teams", icon: "bx bx-group", label: "Teams" },
|
// { key: "teams", icon: "bx bx-group", label: "Teams" },
|
||||||
{
|
// {
|
||||||
key: "infra",
|
// key: "infra",
|
||||||
icon: "bx bx-grid-alt",
|
// icon: "bx bx-grid-alt",
|
||||||
label: "Infrastructure",
|
// label: "Infrastructure",
|
||||||
hidden: !(HasViewInfraStructure || HasManageInfra || HasManageTask),
|
// hidden: !(HasViewInfraStructure || HasManageInfra || HasManageTask),
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
key: "directory",
|
key: "directory",
|
||||||
icon: "bx bxs-contact",
|
icon: "bx bxs-contact",
|
||||||
@ -41,8 +41,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
hidden: !(DirAdmin || DireManager || DirUser),
|
hidden: !(DirAdmin || DireManager || DirUser),
|
||||||
},
|
},
|
||||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
||||||
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
|
// { key: "organization", icon: "bx bx-buildings", label: "Organization"},
|
||||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
// { key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="nav-align-top">
|
<div className="nav-align-top">
|
||||||
|
|||||||
@ -11,8 +11,8 @@ export const projectDefault = {
|
|||||||
startDate: currentDate.toISOString().split("T")[0],
|
startDate: currentDate.toISOString().split("T")[0],
|
||||||
endDate: currentDate.toISOString().split("T")[0],
|
endDate: currentDate.toISOString().split("T")[0],
|
||||||
projectStatusId: DEFAULT_EMPTY_STATUS_ID,
|
projectStatusId: DEFAULT_EMPTY_STATUS_ID,
|
||||||
promoterId: "",
|
// promoterId: "",
|
||||||
pmcId: "",
|
// pmcId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -39,8 +39,8 @@ export const projectSchema = z
|
|||||||
.min(1, { message: "End Date is required" })
|
.min(1, { message: "End Date is required" })
|
||||||
.default(projectDefault),
|
.default(projectDefault),
|
||||||
projectStatusId: z.string().min(1, { message: "Status is required" }),
|
projectStatusId: z.string().min(1, { message: "Status is required" }),
|
||||||
promoterId: z.string().min(1, { message: "Promoter is required" }),
|
// promoterId: z.string().min(1, { message: "Promoter is required" }),
|
||||||
pmcId: z.string().min(1, { message: "PMC is required" }),
|
// pmcId: z.string().min(1, { message: "PMC is required" }),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
|||||||
533
src/components/RecurringExpense/ManageRecurringExpense.jsx
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
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";
|
||||||
|
import UsersTagInput from "../common/usesInput";
|
||||||
|
import { useEmployeesName } from "../../hooks/useEmployees";
|
||||||
|
|
||||||
|
function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error: requestError,
|
||||||
|
} = useRecurringExpenseDetail(requestToEdit);
|
||||||
|
const { data: employees } = useEmployeesName()
|
||||||
|
//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());
|
||||||
|
const handleEmailGetting = (userArray = []) => {
|
||||||
|
if (!Array.isArray(userArray) || userArray.length === 0) return [];
|
||||||
|
|
||||||
|
return userArray
|
||||||
|
.map((empId) => {
|
||||||
|
const foundUser = employees?.data?.find((user) => user.id === empId);
|
||||||
|
return foundUser?.email || null;
|
||||||
|
})
|
||||||
|
.filter(Boolean).join(",")
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestToEdit && data) {
|
||||||
|
reset({
|
||||||
|
title: data.title || "",
|
||||||
|
description: data.description || "",
|
||||||
|
payee: data.payee || "",
|
||||||
|
notifyTo: data.notifyTo ? data.notifyTo.map((usr)=>usr.id) : [],
|
||||||
|
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,
|
||||||
|
notifyTo:handleEmailGetting(fromdata.notifyTo)
|
||||||
|
};
|
||||||
|
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}
|
||||||
|
/> */}
|
||||||
|
<UsersTagInput
|
||||||
|
control={control}
|
||||||
|
name="notifyTo"
|
||||||
|
placeholder="Type to search users"
|
||||||
|
projectId={watch("projectId")}
|
||||||
|
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;
|
||||||
290
src/components/RecurringExpense/RecurringExpenseList.jsx
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
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";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
|
||||||
|
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 paginate = (page) => {
|
||||||
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = data?.data?.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 */}
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data?.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
</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 = () => {
|
||||||
|
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.array(z.string()).min(1,"Please select at lest one user"),
|
||||||
|
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;
|
||||||
307
src/components/RecurringExpense/ViewRecurringExpense.jsx
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useRecurringExpenseDetail } from '../../hooks/useExpense';
|
||||||
|
import { formatUTCToLocalTime } from '../../utils/dateUtils';
|
||||||
|
import { formatFigure, getColorNameFromHex } from '../../utils/appUtils';
|
||||||
|
import Avatar from '../common/Avatar';
|
||||||
|
import { FREQUENCY_FOR_RECURRING } from '../../utils/constants';
|
||||||
|
import { ExpenseDetailsSkeleton } from '../Expenses/ExpenseSkeleton';
|
||||||
|
|
||||||
|
const ViewRecurringExpense = ({ RecurringId }) => {
|
||||||
|
const { data, isLoading, isError, error, isFetching } = useRecurringExpenseDetail(RecurringId);
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
if (isLoading) return <ExpenseDetailsSkeleton />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<form className="container px-3">
|
||||||
|
<div className="col-12 mb-1">
|
||||||
|
<h5 className="fw-semibold m-0">Recurring Payment Details</h5>
|
||||||
|
</div>
|
||||||
|
<div className="row mb-1">
|
||||||
|
{/* <div className="col-12 col-lg-7 col-xl-8 mb-3"> */}
|
||||||
|
<div className="row">
|
||||||
|
|
||||||
|
{/* Row 1 Recurring Id and Status */}
|
||||||
|
|
||||||
|
<div className="col-12 d-flex justify-content-between text-start fw-semibold my-2 mb-4">
|
||||||
|
<span>{data?.recurringPaymentUID}</span>
|
||||||
|
<span
|
||||||
|
className={`badge bg-label-${statusColorMap[data?.status?.id] || "secondary"}`}
|
||||||
|
>
|
||||||
|
{data?.status?.name || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2 Category*/}
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<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 || "N/A"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Row 3 Amount and Project */}
|
||||||
|
|
||||||
|
<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 != null
|
||||||
|
? `${data?.currency?.symbol ?? "¥"} ${Number(data.amount).toFixed(2)} ${data?.currency?.currencyCode ?? "CN"}`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<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 || "N/A"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 4 Created At and Title*/}
|
||||||
|
|
||||||
|
<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 className="text-muted">
|
||||||
|
{data?.createdAt
|
||||||
|
? formatUTCToLocalTime(data.createdAt, true)
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Title :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.title || "N/A"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 5 Payee and Notify*/}
|
||||||
|
|
||||||
|
<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" }}
|
||||||
|
>
|
||||||
|
Payee :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.payee || "N/A"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<div className="d-flex align-items-start">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Notify To :
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="text-muted" style={{ textAlign: "left" }}>
|
||||||
|
{data?.notifyTo?.length > 0
|
||||||
|
? data.notifyTo?.map((user, index) => (
|
||||||
|
<span key={user.id}>
|
||||||
|
{user.email}
|
||||||
|
{index < data?.notifyTo?.length - 1 && ", "}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 6 Strike Date*/}
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Strike Date :
|
||||||
|
</label>
|
||||||
|
{/* <div className="text-muted">
|
||||||
|
{formatUTCToLocalTime(data?.strikeDate)}
|
||||||
|
</div> */}
|
||||||
|
<div className="text-muted">
|
||||||
|
{data?.strikeDate
|
||||||
|
? formatUTCToLocalTime(data.strikeDate, true)
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 7 Frequency and Buffer Days*/}
|
||||||
|
|
||||||
|
<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" }}
|
||||||
|
>
|
||||||
|
Frequency :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted flex-grow-1 text-start">
|
||||||
|
{data?.frequency !== undefined
|
||||||
|
? FREQUENCY_FOR_RECURRING[data.frequency]
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Payment Buffer Days :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.paymentBufferDays || "N/A"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 8 Updated At and Number of Iteration*/}
|
||||||
|
|
||||||
|
<div className="col-md-6 mb-6">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Updated At :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">
|
||||||
|
{data?.updatedAt
|
||||||
|
? formatUTCToLocalTime(data.updatedAt, true)
|
||||||
|
: "N/A"}
|
||||||
|
</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" }}
|
||||||
|
>
|
||||||
|
Number of Iteration :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.numberOfIteration || "N/A"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Row 9 Created By and Updated By*/}
|
||||||
|
|
||||||
|
<div className="col-md-6 text-start mb-6">
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "125px" }}
|
||||||
|
>
|
||||||
|
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: "125px" }}
|
||||||
|
>
|
||||||
|
Updated By :
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{data?.updatedBy ? (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
classAvatar="m-0 me-1"
|
||||||
|
firstName={data.updatedBy.firstName}
|
||||||
|
lastName={data.updatedBy.lastName}
|
||||||
|
/>
|
||||||
|
<span className="text-muted">
|
||||||
|
{`${data.updatedBy.firstName ?? ""} ${data.updatedBy.lastName ?? ""}`.trim() || "N/A"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">N/A</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 10 Description */}
|
||||||
|
|
||||||
|
<div className="col-12 text-start mb-5 d-flex">
|
||||||
|
<label
|
||||||
|
className="fw-semibold form-label mb-0"
|
||||||
|
style={{ minWidth: "140px", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
Description :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted flex-grow-1" style={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{data?.description || "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* </div> */}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewRecurringExpense
|
||||||