Compare commits
197 Commits
main
...
Issues_Exp
| Author | SHA1 | Date | |
|---|---|---|---|
| a837eaab72 | |||
| ee456f5ae1 | |||
| 0729723d5e | |||
| 72e5cf0bbe | |||
| 1278e32da9 | |||
| 28f15f649f | |||
| f18633b3d5 | |||
| 07eedfc9e2 | |||
| 9d35d00009 | |||
| 35956cc9bb | |||
| 41377167e6 | |||
| 428f22ea8e | |||
| 4d40b8e149 | |||
| e4585d6c43 | |||
| c3e030754f | |||
| deda915e74 | |||
| dfaf90ccac | |||
| a253eea33c | |||
| d9e1bfac97 | |||
| 06e8046dde | |||
| 82f173c0ed | |||
| 1f784f330d | |||
| 4ae0b403a6 | |||
| 33256981da | |||
| 1ca1f15422 | |||
| 84ed1984d7 | |||
| b668faab33 | |||
| 8229a44a74 | |||
| 324ad05771 | |||
| 418979568d | |||
| bac9f25df1 | |||
| e1e9920b3e | |||
| 9b091840d6 | |||
| b897a41f95 | |||
| ec2a1706fc | |||
| cb0ed03525 | |||
| 0372991ac1 | |||
| ac9eaa1e67 | |||
| daaebf919d | |||
| 92fa1c9a3d | |||
| c0f30397ef | |||
| 00f405069b | |||
| 2a46ac1349 | |||
| f4838441aa | |||
| c63b13f200 | |||
| 9b65c12239 | |||
| 10ec11a828 | |||
| 3b318a672c | |||
| fba19f6ead | |||
| 4069720ed0 | |||
| e8698473db | |||
| f688a7169e | |||
| 69e60caf23 | |||
| ecf34b499e | |||
| e868d27d5f | |||
| 0dc68eb20d | |||
| 7a749b14e9 | |||
| 76fe342c1e | |||
| 44a7749cd3 | |||
| 611116b70c | |||
| e79702661d | |||
| 10ff3ecd2f | |||
| bede21aff2 | |||
| bce69783f7 | |||
| a8f83571ee | |||
| 616b986deb | |||
| 57d33ab817 | |||
| 5bf5e18c6b | |||
| 190612feed | |||
| 346ef0174b | |||
| 192c04fb6f | |||
| 3aa9934f8b | |||
| d02b14272f | |||
| edafc204b8 | |||
| 1e14bfe0b1 | |||
| 1c4384a62e | |||
| 859eaa747f | |||
| 871daf6036 | |||
| 59c46f7f3b | |||
| e80223beba | |||
| de2d23ec83 | |||
| f3cfb3cf24 | |||
| 0e3c78df11 | |||
| e0c0a14777 | |||
| 17a08304e3 | |||
| c13e0acd3d | |||
| b0a58740a5 | |||
| c123373892 | |||
| 9c03303547 | |||
| d8712c0d04 | |||
| 5f0972ee32 | |||
| 0d1d51d56b | |||
| 166b0a1caf | |||
| 196069b39a | |||
| 10c2e9dfee | |||
| 494b1b2b77 | |||
| 465b67e25c | |||
| ec04426e52 | |||
| dc75121d31 | |||
| d3acf13c0a | |||
| cc4cea0f27 | |||
| 02777a8f47 | |||
| 7c5dca1665 | |||
| 0d0b7f2cbc | |||
| 394d28f80d | |||
| 95c6e71be8 | |||
| 0091b1064e | |||
| cde6794697 | |||
| 78b721dad7 | |||
| a8ae0fac70 | |||
| b318d469f6 | |||
| 73534226e3 | |||
| 9bbe479153 | |||
| ce976a6d5c | |||
| d1af4f402d | |||
| 105beb6062 | |||
| f61c7a4a35 | |||
| dacd72f945 | |||
| a2177dc2af | |||
| a9d1ba08dd | |||
| 9526cb0e5e | |||
| 432cf2122e | |||
| 765b4356a6 | |||
| 6125130cff | |||
| 006ca21e1d | |||
| bb2d7b8923 | |||
| c921dbff09 | |||
| e84da2dcb0 | |||
| 4ba7c72e78 | |||
| 9939973d8d | |||
| e17c2064ac | |||
| 9580c9234c | |||
| 44b3f842f2 | |||
| 9de0b1a0df | |||
| fbb68a4488 | |||
| c1a8b87a2c | |||
| 24501b3c76 | |||
| ef00516256 | |||
| f867a692ed | |||
| 85b4b830d3 | |||
| c1c333008f | |||
| 5370d0eb9d | |||
| 5cbd6c427a | |||
| 5057738d8b | |||
| 063bbee681 | |||
| 3dc667db73 | |||
| 2f2c31db9f | |||
| 0dc540eec2 | |||
| fc7c33ee54 | |||
| eeb886ec36 | |||
| 210b01513e | |||
| 2fe297c6b6 | |||
| ffd7f15488 | |||
| 7f33d5e6bd | |||
| 108ca1cc96 | |||
| 0b1a276898 | |||
| 6246dbd23f | |||
| af3bf11beb | |||
| 2e5951d9d1 | |||
| cdc5e32554 | |||
| bc047a84fc | |||
| 0511ed6d82 | |||
| bc933f4c64 | |||
| bd3645aae6 | |||
| 2f2215ab8a | |||
| 071f862743 | |||
| 040331e27d | |||
| db2730c02b | |||
| 9df813698d | |||
| 6ba018399c | |||
| aeb9778216 | |||
| 81ed65c0c6 | |||
| 1e49cf17a2 | |||
| ab5e0b2f79 | |||
| 7c9d2ddeb1 | |||
| f0f579beae | |||
| b53e6284b9 | |||
| b1c23aab4d | |||
| 4a05e67ff0 | |||
| 87d13d0f77 | |||
| 997971629f | |||
| 18ac8e11bd | |||
| 78808ecac0 | |||
| 3553a7b521 | |||
| aca96c60ae | |||
| 25f4f1e7a7 | |||
| 0c1889e1c1 | |||
| 9884943907 | |||
| 2531d91209 | |||
| 49b597c833 | |||
| 2aae7194b7 | |||
| ae772d925a | |||
| c1e5ff4043 | |||
| 2aff3b9e80 | |||
| 4e48478fbc | |||
| 4ea1e06a9c | |||
| 4ddb8415cc |
20
index.html
@ -5,10 +5,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OnFieldWork.com</title>
|
||||
<title>Marco PMS</title>
|
||||
|
||||
<meta name="description" content="" />
|
||||
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/img/favicon.ico" />
|
||||
|
||||
@ -29,6 +31,8 @@
|
||||
<link rel="stylesheet" href="/assets/css/default.css" />
|
||||
<link rel="stylesheet" href="/assets/css/skeleton.css" />
|
||||
<link rel="stylesheet" href="/assets/css/hover-utility.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-green.css" />
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
|
||||
@ -46,8 +50,6 @@
|
||||
<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/spinkit/spinkit.css" />
|
||||
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.css" />
|
||||
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.js" />
|
||||
|
||||
<!-- Helpers -->
|
||||
<script src="/assets/vendor/js/helpers.js"></script>
|
||||
@ -96,15 +98,6 @@
|
||||
<script src="/assets/js/main.js"></script>
|
||||
|
||||
<!-- Page JS -->
|
||||
<script src="/assets/js/form-wizard-icons.js"></script>
|
||||
<script src="/assets/js/dashboards-analytics.js"></script>
|
||||
|
||||
<!-- Bloack Ui -->
|
||||
<!-- <script src="/assets/js/extended-ui-blockui.js"></script> -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- BlockUI core plugin -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js"></script>
|
||||
<script src="/assets/js/form-wizard-icons.js"></script>
|
||||
<script src="/assets/js/dashboards-analytics.js"></script>
|
||||
|
||||
@ -116,6 +109,9 @@
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script> -->
|
||||
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> -->
|
||||
|
||||
|
||||
<!-- Flatpickr JS -->
|
||||
|
||||
</body>
|
||||
|
||||
@ -3,37 +3,6 @@
|
||||
--bs-nav-link-font-size: 0.7375rem;
|
||||
--bg-border-color :#f8f6f6
|
||||
}
|
||||
.offcanvas.offcanvas-wide {
|
||||
width: 700px !important; /* adjust as needed */
|
||||
}
|
||||
.sticky-section {
|
||||
position: sticky;
|
||||
top: var(--sticky-top, 0px) !important;
|
||||
z-index: 1025;
|
||||
}
|
||||
|
||||
|
||||
/* ===========================% Background_Colors %========================================================== */
|
||||
.bg-light-primary {
|
||||
background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent);
|
||||
border:var(--bs-primary-border-subtle)
|
||||
}
|
||||
.bg-light-secondary {
|
||||
background-color: color-mix(in srgb, var(--bs-secondary) 10.4%, transparent);
|
||||
}
|
||||
.bg-light-danger {
|
||||
background-color: color-mix(in srgb, var(--bs-danger) 10.4%, transparent);
|
||||
}
|
||||
.bg-light-success {
|
||||
background-color: color-mix(in srgb, var(--bs-success) 10.4%, transparent);
|
||||
}
|
||||
|
||||
.bg-light-info {
|
||||
background-color: color-mix(in srgb, var(--bs-info) 10.4%, transparent);
|
||||
}
|
||||
.bg-light-warning {
|
||||
background-color: color-mix(in srgb, var(--bs-warning) 10.4%, transparent);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.5rem var(--bs-card-cap-padding-x);
|
||||
@ -42,116 +11,20 @@
|
||||
border-bottom:2px solid var(--bs-table-border-color) ;
|
||||
}
|
||||
.text-gary-80 {
|
||||
color:var(--bs-gray-500)
|
||||
color:var(--bs-gray-500)
|
||||
}
|
||||
|
||||
.text-royalblue{
|
||||
color: #1796e3;
|
||||
color: #1796e3;
|
||||
}
|
||||
|
||||
.text-md {
|
||||
font-size: 2rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.text-md-b {
|
||||
font-weight: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
.stepper-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-horizontal {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-point {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #dee2e6;
|
||||
color: #6c757d;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.timeline-point.completed {
|
||||
background-color: var(--bs-success);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 5px rgba(25, 135, 84, 0.5);
|
||||
}
|
||||
|
||||
.timeline-point.failed {
|
||||
background-color: var(--bs-danger);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
|
||||
}
|
||||
|
||||
.timeline-point.active {
|
||||
background-color: var(--bs-info);
|
||||
color: #fff;
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 6px rgba(13, 202, 240, 0.5);
|
||||
}
|
||||
|
||||
.timeline-line-horizontal {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #dee2e6;
|
||||
z-index: 1;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Make line green for completed sections */
|
||||
.timeline-item.completed ~ .timeline-line-horizontal {
|
||||
background-color: var(--bs-success);
|
||||
}
|
||||
|
||||
/* Optional: subtle pulse for active step */
|
||||
.timeline-point.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--bs-info);
|
||||
animation: pulse 1.5s infinite;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.text-xxs { font-size: 0.55rem; } /* 8px */
|
||||
.text-xs { font-size: 0.75rem; } /* 12px */
|
||||
.text-sm { font-size: 0.875rem; } /* 14px */
|
||||
@ -223,208 +96,6 @@ font-weight: normal;
|
||||
.h-max { height: max-content; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ==========================
|
||||
Base Font Sizes (mobile first)
|
||||
========================== */
|
||||
.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 */
|
||||
|
||||
/* ==========================
|
||||
Base Heights
|
||||
========================== */
|
||||
.h-0 { height: 0; }
|
||||
.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-70 { height: 20rem; } /* 256px */
|
||||
.h-74 { max-height: 35rem; } /* 256px */
|
||||
.h-full { height: 100%; }
|
||||
|
||||
.h-screen{ height: 100vh; }
|
||||
|
||||
/* ==========================
|
||||
Base Widths
|
||||
========================== */
|
||||
.w-0 { width: 0; }
|
||||
.w-px { width: 1px; }
|
||||
.w-1 { width: 0.25rem; }
|
||||
.w-2 { width: 0.5rem; }
|
||||
.w-3 { width: 0.75rem; }
|
||||
.w-4 { width: 1rem; }
|
||||
.w-5 { width: 1.25rem; }
|
||||
.w-6 { width: 1.5rem; }
|
||||
.w-8 { width: 2rem; }
|
||||
.w-10 { width: 2.5rem; }
|
||||
.w-12 { width: 3rem; }
|
||||
.w-16 { width: 4rem; }
|
||||
.w-20 { width: 5rem; }
|
||||
.w-24 { width: 6rem; }
|
||||
.w-32 { width: 8rem; }
|
||||
.w-40 { width: 10rem; }
|
||||
.w-48 { width: 12rem; }
|
||||
.w-56 { width: 14rem; }
|
||||
.w-64 { width: 16rem; }
|
||||
.w-full { width: 100%; }
|
||||
.w-screen{ width: 100vw; }
|
||||
|
||||
/* ==========================
|
||||
Responsive Variants
|
||||
========================== */
|
||||
@media (min-width: 576px) { /* sm */
|
||||
/* Font */
|
||||
.text-xxs-sm { font-size: 0.55rem; }
|
||||
.text-xs-sm { font-size: 0.75rem; }
|
||||
.text-sm-sm { font-size: 0.875rem; }
|
||||
.text-base-sm{ font-size: 1rem; }
|
||||
.text-lg-sm { font-size: 1.125rem; }
|
||||
.text-xl-sm { font-size: 1.25rem; }
|
||||
.text-2xl-sm{ font-size: 1.5rem; }
|
||||
|
||||
/* Height */
|
||||
.h-1-sm{ height: 0.25rem; }
|
||||
.h-2-sm{ height: 0.5rem; }
|
||||
.h-3-sm{ height: 0.75rem; }
|
||||
.h-4-sm{ height: 1rem; }
|
||||
.h-5-sm{ height: 1.25rem; }
|
||||
.h-6-sm{ height: 1.5rem; }
|
||||
.h-8-sm{ height: 2rem; }
|
||||
.h-10-sm{ height: 2.5rem; }
|
||||
|
||||
/* Width */
|
||||
.w-1-sm{ width: 0.25rem; }
|
||||
.w-2-sm{ width: 0.5rem; }
|
||||
.w-3-sm{ width: 0.75rem; }
|
||||
.w-4-sm{ width: 1rem; }
|
||||
.w-5-sm{ width: 1.25rem; }
|
||||
.w-6-sm{ width: 1.5rem; }
|
||||
.w-8-sm{ width: 2rem; }
|
||||
.w-10-sm{ width: 2.5rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) { /* md */
|
||||
/* Font */
|
||||
.text-xxs-md { font-size: 0.55rem; }
|
||||
.text-xs-md { font-size: 0.75rem; }
|
||||
.text-sm-md { font-size: 0.875rem; }
|
||||
.text-base-md{ font-size: 1rem; }
|
||||
.text-lg-md { font-size: 1.125rem; }
|
||||
.text-xl-md { font-size: 1.25rem; }
|
||||
.text-2xl-md{ font-size: 1.5rem; }
|
||||
|
||||
/* Height */
|
||||
.h-1-md{ height: 0.25rem; }
|
||||
.h-2-md{ height: 0.5rem; }
|
||||
.h-3-md{ height: 0.75rem; }
|
||||
.h-4-md{ height: 1rem; }
|
||||
.h-5-md{ height: 1.25rem; }
|
||||
.h-6-md{ height: 1.5rem; }
|
||||
.h-8-md{ height: 2rem; }
|
||||
.h-10-md{ height: 2.5rem; }
|
||||
|
||||
/* Width */
|
||||
.w-1-md{ width: 0.25rem; }
|
||||
.w-2-md{ width: 0.5rem; }
|
||||
.w-3-md{ width: 0.75rem; }
|
||||
.w-4-md{ width: 1rem; }
|
||||
.w-5-md{ width: 1.25rem; }
|
||||
.w-6-md{ width: 1.5rem; }
|
||||
.w-8-md{ width: 2rem; }
|
||||
.w-10-md{ width: 2.5rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 992px) { /* lg */
|
||||
/* Font */
|
||||
.text-xxs-lg { font-size: 0.55rem; }
|
||||
.text-xs-lg { font-size: 0.75rem; }
|
||||
.text-sm-lg { font-size: 0.875rem; }
|
||||
.text-base-lg{ font-size: 1rem; }
|
||||
.text-lg-lg { font-size: 1.125rem; }
|
||||
.text-xl-lg { font-size: 1.25rem; }
|
||||
.text-2xl-lg{ font-size: 1.5rem; }
|
||||
|
||||
/* Height */
|
||||
.h-1-lg{ height: 0.25rem; }
|
||||
.h-2-lg{ height: 0.5rem; }
|
||||
.h-3-lg{ height: 0.75rem; }
|
||||
.h-4-lg{ height: 1rem; }
|
||||
.h-5-lg{ height: 1.25rem; }
|
||||
.h-6-lg{ height: 1.5rem; }
|
||||
.h-8-lg{ height: 2rem; }
|
||||
.h-10-lg{ height: 2.5rem; }
|
||||
|
||||
/* Width */
|
||||
.w-1-lg{ width: 0.25rem; }
|
||||
.w-2-lg{ width: 0.5rem; }
|
||||
.w-3-lg{ width: 0.75rem; }
|
||||
.w-4-lg{ width: 1rem; }
|
||||
.w-5-lg{ width: 1.25rem; }
|
||||
.w-6-lg{ width: 1.5rem; }
|
||||
.w-8-lg{ width: 2rem; }
|
||||
.w-10-lg{ width: 2.5rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) { /* xl */
|
||||
/* Font */
|
||||
.text-xxs-xl { font-size: 0.55rem; }
|
||||
.text-xs-xl { font-size: 0.75rem; }
|
||||
.text-sm-xl { font-size: 0.875rem; }
|
||||
.text-base-xl{ font-size: 1rem; }
|
||||
.text-lg-xl { font-size: 1.125rem; }
|
||||
.text-xl-xl { font-size: 1.25rem; }
|
||||
.text-2xl-xl{ font-size: 1.5rem; }
|
||||
|
||||
/* Height */
|
||||
.h-1-xl{ height: 0.25rem; }
|
||||
.h-2-xl{ height: 0.5rem; }
|
||||
.h-3-xl{ height: 0.75rem; }
|
||||
.h-4-xl{ height: 1rem; }
|
||||
.h-5-xl{ height: 1.25rem; }
|
||||
.h-6-xl{ height: 1.5rem; }
|
||||
.h-8-xl{ height: 2rem; }
|
||||
.h-10-xl{ height: 2.5rem; }
|
||||
|
||||
/* Width */
|
||||
.w-1-xl{ width: 0.25rem; }
|
||||
.w-2-xl{ width: 0.5rem; }
|
||||
.w-3-xl{ width: 0.75rem; }
|
||||
.w-4-xl{ width: 1rem; }
|
||||
.w-5-xl{ width: 1.25rem; }
|
||||
.w-6-xl{ width: 1.5rem; }
|
||||
.w-8-xl{ width: 2rem; }
|
||||
.w-10-xl{ width: 2.5rem; }
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------Text------------------------- */
|
||||
@media (min-width: 576px) {
|
||||
.fs-sm-1 { font-size: calc(1.3rem + 1.6vw) !important; }
|
||||
@ -456,3 +127,11 @@ font-weight: normal;
|
||||
.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;
|
||||
}
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.app-brand-logo-border {
|
||||
border: 1px solid #d5d5d5;
|
||||
}
|
||||
.app-brand-text {
|
||||
font-size: 1.75rem;
|
||||
letter-spacing: -0.5px;
|
||||
@ -160,9 +165,10 @@ thead tr {
|
||||
|
||||
.app-brand-logo-login {
|
||||
max-width: 50px; /* default for mobile */
|
||||
height: auto; /* keep aspect ratio */
|
||||
height: auto; /* keep aspect ratio */
|
||||
}
|
||||
|
||||
|
||||
/* Tablet and up (≥768px) */
|
||||
@media (min-width: 768px) {
|
||||
.app-brand-logo-login {
|
||||
@ -176,3 +182,4 @@ thead tr {
|
||||
max-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
18
public/assets/vendor/css/core.css
vendored
@ -76,7 +76,6 @@
|
||||
--bs-dark-border-subtle: #bfc0c6;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 34, 48, 62;
|
||||
--bs-font-roboto:"Segoe UI", Roboto, "sans-serif",
|
||||
--bs-font-sans-serif: "Public Sans", -apple-system, blinkmacsystemfont,
|
||||
"Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
@ -89,7 +88,7 @@
|
||||
);
|
||||
--bs-root-font-size: 16px;
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 0.85rem;
|
||||
--bs-body-font-size: 0.8375rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.375;
|
||||
--bs-body-color: #646e78;
|
||||
@ -9060,7 +9059,7 @@ img[data-app-light-img][data-app-dark-img] {
|
||||
}
|
||||
.table th {
|
||||
color: var(--bs-heading-color);
|
||||
font-size: 0.8025rem;
|
||||
font-size: 0.8125rem;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@ -20345,7 +20344,7 @@ li:not(:first-child) .dropdown-item,
|
||||
}
|
||||
|
||||
.fs-6 {
|
||||
font-size: 0.8375rem !important;
|
||||
font-size: 0.9375rem !important;
|
||||
}
|
||||
|
||||
.fs-tiny {
|
||||
@ -32560,7 +32559,9 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
|
||||
.bg-blue {
|
||||
background-color:var(--bs-blue)
|
||||
}
|
||||
|
||||
.text-blue{
|
||||
color:var(--bs-blue)
|
||||
}
|
||||
.bg-indigo {
|
||||
background-color:var(--bs-indigo)
|
||||
}
|
||||
@ -32573,9 +32574,6 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
|
||||
.text-red{
|
||||
color:var(--bs-red)
|
||||
}
|
||||
.text-blue{
|
||||
color:var(--bs-blue)
|
||||
}
|
||||
.text-green{
|
||||
color:var(--bs-green)
|
||||
.bg-gray {
|
||||
background:var(--bs-body-color)
|
||||
}
|
||||
879
public/assets/vendor/libs/tagify/tagify.css
vendored
@ -1,879 +0,0 @@
|
||||
@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
|
Before Width: | Height: | Size: 117 KiB |
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 |
BIN
public/img/app/teams/team-member-1.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/img/app/teams/team-member-2.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/img/app/teams/team-member-3.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/img/app/teams/team-member-4.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/img/avatars/1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/img/avatars/5.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/img/avatars/6.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/avatars/7.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/brand/logo-1.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/img/brand/logo-2.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/img/brand/logo-3.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/img/brand/logo-4.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/img/brand/logo-5.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/img/brand/logo-6.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/img/brand/logo_1-dark.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/img/brand/logo_1-light.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/img/brand/logo_2-dark.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/img/brand/logo_2-light.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/img/brand/logo_3-dark.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/img/brand/logo_3-light.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/img/brand/logo_4-dark.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/img/brand/logo_4-light.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/img/brand/logo_5-dark.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/img/brand/logo_5-light.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 43 KiB |
BIN
public/img/elements/1.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/elements/11.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/img/elements/12.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/img/elements/13.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/img/elements/17.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/img/elements/18.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/img/elements/19.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/img/elements/2.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/img/elements/20.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/img/elements/3.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/img/elements/4.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/img/elements/5.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/img/elements/7.jpg
Normal file
|
After Width: | Height: | Size: 21 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-012.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 233 KiB |
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 |
@ -1,5 +0,0 @@
|
||||
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.2" d="M46.5001 10.5288H32.5001L20.2251 26.5288L32.5001 56.5288L60.5001 26.5288L46.5001 10.5288Z" fill="#03C3EC"/>
|
||||
<path d="M18.5 10.5288H46.5L60.5 26.5288L32.5 56.5288L4.5 26.5288L18.5 10.5288Z" stroke="#03C3EC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2934 9.92012C33.1042 9.67343 32.8109 9.52881 32.5 9.52881C32.1891 9.52881 31.8958 9.67343 31.7066 9.92012L19.7318 25.5288H4.5C3.94772 25.5288 3.5 25.9765 3.5 26.5288C3.5 27.0811 3.94772 27.5288 4.5 27.5288H19.5537L31.5745 56.9075C31.7282 57.2833 32.094 57.5288 32.5 57.5288C32.906 57.5288 33.2718 57.2833 33.4255 56.9075L45.4463 27.5288H60.5C61.0523 27.5288 61.5 27.0811 61.5 26.5288C61.5 25.9765 61.0523 25.5288 60.5 25.5288H45.2682L33.2934 9.92012ZM42.7474 25.5288L32.5 12.1717L22.2526 25.5288H42.7474ZM21.7146 27.5288L32.5 53.8881L43.2854 27.5288H21.7146Z" fill="#03C3EC"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 860 KiB |
|
Before Width: | Height: | Size: 225 KiB |
BIN
public/img/illustrations/man-with-laptop-light.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 11 KiB |
4955
public/img/illustrations/worker_01.svg
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
public/img/illustrations/worker_02.jpg
Normal file
|
After Width: | Height: | Size: 267 KiB |
1117
public/img/illustrations/worker_02.svg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
public/img/illustrations/worker_03.jpg
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
public/img/illustrations/worker_03.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/img/layouts/layout-container-light.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/img/layouts/layout-fluid-light.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/img/layouts/layout-without-menu-light.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/img/layouts/layout-without-navbar-light.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
42
public/img/sneat.svg
Normal file
@ -0,0 +1,42 @@
|
||||
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<path
|
||||
d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z"
|
||||
id="path-1"></path>
|
||||
<path
|
||||
d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z"
|
||||
id="path-3"></path>
|
||||
<path
|
||||
d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z"
|
||||
id="path-4"></path>
|
||||
<path
|
||||
d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z"
|
||||
id="path-5"></path>
|
||||
</defs>
|
||||
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
|
||||
<g id="Icon" transform="translate(27.000000, 15.000000)">
|
||||
<g id="Mask" transform="translate(0.000000, 8.000000)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<use fill="#696cff" xlink:href="#path-1"></use>
|
||||
<g id="Path-3" mask="url(#mask-2)">
|
||||
<use fill="#696cff" xlink:href="#path-3"></use>
|
||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="Path-4" mask="url(#mask-2)">
|
||||
<use fill="#696cff" xlink:href="#path-4"></use>
|
||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Triangle"
|
||||
transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
|
||||
<use fill="#696cff" xlink:href="#path-5"></use>
|
||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@ -6,7 +6,12 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { queryClient } from "./layouts/AuthLayout";
|
||||
import ModalProvider from "./ModalProvider";
|
||||
|
||||
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
if (event.reason?.message?.includes("Failed to fetch")) {
|
||||
event.preventDefault();
|
||||
console.debug("Network issue (fetch failed) - suppressed");
|
||||
}
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
|
||||
@ -3,24 +3,18 @@ import { useOrganizationModal } from "./hooks/useOrganization";
|
||||
import OrganizationModal from "./components/Organization/OrganizationModal";
|
||||
import { useAuthModal, useModal } from "./hooks/useAuth";
|
||||
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
||||
import ChangePasswordPage from "./pages/authentication/ChangePassword";
|
||||
import NewCollection from "./components/collections/ManageCollection";
|
||||
import ServiceProjectTeamAllocation from "./components/ServiceProject/ServiceProjectTeam/ServiceProjectTeamAllocation";
|
||||
import { ProjectModal } from "./components/Project/ManageProjectInfo";
|
||||
|
||||
const ModalProvider = () => {
|
||||
const { isOpen, onClose } = useOrganizationModal();
|
||||
const { isOpen: isAuthOpen } = useAuthModal();
|
||||
const { isOpen: isChangePass } = useModal("ChangePassword");
|
||||
const { isOpen: isCollectionNew } = useModal("newCollection");
|
||||
const { isOpen: isServiceTeamAllocation } = useModal("ServiceTeamAllocation");
|
||||
const {isOpen:isOpenProject} = useModal("ManageProject")
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <OrganizationModal />}
|
||||
{isAuthOpen && <SwitchTenant />}
|
||||
{isChangePass && <ChangePasswordPage />}
|
||||
{isCollectionNew && <NewCollection />}
|
||||
{isServiceTeamAllocation && <ServiceProjectTeamAllocation />}
|
||||
{isOpenProject && <ProjectModal/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
1
src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
2
src/assets/vendor/css/core.css
vendored
@ -72,7 +72,7 @@
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-root-font-size: 16px;
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 0.875rem;
|
||||
--bs-body-font-size: 0.9375rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.375;
|
||||
--bs-body-color: #646e78;
|
||||
|
||||
@ -135,6 +135,7 @@ const AttendLogs = ({ Id }) => {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <p>Loading..</p>}
|
||||
{logs && logs.length > 0 && (
|
||||
<>
|
||||
|
||||
@ -11,9 +11,18 @@ import { useSelector } from "react-redux";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import eventBus from "../../services/eventBus";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, }) => {
|
||||
const Attendance = ({
|
||||
getRole,
|
||||
handleModalData,
|
||||
searchTerm,
|
||||
projectId,
|
||||
organizationId,
|
||||
includeInactive,
|
||||
date,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@ -24,12 +33,12 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
||||
attendance,
|
||||
loading: attLoading,
|
||||
recall: attrecall,
|
||||
isFetching
|
||||
} = useAttendance(selectedProject, organizationId);
|
||||
isFetching,
|
||||
} = useAttendance(selectedProject, organizationId, includeInactive, date);
|
||||
const filteredAttendance = ShowPending
|
||||
? attendance?.filter(
|
||||
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
||||
)
|
||||
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
||||
)
|
||||
: attendance;
|
||||
|
||||
const attendanceList = Array.isArray(filteredAttendance)
|
||||
@ -71,19 +80,19 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
||||
);
|
||||
|
||||
// Reset pagination when the filter or search term changes
|
||||
useEffect(() => {
|
||||
}, [finalFilteredData]);
|
||||
|
||||
useEffect(() => {}, [finalFilteredData]);
|
||||
|
||||
const handler = useCallback(
|
||||
(msg) => {
|
||||
if (selectedProject == msg.projectId) {
|
||||
queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
|
||||
if (!oldData) {
|
||||
queryClient.invalidateQueries({ queryKey: ["attendance"] })
|
||||
};
|
||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||
}
|
||||
return oldData.map((record) =>
|
||||
record.employeeId === msg.response.employeeId ? { ...record, ...msg.response } : record
|
||||
record.employeeId === msg.response.employeeId
|
||||
? { ...record, ...msg.response }
|
||||
: record
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -111,188 +120,145 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex justify-content-between align-items-center py-2 px-2">
|
||||
{/* Left side - Date */}
|
||||
<div className="text-start">
|
||||
<strong>Date: {formatUTCToLocalTime(todayDate)}</strong>
|
||||
</div>
|
||||
|
||||
{/* Right side - Pending Attendance toggle */}
|
||||
<div className="form-check form-switch m-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
disabled={isFetching}
|
||||
checked={ShowPending}
|
||||
onChange={(e) => setShowPending(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="inactiveEmployeesCheckbox">
|
||||
Pending Attendance
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="table-responsive modal-min-h text-nowrap h-100"
|
||||
style={{ minHeight: "200px" }} // Ensures fixed height
|
||||
>
|
||||
{attLoading ? (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "50vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
<div>
|
||||
<div className="table-responsive text-nowrap ">
|
||||
<div className="d-flex justify-content-between align-items-center py-2">
|
||||
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
||||
<div className="form-check form-switch text-start m-0 ms-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
disabled={isFetching}
|
||||
checked={ShowPending}
|
||||
onChange={(e) => setShowPending(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label ms-0">Show Pending</label>
|
||||
</div>
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
|
||||
<>
|
||||
<table className="table ">
|
||||
<thead>
|
||||
<tr className="border-top-1">
|
||||
<th colSpan={2}>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Organization</th>
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>
|
||||
Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0 ">
|
||||
{currentItems &&
|
||||
currentItems
|
||||
.sort((a, b) => {
|
||||
const checkInA = a?.checkInTime
|
||||
? new Date(a.checkInTime)
|
||||
: new Date(0);
|
||||
const checkInB = b?.checkInTime
|
||||
? new Date(b.checkInTime)
|
||||
: new Date(0);
|
||||
return checkInB - checkInA;
|
||||
})
|
||||
.map((item) => (
|
||||
<tr key={item.employeeId}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar
|
||||
firstName={item.firstName}
|
||||
lastName={item.lastName}
|
||||
></Avatar>
|
||||
<div className="d-flex flex-column">
|
||||
<a
|
||||
onClick={(e) =>
|
||||
navigate(
|
||||
`/employee/${item.employeeId}?for=attendance`
|
||||
)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{item.firstName} {item.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{item.jobRoleName}</td>
|
||||
<td>{item.organizationName || "--"}</td>
|
||||
|
||||
<td>
|
||||
{item.checkInTime
|
||||
? convertShortTime(item.checkInTime)
|
||||
: "--"}
|
||||
</td>
|
||||
<td>
|
||||
{item.checkOutTime
|
||||
? convertShortTime(item.checkOutTime)
|
||||
: "--"}
|
||||
</td>
|
||||
|
||||
<td className="text-center">
|
||||
<RenderAttendanceStatus
|
||||
attendanceData={item}
|
||||
handleModalData={handleModalData}
|
||||
Tab={1}
|
||||
currentDate={null}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!attendance && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="text-center text-secondary"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
No employees assigned to the project!
|
||||
</td>
|
||||
{attLoading ? (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "70vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
<>
|
||||
<table className="table table-hover ">
|
||||
<thead>
|
||||
<tr className="border-top-1">
|
||||
<th colSpan={2}>Name</th>
|
||||
<th className="text-start actions-col text-center">Role</th>
|
||||
{/* <th>Organization</th> */}
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>
|
||||
Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>
|
||||
Check-Out
|
||||
</th>
|
||||
<th className="actions-col">Actions</th>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
) : (
|
||||
<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
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0 ">
|
||||
{currentItems &&
|
||||
currentItems
|
||||
.sort((a, b) => {
|
||||
const checkInA = a?.checkInTime
|
||||
? new Date(a.checkInTime)
|
||||
: new Date(0);
|
||||
const checkInB = b?.checkInTime
|
||||
? new Date(b.checkInTime)
|
||||
: new Date(0);
|
||||
return checkInB - checkInA;
|
||||
})
|
||||
.map((item) => (
|
||||
<tr key={item.employeeId}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar
|
||||
firstName={item.firstName}
|
||||
lastName={item.lastName}
|
||||
></Avatar>
|
||||
<div className="d-flex flex-column">
|
||||
<a
|
||||
onClick={(e) =>
|
||||
navigate(
|
||||
`/employee/${item.employeeId}?for=attendance`
|
||||
)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{item.firstName} {item.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="text-start action-col">{item.jobRoleName}</td>
|
||||
{/* <td>{item.organizationName || "--"}</td> */}
|
||||
|
||||
<td>
|
||||
{item.checkInTime
|
||||
? convertShortTime(item.checkInTime)
|
||||
: "--"}
|
||||
</td>
|
||||
<td>
|
||||
{item.checkOutTime
|
||||
? convertShortTime(item.checkOutTime)
|
||||
: "--"}
|
||||
</td>
|
||||
|
||||
<td className="text-center actions-col">
|
||||
<RenderAttendanceStatus
|
||||
attendanceData={item}
|
||||
handleModalData={handleModalData}
|
||||
Tab={1}
|
||||
currentDate={null}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!attendance && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="text-center text-secondary"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
No employees assigned to the project!
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center text-muted"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: attendanceList.length === 0
|
||||
? "No employees assigned to the project."
|
||||
: "No pending records available."}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||
<nav aria-label="Page ">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||
<li
|
||||
className={`page-item ${currentPage === 1 ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
onClick={() => paginate(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,6 +15,7 @@ import AttendanceRepository from "../../repositories/AttendanceRepository";
|
||||
import { useAttendancesLogs } from "../../hooks/useAttendance";
|
||||
import { queryClient } from "../../layouts/AuthLayout";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
@ -45,7 +46,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPending, setShowPending] = useState(false);
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [processedData, setProcessedData] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const today = new Date();
|
||||
@ -69,52 +72,93 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
};
|
||||
|
||||
const sortByName = (a, b) => {
|
||||
const nameA = (a.firstName + a.lastName).toLowerCase();
|
||||
const nameB = (b.firstName + b.lastName).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
|
||||
const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
|
||||
return nameA?.localeCompare(nameB);
|
||||
};
|
||||
|
||||
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
|
||||
const {
|
||||
data = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isFetching,
|
||||
} = useAttendancesLogs(
|
||||
selectedProject,
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
organizationId
|
||||
);
|
||||
const filtering = useCallback(
|
||||
(dataToFilter) => {
|
||||
const filteredData = showPending
|
||||
? dataToFilter.filter((item) => item.checkOutTime === null)
|
||||
: dataToFilter;
|
||||
|
||||
const processedData = useMemo(() => {
|
||||
const filteredData = showPending
|
||||
? data.filter((item) => item.checkOutTime === null)
|
||||
: data;
|
||||
const group1 = filteredData
|
||||
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
|
||||
.sort(sortByName);
|
||||
const group2 = filteredData
|
||||
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
|
||||
.sort(sortByName);
|
||||
const group3 = filteredData
|
||||
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
|
||||
.sort(sortByName);
|
||||
const group4 = filteredData.filter(
|
||||
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
|
||||
);
|
||||
const group5 = filteredData
|
||||
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
|
||||
.sort(sortByName);
|
||||
const group6 = filteredData
|
||||
.filter((d) => d.activity === 5)
|
||||
.sort(sortByName);
|
||||
|
||||
const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName);
|
||||
const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName);
|
||||
const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName);
|
||||
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime));
|
||||
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName);
|
||||
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName);
|
||||
const sortedList = [
|
||||
...group1,
|
||||
...group2,
|
||||
...group3,
|
||||
...group4,
|
||||
...group5,
|
||||
...group6,
|
||||
];
|
||||
|
||||
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6];
|
||||
// Group by date
|
||||
const groupedByDate = sortedList.reduce((acc, item) => {
|
||||
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
||||
if (date) {
|
||||
acc[date] = acc[date] || [];
|
||||
acc[date].push(item);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const groupedByDate = sortedList.reduce((acc, item) => {
|
||||
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
||||
if (date) {
|
||||
acc[date] = acc[date] || [];
|
||||
acc[date].push(item);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const sortedDates = Object.keys(groupedByDate).sort(
|
||||
(a, b) => new Date(b) - new Date(a)
|
||||
);
|
||||
|
||||
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
|
||||
return sortedDates.flatMap((date) => groupedByDate[date]);
|
||||
const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
|
||||
setProcessedData(finalData);
|
||||
},
|
||||
[showPending]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.length) {
|
||||
filtering(data);
|
||||
}
|
||||
}, [data, showPending]);
|
||||
|
||||
// New useEffect to handle search filtering
|
||||
const filteredSearchData = useMemo(() => {
|
||||
if (!searchTerm) return processedData;
|
||||
|
||||
const lowercased = searchTerm.toLowerCase();
|
||||
return processedData.filter((item) =>
|
||||
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
|
||||
);
|
||||
if (!searchTerm) {
|
||||
return processedData;
|
||||
}
|
||||
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
return processedData.filter((item) => {
|
||||
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||
return fullName.includes(lowercasedSearchTerm);
|
||||
});
|
||||
}, [processedData, searchTerm]);
|
||||
|
||||
const {
|
||||
@ -127,27 +171,34 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
resetPage();
|
||||
}, [filteredSearchData]);
|
||||
}, [filteredSearchData, resetPage]);
|
||||
|
||||
const handler = useCallback(
|
||||
(msg) => {
|
||||
const { startDate, endDate } = dateRange;
|
||||
const checkIn = msg.response.checkInTime.substring(0, 10);
|
||||
|
||||
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) {
|
||||
if (
|
||||
selectedProject === msg.projectId &&
|
||||
startDate <= checkIn &&
|
||||
checkIn <= endDate
|
||||
) {
|
||||
queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
|
||||
if (!oldData) {
|
||||
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
|
||||
return;
|
||||
}
|
||||
return oldData.map((record) =>
|
||||
record.id === msg.response.id ? { ...record, ...msg.response } : record
|
||||
const updatedAttendance = oldData.map((record) =>
|
||||
record.id === msg.response.id
|
||||
? { ...record, ...msg.response }
|
||||
: record
|
||||
);
|
||||
filtering(updatedAttendance);
|
||||
return updatedAttendance;
|
||||
});
|
||||
resetPage();
|
||||
}
|
||||
},
|
||||
[selectedProject, dateRange, resetPage]
|
||||
[selectedProject, dateRange, filtering, resetPage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -159,10 +210,18 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
(msg) => {
|
||||
const { startDate, endDate } = dateRange;
|
||||
if (data.some((item) => item.employeeId == msg.employeeId)) {
|
||||
// dispatch(
|
||||
// fetchAttendanceData({
|
||||
// ,
|
||||
// fromDate: startDate,
|
||||
// toDate: endDate,
|
||||
// })
|
||||
// );
|
||||
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[data, refetch]
|
||||
[selectedProject, dateRange, data, refetch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -170,64 +229,37 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
return () => eventBus.off("employee", employeeHandler);
|
||||
}, [employeeHandler]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="dataTables_length text-start py-2 d-flex flex-wrap justify-content-between align-items-center"
|
||||
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
||||
id="DataTables_Table_0_length"
|
||||
>
|
||||
{/* Left Side - Date Picker */}
|
||||
<div className="d-flex align-items-center">
|
||||
<div className=" col-12">
|
||||
<DateRangePicker
|
||||
onRangeChange={setDateRange}
|
||||
defaultStartDate={yesterday}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Pending Attendance Switch */}
|
||||
<div className="form-check form-switch d-flex align-items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
disabled={isFetching}
|
||||
checked={showPending}
|
||||
onChange={(e) => setShowPending(e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label ms-2"
|
||||
htmlFor="inactiveEmployeesCheckbox"
|
||||
>
|
||||
Pending Attendance
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
className="table-responsive modal-min-h text-nowrap"
|
||||
style={{ minHeight: "200px" }}
|
||||
>
|
||||
<div className="table-responsive text-nowrap ">
|
||||
{isLoading ? (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "50vh" }}
|
||||
style={{ minHeight: "70vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
<SpinnerLoader/>
|
||||
</div>
|
||||
) : filteredSearchData?.length > 0 ? (
|
||||
|
||||
<table className="table mb-0">
|
||||
<table className="table mb-0 table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border-top-1" colSpan={2}>
|
||||
Name
|
||||
</th>
|
||||
<th className="border-top-1">Date</th>
|
||||
<th>Organization</th>
|
||||
{/* <th>Organization</th> */}
|
||||
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>{" "}
|
||||
Check-In
|
||||
@ -235,7 +267,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
|
||||
</th>
|
||||
<th>Action</th>
|
||||
<th className="actions-col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -246,9 +278,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
const previousAttendance = arr[index - 1];
|
||||
const previousDate = previousAttendance
|
||||
? moment(
|
||||
previousAttendance.checkInTime ||
|
||||
previousAttendance.checkOutTime
|
||||
).format("YYYY-MM-DD")
|
||||
previousAttendance.checkInTime ||
|
||||
previousAttendance.checkOutTime
|
||||
).format("YYYY-MM-DD")
|
||||
: null;
|
||||
|
||||
if (!previousDate || currentDate !== previousDate) {
|
||||
@ -276,7 +308,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
<div className="d-flex flex-column">
|
||||
<a
|
||||
onClick={() =>
|
||||
navigate(`/employee/${attendance.employeeId}?for=attendance`)
|
||||
navigate(
|
||||
`/employee/${attendance.employeeId}?for=attendance`
|
||||
)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
@ -292,14 +326,14 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
attendance.checkInTime || attendance.checkOutTime
|
||||
).format("DD-MMM-YYYY")}
|
||||
</td>
|
||||
<td>{attendance.organizationName || "--"}</td>
|
||||
{/* <td>{attendance.organizationName || "--"}</td> */}
|
||||
<td>{convertShortTime(attendance.checkInTime)}</td>
|
||||
<td>
|
||||
{attendance.checkOutTime
|
||||
? convertShortTime(attendance.checkOutTime)
|
||||
: "--"}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<td className="text-center actions-col">
|
||||
<RenderAttendanceStatus
|
||||
attendanceData={attendance}
|
||||
handleModalData={handleModalData}
|
||||
@ -314,10 +348,13 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="my-12">
|
||||
<span className="text-secondary">
|
||||
No attendance record found in selected date range.
|
||||
</span>
|
||||
<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>
|
||||
@ -330,45 +367,11 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||
</div>
|
||||
)}
|
||||
{filteredSearchData.length > ITEMS_PER_PAGE && (
|
||||
<nav aria-label="Page ">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</li>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(pageNumber) => (
|
||||
<li
|
||||
key={pageNumber}
|
||||
className={`page-item ${currentPage === pageNumber ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => paginate(pageNumber)}
|
||||
>
|
||||
{pageNumber}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -9,6 +9,7 @@ import showToast from "../../services/toastService";
|
||||
import { checkIfCurrentDate } from "../../utils/dateUtils";
|
||||
import { useMarkAttendance } from "../../hooks/useAttendance";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
|
||||
const createSchema = (modeldata) => {
|
||||
return z
|
||||
@ -19,31 +20,36 @@ const createSchema = (modeldata) => {
|
||||
.max(200, "Description should be less than 200 characters")
|
||||
.optional(),
|
||||
})
|
||||
.refine((data) => {
|
||||
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
|
||||
const checkIn = new Date(modeldata.checkInTime);
|
||||
const [time, modifier] = data.markTime.split(" ");
|
||||
const [hourStr, minuteStr] = time.split(":");
|
||||
let hour = parseInt(hourStr, 10);
|
||||
const minute = parseInt(minuteStr, 10);
|
||||
.refine(
|
||||
(data) => {
|
||||
if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
|
||||
const checkIn = new Date(modeldata.checkInTime);
|
||||
const [time, modifier] = data.markTime.split(" ");
|
||||
const [hourStr, minuteStr] = time.split(":");
|
||||
let hour = parseInt(hourStr, 10);
|
||||
const minute = parseInt(minuteStr, 10);
|
||||
|
||||
if (modifier === "PM" && hour !== 12) hour += 12;
|
||||
if (modifier === "AM" && hour === 12) hour = 0;
|
||||
if (modifier === "PM" && hour !== 12) hour += 12;
|
||||
if (modifier === "AM" && hour === 12) hour = 0;
|
||||
|
||||
const checkOut = new Date(checkIn);
|
||||
checkOut.setHours(hour, minute, 0, 0);
|
||||
const checkOut = new Date(checkIn);
|
||||
checkOut.setHours(hour, minute, 0, 0);
|
||||
|
||||
return checkOut >= checkIn;
|
||||
return checkOut >= checkIn;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Checkout time must be later than check-in time",
|
||||
path: ["markTime"],
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: "Checkout time must be later than check-in time",
|
||||
path: ["markTime"],
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
const [currentProject, setCurrentProject] = useState(null);
|
||||
const projectId = useSelectedProject();
|
||||
const { projectNames, loading } = useProjectName();
|
||||
const { mutate: MarkAttendance } = useMarkAttendance();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const coords = usePositionTracker();
|
||||
@ -95,17 +101,24 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && projectNames) {
|
||||
setCurrentProject(
|
||||
projectNames?.find((project) => project.id === projectId)
|
||||
);
|
||||
}
|
||||
}, [projectNames, projectId, loading]);
|
||||
|
||||
return (
|
||||
<form className="row p-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 d-flex justify-content-center mb-4">
|
||||
<label className="fs-5 tex-semibold text-center">
|
||||
<div className="col-12 d-flex justify-content-center mt-2">
|
||||
<label className="fs-5 text-dark text-center">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
? "Check-Out "
|
||||
: "Check-In "}
|
||||
? `Check out for ${currentProject?.name}`
|
||||
: `Check In for ${currentProject?.name}`}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="col-6 col-md-6 text-start">
|
||||
<label className="form-label" htmlFor="checkInDate">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
@ -207,7 +220,7 @@ export const Regularization = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="row " onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<p>Regularize Attendance</p>
|
||||
<label className="form-label" htmlFor="description">
|
||||
|
||||
@ -24,7 +24,8 @@ import { useProfile } from "../../hooks/useProfile";
|
||||
import { refreshData, setProjectId } from "../../slices/localVariablesSlice";
|
||||
import InfraTable from "../Project/Infrastructure/InfraTable";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
import Loader from "../common/Loader";
|
||||
|
||||
const InfraPlanning = () => {
|
||||
const { profile: LoggedUser, refetch: fetchData } = useProfile();
|
||||
const dispatch = useDispatch();
|
||||
@ -32,7 +33,7 @@ const InfraPlanning = () => {
|
||||
const selectedService = useCurrentService();
|
||||
|
||||
const { projectInfra, isLoading, isError, error, isFetched } =
|
||||
useProjectInfra(selectedProject, selectedService || "");
|
||||
useProjectInfra(selectedProject, selectedService || "" );
|
||||
|
||||
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||
const canApproveTask = useHasUserPermission(APPROVE_TASK);
|
||||
@ -56,25 +57,14 @@ const InfraPlanning = () => {
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "60vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
);
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
|
||||
return (
|
||||
<div
|
||||
className="text-center d-flex justify-content-center align-items-center text-muted"
|
||||
style={{ minHeight: "40vh", fontSize: "0.9rem" }}
|
||||
>
|
||||
<p className="my-3 m-0">No Result Found</p>
|
||||
<div className="text-center">
|
||||
<p className="my-3">No Result Found</p>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,48 +1,42 @@
|
||||
import React, { useCallback, useEffect, useState, useMemo } from "react";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { convertShortTime } from "../../utils/dateUtils";
|
||||
import RegularizationActions from "./RegularizationActions";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useRegularizationRequests } from "../../hooks/useAttendance";
|
||||
import moment from "moment";
|
||||
import usePagination from "../../hooks/usePagination";
|
||||
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 Pagination from "../../components/common/Pagination";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { employee } from "../../data/masters";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
const Regularization = ({
|
||||
handleRequest,
|
||||
searchTerm,
|
||||
projectId,
|
||||
organizationId,
|
||||
IncludeInActive,
|
||||
}) => {
|
||||
const Regularization = ({ handleRequest, searchTerm, projectId, organizationId, IncludeInActive }) => {
|
||||
const queryClient = useQueryClient();
|
||||
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
||||
const selectedProject = useSelectedProject();
|
||||
const [regularizesList, setregularizedList] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const { regularizes, loading, error, refetch } = useRegularizationRequests(
|
||||
selectedProject,
|
||||
organizationId,
|
||||
IncludeInActive
|
||||
);
|
||||
const { regularizes, loading, error, refetch } =
|
||||
useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
|
||||
|
||||
useEffect(() => {
|
||||
if (!regularizes) return
|
||||
if (regularizes?.length) {
|
||||
setregularizedList(regularizes);
|
||||
|
||||
if (regularizes && regularizes.length) {
|
||||
setregularizedList((prev) => {
|
||||
const prevIds = prev.map((i) => i.id).join(",");
|
||||
const newIds = regularizes.map((i) => i.id).join(",");
|
||||
if (prevIds !== newIds) {
|
||||
return regularizes;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}, [regularizes]);
|
||||
|
||||
|
||||
const sortByName = (a, b) => {
|
||||
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
|
||||
const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
|
||||
@ -75,15 +69,48 @@ const Regularization = ({
|
||||
}
|
||||
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
return sortedList.filter((item) => {
|
||||
const fullName = `${item?.firstName} ${item?.lastName}`.toLowerCase();
|
||||
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||
return fullName.includes(lowercasedSearchTerm);
|
||||
});
|
||||
}, [regularizesList, searchTerm]);
|
||||
|
||||
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
||||
filteredSearchData,
|
||||
20
|
||||
);
|
||||
// const filteredSearchData = useMemo(() => {
|
||||
// let sortedList = [...regularizesList].sort(sortByName);
|
||||
|
||||
// // Search filter
|
||||
// if (searchTerm) {
|
||||
// const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
// sortedList = sortedList.filter((item) => {
|
||||
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||
// return fullName.includes(lowercasedSearchTerm);
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Organization filter
|
||||
// if (filters?.selectedOrganization) {
|
||||
// sortedList = sortedList.filter(
|
||||
// (item) => item.organization?.name === filters.selectedOrganization
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Services filter
|
||||
// if (filters?.selectedServices?.length > 0) {
|
||||
// sortedList = sortedList.filter((item) =>
|
||||
// filters.selectedServices.includes(item.service?.name)
|
||||
// );
|
||||
// }
|
||||
|
||||
// return sortedList;
|
||||
// }, [regularizesList, searchTerm, filters]);
|
||||
|
||||
|
||||
const { currentPage, totalPages, currentItems, paginate } =
|
||||
usePagination(filteredSearchData, 20);
|
||||
|
||||
// Reset pagination when the search term or data changes
|
||||
useEffect(() => {
|
||||
|
||||
}, [filteredSearchData]);
|
||||
|
||||
useEffect(() => {
|
||||
eventBus.on("regularization", handler);
|
||||
@ -104,39 +131,34 @@ const Regularization = ({
|
||||
return () => eventBus.off("employee", employeeHandler);
|
||||
}, [employeeHandler]);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="table-responsive modal-min-h pt-3 text-nowrap pb-4"
|
||||
style={{ minHeight: "200px" }}
|
||||
>
|
||||
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
|
||||
{loading ? (
|
||||
<div
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "50vh" }}
|
||||
style={{ minHeight: "70vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
<SpinnerLoader/>
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
<table className="table mb-0">
|
||||
<table className="table mb-0 table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}>Name</th>
|
||||
<th>Date</th>
|
||||
<th>Organization</th>
|
||||
{/* <th>Organization</th> */}
|
||||
<th>
|
||||
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
||||
</th>
|
||||
<th>
|
||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||
</th>
|
||||
<th colSpan={2}>
|
||||
Requested By
|
||||
</th>
|
||||
<th >
|
||||
Requested At
|
||||
</th>
|
||||
<th>Action</th>
|
||||
|
||||
<th>Request By</th>
|
||||
<th>Requested At</th>
|
||||
<th className="actions-col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -144,42 +166,47 @@ const Regularization = ({
|
||||
<tr key={index}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar firstName={att.firstName} lastName={att.lastName} />
|
||||
<div className="d-flex flex-column"> <a
|
||||
onClick={() =>
|
||||
navigate(`/employee/${att.employeeId}?for=attendance`)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer" >
|
||||
<Avatar
|
||||
firstName={att.firstName}
|
||||
lastName={att.lastName}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
{/* <a href="#" className="text-heading text-truncate">
|
||||
<span className="fw-normal">
|
||||
{att.firstName} {att.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||
|
||||
<td>{att.organizationName || "--"}</td>
|
||||
|
||||
<td>{convertShortTime(att.checkInTime)}</td>
|
||||
<td>
|
||||
{att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
|
||||
</td>
|
||||
|
||||
<td colSpan={2}>
|
||||
{att.requestedBy ? (<div className="d-flex justify-content-start align-items-center">
|
||||
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
|
||||
<div className="d-flex flex-column">
|
||||
<a href="#" className="text-heading text-truncate">
|
||||
</a> */}
|
||||
<a
|
||||
onClick={() =>
|
||||
navigate(`/employee/${att.employeeId}?for=attendance`)
|
||||
}
|
||||
className="text-heading text-truncate cursor-pointer"
|
||||
>
|
||||
<span className="fw-normal">
|
||||
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName}
|
||||
{att.firstName} {att.lastName}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>) : (<small>--</small>)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||
|
||||
{/* <td>{att.organizationName || "--"}</td> */}
|
||||
|
||||
<td>{convertShortTime(att.checkInTime)}</td>
|
||||
<td>
|
||||
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
||||
</td>
|
||||
<td>
|
||||
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt, true) : "--"}
|
||||
{att.requestedBy
|
||||
? `${att.requestedBy?.firstName} ${att.requestedBy?.lastName}`
|
||||
: "--"}
|
||||
</td>
|
||||
<td>
|
||||
{att.requestedAt
|
||||
? moment(att.requestedAt).format("DD-MMM-YYYY")
|
||||
: "--"}
|
||||
</td>
|
||||
<td className="text-center ">
|
||||
<RegularizationActions
|
||||
@ -192,6 +219,7 @@ const Regularization = ({
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
) : (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
@ -205,14 +233,13 @@ const Regularization = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{totalPages > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && totalPages > 1 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense";
|
||||
import { useExpenseTransactions } from "../../hooks/useExpense";
|
||||
import Error from "../common/Error";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Loader, { SpinnerLoader } from "../common/Loader";
|
||||
@ -11,10 +10,11 @@ import { employee } from "../../data/masters";
|
||||
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
|
||||
import { formatFigure } from "../../utils/appUtils";
|
||||
|
||||
const AdvancePaymentList = ({ employeeId, searchString }) => {
|
||||
const { setBalance } = useAdvancePaymentContext();
|
||||
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;
|
||||
@ -174,7 +174,7 @@ const AdvancePaymentList = ({ employeeId, searchString }) => {
|
||||
)
|
||||
) : col.key === "balance" ? (
|
||||
<div className="d-flex align-items-center justify-content-end">
|
||||
{/* <DecideCreditOrDebit financeUId={row?.financeUId} /> */}
|
||||
<DecideCreditOrDebit financeUId={row?.financeUId} />
|
||||
<span className="mx-2">
|
||||
{formatFigure(row.currentBalance)}
|
||||
</span>
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
import React from 'react'
|
||||
import Avatar from "../../components/common/Avatar"; // <-- ADD THIS
|
||||
import { useExpenseAllTransactionsList } from '../../hooks/useExpense';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatFigure } from '../../utils/appUtils';
|
||||
|
||||
const AdvancePaymentList1 = ({ searchString }) => {
|
||||
|
||||
const { data, isError, isLoading, error } =
|
||||
useExpenseAllTransactionsList(searchString);
|
||||
|
||||
const rows = data || [];
|
||||
const navigate = useNavigate();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "employee",
|
||||
label: "Employee Name",
|
||||
align: "text-start",
|
||||
customRender: (r) => (
|
||||
<div className="d-flex align-items-center gap-2" onClick={() => navigate(`/advance-payment/${r.id}`)}
|
||||
style={{ cursor: "pointer" }}>
|
||||
<Avatar firstName={r.firstName} lastName={r.lastName} />
|
||||
|
||||
<span className="fw-medium">
|
||||
{r.firstName} {r.lastName}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "jobRoleName",
|
||||
label: "Job Role",
|
||||
align: "text-start",
|
||||
customRender: (r) => (
|
||||
<span className="fw-semibold">
|
||||
{r.jobRoleName}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "balanceAmount",
|
||||
label: "Balance (₹)",
|
||||
align: "text-end",
|
||||
customRender: (r) => (
|
||||
<span className="fw-semibold fs-6">
|
||||
{formatFigure(r.balanceAmount, {
|
||||
// type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) return <p className="text-center py-4">Loading...</p>;
|
||||
if (isError) return <p className="text-center py-4 text-danger">{error.message}</p>;
|
||||
|
||||
return (
|
||||
<div className="card-datatable" id="payment-request-table">
|
||||
<div className="mx-2">
|
||||
<table className="table border-top dataTable text-nowrap align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{rows.length > 0 ? (
|
||||
rows.map((row) => (
|
||||
<tr key={row.id} className="align-middle" style={{ height: "40px" }}>
|
||||
{columns.map((col) => (
|
||||
<td key={col.key} className={`d-table-cell ${col.align} py-3`}>
|
||||
{col.customRender
|
||||
? col.customRender(row)
|
||||
: col.getValue(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center border-0 py-3">
|
||||
No Employees Found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdvancePaymentList1;
|
||||
@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
import PropTypes from "prop-types";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
const HorizontalBarChart = ({
|
||||
seriesData = [],
|
||||
@ -24,12 +23,8 @@ const HorizontalBarChart = ({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl">
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "50vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
<span className="text-gray-500 text-sm">Loading chart...</span>
|
||||
{/* Replace this with a skeleton or spinner if you prefer */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
import PropTypes from "prop-types";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
const LineChart = ({
|
||||
seriesData = [],
|
||||
@ -10,28 +9,24 @@ const LineChart = ({
|
||||
loading = false,
|
||||
lineChartCategoriesDates = [],
|
||||
}) => {
|
||||
const hasValidData =
|
||||
Array.isArray(seriesData) &&
|
||||
seriesData.length > 0 &&
|
||||
Array.isArray(categories) &&
|
||||
categories.length > 0;
|
||||
const hasValidData =
|
||||
Array.isArray(seriesData) &&
|
||||
seriesData.length > 0 &&
|
||||
Array.isArray(categories) &&
|
||||
categories.length > 0;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[350px] text-gray-500">
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "50vh" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[350px] text-gray-500">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500 mr-2" />
|
||||
Loading chart...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasValidData) {
|
||||
return <div className="text-center text-gray-500">No data to display</div>;
|
||||
}
|
||||
if (!hasValidData) {
|
||||
return <div className="text-center text-gray-500">No data to display</div>;
|
||||
}
|
||||
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
@ -134,16 +129,16 @@ const LineChart = ({
|
||||
};
|
||||
|
||||
LineChart.propTypes = {
|
||||
seriesData: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
})
|
||||
),
|
||||
categories: PropTypes.arrayOf(PropTypes.string),
|
||||
colors: PropTypes.arrayOf(PropTypes.string),
|
||||
title: PropTypes.string,
|
||||
loading: PropTypes.bool
|
||||
seriesData: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
})
|
||||
),
|
||||
categories: PropTypes.arrayOf(PropTypes.string),
|
||||
colors: PropTypes.arrayOf(PropTypes.string),
|
||||
title: PropTypes.string,
|
||||
loading: PropTypes.bool
|
||||
};
|
||||
|
||||
export default LineChart;
|
||||
|
||||
42
src/components/Context/ChangePasswordContext.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { createContext, useState, useContext } from "react";
|
||||
import ChangePasswordPage from "../../pages/authentication/ChangePassword";
|
||||
|
||||
const ChangePasswordContext = createContext();
|
||||
|
||||
export const ChangePasswordProvider = ({ children }) => {
|
||||
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
||||
|
||||
const openChangePassword = () => setIsChangePasswordOpen(true);
|
||||
const closeChangePassword = () => setIsChangePasswordOpen(false);
|
||||
|
||||
return (
|
||||
<ChangePasswordContext.Provider
|
||||
value={{ isChangePasswordOpen, openChangePassword, closeChangePassword }}
|
||||
>
|
||||
{children}
|
||||
|
||||
{isChangePasswordOpen && (
|
||||
<>
|
||||
{/* This is the main Bootstrap modal container */}
|
||||
{/* It provides the fixed positioning and high z-index */}
|
||||
<div
|
||||
className="modal fade show" // 'fade' for animation, 'show' to make it visible
|
||||
style={{ display: 'block' }} // Explicitly set display: block for immediate visibility
|
||||
tabIndex="-1" // Makes the modal focusable
|
||||
role="dialog" // ARIA role for accessibility
|
||||
aria-labelledby="changePasswordModalLabel" // Link to a heading for accessibility
|
||||
aria-modal="true" // Indicate it's a modal dialog
|
||||
>
|
||||
{/* The ChangePasswordPage component itself contains the modal-dialog and modal-content */}
|
||||
<ChangePasswordPage onClose={closeChangePassword} />
|
||||
</div>
|
||||
|
||||
{/* The modal backdrop */}
|
||||
<div className="modal-backdrop fade show"></div>
|
||||
</>
|
||||
)}
|
||||
</ChangePasswordContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useChangePassword = () => useContext(ChangePasswordContext);
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCurrentService } from "../../hooks/useProjects";
|
||||
import { useCurrentService, useProjectInfra } from "../../hooks/useProjects";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -10,14 +10,15 @@ import {
|
||||
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
import { localToUtc } from "../../utils/appUtils";
|
||||
import { useTaskFilter } from "../../hooks/useTasks";
|
||||
|
||||
const TaskReportFilterPanel = ({ handleFilter }) => {
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
const selectedProject = useSelectedProject();
|
||||
const selectedProjec = useSelectedProject();
|
||||
const selectedService = useCurrentService();
|
||||
const { data } = useTaskFilter(selectedProject);
|
||||
|
||||
const { projectInfra, isLoading, error, isFetched } = useProjectInfra(
|
||||
selectedProjec,
|
||||
selectedService
|
||||
);
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(TaskReportFilterSchema),
|
||||
defaultValues: TaskReportDefaultValue,
|
||||
@ -29,75 +30,58 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
const closePanel = () => {
|
||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||
};
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const filterPayload = {
|
||||
...formData,
|
||||
dateFrom: localToUtc(formData.dateFrom),
|
||||
dateTo: localToUtc(formData.dateTo),
|
||||
};
|
||||
console.log(formData)
|
||||
const filterPayload = {
|
||||
startDate:localToUtc(formData.startDate),
|
||||
endDate:localToUtc(formData.endDate)
|
||||
|
||||
}
|
||||
handleFilter(filterPayload);
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
const onClear =()=>{
|
||||
setResetKey((prev) => prev + 1);
|
||||
handleFilter(TaskReportDefaultValue);
|
||||
reset(TaskReportDefaultValue);
|
||||
closePanel();
|
||||
};
|
||||
handleFilter(TaskReportDefaultValue)
|
||||
reset(TaskReportDefaultValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||
<div className="mb-3 w-100">
|
||||
<label className="fw-semibold">Choose Date Range:</label>
|
||||
<DateRangePicker1
|
||||
className="w-100"
|
||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||
startField="dateFrom"
|
||||
endField="dateTo"
|
||||
startField="startDate"
|
||||
endField="endDate"
|
||||
resetSignal={resetKey}
|
||||
defaultRange={false}
|
||||
maxDate={new Date()}
|
||||
defaultRange={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row mb-2">
|
||||
<SelectMultiple
|
||||
name="buildingIds"
|
||||
label="Building"
|
||||
options={data?.buildings}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
</div>
|
||||
<div className="row mb-2">
|
||||
<SelectMultiple
|
||||
name="floorIds"
|
||||
label="Floor"
|
||||
options={data?.floors}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
</div>
|
||||
<div className="row mb-2">
|
||||
<SelectMultiple
|
||||
name="activityIds"
|
||||
label="Activities"
|
||||
options={data?.activities}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="row g-2">
|
||||
<SelectMultiple
|
||||
name="buildingIds"
|
||||
label="Projects"
|
||||
options={projectInfra}
|
||||
labelKey="buildingName"
|
||||
valueKey="id"
|
||||
/>
|
||||
</div>
|
||||
<div className="row g-2">
|
||||
<SelectMultiple
|
||||
name="floorIds"
|
||||
label="Floor"
|
||||
options={projectInfra}
|
||||
labelKey="floorName"
|
||||
valueKey="id"
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<div className="d-flex justify-content-end py-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-label-secondary btn-sm"
|
||||
onClick={onClear}
|
||||
>
|
||||
<button type="button" className="btn btn-label-secondary btn-sm" onClick={onClear}>
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-sm">
|
||||
|
||||
@ -29,7 +29,7 @@ const TaskReportList = () => {
|
||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||
|
||||
const { service, openModal, closeModal, filter } = useDailyProgrssContext();
|
||||
const { service, openModal, closeModal,filter } = useDailyProgrssContext();
|
||||
const selectedProject = useSelectedProject();
|
||||
const { projectNames } = useProjectName();
|
||||
|
||||
@ -37,7 +37,7 @@ const TaskReportList = () => {
|
||||
selectedProject,
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
service, filter
|
||||
service,filter
|
||||
);
|
||||
|
||||
const ProgrssReportColumn = [
|
||||
@ -192,116 +192,109 @@ const TaskReportList = () => {
|
||||
if (isLoading) return <TaskReportListSkeleton />;
|
||||
if (isError) return <div>Loading....</div>;
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-2 table-responsive text-nowrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<div className="mt-2">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-start">Activity</th>
|
||||
<th>
|
||||
<span>
|
||||
Total Pending{" "}
|
||||
<HoverPopup
|
||||
title="Total Pending Task"
|
||||
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
||||
>
|
||||
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||
</HoverPopup>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>
|
||||
Reported/Planned{" "}
|
||||
<HoverPopup
|
||||
title="Reported and Planned Task"
|
||||
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
||||
>
|
||||
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||
</HoverPopup>
|
||||
</span>
|
||||
</th>
|
||||
<th>Assign Date</th>
|
||||
<th>Team</th>
|
||||
<th className="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedTasks.length === 0 && (
|
||||
<tr>
|
||||
<th className="text-start">Activity</th>
|
||||
<th>
|
||||
<span>
|
||||
Total Pending{" "}
|
||||
<HoverPopup
|
||||
id="total_pending_task"
|
||||
title="Total Pending Task"
|
||||
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
||||
>
|
||||
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||
</HoverPopup>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>
|
||||
Reported/Planned{" "}
|
||||
<HoverPopup
|
||||
id="reportes_and_planned_task"
|
||||
title="Reported and Planned Task"
|
||||
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
||||
>
|
||||
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||
</HoverPopup>
|
||||
</span>
|
||||
</th>
|
||||
<th>Assign Date</th>
|
||||
<th>Team</th>
|
||||
<th className="text-center">Actions</th>
|
||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
|
||||
No reports available
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedTasks.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
|
||||
No reports available
|
||||
)}
|
||||
|
||||
{groupedTasks.map(({ date, tasks }) => (
|
||||
<React.Fragment key={date}>
|
||||
<tr className="table-row-header text-start">
|
||||
<td colSpan={6}>
|
||||
<strong>{formatUTCToLocalTime(date)}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{groupedTasks.map(({ date, tasks }) => (
|
||||
<React.Fragment key={date}>
|
||||
<tr className="table-row-header text-start">
|
||||
<td colSpan={6}>
|
||||
<strong>{formatUTCToLocalTime(date)}</strong>
|
||||
{tasks.map((task, idx) => (
|
||||
<tr key={task.id || idx}>
|
||||
<td className="flex-wrap text-start">
|
||||
<div>
|
||||
{task.workItem.activityMaster?.activityName || "No Activity Name"}
|
||||
</div>
|
||||
<div className="text-sm py-2">
|
||||
{task.workItem.workArea?.floor?.building?.name} ›{" "}
|
||||
{task.workItem.workArea?.floor?.floorName} ›{" "}
|
||||
{task.workItem.workArea?.areaName}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{formatNumber(task.workItem.plannedWork)}
|
||||
</td>
|
||||
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
|
||||
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
||||
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
||||
<td className="text-center">
|
||||
<div className="d-flex justify-content-end gap-2">
|
||||
{ReportTaskRights && !task.reportedDate && (
|
||||
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
|
||||
Report
|
||||
</button>
|
||||
)}
|
||||
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
||||
<button
|
||||
className="btn btn-xs btn-warning"
|
||||
onClick={() => openModal("comments", { task, isActionAllow: true })}
|
||||
>
|
||||
QC
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-xs btn-primary"
|
||||
onClick={() => openModal("comments", { task, isActionAllow: false })}
|
||||
>
|
||||
Comment
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{tasks.map((task, idx) => (
|
||||
<tr key={task.id || idx}>
|
||||
<td className="flex-wrap text-start">
|
||||
<div>
|
||||
{task.workItem.activityMaster?.activityName || "No Activity Name"}
|
||||
</div>
|
||||
<div className="text-sm py-2">
|
||||
{task.workItem.workArea?.floor?.building?.name} ›{" "}
|
||||
{task.workItem.workArea?.floor?.floorName} ›{" "}
|
||||
{task.workItem.workArea?.areaName}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{formatNumber(task.workItem.plannedWork)}
|
||||
</td>
|
||||
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
|
||||
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
||||
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
||||
<td className="text-center">
|
||||
<div className="d-flex justify-content-end gap-2">
|
||||
{ReportTaskRights && !task.reportedDate && (
|
||||
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
|
||||
Report
|
||||
</button>
|
||||
)}
|
||||
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
||||
<button
|
||||
className="btn btn-xs btn-warning"
|
||||
onClick={() => openModal("comments", { task, isActionAllow: true })}
|
||||
>
|
||||
QC
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-xs btn-primary"
|
||||
onClick={() => openModal("comments", { task, isActionAllow: false })}
|
||||
>
|
||||
Comment
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
{
|
||||
data?.data?.length > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={data.totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{data?.data?.length > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={data.totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const TaskReportFilterSchema = z.object({
|
||||
buildingIds: z.array(z.string()).optional(),
|
||||
floorIds: z.array(z.string()).optional(),
|
||||
activityIds: z.array(z.string()).optional(),
|
||||
dateFrom: z.string().optional(),
|
||||
dateTo: z.string().optional(),
|
||||
// buildingIds: z.array(z.string()).optional(),
|
||||
// floorIds: z.array(z.string()).optional(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
});
|
||||
|
||||
export const TaskReportDefaultValue = {
|
||||
buildingIds:[],
|
||||
floorIds:[],
|
||||
activityIds:[],
|
||||
dateFrom:null,
|
||||
dateTo:null
|
||||
// buildingIds:[],
|
||||
// floorIds:[],
|
||||
startDate:null,
|
||||
endDate:null
|
||||
}
|
||||
@ -2,14 +2,13 @@ import React, { useState, useMemo } from "react";
|
||||
import ApexChart from "../Charts/Circle";
|
||||
import { useProjects } from "../../hooks/useProjects";
|
||||
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
|
||||
import { useSelectedProject } from "../../hooks/useSelectedProject"; // ✅ your custom hook
|
||||
import { useSelectedProject } from "../../hooks/useSelectedProject";
|
||||
|
||||
const Attendance = () => {
|
||||
const { projects } = useProjects();
|
||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
const [selectedDate, setSelectedDate] = useState(today);
|
||||
|
||||
// central project selection hook
|
||||
const selectedProjectId = useSelectedProject()
|
||||
|
||||
const {
|
||||
@ -31,7 +30,7 @@ const selectedProjectId = useSelectedProject()
|
||||
<div className="card-header mb-1 pb-0">
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
||||
<div className="card-title mb-0 text-start">
|
||||
<h5 className="mb-1">Attendance</h5>
|
||||
<h5 className="mb-1 card-title">Attendance</h5>
|
||||
<p className="card-subtitle">Daily Attendance Data</p>
|
||||
</div>
|
||||
|
||||
@ -136,7 +135,6 @@ const selectedProjectId = useSelectedProject()
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Details */}
|
||||
{AttendanceData?.activeTab === "Details" && (
|
||||
<div className="table-responsive" style={{ maxHeight: "300px" }}>
|
||||
<table className="table table-hover mb-0 text-start">
|
||||
|
||||
@ -4,40 +4,41 @@ import ReactApexChart from "react-apexcharts";
|
||||
import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data";
|
||||
import flatColors from "../Charts/flatColor";
|
||||
import ChartSkeleton from "../Charts/Skelton";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
});
|
||||
};
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { formatDate_DayMonth } from "../../utils/dateUtils";
|
||||
|
||||
const AttendanceOverview = () => {
|
||||
const [dayRange, setDayRange] = useState(7);
|
||||
const [view, setView] = useState("chart");
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const { attendanceOverviewData, loading, error } = useAttendanceOverviewData(
|
||||
projectId,
|
||||
dayRange
|
||||
);
|
||||
const {
|
||||
data: attendanceOverviewData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useAttendanceOverviewData(selectedProject, dayRange);
|
||||
|
||||
// Use empty array while loading
|
||||
const attendanceData = attendanceOverviewData || [];
|
||||
|
||||
const { tableData, roles, dates } = useMemo(() => {
|
||||
if (!attendanceData || attendanceData.length === 0) {
|
||||
return { tableData: [], roles: [], dates: [] };
|
||||
}
|
||||
|
||||
const map = new Map();
|
||||
|
||||
attendanceOverviewData.forEach((entry) => {
|
||||
const date = formatDate(entry.date);
|
||||
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(attendanceOverviewData.map((e) => e.role.trim())),
|
||||
];
|
||||
const uniqueRoles = [...new Set(attendanceData.map((e) => e.role.trim()))];
|
||||
const sortedDates = [...map.keys()];
|
||||
const data = sortedDates.map((date) => {
|
||||
|
||||
const tableData = sortedDates.map((date) => {
|
||||
const row = { date };
|
||||
uniqueRoles.forEach((role) => {
|
||||
row[role] = map.get(date)?.[role] ?? 0;
|
||||
@ -45,12 +46,8 @@ const AttendanceOverview = () => {
|
||||
return row;
|
||||
});
|
||||
|
||||
return {
|
||||
tableData: data,
|
||||
roles: uniqueRoles,
|
||||
dates: sortedDates,
|
||||
};
|
||||
}, [attendanceOverviewData]);
|
||||
return { tableData, roles: uniqueRoles, dates: sortedDates };
|
||||
}, [attendanceData]);
|
||||
|
||||
const chartSeries = roles.map((role) => ({
|
||||
name: role,
|
||||
@ -64,52 +61,31 @@ const AttendanceOverview = () => {
|
||||
height: 400,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 2,
|
||||
columnWidth: "60%",
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: tableData.map((row) => row.date),
|
||||
},
|
||||
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,
|
||||
axisBorder: { show: true, color: "#78909C" },
|
||||
axisTicks: { show: true, color: "#78909C", width: 6 },
|
||||
},
|
||||
legend: { position: "bottom" },
|
||||
fill: { opacity: 1 },
|
||||
colors: roles.map((_, i) => flatColors[i % flatColors.length]),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white px-4 rounded shadow d-flex flex-column h-100">
|
||||
{/* Header */}
|
||||
<div className="d-flex mt-2 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 className="bg-white p-4 rounded shadow d-flex flex-column position-relative">
|
||||
<div className="row mb-3 align-items-center">
|
||||
<div className="col-md-6 text-start">
|
||||
<p className="mb-1 fs-6 fs-md-5 fw-medium">Attendance Overview</p>
|
||||
<p className="card-subtitle text-muted mb-0">
|
||||
Role-wise present count
|
||||
</p>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
|
||||
<div className="col-md-6 d-flex flex-column align-items-end gap-2">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
className="form-select form-select-sm w-auto"
|
||||
value={dayRange}
|
||||
onChange={(e) => setDayRange(Number(e.target.value))}
|
||||
>
|
||||
@ -117,41 +93,53 @@ const AttendanceOverview = () => {
|
||||
<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"
|
||||
|
||||
<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"></i>
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm p-1 ${view === "table" ? "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>
|
||||
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">
|
||||
{loading ? (
|
||||
<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>
|
||||
{/* Content Section */}
|
||||
<div className="flex-grow-1 d-flex align-items-center justify-content-center position-relative">
|
||||
{isLoading && (
|
||||
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-50">
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && (!attendanceData || attendanceData.length === 0) ? (
|
||||
<div
|
||||
className="text-muted fw-semibold d-flex align-items-center justify-content-center"
|
||||
style={{ minHeight: "250px" }}
|
||||
>
|
||||
No data found
|
||||
</div>
|
||||
) : view === "chart" ? (
|
||||
<div className="w-100">
|
||||
<ReactApexChart
|
||||
options={chartOptions}
|
||||
series={chartSeries}
|
||||
type="bar"
|
||||
height={400}
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@ -159,32 +147,34 @@ const AttendanceOverview = () => {
|
||||
className="table-responsive w-100"
|
||||
style={{ maxHeight: "350px", overflowY: "auto" }}
|
||||
>
|
||||
<table className="table table-bordered table-sm text-start align-middle mb-0">
|
||||
<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", textTransform: "none" }}>Role</th>
|
||||
<th style={{ background: "#f8f9fa" }}>Role</th>
|
||||
{dates.map((date, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
style={{ background: "#f8f9fa", textTransform: "none" }}
|
||||
>
|
||||
<th key={idx} style={{ background: "#f8f9fa" }}>
|
||||
{date}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{roles.map((role) => (
|
||||
<tr key={role}>
|
||||
<td>{role}</td>
|
||||
<td className="fw-medium text-start table-cell">{role}</td>
|
||||
{tableData.map((row, idx) => {
|
||||
const value = row[role];
|
||||
const cellStyle = value > 0 ? { backgroundColor: "#d5d5d5" } : {};
|
||||
return (
|
||||
<td key={idx} style={cellStyle}>
|
||||
<td
|
||||
key={idx}
|
||||
style={
|
||||
value > 0 ? { backgroundColor: "#e9ecef" } : {}
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</td>
|
||||
);
|
||||
|
||||
@ -1,82 +1,76 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
useDashboardProjectsCardData,
|
||||
useDashboardTeamsCardData,
|
||||
useDashboardTasksCardData,
|
||||
useAttendanceOverviewData
|
||||
} from "../../hooks/useDashboard_Data";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
// import {
|
||||
// useDashboardProjectsCardData,
|
||||
// useDashboardTeamsCardData,
|
||||
// useDashboardTasksCardData,
|
||||
// useAttendanceOverviewData
|
||||
// } from "../../hooks/useDashboard_Data";
|
||||
|
||||
import Projects from "./Projects";
|
||||
import Teams from "./Teams";
|
||||
import TasksCard from "./Tasks";
|
||||
import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||
import ProjectProgressChart from "./ProjectProgressChart";
|
||||
// import Projects from "./Projects";
|
||||
// import Teams from "./Teams";
|
||||
// import TasksCard from "./Tasks";
|
||||
// import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||
// import ProjectProgressChart from "./ProjectProgressChart";
|
||||
// import ProjectOverview from "../Project/ProjectOverview";
|
||||
import AttendanceOverview from "./AttendanceOverview";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import ExpenseAnalysis from "./ExpenseAnalysis";
|
||||
import ExpenseStatus from "./ExpenseStatus";
|
||||
import ExpenseByProject from "./ExpenseByProject";
|
||||
import ProjectStatistics from "../Project/ProjectStatistics";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import {
|
||||
APPROVE_EXPENSE,
|
||||
EXPENSE_MANAGE,
|
||||
VIEW_ALL_EXPNESE,
|
||||
} from "../../utils/constants";
|
||||
import { useHasAnyPermission } from "../../hooks/useExpense";
|
||||
|
||||
const Dashboard = () => {
|
||||
// const { projectsCardData } = useDashboardProjectsCardData();
|
||||
// const { teamsCardData } = useDashboardTeamsCardData();
|
||||
// const { tasksCardData } = useDashboardTasksCardData();
|
||||
|
||||
// Get the selected project ID from Redux store
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isAllProjectsSelected = projectId === null;
|
||||
// Get the selected project ID from Redux store
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isAllProjectsSelected = projectId === null;
|
||||
|
||||
return (
|
||||
<div className="container-fluid mt-5">
|
||||
<div className="row gy-4">
|
||||
{isAllProjectsSelected && (
|
||||
<div className="col-sm-6 col-lg-4">
|
||||
<Projects />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||
<Teams />
|
||||
</div>
|
||||
|
||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||
<TasksCard />
|
||||
</div>
|
||||
|
||||
{isAllProjectsSelected && (
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<ProjectCompletionChart />
|
||||
</div>
|
||||
)}
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<ProjectProgressChart />
|
||||
</div>
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-12 col-md-6 mb-sm-0 mb-4">
|
||||
<AttendanceOverview />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-xxl-4 col-lg-4">
|
||||
<ProjectStatistics />
|
||||
</div>
|
||||
)}
|
||||
<div className="col-12 col-xl-4 col-md-6">
|
||||
<div className="card ">
|
||||
<ExpenseStatus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="card h-100">
|
||||
<ExpenseAnalysis />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<ExpenseByProject />
|
||||
</div>
|
||||
const isViewExpense = useHasAnyPermission(
|
||||
VIEW_ALL_EXPNESE,
|
||||
APPROVE_EXPENSE,
|
||||
EXPENSE_MANAGE
|
||||
);
|
||||
return (
|
||||
<div className="container-fluid py-5">
|
||||
{isViewExpense && (
|
||||
<div className="row mb-6 g-6">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="card h-100">
|
||||
<ExpenseAnalysis />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-xl-4 col-md-6">
|
||||
<div className="card h-100">
|
||||
<ExpenseStatus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
|
||||
<div className="row vh-100">
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-12 col-md-6 mb-sm-0 mb-4 ">
|
||||
<AttendanceOverview />
|
||||
</div>
|
||||
)}
|
||||
<div className="col-12 col-md-6">
|
||||
<ExpenseByProject />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@ -1,110 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||
<div
|
||||
className={`skeleton ${className}`}
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
borderRadius: "4px",
|
||||
background: "linear-gradient(90deg, #eee, #f5f5f5, #eee)",
|
||||
backgroundSize: "200% 100%",
|
||||
animation: "skeleton-loading 1.5s infinite",
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
|
||||
const skeletonStyle = `
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
`;
|
||||
|
||||
export const ProjectCardSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Inject animation CSS once */}
|
||||
<style>{skeletonStyle}</style>
|
||||
|
||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||
<h5 className="fw-bold mb-0 ms-2">
|
||||
<i className="rounded-circle bx bx-building-house text-primary"></i>{" "}
|
||||
Projects
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
{/* Skeleton body */}
|
||||
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
|
||||
<div className="text-center">
|
||||
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||
<SkeletonLine height={14} width="40px" className="mx-auto" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||
<SkeletonLine height={14} width="40px" className="mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamsSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
<style>{skeletonStyle}</style>
|
||||
|
||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||
<h5 className="fw-bold mb-0 ms-2">
|
||||
<i className="bx bx-group text-warning"></i> Teams
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
{/* Skeleton Body */}
|
||||
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
|
||||
<div className="text-center">
|
||||
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||
<SkeletonLine height={14} width="90px" className="mx-auto" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||
<SkeletonLine height={14} width="70px" className="mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const TasksSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
<style>{skeletonStyle}</style>
|
||||
|
||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||
<h5 className="fw-bold mb-0 ms-2">
|
||||
<i className="bx bx-task text-success"></i> Tasks
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
{/* Skeleton Body */}
|
||||
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
|
||||
<div className="text-center">
|
||||
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||
<SkeletonLine height={14} width="70px" className="mx-auto" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||
<SkeletonLine height={14} width="90px" className="mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,32 +1,18 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
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 { useProjectName } from "../../hooks/useProjects";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
import flatColors from "../Charts/flatColor";
|
||||
|
||||
const ExpenseAnalysis = () => {
|
||||
const projectId = useSelectedProject();
|
||||
const [projectName, setProjectName] = useState("All Project");
|
||||
const { projectNames } = useProjectName();
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: { startDate: "", endDate: "" },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && projectNames?.length) {
|
||||
const project = projectNames.find((p) => p.id === projectId);
|
||||
setProjectName(project?.name || "All Project");
|
||||
} else {
|
||||
setProjectName("All Project");
|
||||
}
|
||||
}, [projectNames, projectId]);
|
||||
|
||||
const { watch } = methods;
|
||||
const [startDate, endDate] = watch(["startDate", "endDate"]);
|
||||
|
||||
@ -51,7 +37,7 @@ const ExpenseAnalysis = () => {
|
||||
labels,
|
||||
legend: { show: false },
|
||||
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
|
||||
colors: flatColors,
|
||||
colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
@ -68,48 +54,63 @@ const ExpenseAnalysis = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 576, // mobile breakpoint
|
||||
options: {
|
||||
chart: { width: "100%" },
|
||||
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">
|
||||
<h5 className="mb-1 card-title">Expense Breakdown</h5>
|
||||
<p className="card-subtitle m-0">{projectName}</p>
|
||||
<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="text-end text-sm-end">
|
||||
<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" }}
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<SpinnerLoader />
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && report.length === 0 && (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center text-muted"
|
||||
style={{ height: "300px" }}
|
||||
>
|
||||
No data found
|
||||
</div>
|
||||
<div className="d-flex justify-content-center align-items-center">No data found</div>
|
||||
)}
|
||||
|
||||
{!isLoading && report.length > 0 && (
|
||||
@ -120,54 +121,42 @@ const ExpenseAnalysis = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
{/* Chart Column */}
|
||||
<div className="col-12 col-lg-6 d-flex justify-content-center mt-5 mb-3 mb-lg-0">
|
||||
<Chart
|
||||
options={donutOptions}
|
||||
series={series}
|
||||
type="donut"
|
||||
width="70%"
|
||||
height={320}
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex justify-content-center mb-3">
|
||||
<Chart
|
||||
options={donutOptions}
|
||||
series={series}
|
||||
type="donut"
|
||||
width="100%"
|
||||
height={320}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Data/Legend Column */}
|
||||
<div className="col-12 mt-6 col-lg-6">
|
||||
<div className="row g-4">
|
||||
{report.map((item, idx) => (
|
||||
<div
|
||||
className="col-6"
|
||||
key={idx}
|
||||
style={{
|
||||
borderLeft: `3px solid ${flatColors[idx % flatColors.length]}`,
|
||||
}}
|
||||
>
|
||||
<div className="d-flex flex-column text-start">
|
||||
<small
|
||||
className="fw-semibold text-wrap text-dark"
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "break-word",
|
||||
lineHeight: "1.2",
|
||||
}}
|
||||
>
|
||||
{item.projectName}
|
||||
</small>
|
||||
<span
|
||||
className="fw-semibold text-muted"
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{formatCurrency(item.totalApprovedAmount)}
|
||||
</span>
|
||||
</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>
|
||||
<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>
|
||||
</>
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
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 { useProjectName } from "../../hooks/useProjects";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { SpinnerLoader } from "../common/Loader";
|
||||
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
|
||||
|
||||
const ExpenseByProject = () => {
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const [projectName, setProjectName] = useState("All Project");
|
||||
const [range, setRange] = useState("12M");
|
||||
const { projectNames, loading } = useProjectName();
|
||||
const [selectedType, setSelectedType] = useState("");
|
||||
const [viewMode, setViewMode] = useState("Category");
|
||||
const [chartData, setChartData] = useState({ categories: [], data: [] });
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
const {expenseCategories , loading: typeLoading } = useExpenseCategory();
|
||||
const { ExpenseCategories, loading: typeLoading } = useExpenseCategory();
|
||||
|
||||
const { data: expenseApiData, isLoading } = useExpenseDataByProject(
|
||||
projectId,
|
||||
@ -27,15 +21,6 @@ const ExpenseByProject = () => {
|
||||
range === "All" ? null : parseInt(range)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject && projectNames?.length) {
|
||||
const project = projectNames.find((p) => p.id === selectedProject);
|
||||
setProjectName(project?.name || "All Project");
|
||||
} else {
|
||||
setProjectName("All Project");
|
||||
}
|
||||
}, [projectNames, selectedProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (expenseApiData) {
|
||||
const categories = expenseApiData.map((item) =>
|
||||
@ -50,7 +35,7 @@ const ExpenseByProject = () => {
|
||||
|
||||
const getSelectedTypeName = () => {
|
||||
if (!selectedType) return "All Types";
|
||||
const found = expenseCategories?.find((t) => t.id === selectedType);
|
||||
const found = ExpenseTypes.find((t) => t.id === selectedType);
|
||||
return found ? found.name : "All Types";
|
||||
};
|
||||
|
||||
@ -64,12 +49,6 @@ const ExpenseByProject = () => {
|
||||
categories: chartData.categories,
|
||||
labels: { style: { fontSize: "12px" }, rotate: -45 },
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: (val) => formatCurrency(val),
|
||||
style: { fontSize: "12px", colors: "#555" },
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`,
|
||||
@ -88,16 +67,22 @@ const ExpenseByProject = () => {
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const ExpenseCategoryType = [
|
||||
{id:1,category:"Category",label:"Category"},
|
||||
{id:2,category:"Project",label:"Project"}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="card shadow-sm h-100 rounded ">
|
||||
<div className="card shadow-sm rounded ">
|
||||
{/* Header */}
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between align-items-center mb-1 mt-1">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3 mt-3">
|
||||
<div className="text-start">
|
||||
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5>
|
||||
<p className="card-subtitle m-0">{projectName}</p>
|
||||
<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-5 ms-n8">
|
||||
<div className="btn-group mb-4 ms-n8">
|
||||
<button
|
||||
className="btn btn-sm dropdown-toggle fs-5"
|
||||
type="button"
|
||||
@ -106,29 +91,20 @@ const ExpenseByProject = () => {
|
||||
>
|
||||
{viewMode}
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end ">
|
||||
<li>
|
||||
<ul className="dropdown-menu dropdown-menu-end ">
|
||||
{ExpenseCategoryType.map((cat)=>(
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
setViewMode("Category");
|
||||
setViewMode(cat.category);
|
||||
setSelectedType("");
|
||||
}}
|
||||
>
|
||||
Category
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
setViewMode("Project");
|
||||
setSelectedType("");
|
||||
}}
|
||||
>
|
||||
Project
|
||||
{cat.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,8 +115,8 @@ const ExpenseByProject = () => {
|
||||
<button
|
||||
key={item}
|
||||
className={`border-0 px-2 py-1 text-sm rounded ${range === item
|
||||
? "text-white bg-primary"
|
||||
: "text-body bg-transparent"
|
||||
? "text-white bg-primary"
|
||||
: "text-body bg-transparent"
|
||||
}`}
|
||||
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
|
||||
onClick={() => setRange(item)}
|
||||
@ -157,7 +133,7 @@ const ExpenseByProject = () => {
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
{expenseCategories?.map((type) => (
|
||||
{ExpenseCategories?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
@ -170,15 +146,12 @@ const ExpenseByProject = () => {
|
||||
{/* 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>
|
||||
<p>Loading chart...</p>
|
||||
) : !expenseApiData || expenseApiData.length === 0 ? (
|
||||
<div className="text-center text-muted py-12">No data found</div>
|
||||
<div className="text-center text-muted py-5">No data found</div>
|
||||
) : (
|
||||
<Chart options={options} series={series} type="bar" height={235} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,6 @@ 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();
|
||||
@ -37,14 +36,14 @@ const ExpenseStatus = () => {
|
||||
<>
|
||||
<div className="card-header d-flex justify-content-between text-start ">
|
||||
<div className="m-0">
|
||||
<h5 className="card-title mb-1">Expense - By Status</h5>
|
||||
<p className="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">
|
||||
<div className="report-list text-start h-max">
|
||||
{[
|
||||
{
|
||||
title: "Pending Payment",
|
||||
@ -103,7 +102,7 @@ const ExpenseStatus = () => {
|
||||
</div>
|
||||
<div>
|
||||
<small
|
||||
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-xl"
|
||||
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
|
||||
} text-gray-500`}
|
||||
>
|
||||
{item?.count || 0}
|
||||
@ -122,7 +121,7 @@ const ExpenseStatus = () => {
|
||||
{isManageExpense && (
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center cursor-pointer"
|
||||
onClick={() => handleNavigate(EXPENSE_STATUS.payment_processed)}
|
||||
onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
|
||||
>
|
||||
<div className="d-block">
|
||||
<span
|
||||
@ -137,7 +136,7 @@ const ExpenseStatus = () => {
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span
|
||||
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-xl" : "text-3xl"
|
||||
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
|
||||
} text-md`}
|
||||
>
|
||||
{formatCurrency(data?.totalAmount || 0)}
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
||||
import { useProjects } from "../../hooks/useProjects";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
import { useProjectCompletionStatus } from "../../hooks/useDashboard_Data";
|
||||
|
||||
const ProjectCompletionChart = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const {
|
||||
data: projects,
|
||||
isLoading: loading,
|
||||
isError,
|
||||
error,
|
||||
} = useProjectCompletionStatus();
|
||||
const { projects, loading } = useProjects();
|
||||
|
||||
// Bar chart logic
|
||||
const projectNames = projects?.map((p) => p.name) || [];
|
||||
const projectProgress =
|
||||
projects?.map((p) => {
|
||||
const completed = p.completedWork || 0;
|
||||
const planned = p.plannedWork || 1;
|
||||
const percent = planned ? (completed / planned) * 100 : 0;
|
||||
const percent = (completed / planned) * 100;
|
||||
return Math.min(Math.round(percent), 100);
|
||||
}) || [];
|
||||
|
||||
@ -29,7 +23,7 @@ const ProjectCompletionChart = () => {
|
||||
<p className="card-subtitle">Projects Completion Status</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body ms-n7">
|
||||
<div className="card-body">
|
||||
<HorizontalBarChart
|
||||
categories={projectNames}
|
||||
seriesData={projectProgress}
|
||||
|
||||