diff --git a/index.html b/index.html index 0ac2d657..24a9ef36 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - Marco PMS + OnFieldWork.com @@ -46,6 +46,8 @@ + + @@ -94,6 +96,15 @@ + + + + + + + + + diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css index 68fd44df..b05f71c1 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -3,10 +3,456 @@ --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); } .table_header_border { border-bottom:2px solid var(--bs-table-border-color) ; -} \ No newline at end of file +} +.text-gary-80 { +color:var(--bs-gray-500) +} + +.text-royalblue{ +color: #1796e3; +} + +.text-md { +font-size: 2rem; +} + +.text-md-b { +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 */ +.text-base { font-size: 1rem; } /* 16px */ +.text-lg { font-size: 1.125rem; } /* 18px */ +.text-xl { font-size: 1.25rem; } /* 20px */ +.text-2xl { font-size: 1.5rem; } /* 24px */ +.text-3xl { font-size: 1.875rem; } /* 30px */ +.text-4xl { font-size: 2.25rem; } /* 36px */ +.text-5xl { font-size: 3rem; } /* 48px */ +.text-6xl { font-size: 3.75rem; } /* 60px */ +.text-7xl { font-size: 4.5rem; } /* 72px */ +.text-8xl { font-size: 6rem; } /* 96px */ +.text-9xl { font-size: 8rem; } /* 128px */ + + +/* */ + +.w-0 { width: 0px; } +.w-px { width: 1px; } +.w-1 { width: 0.25rem; } /* 4px */ +.w-2 { width: 0.5rem; } /* 8px */ +.w-3 { width: 0.75rem; } /* 12px */ +.w-4 { width: 1rem; } /* 16px */ +.w-5 { width: 1.25rem; } /* 20px */ +.w-6 { width: 1.5rem; } /* 24px */ +.w-8 { width: 2rem; } /* 32px */ +.w-10 { width: 2.5rem; } /* 40px */ +.w-12 { width: 3rem; } /* 48px */ +.w-16 { width: 4rem; } /* 64px */ +.w-20 { width: 5rem; } /* 80px */ +.w-24 { width: 6rem; } /* 96px */ +.w-32 { width: 8rem; } /* 128px */ +.w-40 { width: 10rem; } /* 160px */ +.w-48 { width: 12rem; } /* 192px */ +.w-56 { width: 14rem; } /* 224px */ +.w-64 { width: 16rem; } /* 256px */ +.w-auto { width: auto; } +.w-full { width: 100%; } +.w-screen{ width: 100vw; } +.w-min { width: min-content; } +.w-max { width: max-content; } + + + +.h-0 { height: 0px; } +.h-px { height: 1px; } +.h-1 { height: 0.25rem; } /* 4px */ +.h-2 { height: 0.5rem; } /* 8px */ +.h-3 { height: 0.75rem; } /* 12px */ +.h-4 { height: 1rem; } /* 16px */ +.h-5 { height: 1.25rem; } /* 20px */ +.h-6 { height: 1.5rem; } /* 24px */ +.h-8 { height: 2rem; } /* 32px */ +.h-10 { height: 2.5rem; } /* 40px */ +.h-12 { height: 3rem; } /* 48px */ +.h-16 { height: 4rem; } /* 64px */ +.h-20 { height: 5rem; } /* 80px */ +.h-24 { height: 6rem; } /* 96px */ +.h-32 { height: 8rem; } /* 128px */ +.h-40 { height: 10rem; } /* 160px */ +.h-48 { height: 12rem; } /* 192px */ +.h-56 { height: 14rem; } /* 224px */ +.h-64 { height: 16rem; } /* 256px */ +.h-auto { height: auto; } +.h-full { height: 100%; } +.h-screen{ height: 100vh; } +.h-min { height: min-content; } +.h-max { height: max-content; } + + + + + +/* ========================== + 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; } + .fs-sm-2 { font-size: calc(1.2rem + 1.2vw) !important; } + .fs-sm-3 { font-size: calc(1.1rem + 0.8vw) !important; } + .fs-sm-4 { font-size: calc(1rem + 0.5vw) !important; } + .fs-sm-5 { font-size: 1.05rem !important; } + .fs-sm-6 { font-size: 0.9rem !important; } + + .fs-sm-tiny { font-size: 72% !important; } + .fs-sm-big { font-size: 115% !important; } + .fs-sm-large { font-size: 155% !important; } + .fs-sm-xlarge { font-size: 175% !important; } + .fs-sm-xxlarge { font-size: calc(1.6rem + 3.5vw) !important; } +} + +/* 💻 Medium devices (≥768px) */ +@media (min-width: 768px) { + .fs-md-1 { font-size: calc(1.4125rem + 1.95vw) !important; } + .fs-md-2 { font-size: calc(1.3625rem + 1.35vw) !important; } + .fs-md-3 { font-size: calc(1.3rem + 0.6vw) !important; } + .fs-md-4 { font-size: calc(1.275rem + 0.3vw) !important; } + .fs-md-5 { font-size: 1.125rem !important; } + .fs-md-6 { font-size: 0.9375rem !important; } + + .fs-md-tiny { font-size: 70% !important; } + .fs-md-big { font-size: 112% !important; } + .fs-md-large { font-size: 150% !important; } + .fs-md-xlarge { font-size: 170% !important; } + .fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; } +} diff --git a/public/assets/css/default.css b/public/assets/css/default.css index db070129..8503dc87 100644 --- a/public/assets/css/default.css +++ b/public/assets/css/default.css @@ -30,11 +30,6 @@ width: 45px; } - - -.app-brand-logo-border { - border: 1px solid #d5d5d5; -} .app-brand-text { font-size: 1.75rem; letter-spacing: -0.5px; @@ -165,10 +160,9 @@ 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 { @@ -182,4 +176,3 @@ thead tr { max-width: 80px; } } - diff --git a/public/assets/vendor/css/core.css b/public/assets/vendor/css/core.css index 48163eb5..1e6ee587 100644 --- a/public/assets/vendor/css/core.css +++ b/public/assets/vendor/css/core.css @@ -76,6 +76,7 @@ --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; @@ -88,7 +89,7 @@ ); --bs-root-font-size: 16px; --bs-body-font-family: var(--bs-font-sans-serif); - --bs-body-font-size: 0.8375rem; + --bs-body-font-size: 0.85rem; --bs-body-font-weight: 400; --bs-body-line-height: 1.375; --bs-body-color: #646e78; @@ -9059,7 +9060,7 @@ img[data-app-light-img][data-app-dark-img] { } .table th { color: var(--bs-heading-color); - font-size: 0.8125rem; + font-size: 0.8025rem; letter-spacing: 0.2px; text-transform: uppercase; } @@ -20344,7 +20345,7 @@ li:not(:first-child) .dropdown-item, } .fs-6 { - font-size: 0.9375rem !important; + font-size: 0.8375rem !important; } .fs-tiny { @@ -32559,9 +32560,7 @@ 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,4 +32572,10 @@ 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) } \ No newline at end of file diff --git a/public/assets/vendor/libs/tagify/tagify.css b/public/assets/vendor/libs/tagify/tagify.css new file mode 100644 index 00000000..9c861220 --- /dev/null +++ b/public/assets/vendor/libs/tagify/tagify.css @@ -0,0 +1,879 @@ +@charset "UTF-8"; +:root { + --tagify-dd-color-primary: rgb(53,149,246); + --tagify-dd-bg-color: white; + --tagify-dd-item-pad: .3em .5em; + --tagify-dd-max-height: 300px; +} + +.tagify { + --tags-disabled-bg: #F1F1F1; + --tags-border-color: #DDD; + --tags-hover-border-color: #CCC; + --tags-focus-border-color: #3595f6; + --tag-border-radius: 3px; + --tag-bg: rgba(167, 172, 178, 0.5); + --tag-hover: #D3E2E2; + --tag-text-color: black; + --tag-text-color--edit: black; + --tag-pad: 0.3em 0.5em; + --tag-inset-shadow-size: 2em; + --tag-invalid-color: #ff3e1d; + --tag-invalid-bg: rgba(255, 62, 29, 0.5); + --tag--min-width: 1ch; + --tag--max-width: auto; + --tag-hide-transition: 0.3s; + --tag-remove-bg: rgba(255, 62, 29, 0.3); + --tag-remove-btn-color: #7a838b; + --tag-remove-btn-bg: none; + --tag-remove-btn-bg--hover: #ff2804; + --input-color: inherit; + --placeholder-color: rgba(0, 0, 0, 0.4); + --placeholder-color-focus: rgba(0, 0, 0, 0.25); + --loader-size: .8em; + --readonly-striped: 1; + display: inline-flex; + align-items: flex-start; + flex-wrap: wrap; + border: 1px solid var(--tags-border-color); + padding: 0; + line-height: 0; + cursor: text; + outline: none; + position: relative; + box-sizing: border-box; + transition: 0.1s; +} +@keyframes tags--bump { + 30% { + transform: scale(1.2); + } +} +@keyframes rotateLoader { + to { + transform: rotate(1turn); + } +} +.tagify:hover:not(.tagify--focus):not(.tagify--invalid) { + --tags-border-color: var(--tags-hover-border-color); +} +.tagify[disabled] { + background: var(--tags-disabled-bg); + filter: saturate(0); + opacity: 0.5; + pointer-events: none; +} +.tagify[readonly].tagify--select, .tagify[disabled].tagify--select { + pointer-events: none; +} +.tagify[readonly]:not(.tagify--mix):not(.tagify--select), .tagify[disabled]:not(.tagify--mix):not(.tagify--select) { + cursor: default; +} +.tagify[readonly]:not(.tagify--mix):not(.tagify--select) > .tagify__input, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) > .tagify__input { + visibility: hidden; + width: 0; + margin: 5px 0; +} +.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div { + padding: var(--tag-pad); +} +.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before { + animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused; +} +@keyframes readonlyStyles { + 0% { + background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px; + box-shadow: none; + filter: brightness(0.95); + } +} +.tagify[readonly] .tagify__tag__removeBtn, .tagify[disabled] .tagify__tag__removeBtn { + display: none; +} +.tagify--loading .tagify__input > br:last-child { + display: none; +} +.tagify--loading .tagify__input::before { + content: none; +} +.tagify--loading .tagify__input::after { + content: ""; + vertical-align: middle; + opacity: 1; + width: 0.7em; + height: 0.7em; + width: var(--loader-size); + height: var(--loader-size); + min-width: 0; + border: 3px solid; + border-color: #EEE #BBB #888 transparent; + border-radius: 50%; + animation: rotateLoader 0.4s infinite linear; + content: "" !important; + margin: -2px 0 -2px 0.5em; +} +.tagify--loading .tagify__input:empty::after { + margin-left: 0; +} +.tagify + input, +.tagify + textarea { + position: absolute !important; + left: -9999em !important; + transform: scale(0) !important; +} +.tagify__tag { + display: inline-flex; + align-items: center; + max-width: calc(var(--tag--max-width) - 10px); + margin-inline: 5px 0; + margin-block: 5px; + position: relative; + z-index: 1; + outline: none; + line-height: normal; + cursor: default; + transition: 0.13s ease-out; +} +.tagify__tag > div { + vertical-align: top; + box-sizing: border-box; + max-width: 100%; + padding: var(--tag-pad); + color: var(--tag-text-color); + line-height: inherit; + border-radius: var(--tag-border-radius); + white-space: nowrap; + transition: 0.13s ease-out; +} +.tagify__tag > div > * { + white-space: pre-wrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: top; + min-width: var(--tag--min-width); + max-width: var(--tag--max-width); + transition: 0.8s ease, 0.1s color; +} +.tagify__tag > div > *[contenteditable] { + outline: none; + user-select: text; + cursor: text; + margin: -2px; + padding: 2px; + max-width: 350px; +} +.tagify__tag > div::before { + content: ""; + position: absolute; + border-radius: inherit; + inset: var(--tag-bg-inset, 0); + z-index: -1; + pointer-events: none; + transition: 120ms ease; + animation: tags--bump 0.3s ease-out 1; + box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset; +} +.tagify__tag:hover:not([readonly]) div::before, .tagify__tag:focus div::before { + --tag-bg-inset: -2.5px; + --tag-bg: var(--tag-hover); +} +.tagify__tag--loading { + pointer-events: none; +} +.tagify__tag--loading .tagify__tag__removeBtn { + display: none; +} +.tagify__tag--loading::after { + --loader-size: .4em; + content: ""; + vertical-align: middle; + opacity: 1; + width: 0.7em; + height: 0.7em; + width: var(--loader-size); + height: var(--loader-size); + min-width: 0; + border: 3px solid; + border-color: #EEE #BBB #888 transparent; + border-radius: 50%; + animation: rotateLoader 0.4s infinite linear; + margin: 0 0.5em 0 -0.1em; +} +.tagify__tag--flash div::before { + animation: none; +} +.tagify__tag--hide { + width: 0 !important; + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + opacity: 0; + transform: scale(0); + transition: var(--tag-hide-transition); + pointer-events: none; +} +.tagify__tag--hide > div > * { + white-space: nowrap; +} +.tagify__tag.tagify--noAnim > div::before { + animation: none; +} +.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div > span { + opacity: 0.5; +} +.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before { + --tag-bg: var(--tag-invalid-bg); + transition: 0.2s; +} +.tagify__tag[readonly] .tagify__tag__removeBtn { + display: none; +} +.tagify__tag[readonly] > div::before { + animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused; +} +@keyframes readonlyStyles { + 0% { + background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px; + box-shadow: none; + filter: brightness(0.95); + } +} +.tagify__tag--editable > div { + color: var(--tag-text-color--edit); +} +.tagify__tag--editable > div::before { + box-shadow: 0 0 0 2px var(--tag-hover) inset !important; +} +.tagify__tag--editable > .tagify__tag__removeBtn { + pointer-events: none; +} +.tagify__tag--editable > .tagify__tag__removeBtn::after { + opacity: 0; + transform: translateX(100%) translateX(5px); +} +.tagify__tag--editable.tagify--invalid > div::before { + box-shadow: 0 0 0 2px var(--tag-invalid-color) inset !important; +} +.tagify__tag__removeBtn { + order: 5; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50px; + cursor: pointer; + font: 14px/1 Arial; + background: var(--tag-remove-btn-bg); + color: var(--tag-remove-btn-color); + width: 14px; + height: 14px; + margin-inline: auto 4.6666666667px; + overflow: hidden; + transition: 0.2s ease-out; +} +.tagify__tag__removeBtn::after { + content: "×"; + transition: 0.3s, color 0s; +} +.tagify__tag__removeBtn:hover { + color: white; + background: var(--tag-remove-btn-bg--hover); +} +.tagify__tag__removeBtn:hover + div > span { + opacity: 0.5; +} +.tagify__tag__removeBtn:hover + div::before { + box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg, rgba(255, 62, 29, 0.3)) inset !important; + transition: box-shadow 0.2s; +} +.tagify:not(.tagify--mix) .tagify__input br { + display: none; +} +.tagify:not(.tagify--mix) .tagify__input * { + display: inline; + white-space: nowrap; +} +.tagify__input { + flex-grow: 1; + display: inline-block; + min-width: 110px; + margin: 5px; + padding: var(--tag-pad); + line-height: normal; + position: relative; + white-space: pre-wrap; + color: var(--input-color); + box-sizing: inherit; + /* Seems firefox newer versions don't need this any more + @supports ( -moz-appearance:none ){ + &::before{ + line-height: inherit; + position:relative; + } + } + */ +} +@-moz-document url-prefix() {} +.tagify__input:empty::before { + position: static; +} +.tagify__input:focus { + outline: none; +} +.tagify__input:focus::before { + transition: 0.2s ease-out; + opacity: 0; + transform: translatex(6px); + /* ALL MS BROWSERS: hide placeholder (on focus) otherwise the caret is placed after it, which is weird */ + /* IE Edge 12+ CSS styles go here */ +} +@supports (-ms-ime-align: auto) { + .tagify__input:focus::before { + display: none; + } +} +.tagify__input:focus:empty::before { + transition: 0.2s ease-out; + opacity: 1; + transform: none; + color: rgba(0, 0, 0, 0.25); + color: var(--placeholder-color-focus); +} +@-moz-document url-prefix() { + .tagify__input:focus:empty::after { + display: none; + } +} +.tagify__input::before { + content: attr(data-placeholder); + height: 1em; + line-height: 1em; + margin: auto 0; + z-index: 1; + color: var(--placeholder-color); + white-space: nowrap; + pointer-events: none; + opacity: 0; + position: absolute; +} +.tagify__input::after { + content: attr(data-suggest); + display: inline-block; + vertical-align: middle; + position: absolute; + min-width: calc(100% - 1.5em); + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; /* allows spaces at the beginning */ + color: var(--tag-text-color); + opacity: 0.3; + pointer-events: none; + max-width: 100px; +} +.tagify__input .tagify__tag { + margin: 0 1px; +} +.tagify--mix { + display: block; +} +.tagify--mix .tagify__input { + padding: 5px; + margin: 0; + width: 100%; + height: 100%; + line-height: 1.5; + display: block; +} +.tagify--mix .tagify__input::before { + height: auto; + display: none; + line-height: inherit; +} +.tagify--mix .tagify__input::after { + content: none; +} +.tagify--select::after { + content: ">"; + opacity: 0.5; + position: absolute; + top: 50%; + right: 0; + bottom: 0; + font: 16px monospace; + line-height: 8px; + height: 8px; + pointer-events: none; + transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg); + transition: 0.2s ease-in-out; +} +.tagify--select[aria-expanded=true]::after { + transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2); +} +.tagify--select .tagify__tag { + position: absolute; + top: 0; + right: 1.8em; + bottom: 0; +} +.tagify--select .tagify__tag div { + display: none; +} +.tagify--select .tagify__input { + width: 100%; +} +.tagify--empty .tagify__input::before { + transition: 0.2s ease-out; + opacity: 1; + transform: none; + display: inline-block; + width: auto; +} +.tagify--mix .tagify--empty .tagify__input::before { + display: inline-block; +} +.tagify--focus { + --tags-border-color: var(--tags-focus-border-color); + transition: 0s; +} +.tagify--invalid { + --tags-border-color: #ff3e1d; +} +.tagify__dropdown { + position: absolute; + z-index: 9999; + transform: translateY(-1px); + border-top: 1px solid var(--tagify-dd-color-primary); + overflow: hidden; +} +.tagify__dropdown[dir=rtl] { + transform: translate(-100%, -1px); +} +.tagify__dropdown[placement=top] { + margin-top: 0; + transform: translateY(-100%); +} +.tagify__dropdown[placement=top] .tagify__dropdown__wrapper { + border-top-width: 1.1px; + border-bottom-width: 0; +} +.tagify__dropdown[position=text] { + box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), 0.1); + font-size: 0.9em; +} +.tagify__dropdown[position=text] .tagify__dropdown__wrapper { + border-width: 1px; +} +.tagify__dropdown__wrapper { + max-height: var(--tagify-dd-max-height); + overflow: hidden; + overflow-x: hidden; + background: var(--tagify-dd-bg-color); + border: 1px solid; + border-color: var(--tagify-dd-color-primary); + border-bottom-width: 1.5px; + border-top-width: 0; + box-shadow: 0 2px 4px -2px rgba(0, 0, 0, 0.2); + transition: 0.3s cubic-bezier(0.5, 0, 0.3, 1), transform 0.15s; + animation: dd-wrapper-show 0s 0.3s forwards; +} +@keyframes dd-wrapper-show { + to { + overflow-y: auto; + } +} +.tagify__dropdown__header:empty { + display: none; +} +.tagify__dropdown__footer { + display: inline-block; + margin-top: 0.5em; + padding: var(--tagify-dd-item-pad); + font-size: 0.7em; + font-style: italic; + opacity: 0.5; +} +.tagify__dropdown__footer:empty { + display: none; +} +.tagify__dropdown--initial .tagify__dropdown__wrapper { + max-height: 20px; + transform: translateY(-1em); +} +.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper { + transform: translateY(2em); +} +.tagify__dropdown__item { + box-sizing: border-box; + padding: var(--tagify-dd-item-pad); + margin: 1px; + white-space: pre-wrap; + cursor: pointer; + border-radius: 2px; + position: relative; + outline: none; + max-height: 60px; + max-width: 100%; + /* custom hidden transition effect is needed for horizontal-layout suggestions */ +} +.tagify__dropdown__item--active { + background: var(--tagify-dd-color-primary); + color: white; +} +.tagify__dropdown__item:active { + filter: brightness(105%); +} +.tagify__dropdown__item--hidden { + padding-top: 0; + padding-bottom: 0; + margin: 0 1px; + pointer-events: none; + overflow: hidden; + max-height: 0; + transition: var(--tagify-dd-item--hidden-duration, 0.3s) !important; +} +.tagify__dropdown__item--hidden > * { + transform: translateY(-100%); + opacity: 0; + transition: inherit; +} + +/* Suggestions items */ +.tagify__dropdown.users-list { + font-size: 1rem; +} +.tagify__dropdown.users-list .addAll { + display: block !important; +} +.tagify__dropdown.users-list .tagify__dropdown__item { + padding: 0.5em 0.7em; + display: grid; + grid-template-columns: auto 1fr; + gap: 0 1em; + grid-template-areas: "avatar name" "avatar email"; +} +.tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap { + grid-area: avatar; + width: 36px; + height: 36px; + border-radius: 50%; + overflow: hidden; + transition: 0.1s ease-out; +} +.tagify__dropdown.users-list img { + width: 100%; + vertical-align: top; +} +.tagify__dropdown.users-list strong { + grid-area: name; + width: 100%; + align-self: center; + font-weight: 500; +} +.tagify__dropdown.users-list span { + grid-area: email; + width: 100%; + font-size: 0.9em; + opacity: 0.6; +} + +/* Tags items */ +.tagify__tag { + white-space: nowrap; +} +.tagify__tag .tagify__tag__avatar-wrap { + width: 22px; + height: 22px; + white-space: normal; + border-radius: 50%; + margin-right: 5px; + transition: 0.12s ease-out; + vertical-align: middle; +} +.tagify__tag img { + width: 100%; + vertical-align: top; +} + +[dir=rtl] .tagify__tag .tagify__tag__avatar-wrap { + margin-left: 5px; + margin-right: auto; +} + +.light-style .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap { + background: #f5f5f9; +} +.light-style .tagify__tag .tagify__tag__avatar-wrap { + background: #f5f5f9; +} +.light-style .tagify__dropdown.users-list .addAll { + border-bottom: 1px solid #e4e6e8; +} + +.dark-style .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap { + background: #232333; +} +.dark-style .tagify__tag .tagify__tag__avatar-wrap { + background: #232333; +} +.dark-style .tagify__dropdown.users-list .addAll { + border-bottom: 1px solid #4e4f6c; +} + +.tags-inline .tagify__dropdown__wrapper { + padding: 0 0.4375rem 0.4375rem 0.4375rem; +} +.tags-inline .tagify__dropdown__item { + display: inline-block; + border-radius: 3px; + padding: 0.3em 0.5em; + margin: 0.4375rem 0.4375rem 0 0; + font-size: 0.85em; + transition: 0s; +} + +[dir=rtl] .tags-inline .tagify__dropdown__item { + margin: 0.4375rem 0 0 0.4375rem; +} + +.light-style .tags-inline .tagify__dropdown__item { + border: 1px solid #e4e6e8; + color: #646e78; +} + +.dark-style .tags-inline .tagify__dropdown__item { + border: 1px solid #4e4f6c; + color: #b2b2c4; +} + +.tagify-email-list { + display: inline-block; + min-width: 0; + border: none; + /* Do not show the "remove tag" (x) button when only a single tag remains */ +} +.tagify-email-list.tagify { + padding: 0 !important; + padding-bottom: calc(0.4375rem - var(--bs-border-width)) !important; +} +.tagify-email-list.tagify { + padding: 0 !important; + padding-bottom: calc(0.4375rem - var(--bs-border-width)) !important; +} +.tagify-email-list.tagify.tagify--focus { + padding-left: 0 !important; +} +.tagify-email-list .tagify__tag { + margin: 0; + margin-inline-start: 0 !important; + margin-inline-end: 0.625rem !important; + margin-bottom: 0.4375rem !important; +} +.tagify-email-list .tagify__tag > div { + padding: 0.21875rem 0.4375rem !important; + padding-inline: 0.875rem !important; +} +.tagify-email-list .tagify__tag:only-of-type > div { + padding-inline: 0.4375rem !important; +} +.tagify-email-list .tagify__tag:only-of-type .tagify__tag__removeBtn { + display: none; +} +.tagify-email-list .tagify__tag__removeBtn { + opacity: 0; + transform: translateX(-6px) scale(0.5); + margin-left: -3ch; + transition: 0.12s; + position: absolute; + inset-inline-end: 0; +} +.tagify-email-list .tagify__tag:hover .tagify__tag__removeBtn { + transform: none; + opacity: 1; + margin-left: -1ch; +} +.tagify-email-list .tagify__input { + display: none; +} + +.tagify__tag > div { + border-radius: 50rem; +} + +[dir=rtl] .tagify-email-list .tagify__tag { + margin: 0 0.4375rem 0.4375rem 0; +} +[dir=rtl] .tagify-email-list .tagify__tag:hover .tagify__tag__removeBtn { + margin-left: auto; + margin-right: -1ch; +} +[dir=rtl] .tagify-email-list .tagify__tag__removeBtn { + transform: translateX(6px) scale(0.5); + margin-left: auto; + margin-right: -3ch; +} + +.light-style .tagify-email-list .tagify__tag--editable:not(.tagify--invalid) > div::before { + box-shadow: 0 0 0 2px #e4e6e8 inset !important; +} + +.dark-style .tagify-email-list .tagify__tag--editable:not(.tagify--invalid) > div::before { + box-shadow: 0 0 0 2px #4e4f6c inset !important; +} + +.tagify.form-control { + transition: none; + display: flex; + align-items: flex-end; + /* padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.4231rem !important; */ + padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.2rem !important; +} +.fv-plugins-bootstrap5-row-invalid .tagify.form-control { + padding: 0 calc(0.4375rem - var(--bs-border-width)) calc(0.4375rem - 2px) !important; +} +.tagify.tagify--focus, .tagify.form-control:focus { + padding: 0 calc(0.4375rem - var(--bs-border-width)) 0.3606rem !important; + border-width: 2px; +} +.tagify__tag, .tagify__input { + margin: 0.1875rem 0.625rem 0 0 !important; + line-height: 1; +} +.tagify__input { + line-height: 1.5rem; +} +.tagify__input:empty::before { + top: 4px; +} +.tagify__tag > div { + line-height: 1.5rem; + padding: 0 0 0 0.4375rem; +} +.tagify__tag__removeBtn { + margin-right: 0.1375rem; + margin-left: 0.21875rem; + font-family: "boxicons"; + font-size: 1rem; + opacity: 0.7; +} +.tagify__tag__removeBtn:hover { + background: none; + color: #ff2804 !important; +} +.tagify__tag__removeBtn::after { + content: "\ef06"; +} +.tagify__tag:hover:not([readonly]) div::before, .tagify__tag:focus div::before { + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; +} +.tagify__dropdown { + transform: translateY(0); +} +.tagify[readonly]:not(.tagify--mix) .tagify__tag > div { + padding: 0 0.4375rem 0 0.4375rem !important; +} +.tagify__input { + padding: 0; +} +.tagify__tag-text { + font-size: 0.8125rem; + font-weight: 500; +} + +.tagify.form-control { + padding-top: 0.1412rem !important; +} +.tagify.tagify--focus, .tagify.form-control:focus { + padding-top: calc(0.1412rem - 1px) !important; +} + +.tagify__tag__removeBtn { + margin-inline-end: 0.3rem; +} + +[dir=rtl] .tagify__tag, [dir=rtl] .tagify__input { + margin: 0.4375rem 0 0 0.4375rem; +} +[dir=rtl] .tagify + input, +[dir=rtl] .tagify + textarea { + left: 0; + right: -9999em !important; +} +[dir=rtl] .tagify__tag > div { + padding: 0 0.6875rem 0 0; +} +[dir=rtl] .tagify__tag__removeBtn { + margin-left: 0.4375rem; + margin-right: 0.21875rem; +} + +.light-style .tagify__tag > div::before { + box-shadow: 0 0 0 1.3em rgba(34, 48, 62, 0.08) inset; +} +.light-style .tagify__tag .tagify__tag-text { + color: #384551; +} +.light-style .tagify__tag:hover:not([readonly]) div::before, .light-style .tagify__tag:focus div::before { + box-shadow: 0 0 0 1.3em rgba(34, 48, 62, 0.12) inset; +} +.light-style .tagify__tag__removeBtn { + color: #7a838b; +} +.light-style .tagify__tag__removeBtn:hover + div::before { + background: rgba(255, 62, 29, 0.3); +} +.light-style .tagify:hover:not([readonly]) { + border-color: #ced1d5; +} +.light-style .tagify__input::before { + color: #a7acb2 !important; +} +.light-style .tagify__dropdown { + box-shadow: 0 0.25rem 0.75rem 0 rgba(34, 48, 62, 0.14); + border-top-color: #e4e6e8; +} +.light-style .tagify__dropdown__wrapper { + background: #fff; + border-color: #e4e6e8; +} + +.dark-style .tagify__tag > div::before { + box-shadow: 0 0 0 1.3em rgba(230, 230, 241, 0.08) inset; +} +.dark-style .tagify__tag > div .tagify__tag-text { + color: #d5d5e2; +} +.dark-style .tagify__tag:hover:not([readonly]) div::before, .dark-style .tagify__tag:focus div::before { + box-shadow: 0 0 0 1.3em rgba(230, 230, 241, 0.12) inset; +} +.dark-style .tagify__tag__removeBtn { + color: #a1a1b5; +} +.dark-style .tagify__tag__removeBtn:hover + div::before { + background: rgba(255, 62, 29, 0.3); +} +.dark-style .tagify:hover:not([readonly]) { + border-color: #5f607b; +} +.dark-style .tagify__input::before { + color: #7e7f96 !important; +} +.dark-style .tagify[readonly]:not(.tagify--mix) .tagify__tag > div::before { + background: linear-gradient(45deg, #5f607b 25%, transparent 25%, transparent 50%, #5f607b 50%, #5f607b 75%, transparent 75%, transparent) 0/5px 5px; +} +.dark-style .tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before { + animation: none; + box-shadow: none; +} +.dark-style .tagify__dropdown { + box-shadow: 0 0.25rem 0.75rem 0 rgba(20, 20, 29, 0.24); + border-top-color: #4e4f6c; +} +.dark-style .tagify__dropdown__wrapper { + box-shadow: 0 0.25rem 0.75rem 0 rgba(20, 20, 29, 0.24); + background: #2b2c40; + border-color: #4e4f6c; +} diff --git a/public/assets/vendor/libs/tagify/tagify.js b/public/assets/vendor/libs/tagify/tagify.js new file mode 100644 index 00000000..a952d07d --- /dev/null +++ b/public/assets/vendor/libs/tagify/tagify.js @@ -0,0 +1,120 @@ +/* + * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). + * This devtool is neither made for production nor for readable output files. + * It uses "eval()" calls to create a separate source file in the browser devtools. + * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) + * or disable the default devtool with "devtool: false". + * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). + */ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else { + var a = factory(); + for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; + } +})(self, function() { +return /******/ (function() { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "./node_modules/@yaireo/tagify/dist/tagify.min.js": +/*!********************************************************!*\ + !*** ./node_modules/@yaireo/tagify/dist/tagify.min.js ***! + \********************************************************/ +/***/ (function(module) { + +eval("/**\n * Tagify (v 4.18.3) - tags input component\n * By undefined\n * https://github.com/yairEO/tagify\n * Permission is hereby granted, free of charge, to any person obtaining a copy\r\n * of this software and associated documentation files (the \"Software\"), to deal\r\n * in the Software without restriction, including without limitation the rights\r\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n * copies of the Software, and to permit persons to whom the Software is\r\n * furnished to do so, subject to the following conditions:\r\n * \r\n * The above copyright notice and this permission notice shall be included in\r\n * all copies or substantial portions of the Software.\r\n * \r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r\n * THE SOFTWARE.\r\n * \r\n * THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD.\n */\n\n!function(t,e){ true?module.exports=e():0}(this,(function(){\"use strict\";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s(t=\"\"+t,e=\"\"+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase()),a=(t,e)=>t&&Array.isArray(t)&&t.map((t=>n(t,e)));function n(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function o(t){var e=document.createElement(\"div\");return t.replace(/\\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function r(t){return(new DOMParser).parseFromString(t.trim(),\"text/html\").body.firstElementChild}function l(t,e){for(e=e||\"previous\";t=t[e+\"Sibling\"];)if(3==t.nodeType)return t}function d(t){return\"string\"==typeof t?t.replace(/&/g,\"&\").replace(//g,\">\").replace(/\"/g,\""\").replace(/`|'/g,\"'\"):t}function h(t){var e=Object.prototype.toString.call(t).split(\" \")[1].slice(0,-1);return t===Object(t)&&\"Array\"!=e&&\"Function\"!=e&&\"RegExp\"!=e&&\"HTMLUnknownElement\"!=e}function g(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(h(e[i])){h(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function p(){const t=[],e={};for(let i of arguments)for(let s of i)h(s)?e[s.value]||(t.push(s),e[s.value]=1):t.includes(s)||t.push(s);return t}function c(t){return String.prototype.normalize?\"string\"==typeof t?t.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g,\"\"):void 0:t}var u=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function m(){return([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}function v(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}function f(t,e){var i=window.getSelection();return e=e||i.getRangeAt(0),\"string\"==typeof t&&(t=document.createTextNode(t)),e&&(e.deleteContents(),e.insertNode(t)),t}function T(t,e,i){return t?(e&&(t.__tagifyTagData=i?e:g({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn(\"tag element doesn't exist\",t,e),e)}function w(t){if(t&&t.parentNode){var e=t,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}}function b(t,e){t.forEach((t=>{if(T(t.previousSibling)||!t.previousSibling){var i=document.createTextNode(\"​\");t.before(i),e&&w(i)}}))}var y={delimiters:\",\",pattern:null,tagTextProp:\"value\",maxTags:1/0,callbacks:{},addTagOnBlur:!0,addTagOn:[\"blur\",\"tab\",\"enter\"],onChangeAfterBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,createInvalidTags:!0,mixTagsAllowedAfter:/,|\\.|\\:|\\s/,mixTagsInterpolator:[\"[[\",\"]]\"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:\" \"},autoComplete:{enabled:!0,rightKey:!1,tabKey:!1},classNames:{namespace:\"tagify\",mixMode:\"tagify--mix\",selectMode:\"tagify--select\",input:\"tagify__input\",focus:\"tagify--focus\",tagNoAnimation:\"tagify--noAnim\",tagInvalid:\"tagify--invalid\",tagNotAllowed:\"tagify--notAllowed\",scopeLoading:\"tagify--loading\",hasMaxTags:\"tagify--hasMaxTags\",hasNoTags:\"tagify--noTags\",empty:\"tagify--empty\",inputInvalid:\"tagify__input--invalid\",dropdown:\"tagify__dropdown\",dropdownWrapper:\"tagify__dropdown__wrapper\",dropdownHeader:\"tagify__dropdown__header\",dropdownFooter:\"tagify__dropdown__footer\",dropdownItem:\"tagify__dropdown__item\",dropdownItemActive:\"tagify__dropdown__item--active\",dropdownItemHidden:\"tagify__dropdown__item--hidden\",dropdownInital:\"tagify__dropdown--initial\",tag:\"tagify__tag\",tagText:\"tagify__tag-text\",tagX:\"tagify__tag__removeBtn\",tagLoading:\"tagify__tag--loading\",tagEditing:\"tagify__tag--editable\",tagFlash:\"tagify__tag--flash\",tagHide:\"tagify__tag--hide\"},dropdown:{classname:\"\",enabled:2,maxItems:10,searchKeys:[\"value\",\"searchBy\"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,includeSelectedTags:!1,escapeHTML:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:\"all\",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve(),beforeKeyDown:()=>Promise.resolve()}};function x(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]=\"function\"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.dropdown.refs()}var O={refs(){this.DOM.dropdown=this.parseTemplate(\"dropdown\",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-wrapper']\")},getHeaderRef(){return this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-header']\")},getFooterRef(){return this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-footer']\")},getAllSuggestionsRefs(){return[...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]},show(t){var e,i,a,n=this.settings,o=\"mix\"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,l=\"manual\"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,!(r&&!o&&!n.templates.dropdownItemNoMatch||!1===n.dropdown.enable||this.state.isLoading||this.settings.readonly)){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger(\"dropdown:noMatch\",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=\"\"+(h(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(n.classNames.dropdownItemSelector)),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),l||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger(\"dropdown:show\",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a=\"manual\"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener(\"resize\",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute(\"aria-expanded\",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger(\"dropdown:hide\",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?\"hide\":\"show\"]()},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText=\"position:fixed; top:-9999px; opacity:0\",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return\"number\"==typeof a.dropdown.enabled&&a.dropdown.enabled>=0?(this.DOM.scope.setAttribute(\"aria-expanded\",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position(s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this):this},fill(t){t=\"string\"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems);var e,i=this.settings.templates.dropdownContent.call(this,t);this.DOM.dropdown.content.innerHTML=(e=i)?e.replace(/\\>[\\r\\n ]+\\<\").split(/>\\s+<\").trim():\"\"},fillHeaderFooter(){var t=this.dropdown.filterListItems(this.state.dropdown.query),e=this.parseTemplate(\"dropdownHeader\",[t]),i=this.parseTemplate(\"dropdownFooter\",[t]),s=this.dropdown.getHeaderRef(),a=this.dropdown.getFooterRef();e&&s?.parentNode.replaceChild(e,s),i&&a?.parentNode.replaceChild(i,a)},refilter(t){t=t||this.state.dropdown.query||\"\",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger(\"dropdown:updated\",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if(\"manual\"!=e.position){var i,s,a,n,o,r,l,d,h,g=this.DOM.dropdown,p=e.RTL,c=e.appendTarget===document.body,u=c?window.pageYOffset:e.appendTarget.scrollTop,m=document.fullscreenElement||document.webkitFullscreenElement||document.documentElement,v=m.clientHeight,f=Math.max(m.clientWidth||0,window.innerWidth||0)>480?e.position:\"all\",T=this.DOM[\"input\"==f?\"input\":\"scope\"];if(t=t||g.clientHeight,this.state.dropdown.visible){if(\"text\"==f?(a=(i=function(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}}()).bottom,s=i.top,n=i.left,o=\"auto\"):(r=function(t){for(var e=0,i=0;t&&t!=m;)e+=t.offsetTop||0,i+=t.offsetLeft||0,t=t.parentNode;return{top:e,left:i}}(e.appendTarget),s=(i=T.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+\"px\"),!c){let t=function(){for(var t=0,i=e.appendTarget.parentNode;i;)t+=i.scrollTop||0,i=i.parentNode;return t}();s+=t,a+=t}s=Math.floor(s),a=Math.ceil(a),d=((l=e.placeAbove??v-i.bottom0&&void 0!==arguments[0])||arguments[0];var e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this,null),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?\"addEventListener\":\"removeEventListener\";\"manual\"!=this.settings.dropdown.position&&(document[s](\"scroll\",i.position,!0),window[s](\"resize\",i.position),window[s](\"keydown\",i.onKeyDown)),this.DOM.dropdown[s](\"mouseover\",i.onMouseOver),this.DOM.dropdown[s](\"mouseleave\",i.onMouseLeave),this.DOM.dropdown[s](\"mousedown\",i.onClick),this.DOM.dropdown.content[s](\"scroll\",i.onScroll)},callbacks:{onKeyDown(t){if(this.state.hasFocus&&!this.state.composing){var e=this.settings,i=this.DOM.dropdown.querySelector(e.classNames.dropdownItemActiveSelector),s=this.dropdown.getSuggestionDataByNode(i),a=\"mix\"==e.mode;e.hooks.beforeKeyDown(t,{tagify:this}).then((n=>{switch(t.key){case\"ArrowDown\":case\"ArrowUp\":case\"Down\":case\"Up\":t.preventDefault();var o=this.dropdown.getAllSuggestionsRefs(),r=\"ArrowUp\"==t.key||\"Up\"==t.key;i&&(i=this.dropdown.getNextOrPrevOption(i,!r)),i&&i.matches(e.classNames.dropdownItemSelector)||(i=o[r?o.length-1:0]),this.dropdown.highlightOption(i,!0);break;case\"Escape\":case\"Esc\":this.dropdown.hide();break;case\"ArrowRight\":if(this.state.actions.ArrowLeft)return;case\"Tab\":{let n=!e.autoComplete.rightKey||!e.autoComplete.tabKey;if(!a&&i&&n&&!this.state.editing){t.preventDefault();var l=this.dropdown.getMappedValue(s);return this.input.autocomplete.set.call(this,l),!1}return!0}case\"Enter\":t.preventDefault(),e.hooks.suggestionClick(t,{tagify:this,tagData:s,suggestionElm:i}).then((()=>{if(i)return this.dropdown.selectOption(i),i=this.dropdown.getNextOrPrevOption(i,!r),void this.dropdown.highlightOption(i);this.dropdown.hide(),a||this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case\"Backspace\":{if(a||this.state.editing.scope)return;const t=this.input.raw.call(this);\"\"!=t&&8203!=t.charCodeAt(0)||(!0===e.backspace?this.removeTags():\"edit\"==e.backspace&&setTimeout(this.editTag.bind(this),0))}}}))}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e,t):this.dropdown.hide()})).catch((t=>console.warn(t)))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger(\"dropdown:scroll\",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t&&t.getAttribute(\"value\");return this.suggestedListItems.find((t=>t.value==e))||null},getNextOrPrevOption(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.dropdown.getAllSuggestionsRefs(),s=i.findIndex((e=>e===t));return e?i[s+1]:i[s-1]},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute(\"aria-selected\")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.dropdown.getSuggestionDataByNode(t),this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute(\"aria-selected\",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t,e){var i=this.settings,s=i.dropdown,a=s.clearOnSelect,n=s.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(n&&this.dropdown.hide());e=e||{};var o=t.getAttribute(\"value\"),r=\"noMatch\"==o,l=this.suggestedListItems.find((t=>(t.value??t)==o));if(this.trigger(\"dropdown:select\",{data:l,elm:t,event:e}),o&&(l||r)){if(this.state.editing){let t=this.normalizeTags([l])[0];l=i.transformTag.call(this,t)||t,this.onEditTagDone(null,g({__isValid:!0},l))}else this[\"mix\"==i.mode?\"addMixTags\":\"addTags\"]([l||this.input.raw.call(this)],a);this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),n&&setTimeout(this.dropdown.hide.bind(this)),t.addEventListener(\"transitionend\",(()=>{this.dropdown.fillHeaderFooter(),setTimeout((()=>t.remove()),100)}),{once:!0}),t.classList.add(this.settings.classNames.dropdownItemHidden))}else n&&setTimeout(this.dropdown.hide.bind(this))},selectAll(t){this.suggestedListItems.length=0,this.dropdown.hide(),this.dropdown.filterListItems(\"\");var e=this.dropdown.filterListItems(\"\");return t||(e=this.state.dropdown.suggestions),this.addTags(e,!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,l=r.dropdown,d=(e=e||{},[]),g=[],p=r.whitelist,u=l.maxItems>=0?l.maxItems:1/0,m=l.searchKeys,v=0;if(!(t=\"select\"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?\"\":t)||!m.length)return d=l.includeSelectedTags?p:p.filter((t=>!this.isTagDuplicate(h(t)?t.value:t))),this.state.dropdown.suggestions=d,d.slice(0,u);function f(t,e){return e.toLowerCase().split(\" \").every((e=>t.includes(e.toLowerCase())))}for(o=l.caseSensitive?\"\"+t:(\"\"+t).toLowerCase();vm.includes(t)))?[\"value\"]:m;l.fuzzySearch&&!e.exact?(a=u.reduce(((t,e)=>t+\" \"+(i[e]||\"\")),\"\").toLowerCase().trim(),l.accentedSearch&&(a=c(a),o=c(o)),t=0==a.indexOf(o),r=a===o,s=f(a,o)):(t=!0,s=u.some((t=>{var s=\"\"+(i[t]||\"\");return l.accentedSearch&&(s=c(s),o=c(o)),l.caseSensitive||(s=s.toLowerCase()),r=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!l.includeSelectedTags&&this.isTagDuplicate(h(i)?i.value:i),s&&!n&&(r&&t?g.push(i):\"startsWith\"==l.sortby&&t?d.unshift(i):d.push(i))}return this.state.dropdown.suggestions=g.concat(d),\"function\"==typeof l.sortby?l.sortby(g.concat(d),o):g.concat(d).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?\"function\"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return g([],t).map(((t,i)=>{\"string\"!=typeof t&&\"number\"!=typeof t||(t={value:t});var s=this.dropdown.getMappedValue(t);return s=\"string\"==typeof s&&this.settings.dropdown.escapeHTML?d(s):s,this.settings.templates.dropdownItem.apply(this,[e(e({},t),{},{mappedValue:s}),this])})).join(\"\")}};const D=\"@yaireo/tagify/\";var M,I={empty:\"empty\",exceed:\"number of tags exceeded\",pattern:\"pattern mismatch\",duplicate:\"already exists\",notAllowed:\"not allowed\"},N={wrapper:(t,e)=>`\\n \\n ​\\n `,tag(t,e){let i=e.settings;return`\\n \\n
\\n ${t[i.tagTextProp]||t.value}\\n
\\n
`},dropdown(t){var e=t.dropdown;return`
\\n
\\n
`},dropdownContent(t){var e=this.settings.templates,i=this.state.dropdown.suggestions;return`\\n ${e.dropdownHeader.call(this,i)}\\n ${t}\\n ${e.dropdownFooter.call(this,i)}\\n `},dropdownItem(t){return`
${t.mappedValue||t.value}
`},dropdownHeader(t){return`
`},dropdownFooter(t){var e=t.length-this.settings.dropdown.maxItems;return e>0?`
\\n ${e} more items. Refine your search.\\n
`:\"\"},dropdownItemNoMatch:null};var _={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e,i=this.events.callbacks,s=t?\"addEventListener\":\"removeEventListener\";if(!this.state.mainEvents||!t){for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on(\"tagify.removeAllTags\",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:[\"input\",i.onFocusBlur.bind(this)],keydown:[\"input\",i.onKeydown.bind(this)],click:[\"scope\",i.onClickScope.bind(this)],dblclick:[\"scope\",i.onDoubleClickScope.bind(this)],paste:[\"input\",i.onPaste.bind(this)],drop:[\"input\",i.onDrop.bind(this)],compositionstart:[\"input\",i.onCompositionStart.bind(this)],compositionend:[\"input\",i.onCompositionEnd.bind(this)]})this.DOM[e[a][0]][s](a,e[a][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(i.observeOriginalInputValue.bind(this),500);var n=this.listeners.main.inputMutationObserver||new MutationObserver(i.onInputDOMChange.bind(this));n.disconnect(),\"mix\"==this.settings.mode&&n.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?\"removeEventListener\":\"addEventListener\";if(this.listeners&&(t||!this.listeners.global))for(e of(this.listeners.global=this.listeners.global||[{type:this.isIE?\"keydown\":\"input\",target:this.DOM.input,cb:i[this.isIE?\"onInputIE\":\"onInput\"].bind(this)},{type:\"keydown\",target:window,cb:i.onWindowKeyDown.bind(this)},{type:\"blur\",target:this.DOM.input,cb:i.onFocusBlur.bind(this)},{type:\"click\",target:document,cb:i.onClickAnywhere.bind(this)}],this.listeners.global))e.target[s](e.type,e.cb)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=this.settings,i=t.target?this.trim(t.target.textContent):\"\",s=this.value?.[0]?.[e.tagTextProp],a=t.type,n=e.dropdown.enabled>=0,o={relatedTarget:t.relatedTarget},r=this.state.actions.selectOption&&(n||!e.dropdown.closeOnSelect),l=this.state.actions.addNew&&n,d=t.relatedTarget&&v.call(this,t.relatedTarget)&&this.DOM.scope.contains(t.relatedTarget);if(\"blur\"==a){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),e.onChangeAfterBlur&&this.triggerChangeEvent()}if(!r&&!l)if(this.state.hasFocus=\"focus\"==a&&+new Date,this.toggleFocusClass(this.state.hasFocus),\"mix\"!=e.mode){if(\"focus\"==a)return this.trigger(\"focus\",o),void(0!==e.dropdown.enabled&&e.userInput||this.dropdown.show(this.value.length?\"\":void 0));\"blur\"==a&&(this.trigger(\"blur\",o),this.loading(!1),\"select\"==e.mode&&(d&&(this.removeTags(),i=\"\"),s===i&&(i=\"\")),i&&!this.state.actions.selectOption&&e.addTagOnBlur&&e.addTagOn.includes(\"blur\")&&this.addTags(i,!0)),this.DOM.input.removeAttribute(\"style\"),this.dropdown.hide()}else\"focus\"==a?this.trigger(\"focus\",o):\"blur\"==t.type&&(this.trigger(\"blur\",o),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onCompositionStart(t){this.state.composing=!0},onCompositionEnd(t){this.state.composing=!1},onWindowKeyDown(t){var e,i=document.activeElement,s=v.call(this,i)&&this.DOM.scope.contains(document.activeElement),a=s&&i.hasAttribute(\"readonly\");if(s&&!a)switch(e=i.nextElementSibling,t.key){case\"Backspace\":this.settings.readonly||(this.removeTags(i),(e||this.DOM.input).focus());break;case\"Enter\":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;if(!this.state.composing&&e.userInput){\"select\"==e.mode&&e.enforceWhitelist&&this.value.length&&\"Tab\"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);this.trigger(\"keydown\",{event:t}),e.hooks.beforeKeyDown(t,{tagify:this}).then((s=>{if(\"mix\"==e.mode){switch(t.key){case\"Left\":case\"ArrowLeft\":this.state.actions.ArrowLeft=!0;break;case\"Delete\":case\"Backspace\":if(this.state.editing)return;var a=document.getSelection(),n=\"Delete\"==t.key&&a.anchorOffset==(a.anchorNode.length||0),r=a.anchorNode.previousSibling,d=1==a.anchorNode.nodeType||!a.anchorOffset&&r&&1==r.nodeType&&a.anchorNode.previousSibling;o(this.DOM.input.innerHTML);var h,g,p,c=this.getTagElms(),m=1===a.anchorNode.length&&a.anchorNode.nodeValue==String.fromCharCode(8203);if(\"edit\"==e.backspace&&d)return h=1==a.anchorNode.nodeType?null:a.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,h),void t.preventDefault();if(u()&&d instanceof Element)return p=l(d),d.hasAttribute(\"readonly\")||d.remove(),this.DOM.input.focus(),void setTimeout((()=>{w(p),this.DOM.input.click()}));if(\"BR\"==a.anchorNode.nodeName)return;if((n||d)&&1==a.anchorNode.nodeType?g=0==a.anchorOffset?n?c[0]:null:c[Math.min(c.length,a.anchorOffset)-1]:n?g=a.anchorNode.nextElementSibling:d instanceof Element&&(g=d),3==a.anchorNode.nodeType&&!a.anchorNode.nodeValue&&a.anchorNode.previousElementSibling&&t.preventDefault(),(d||n)&&!e.backspace)return void t.preventDefault();if(\"Range\"!=a.type&&!a.anchorOffset&&a.anchorNode==this.DOM.input&&\"Delete\"!=t.key)return void t.preventDefault();if(\"Range\"!=a.type&&g&&g.hasAttribute(\"readonly\"))return void w(l(g));\"Delete\"==t.key&&m&&T(a.anchorNode.nextSibling)&&this.removeTags(a.anchorNode.nextSibling),clearTimeout(M),M=setTimeout((()=>{var t=document.getSelection();o(this.DOM.input.innerHTML),!n&&t.anchorNode.previousSibling,this.value=[].map.call(c,((t,e)=>{var i=T(t);if(t.parentNode||i.readonly)return i;this.trigger(\"remove\",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}var v=\"manual\"==e.dropdown.position;switch(t.key){case\"Backspace\":\"select\"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&\"manual\"!=e.dropdown.position||\"\"!=t.target.textContent&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():\"edit\"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case\"Esc\":case\"Escape\":if(this.state.dropdown.visible)return;t.target.blur();break;case\"Down\":case\"ArrowDown\":this.state.dropdown.visible||this.dropdown.show();break;case\"ArrowRight\":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case\"Tab\":{let s=\"select\"==e.mode;if(!i||s)return!0;t.preventDefault()}case\"Enter\":if(this.state.dropdown.visible&&!v)return;t.preventDefault(),setTimeout((()=>{this.state.dropdown.visible&&!v||this.state.actions.selectOption||!e.addTagOn.includes(t.key.toLowerCase())||this.addTags(i,!0)}))}})).catch((t=>t))}},onInput(t){this.postUpdate();var e=this.settings;if(\"mix\"==e.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var i=this.input.normalize.call(this,void 0,{trim:!1}),s=i.length>=e.dropdown.enabled,a={value:i,inputElm:this.DOM.input},n=this.validateTag({value:i});\"select\"==e.mode&&this.toggleScopeValidation(n),a.isValid=n,this.state.inputText!=i&&(this.input.set.call(this,i,!1),-1!=i.search(e.delimiters)?this.addTags(i)&&this.input.set.call(this):e.dropdown.enabled>=0&&this.dropdown[s?\"show\":\"hide\"](i),this.trigger(\"input\",a))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,d=this.settings,h=this.value.length,p=this.getTagElms(),c=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>T(t).value));if(\"deleteContentBackward\"==t.inputType&&u()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:\"Backspace\"}),b(this.getTagElms()),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&c.appendChild(this.createTagElem(t))})),c.childNodes.length&&(m.insertNode(c),this.setRangeAtStartEnd(!1,c.lastChild)),p.length!=h)return this.value=[].map.call(this.getTagElms(),(t=>T(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(d.pattern).length-1,(i=e.match(d.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(d.pattern)[0],value:a.replace(d.pattern,\"\")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(d.delimiters))return this.state.tag.value=this.state.tag.value.replace(d.delimiters,\"\"),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,d.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=d.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s{this.update({withoutChangeEvent:!0}),this.trigger(\"input\",g({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?\"show\":\"hide\"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.parentNode||this.destroy(),this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickAnywhere(t){t.target==this.DOM.scope||this.DOM.scope.contains(t.target)||(this.toggleFocusClass(!1),this.state.hasFocus=!1)},onClickScope(t){var e=this.settings,i=t.target.closest(\".\"+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger(\"click\",{tag:i,index:this.getNodeIndex(i),data:T(i),event:t}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&(\"mix\"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide():0===e.dropdown.enabled&&\"mix\"!=e.mode&&this.dropdown.show(this.value.length?\"\":void 0):\"select\"!=e.mode||0!==e.dropdown.enabled||this.state.dropdown.visible||this.dropdown.show());this.removeTags(t.target.parentNode)}else this.DOM.input.focus()},onPaste(t){t.preventDefault();var e,i,s=this.settings;if(\"select\"==s.mode&&s.enforceWhitelist||!s.userInput)return!1;s.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData(\"Text\"),s.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),\"mix\"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?this.addTags(this.state.inputText+e,!0):(this.state.inputText=e,this.dropdown.show(e)))})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest(\".\"+this.settings.classNames.tag),s=this.getNodeIndex(i),a=T(i),n=this.input.normalize.call(this,t),o={[this.settings.tagTextProp]:n,__tagId:a.__tagId},r=this.validateTag(o);this.editTagChangeDetected(g(a,o))||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger(\"edit:input\",{tag:i,index:s,data:g({},this.value[s],{newValue:n}),event:e})},onEditTagPaste(t,e){var i=(e.clipboardData||window.clipboardData).getData(\"Text\");e.preventDefault();var s=f(i);this.setRangeAtStartEnd(!1,s)},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector(\"[contenteditable]\")}},onEditTagBlur(t){if(this.state.editing&&(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t))){var e,i,s=this.settings,a=t.closest(\".\"+s.classNames.tag),n=T(a),o=this.input.normalize.call(this,t),r={[s.tagTextProp]:o,__tagId:n.__tagId},l=n.__originalData,d=this.editTagChangeDetected(g(n,r)),h=this.validateTag(r);if(o)if(d){if(e=this.hasMaxTags(),i=g({},l,{[s.tagTextProp]:this.trim(o),__isValid:h}),s.transformTag.call(this,i,l),!0!==(h=(!e||!0===l.__isValid)&&this.validateTag(i))){if(this.trigger(\"invalid\",{data:i,tag:a,message:h}),s.editTags.keepInvalid)return;s.keepInvalidTags?i.__isValid=h:i=l}else s.keepInvalidTags&&(delete i.title,delete i[\"aria-invalid\"],delete i.class);this.onEditTagDone(a,i)}else this.onEditTagDone(a,l);else this.onEditTagDone(a)}},onEditTagkeydown(t,e){if(!this.state.composing)switch(this.trigger(\"edit:keydown\",{event:t}),t.key){case\"Esc\":case\"Escape\":this.state.editing=!1,!!e.__tagifyTagData.__originalData.value?e.parentNode.replaceChild(e.__tagifyTagData.__originalHTML,e):e.remove();break;case\"Enter\":case\"Tab\":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest(\".\"+this.settings.classNames.tag),a=T(s),n=this.settings;s&&n.userInput&&!1!==a.editable&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute(\"readonly\"),\"select\"==n.mode||n.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger(\"dblclick\",{tag:s,index:this.getNodeIndex(s),data:T(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if(\"

\"==t.outerHTML)t.replaceWith(document.createElement(\"br\"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode(\"\");3==t.childNodes[0].nodeType&&\"BR\"!=t.previousSibling.nodeName&&(e=document.createTextNode(\"\\n\")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),w(e)}else if(v.call(this,t))if(3!=t.previousSibling?.nodeType||t.previousSibling.textContent||t.previousSibling.remove(),t.previousSibling&&\"BR\"==t.previousSibling.nodeName){t.previousSibling.replaceWith(\"\\n​\");let e=t.nextSibling,i=\"\";for(;e;)i+=e.textContent,e=e.nextSibling;i.trim()&&w(t.previousSibling)}else t.previousSibling&&!T(t.previousSibling)||t.before(\"​\")})),t.removedNodes.forEach((t=>{t&&\"BR\"==t.nodeName&&v.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&\"\"==e.nodeValue&&e.remove(),e&&\"BR\"==e.nodeName||this.DOM.input.appendChild(document.createElement(\"br\"))}}};function S(t,e){if(!t){console.warn(\"Tagify:\",\"input element not found\",t);const e=new Proxy(this,{get:()=>()=>e});return e}if(t.__tagify)return console.warn(\"Tagify: \",\"input element is already Tagified - Same instance is returned.\",t),t.__tagify;var i;g(this,function(t){var e=document.createTextNode(\"\");function i(t,i,s){s&&i.split(/\\s+/g).forEach((i=>e[t+\"EventListener\"].call(e,i,s)))}return{off(t,e){return i(\"remove\",t,e),this},on(t,e){return e&&\"function\"==typeof e&&i(\"add\",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)\"remove\"==i&&(i=\"removeTag\"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o=\"object\"==typeof s?s:{value:s};if((o=a.cloneData?g({},o):o).tagify=this,s.event&&(o.event=this.cloneEvent(s.event)),s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox=/firefox|fxios/i.test(navigator.userAgent)&&!/seamonkey/i.test(navigator.userAgent),this.isIE=window.document.documentMode,e=e||{},this.getPersistedData=(i=e.id,t=>{let e,s=\"/\"+t;if(1==localStorage.getItem(D+i+\"/v\",1))try{e=JSON.parse(localStorage[D+i+s])}catch(t){}return e}),this.setPersistedData=(t=>t?(localStorage.setItem(D+t+\"/v\",1),(e,i)=>{let s=\"/\"+i,a=JSON.stringify(e);e&&i&&(localStorage.setItem(D+t+s,a),dispatchEvent(new Event(\"storage\")))}):()=>{})(e.id),this.clearPersistedData=(t=>e=>{const i=D+\"/\"+t+\"/\";if(e)localStorage.removeItem(i+e);else for(let t in localStorage)t.includes(i)&&localStorage.removeItem(t)})(e.id),this.applySettings(t,e),this.state={inputText:\"\",editing:!1,composing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),x.call(this),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus(),t.__tagify=this}return S.prototype={_dropdown:O,placeCaretAfterNode:w,getSetTagData:T,helpers:{sameStr:s,removeCollectionProp:a,omit:n,isObject:h,parseHTML:r,escapeHTML:d,extend:g,concatWithoutDups:p,getUID:m,isNodeTag:v},customEventsList:[\"change\",\"add\",\"remove\",\"invalid\",\"input\",\"click\",\"keydown\",\"focus\",\"blur\",\"edit:input\",\"edit:beforeUpdate\",\"edit:updated\",\"edit:start\",\"edit:keydown\",\"dropdown:show\",\"dropdown:hide\",\"dropdown:select\",\"dropdown:updated\",\"dropdown:noMatch\",\"dropdown:scroll\"],dataProps:[\"__isValid\",\"__removed\",\"__originalData\",\"__originalHTML\",\"__tagId\"],trim(t){return this.settings.trim&&t&&\"string\"==typeof t?t.trim():t},parseHTML:r,templates:N,parseTemplate(t,e){return r((t=this.settings.templates[t]||t).apply(this,e))},set whitelist(t){const e=t&&Array.isArray(t);this.settings.whitelist=e?t:[],this.setPersistedData(e?t:[],\"whitelist\")},get whitelist(){return this.settings.whitelist},generateClassSelectors(t){for(let e in t){let i=e;Object.defineProperty(t,i+\"Selector\",{get(){return\".\"+this[i].split(\" \")[0]}})}},applySettings(t,i){y.templates=this.templates;var s=g({},y,\"mix\"==i.mode?{dropdown:{position:\"text\"}}:{}),a=this.settings=g({},s,i);if(a.disabled=t.hasAttribute(\"disabled\"),a.readonly=a.readonly||t.hasAttribute(\"readonly\"),a.placeholder=d(t.getAttribute(\"placeholder\")||a.placeholder||\"\"),a.required=t.hasAttribute(\"required\"),this.generateClassSelectors(a.classNames),void 0===a.dropdown.includeSelectedTags&&(a.dropdown.includeSelectedTags=a.duplicates),this.isIE&&(a.autoComplete=!1),[\"whitelist\",\"blacklist\"].forEach((e=>{var i=t.getAttribute(\"data-\"+e);i&&(i=i.split(a.delimiters))instanceof Array&&(a[e]=i)})),\"autoComplete\"in i&&!h(i.autoComplete)&&(a.autoComplete=y.autoComplete,a.autoComplete.enabled=i.autoComplete),\"mix\"==a.mode&&(a.pattern=a.pattern||/@/,a.autoComplete.rightKey=!0,a.delimiters=i.delimiters||null,a.tagTextProp&&!a.dropdown.searchKeys.includes(a.tagTextProp)&&a.dropdown.searchKeys.push(a.tagTextProp)),t.pattern)try{a.pattern=new RegExp(t.pattern)}catch(t){}if(a.delimiters){a._delimiters=a.delimiters;try{a.delimiters=new RegExp(this.settings.delimiters,\"g\")}catch(t){}}a.disabled&&(a.userInput=!1),this.TEXTS=e(e({},I),a.texts||{}),(\"select\"!=a.mode||i.dropdown?.enabled)&&a.userInput||(a.dropdown.enabled=0),a.dropdown.appendTarget=i.dropdown?.appendTarget||document.body;let n=this.getPersistedData(\"whitelist\");Array.isArray(n)&&(this.whitelist=Array.isArray(a.whitelist)?p(a.whitelist,n):n)},getAttributes(t){var e,i=this.getCustomAttributes(t),s=\"\";for(e in i)s+=\" \"+e+(void 0!==t[e]?`=\"${i[e]}\"`:\"\");return s},getCustomAttributes(t){if(!h(t))return\"\";var e,i={};for(e in t)\"__\"!=e.slice(0,2)&&\"class\"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=d(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(t=>{let e=t.value;return\"s\"==t.unit?1e3*e:e})(function(t){if(!t)return{};var e=(t=t.trim().split(\" \")[0]).split(/\\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e=\"tag-hide-transition\",t.getPropertyValue(\"--\"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.originalInput_tabIndex=t.tabIndex,e.scope=this.parseTemplate(\"wrapper\",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t),t.tabIndex=-1)},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.DOM.originalInput.tabIndex=this.DOM.originalInput_tabIndex,delete this.DOM.originalInput.__tagify,this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout),clearInterval(this.listeners.main.originalInputValueObserverInterval)},loadOriginalValues(t){var e,i=this.settings;if(this.state.blockChangeEvent=!0,void 0===t){const e=this.getPersistedData(\"value\");t=e&&!this.DOM.originalInput.value?e:i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value}if(this.removeAllTags(),t)if(\"mix\"==i.mode)this.parseMixTags(t),(e=this.DOM.input.lastChild)&&\"BR\"==e.tagName||this.DOM.input.insertAdjacentHTML(\"beforeend\",\"
\");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t,!0).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?\"\":this.DOM.originalInput.value},cloneEvent(t){var e={};for(var i in t)\"path\"!=i&&(e[i]=t[i]);return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?\"add\":\"remove\"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?\"add\":\"remove\"](this.settings.classNames.tagLoading),this},toggleClass(t,e){\"string\"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleScopeValidation(t){var e=!0===t||void 0===t;!this.settings.required&&t&&t===this.TEXTS.empty&&(e=!0),this.toggleClass(this.settings.classNames.tagInvalid,!e),this.DOM.scope.title=e?\"\":t},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent(\"change\",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger(\"change\",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:_,fixFirefoxLastTagNoCaret(){},setRangeAtStartEnd(t,e){if(e){t=\"number\"==typeof t?t:!!t,e=e.lastChild||e;var i=document.getSelection();if(i.focusNode instanceof Element&&!this.DOM.input.contains(i.focusNode))return!0;try{i.rangeCount>=1&&[\"Start\",\"End\"].forEach((s=>i.getRangeAt(0)[\"set\"+s](e,t||e.length)))}catch(t){console.warn(\"Tagify: \",t)}}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e=\"string\"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTagChangeDetected(t){var e=t.__originalData;for(var i in e)if(!this.dataProps.includes(i)&&t[i]!=e[i])return!0;return!1},getTagTextNode(t){return t.querySelector(this.settings.classNames.tagTextSelector)},setTagTextNode(t,e){this.getTagTextNode(t).innerHTML=d(e)},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings,s=this.getTagTextNode(t),a=this.getNodeIndex(t),n=T(t),o=this.events.callbacks,r=!0;if(s){if(!(n instanceof Object&&\"editable\"in n)||n.editable)return n=T(t,{__originalData:g({},n),__originalHTML:t.cloneNode(!0)}),T(n.__originalHTML,n.__originalData),s.setAttribute(\"contenteditable\",!0),t.classList.add(i.classNames.tagEditing),s.addEventListener(\"focus\",o.onEditTagFocus.bind(this,t)),s.addEventListener(\"blur\",o.onEditTagBlur.bind(this,this.getTagTextNode(t))),s.addEventListener(\"input\",o.onEditTagInput.bind(this,s)),s.addEventListener(\"paste\",o.onEditTagPaste.bind(this,s)),s.addEventListener(\"keydown\",(e=>o.onEditTagkeydown.call(this,e,t))),s.addEventListener(\"compositionstart\",o.onCompositionStart.bind(this)),s.addEventListener(\"compositionend\",o.onCompositionEnd.bind(this)),e.skipValidation||(r=this.editTagToggleValidity(t)),s.originalIsValid=r,this.trigger(\"edit:start\",{tag:t,index:a,data:n,isValid:r}),s.focus(),this.setRangeAtStartEnd(!1,s),this}else console.warn(\"Cannot find element in Tag template: .\",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||T(t))return(i=!(\"__isValid\"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid=i,e.__isValid;console.warn(\"tag has no data: \",t,e)},onEditTagDone(t,e){t=t||this.state.editing.scope,e=e||{};var i,s={tag:t,index:this.getNodeIndex(t),previousData:T(t),data:e},a=this.settings;this.trigger(\"edit:beforeUpdate\",s,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&((i=e[a.tagTextProp])?i.trim()&&i:a.tagTextProp in e?void 0:e.value)?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),a.a11y.focusableTags?t.focus():w(t)):t&&this.removeTags(t),this.trigger(\"edit:updated\",s),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&g(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(\" \")[0])||this.value.push(T(t))})),this.update()},injectAtCaret(t,e){if(!(e=e||this.state.selection?.range)&&t)return this.appendMixTags(t),this;let i=f(t,e);return this.setRangeAtStartEnd(!1,i),this.updateValueByDOMTags(),this.update(),this},input:{set(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:\"\",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=d(\"\"+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t,e){var i=t||this.DOM.input,s=[];i.childNodes.forEach((t=>3==t.nodeType&&s.push(t.nodeValue))),s=s.join(\"\\n\");try{s=s.replace(/(?:\\r\\n|\\r|\\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return s=s.replace(/\\s/g,\" \"),e?.trim?this.trim(s):s},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){\"string\"==typeof(t=t||{value:\"\"})&&(t={value:t});var e=this.dropdown.getMappedValue(t);if(\"number\"!=typeof e){var i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute(\"data-suggest\",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute(\"data-suggest\"),delete this.state.inputSuggestion)}}},set(t){var e=this.DOM.input.getAttribute(\"data-suggest\"),i=t||(e?this.state.inputText+e:null);return!!i&&(\"mix\"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd(!1,this.DOM.input)),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(){for(var t=arguments.length,e=new Array(t),i=0;i{a.__tagifyTagData&&s(this.trim(a.__tagifyTagData.value),t,i)&&e.push(n)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(\"\"+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||\"value\";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o=\"string\"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a=\"string\"==typeof i?{value:i}:i,!0})),a||\"value\"!=e||\"value\"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i=\"value\"in t?\"value\":e.tagTextProp,s=this.trim(t[i]+\"\");return(t[i]+\"\").trim()?\"mix\"!=e.mode&&e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,e.dropdown.caseSensitive,t.__tagId)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{\"aria-invalid\":!0,class:`${t.class||\"\"} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||\"readonly\"]=t,this.DOM.scope[(t?\"set\":\"remove\")+\"Attribute\"](e||\"readonly\",!0),this.settings.userInput=!0,this.setContentEditable(!t)},setContentEditable(t){this.settings.userInput&&(this.DOM.input.contentEditable=t,this.DOM.input.tabIndex=t?0:-1)},setDisabled(t){this.setReadonly(t,\"disabled\")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp,o=[],r=!!i&&i[0]instanceof Object,l=Array.isArray(t),d=l&&t[0].value,h=t=>(t+\"\").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if(\"number\"==typeof t&&(t=t.toString()),\"string\"==typeof t){if(!t.trim())return[];t=h(t)}else l&&(t=[].concat(...t.map((t=>null!=t.value?t:h(t)))));return r&&!d&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):\"mix\"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(a.call(this,h),u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else h[d=h[r]?r:\"value\"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join(\"\")})).join(\"\"),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode(\"\")),this.DOM.input.normalize();var d=this.getTagElms();return d.forEach(((t,e)=>T(t,l[e]))),this.update({withoutChangeEvent:!0}),b(d,this.state.hasFocus),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=this.state.selection||window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),-1==(i=n.nodeValue.lastIndexOf(e))?!0:(s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0)}},selectTag(t,e){var i=this.settings;if(!i.enforceWhitelist||this.isTagWhitelisted(e.value)){this.input.set.call(this,e[i.tagTextProp]||e.value,!0),this.state.actions.selectOption&&setTimeout((()=>this.setRangeAtStartEnd(!1,this.DOM.input)));var s=this.getLastTag();return s?this.replaceTag(s,e):this.appendTag(t),this.value[0]=e,this.update(),this.trigger(\"add\",{tag:t,data:e}),[t]}},addEmptyTag(t){var e=g({value:\"\"},t||{}),i=this.createTagElem(e);T(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i){var s=[],a=this.settings,n=[],o=document.createDocumentFragment();if(i=i||a.skipInvalid,!t||0==t.length)return s;switch(t=this.normalizeTags(t),a.mode){case\"mix\":return this.addMixTags(t);case\"select\":e=!1,this.removeAllTags()}return this.DOM.input.removeAttribute(\"style\"),t.forEach((t=>{var e,r={},l=Object.assign({},t,{value:t.value+\"\"});if(t=Object.assign({},l),a.transformTag.call(this,t),t.__isValid=this.hasMaxTags()||this.validateTag(t),!0!==t.__isValid){if(i)return;if(g(r,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:l}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value)),!a.createInvalidTags)return void n.push(t.value)}if(\"readonly\"in t&&(t.readonly?r[\"aria-readonly\"]=!0:delete t.readonly),e=this.createTagElem(t,r),s.push(e),\"select\"==a.mode)return this.selectTag(e,t);o.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger(\"add\",{tag:e,index:this.value.length-1,data:t})):(this.trigger(\"invalid\",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(o),this.update(),t.length&&e&&(this.input.set.call(this,a.createInvalidTags?\"\":n.join(a._delimiters)),this.setRangeAtStartEnd(!1,this.DOM.input)),a.dropdown.enabled&&this.dropdown.refilter(),s},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);var e=document.createDocumentFragment();return t.forEach((t=>{var i=this.createTagElem(t);e.appendChild(i)})),this.appendMixTags(e),e},appendMixTags(t){var e=!!this.state.selection;e?this.injectAtCaret(t):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(t),this.updateValueByDOMTags(),this.update())},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;setTimeout(w,0,a)}return this.state.tag=null,this.trigger(\"add\",g({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.input;e.scope.insertBefore(t,i)},createTagElem(t,i){t.__tagId=m();var s,a=g({},t,e({value:d(t.value+\"\")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate(\"tag\",[a,this])),T(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((e,i)=>{var s=T(e),a=this.hasMaxTags(),n=this.validateTag(s),o=!0===n&&!a;if(\"select\"==t.mode&&this.toggleScopeValidation(n),o)return s=s.__preInvalidData?s.__preInvalidData:{value:s.value},this.replaceTag(e,s);e.title=a||n}))},removeTags(t,e,i){var s,a=this.settings;if(t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>{e&&\"string\"==typeof e&&(e=this.getTagElmByValue(e));var i=T(e);return e&&i&&!i.readonly&&t.push({node:e,idx:this.getTagIdx(i),data:T(e,{__removed:!0})}),t}),[]),i=\"number\"==typeof i?i:this.CSSVars.tagHideTransition,\"select\"==a.mode&&(i=0,this.input.set.call(this)),1==s.length&&\"select\"!=a.mode&&s[0].node.classList.contains(a.classNames.tagNotAllowed)&&(e=!0),s.length)return a.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?a.keepInvalidTags&&this.trigger(\"remove\",{tag:t.node,index:t.idx}):(this.trigger(\"remove\",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),a.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+\"px\",document.body.clientTop,e.node.classList.add(a.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),\"select\"==a.mode&&this.setContentEditable(!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=T(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],\"mix\"==this.settings.mode?this.DOM.input.innerHTML=\"\":this.removeTagsFromDOM(),this.dropdown.refilter(),this.dropdown.position(),this.state.dropdown.visible&&setTimeout((()=>{this.DOM.input.focus()})),\"select\"==this.settings.mode&&(this.input.set.call(this),this.setContentEditable(!0)),this.update(t)},postUpdate(){this.state.blockChangeEvent=!1;var t=this.settings,e=t.classNames,i=\"mix\"==t.mode?t.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value.trim():this.value.length+this.input.raw.call(this).length;this.toggleClass(e.hasMaxTags,this.value.length>=t.maxTags),this.toggleClass(e.hasNoTags,!this.value.length),this.toggleClass(e.empty,!i),\"select\"==t.mode&&this.toggleScopeValidation(this.value?.[0]?.__isValid)},setOriginalInputValue(t){var e=this.DOM.originalInput;this.settings.mixMode.integrated||(e.value=t,e.tagifyValue=e.value,this.setPersistedData(t,\"value\"))},update(t){clearTimeout(this.debouncedUpdateTimeout),this.debouncedUpdateTimeout=setTimeout(function(){var e=this.getInputValue();this.setOriginalInputValue(e),this.settings.onChangeAfterBlur&&(t||{}).withoutChangeEvent||this.state.blockChangeEvent||this.triggerChangeEvent();this.postUpdate()}.bind(this),100)},getInputValue(){var t=this.getCleanValue();return\"mix\"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):\"\"},getCleanValue(t){return a(t||this.value,this.dataProps)},getMixedTagsAsString(){var t=\"\",e=this,i=this.settings,s=i.originalInputValueFormat||JSON.stringify,a=i.mixTagsInterpolator;return function i(o){o.childNodes.forEach((o=>{if(1==o.nodeType){const r=T(o);if(\"BR\"==o.tagName&&(t+=\"\\r\\n\"),r&&v.call(e,o)){if(r.__removed)return;t+=a[0]+s(n(r,e.dataProps))+a[1]}else o.getAttribute(\"style\")||[\"B\",\"I\",\"U\"].includes(o.tagName)?t+=o.textContent:\"DIV\"!=o.tagName&&\"P\"!=o.tagName||(t+=\"\\r\\n\",i(o))}else t+=o.textContent}))}(this.DOM.input),t}},S.prototype.removeTag=S.prototype.removeTags,S}));\n\n\n//# sourceURL=webpack://Sneat/./node_modules/@yaireo/tagify/dist/tagify.min.js?"); + +/***/ }), + +/***/ "./libs/tagify/tagify.js": +/*!*******************************!*\ + !*** ./libs/tagify/tagify.js ***! + \*******************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ Tagify: function() { return /* reexport default from dynamic */ _yaireo_tagify__WEBPACK_IMPORTED_MODULE_0___default.a; }\n/* harmony export */ });\n/* harmony import */ var _yaireo_tagify__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @yaireo/tagify */ \"./node_modules/@yaireo/tagify/dist/tagify.min.js\");\n/* harmony import */ var _yaireo_tagify__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yaireo_tagify__WEBPACK_IMPORTED_MODULE_0__);\n\ntry {\n window.Tagify = (_yaireo_tagify__WEBPACK_IMPORTED_MODULE_0___default());\n} catch (e) {}\n\n\n//# sourceURL=webpack://Sneat/./libs/tagify/tagify.js?"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ !function() { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function() { return module['default']; } : +/******/ function() { return module; }; +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ }(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ !function() { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = function(exports, definition) { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ }(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ !function() { +/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } +/******/ }(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ !function() { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ }(); +/******/ +/************************************************************************/ +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module can't be inlined because the eval devtool is used. +/******/ var __webpack_exports__ = __webpack_require__("./libs/tagify/tagify.js"); +/******/ +/******/ return __webpack_exports__; +/******/ })() +; +}); \ No newline at end of file diff --git a/public/img/app/dashboard-light-09.png b/public/img/app/dashboard-light-09.png new file mode 100644 index 00000000..316a52e8 Binary files /dev/null and b/public/img/app/dashboard-light-09.png differ diff --git a/public/img/avatars/1.png b/public/img/avatars/1.png deleted file mode 100644 index 02ffed82..00000000 Binary files a/public/img/avatars/1.png and /dev/null differ diff --git a/public/img/avatars/5.png b/public/img/avatars/5.png deleted file mode 100644 index 649f9ec0..00000000 Binary files a/public/img/avatars/5.png and /dev/null differ diff --git a/public/img/avatars/6.png b/public/img/avatars/6.png deleted file mode 100644 index 99ad3a60..00000000 Binary files a/public/img/avatars/6.png and /dev/null differ diff --git a/public/img/avatars/7.png b/public/img/avatars/7.png deleted file mode 100644 index 335a7417..00000000 Binary files a/public/img/avatars/7.png and /dev/null differ diff --git a/public/img/brand/logo-1.png b/public/img/brand/logo-1.png deleted file mode 100644 index 7f551d70..00000000 Binary files a/public/img/brand/logo-1.png and /dev/null differ diff --git a/public/img/brand/logo-2.png b/public/img/brand/logo-2.png deleted file mode 100644 index 5e3f2698..00000000 Binary files a/public/img/brand/logo-2.png and /dev/null differ diff --git a/public/img/brand/logo-3.png b/public/img/brand/logo-3.png deleted file mode 100644 index e854e939..00000000 Binary files a/public/img/brand/logo-3.png and /dev/null differ diff --git a/public/img/brand/logo-4.png b/public/img/brand/logo-4.png deleted file mode 100644 index 6c5d3f3c..00000000 Binary files a/public/img/brand/logo-4.png and /dev/null differ diff --git a/public/img/brand/logo-5.png b/public/img/brand/logo-5.png deleted file mode 100644 index bf3cc14e..00000000 Binary files a/public/img/brand/logo-5.png and /dev/null differ diff --git a/public/img/brand/logo-6.png b/public/img/brand/logo-6.png deleted file mode 100644 index 99980b2a..00000000 Binary files a/public/img/brand/logo-6.png and /dev/null differ diff --git a/public/img/brand/logo_1-dark.png b/public/img/brand/logo_1-dark.png deleted file mode 100644 index cb1a58d2..00000000 Binary files a/public/img/brand/logo_1-dark.png and /dev/null differ diff --git a/public/img/brand/logo_1-light.png b/public/img/brand/logo_1-light.png deleted file mode 100644 index e0308e90..00000000 Binary files a/public/img/brand/logo_1-light.png and /dev/null differ diff --git a/public/img/brand/logo_2-dark.png b/public/img/brand/logo_2-dark.png deleted file mode 100644 index f5e92478..00000000 Binary files a/public/img/brand/logo_2-dark.png and /dev/null differ diff --git a/public/img/brand/logo_2-light.png b/public/img/brand/logo_2-light.png deleted file mode 100644 index 719e6103..00000000 Binary files a/public/img/brand/logo_2-light.png and /dev/null differ diff --git a/public/img/brand/logo_3-dark.png b/public/img/brand/logo_3-dark.png deleted file mode 100644 index 27c68e49..00000000 Binary files a/public/img/brand/logo_3-dark.png and /dev/null differ diff --git a/public/img/brand/logo_3-light.png b/public/img/brand/logo_3-light.png deleted file mode 100644 index 5ec4f174..00000000 Binary files a/public/img/brand/logo_3-light.png and /dev/null differ diff --git a/public/img/brand/logo_4-dark.png b/public/img/brand/logo_4-dark.png deleted file mode 100644 index 3e5bfc3d..00000000 Binary files a/public/img/brand/logo_4-dark.png and /dev/null differ diff --git a/public/img/brand/logo_4-light.png b/public/img/brand/logo_4-light.png deleted file mode 100644 index 0929f535..00000000 Binary files a/public/img/brand/logo_4-light.png and /dev/null differ diff --git a/public/img/brand/logo_5-dark.png b/public/img/brand/logo_5-dark.png deleted file mode 100644 index 34342001..00000000 Binary files a/public/img/brand/logo_5-dark.png and /dev/null differ diff --git a/public/img/brand/logo_5-light.png b/public/img/brand/logo_5-light.png deleted file mode 100644 index deb1071f..00000000 Binary files a/public/img/brand/logo_5-light.png and /dev/null differ diff --git a/public/img/brand/marco-250x250.png b/public/img/brand/marco-250x250.png new file mode 100644 index 00000000..ca14f303 Binary files /dev/null and b/public/img/brand/marco-250x250.png differ diff --git a/public/img/brand/ofw-500x500.png b/public/img/brand/ofw-500x500.png new file mode 100644 index 00000000..4e8c3112 Binary files /dev/null and b/public/img/brand/ofw-500x500.png differ diff --git a/public/img/elements/1.jpg b/public/img/elements/1.jpg deleted file mode 100644 index 779350ea..00000000 Binary files a/public/img/elements/1.jpg and /dev/null differ diff --git a/public/img/elements/11.jpg b/public/img/elements/11.jpg deleted file mode 100644 index 30f1d630..00000000 Binary files a/public/img/elements/11.jpg and /dev/null differ diff --git a/public/img/elements/12.jpg b/public/img/elements/12.jpg deleted file mode 100644 index e7347643..00000000 Binary files a/public/img/elements/12.jpg and /dev/null differ diff --git a/public/img/elements/13.jpg b/public/img/elements/13.jpg deleted file mode 100644 index 5b19ce51..00000000 Binary files a/public/img/elements/13.jpg and /dev/null differ diff --git a/public/img/elements/17.jpg b/public/img/elements/17.jpg deleted file mode 100644 index 2004cda1..00000000 Binary files a/public/img/elements/17.jpg and /dev/null differ diff --git a/public/img/elements/18.jpg b/public/img/elements/18.jpg deleted file mode 100644 index 46af1552..00000000 Binary files a/public/img/elements/18.jpg and /dev/null differ diff --git a/public/img/elements/19.jpg b/public/img/elements/19.jpg deleted file mode 100644 index cae34490..00000000 Binary files a/public/img/elements/19.jpg and /dev/null differ diff --git a/public/img/elements/2.jpg b/public/img/elements/2.jpg deleted file mode 100644 index 78bc4d8e..00000000 Binary files a/public/img/elements/2.jpg and /dev/null differ diff --git a/public/img/elements/20.jpg b/public/img/elements/20.jpg deleted file mode 100644 index 1d7fb7c2..00000000 Binary files a/public/img/elements/20.jpg and /dev/null differ diff --git a/public/img/elements/3.jpg b/public/img/elements/3.jpg deleted file mode 100644 index f34f3de6..00000000 Binary files a/public/img/elements/3.jpg and /dev/null differ diff --git a/public/img/elements/4.jpg b/public/img/elements/4.jpg deleted file mode 100644 index 48432005..00000000 Binary files a/public/img/elements/4.jpg and /dev/null differ diff --git a/public/img/elements/5.jpg b/public/img/elements/5.jpg deleted file mode 100644 index 29714f59..00000000 Binary files a/public/img/elements/5.jpg and /dev/null differ diff --git a/public/img/elements/7.jpg b/public/img/elements/7.jpg deleted file mode 100644 index 01a42a51..00000000 Binary files a/public/img/elements/7.jpg and /dev/null differ diff --git a/public/img/hero/bg-01.jpg b/public/img/hero/bg-01.jpg new file mode 100644 index 00000000..0ea8eebf Binary files /dev/null and b/public/img/hero/bg-01.jpg differ diff --git a/public/img/hero/bg-02.png b/public/img/hero/bg-02.png new file mode 100644 index 00000000..e6acaea7 Binary files /dev/null and b/public/img/hero/bg-02.png differ diff --git a/public/img/hero/bg-03.png b/public/img/hero/bg-03.png new file mode 100644 index 00000000..622a1104 Binary files /dev/null and b/public/img/hero/bg-03.png differ diff --git a/public/img/hero/bg-04.png b/public/img/hero/bg-04.png new file mode 100644 index 00000000..988f506f Binary files /dev/null and b/public/img/hero/bg-04.png differ diff --git a/public/img/images/contact-customer-service.png b/public/img/hero/contact-customer-service.png similarity index 100% rename from public/img/images/contact-customer-service.png rename to public/img/hero/contact-customer-service.png diff --git a/public/img/icons/ai.png b/public/img/icons/ai.png new file mode 100644 index 00000000..bf9b1f11 Binary files /dev/null and b/public/img/icons/ai.png differ diff --git a/public/img/icons/apple-icon-lite.png b/public/img/icons/apple-icon-lite.png new file mode 100644 index 00000000..2654c2d3 Binary files /dev/null and b/public/img/icons/apple-icon-lite.png differ diff --git a/public/img/icons/attendance.png b/public/img/icons/attendance.png new file mode 100644 index 00000000..ea2460b6 Binary files /dev/null and b/public/img/icons/attendance.png differ diff --git a/public/img/icons/cloud-service.png b/public/img/icons/cloud-service.png new file mode 100644 index 00000000..de9dd0ac Binary files /dev/null and b/public/img/icons/cloud-service.png differ diff --git a/public/img/icons/dashboard.png b/public/img/icons/dashboard.png new file mode 100644 index 00000000..5d62aef8 Binary files /dev/null and b/public/img/icons/dashboard.png differ diff --git a/public/img/icons/diamond-info - Copy.svg b/public/img/icons/diamond-info - Copy.svg new file mode 100644 index 00000000..5f24f9ef --- /dev/null +++ b/public/img/icons/diamond-info - Copy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/icons/directory.png b/public/img/icons/directory.png new file mode 100644 index 00000000..038852cd Binary files /dev/null and b/public/img/icons/directory.png differ diff --git a/public/img/icons/document.png b/public/img/icons/document.png new file mode 100644 index 00000000..aec11224 Binary files /dev/null and b/public/img/icons/document.png differ diff --git a/public/img/icons/google-play-icon-lite.png b/public/img/icons/google-play-icon-lite.png new file mode 100644 index 00000000..6952cb9f Binary files /dev/null and b/public/img/icons/google-play-icon-lite.png differ diff --git a/public/img/icons/profile.png b/public/img/icons/profile.png new file mode 100644 index 00000000..8333da3a Binary files /dev/null and b/public/img/icons/profile.png differ diff --git a/public/img/icons/report.png b/public/img/icons/report.png new file mode 100644 index 00000000..62b6682c Binary files /dev/null and b/public/img/icons/report.png differ diff --git a/public/img/icons/spending.png b/public/img/icons/spending.png new file mode 100644 index 00000000..aaf8d53c Binary files /dev/null and b/public/img/icons/spending.png differ diff --git a/public/img/illustrations/03.png b/public/img/illustrations/03.png new file mode 100644 index 00000000..8d517756 Binary files /dev/null and b/public/img/illustrations/03.png differ diff --git a/public/img/illustrations/contact-customer-service.png b/public/img/illustrations/contact-customer-service.png new file mode 100644 index 00000000..4e5aaaad Binary files /dev/null and b/public/img/illustrations/contact-customer-service.png differ diff --git a/public/img/illustrations/contact-us.png b/public/img/illustrations/contact-us.png new file mode 100644 index 00000000..4886aca8 Binary files /dev/null and b/public/img/illustrations/contact-us.png differ diff --git a/public/img/illustrations/fm-01.png b/public/img/illustrations/fm-01.png new file mode 100644 index 00000000..885bde1a Binary files /dev/null and b/public/img/illustrations/fm-01.png differ diff --git a/public/img/illustrations/man-with-laptop-light.png b/public/img/illustrations/man-with-laptop-light.png deleted file mode 100644 index 42661207..00000000 Binary files a/public/img/illustrations/man-with-laptop-light.png and /dev/null differ diff --git a/public/img/illustrations/undraw_pricing.png b/public/img/illustrations/undraw_pricing.png new file mode 100644 index 00000000..151bb16a Binary files /dev/null and b/public/img/illustrations/undraw_pricing.png differ diff --git a/public/img/illustrations/undraw_pricing.svg b/public/img/illustrations/undraw_pricing.svg new file mode 100644 index 00000000..8966ea28 --- /dev/null +++ b/public/img/illustrations/undraw_pricing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/illustrations/worker_01.svg b/public/img/illustrations/worker_01.svg deleted file mode 100644 index 2170c2f4..00000000 --- a/public/img/illustrations/worker_01.svg +++ /dev/null @@ -1,4955 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/img/illustrations/worker_02.jpg b/public/img/illustrations/worker_02.jpg deleted file mode 100644 index 274c55c3..00000000 Binary files a/public/img/illustrations/worker_02.jpg and /dev/null differ diff --git a/public/img/illustrations/worker_02.svg b/public/img/illustrations/worker_02.svg deleted file mode 100644 index c673e01c..00000000 --- a/public/img/illustrations/worker_02.svg +++ /dev/null @@ -1,1117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/img/illustrations/worker_03.jpg b/public/img/illustrations/worker_03.jpg deleted file mode 100644 index 99c9be24..00000000 Binary files a/public/img/illustrations/worker_03.jpg and /dev/null differ diff --git a/public/img/illustrations/worker_03.png b/public/img/illustrations/worker_03.png deleted file mode 100644 index d5fdab98..00000000 Binary files a/public/img/illustrations/worker_03.png and /dev/null differ diff --git a/public/img/layouts/layout-container-light.png b/public/img/layouts/layout-container-light.png deleted file mode 100644 index 513338f0..00000000 Binary files a/public/img/layouts/layout-container-light.png and /dev/null differ diff --git a/public/img/layouts/layout-fluid-light.png b/public/img/layouts/layout-fluid-light.png deleted file mode 100644 index ca093f4c..00000000 Binary files a/public/img/layouts/layout-fluid-light.png and /dev/null differ diff --git a/public/img/layouts/layout-without-menu-light.png b/public/img/layouts/layout-without-menu-light.png deleted file mode 100644 index fe7d9198..00000000 Binary files a/public/img/layouts/layout-without-menu-light.png and /dev/null differ diff --git a/public/img/layouts/layout-without-navbar-light.png b/public/img/layouts/layout-without-navbar-light.png deleted file mode 100644 index 68e69ba9..00000000 Binary files a/public/img/layouts/layout-without-navbar-light.png and /dev/null differ diff --git a/public/img/sneat.svg b/public/img/sneat.svg deleted file mode 100644 index 347e4f0d..00000000 --- a/public/img/sneat.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/public/img/teams/team-member-1.png b/public/img/teams/team-member-1.png deleted file mode 100644 index 2a007f12..00000000 Binary files a/public/img/teams/team-member-1.png and /dev/null differ diff --git a/public/img/teams/team-member-2.png b/public/img/teams/team-member-2.png deleted file mode 100644 index b1b7e7c0..00000000 Binary files a/public/img/teams/team-member-2.png and /dev/null differ diff --git a/public/img/teams/team-member-3.png b/public/img/teams/team-member-3.png deleted file mode 100644 index 805b2825..00000000 Binary files a/public/img/teams/team-member-3.png and /dev/null differ diff --git a/public/img/teams/team-member-4.png b/public/img/teams/team-member-4.png deleted file mode 100644 index 8718f3c1..00000000 Binary files a/public/img/teams/team-member-4.png and /dev/null differ diff --git a/src/ModalProvider.jsx b/src/ModalProvider.jsx index c4e0779f..8ef00bcd 100644 --- a/src/ModalProvider.jsx +++ b/src/ModalProvider.jsx @@ -4,19 +4,25 @@ 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"; const ModalProvider = () => { const { isOpen, onClose } = useOrganizationModal(); const { isOpen: isAuthOpen } = useAuthModal(); - const {isOpen:isChangePass} = useModal("ChangePassword") + const { isOpen: isChangePass } = useModal("ChangePassword"); + const { isOpen: isCollectionNew } = useModal("newCollection"); + const { isOpen: isServiceTeamAllocation } = useModal("ServiceTeamAllocation"); return ( <> {isOpen && } {isAuthOpen && } - {isChangePass && } + {isChangePass && } + {isCollectionNew && } + {isServiceTeamAllocation && } ); }; -export default ModalProvider; \ No newline at end of file +export default ModalProvider; diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/vendor/css/core.css b/src/assets/vendor/css/core.css index 70f4fb09..bf74ae48 100644 --- a/src/assets/vendor/css/core.css +++ b/src/assets/vendor/css/core.css @@ -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.9375rem; + --bs-body-font-size: 0.875rem; --bs-body-font-weight: 400; --bs-body-line-height: 1.375; --bs-body-color: #646e78; diff --git a/src/components/Activities/AttendLogs.jsx b/src/components/Activities/AttendLogs.jsx index 7c1e686d..39af03ab 100644 --- a/src/components/Activities/AttendLogs.jsx +++ b/src/components/Activities/AttendLogs.jsx @@ -123,12 +123,15 @@ const AttendLogs = ({ Id }) => { }, []); return (
-
+
+
Attendance Logs
{logs && !loading && ( -

- Attendance logs for{" "} - {logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName}{" "} - on {formatUTCToLocalTime(logs[0]?.activityTime)} +

+ Showing logs for{" "} + + {logs[0]?.employee?.firstName + " " + logs[0]?.employee?.lastName} + {" "} + on {formatUTCToLocalTime(logs[0]?.activityTime)}

)}
@@ -142,9 +145,9 @@ const AttendLogs = ({ Id }) => { + - @@ -156,11 +159,16 @@ const AttendLogs = ({ Id }) => { .sort((a, b) => b.id - a.id) .map((log, index) => ( - - + + + + + + + + + + + ))} + + ))} + +
Activity Date TimeActivity Location Recored By Description
{formatUTCToLocalTime(log.activityTime)}{convertShortTime(log.activityTime)} {whichActivityPerform(log.activity, log.activityTime)} +
+ {formatUTCToLocalTime(log.activityTime)} +
+
{convertShortTime(log.activityTime)} {log?.latitude != 0 ? ( { )} - {`${log?.updatedByEmployee?.firstName ?? ""} ${ - log?.updatedByEmployee?.lastName ?? "" - }`} + {`${log?.updatedByEmployee?.firstName ?? ""} ${log?.updatedByEmployee?.lastName ?? "" + }`} {log?.comment?.length > 50 diff --git a/src/components/Activities/Attendance.jsx b/src/components/Activities/Attendance.jsx index 9a082976..681434df 100644 --- a/src/components/Activities/Attendance.jsx +++ b/src/components/Activities/Attendance.jsx @@ -11,6 +11,7 @@ import { useSelector } from "react-redux"; import { useQueryClient } from "@tanstack/react-query"; import eventBus from "../../services/eventBus"; import { useSelectedProject } from "../../slices/apiDataManager"; +import { SpinnerLoader } from "../common/Loader"; const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, }) => { const queryClient = useQueryClient(); @@ -110,28 +111,41 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat return ( <> +
+ {/* Left side - Date */} +
+ Date: {formatUTCToLocalTime(todayDate)} +
+ + {/* Right side - Pending Attendance toggle */} +
+ setShowPending(e.target.checked)} + /> + +
+
-
- Date : {formatUTCToLocalTime(todayDate)} -
- setShowPending(e.target.checked)} - /> - -
-
{attLoading ? ( -
Loading...
+
+ +
) : currentItems?.length > 0 ? ( + <> @@ -223,50 +237,6 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat )}
- - - {!loading && finalFilteredData.length > ITEMS_PER_PAGE && ( - - )} ) : (
)}
+ {!loading && finalFilteredData.length > ITEMS_PER_PAGE && ( + + )} ); }; diff --git a/src/components/Activities/AttendcesLogs.jsx b/src/components/Activities/AttendcesLogs.jsx index 267bec1d..f1d8e008 100644 --- a/src/components/Activities/AttendcesLogs.jsx +++ b/src/components/Activities/AttendcesLogs.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useMemo, useCallback } from "react"; import moment from "moment"; import Avatar from "../common/Avatar"; -import { convertShortTime } from "../../utils/dateUtils"; +import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils"; import RenderAttendanceStatus from "./RenderAttendanceStatus"; import { useSelector, useDispatch } from "react-redux"; import DateRangePicker from "../common/DateRangePicker"; @@ -15,6 +15,8 @@ import AttendanceRepository from "../../repositories/AttendanceRepository"; import { useAttendancesLogs } from "../../hooks/useAttendance"; import { queryClient } from "../../layouts/AuthLayout"; import { ITEMS_PER_PAGE } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; +import { SpinnerLoader } from "../common/Loader"; const usePagination = (data, itemsPerPage) => { const [currentPage, setCurrentPage] = useState(1); @@ -38,17 +40,13 @@ const usePagination = (data, itemsPerPage) => { }; const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { - // const selectedProject = useSelector( - // (store) => store.localVariables.projectId - // ); const selectedProject = useSelectedProject(); const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); 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(); today.setHours(0, 0, 0, 0); @@ -71,57 +69,32 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { }; const sortByName = (a, b) => { - const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase(); - const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase(); - return nameA?.localeCompare(nameB); + const nameA = (a.firstName + a.lastName).toLowerCase(); + const nameB = (b.firstName + 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 = (data) => { + + 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) { @@ -131,28 +104,17 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { return acc; }, {}); - const sortedDates = Object.keys(groupedByDate).sort( - (a, b) => new Date(b) - new Date(a) - ); - - const finalData = sortedDates.flatMap((date) => groupedByDate[date]); - setProcessedData(finalData); - }; - - useEffect(() => { - filtering(data); + const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a)); + return sortedDates.flatMap((date) => groupedByDate[date]); }, [data, showPending]); - // New useEffect to handle search filtering const filteredSearchData = useMemo(() => { - if (!searchTerm) { - return processedData; - } - const lowercasedSearchTerm = searchTerm.toLowerCase(); - return processedData.filter((item) => { - const fullName = `${item.firstName} ${item.lastName}`.toLowerCase(); - return fullName.includes(lowercasedSearchTerm); - }); + if (!searchTerm) return processedData; + + const lowercased = searchTerm.toLowerCase(); + return processedData.filter((item) => + `${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased) + ); }, [processedData, searchTerm]); const { @@ -165,34 +127,27 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { useEffect(() => { resetPage(); - }, [filteredSearchData, resetPage]); + }, [filteredSearchData]); 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; } - const updatedAttendance = oldData.map((record) => - record.id === msg.response.id - ? { ...record, ...msg.response } - : record + return oldData.map((record) => + record.id === msg.response.id ? { ...record, ...msg.response } : record ); - filtering(updatedAttendance); - return updatedAttendance; }); resetPage(); } }, - [selectedProject, dateRange, filtering, resetPage] + [selectedProject, dateRange, resetPage] ); useEffect(() => { @@ -204,18 +159,10 @@ 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(); } }, - [selectedProject, dateRange, data, refetch] + [data, refetch] ); useEffect(() => { @@ -223,43 +170,56 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { return () => eventBus.off("employee", employeeHandler); }, [employeeHandler]); + + return ( <>
-
+ {/* Left Side - Date Picker */} +
-
- setShowPending(e.target.checked)} - /> - -
+
+ + {/* Right Side - Pending Attendance Switch */} +
+ setShowPending(e.target.checked)} + /> +
+ +
{isLoading ? (
-

Loading...

+
) : filteredSearchData?.length > 0 ? ( + @@ -286,9 +246,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) { @@ -298,8 +258,8 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { className="table-row-header" > @@ -314,7 +274,12 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { lastName={attendance.lastName} />
- + + navigate(`/employee/${attendance.employeeId}?for=attendance`) + } + className="text-heading text-truncate cursor-pointer" + > {attendance.firstName} {attendance.lastName} @@ -351,8 +316,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { ) : (
- No data available for the selected date range. Please Select - another date. + No attendance record found in selected date range.
)} @@ -380,9 +344,8 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { (pageNumber) => (
  • - - {moment(currentDate).format("DD-MM-YYYY")} + + {formatUTCToLocalTime(currentDate)}
    - - - - - - - - - - - - {currentItems?.map((att, index) => ( - - + - ) : ( -
    - - {searchTerm - ? "No results found for your search." - : "No Requests Found !"} - -
    - )} - {!loading && totalPages > 1 && ( - +
    + + + + ))} + +
    NameDateOrganization - Check-In - - Check-Out - Action
    - {convertShortTime(att.checkInTime)} + {att.requestedAt ? convertShortTime(att.checkOutTime) : "--"} + + {att.requestedBy ? () : (--)} + + {att?.requestedAt ? formatUTCToLocalTime(att.requestedAt, true) : "--"} + + +
    + ) : ( +
    + + {searchTerm + ? "No results found for your search." + : "No Requests Found !"} + +
    + )} +
    + {totalPages > 0 && ( + )} +
    ); }; diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx new file mode 100644 index 00000000..c6e9d004 --- /dev/null +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -0,0 +1,233 @@ + +import React, { useEffect, useMemo } from "react"; +import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense"; +import Error from "../common/Error"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Loader, { SpinnerLoader } from "../common/Loader"; +import { useForm, useFormContext } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { employee } from "../../data/masters"; +import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage"; +import { formatFigure } from "../../utils/appUtils"; + +const AdvancePaymentList = ({ employeeId, searchString }) => { + const { setBalance } = useAdvancePaymentContext(); + const { data, isError, isLoading, error, isFetching } = + useExpenseTransactions(employeeId, { enabled: !!employeeId }); + const records = Array.isArray(data) ? data : []; + + let currentBalance = 0; + const rowsWithBalance = records.map((r) => { + const isCredit = r.amount > 0; + const credit = isCredit ? r.amount : 0; + const debit = !isCredit ? Math.abs(r.amount) : 0; + currentBalance += credit - debit; + return { + id: r.id, + description: r.title || "-", + projectName: r.project?.name || "-", + createdAt: r.createdAt, + credit, + debit, + financeUId: r.financeUId, + balance: currentBalance, + }; + }); + + useEffect(() => { + if (!employeeId) { + setBalance(null); + return; + } + + if (rowsWithBalance.length > 0) { + setBalance(rowsWithBalance[rowsWithBalance.length - 1].balance); + } else { + setBalance(0); + } + }, [employeeId, data, setBalance]); + + if (!employeeId) { + return ( +
    +

    Please select an employee

    +
    + ); + } + + if (isLoading || isFetching) { + return ( +
    + +
    + ); + } + + if (isError) { + return ( +
    + {error?.status === 404 + ? "No advance payment transactions found." + : } +
    + ); + } + const columns = [ + { + key: "date", + label: ( + <> + Date + + ), + align: "text-start", + }, + { key: "description", label: "Description", align: "text-start" }, + + { + key: "credit", + label: ( + <> + Credit + + ), + align: "text-end", + }, + { + key: "debit", + label: ( + <> + Debit + + ), + align: "text-end", + }, + + { + key: "balance", + label: ( + <> + Balance + + ), + align: "text-end fw-bold", + }, + ]; + + // Handle empty records + if (rowsWithBalance.length === 0) { + return ( +
    + No advance payment records found. +
    + ); + } + const DecideCreditOrDebit = ({ financeUId }) => { + if (!financeUId) return null; + + const prefix = financeUId?.substring(0, 2).toUpperCase(); + + if (prefix === "PR") return +; + if (prefix === "EX") return -; + + return null; + }; + + return ( +
    + + + + {columns.map((col) => ( + + ))} + + + + {Array.isArray(data) && data.length > 0 ? ( + data.map((row) => ( + + {columns.map((col) => ( + + ))} + + )) + ) : ( + + + + )} + + + + + + + + +
    + {col.label} +
    + {col.key === "credit" ? ( + row.amount > 0 ? ( + {row.amount.toLocaleString("en-IN")} + ) : ( + "-" + ) + ) : col.key === "debit" ? ( + row.amount < 0 ? ( + + {Math.abs(row.amount).toLocaleString("en-IN")} + + ) : ( + "-" + ) + ) : col.key === "balance" ? ( +
    + {/* */} + + {formatFigure(row.currentBalance)} + +
    + ) : col.key === "date" ? ( + + {formatUTCToLocalTime(row.paidAt)} + + ) : ( +
    + + {row.project?.name || "-"} + + {row.title || "-"} +
    + )} +
    + No advance payment records found. +
    + {" "} +
    + Final Balance +
    +
    +
    + {currentBalance.toLocaleString("en-IN", { + style: "currency", + currency: "INR", + })} +
    +
    +
    + ); +}; + +export default AdvancePaymentList; diff --git a/src/components/AdvancePayment/AdvancePaymentList1.jsx b/src/components/AdvancePayment/AdvancePaymentList1.jsx new file mode 100644 index 00000000..e17cd1b1 --- /dev/null +++ b/src/components/AdvancePayment/AdvancePaymentList1.jsx @@ -0,0 +1,100 @@ +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) => ( +
    navigate(`/advance-payment/${r.id}`)} + style={{ cursor: "pointer" }}> + + + + {r.firstName} {r.lastName} + +
    + ), + }, + { + key: "jobRoleName", + label: "Job Role", + align: "text-start", + customRender: (r) => ( + + {r.jobRoleName} + + ), + }, + { + key: "balanceAmount", + label: "Balance (₹)", + align: "text-end", + customRender: (r) => ( + + {formatFigure(r.balanceAmount, { + // type: "currency", + currency: "INR", + })} + + ), + }, + ]; + + if (isLoading) return

    Loading...

    ; + if (isError) return

    {error.message}

    ; + + return ( +
    +
    + + + + {columns.map((col) => ( + + ))} + + + + + {rows.length > 0 ? ( + rows.map((row) => ( + + {columns.map((col) => ( + + ))} + + )) + ) : ( + + + + )} + +
    + {col.label} +
    + {col.customRender + ? col.customRender(row) + : col.getValue(row)} +
    + No Employees Found +
    +
    +
    + ) +} + +export default AdvancePaymentList1; diff --git a/src/components/Charts/HorizontalBarChart.jsx b/src/components/Charts/HorizontalBarChart.jsx index 82608da6..c28778fc 100644 --- a/src/components/Charts/HorizontalBarChart.jsx +++ b/src/components/Charts/HorizontalBarChart.jsx @@ -1,6 +1,7 @@ import React from "react"; import ReactApexChart from "react-apexcharts"; import PropTypes from "prop-types"; +import { SpinnerLoader } from "../common/Loader"; const HorizontalBarChart = ({ seriesData = [], @@ -23,8 +24,12 @@ const HorizontalBarChart = ({ if (loading) { return (
    - Loading chart... - {/* Replace this with a skeleton or spinner if you prefer */} +
    + +
    ); } diff --git a/src/components/Charts/LineChart.jsx b/src/components/Charts/LineChart.jsx index 6eb4840c..be8b5ebe 100644 --- a/src/components/Charts/LineChart.jsx +++ b/src/components/Charts/LineChart.jsx @@ -1,6 +1,7 @@ import React from "react"; import ReactApexChart from "react-apexcharts"; import PropTypes from "prop-types"; +import { SpinnerLoader } from "../common/Loader"; const LineChart = ({ seriesData = [], @@ -9,24 +10,28 @@ 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 ( -
    -
    - Loading chart... -
    - ); - } + if (loading) { + return ( +
    +
    + +
    +
    + ); + } - if (!hasValidData) { - return
    No data to display
    ; - } + if (!hasValidData) { + return
    No data to display
    ; + } const chartOptions = { chart: { @@ -129,16 +134,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; diff --git a/src/components/DailyProgressRport/TaskReportFilterPanel.jsx b/src/components/DailyProgressRport/TaskReportFilterPanel.jsx index 003339b4..8394a158 100644 --- a/src/components/DailyProgressRport/TaskReportFilterPanel.jsx +++ b/src/components/DailyProgressRport/TaskReportFilterPanel.jsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useCurrentService, useProjectInfra } from "../../hooks/useProjects"; +import { useCurrentService } from "../../hooks/useProjects"; import { useSelectedProject } from "../../slices/apiDataManager"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -10,15 +10,14 @@ 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 selectedProjec = useSelectedProject(); + const selectedProject = useSelectedProject(); const selectedService = useCurrentService(); - const { projectInfra, isLoading, error, isFetched } = useProjectInfra( - selectedProjec, - selectedService - ); + const { data } = useTaskFilter(selectedProject); + const methods = useForm({ resolver: zodResolver(TaskReportFilterSchema), defaultValues: TaskReportDefaultValue, @@ -30,58 +29,75 @@ const TaskReportFilterPanel = ({ handleFilter }) => { handleSubmit, formState: { errors }, } = methods; - + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }; const onSubmit = (formData) => { - console.log(formData) - const filterPayload = { - startDate:localToUtc(formData.startDate), - endDate:localToUtc(formData.endDate) - - } + const filterPayload = { + ...formData, + dateFrom: localToUtc(formData.dateFrom), + dateTo: localToUtc(formData.dateTo), + }; handleFilter(filterPayload); + closePanel(); }; - const onClear =()=>{ + const onClear = () => { setResetKey((prev) => prev + 1); - handleFilter(TaskReportDefaultValue) - reset(TaskReportDefaultValue) - } - + handleFilter(TaskReportDefaultValue); + reset(TaskReportDefaultValue); + closePanel(); + }; return (
    - {/*
    - -
    -
    - -
    */} +
    + +
    +
    + +
    +
    + +
    -
    +
    + {task.workItem.activityMaster?.activityName || "No Activity Name"} +
    +
    + {task.workItem.workArea?.floor?.building?.name} ›{" "} + {task.workItem.workArea?.floor?.floorName} ›{" "} + {task.workItem.workArea?.areaName} +
    +
    + {formatNumber(task.workItem.plannedWork)} + {`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}{formatUTCToLocalTime(task.assignmentDate)}{renderTeamMembers(task, idx)} +
    + {ReportTaskRights && !task.reportedDate && ( + + )} + {ApprovedTaskRights && task.reportedDate && !task.approvedBy && ( + + )} + +
    +
    + +
    + { + data?.data?.length > 0 && ( + + ) + } +
    ); }; diff --git a/src/components/DailyProgressRport/TaskRportScheam.jsx b/src/components/DailyProgressRport/TaskRportScheam.jsx index 11a79e6f..0cb38a77 100644 --- a/src/components/DailyProgressRport/TaskRportScheam.jsx +++ b/src/components/DailyProgressRport/TaskRportScheam.jsx @@ -1,15 +1,17 @@ import { z } from "zod"; export const TaskReportFilterSchema = z.object({ - // buildingIds: z.array(z.string()).optional(), - // floorIds: z.array(z.string()).optional(), - startDate: z.string().optional(), - endDate: z.string().optional(), + 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(), }); export const TaskReportDefaultValue = { - // buildingIds:[], - // floorIds:[], - startDate:null, - endDate:null + buildingIds:[], + floorIds:[], + activityIds:[], + dateFrom:null, + dateTo:null } \ No newline at end of file diff --git a/src/components/Dashboard/AttendanceChart.jsx b/src/components/Dashboard/AttendanceOverview.jsx similarity index 85% rename from src/components/Dashboard/AttendanceChart.jsx rename to src/components/Dashboard/AttendanceOverview.jsx index 67950d9a..77fbee51 100644 --- a/src/components/Dashboard/AttendanceChart.jsx +++ b/src/components/Dashboard/AttendanceOverview.jsx @@ -4,6 +4,7 @@ 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); @@ -99,9 +100,9 @@ const AttendanceOverview = () => { }; return ( -
    +
    {/* Header */} -
    +
    Attendance Overview

    Role-wise present count

    @@ -117,18 +118,16 @@ const AttendanceOverview = () => {
    {/* Content */} -
    +
    {loading ? ( - + ) : error ? (

    {error}

    + ) : attendanceOverviewData.length === 0 || + attendanceOverviewData.every((item) => item.present === 0) ? ( +
    No data found
    ) : view === "chart" ? (
    { style={{ position: "sticky", top: 0, zIndex: 1 }} > - - Role - + Role {dates.map((date, idx) => ( { ))} - {roles.map((role) => ( {role} {tableData.map((row, idx) => { const value = row[role]; - const cellStyle = - value > 0 ? { backgroundColor: "#d5d5d5" } : {}; + const cellStyle = value > 0 ? { backgroundColor: "#d5d5d5" } : {}; return ( {value} diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 311fa763..ad5ead8c 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -12,13 +12,13 @@ import Teams from "./Teams"; import TasksCard from "./Tasks"; import ProjectCompletionChart from "./ProjectCompletionChart"; import ProjectProgressChart from "./ProjectProgressChart"; -import ProjectOverview from "../Project/ProjectOverview"; -import AttendanceOverview from "./AttendanceChart"; +import AttendanceOverview from "./AttendanceOverview"; +import ExpenseAnalysis from "./ExpenseAnalysis"; +import ExpenseStatus from "./ExpenseStatus"; +import ExpenseByProject from "./ExpenseByProject"; +import ProjectStatistics from "../Project/ProjectStatistics"; 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); @@ -29,16 +29,16 @@ const Dashboard = () => {
    {isAllProjectsSelected && (
    - +
    )}
    - +
    - +
    {isAllProjectsSelected && ( @@ -46,21 +46,34 @@ const Dashboard = () => {
    )} - - {!isAllProjectsSelected && ( -
    - -
    - )} -
    {!isAllProjectsSelected && ( -
    - {/* ✅ Removed unnecessary projectId prop */} +
    +
    )} + + {!isAllProjectsSelected && ( +
    + +
    + )} +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    ); diff --git a/src/components/Dashboard/DashboardSkeleton.jsx b/src/components/Dashboard/DashboardSkeleton.jsx new file mode 100644 index 00000000..89d25636 --- /dev/null +++ b/src/components/Dashboard/DashboardSkeleton.jsx @@ -0,0 +1,110 @@ +import React from "react"; + +const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => ( +
    +); + +const skeletonStyle = ` +@keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} +`; + +export const ProjectCardSkeleton = () => { + return ( + <> + {/* Inject animation CSS once */} + + +
    + {/* Header */} +
    +
    + {" "} + Projects +
    +
    + + {/* Skeleton body */} +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; + +export const TeamsSkeleton = () => { + return ( + <> + + +
    + {/* Header */} +
    +
    + Teams +
    +
    + + {/* Skeleton Body */} +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; +export const TasksSkeleton = () => { + return ( + <> + + +
    + {/* Header */} +
    +
    + Tasks +
    +
    + + {/* Skeleton Body */} +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; diff --git a/src/components/Dashboard/ExpenseAnalysis.jsx b/src/components/Dashboard/ExpenseAnalysis.jsx new file mode 100644 index 00000000..3c6b0d08 --- /dev/null +++ b/src/components/Dashboard/ExpenseAnalysis.jsx @@ -0,0 +1,180 @@ +import React, { useEffect, useMemo, useState } 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"]); + + const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis( + projectId, + startDate ? localToUtc(startDate) : null, + endDate ? localToUtc(endDate) : null + ); + + if (isError) return
    {error.message}
    ; + + const report = data?.report ?? []; + const { labels, series, total } = useMemo(() => { + const labels = report.map((item) => item.projectName); + const series = report.map((item) => item.totalApprovedAmount || 0); + const total = formatCurrency(data?.totalAmount || 0); + return { labels, series, total }; + }, [report, data?.totalAmount]); + + const donutOptions = { + chart: { type: "donut" }, + labels, + legend: { show: false }, + dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` }, + colors: flatColors, + plotOptions: { + pie: { + donut: { + size: "70%", + labels: { + show: true, + total: { + show: true, + label: "Total", + fontSize: "16px", + formatter: () => `${total}`, + }, + }, + }, + }, + }, + responsive: [ + { + breakpoint: 576, // mobile breakpoint + options: { + chart: { width: "100%" }, + }, + }, + ], + }; + + return ( + <> +
    +
    +
    Expense Breakdown
    +

    {projectName}

    +
    + +
    + + + +
    +
    + +
    + {isLoading && ( +
    + +
    + )} + + {!isLoading && report.length === 0 && ( +
    + No data found +
    + )} + + {!isLoading && report.length > 0 && ( + <> + {isFetching && ( +
    + Loading... +
    + )} + +
    + {/* Chart Column */} +
    + +
    + + {/* Data/Legend Column */} +
    +
    + {report.map((item, idx) => ( +
    +
    + + {item.projectName} + + + {formatCurrency(item.totalApprovedAmount)} + +
    +
    + + ))} +
    +
    +
    + + )} +
    + + ); +}; + +export default ExpenseAnalysis; diff --git a/src/components/Dashboard/ExpenseByProject.jsx b/src/components/Dashboard/ExpenseByProject.jsx new file mode 100644 index 00000000..c589bc45 --- /dev/null +++ b/src/components/Dashboard/ExpenseByProject.jsx @@ -0,0 +1,188 @@ +import React, { useState, useEffect } from "react"; +import Chart from "react-apexcharts"; +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 { data: expenseApiData, isLoading } = useExpenseDataByProject( + projectId, + selectedType, + 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) => + formatDate_DayMonth(item.monthName, item.year) + ); + const data = expenseApiData.map((item) => item.total); + setChartData({ categories, data }); + } else { + setChartData({ categories: [], data: [] }); + } + }, [expenseApiData]); + + const getSelectedTypeName = () => { + if (!selectedType) return "All Types"; + const found = expenseCategories?.find((t) => t.id === selectedType); + return found ? found.name : "All Types"; + }; + + const options = { + chart: { type: "bar", toolbar: { show: false } }, + plotOptions: { + bar: { horizontal: false, columnWidth: "55%", borderRadius: 4 }, + }, + dataLabels: { enabled: true, formatter: (val) => formatCurrency(val) }, + xaxis: { + categories: chartData.categories, + labels: { style: { fontSize: "12px" }, rotate: -45 }, + }, + yaxis: { + labels: { + formatter: (val) => formatCurrency(val), + style: { fontSize: "12px", colors: "#555" }, + }, + }, + tooltip: { + y: { + formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`, + }, + }, + + annotations: { xaxis: [{ x: 0, strokeDashArray: 0 }] }, + fill: { opacity: 1 }, + colors: ["#2196f3"], + }; + + const series = [ + { + name: getSelectedTypeName(), + data: chartData.data, + }, + ]; + + return ( +
    + {/* Header */} +
    +
    +
    +
    Monthly Expense -
    +

    {projectName}

    +
    +
    + +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    + + {/* Range Buttons + Expense Dropdown */} +
    + {["1M", "3M", "6M", "12M", "All"].map((item) => ( + + ))} + {viewMode === "Category" && ( + + )} +
    +
    + + {/* Chart */} +
    + {isLoading ? ( +
    + +
    + ) : !expenseApiData || expenseApiData.length === 0 ? ( +
    No data found
    + ) : ( + + )} + +
    + +
    + ); +}; + +export default ExpenseByProject; diff --git a/src/components/Dashboard/ExpenseStatus.jsx b/src/components/Dashboard/ExpenseStatus.jsx new file mode 100644 index 00000000..a45fb343 --- /dev/null +++ b/src/components/Dashboard/ExpenseStatus.jsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from "react"; +import { useExpense } from "../../hooks/useExpense"; +import { useExpenseStatus } from "../../hooks/useDashboard_Data"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { useProjectName } from "../../hooks/useProjects"; +import { countDigit, formatCurrency } from "../../utils/appUtils"; +import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; + + +const ExpenseStatus = () => { + const [projectName, setProjectName] = useState("All Project"); + const selectedProject = useSelectedProject(); + const { projectNames, loading } = useProjectName(); + const { data, isPending, error } = useExpenseStatus(selectedProject); + const navigate = useNavigate(); + const isManageExpense = useHasUserPermission(EXPENSE_MANAGE) + + useEffect(() => { + if (selectedProject && projectNames?.length) { + const project = projectNames.find((p) => p.id === selectedProject); + setProjectName(project?.name || "All Project"); + } else { + setProjectName("All Project"); + } + }, [projectNames, selectedProject]); + + const handleNavigate = (status) => { + if (selectedProject) { + navigate(`/expenses/${status}/${selectedProject}`); + } else { + navigate(`/expenses/${status}`); + } + }; + return ( + <> +
    +
    +
    Expense - By Status
    +

    {projectName}

    +
    +
    + +
    + +
    + {[ + { + title: "Pending Payment", + count: data?.processPending?.count || 0, + amount: data?.processPending?.totalAmount || 0, + icon: "bx bx-rupee", + iconColor: "text-primary", + status: EXPENSE_STATUS.payment_pending, + }, + { + title: "Pending Approve", + count: data?.approvePending?.count || 0, + amount: data?.approvePending?.totalAmount || 0, + icon: "fa-solid fa-check", + iconColor: "text-warning", + status: EXPENSE_STATUS.approve_pending, + }, + { + title: "Pending Review", + count: data?.reviewPending?.count || 0, + amount: data?.reviewPending?.totalAmount || 0, + icon: "bx bx-search-alt-2", + iconColor: "text-secondary", + status: EXPENSE_STATUS.review_pending, + }, + { + title: "Draft", + count: data?.draft?.count || 0, + amount: data?.draft?.totalAmount || 0, + icon: "bx bx-file-blank", + iconColor: "text-info", + status: EXPENSE_STATUS.daft, + }, + ].map((item, idx) => ( +
    handleNavigate(item?.status)} + > +
    +
    + + + +
    +
    +
    + {item?.title} + {item?.amount ? ( + + {formatCurrency(item?.amount)} + + ) : ( + {formatCurrency(0)} + )} +
    +
    + = 3 ? "text-xl" : "text-xl" + } text-gray-500`} + > + {item?.count || 0} + + + + +
    +
    +
    +
    + ))} +
    + +
    + {isManageExpense && ( +
    handleNavigate(EXPENSE_STATUS.payment_processed)} + > +
    + 3 ? "text-base" : "text-lg" + }`} + > + Project Spendings: + {" "} + + (All Processed Payments) + +
    +
    + 3 ? "text-xl" : "text-3xl" + } text-md`} + > + {formatCurrency(data?.totalAmount || 0)} + + + + +
    +
    + )} +
    +
    + + ); +}; + +export default ExpenseStatus; diff --git a/src/components/Dashboard/ProjectCompletionChart.jsx b/src/components/Dashboard/ProjectCompletionChart.jsx index 8ce4b13a..f1f63936 100644 --- a/src/components/Dashboard/ProjectCompletionChart.jsx +++ b/src/components/Dashboard/ProjectCompletionChart.jsx @@ -1,17 +1,23 @@ -import React from "react"; +import React, { useState } 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 { projects, loading } = useProjects(); - - // Bar chart logic + const [currentPage, setCurrentPage] = useState(1); + const { + data: projects, + isLoading: loading, + isError, + error, + } = useProjectCompletionStatus(); const projectNames = projects?.map((p) => p.name) || []; const projectProgress = projects?.map((p) => { const completed = p.completedWork || 0; const planned = p.plannedWork || 1; - const percent = (completed / planned) * 100; + const percent = planned ? (completed / planned) * 100 : 0; return Math.min(Math.round(percent), 100); }) || []; @@ -23,7 +29,7 @@ const ProjectCompletionChart = () => {

    Projects Completion Status

    -
    +
    { const { @@ -8,6 +11,7 @@ const Projects = () => { isLoading, isError, error, + isFetching, refetch, } = useDashboardProjectsCardData(); @@ -23,7 +27,7 @@ const Projects = () => { const totalProjects = projectsCardData?.totalProjects ?? 0; const ongoingProjects = projectsCardData?.ongoingProjects ?? 0; - + if (isLoading) return ; return (
    @@ -33,24 +37,41 @@ const Projects = () => {
    - {isLoading ? ( -
    -
    - Loading... -
    -
    - ) : isError ? ( -
    - {error?.message || "Error loading data"} + {isError ? ( +
    + + + {error?.message || "Unable to load data at the moment."} + + + {" "} + Retry +
    ) : (
    -

    {totalProjects.toLocaleString()}

    +

    + {formatFigure(totalProjects ?? 0, { + notation: "compact", + })} +

    + Total
    -

    {ongoingProjects.toLocaleString()}

    +

    + {formatFigure(ongoingProjects ?? 0, { + notation: "compact", + })} +

    Ongoing
    diff --git a/src/components/Dashboard/Tasks.jsx b/src/components/Dashboard/Tasks.jsx index 56eb00f0..76072cfb 100644 --- a/src/components/Dashboard/Tasks.jsx +++ b/src/components/Dashboard/Tasks.jsx @@ -1,6 +1,8 @@ import React from "react"; import { useSelectedProject } from "../../slices/apiDataManager"; import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data"; +import { TasksSkeleton } from "./DashboardSkeleton"; +import { formatCurrency, formatFigure } from "../../utils/appUtils"; const TasksCard = () => { const projectId = useSelectedProject(); @@ -10,42 +12,57 @@ const TasksCard = () => { isLoading, isError, error, + isFetching, + refetch, } = useDashboardTasksCardData(projectId); - + if (isLoading) return ; return ( -
    +
    + {/* Header */}
    Tasks
    - {isLoading ? ( - // Loader while fetching -
    -
    - Loading... -
    -
    - ) : isError ? ( - // Show error -
    - {error?.message || "Error loading data"} + {isError ? ( +
    + + + {error?.message || "Unable to load data at the moment."} + + + + Retry +
    ) : ( - // Show data
    + {/* Total Tasks */}
    -

    - {tasksCardData?.totalTasks?.toLocaleString() ?? 0} +

    + {formatFigure(tasksCardData?.totalTasks ?? 0, { + notation: "compact", + })}

    - Total + Total
    + + {/* Completed Tasks */}
    -

    - {tasksCardData?.completedTasks?.toLocaleString() ?? 0} +

    + {formatFigure(tasksCardData?.completedTasks ?? 0, { + notation: "compact", + })}

    - Completed + Completed
    )} diff --git a/src/components/Dashboard/Teams.jsx b/src/components/Dashboard/Teams.jsx index 9e9d31f9..05888e77 100644 --- a/src/components/Dashboard/Teams.jsx +++ b/src/components/Dashboard/Teams.jsx @@ -4,16 +4,20 @@ import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data"; import eventBus from "../../services/eventBus"; import { useQueryClient } from "@tanstack/react-query"; import { useSelectedProject } from "../../slices/apiDataManager"; +import { TeamsSkeleton } from "./DashboardSkeleton"; +import { formatFigure } from "../../utils/appUtils"; const Teams = () => { const queryClient = useQueryClient(); - const projectId = useSelectedProject() + const projectId = useSelectedProject(); const { data: teamsCardData, isLoading, isError, error, + isFetching, + refetch, } = useDashboardTeamsCardData(projectId); // Handle real-time updates via eventBus @@ -40,6 +44,7 @@ const Teams = () => { const inToday = teamsCardData?.inToday ?? 0; const totalEmployees = teamsCardData?.totalEmployees ?? 0; + if (isLoading) return ; return (
    @@ -48,24 +53,41 @@ const Teams = () => {
    - {isLoading ? ( -
    -
    - Loading... -
    -
    - ) : isError ? ( -
    - {error?.message || "Error loading data"} + {isError ? ( +
    + + + + {error?.message || "Unable to load data at the moment."} + + + {" "} + Retry +
    ) : (
    -

    {totalEmployees.toLocaleString()}

    +

    + {formatFigure(totalEmployees ?? 0, { + notation: "compact", + })} +

    Total Employees
    -

    {inToday.toLocaleString()}

    +

    + {formatFigure(inToday ?? 0, { + notation: "compact", + })} +

    In Today
    diff --git a/src/components/Directory/CardViewContact.jsx b/src/components/Directory/CardViewContact.jsx index 8e6c8068..0b446da0 100644 --- a/src/components/Directory/CardViewContact.jsx +++ b/src/components/Directory/CardViewContact.jsx @@ -61,7 +61,7 @@ const CardViewContact = ({ (contact?.name || "").trim().split(" ")[1]?.charAt(0) || "" } />{" "} - {contact?.name} + {contact?.name}
    {IsActive && ( diff --git a/src/components/Directory/ContactFilterChips.jsx b/src/components/Directory/ContactFilterChips.jsx new file mode 100644 index 00000000..d321afa3 --- /dev/null +++ b/src/components/Directory/ContactFilterChips.jsx @@ -0,0 +1,56 @@ +import React, { useMemo } from "react"; + +const ContactFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => { + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const addGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((i) => i.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + addGroup(filters.bucketIds, data.buckets, "Buckets", "bucketIds"); + addGroup(filters.categoryIds, data.contactCategories, "Category", "categoryIds"); + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
    + {filterChips.map((chipGroup) => ( +
    + {chipGroup.label}: + {chipGroup.items.map((item) => ( + + {item.name} +
    + ))} + +
    + ); +}; + +export default ContactFilterChips; \ No newline at end of file diff --git a/src/components/Directory/ListViewContact.jsx b/src/components/Directory/ListViewContact.jsx index 16e737a7..f4f15531 100644 --- a/src/components/Directory/ListViewContact.jsx +++ b/src/components/Directory/ListViewContact.jsx @@ -160,8 +160,7 @@ const ListViewContact = ({ data, Pagination }) => {
    ) : ( { No contacts found @@ -188,13 +187,13 @@ const ListViewContact = ({ data, Pagination }) => { )} - {Pagination && ( -
    - {Pagination} -
    - )}
    + {Pagination && ( +
    + {Pagination} +
    + )}
    ); diff --git a/src/components/Directory/ManageContact.jsx b/src/components/Directory/ManageContact.jsx index 56d45e32..f55ed858 100644 --- a/src/components/Directory/ManageContact.jsx +++ b/src/components/Directory/ManageContact.jsx @@ -23,7 +23,7 @@ import Label from "../common/Label"; const ManageContact = ({ contactId, closeModal }) => { // fetch master data const { buckets, loading: bucketsLoaging } = useBuckets(); - const { data:projects, loading: projectLoading } = useProjects(); + const { data: projects, loading: projectLoading } = useProjects(); const { contactCategory, loading: contactCategoryLoading } = useContactCategory(); const { organizationList } = useOrganization(); @@ -205,12 +205,12 @@ const ManageContact = ({ contactId, closeModal }) => { - setValue("organization", val, { shouldValidate: true })} - error={errors.organization?.message} -/> + setValue("organization", val, { shouldValidate: true })} + error={errors.organization?.message} + />
    @@ -408,6 +408,7 @@ const ManageContact = ({ contactId, closeModal }) => { label="Tags" options={contactTags} isRequired={true} + placeholder="Enter Tag" /> {errors.tags && ( {errors.tags.message} @@ -482,7 +483,7 @@ const ManageContact = ({ contactId, closeModal }) => { - +
    diff --git a/src/components/Directory/NoteCardDirectoryEditable.jsx b/src/components/Directory/NoteCardDirectoryEditable.jsx index 5ef8d08f..bc6ff517 100644 --- a/src/components/Directory/NoteCardDirectoryEditable.jsx +++ b/src/components/Directory/NoteCardDirectoryEditable.jsx @@ -87,7 +87,7 @@ const NoteCardDirectoryEditable = ({ />
    contactProfile(noteItem.contactId)} > @@ -98,7 +98,7 @@ const NoteCardDirectoryEditable = ({
    -
    +
    by{" "} @@ -184,7 +184,7 @@ const NoteCardDirectoryEditable = ({ ) : (
    )} diff --git a/src/components/Directory/NoteFilterChips.jsx b/src/components/Directory/NoteFilterChips.jsx new file mode 100644 index 00000000..5569a460 --- /dev/null +++ b/src/components/Directory/NoteFilterChips.jsx @@ -0,0 +1,79 @@ +import React, { useMemo } from "react"; +import moment from "moment"; + +const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Normalize data (in case it’s wrapped in .data) + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + // Build chips dynamically + buildGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds"); + buildGroup(filters.organizations, data.organizations, "Organization", "organizations"); + + // Example: Add date range if you ever add in future + if (filters.startDate || filters.endDate) { + const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : ""; + const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
    +
    +
    + {filterChips.map((chip) => ( +
    + {chip.label}: +
    + {chip.items.map((item) => ( + + {item.name} +
    +
    + ))} +
    +
    +
    + ); +}; + +export default NoteFilterChips; \ No newline at end of file diff --git a/src/components/Documents/DocumentFilterChips.jsx b/src/components/Documents/DocumentFilterChips.jsx new file mode 100644 index 00000000..e6b56d7c --- /dev/null +++ b/src/components/Documents/DocumentFilterChips.jsx @@ -0,0 +1,94 @@ +import React, { useMemo } from "react"; +import moment from "moment"; + +const DocumentFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Normalize structure: handle both "filterData.data" and plain "filterData" + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + // Build chips using normalized data + buildGroup(filters.uploadedByIds, data.uploadedBy || [], "Uploaded By", "uploadedByIds"); + buildGroup(filters.documentCategoryIds, data.documentCategory || [], "Category", "documentCategoryIds"); + buildGroup(filters.documentTypeIds, data.documentType || [], "Type", "documentTypeIds"); + buildGroup(filters.documentTagIds, data.documentTag || [], "Tags", "documentTagIds"); + + if (filters.statusIds?.length) { + const items = filters.statusIds.map((status) => ({ + id: status, + name: + status === true + ? "Verified" + : status === false + ? "Rejected" + : "Pending", + })); + chips.push({ key: "statusIds", label: "Status", items }); + } + + if (filters.startDate || filters.endDate) { + const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : ""; + const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + + return ( +
    +
    +
    + {filterChips.map((chip) => ( +
    + {chip.label}: +
    + {chip.items.map((item) => ( + + {item.name} +
    +
    + ))} +
    +
    +
    + ); +}; + +export default DocumentFilterChips; diff --git a/src/components/Documents/DocumentFilterPanel.jsx b/src/components/Documents/DocumentFilterPanel.jsx index 15a2cbf1..04f54956 100644 --- a/src/components/Documents/DocumentFilterPanel.jsx +++ b/src/components/Documents/DocumentFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState, useMemo, useImperativeHandle, forwardRef } from "react"; import { useDocumentFilterEntities } from "../../hooks/useDocument"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -9,198 +9,240 @@ import { import { DateRangePicker1 } from "../common/DateRangePicker"; import SelectMultiple from "../common/SelectMultiple"; import moment from "moment"; +import { useParams } from "react-router-dom"; -const DocumentFilterPanel = ({ entityTypeId, onApply }) => { - const [resetKey, setResetKey] = useState(0); +const DocumentFilterPanel = forwardRef( + ({ entityTypeId, onApply, setFilterdata }, ref) => { + const [resetKey, setResetKey] = useState(0); + const { status } = useParams(); - const { data, isError, isLoading, error } = - useDocumentFilterEntities(entityTypeId); + const { data, isError, isLoading, error } = + useDocumentFilterEntities(entityTypeId); - const methods = useForm({ - resolver: zodResolver(DocumentFilterSchema), - defaultValues: DocumentFilterDefaultValues, - }); + useEffect(() => { + return () => { + closePanel(); + }; + }, []); - const { handleSubmit, reset, setValue, watch } = methods; + //changes - // Watch values from form - const isUploadedAt = watch("isUploadedAt"); - const isVerified = watch("isVerified"); + const dynamicDocumentFilterDefaultValues = useMemo(() => { + return { + ...DocumentFilterDefaultValues, + uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [], + documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [], + documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [], + documentTagIds: DocumentFilterDefaultValues.documentTagIds || [], + startDate: DocumentFilterDefaultValues.startDate, + endDate: DocumentFilterDefaultValues.endDate, + }; - // Close the offcanvas (bootstrap specific) - const closePanel = () => { - document.querySelector(".offcanvas.show .btn-close")?.click(); - }; + }, [status]); - const onSubmit = (values) => { - onApply({ - ...values, - startDate: values.startDate - ? moment.utc(values.startDate, "DD-MM-YYYY").toISOString() - : null, - endDate: values.endDate - ? moment.utc(values.endDate, "DD-MM-YYYY").toISOString() - : null, + const methods = useForm({ + resolver: zodResolver(DocumentFilterSchema), + defaultValues: dynamicDocumentFilterDefaultValues, }); - closePanel(); - }; - const onClear = () => { - reset(DocumentFilterDefaultValues); - setResetKey((prev) => prev + 1); - onApply(DocumentFilterDefaultValues); - closePanel(); - }; + const { handleSubmit, reset, setValue, watch } = methods; - if (isLoading) return
    Loading...
    ; - if (isError) - return
    Error: {error?.message || "Something went wrong!"}
    ; + // Watch values from form + const isUploadedAt = watch("isUploadedAt"); + const isVerified = watch("isVerified"); - const { - uploadedBy = [], - documentCategory = [], - documentType = [], - documentTag = [], - } = data?.data || {}; + // Close the offcanvas (bootstrap specific) + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); + }; - return ( - -
    - {/* Date Range Section */} -
    -
    - -
    - - + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] }); + } + }, + getValues: methods.getValues, // optional, to read current filter state + })); + + //changes + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + + const onSubmit = (values) => { + onApply({ + ...values, + startDate: values.startDate + ? moment.utc(values.startDate, "DD-MM-YYYY").toISOString() + : null, + endDate: values.endDate + ? moment.utc(values.endDate, "DD-MM-YYYY").toISOString() + : null, + }); + // closePanel(); + }; + + const onClear = () => { + reset(DocumentFilterDefaultValues); + setResetKey((prev) => prev + 1); + onApply(DocumentFilterDefaultValues); + // closePanel(); + }; + + if (isLoading) return
    Loading...
    ; + if (isError) + return
    Error: {error?.message || "Something went wrong!"}
    ; + + const { + uploadedBy = [], + documentCategory = [], + documentType = [], + documentTag = [], + } = data?.data || {}; + + + + return ( + + + {/* Date Range Section */} +
    +
    + +
    + + +
    +
    + + +
    + + {/* Dropdown Filters */} +
    + + + + +
    + + {/* Status Filter */} +
    + +
    + + + + +
    - -
    - - {/* Dropdown Filters */} -
    - - - - -
    - - {/* Status Filter */} -
    - -
    - - - - - + {/* Footer Buttons */} +
    + +
    -
    - - {/* Footer Buttons */} -
    - - -
    - - - ); -}; + + + ); + }); export default DocumentFilterPanel; diff --git a/src/components/Documents/DocumentVersionList.jsx b/src/components/Documents/DocumentVersionList.jsx index ea1bdc13..9061c52c 100644 --- a/src/components/Documents/DocumentVersionList.jsx +++ b/src/components/Documents/DocumentVersionList.jsx @@ -74,7 +74,7 @@ const DocumentVersionList = ({ firstName={currentDoc.uploadedBy?.firstName} lastName={currentDoc.uploadedBy?.lastName} /> - + {`${currentDoc.uploadedBy?.firstName ?? ""} ${currentDoc.uploadedBy?.lastName ?? ""}`.trim() || "N/A"} @@ -196,7 +196,7 @@ const DocumentVersionList = ({ firstName={document.uploadedBy?.firstName} lastName={document.uploadedBy?.lastName} /> - + {`${document.uploadedBy?.firstName ?? ""} ${document.uploadedBy?.lastName ?? ""}`.trim() || "N/A"} @@ -216,7 +216,7 @@ const DocumentVersionList = ({ firstName={document.verifiedBy?.firstName} lastName={document.verifiedBy?.lastName} /> - + {`${document.verifiedBy?.firstName ?? ""} ${document.verifiedBy?.lastName ?? ""}`.trim() || "N/A"} diff --git a/src/components/Documents/Documents.jsx b/src/components/Documents/Documents.jsx index 8210bf58..930ceb8c 100644 --- a/src/components/Documents/Documents.jsx +++ b/src/components/Documents/Documents.jsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; import GlobalModel from "../common/GlobalModel"; import NewDocument from "./ManageDocument"; import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants"; @@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument"; import DocumentViewerModal from "./DocumentViewerModal"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useProfile } from "../../hooks/useProfile"; +import DocumentFilterChips from "./DocumentFilterChips"; // Context export const DocumentContext = createContext(); @@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => { const [isSelf, setIsSelf] = useState(false); const [searchText, setSearchText] = useState(""); const [isActive, setIsActive] = useState(true); - const [filters, setFilter] = useState(); + const [filters, setFilter] = useState(DocumentFilterDefaultValues); const [isRefetching, setIsRefetching] = useState(false); const [refetchFn, setRefetchFn] = useState(null); const [DocumentEntity, setDocumentEntity] = useState(Document_Entity); const { employeeId } = useParams(); const [OpenDocument, setOpenDocument] = useState(false); + const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues); + const updatedRef = useRef(); const [ManageDoc, setManageDoc] = useState({ document: null, isOpen: false, @@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => { setShowTrigger(true); setOffcanvasContent( "Document Filters", - + ); return () => { @@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => { setDocumentEntity(Document_Entity); } }, [Document_Entity]); + + + const removeFilterChip = (key, id) => { + const updatedFilters = { ...filters }; + if (Array.isArray(updatedFilters[key])) { + updatedFilters[key] = updatedFilters[key].filter((v) => v !== id); + updatedRef.current?.resetFieldValue(key,updatedFilters[key]); + } + else if (key === "dateRange") { + updatedFilters.startDate = null; + updatedFilters.endDate = null; + updatedRef.current?.resetFieldValue("startDate",null); + updatedRef.current?.resetFieldValue("endDate",null); + } + else { + updatedFilters[key] = null; + } + setFilter(updatedFilters); + return updatedFilters; + }; + return ( -
    -
    +
    +
    +
    {/* Search */} -
    +
    {" "} {
    -
    +
    {(isSelf || canUploadDocument) && (
    ), getValue: (e) => - `${e.uploadedBy?.firstName ?? ""} ${ - e.uploadedBy?.lastName ?? "" - }`.trim() || "N/A", + `${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? "" + }`.trim() || "N/A", }, { key: "uploadedAt", @@ -217,7 +215,7 @@ const DocumentsList = ({ } > - {(isSelf || canModifyDocument) && ( + {(isSelf || canModifyDocument) && ( @@ -226,7 +224,7 @@ const DocumentsList = ({ > )} - {(isSelf || canDeleteDocument) && ( + {(isSelf || canDeleteDocument) && ( { diff --git a/src/components/Employee/EmpActivities.jsx b/src/components/Employee/EmpActivities.jsx index e79b181d..8d23b68a 100644 --- a/src/components/Employee/EmpActivities.jsx +++ b/src/components/Employee/EmpActivities.jsx @@ -12,13 +12,13 @@ const EmpActivities = ({ employee }) => { const { data, -isError, -isLoading, -error, + isError, + isLoading, + error, refetch, - } = useProjectTasksByEmployee(employee?.id,dateRange.startDate,dateRange.endDate); + } = useProjectTasksByEmployee(employee?.id, dateRange.startDate, dateRange.endDate); - if(isLoading) return
    Loading...
    + if (isLoading) return
    Loading...
    return ( <>
    @@ -31,28 +31,28 @@ error, />
      - {data?.map((activity)=>( -
    • - -
      -
      -
      {activity.projectName}
      - - {useFormattedDate(activity.assignmentDate, "dd-MMM-yyyy")} - + {data?.map((activity) => ( +
    • + +
      +
      +
      {activity.projectName}
      + + {useFormattedDate(activity.assignmentDate, "dd-MMM-yyyy")} + +
      +

      Activity:{activity.activityName}

      +

      + Location: {activity.location} +

      +

      + Planned: {activity.plannedTask} + Completed : {activity.completedTask} +

      -

      Activity:{activity.activityName}

      -

      - Location: {activity.location} -

      -

      - Planned: {activity.plannedTask} - Completed : {activity.completedTask} -

      -
    - + ))} - + {/*
  • diff --git a/src/components/Employee/EmpAttendance.jsx b/src/components/Employee/EmpAttendance.jsx index b630ac7f..481cd03f 100644 --- a/src/components/Employee/EmpAttendance.jsx +++ b/src/components/Employee/EmpAttendance.jsx @@ -15,6 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { localToUtc } from "../../utils/appUtils"; import { useParams } from "react-router-dom"; +import { SpinnerLoader } from "../common/Loader"; const EmpAttendance = () => { const { employeeId } = useParams(); @@ -51,7 +52,6 @@ const EmpAttendance = () => { new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime() ); - console.log(sorted); const { currentPage, totalPages, currentItems, paginate } = usePagination( sorted, @@ -83,21 +83,19 @@ const EmpAttendance = () => {
  • -
    - refetch()} - /> -
    - {!loading && data.length === 0 && No employee logs} + {!loading && data.length === 0 && ( +
    No employee logs
    + )} + {isError &&
    {error.message}
    } - {loading && !data &&
    Loading...
    } + {loading && ( +
    + +
    + )} + {data && data.length > 0 && ( @@ -180,9 +178,8 @@ const EmpAttendance = () => { {[...Array(totalPages)].map((_, index) => (
  • ))}
  • + + + + + + {/* Manage Reporting Modal */} + {showManageReportingModal && ( + setShowManageReportingModal(false)} + > + setShowManageReportingModal(false)} + /> + + )} + + ); +}; + +export default EmpReportingManager; + diff --git a/src/components/Employee/EmployeeSchema.jsx b/src/components/Employee/EmployeeSchema.jsx index ba540ef4..b8c9f37a 100644 --- a/src/components/Employee/EmployeeSchema.jsx +++ b/src/components/Employee/EmployeeSchema.jsx @@ -3,8 +3,8 @@ import { z } from "zod" const mobileNumberRegex = /^[0-9]\d{9}$/; -export const employeeSchema = - z.object({ +export const employeeSchema = + z.object({ firstName: z.string().min(1, { message: "First Name is required" }), middleName: z.string().optional(), lastName: z.string().min(1, { message: "Last Name is required" }), @@ -90,35 +90,46 @@ export const employeeSchema = .min(1, { message: "Phone Number is required" }) .regex(mobileNumberRegex, { message: "Invalid phone number " }), jobRoleId: z.string().min(1, { message: "Role is required" }), - organizationId:z.string().min(1,{message:"Organization is required"}), - hasApplicationAccess:z.boolean().default(false), + organizationId: z.string().min(1, { message: "Organization is required" }), + hasApplicationAccess: z.boolean().default(false), }).refine((data) => { - if (data.hasApplicationAccess) { - return data.email && data.email.trim() !== ""; - } - return true; -}, { - message: "Email is required when employee has access", - path: ["email"], + if (data.hasApplicationAccess) { + return data.email && data.email.trim() !== ""; + } + return true; + }, { + message: "Email is required when employee has access", + path: ["email"], + }); + + +export const defatEmployeeObj = { + firstName: "", + middleName: "", + lastName: "", + email: "", + currentAddress: "", + birthDate: "", + joiningDate: "", + emergencyPhoneNumber: "", + emergencyContactPerson: "", + aadharNumber: "", + gender: "", + panNumber: "", + permanentAddress: "", + phoneNumber: "", + jobRoleId: null, + organizationId: "", + hasApplicationAccess: false +} + +export const ManageReportingSchema = z.object({ + primaryNotifyTo: z.array(z.string()).min(1, "Primary Reporting Manager is required"), + secondaryNotifyTo: z.array(z.string()).optional(), }); - -export const defatEmployeeObj = { - firstName: "", - middleName: "", - lastName: "", - email: "", - currentAddress: "", - birthDate: "", - joiningDate: "", - emergencyPhoneNumber: "", - emergencyContactPerson: "", - aadharNumber: "", - gender: "", - panNumber: "", - permanentAddress: "", - phoneNumber: "", - jobRoleId: null, - organizationId:"", - hasApplicationAccess:false - } \ No newline at end of file +export const defaultManageReporting = { + primaryNotifyTo: [], + secondaryNotifyTo: [], +}; + diff --git a/src/components/Employee/ManageEmployee.jsx b/src/components/Employee/ManageEmployee.jsx index b3aad4b4..5ee82674 100644 --- a/src/components/Employee/ManageEmployee.jsx +++ b/src/components/Employee/ManageEmployee.jsx @@ -18,7 +18,7 @@ import { defatEmployeeObj, employeeSchema } from "./EmployeeSchema"; import { useOrganizationsList } from "../../hooks/useOrganization"; import { ITEMS_PER_PAGE } from "../../utils/constants"; -const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => { +const ManageEmployee = ({ employeeId, onClosed }) => { const dispatch = useDispatch(); const { mutate: updateEmployee, isPending } = useUpdateEmployee(); const { @@ -72,7 +72,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => { data.email = null; } - const payload = { ...data, IsAllEmployee }; + const payload = { ...data }; if (employeeId) { payload.id = employeeId; diff --git a/src/components/Employee/ManageReporting.jsx b/src/components/Employee/ManageReporting.jsx new file mode 100644 index 00000000..b9857fea --- /dev/null +++ b/src/components/Employee/ManageReporting.jsx @@ -0,0 +1,188 @@ +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Label from "../common/Label"; +import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag"; +import { useManageEmployeeHierarchy, useOrganizationHierarchy } from "../../hooks/useEmployees"; +import { ManageReportingSchema, defaultManageReporting } from "./EmployeeSchema"; +import Avatar from "../common/Avatar"; +import { useNavigate } from "react-router-dom"; + +const ManageReporting = ({ onClosed, employee, employeeId }) => { + const { + handleSubmit, + control, + reset, + formState: { errors }, + watch, + } = useForm({ + resolver: zodResolver(ManageReportingSchema), + defaultValues: defaultManageReporting, + }); + const navigate = useNavigate(); + + const { data, isLoading } = useOrganizationHierarchy(employeeId); + + // mutation hook + const { mutate: manageHierarchy, isPending } = useManageEmployeeHierarchy( + employeeId, + onClosed + ); + const primaryValue = watch("primaryNotifyTo"); + const secondaryValue = watch("secondaryNotifyTo"); + + // Prefill hierarchy data + useEffect(() => { + if (data && Array.isArray(data)) { + const primary = data.find((r) => r.isPrimary); + const secondary = data.filter((r) => !r.isPrimary); + + reset({ + primaryNotifyTo: primary ? [primary.reportTo.id] : [], + secondaryNotifyTo: secondary.map((r) => r.reportTo.id), + }); + } + }, [data, reset]); + + const handleClose = () => { + reset(defaultManageReporting); + onClosed(); + }; + + const onSubmit = (formData) => { + // Build set of currently selected IDs + const selectedIds = new Set([ + ...(formData.primaryNotifyTo || []), + ...(formData.secondaryNotifyTo || []), + ]); + + // Build payload including previous assignments, setting isActive true/false accordingly + const payload = (data || []).map((item) => ({ + reportToId: item.reportTo.id, + isPrimary: item.isPrimary, + isActive: selectedIds.has(item.reportTo.id), + })); + + // Add any new IDs that were not previously assigned + if (formData.primaryNotifyTo?.length) { + const primaryId = formData.primaryNotifyTo[0]; + if (!data?.some((d) => d.reportTo.id === primaryId)) { + payload.push({ + reportToId: primaryId, + isPrimary: true, + isActive: true, + }); + } + } + + if (formData.secondaryNotifyTo?.length) { + formData.secondaryNotifyTo.forEach((id) => { + if (!data?.some((d) => d.reportTo.id === id)) { + payload.push({ + reportToId: id, + isPrimary: false, + isActive: true, + }); + } + }); + } + + manageHierarchy(payload); + }; + + const handleClick = () => { + handleClose(); + navigate(`/employee/${employee.id}`); + }; + + + return ( +
    +
    +
    Reporting Manager
    + + {/* Employee Info */} +
    +
    + +
    + + {/* Employee Name + Role */} +
    +
    + + {`${employee.firstName || ""} ${employee.middleName || ""} ${employee.lastName || ""}`.trim() || "Employee Name NA"} + + + {/* External Link Icon (Navigate to Employee Profile) */} + +
    + +
    + {employee.jobRole && ( + {employee.jobRole} + )} +
    +
    +
    + + {/* Primary Reporting Manager */} +
    + + 0 ? "" : "Search and select primary manager"} + forAll={true} + disabled={primaryValue?.length > 0} + /> + {errors.primaryNotifyTo && ( +
    + {errors.primaryNotifyTo.message} +
    + )} +
    + + {/* Secondary Reporting Manager */} +
    + + +
    + + {/* Buttons */} +
    + + + +
    + +
    + ); +}; + +export default ManageReporting; \ No newline at end of file diff --git a/src/components/Employee/handleEmployeeExport.jsx b/src/components/Employee/handleEmployeeExport.jsx new file mode 100644 index 00000000..a4883d4f --- /dev/null +++ b/src/components/Employee/handleEmployeeExport.jsx @@ -0,0 +1,84 @@ +import moment from "moment"; +import { exportToExcel, exportToCSV, exportToPDF, printTable } from "../../utils/tableExportUtils"; + +/** + * Handles export operations for employee data. + * @param {string} type - Export type: 'csv', 'excel', 'pdf', or 'print' + * @param {Array} employeeList - Full employee data array + * @param {Array} filteredData - Filtered employee data (if search applied) + * @param {string} searchText - Current search text (used to decide dataset) + * @param {RefObject} tableRef - Table reference (used for print) + */ +const handleEmployeeExport = (type, employeeList, filteredData, searchText, tableRef) => { + // Export full list (filtered if search applied) + const dataToExport = searchText ? filteredData : employeeList; + + if (!dataToExport || dataToExport.length === 0) return; + + // Map and format employee data for export + const exportData = dataToExport.map((item) => ({ + "First Name": item.firstName || "", + "Middle Name": item.middleName || "", + "Last Name": item.lastName || "", + "Email": item.email || "", + "Gender": item.gender || "", + "Birth Date": item.birthdate + ? moment(item.birthdate).format("DD-MMM-YYYY") + : "", + "Joining Date": item.joiningDate + ? moment(item.joiningDate).format("DD-MMM-YYYY") + : "", + "Permanent Address": item.permanentAddress || "", + "Current Address": item.currentAddress || "", + "Phone Number": item.phoneNumber || "", + "Emergency Phone Number": item.emergencyPhoneNumber || "", + "Emergency Contact Person": item.emergencyContactPerson || "", + "Is Active": item.isActive ? "Active" : "Inactive", + "Job Role": item.jobRole || "", + })); + + switch (type) { + case "csv": + exportToCSV(exportData, "employees"); + break; + + case "excel": + exportToExcel(exportData, "employees"); + break; + + case "pdf": + exportToPDF( + dataToExport.map((item) => ({ + Name: `${item.firstName || ""} ${item.lastName || ""}`.trim(), + Email: item.email || "", + "Phone Number": item.phoneNumber || "", + "Job Role": item.jobRole || "", + "Joining Date": item.joiningDate + ? moment(item.joiningDate).format("DD-MMM-YYYY") + : "", + Gender: item.gender || "", + Status: item.isActive ? "Active" : "Inactive", + })), + "employees", + [ + "Name", + "Email", + "Phone Number", + "Job Role", + "Joining Date", + "Gender", + "Status", + ] + ); + break; + + case "print": + printTable(tableRef.current); + break; + + default: + break; + } +}; + +export default handleEmployeeExport; diff --git a/src/components/Expenses/ActiveFilters.jsx b/src/components/Expenses/ActiveFilters.jsx new file mode 100644 index 00000000..ebd8639d --- /dev/null +++ b/src/components/Expenses/ActiveFilters.jsx @@ -0,0 +1,50 @@ +const ActiveFilters = ({ filters, optionsLookup = {}, onRemove }) => { + const entries = Object.entries(filters || {}); + + return ( +
    + {entries.map(([key, value]) => { + if (!value || (Array.isArray(value) && value.length === 0)) return null; + + if (Array.isArray(value)) { + return value.map((v) => { + const label = optionsLookup[key]?.[v] || v; + return ( + onRemove(key, v)} + > + {label} + + ); + }); + } + + if (typeof value === "boolean") { + return ( + onRemove(key)} + > + {key}: {value ? "Yes" : "No"} + + ); + } + + return ( + + {data?.startDate && data?.endDate + ? `${formatUTCToLocalTime( + data.startDate + )} - ${formatUTCToLocalTime(data.endDate)}` + : "No dates"} + + ); + })} +
    + ); +}; + +export default ActiveFilters; diff --git a/src/components/Expenses/ExpenseFilterChips.jsx b/src/components/Expenses/ExpenseFilterChips.jsx new file mode 100644 index 00000000..77bec83e --- /dev/null +++ b/src/components/Expenses/ExpenseFilterChips.jsx @@ -0,0 +1,87 @@ +import React, { useMemo } from "react"; + +const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Build chips from filters + const filterChips = useMemo(() => { + const chips = []; + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds"); + buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds"); + buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById"); + buildGroup(filters.statusIds, filterData.status, "Status", "statusIds"); + buildGroup(filters.expenseCategoryIds, filterData.expenseCategory, "Category", "expenseCategoryIds"); + + if (filters.startDate || filters.endDate) { + const start = filters.startDate + ? new Date(filters.startDate).toLocaleDateString() + : ""; + const end = filters.endDate + ? new Date(filters.endDate).toLocaleDateString() + : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
    +
    +
    + {filterChips.map((chip) => ( +
    + {/* Chip Label */} + {chip.label}: + + {/* Chip Items */} +
    + {chip.items.map((item) => ( + + {item.name} +
    +
    + ))} +
    +
    +
    + + + + ); +}; + +export default ExpenseFilterChips; + + diff --git a/src/components/Expenses/ExpenseFilterPanel.jsx b/src/components/Expenses/ExpenseFilterPanel.jsx index 250f5de4..61583902 100644 --- a/src/components/Expenses/ExpenseFilterPanel.jsx +++ b/src/components/Expenses/ExpenseFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from "react"; +import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react"; import { FormProvider, useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { defaultFilter, SearchSchema } from "./ExpenseSchema"; @@ -13,9 +13,11 @@ import { useSelector } from "react-redux"; import moment from "moment"; import { useExpenseFilter } from "../../hooks/useExpense"; import { ExpenseFilterSkeleton } from "./ExpenseSkeleton"; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; -const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { +const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => { + const { status } = useParams(); + const navigate = useNavigate(); const selectedProjectId = useSelector( (store) => store.localVariables.projectId ); @@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { { id: "submittedBy", name: "Submitted By" }, { id: "project", name: "Project" }, { id: "paymentMode", name: "Payment Mode" }, - { id: "expensesType", name: "Expense Type" }, + { id: "expenseCategory", name: "Expense Category" }, { id: "createdAt", name: "Submitted Date" }, ].sort((a, b) => a.name.localeCompare(b.name)); }, []); - const [selectedGroup, setSelectedGroup] = useState(groupByList[0]); + const [selectedGroup, setSelectedGroup] = useState(groupByList[6]); const [resetKey, setResetKey] = useState(0); + const dynamicDefaultFilter = useMemo(() => { + return { + ...defaultFilter, + statusIds: status ? [status] : defaultFilter.statusIds || [], + projectIds: defaultFilter.projectIds || [], + createdByIds: defaultFilter.createdByIds || [], + paidById: defaultFilter.paidById || [], + expenseCategoryIds: defaultFilter.expenseCategoryIds || [], + isTransactionDate: defaultFilter.isTransactionDate ?? true, + startDate: defaultFilter.startDate, + endDate: defaultFilter.endDate, + }; + }, [status, selectedProjectId]); + const methods = useForm({ resolver: zodResolver(SearchSchema), - defaultValues: defaultFilter, + defaultValues: dynamicDefaultFilter, }); const { control, handleSubmit, reset, setValue, watch } = methods; @@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { document.querySelector(".offcanvas.show .btn-close")?.click(); }; + // Change here + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + const handleGroupChange = (e) => { const group = groupByList.find((g) => g.id === e.target.value); if (group) setSelectedGroup(group); }; + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + // Reset specific field + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...methods.getValues(), [name]: defaultFilter[name] }); + } + }, + getValues: methods.getValues, // optional, to read current filter state + })); + const onSubmit = (formData) => { onApply({ ...formData, @@ -61,7 +96,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(), }); handleGroupBy(selectedGroup.id); - closePanel(); + // closePanel(); }; const onClear = () => { @@ -70,18 +105,55 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { setSelectedGroup(groupByList[0]); onApply(defaultFilter); handleGroupBy(groupByList[0].id); - closePanel(); + // closePanel(); + if (status) { + navigate("/expenses", { replace: true }); + } }; - // Close popup when navigating to another component const location = useLocation(); useEffect(() => { closePanel(); }, [location]); + const [appliedStatusId, setAppliedStatusId] = useState(null); + + useEffect(() => { + if (!status || !data) return; + + if (status !== appliedStatusId) { + const filterWithStatus = { + ...dynamicDefaultFilter, + projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [], + startDate: dynamicDefaultFilter.startDate + ? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString() + : undefined, + endDate: dynamicDefaultFilter.endDate + ? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString() + : undefined, + }; + + onApply(filterWithStatus); + handleGroupBy(selectedGroup.id); + setAppliedStatusId(status); + } + }, [ + status, + data, + dynamicDefaultFilter, + onApply, + handleGroupBy, + selectedGroup.id, + appliedStatusId, + selectedProjectId, + ]); + if (isLoading || isFetching) return ; if (isError && isFetched) return
    Something went wrong Here- {error.message}
    ; + + + return ( <> @@ -92,31 +164,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
    - @@ -142,6 +213,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { labelKey={(item) => item.name} valueKey="id" /> + item.name} + valueKey="id" + />
    @@ -213,6 +291,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { ); -}; +}); -export default ExpenseFilterPanel; +export default ExpenseFilterPanel; \ No newline at end of file diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index c18d2650..16e47489 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -10,20 +10,34 @@ import { EXPENSE_REJECTEDBY, ITEMS_PER_PAGE, } from "../../utils/constants"; -import { getColorNameFromHex, useDebounce } from "../../utils/appUtils"; +import { + formatCurrency, + formatFigure, + getColorNameFromHex, + useDebounce, +} from "../../utils/appUtils"; import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import ConfirmModal from "../common/ConfirmModal"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useSelector } from "react-redux"; +import ExpenseFilterChips from "./ExpenseFilterChips"; +import { defaultFilter } from "./ExpenseSchema"; +import { useNavigate } from "react-router-dom"; const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { const [deletingId, setDeletingId] = useState(null); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { setViewExpense, setManageExpenseModal } = useExpenseContext(); + const { + setViewExpense, + setManageExpenseModal, + filterData, + removeFilterChip, + } = useExpenseContext(); const IsExpenseEditable = useHasUserPermission(); const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE); const [currentPage, setCurrentPage] = useState(1); const debouncedSearch = useDebounce(searchText, 500); + const navigate = useNavigate(); const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { data, isLoading, isError, isInitialLoading, error } = useExpenseList( @@ -59,44 +73,64 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { const groupByField = (items, field) => { return items.reduce((acc, item) => { let key; + let displayField; + switch (field) { case "transactionDate": - key = item.transactionDate?.split("T")[0]; + key = formatUTCToLocalTime(item?.transactionDate); + displayField = "Transaction Date"; break; case "status": - key = item.status?.displayName || "Unknown"; + key = item?.status?.displayName || "Unknown"; + displayField = "Status"; break; case "submittedBy": - key = `${item.createdBy?.firstName ?? ""} ${ - item.createdBy?.lastName ?? "" - }`.trim(); + key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? "" + }`.trim(); + displayField = "Submitted By"; break; case "project": - key = item.project?.name || "Unknown Project"; + key = item?.project?.name || "Unknown Project"; + displayField = "Project"; break; case "paymentMode": - key = item.paymentMode?.name || "Unknown Mode"; + key = item?.paymentMode?.name || "Unknown Mode"; + displayField = "Payment Mode"; break; - case "expensesType": - key = item.expensesType?.name || "Unknown Type"; + case "expenseCategory": + key = item?.expenseCategory?.name || "Unknown Type"; + displayField = "Expense Category"; break; case "createdAt": - key = item.createdAt?.split("T")[0] || "Unknown Type"; + key = item?.createdAt?.split("T")[0] || "Unknown Date"; + displayField = "Created Date"; break; default: key = "Others"; + displayField = "Others"; } - if (!acc[key]) acc[key] = []; - acc[key].push(item); + + const groupKey = `${field}_${key}`; // unique key for object property + if (!acc[groupKey]) { + acc[groupKey] = { key, displayField, items: [] }; + } + + acc[groupKey].items.push(item); return acc; }, {}); }; const expenseColumns = [ { - key: "expensesType", - label: "Expense Type", - getValue: (e) => e.expensesType?.name || "N/A", + key: "expenseUId", + label: "Expense Id", + getValue: (e) => e.expenseUId || "N/A", + align: "text-start mx-2", + }, + { + key: "expenseCategory", + label: "Expense Category", + getValue: (e) => e.expenseCategory?.name || "N/A", align: "text-start", }, { @@ -110,11 +144,13 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { label: "Submitted By", align: "text-start", getValue: (e) => - `${e.createdBy?.firstName ?? ""} ${ - e.createdBy?.lastName ?? "" - }`.trim() || "N/A", + `${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? "" + }`.trim() || "N/A", customRender: (e) => ( -
    +
    navigate(`/employee/${e.createdBy?.id}`)} + > { lastName={e.createdBy?.lastName} /> - {`${e.createdBy?.firstName ?? ""} ${ - e.createdBy?.lastName ?? "" - }`.trim() || "N/A"} + {`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? "" + }`.trim() || "N/A"}
    ), @@ -140,7 +175,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { label: "Amount", getValue: (e) => ( <> - {e?.amount} + {" "} + {formatFigure(e?.amount, { + type: "currency", + currency: e?.currency?.currencyCode, + })} ), isAlwaysVisible: true, @@ -152,34 +191,46 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { align: "text-center", getValue: (e) => ( {e.status?.name || "Unknown"} ), }, ]; - - if (isInitialLoading) return ; - if (isError) return
    {error.message}
    ; + const headers = [ + "Expense Category", + "Payment Mode", + "Submitted By", + "Submitted", + "Amount", + "Status", + "Action", + ]; + if (isInitialLoading && !data) + return ; + if (isError) return
    {error?.message}
    ; const grouped = groupBy ? groupByField(data?.data ?? [], groupBy) : { All: data?.data ?? [] }; - const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy); + const IsGroupedByDate = [ + { key: "transactionDate", displayField: "Transaction Date" }, + { key: "createdAt", displayField: "created Date" }, + ]?.includes(groupBy); + const canEditExpense = (expense) => { return ( - (expense.status.id === EXPENSE_DRAFT || - EXPENSE_REJECTEDBY.includes(expense.status.id)) && - expense.createdBy?.id === SelfId + (expense?.status?.id === EXPENSE_DRAFT || + EXPENSE_REJECTEDBY.includes(expense?.status?.id)) && + expense?.createdBy?.id === SelfId ); }; const canDetetExpense = (expense) => { return ( - expense.status.id === EXPENSE_DRAFT && expense.createdBy.id === SelfId + expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId ); }; @@ -198,7 +249,14 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { /> )} -
    +
    + {/* Filter Chips */} +
    { (col.isAlwaysVisible || groupBy !== col.key) && (
  • ) )} @@ -226,34 +284,57 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { {Object.keys(grouped).length > 0 ? ( - Object.entries(grouped).map(([group, expenses]) => ( - + Object.values(grouped).map(({ key, displayField, items }) => ( + - {expenses.map((expense) => ( + {items?.map((expense) => ( {expenseColumns.map( (col) => (col.isAlwaysVisible || groupBy !== col.key) && ( ) )} @@ -292,22 +404,24 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { )) ) : ( - )}
    -
    {col.label}
    +
    {col.label}
    - - {IsGroupedByDate - ? formatUTCToLocalTime(group) - : group} - +
    + {" "} + + {displayField} :{" "} + {" "} + + {IsGroupedByDate + ? formatUTCToLocalTime(key) + : key} + +
    - {col.customRender - ? col.customRender(expense) - : col.getValue(expense)} +
    + {col.customRender + ? col.customRender(expense) + : col.getValue(expense)} +
    -
    +
    @@ -264,25 +345,56 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { } > {canEditExpense(expense) && ( - - setManageExpenseModal({ - IsOpen: true, - expenseId: expense.id, - }) - } - > - )} +
    + +
      +
    • + setManageExpenseModal({ + IsOpen: true, + expenseId: expense.id, + }) + } + > + + + + Modify + + +
    • - {canDetetExpense(expense) && ( - { - setIsDeleteModalOpen(true); - setDeletingId(expense.id); - }} - > + {canDetetExpense(expense) && ( +
    • { + setIsDeleteModalOpen(true); + setDeletingId(expense.id); + }} + > + + + + Delete + + +
    • + )} +
    +
    )}
    - No Expense Found + +
    +

    No Expense Found

    +
    - {data?.data?.length > 0 && ( - - )}
    + {data?.data?.length > 0 && ( + + )}
    ); diff --git a/src/components/Expenses/ExpenseSchema.js b/src/components/Expenses/ExpenseSchema.js index b1339228..428ccf52 100644 --- a/src/components/Expenses/ExpenseSchema.js +++ b/src/components/Expenses/ExpenseSchema.js @@ -1,4 +1,6 @@ import { z } from "zod"; +import { localToUtc } from "../../utils/appUtils"; +import { DEFAULT_CURRENCY } from "../../utils/constants"; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ALLOWED_TYPES = [ @@ -8,24 +10,25 @@ const ALLOWED_TYPES = [ "image/jpeg", ]; -export const ExpenseSchema = (expenseTypes) => { +export const ExpenseSchema = (expenseCategories) => { return z .object({ projectId: z.string().min(1, { message: "Project is required" }), - expensesTypeId: z + expenseCategoryId: z .string() .min(1, { message: "Expense type is required" }), paymentModeId: z.string().min(1, { message: "Payment mode is required" }), paidById: z.string().min(1, { message: "Employee name is required" }), - transactionDate: z - .string() - .min(1, { message: "Date is required" }) - , + transactionDate: z.string().min(1, { message: "Date is required" }), transactionId: z.string().optional(), description: z.string().min(1, { message: "Description is required" }), location: z.string().min(1, { message: "Location is required" }), supplerName: z.string().min(1, { message: "Supplier name is required" }), - gstNumber :z.string().optional(), + gstNumber: z.string().optional(), + currencyId: z + .string() + .min(1, { message: "currency is required" }) + .default(DEFAULT_CURRENCY), amount: z.coerce .number({ invalid_type_error: "Amount is required and must be a number", @@ -54,8 +57,6 @@ export const ExpenseSchema = (expenseTypes) => { }) ) .nonempty({ message: "At least one file attachment is required" }), - - }) .refine( (data) => { @@ -68,9 +69,14 @@ export const ExpenseSchema = (expenseTypes) => { path: ["paidById"], } ) - .superRefine((data, ctx) => { - const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId); - if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) { + .superRefine((data, ctx) => { + const ExpenseCategory = expenseCategories?.find( + (et) => et.id === data?.expenseCategoryId + ); + if ( + ExpenseCategory?.noOfPersonsRequired && + (!data?.noOfPersons || data?.noOfPersons < 1) + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "No. of Persons is required and must be at least 1", @@ -82,7 +88,7 @@ export const ExpenseSchema = (expenseTypes) => { export const defaultExpense = { projectId: "", - expensesTypeId: "", + expenseCategoryId: "", paymentModeId: "", paidById: "", transactionDate: "", @@ -92,12 +98,15 @@ export const defaultExpense = { supplerName: "", amount: "", noOfPersons: "", - gstNumber:"", + gstNumber: "", + currencyId: DEFAULT_CURRENCY, billAttachments: [], }; - -export const ExpenseActionScheam = (isReimbursement = false) => { +export const ExpenseActionScheam = ( + isReimbursement = false, + transactionDate +) => { return z .object({ comment: z.string().min(1, { message: "Please leave comment" }), @@ -105,6 +114,9 @@ export const ExpenseActionScheam = (isReimbursement = false) => { reimburseTransactionId: z.string().nullable().optional(), reimburseDate: z.string().nullable().optional(), reimburseById: z.string().nullable().optional(), + tdsPercentage: z.string().nullable().optional(), + baseAmount: z.string().nullable().optional(), + taxAmount: z.string().nullable().optional(), }) .superRefine((data, ctx) => { if (isReimbursement) { @@ -122,6 +134,7 @@ export const ExpenseActionScheam = (isReimbursement = false) => { message: "Reimburse Date is required", }); } + if (!data.reimburseById) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -129,26 +142,42 @@ export const ExpenseActionScheam = (isReimbursement = false) => { message: "Reimburse By is required", }); } + if (!data.baseAmount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["baseAmount"], + message: "Base Amount i required", + }); + } + if (!data.taxAmount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["taxAmount"], + message: "Tax is required", + }); + } } }); }; - export const defaultActionValues = { +export const defaultActionValues = { comment: "", statusId: "", reimburseTransactionId: null, reimburseDate: null, reimburseById: null, + tdsPercentage: null, + baseAmount: null, + taxAmount: null, }; - - export const SearchSchema = z.object({ projectIds: z.array(z.string()).optional(), statusIds: z.array(z.string()).optional(), createdByIds: z.array(z.string()).optional(), paidById: z.array(z.string()).optional(), + expenseCategoryIds: z.array(z.string()).optional(), startDate: z.string().optional(), endDate: z.string().optional(), isTransactionDate: z.boolean().default(true), @@ -159,8 +188,8 @@ export const defaultFilter = { statusIds: [], createdByIds: [], paidById: [], + expenseCategoryIds: [], isTransactionDate: true, startDate: null, endDate: null, }; - diff --git a/src/components/Expenses/ExpenseSkeleton.jsx b/src/components/Expenses/ExpenseSkeleton.jsx index dbe1a5d7..8a4afe95 100644 --- a/src/components/Expenses/ExpenseSkeleton.jsx +++ b/src/components/Expenses/ExpenseSkeleton.jsx @@ -154,7 +154,7 @@ export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => { -
    Expense Type
    +
    Expense Category
    Payment Mode
    diff --git a/src/components/Expenses/ExpenseStatusLogs.jsx b/src/components/Expenses/ExpenseStatusLogs.jsx index 575508db..d5014a8b 100644 --- a/src/components/Expenses/ExpenseStatusLogs.jsx +++ b/src/components/Expenses/ExpenseStatusLogs.jsx @@ -1,10 +1,11 @@ -import { useState,useMemo } from "react"; +import { useState, useMemo } from "react"; import Avatar from "../common/Avatar"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; - - +import Timeline from "../common/TimeLine"; +import moment from "moment"; +import { getColorNameFromHex } from "../../utils/appUtils"; const ExpenseStatusLogs = ({ data }) => { - const [visibleCount, setVisibleCount] = useState(4); + const sortedLogs = useMemo(() => { if (!data?.expenseLogs) return []; @@ -13,56 +14,35 @@ const ExpenseStatusLogs = ({ data }) => { ); }, [data?.expenseLogs]); - const logsToShow = sortedLogs.slice(0, visibleCount); + const timelineData = useMemo(() => { + return sortedLogs.map((log, index) => ({ + id: index + 1, + title: log.action || "Status Updated", + description: log.comment || "", + timeAgo: log.updateAt, + color: getColorNameFromHex(log.nextStatus?.color) || "primary", + users: log.updatedBy + ? [ + { + firstName: log.updatedBy.firstName || "", + lastName: log?.updatedBy?.lastName || "", + role: log.updatedBy.jobRoleName || "", + avatar: log.updatedBy.photo, + }, + ] + : [], + })); + }, [sortedLogs]); const handleShowMore = () => { setVisibleCount((prev) => prev + 4); }; return ( - <> -
    - {logsToShow.map((log) => ( -
    - - -
    -
    -
    - {`${log.updatedBy.firstName} ${log.updatedBy.lastName}`} - - {log.action} - - - {formatUTCToLocalTime(log.updateAt,true)} - -
    -
    - {log.comment} -
    -
    -
    -
    - ))} -
    - - {sortedLogs.length > visibleCount && ( -
    - -
    - )} - +
    + +
    ); }; - export default ExpenseStatusLogs; diff --git a/src/components/Expenses/Filelist.jsx b/src/components/Expenses/Filelist.jsx new file mode 100644 index 00000000..35a4a986 --- /dev/null +++ b/src/components/Expenses/Filelist.jsx @@ -0,0 +1,95 @@ +import React from "react"; +import { formatFileSize, getIconByFileType } from "../../utils/appUtils"; +import Tooltip from "../common/Tooltip"; + +const Filelist = ({ files, removeFile, expenseToEdit,sm=6,md=4 }) => { + return ( +
    + {files + .filter((file) => { + if (expenseToEdit) { + return file.isActive; + } + return true; + }) + .map((file, idx) => ( +
    +
    + {/* File icon and info */} +
    + + +
    + + {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : ""} + +
    +
    + + {/* Delete icon */} + + { + e.preventDefault(); + removeFile(expenseToEdit ? file.documentId : idx); + }} + > + +
    +
    + ))} +
    + + ); +}; + +export default Filelist; +export const FilelistView = ({ files, viewFile }) => { + return ( +
    + {files?.map((file, idx) => ( +
    +
    + {/* File icon and info */} +
    + + +
    { + e.preventDefault(); + viewFile({ + IsOpen: true, + Image: file.preSignedUrl, + }); + }} + > + + {file.fileName} + + + + {" "} + {file.fileSize ? formatFileSize(file.fileSize) : ""} + + +
    +
    +
    +
    + ))} +
    + ); +}; \ No newline at end of file diff --git a/src/components/Expenses/ManageExpense.jsx b/src/components/Expenses/ManageExpense.jsx index 9b14bfcf..40b4bebf 100644 --- a/src/components/Expenses/ManageExpense.jsx +++ b/src/components/Expenses/ManageExpense.jsx @@ -7,8 +7,9 @@ import { useProjectName } from "../../hooks/useProjects"; import { useDispatch, useSelector } from "react-redux"; import { changeMaster } from "../../slices/localVariablesSlice"; import useMaster, { + useCurrencies, + useExpenseCategory, useExpenseStatus, - useExpenseType, usePaymentMode, } from "../../hooks/masterHook/useMaster"; import { @@ -29,6 +30,13 @@ import DatePicker from "../common/DatePicker"; import ErrorPage from "../../pages/ErrorPage"; import Label from "../common/Label"; import EmployeeSearchInput from "../common/EmployeeSearchInput"; +import Filelist from "./Filelist"; +import { DEFAULT_CURRENCY } from "../../utils/constants"; +import SelectEmployeeServerSide, { + SelectProjectField, +} from "../common/Forms/SelectFieldServerSide"; +import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject"; +import { AppFormController } from "../../hooks/appHooks/useAppForm"; const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const { @@ -36,14 +44,15 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { isLoading, error: ExpenseErrorLoad, } = useExpense(expenseToEdit); - const [ExpenseType, setExpenseType] = useState(); + const [expenseCategory, setExpenseCategory] = useState(); + const [selectedEmployees, setSelectedEmployees] = useState([]); const dispatch = useDispatch(); const { - ExpenseTypes, + expenseCategories, loading: ExpenseLoading, error: ExpenseError, - } = useExpenseType(); - const schema = ExpenseSchema(ExpenseTypes); + } = useExpenseCategory(); + const schema = ExpenseSchema(expenseCategories); const { register, handleSubmit, @@ -65,7 +74,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { error, isError: isProjectError, } = useProjectName(); - + const { + data: currencies, + isLoading: currencyLoading, + error: currencyError, + } = useCurrencies(); const { PaymentModes, loading: PaymentModeLoading, @@ -76,11 +89,12 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { loading: StatusLoadding, error: stausError, } = useExpenseStatus(); - const { - data: employees, - isLoading: EmpLoading, - isError: isEmployeeError, - } = useEmployeesNameByProject(selectedproject); + // const { + // data: employees, + // isLoading: EmpLoading, + // isError: isEmployeeError, + // } = useEmployeesNameByProject(selectedproject); + const files = watch("billAttachments"); const onFileChange = async (e) => { const newFiles = Array.from(e.target.files); @@ -142,11 +156,19 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { } }; + const { mutate: AllocationTeam, isPending1 } = + useAllocationServiceProjectTeam(() => { + setSelectedEmployees([]); + setSeletingEmp({ + employee: null, + isOpen: false, + }); + }); useEffect(() => { if (expenseToEdit && data) { reset({ projectId: data.project.id || "", - expensesTypeId: data.expensesType.id || "", + expenseCategoryId: data?.expenseCategory?.id || "", paymentModeId: data.paymentMode.id || "", paidById: data.paidBy.id || "", transactionDate: data.transactionDate?.slice(0, 10) || "", @@ -157,6 +179,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { amount: data.amount || "", noOfPersons: data.noOfPersons || "", gstNumber: data.gstNumber || "", + currencyId: data.currency.id || DEFAULT_CURRENCY, billAttachments: data.documents ? data.documents.map((doc) => ({ fileName: doc.fileName, @@ -171,7 +194,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { : [], }); } - }, [data, reset, employees]); + }, [data, reset]); const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() => handleClose() ); @@ -192,11 +215,13 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { CreateExpense(payload); } }; - const ExpenseTypeId = watch("expensesTypeId"); + const expenseCategoryId = watch("expenseCategoryId"); useEffect(() => { - setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId)); - }, [ExpenseTypeId]); + setExpenseCategory( + expenseCategories?.find((type) => type.id === expenseCategoryId) + ); + }, [expenseCategoryId]); const handleClose = () => { reset(); @@ -212,7 +237,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
    -
    + {/*
    @@ -234,33 +259,50 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { {errors.projectId && ( {errors.projectId.message} )} +
    */} +
    + + setValue("projectId", val, { + shouldDirty: true, + shouldValidate: true, + }) + } + /> + {errors.projectId && ( + {errors.projectId.message} + )}
    -
    @@ -295,39 +337,29 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { )}
    -
    - - {/* - {errors.paidById && ( - {errors.paidById.message} - )} */} - - + Paid By{" "} + */} + {/* */} + + ( + + )} />
    @@ -339,6 +371,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { @@ -435,10 +468,39 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { {errors.gstNumber.message} )}
    - - {ExpenseType?.noOfPersonsRequired && ( -
    - +
    +
    +
    + + + {errors.currencyId && ( + {errors.currencyId.message} + )} +
    + {expenseCategory?.noOfPersonsRequired && ( +
    + { )} {files.length > 0 && ( -
    - {files - .filter((file) => { - if (expenseToEdit) { - return file.isActive; - } - return true; - }) - .map((file, idx) => ( - -
    - - {file.fileName} - - - {file.fileSize ? formatFileSize(file.fileSize) : ""} - -
    - { - e.preventDefault(); - removeFile(expenseToEdit ? file.documentId : idx); - }} - > -
    - ))} -
    + )} {Array.isArray(errors.billAttachments) && @@ -579,7 +612,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { ? "Please Wait..." : expenseToEdit ? "Update" - : "Submit"} + : "Save as Draft"}
    diff --git a/src/components/Expenses/PreviewDocument.jsx b/src/components/Expenses/PreviewDocument.jsx index 9c0f5c49..2f0e796b 100644 --- a/src/components/Expenses/PreviewDocument.jsx +++ b/src/components/Expenses/PreviewDocument.jsx @@ -1,28 +1,80 @@ -import { useState } from 'react'; +import { useState } from "react"; + const PreviewDocument = ({ imageUrl }) => { const [loading, setLoading] = useState(true); + const [rotation, setRotation] = useState(0); + const [scale, setScale] = useState(1); + + const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3)); + const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.4)); + const resetAll = () => { + setRotation(0); + setScale(1); + }; return ( -
    - {loading && ( -
    - Loading... + <> +
    + setRotation((prev) => prev + 90)} + > + + + + +
    + +
    + {loading && ( +
    + Loading... +
    + )} + +
    + Full View setLoading(false)} + />
    - )} - Full View setLoading(false)} - /> -
    + +
    + +
    +
    + ); }; + + export default PreviewDocument; diff --git a/src/components/Expenses/ViewExpense.jsx b/src/components/Expenses/ViewExpense.jsx index e6ca4426..8c032fc0 100644 --- a/src/components/Expenses/ViewExpense.jsx +++ b/src/components/Expenses/ViewExpense.jsx @@ -9,11 +9,19 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema"; import { useExpenseContext } from "../../pages/Expense/ExpensePage"; -import { getColorNameFromHex, getIconByFileType, localToUtc } from "../../utils/appUtils"; +import { + calculateTDSPercentage, + formatCurrency, + formatFigure, + getColorNameFromHex, + getIconByFileType, + localToUtc, +} from "../../utils/appUtils"; import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { EXPENSE_REJECTEDBY, + EXPENSE_STATUS, PROCESS_EXPENSE, REVIEW_EXPENSE, } from "../../utils/constants"; @@ -38,7 +46,8 @@ const ViewExpense = ({ ExpenseId }) => { const IsReview = useHasUserPermission(REVIEW_EXPENSE); const [imageLoaded, setImageLoaded] = useState({}); const { setDocumentView } = useExpenseContext(); - const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({}); + const ActionSchema = + ExpenseActionScheam(IsPaymentProcess, data?.createdAt) ?? z.object({}); const navigate = useNavigate(); const { register, @@ -46,12 +55,22 @@ const ViewExpense = ({ ExpenseId }) => { setValue, reset, control, + watch, formState: { errors }, } = useForm({ resolver: zodResolver(ActionSchema), defaultValues: defaultActionValues, }); +const baseAmount = Number(watch("baseAmount")) || 0; +const taxAmount = Number(watch("taxAmount")) || 0; +const tdsPercentage = Number(watch("tdsPercentage")) || 0; + + + const { grossAmount, tdsAmount, netPayable } = useMemo(() => { + return calculateTDSPercentage(baseAmount, taxAmount, tdsPercentage); + }, [baseAmount, taxAmount, tdsPercentage]); + const userPermissions = useSelector( (state) => state?.globalVariables?.loginUser?.featurePermissions || [] ); @@ -103,362 +122,500 @@ const ViewExpense = ({ ExpenseId }) => { const handleImageLoad = (id) => { setImageLoaded((prev) => ({ ...prev, [id]: true })); }; - + const STATUS_HEADING = { + [EXPENSE_STATUS.daft]: "Expense - Initiation", + [EXPENSE_STATUS.review_pending]: "Expense - Review & Validation", + [EXPENSE_STATUS.approve_pending]: "Expense - Approval", + [EXPENSE_STATUS.payment_pending]: "Expense - Processing & Disbursement", + }; return (
    -
    -
    -
    Expense Details
    -
    -
    -
    -
    {data?.description}
    -
    - {/* Row 1 */} -
    -
    - -
    - {formatUTCToLocalTime(data?.transactionDate)} -
    -
    -
    -
    -
    - -
    {data?.expensesType?.name}
    -
    -
    +
    +
    {STATUS_HEADING[data?.status?.id] || "Expense Details"}
    +
    - {/* Row 2 */} -
    -
    - -
    {data?.supplerName}
    -
    -
    -
    -
    - -
    ₹ {data.amount}
    -
    -
    - - {/* Row 3 */} -
    -
    - -
    {data?.paymentMode?.name}
    -
    -
    - {data?.gstNumber && ( -
    -
    -
    +
    +
    + ); +}; + +export default Invoice; diff --git a/src/components/UserSubscription/ProcessedPayment.jsx b/src/components/UserSubscription/ProcessedPayment.jsx new file mode 100644 index 00000000..c671301c --- /dev/null +++ b/src/components/UserSubscription/ProcessedPayment.jsx @@ -0,0 +1,427 @@ +import React, { useState, useMemo, useEffect } from "react"; +import { useSubscription } from "../../hooks/useAuth"; +import { useParams } from "react-router-dom"; +import { useCreateTenant, useIndustries } from "../../hooks/useTenant"; +import { + formatCurrency, + formatFigure, + frequencyLabel, +} from "../../utils/appUtils"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { PaymentRepository } from "../../repositories/PaymentRepository"; +import { useDispatch, useSelector } from "react-redux"; +import { setSelfTenant } from "../../slices/localVariablesSlice"; +import { unblockUI } from "../../utils/blockUI"; +import showToast from "../../services/toastService"; +import SelectedPlanSkeleton from "./SelectedPlaneSkeleton"; +import { useMakePayment } from "../../hooks/usePayments"; + +const ProcessedPayment = ({ + onNext, + resetPaymentStep, + setCurrentStep, + setStepStatus, + resetFormStep, +}) => { + const { planName } = useParams(); + + const { + details: client, + planId: selectedPlanId, + frequency, + } = useSelector((store) => store.localVariables.selfTenant); + const [selectedPlan, setSelectedPlan] = useState(null); + const [currentPlan, setCurrentPlan] = useState(null); + const [failPayment, setFailPayment] = useState(null); + + const { + data: plans, + isError: isPlanError, + isLoading, + isError, + isRefetching, + refetch, + } = useSubscription(frequency); + useEffect(() => { + if (!plans || !selectedPlanId) return; + const selected = plans.find((p) => p.id === selectedPlanId); + setSelectedPlan(selected); + }, [plans, selectedPlanId]); + + const loadScript = (src) => + new Promise((resolve) => { + const script = document.createElement("script"); + script.src = src; + script.onload = () => resolve(true); + script.onerror = () => resolve(false); + document.body.appendChild(script); + }); + + const { mutate: MakePayment, isPending } = useMakePayment( + (response) => { + unblockUI(); + onNext(response); + }, + (fail) => { + unblockUI(); + setFailPayment(fail); + onNext(fail); + }, + currentPlan + ); + + const ProcessToPayment = async () => { + setStepStatus((prev) => ({ ...prev, 3: "success" })); + setCurrentStep(4); + const res = await loadScript( + "https://checkout.razorpay.com/v1/checkout.js" + ); + if (!res) { + alert("Failed to load Razorpay SDK"); + return; + } + let price = 0; + price = + frequencyLabel(selectedPlan?.frequency, true, true)?.planDurationInInt * + selectedPlan?.price; + MakePayment({ amount: price }); + }; + + const handleRetry = () => { + setFailPayment(null); + if (typeof resetPaymentStep === "function") resetPaymentStep(); + }; + const handlPrevious = () => { + setCurrentStep, + setStepStatus((prev) => ({ ...prev, 2: "pending", 3: "pending" })); + setCurrentStep(2); + }; + + // useEffect(() => { + // if (!client || Object.keys(client).length === 0) { + // setFailPayment(null); + // if (typeof resetFormStep === "function") { + // resetFormStep(); + // } + // } + // }, [client]); + + if (failPayment) { + return ( +
    +
    +
    + +
    +

    Payment Failed!

    +

    + Unfortunately, your payment could not be completed. Please try again + or use a different payment method. +

    + +
    + + + Go Back to Dashboard + +
    + + {failPayment?.error && ( +
    + Error Details: +
    +                {JSON.stringify(failPayment.error, null, 2)}
    +              
    +
    + )} +
    +
    + ); + } + return ( +
    +
    +
    +
    +
    +

    You’ve Selected the Perfect Plan for Your Organization

    +

    + Great choice! This plan is tailored to meet your team’s needs + and help you maximize productivity. +

    +
    + {isError && ( +
    +

    {error?.message}

    + {error?.name} + + {isRefetching ? ( + <> + {" "} + Retrying... + + ) : ( + "Try to refetch" + )} + +
    + )} + {isLoading ? ( + + ) : ( + <> + {selectedPlan && ( +
    +
    +
    + + {selectedPlan?.description} + + +
    + +
    + + Price -{" "} + + {selectedPlan.currency?.symbol} {selectedPlan.price}{" "} + per {frequencyLabel(frequency)} + + +
    + +
    + {selectedPlan?.planName} + + billed {frequencyLabel(frequency, true)} + +
    +
    +
    + )} + + {selectedPlan && ( +
    +
    + {(() => { + const { + planName, + description, + price, + frequency, + trialDays, + maxUser, + maxStorage, + currency, + features, + } = selectedPlan; + return ( + <> +
    +
    +
    + + Max Users: {maxUser} +
    +
    +
    +
    + + Max Storage: {maxStorage} MB +
    +
    +
    +
    + + Trial Days: {trialDays} +
    +
    +
    + +
    + Included Features +
    +
    + {features && + Object.entries(features?.modules || {}) + .filter(([key]) => key !== "id") + .map(([key, mod]) => ( +
    + + {mod.name} +
    + ))} +
    + +
    + Support +
    +
      + {features?.supports?.emailSupport && ( +
    • + + Email Support +
    • + )} + {features?.supports?.phoneSupport && ( +
    • + + Phone Support +
    • + )} + {features?.supports?.prioritySupport && ( +
    • + + Priority Support +
    • + )} +
    +
    +
    +
    +
    Duration
    +
    + {frequencyLabel( + selectedPlan?.frequency, + true + )} +
    +
    + +
    +
    Total Price
    +
    + {formatFigure( + frequencyLabel( + selectedPlan?.frequency, + true, + true + )?.planDurationInInt * price, + { + type: "currency", + currency: + selectedPlan?.currency.currencyCode, + } + )} +
    +
    +
    + + ); + })()} +
    +
    + )} + + )} +
    +
    +
    + {client && ( +
    +
    + Confirm your organization details. +
    +
    +
    + Name: +
    +
    + {client.firstName} {client.lastName} +
    + +
    + Email: +
    +
    {client.email}
    + +
    + Contact Number: +
    +
    {client.contactNumber}
    + +
    + Organization Name: +
    +
    {client.organizationName}
    + +
    + Onboarding Date: +
    +
    + {formatUTCToLocalTime(client.onBoardingDate)} +
    + +
    + Billing Address: +
    +
    {client.billingAddress}
    + +
    + Industry : +
    +
    {client?.industry?.name}
    +
    +
    + )} +
    +
    +
    + + + +
    +
    + ); +}; + +export default ProcessedPayment; diff --git a/src/components/UserSubscription/Review.jsx b/src/components/UserSubscription/Review.jsx new file mode 100644 index 00000000..a7c6ccd4 --- /dev/null +++ b/src/components/UserSubscription/Review.jsx @@ -0,0 +1,11 @@ +import React from 'react' + +const Review = () => { + return ( +
    + +
    + ) +} + +export default Review diff --git a/src/components/UserSubscription/SelectPlan.jsx b/src/components/UserSubscription/SelectPlan.jsx new file mode 100644 index 00000000..75571680 --- /dev/null +++ b/src/components/UserSubscription/SelectPlan.jsx @@ -0,0 +1,424 @@ +import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router-dom"; +import { useSubscription } from "../../hooks/useAuth"; +import { formatFigure, frequencyLabel } from "../../utils/appUtils"; +import { setSelfTenant } from "../../slices/localVariablesSlice"; +import { error } from "pdf-lib"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CouponDiscount } from "../../pages/Home/HomeSchema"; +import SelectedPlanSkeleton from "./SelectedPlaneSkeleton"; + +const SelectPlan = ({ currentStep, setStepStatus, onNext }) => { + const { frequency, planId } = useParams(); + const [selectedFrequency, setSelectedFrequency] = useState( + parseInt(frequency) + ); + const dispatch = useDispatch(); + + const client = useSelector( + (store) => store.localVariables.selfTenant.details + ); + const [selectedPlan, setSelectedPlan] = useState(planId); + const [currentPlan, setCurrentPlan] = useState(null); + const [failPayment, setFailPayment] = useState(null); + + const { + data: plans, + isError, + isLoading, + error, + refetch, + isRefetching, + } = useSubscription(selectedFrequency); + + const handleChange = (e) => { + setSelectedPlan(e.target.value); + }; + + useEffect(() => { + if (!plans || plans.length === 0) return; + + // Prefer route param if exists, else default to first plan + const matchingPlan = plans.find((p) => p.planId === planId) || plans[0]; + + setSelectedPlan(matchingPlan.id); + setCurrentPlan(matchingPlan); + + // Dispatch correct plan + frequency only once data is ready + dispatch( + setSelfTenant({ + planId: matchingPlan.id, + frequency: selectedFrequency, + }) + ); + }, [plans, selectedFrequency, planId, dispatch]); + + const handleNextStep = () => { + if (!selectedPlan) { + toast.warning("Please select a plan before continuing."); + return; + } + + dispatch( + setSelfTenant({ + planId: selectedPlan, + frequency: selectedFrequency, + }) + ); + + setStepStatus((prev) => ({ ...prev, 2: "success" })); + onNext(); + }; + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ resolver: zodResolver(CouponDiscount) }); + + return ( +
    +
    +
    +
    +
    +

    Choose the Perfect Plan for Your Organization

    +

    + Select a plan that fits your team’s needs and unlock the + features that drive productivity. +

    + +
    +
    + + setSelectedFrequency(Number(e.target.value)) + } + /> + +
    + +
    + + setSelectedFrequency(Number(e.target.value)) + } + /> + +
    + +
    + + setSelectedFrequency(Number(e.target.value)) + } + /> + +
    + +
    + + setSelectedFrequency(Number(e.target.value)) + } + /> + +
    +
    +
    + + {isError && ( +
    +

    {error?.message}

    + {error?.name} + + {isRefetching ? ( + <> + {" "} + Retrying... + + ) : ( + "Try to refetch" + )} + +
    + )} + + {isLoading ? ( + + ) : ( + <> +
    + {plans?.map((plan) => ( +
    +
    + +
    +
    + ))} +
    + + {selectedPlan && ( +
    +
    + {(() => { + const selected = plans?.find( + (p) => p.id === selectedPlan + ); + if (!selected) return null; + + const { + price, + frequency, + trialDays, + maxUser, + maxStorage, + currency, + features, + } = selected; + + return ( + <> +
    +
    +
    + + Max Users: {maxUser} +
    +
    +
    +
    + + Max Storage: {maxStorage} MB +
    +
    +
    +
    + + Trial Days: {trialDays} +
    +
    +
    +
    + Included Features +
    +
    + {features && + Object.entries(features?.modules || {}) + .filter(([key]) => key !== "id") + .map(([key, mod]) => ( +
    + + {mod.name} +
    + ))} +
    +
    + Support +
    +
      + {features?.supports?.emailSupport && ( +
    • + + Email Support +
    • + )} + {features?.supports?.phoneSupport && ( +
    • + + Phone Support +
    • + )} + {features?.supports?.prioritySupport && ( +
    • + + Priority Support +
    • + )} +
    +
    +
    + +
    +
    + {" "} + +
    +
    + {/* {errors.coupon && ({error.coupon.message})} */}{" "} + + Currently, no coupon codes are available!{" "} + +
    +
    +
    +
    Duration
    +
    + {frequencyLabel(frequency, true)} +
    +
    + +
    +
    Total Price
    +
    + {formatFigure( + frequencyLabel( + selectedFrequency, + true, + true + )?.planDurationInInt * price, + { + type: "currency", + currency: currency.currencyCode, + } + )} +
    +
    +
    + + ); + })()} +
    +
    + )} + + )} +
    +
    + + {/* Image Section */} +
    + image +
    +
    + +
    + +
    +
    + ); +}; + +export default SelectPlan; diff --git a/src/components/UserSubscription/SelectedPlaneSkeleton.jsx b/src/components/UserSubscription/SelectedPlaneSkeleton.jsx new file mode 100644 index 00000000..b453a505 --- /dev/null +++ b/src/components/UserSubscription/SelectedPlaneSkeleton.jsx @@ -0,0 +1,88 @@ +import React from "react"; + + +const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => ( +
    +); + +const SelectedPlanSkeleton = () => { + return ( +
    + {/* Plan Summary Card */} +
    +
    +
    + + +
    + + + +
    +
    + + {/* Plan Details */} +
    +
    + {/* Stats (Max Users, Storage, Trial Days) */} +
    + {[...Array(3)].map((_, i) => ( +
    +
    + +
    +
    + ))} +
    + + {/* Included Features */} +
    + +
    +
    + {[...Array(6)].map((_, i) => ( +
    + +
    + ))} +
    + + {/* Support */} +
    + +
    +
    + {[...Array(3)].map((_, i) => ( + + ))} +
    + +
    + + {/* Duration and Total */} +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + ); +}; + +export default SelectedPlanSkeleton; diff --git a/src/components/UserSubscription/SubscriptionForm.jsx b/src/components/UserSubscription/SubscriptionForm.jsx new file mode 100644 index 00000000..fda0a334 --- /dev/null +++ b/src/components/UserSubscription/SubscriptionForm.jsx @@ -0,0 +1,271 @@ +import React, { useState, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import { + OrganizationDefaultValue, + OrganizationSchema, +} from "../../pages/Home/HomeSchema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Label from "../common/Label"; +import { orgSize, reference } from "../../utils/constants"; +import DatePicker from "../common/DatePicker"; +import { useCreateTenant, useIndustries } from "../../hooks/useTenant"; +import { useCreateSelfTenant } from "../../hooks/useAuth"; +import { blockUI } from "../../utils/blockUI"; + +const SubscriptionForm = ({ currentStep, setCurrentStep, setStepStatus }) => { + const { data, isError, isLoading: industryLoading } = useIndustries(); + const { + register, + handleSubmit, + control, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(OrganizationSchema), + defaultValues: OrganizationDefaultValue, + }); + + const { mutate: CreateTenant, isPending } = useCreateSelfTenant( + (resp) => { + setStepStatus((prev) => ({ ...prev, [currentStep]: "success" })); + setCurrentStep((prev) => prev + 1); + }, + (error) => { + setStepStatus((prev) => ({ ...prev, [currentStep]: "failed" })); + } + ); + + const onSubmit = (data) => { + CreateTenant(data); + // reset(); + }; + return ( +
    +
    +
    +
    +
    +

    Please provide your personal and organizational information to help us set up your account.

    +
    +
    + {/* First Name */} +
    + + + {errors.firstName && ( +
    + {errors.firstName.message} +
    + )} +
    + + {/* Last Name */} +
    + + + {errors.lastName && ( +
    + {errors.lastName.message} +
    + )} +
    + + {/* Email */} +
    + + + {errors.email && ( +
    {errors.email.message}
    + )} +
    + + {/* Contact Number */} +
    + + + {errors.contactNumber && ( +
    + {errors.contactNumber.message} +
    + )} +
    + + {/* Billing Address */} +
    + + + {errors.description && ( + + {errors.description.message} + + )} +
    +
    + +
    + + +
    document.getElementById("attachments").click()} + > + + + Click to select or click here to browse + + + (PDF, JPG, PNG,Doc,docx,xls,xlsx max 5MB) + + + { + onFileChange(e); + e.target.value = ""; + }} + /> +
    + {errors.attachments && ( + + {errors.attachments.message} + + )} + {files.length > 0 && ( +
    + {files + .filter((file) => { + if (collectionId) { + return file.isActive; + } + return true; + }) + .map((file, idx) => ( + +
    + + {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : ""} + +
    + { + e.preventDefault(); + removeFile(collectionId ? file.documentId : idx); + }} + > +
    + ))} +
    + )} + + {Array.isArray(errors.attachments) && + errors.attachments.map((fileError, index) => ( +
    + { + (fileError?.fileSize?.message || + fileError?.contentType?.message || + fileError?.base64Data?.message, + fileError?.documentId?.message) + } +
    + ))} +
    + +
    + {" "} + + +
    +
    + + +
    + ); +}; + +export default ManageCollection; diff --git a/src/components/collections/PaymentHistoryTable.jsx b/src/components/collections/PaymentHistoryTable.jsx new file mode 100644 index 00000000..e7b31fc9 --- /dev/null +++ b/src/components/collections/PaymentHistoryTable.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import { formatUTCToLocalTime } from '../../utils/dateUtils' +import { formatFigure } from '../../utils/appUtils' +import Avatar from '../common/Avatar' + +const PaymentHistoryTable = ({data}) => { + return ( +
    + + {data?.receivedInvoicePayments?.length > 0 ? ( +
    + + + + + + + + + + + + + {data.receivedInvoicePayments.map((payment, index) => ( + + + + + + + + + ))} + +
    Sr.NoTransaction ID Received Date Payment Adjustment-HeadAmountUpdated By
    {index + 1}{payment.transactionId}{formatUTCToLocalTime(payment.paymentReceivedDate)}{payment?.paymentAdjustmentHead?.name ?? "--"} + {formatFigure(payment.amount, { + type: "currency", + currency: "INR", + })} + +
    + + {payment.createdBy?.firstName}{" "} + {payment.createdBy?.lastName} +
    +
    +
    + ):(

    No History

    )} +
    + ) +} + +export default PaymentHistoryTable diff --git a/src/components/collections/ViewCollection.jsx b/src/components/collections/ViewCollection.jsx new file mode 100644 index 00000000..fdf563b9 --- /dev/null +++ b/src/components/collections/ViewCollection.jsx @@ -0,0 +1,256 @@ +import React, { useState } from "react"; +import { useCollectionContext } from "../../pages/collections/CollectionPage"; +import { useCollection } from "../../hooks/useCollections"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { formatFigure, getIconByFileType } from "../../utils/appUtils"; +import Avatar from "../common/Avatar"; +import PaymentHistoryTable from "./PaymentHistoryTable"; +import Comment from "./Comment"; +import { CollectionDetailsSkeleton } from "./CollectionSkeleton"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { ADMIN_COLLECTION, EDIT_COLLECTION } from "../../utils/constants"; + +const ViewCollection = ({ onClose }) => { + const [activeTab, setActiveTab] = useState("payments"); + const isAdmin = useHasUserPermission(ADMIN_COLLECTION); + const canEditCollection = useHasUserPermission(EDIT_COLLECTION); + const { viewCollection, setCollection, setDocumentView } = + useCollectionContext(); + const { data, isLoading, isError, error } = useCollection(viewCollection); + + const handleEdit = () => { + setCollection({ isOpen: true, invoiceId: viewCollection }); + onClose(); + }; + + if (isLoading) return ; + if (isError) return
    {error.message}
    ; + return ( +
    +

    Collection Details

    +
    +
    +
    + +
    {data?.project?.name}
    +
    +
    + {" "} + + {data?.isActive ? "Active" : "Inactive"} + + {(isAdmin || canEditCollection) && + !data?.receivedInvoicePayments && ( + + + + )} +
    +
    +
    +
    +
    Title :
    +
    {data?.title}
    +
    +
    +
    +
    +
    Invoice Number:
    +
    {data?.invoiceNumber}
    +
    +
    + {/* Row 2: E-Invoice Number + Project */} +
    +
    +
    E-Invoice Number:
    +
    {data?.eInvoiceNumber}
    +
    +
    + + {/* Row 3: Invoice Date + Client Submitted Date */} +
    +
    +
    Invoice Date:
    +
    + {formatUTCToLocalTime(data?.invoiceDate)} +
    +
    +
    +
    +
    +
    Client Submission Date:
    +
    + {formatUTCToLocalTime(data?.clientSubmitedDate)} +
    +
    +
    + {/* Row 4: Expected Payment Date + Mark as Completed */} +
    +
    +
    Expected Payment Date:
    +
    + {formatUTCToLocalTime(data?.exceptedPaymentDate)} +
    +
    +
    + + {/* Row 5: Basic Amount + Tax Amount */} +
    +
    +
    Basic Amount :
    +
    + {formatFigure(data?.basicAmount, { + type: "currency", + currency: "INR", + })} +
    +
    +
    +
    +
    +
    Tax Amount :
    +
    + {formatFigure(data?.taxAmount, { + type: "currency", + currency: "INR", + })} +
    +
    +
    + {/* Row 6: Balance Amount + Created At */} +
    +
    +
    Balance Amount :
    +
    + {formatFigure(data?.balanceAmount, { + type: "currency", + currency: "INR", + })} +
    +
    +
    +
    +
    +
    Created At :
    +
    {formatUTCToLocalTime(data?.createdAt)}
    +
    +
    + {/* Row 7: Created By */} +
    +
    +
    Created By :
    +
    + + + {data?.createdBy?.firstName} {data?.createdBy?.lastName} + +
    +
    +
    + {/* Description */} +
    +
    Description :
    +
    {data?.description}
    +
    + +
    + + +
    + {data?.attachments?.map((doc) => { + const isImage = doc.contentType?.startsWith("image"); + + return ( +
    { + if (isImage) { + setDocumentView({ + IsOpen: true, + Image: doc.preSignedUrl, + }); + } else { + window.open(doc.preSignedUrl, "_blank"); + } + }} + > + + + {doc.fileName} + +
    + ); + }) ?? "No Attachment"} +
    +
    + +
    + {/* Tabs Navigation */} +
      +
    • + +
    • +
    • + +
    • +
    + + {/* Tab Content */} +
    + {activeTab === "payments" && ( +
    + +
    + )} + + {activeTab === "details" && ( +
    + +
    + )} +
    +
    +
    +
    + ); +}; + +export default ViewCollection; diff --git a/src/components/collections/collectionSchema.jsx b/src/components/collections/collectionSchema.jsx new file mode 100644 index 00000000..84ea806b --- /dev/null +++ b/src/components/collections/collectionSchema.jsx @@ -0,0 +1,103 @@ +import { z } from "zod"; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_TYPES = [ + "application/pdf", + "application/doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "image/png", + "image/jpg", + "image/jpeg", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +]; + + +export const newCollection = z.object({ + title: z.string().trim().min(1, { message: "Title is required" }), + projectId: z.string().trim().min(1, { message: "Project is required" }), + invoiceDate: z.string().min(1, { message: "Date is required" }), + description: z.string().trim().optional(), + clientSubmitedDate: z.string().min(1, { message: "Date is required" }), + billedToId: z.string().min(1, { message: "Date is required" }), + exceptedPaymentDate: z.string().min(1, { message: "Date is required" }), + invoiceNumber: z + .string() + .trim() + .min(1, { message: "Invoice is required" }) + .max(17, { message: "Invalid Number" }), + eInvoiceNumber: z + .string() + .trim() + .min(1, { message: "E-Invoice No is required" }), + taxAmount: z.coerce + .number({ + invalid_type_error: "Amount is required and must be a number", + }) + .min(1, "Amount must be Enter") + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + basicAmount: z.coerce + .number({ + invalid_type_error: "Amount is required and must be a number", + }) + .min(1, "Amount must be Enter") + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + attachments: z + .array( + z.object({ + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().nullable(), + contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + documentId: z.string().optional(), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional(), + isActive: z.boolean().default(true), + }) + ) + .nonempty({ message: "At least one file attachment is required" }), +}); + +export const defaultCollection = { + projectId: "", + invoiceNumber: " ", + eInvoiceNumber: "", + title: "", + clientSubmitedDate: null, + invoiceDate: null, + exceptedPaymentDate: null, + taxAmount: "", + basicAmount: "", + description: "", + billedToId:"", + attachments: [], +}; + +export const paymentSchema = z.object({ + paymentReceivedDate: z.string().min(1, { message: "Date is required" }), + transactionId: z.string().min(1, "Transaction ID is required"), + amount: z.number().min(1, "Amount must be greater than zero"), + comment:z.string().min(1,{message:"Comment required"}), + paymentAdjustmentHeadId:z.string().min(1,{message:"Payment Type required"}) +}); + +// Default Value +export const defaultPayment = { + paymentReceivedDate: null, + transactionId: "", + amount: 0, + comment:"", + paymentAdjustmentHeadId:"" +}; + + +export const CommentSchema = z.object({ + comment:z.string().min(1,{message:"Comment required"}) +}) diff --git a/src/components/common/AccessDenied.jsx b/src/components/common/AccessDenied.jsx new file mode 100644 index 00000000..62cc5f4e --- /dev/null +++ b/src/components/common/AccessDenied.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import Breadcrumb from "./Breadcrumb"; + +const AccessDenied = ({data}) => { + return ( +
    + + +
    + +

    + Access Denied: You don't have permission to perform this action ! +

    +
    + +
    + ); +}; + +export default AccessDenied; diff --git a/src/components/common/Avatar.jsx b/src/components/common/Avatar.jsx index 5c3f25e6..57fc9e0f 100644 --- a/src/components/common/Avatar.jsx +++ b/src/components/common/Avatar.jsx @@ -51,8 +51,8 @@ const Avatar = ({ firstName, lastName, size = "sm", classAvatar }) => { return (
    -
    - +
    + {generateAvatarText(firstName, lastName)}
    diff --git a/src/components/common/Chips.jsx b/src/components/common/Chips.jsx new file mode 100644 index 00000000..18972b2d --- /dev/null +++ b/src/components/common/Chips.jsx @@ -0,0 +1,44 @@ +import React from "react"; + +export const EmployeeChip = ({ handleRemove, employee }) => { + return ( + +
    + {employee?.photo ? ( + + {`${employee?.firstName + + ) : ( +
    + + {employee?.firstName?.[0] || ""} + {employee?.lastName?.[0] || ""} + +
    + )} + +
    + + {employee?.firstName} {employee?.lastName} + +
    +
    + + handleRemove(employee?.id)} + aria-label={`Remove ${employee?.firstName}`} + title="Remove" + /> +
    + ); +}; diff --git a/src/components/common/ConfirmModal.jsx b/src/components/common/ConfirmModal.jsx index 037c6a59..5a266086 100644 --- a/src/components/common/ConfirmModal.jsx +++ b/src/components/common/ConfirmModal.jsx @@ -13,19 +13,22 @@ const ConfirmModal = ({ if (!isOpen) return null; const TypeofIcon = () => { - if (type === "delete") { - return ( - - ); + switch (type) { + case "delete": + return ; + case "success": + return ; + case "archive": + return ; + case "Un-archive": + return ; + case "undo": + return ; + default: + return null; } - return null; }; - const modalSize = type === "delete" ? "sm" : "md"; - return (
    -
    +
    {header && {header}}
    -
    {TypeofIcon()}
    -
    +
    + {TypeofIcon()} +
    +
    {message}
    +
    diff --git a/src/components/common/DatePicker.jsx b/src/components/common/DatePicker.jsx index 6b73dc36..c3592bc9 100644 --- a/src/components/common/DatePicker.jsx +++ b/src/components/common/DatePicker.jsx @@ -4,11 +4,13 @@ import { useController } from "react-hook-form"; const DatePicker = ({ name, control, + size="sm", placeholder = "DD-MM-YYYY", className = "", allowText = false, maxDate, minDate, + disabled = false, ...rest }) => { const inputRef = useRef(null); @@ -47,10 +49,10 @@ const DatePicker = ({ const displayValue = value ? flatpickr.formatDate(new Date(value), "d-m-Y") : ""; return ( -
    +
    { @@ -64,6 +66,7 @@ const DatePicker = ({ }} readOnly={!allowText} autoComplete="off" + disabled={disabled} /> +
    - + > + +
    ); }; export default DateRangePicker; - - - - - export const DateRangePicker1 = ({ startField = "startDate", endField = "endDate", @@ -130,7 +126,7 @@ export const DateRangePicker1 = ({ mode: "range", dateFormat: "d-m-Y", allowInput: allowText, - maxDate , + maxDate, onChange: (selectedDates) => { if (selectedDates.length === 2) { const [start, end] = selectedDates; @@ -160,30 +156,29 @@ export const DateRangePicker1 = ({ }, []); useEffect(() => { - if (resetSignal !== undefined) { - if (defaultRange) { - applyDefaultDates(); - } else { - setValue(startField, "", { shouldValidate: true }); - setValue(endField, "", { shouldValidate: true }); + if (resetSignal !== undefined) { + if (defaultRange) { + applyDefaultDates(); + } else { + setValue(startField, "", { shouldValidate: true }); + setValue(endField, "", { shouldValidate: true }); - if (inputRef.current?._flatpickr) { - inputRef.current._flatpickr.clear(); + if (inputRef.current?._flatpickr) { + inputRef.current._flatpickr.clear(); + } } } - } -}, [resetSignal, defaultRange, setValue, startField, endField]); - + }, [resetSignal, defaultRange, setValue, startField, endField]); const start = getValues(startField); const end = getValues(endField); const formattedValue = start && end ? `${start} To ${end}` : ""; return ( -
    +
    { @@ -202,4 +197,3 @@ export const DateRangePicker1 = ({
    ); }; - diff --git a/src/components/common/EmployeeAvatarGroup.jsx b/src/components/common/EmployeeAvatarGroup.jsx new file mode 100644 index 00000000..2753e3d7 --- /dev/null +++ b/src/components/common/EmployeeAvatarGroup.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import Avatar from "./Avatar"; +import Tooltip from "./Tooltip"; +import HoverPopup from "./HoverPopup"; +import { map } from "zod"; + +const EmployeeAvatarGroup = ({ employees = [] }) => { + const visibleEmployees = employees.slice(0, 3); + const remainingEmployees = employees.slice(3); + const remainingCount = employees.length - visibleEmployees.length; + + return ( +
    + {visibleEmployees.map((emp, i) => ( +
    + }> + {emp.avatarUrl ? ( + {`${emp.firstName} + ) : ( + + )} + +
    + ))} + + {remainingCount > 0 && ( +
    +
    + + } + > + +{remainingCount} + + +
    +
    + )} +
    + ); +}; + +export default EmployeeAvatarGroup; + +const EmployeeDetails = ({ e }) => { + return ( + +
    + +
    +

    {`${e?.firstName}' ${e?.lastName} `}

    + + {e?.jobRoleName ?? "employee"} + +
    +
    +
    + ); +}; +export const Remaning = ({ emp }) => { + return ( + + ); +}; diff --git a/src/components/common/EmployeeSearchInput.jsx b/src/components/common/EmployeeSearchInput.jsx index 34fdecaa..32e78bec 100644 --- a/src/components/common/EmployeeSearchInput.jsx +++ b/src/components/common/EmployeeSearchInput.jsx @@ -7,6 +7,7 @@ import Avatar from "./Avatar"; const EmployeeSearchInput = ({ control, name, + size = "sm", projectId, placeholder, forAll, @@ -46,7 +47,7 @@ const EmployeeSearchInput = ({ { diff --git a/src/components/common/Forms/InputSuggesstionField.jsx b/src/components/common/Forms/InputSuggesstionField.jsx new file mode 100644 index 00000000..2dd700c0 --- /dev/null +++ b/src/components/common/Forms/InputSuggesstionField.jsx @@ -0,0 +1,91 @@ +import React, { useEffect, useRef, useState } from "react"; +import Label from "../Label"; + +const InputSuggessionField = ({ + suggesstionList = [], + value, + onChange, + error, + disabled = false, + label = "Label", + placeholder = "Please Enter", + required = false, + isLoading = false, +}) => { + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const selectedOption = suggesstionList.find((opt) => opt === value); + + const displayText = selectedOption ? selectedOption : placeholder; + + const handleSelect = (option) => { + onChange(option); + setOpen(false); + }; + + const toggleDropdown = () => setOpen((prev) => !prev); + + return ( +
    + {label && ( + + )} + + + + {open && !isLoading && ( +
      + {suggesstionList.map((option, i) => ( +
    • + +
    • + ))} +
    + )} +
    + ); +}; + +export default InputSuggessionField; diff --git a/src/components/common/Forms/SelectField.jsx b/src/components/common/Forms/SelectField.jsx new file mode 100644 index 00000000..a08eaf8c --- /dev/null +++ b/src/components/common/Forms/SelectField.jsx @@ -0,0 +1,94 @@ +import React, { useEffect, useRef, useState } from "react"; +import Label from "../Label"; + +const SelectField = ({ + label = "Select", + options = [], + placeholder = "Select Option", + required = false, + value, + onChange, + valueKey = "id", + labelKey = "name", + isLoading = false, +}) => { + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const selectedOption = options.find((opt) => opt[valueKey] === value); + + const displayText = selectedOption ? selectedOption[labelKey] : placeholder; + + const handleSelect = (option) => { + onChange(option[valueKey]); + setOpen(false); + }; + + const toggleDropdown = () => setOpen((prev) => !prev); + + return ( +
    + {label && ( + + )} + + + + {open && !isLoading && ( +
      + {options.map((option, i) => ( +
    • + +
    • + ))} +
    + )} +
    + ); +}; + +export default SelectField; diff --git a/src/components/common/Forms/SelectFieldServerSide.jsx b/src/components/common/Forms/SelectFieldServerSide.jsx new file mode 100644 index 00000000..47b1d5ad --- /dev/null +++ b/src/components/common/Forms/SelectFieldServerSide.jsx @@ -0,0 +1,574 @@ +import React, { useEffect, useRef, useState } from "react"; +import Label from "../Label"; +import { useDebounce } from "../../../utils/appUtils"; +import { useEmployeesName } from "../../../hooks/useEmployees"; +import { useProjectBothName } from "../../../hooks/useProjects"; +import EmployeeRepository from "../../../repositories/EmployeeRepository"; + +const SelectEmployeeServerSide = ({ + label = "Select", + placeholder = "Select Employee", + required = false, + value = null, + onChange, + valueKey = "id", + isFullObject = false, + isMultiple = false, + projectId = null, + isAllEmployee = false, +}) => { + const [searchText, setSearchText] = useState(""); + const debounce = useDebounce(searchText, 300); + const [forcedSelected, setForcedSelected] = useState(null); + + const { data, isLoading } = useEmployeesName( + projectId, + debounce, + isAllEmployee + ); + + const options = data?.data ?? []; + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + const getDisplayName = (emp) => { + if (!emp) return ""; + return `${emp.firstName || ""} ${emp.lastName || ""}`.trim(); + }; + + let selectedSingle = null; + if (!isMultiple) { + if (isFullObject && value) selectedSingle = value; + else if (!isFullObject && value) + selectedSingle = + options.find((o) => o[valueKey] === value) || forcedSelected; + } + + let selectedList = []; + if (isMultiple && Array.isArray(value)) { + if (isFullObject) selectedList = value; + else { + selectedList = options.filter((opt) => value.includes(opt[valueKey])); + } + } + + const displayText = !isMultiple + ? getDisplayName(selectedSingle) || placeholder + : selectedList.length > 0 + ? selectedList.map((e) => getDisplayName(e)).join(", ") + : placeholder; + + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleSelect = (option) => { + if (!isMultiple) { + if (isFullObject) onChange(option); + else onChange(option[valueKey]); + setOpen(false); + } else { + let updated = []; + const exists = selectedList.some((e) => e[valueKey] === option[valueKey]); + updated = exists + ? selectedList.filter((e) => e[valueKey] !== option[valueKey]) + : [...selectedList, option]; + if (isFullObject) onChange(updated); + else onChange(updated.map((x) => x[valueKey])); + } + }; + + useEffect(() => { + if (!value || isFullObject) return; + + const exists = options.some((o) => o[valueKey] === value); + if (exists) return; + + const loadSingleEmployee = async () => { + try { + const emp = await EmployeeRepository.getEmployeeName( + null, + null, + true, + value + ); + setForcedSelected(emp.data[0]); + } catch (err) { + console.error("Failed to load selected employee", err); + } + }; + + loadSingleEmployee(); + }, [value, options, isFullObject, valueKey]); + + return ( +
    + {label && ( + + )} + + {/* MAIN BUTTON */} + + + {open && ( +
      +
    • + setSearchText(e.target.value)} + className="form-control form-control-sm" + placeholder="Search..." + /> +
    • + + {isLoading && ( +
    • Loading...
    • + )} + + {!isLoading && options.length === 0 && ( +
    • + No results found +
    • + )} + + {!isLoading && + options.map((option) => { + const isActive = isMultiple + ? selectedList.some((x) => x[valueKey] === option[valueKey]) + : selectedSingle && + selectedSingle[valueKey] === option[valueKey]; + + return ( +
    • + +
    • + ); + })} +
    + )} +
    + ); +}; + +export default SelectEmployeeServerSide; + +export const SelectProjectField = ({ + label = "Select", + placeholder = "Select Project", + required = false, + value = null, + onChange, + valueKey = "id", + isFullObject = false, + isMultiple = false, + isAllProject = false, + disabled +}) => { + const [searchText, setSearchText] = useState(""); + const debounce = useDebounce(searchText, 300); + + const { data, isLoading } = useProjectBothName(debounce); + + const options = data ?? []; + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + const getDisplayName = (project) => { + if (!project) return ""; + return `${project.name || ""}`.trim(); + }; + + let selectedSingle = null; + + if (!isMultiple) { + if (isFullObject && value) selectedSingle = value; + else if (!isFullObject && value) + selectedSingle = options.find((o) => o[valueKey] === value); + } + + let selectedList = []; + + if (isMultiple && Array.isArray(value)) { + if (isFullObject) selectedList = value; + else { + selectedList = options.filter((opt) => value.includes(opt[valueKey])); + } + } + + /** Main button label */ + const displayText = !isMultiple + ? getDisplayName(selectedSingle) || placeholder + : selectedList.length > 0 + ? selectedList.map((e) => getDisplayName(e)).join(", ") + : placeholder; + + /** ----------------------------- + * HANDLE OUTSIDE CLICK + * ----------------------------- */ + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + /** ----------------------------- + * HANDLE SELECT + * ----------------------------- */ + const handleSelect = (option) => { + if (!isMultiple) { + // SINGLE SELECT + if (isFullObject) onChange(option); + else onChange(option[valueKey]); + } else { + // MULTIPLE SELECT + let updated = []; + + const exists = selectedList.some((e) => e[valueKey] === option[valueKey]); + + if (exists) { + // remove + updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]); + } else { + // add + updated = [...selectedList, option]; + } + + if (isFullObject) onChange(updated); + else onChange(updated.map((x) => x[valueKey])); + } + }; + + return ( +
    + {label && ( + + )} + + {/* MAIN BUTTON */} + + + {/* DROPDOWN */} + {open && ( +
      +
      + setSearchText(e.target.value)} + className="form-control form-control-sm" + placeholder="Search..." + /> +
      + + {isLoading && ( +
    • Loading...
    • + )} + + {!isLoading && options.length === 0 && ( +
    • + No results found +
    • + )} + + {!isLoading && + options.map((option) => { + const isActive = isMultiple + ? selectedList.some((x) => x[valueKey] === option[valueKey]) + : selectedSingle && + selectedSingle[valueKey] === option[valueKey]; + + return ( +
    • + +
    • + ); + })} +
    + )} +
    + ); +}; + +export const SelectFieldSearch = ({ + label = "Select", + placeholder = "Select ", + required = false, + value = null, + onChange, + valueKey = "id", + labelKey = "name", + disabled = false, + isFullObject = false, + isMultiple = false, + hookParams, + useFetchHook, +}) => { + const [searchText, setSearchText] = useState(""); + const debounce = useDebounce(searchText, 300); + + const { data, isLoading } = useFetchHook(...hookParams, debounce); + const options = data?.data ?? []; + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + const getDisplayName = (entity) => { + if (!entity) return ""; + return `${entity[labelKey] || ""}`.trim(); + }; + + /** ----------------------------- + * SELECTED OPTION (SINGLE) + * ----------------------------- */ + let selectedSingle = null; + + if (!isMultiple) { + if (isFullObject && value) selectedSingle = value; + else if (!isFullObject && value) + selectedSingle = options.find((o) => o[valueKey] === value); + } + + /** ----------------------------- + * SELECTED OPTION (MULTIPLE) + * ----------------------------- */ + let selectedList = []; + if (isMultiple && Array.isArray(value)) { + if (isFullObject) selectedList = value; + else { + selectedList = options.filter((opt) => value.includes(opt[valueKey])); + } + } + + /** Main button label */ + const displayText = !isMultiple + ? getDisplayName(selectedSingle) || placeholder + : selectedList.length > 0 + ? selectedList.map((e) => getDisplayName(e)).join(", ") + : placeholder; + + /** ----------------------------- + * HANDLE OUTSIDE CLICK + * ----------------------------- */ + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + // MERGED OPTIONS TO ENSURE SELECTED VALUE APPEARS EVEN IF NOT IN SEARCH RESULT + const [mergedOptions, setMergedOptions] = useState([]); + + useEffect(() => { + let finalList = [...options]; + + if (!isMultiple && value && !isFullObject) { + // already selected option inside options? + const exists = options.some((o) => o[valueKey] === value); + + // if selected item not found, try to get from props (value) as fallback + if (!exists && typeof value === "object") { + finalList.unshift(value); + } + } + + if (isMultiple && Array.isArray(value)) { + value.forEach((val) => { + const id = isFullObject ? val[valueKey] : val; + const exists = options.some((o) => o[valueKey] === id); + + if (!exists && typeof val === "object") { + finalList.unshift(val); + } + }); + } + + setMergedOptions(finalList); + }, [options, value]); + + /** ----------------------------- + * HANDLE SELECT + * ----------------------------- */ + const handleSelect = (option) => { + if (!isMultiple) { + // SINGLE SELECT + if (isFullObject) onChange(option); + else onChange(option[valueKey]); + } else { + // MULTIPLE SELECT + let updated = []; + + const exists = selectedList.some((e) => e[valueKey] === option[valueKey]); + + if (exists) { + // remove + updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]); + } else { + // add + updated = [...selectedList, option]; + } + + if (isFullObject) onChange(updated); + else onChange(updated.map((x) => x[valueKey])); + } + }; + + return ( +
    + {label && ( + + )} + + {/* MAIN BUTTON */} + + + {/* DROPDOWN */} + {open && ( +
      +
      + setSearchText(e.target.value)} + className="form-control form-control-sm" + placeholder="Search..." + disabled={disabled} + /> +
      + + {isLoading && ( +
    • Loading...
    • + )} + + {!isLoading && options.length === 0 && ( +
    • + No results found +
    • + )} + + {!isLoading && + options.map((option) => { + const isActive = isMultiple + ? selectedList.some((x) => x[valueKey] === option[valueKey]) + : selectedSingle && + selectedSingle[valueKey] === option[valueKey]; + + return ( +
    • + +
    • + ); + })} +
    + )} +
    + ); +}; diff --git a/src/components/common/GlobalModal/CommentEditor.jsx b/src/components/common/GlobalModal/CommentEditor.jsx index 68d4e353..5d4dc626 100644 --- a/src/components/common/GlobalModal/CommentEditor.jsx +++ b/src/components/common/GlobalModal/CommentEditor.jsx @@ -28,7 +28,6 @@ const CommentEditor = () => { const [value, setValue] = useState(""); const handleSubmit = () => { - console.log("Comment:", value); // Submit or handle content }; diff --git a/src/components/common/HoverPopup.jsx b/src/components/common/HoverPopup.jsx index d65f365e..1063745b 100644 --- a/src/components/common/HoverPopup.jsx +++ b/src/components/common/HoverPopup.jsx @@ -1,65 +1,197 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + closePopup, + openPopup, + togglePopup, +} from "../../slices/localVariablesSlice"; + +/** + * align: "auto" | "left" | "right" + * boundaryRef: optional ref to the drawer/container element to use as boundary + */ +const HoverPopup = ({ + id, + title, + content, + children, + className = "", + Mode = "hover", + align = "auto", + boundaryRef = null, +}) => { + const dispatch = useDispatch(); + const visible = useSelector((s) => s.localVariables.popups[id] || false); -const HoverPopup = ({ title, content, children }) => { - const [visible, setVisible] = useState(false); const triggerRef = useRef(null); const popupRef = useRef(null); - // Toggle on hover or click - const handleMouseEnter = () => setVisible(true); - const handleClick = () => setVisible((prev) => !prev); + const handleMouseEnter = () => { + if (Mode === "hover") dispatch(openPopup(id)); + }; + const handleMouseLeave = () => { + if (Mode === "hover") dispatch(closePopup(id)); + }; + const handleClick = (e) => { + if (Mode === "click") { + e.stopPropagation(); + dispatch(togglePopup(id)); + } + }; - // Hide on outside click + // Close on outside click when using click mode useEffect(() => { - const handleDocumentClick = (e) => { + if (Mode !== "click" || !visible) return; + + const handler = (e) => { if ( - !popupRef.current?.contains(e.target) && - !triggerRef.current?.contains(e.target) + popupRef.current && + !popupRef.current.contains(e.target) && + triggerRef.current && + !triggerRef.current.contains(e.target) ) { - setVisible(false); + dispatch(closePopup(id)); } }; - if (visible) { - document.addEventListener("click", handleDocumentClick); - } + document.addEventListener("click", handler); + return () => document.removeEventListener("click", handler); + }, [Mode, visible, dispatch, id]); - return () => { - document.removeEventListener("click", handleDocumentClick); - }; - }, [visible]); + // Positioning effect: respects align prop and stays inside boundary (drawer) + useEffect(() => { + if (!visible || !popupRef.current || !triggerRef.current) return; - return ( + // run in next frame so DOM/layout settles + requestAnimationFrame(() => { + const popup = popupRef.current; + + // choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement) + const boundaryEl = + (boundaryRef && boundaryRef.current) || popup.parentElement; + if (!boundaryEl) return; + + const boundaryRect = boundaryEl.getBoundingClientRect(); + const triggerRect = triggerRef.current.getBoundingClientRect(); + + // reset styles first + popup.style.left = ""; + popup.style.right = ""; + popup.style.transform = ""; + popup.style.top = ""; + + const popupRect = popup.getBoundingClientRect(); + const parentRect = boundaryRect; // alias + + // Convert trigger center to parent coordinates + const triggerCenterX = + triggerRect.left + triggerRect.width / 2 - parentRect.left; + + // preferred left so popup center aligns to trigger center: + const preferredLeft = triggerCenterX - popupRect.width / 2; + + // Helpers to set styles in parent's coordinate system: + const setLeft = (leftPx) => { + popup.style.left = `${leftPx}px`; + popup.style.right = "auto"; + popup.style.transform = "none"; + }; + const setRight = (rightPx) => { + popup.style.left = "auto"; + popup.style.right = `${rightPx}px`; + popup.style.transform = "none"; + }; + + // If user forced align: + if (align === "left") { + // align popup's left to parent's left (0) + setLeft(0); + return; + } + if (align === "right") { + // align popup's right to parent's right (0) + setRight(0); + return; + } + if (align === "center") { + popup.style.left = "50%"; + popup.style.right = "auto"; + popup.style.transform = "translateX(-50%)"; + return; + } + + // align === "auto": try preferred centered position, but flip fully if overflow + // clamp preferredLeft to boundaries so it doesn't render partially outside + const leftIfCentered = Math.max( + 0, + Math.min(preferredLeft, parentRect.width - popupRect.width) + ); + + // if centered fits, use it + if (leftIfCentered === preferredLeft) { + setLeft(leftIfCentered); + return; + } + + // if centering would overflow right -> stick popup fully to left (left=0) + if (preferredLeft > parentRect.width - popupRect.width) { + // place popup so its right aligns to parent's right + // i.e., left = parent width - popup width + setLeft(parentRect.width - popupRect.width); + return; + } + + // if centering would overflow left -> stick popup fully to left=0 + if (preferredLeft < 0) { + setLeft(0); + return; + } + + // fallback center + setLeft(leftIfCentered); + }); + }, [visible, align, boundaryRef]); + + return ( +
    {children} - - {visible && ( -
    - {title &&
    {title}
    } -
    {content}
    -
    - )}
    - ); + + {visible && ( +
    e.stopPropagation()} + > + {title &&
    {title}
    } +
    {content}
    +
    + )} +
    +); + }; export default HoverPopup; - diff --git a/src/components/common/InputSuggestion.jsx b/src/components/common/InputSuggestion.jsx index de5579f4..0a79fa35 100644 --- a/src/components/common/InputSuggestion.jsx +++ b/src/components/common/InputSuggestion.jsx @@ -5,13 +5,13 @@ const InputSuggestions = ({ value, onChange, error, + disabled = false, }) => { const [filteredList, setFilteredList] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const handleInputChange = (e) => { const val = e.target.value; onChange(val); - const matches = organizationList.filter((org) => org.toLowerCase().includes(val.toLowerCase()) ); @@ -25,7 +25,7 @@ const InputSuggestions = ({ }; return ( -
    +
    { if (value) setShowSuggestions(true); }} + disabled={disabled} /> {showSuggestions && filteredList.length > 0 && (
      {filteredList.map((org) => (
    • handleSelectSuggestion(org)} - onMouseEnter={(e) => - (e.currentTarget.style.backgroundColor = "#f8f9fa") - } - onMouseLeave={(e) => - (e.currentTarget.style.backgroundColor = "transparent") - } + className={`dropdown-item ${ + org === value ? "active" : "" + }`} > {org}
    • ))}
    + )} {error && {error}} diff --git a/src/components/common/Loader.jsx b/src/components/common/Loader.jsx index 07078edb..6727117f 100644 --- a/src/components/common/Loader.jsx +++ b/src/components/common/Loader.jsx @@ -19,3 +19,13 @@ const Loader = () => { export default Loader; +export const SpinnerLoader = () => { + return ( +
    +
    + Loading... +
    +

    Loading data...

    +
    + ) +} \ No newline at end of file diff --git a/src/components/common/Modal.jsx b/src/components/common/Modal.jsx index 876ff0e8..e8292153 100644 --- a/src/components/common/Modal.jsx +++ b/src/components/common/Modal.jsx @@ -40,7 +40,7 @@ const Modal = ({
    {/* Body */} -
    {body}
    +
    {body}
    diff --git a/src/components/common/MultiSelectDropdown.css b/src/components/common/MultiSelectDropdown.css index b865cd1e..3bbd482c 100644 --- a/src/components/common/MultiSelectDropdown.css +++ b/src/components/common/MultiSelectDropdown.css @@ -10,7 +10,7 @@ display: flex; justify-content: space-between; align-items: center; - padding: 5px; + padding: 4px; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; @@ -99,7 +99,7 @@ .multi-select-dropdown-option.selected { background-color: #dbe7ff; - color: #0d6efd; + color: #696cff; } .multi-select-dropdown-option input[type="checkbox"] { diff --git a/src/components/common/OffcanvasComponent.jsx b/src/components/common/OffcanvasComponent.jsx new file mode 100644 index 00000000..88d3ba57 --- /dev/null +++ b/src/components/common/OffcanvasComponent.jsx @@ -0,0 +1,75 @@ +import React, { useEffect, useRef } from "react"; + +const OffcanvasComponent = ({ + id = "globalOffcanvas", + title = "Offcanvas Title", + placement = "start", // start | end | top | bottom + children, + show = false, + onClose = () => { }, +}) => { + const offcanvasRef = useRef(null); + const bsInstance = useRef(null); + + useEffect(() => { + if (!offcanvasRef.current) return; + + // initialize once + bsInstance.current = bootstrap.Offcanvas.getOrCreateInstance(offcanvasRef.current); + + const el = offcanvasRef.current; + const handleHide = () => onClose(); + + el.addEventListener("hidden.bs.offcanvas", handleHide); + + return () => { + el.removeEventListener("hidden.bs.offcanvas", handleHide); + }; + }, [onClose]); + + // react to `show` changes + useEffect(() => { + if (!bsInstance.current) return; + + if (show) bsInstance.current.show(); + else bsInstance.current.hide(); + }, [show]); + + return ( +
    +
    +
    +
    + + +
    + {title} +
    +
    + + + +
    + +
    {children}
    +
    +
    + ); +}; + +export default OffcanvasComponent; diff --git a/src/components/common/OfwLabel.jsx b/src/components/common/OfwLabel.jsx new file mode 100644 index 00000000..772ea11a --- /dev/null +++ b/src/components/common/OfwLabel.jsx @@ -0,0 +1,12 @@ +const OfwLabel = () => { + return ( + <> + + OnField + Work + .com + + + ); +}; +export default OfwLabel; diff --git a/src/components/common/PmsEmployeeInputTag.jsx b/src/components/common/PmsEmployeeInputTag.jsx new file mode 100644 index 00000000..c2bfdafa --- /dev/null +++ b/src/components/common/PmsEmployeeInputTag.jsx @@ -0,0 +1,290 @@ +import { useState, useEffect, useRef, useMemo } from "react"; +import { useController } from "react-hook-form"; +import { useDebounce } from "../../utils/appUtils"; +import { useEmployeesName, useUserCache } from "../../hooks/useEmployees"; +import Avatar from "./Avatar"; + +const PmsEmployeeInputTag = ({ + control, + name, + placeholder, + projectId, + forAll, + isApplicationUser = false, + disabled, +}) => { + const { + field: { value = [], onChange }, + } = useController({ name, control }); + + const { getUser, addToCache, userCache } = useUserCache(); + + const [search, setSearch] = useState(""); + const [showDropdown, setShowDropdown] = useState(false); + const [filteredUsers, setFilteredUsers] = useState([]); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + const activeIndexRef = useRef(-1); + + const debouncedSearch = useDebounce(search, 300); + const { data: employees, isLoading } = useEmployeesName( + projectId, + debouncedSearch, + forAll + ); + + useEffect(() => { + if (employees?.data?.length) { + setFilteredUsers(employees.data); + activeIndexRef.current = -1; + + employees.data.forEach((u) => addToCache(u.id, u)); + } else { + setFilteredUsers([]); + } + }, [employees]); + + // load selected employees not in filtered list + useEffect(() => { + if (!Array.isArray(value) || value.length === 0) return; + + value.forEach(async (id) => { + if (!userCache[id]) { + await getUser(id); // fetch and cache + } + }); + }, [value]); + + useEffect(() => { + const onDocClick = (e) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(e.target) && + !inputRef.current.contains(e.target) + ) { + setShowDropdown(false); + } + }; + document.addEventListener("mousedown", onDocClick); + return () => document.removeEventListener("mousedown", onDocClick); + }, []); + + const handleSelect = (user) => { + if (value.includes(user.id)) return; + const updated = [...value, user.id]; + onChange(updated); + setSearch(""); + setShowDropdown(false); + setTimeout(() => inputRef.current?.focus(), 0); + }; + + const handleRemove = (id) => { + const updated = value.filter((uid) => uid !== id); + onChange(updated); + }; + + const onInputKeyDown = (e) => { + if (!showDropdown) return; + const max = Math.max(0, filteredUsers.length - 1); + if (e.key === "ArrowDown") { + e.preventDefault(); + activeIndexRef.current = Math.min(max, activeIndexRef.current + 1); + scrollToActive(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + activeIndexRef.current = Math.max(0, activeIndexRef.current - 1); + scrollToActive(); + } else if (e.key === "Enter") { + e.preventDefault(); + const idx = activeIndexRef.current; + if (idx >= 0 && filteredUsers[idx]) handleSelect(filteredUsers[idx]); + } else if (e.key === "Escape") { + setShowDropdown(false); + } + }; + + const scrollToActive = () => { + const wrapper = dropdownRef.current?.querySelector( + ".tagify__dropdown__wrapper" + ); + const items = wrapper?.querySelectorAll(".tagify__dropdown__item"); + const idx = activeIndexRef.current; + if (items && items[idx]) { + const item = items[idx]; + const itemTop = item.offsetTop; + const itemBottom = itemTop + item.offsetHeight; + if (wrapper.scrollTop > itemTop) wrapper.scrollTop = itemTop; + else if (wrapper.scrollTop + wrapper.clientHeight < itemBottom) + wrapper.scrollTop = itemBottom - wrapper.clientHeight; + } + }; + + // ⬆️ FIX: allow null (not found yet) + const resolveUserById = (id) => { + return userCache[id] || filteredUsers.find((u) => u.id === id) || null; + }; + + const visibleUsers = useMemo(() => { + const baseList = isApplicationUser + ? (filteredUsers || []).filter((u) => u?.email) + : filteredUsers || []; + + const selectedUsers = + Array.isArray(value) && value.length + ? value.map((uid) => userCache[uid]).filter(Boolean) + : []; + + const merged = [ + ...selectedUsers, + ...baseList.filter((u) => !selectedUsers.some((s) => s.id === u.id)), + ]; + + return merged; + }, [filteredUsers, isApplicationUser, value, userCache]); + + return ( +
    + {value.map((id) => { + const u = resolveUserById(id); + if (!u) return null; + return ( + +
    + {u.photo ? ( + + {`${u.firstName + + ) : ( +
    + + {u.firstName?.[0] || ""} + {u.lastName?.[0] || ""} + +
    + )} + +
    + + {u.firstName} {u.lastName} + +
    +
    + +
    + ); +}; + +export default PmsEmployeeInputTag; diff --git a/src/components/common/ProgressBar.jsx b/src/components/common/ProgressBar.jsx index 947c6a00..cb6e7b8a 100644 --- a/src/components/common/ProgressBar.jsx +++ b/src/components/common/ProgressBar.jsx @@ -3,35 +3,65 @@ import React from "react"; const ProgressBar = ({ plannedWork = 100, completedWork = 0, - height = "8px", + height = "6px", className = "mb-4", rounded = true, + showLabel = true, }) => { const getProgress = (planned, completed) => { - if (!planned || planned === 0) return "0%"; - return `${Math.min((completed / planned) * 100, 100).toFixed(2)}%`; + if (!planned || planned === 0) return 0; + return Math.min((completed / planned) * 100, 100); }; - const progressStyle = { - width: getProgress(plannedWork, completedWork), + const percentage = getProgress(plannedWork, completedWork); + + const progressBarStyle = { + width: ` ${percentage.toFixed(2)}%`, + transition: "width 0.4s ease", + }; + + const containerStyle = { + height, + display: "flex", + alignItems: "center", + gap: "8px", }; return (
    -
    +
    +
    +
    +
    +
    + + {showLabel && ( + + {percentage.toFixed(2)}% + + )}
    ); }; export default ProgressBar; - diff --git a/src/components/common/SelectMultiple.jsx b/src/components/common/SelectMultiple.jsx index 7e13e290..8d4aae7d 100644 --- a/src/components/common/SelectMultiple.jsx +++ b/src/components/common/SelectMultiple.jsx @@ -8,24 +8,29 @@ const SelectMultiple = ({ name, options = [], label = "Select options", - labelKey = "name", + labelKey = "name", valueKey = "id", placeholder = "Please select...", - IsLoading = false,required = false + IsLoading = false, + required = false, }) => { - const { setValue, watch,register } = useFormContext(); + const { setValue, watch, register } = useFormContext(); useEffect(() => { - register(name, { value: [] }); -}, [register, name]); + register(name, { value: [] }); + }, [register, name]); -const selectedValues = watch(name) || []; + const selectedValues = watch(name) || []; const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(""); const containerRef = useRef(null); const dropdownRef = useRef(null); - const [dropdownStyles, setDropdownStyles] = useState({ top: 0, left: 0, width: 0 }); + const [dropdownStyles, setDropdownStyles] = useState({ + top: 0, + left: 0, + width: 0, + }); useEffect(() => { const handleClickOutside = (e) => { @@ -65,13 +70,12 @@ const selectedValues = watch(name) || []; }; const filteredOptions = (options || []).filter((item) => { - const label = getLabel(item); - return ( - typeof label === "string" && - label.toLowerCase().includes(searchText.toLowerCase()) - ); -}); - + const label = getLabel(item); + return ( + typeof label === "string" && + label.toLowerCase().includes(searchText.toLowerCase()) + ); + }); const dropdownElement = (
    setSearchText(e.target.value)} className="multi-select-dropdown-search-input" - style={{ width: "100%", padding: 4 }} + style={{ width: "100%", }} />
    @@ -109,8 +113,14 @@ const selectedValues = watch(name) || []; return (
    -
    +
    @@ -150,7 +164,9 @@ const selectedValues = watch(name) || []; > 0 ? "placeholder-style-selected" : "placeholder-style" + selectedValues.length > 0 + ? "placeholder-style-selected" + : "placeholder-style" } >
    @@ -159,7 +175,10 @@ const selectedValues = watch(name) || []; const found = options.find((opt) => opt[valueKey] === val); const label = found ? getLabel(found) : ""; return ( - + {label} ); diff --git a/src/components/common/TagInput.jsx b/src/components/common/TagInput.jsx index e39aca50..d0a015ae 100644 --- a/src/components/common/TagInput.jsx +++ b/src/components/common/TagInput.jsx @@ -2,7 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form"; import React, { useEffect, useState } from "react"; import Label from "./Label"; -const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] }) => { +const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = false, options = [] }) => { const { setValue, watch } = useFormContext(); const tags = watch(name) || []; const [input, setInput] = useState(""); @@ -33,29 +33,29 @@ const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] }) } }; -const handleChange = (e) => { - const val = e.target.value; - setInput(val); + const handleChange = (e) => { + const val = e.target.value; + setInput(val); - if (val) { - setSuggestions( - options - .filter((opt) => { - const label = typeof opt === "string" ? opt : opt.name; - return ( - label.toLowerCase().includes(val.toLowerCase()) && - !tags.some((t) => t.name === label) - ); - }) - .map((opt) => ({ - name: typeof opt === "string" ? opt : opt.name, - isActive: true, - })) - ); - } else { - setSuggestions([]); - } -}; + if (val) { + setSuggestions( + options + .filter((opt) => { + const label = typeof opt === "string" ? opt : opt.name; + return ( + label.toLowerCase().includes(val.toLowerCase()) && + !tags.some((t) => t.name === label) + ); + }) + .map((opt) => ({ + name: typeof opt === "string" ? opt : opt.name, + isActive: true, + })) + ); + } else { + setSuggestions([]); + } + }; const handleSuggestionClick = (sugg) => { handleAdd(sugg); @@ -65,13 +65,13 @@ const handleChange = (e) => { return ( <> -
    {tags.map((tag, index) => ( @@ -105,6 +105,9 @@ const handleChange = (e) => { outline: "none", flex: 1, minWidth: "120px", + backgroundColor: "white", + color: "black" + }} />
    diff --git a/src/components/common/TimeLine.jsx b/src/components/common/TimeLine.jsx new file mode 100644 index 00000000..c8ad33df --- /dev/null +++ b/src/components/common/TimeLine.jsx @@ -0,0 +1,104 @@ +import React from "react"; +import Avatar from "./Avatar"; +import Tooltip from "./Tooltip"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import moment from "moment"; + +const Timeline = ({ items = [], transparent = true }) => { + if(items.length === 0){ + return ( +
    +

    Not Action yet

    +
    + ) + } + return ( +
      + {items.map((item) => ( +
    • + + +
      +
      +
      {item.title}
      + {moment.utc(item.timeAgo).local().fromNow()} +
      + + {item.description &&

      {item.description}

      } + + {item.attachments && item.attachments.length > 0 && ( +
      + {item.attachments.map((att, i) => ( +
      + {att.icon && ( + file + )} + {att.name} +
      + ))} +
      + )} + + {item.users && item.users.length > 0 && ( +
      +
        + {item.users.map((user, i) => ( +
      • + {user.avatar ? ( + {user.name} + ) : ( + + )} +
      • + ))} +
      + + {item.users?.length === 1 && ( +
      +

      {`${item.users[0].firstName} ${item.users[0].lastName}`}

      + {item.users[0].role} +
      + )} + + +
      + )} +
      {item.userComment &&

      {item.userComment}

      }
      +
      +
    • + ))} +
    + ); +}; + +export default Timeline; diff --git a/src/components/common/Tooltip.jsx b/src/components/common/Tooltip.jsx new file mode 100644 index 00000000..527e839d --- /dev/null +++ b/src/components/common/Tooltip.jsx @@ -0,0 +1,24 @@ +import { useEffect } from "react"; + +const Tooltip = ({ text, placement = "top", children }) => { + useEffect(() => { + + const el = document.querySelector(`[data-tooltip-id="${text}"]`); + if (el) { + new window.bootstrap.Tooltip(el); + } + }, [text]); + + return ( + + {children} + + ); +}; +export default Tooltip; \ No newline at end of file diff --git a/src/components/gallary/GalleryFilterPanel.jsx b/src/components/gallary/GalleryFilterPanel.jsx index e91ba9d1..6518eb74 100644 --- a/src/components/gallary/GalleryFilterPanel.jsx +++ b/src/components/gallary/GalleryFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useImageGalleryFilter } from "../../hooks/useImageGallery"; import { useSelectedProject } from "../../slices/apiDataManager"; import { FormProvider, useForm } from "react-hook-form"; @@ -8,6 +8,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { defaultGalleryFilterValue, gallerySchema } from "./GallerySchema"; import SelectMultiple from "../common/SelectMultiple"; import { localToUtc } from "../../utils/appUtils"; +import { useLocation } from "react-router-dom"; const GalleryFilterPanel = ({ onApply }) => { const selectedProject = useSelectedProject(); @@ -27,7 +28,7 @@ const GalleryFilterPanel = ({ onApply }) => { const { handleSubmit, register, - setValue,reset, + setValue, reset, formState: { errors }, } = methods; @@ -37,15 +38,20 @@ const GalleryFilterPanel = ({ onApply }) => { startDate: localToUtc(formData.startDate), endDate: localToUtc(formData.endDate), }); - closePanel() + // closePanel() }; - const onClear=()=>{ + const onClear = () => { reset(defaultGalleryFilterValue); - setResetKey((prev) => prev + 1); - closePanel() + setResetKey((prev) => prev + 1); + // closePanel() } + const location = useLocation(); + useEffect(() => { + closePanel(); + }, [location]); + if (isLoading) return
    Loading....
    ; if (isError) return
    {error.message}
    ; return ( @@ -54,12 +60,13 @@ const GalleryFilterPanel = ({ onApply }) => {
    -
    diff --git a/src/components/gallary/ImageGalleryListView.jsx b/src/components/gallary/ImageGalleryListView.jsx index 945736e3..af5cfbc2 100644 --- a/src/components/gallary/ImageGalleryListView.jsx +++ b/src/components/gallary/ImageGalleryListView.jsx @@ -7,8 +7,7 @@ import { useSelectedProject } from "../../slices/apiDataManager"; import { ITEMS_PER_PAGE } from "../../utils/constants"; import Pagination from "../common/Pagination"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; -import Loader from "../common/Loader"; - +import { SpinnerLoader } from "../common/Loader"; const ImageGalleryListView = ({filter}) => { const [hoveredImage, setHoveredImage] = useState(null); const selectedProject = useSelectedProject(); @@ -28,18 +27,26 @@ const ImageGalleryListView = ({filter}) => { } }; - if (!data?.data?.length && !isLoading) { - return ( -

    - {selectedProject ? " No images match the selected filters.":"Please Select Project!"} -

    - ); - } + if (!data?.data?.length && !isLoading) { + return ( +
    + + {selectedProject + ? "No images match the selected filters." + : "Please Select Project!"} + +
    + ); +} - if (isLoading) { + + if (isLoading) { return (
    - +
    ); } diff --git a/src/components/gallary/ViewGallery.jsx b/src/components/gallary/ViewGallery.jsx index 869284e8..23286316 100644 --- a/src/components/gallary/ViewGallery.jsx +++ b/src/components/gallary/ViewGallery.jsx @@ -4,7 +4,6 @@ import { formatUTCToLocalTime } from "../../utils/dateUtils"; const ViewGallery = ({ batch, index }) => { const [loading, setLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(index); - console.log(batch); useEffect(() => { setCurrentIndex(index); }, [index, batch]); diff --git a/src/components/master/EditContactTag.jsx b/src/components/master/EditContactTag.jsx index bb19d58e..af1da622 100644 --- a/src/components/master/EditContactTag.jsx +++ b/src/components/master/EditContactTag.jsx @@ -41,7 +41,6 @@ const EditContactTag = ({ data, onClose }) => { name: formData.name, description: formData.description, }; - debugger updateContactTag({ id: data?.id, payload }); } // const onSubmit = (formdata) => { diff --git a/src/components/master/ManageExpenseType.jsx b/src/components/master/ManageExpenseCategory.jsx similarity index 86% rename from src/components/master/ManageExpenseType.jsx rename to src/components/master/ManageExpenseCategory.jsx index a98cedf3..7ac4472f 100644 --- a/src/components/master/ManageExpenseType.jsx +++ b/src/components/master/ManageExpenseCategory.jsx @@ -2,10 +2,7 @@ import React, { useEffect } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { - useCreateExpenseType, - useUpdateExpenseType, -} from "../../hooks/masterHook/useMaster"; +import { useCreateExpenseCategory, useUpdateExpenseCategory } from "../../hooks/masterHook/useMaster"; import Label from "../common/Label"; const ExpnseSchema = z.object({ @@ -14,7 +11,7 @@ const ExpnseSchema = z.object({ description: z.string().min(1, { message: "Description is required" }), }); -const ManageExpenseType = ({ data = null, onClose }) => { +const ManageExpenseCategory = ({ data = null, onClose }) => { const { register, handleSubmit, @@ -24,21 +21,21 @@ const ManageExpenseType = ({ data = null, onClose }) => { resolver: zodResolver(ExpnseSchema), defaultValues: { name: "", noOfPersonsRequired: false, description: "" }, }); - const { mutate: UpdateExpenseType, isPending:isPendingUpdate } = useUpdateExpenseType( + const { mutate: UpdateExpenseCategory, isPending:isPendingUpdate } = useUpdateExpenseCategory( () => onClose?.() ); - const { mutate: CreateExpenseType, isPending } = useCreateExpenseType(() => + const { mutate: CreateExpenseCategory, isPending } = useCreateExpenseCategory(() => onClose?.() ); const onSubmit = (payload) => { if (data) { - UpdateExpenseType({ + UpdateExpenseCategory({ id: data.id, payload: { ...payload, id: data.id }, }); } else { - CreateExpenseType(payload); + CreateExpenseCategory(payload); } }; @@ -112,4 +109,4 @@ const ManageExpenseType = ({ data = null, onClose }) => { ); }; -export default ManageExpenseType; +export default ManageExpenseCategory; diff --git a/src/components/master/MasterModal.jsx b/src/components/master/MasterModal.jsx index 44d2af60..ab873708 100644 --- a/src/components/master/MasterModal.jsx +++ b/src/components/master/MasterModal.jsx @@ -9,17 +9,17 @@ import CreateCategory from "./CreateContactCategory"; import CreateContactTag from "./CreateContactTag"; import EditContactCategory from "./EditContactCategory"; import EditContactTag from "./EditContactTag"; -import ManageExpenseType from "./ManageExpenseType"; import ManagePaymentMode from "./ManagePaymentMode"; import ManageExpenseStatus from "./ManageExpenseStatus"; import ManageDocumentCategory from "./ManageDocumentCategory"; import ManageDocumentType from "./ManageDocumentType"; import ManageServices from "./Services/ManageServices"; import ServiceGroups from "./Services/ServicesGroups"; +import ManagePaymentHead from "./paymentAdjustmentHead/ManagePaymentHead"; +import ManageExpenseCategory from "./ManageExpenseCategory"; const MasterModal = ({ modaldata, closeModal }) => { if (!modaldata?.modalType || modaldata.modalType === "delete") { - return null; } @@ -33,7 +33,7 @@ const MasterModal = ({ modaldata, closeModal }) => { ), "Job Role": , - "Edit-Job Role": , + "Edit-Job Role": , "Work Category": , "Edit-Work Category": , "Contact Category": , @@ -42,8 +42,8 @@ const MasterModal = ({ modaldata, closeModal }) => { ), "Contact Tag": , "Edit-Contact Tag": , - "Expense Type": , - "Edit-Expense Type": , + "Expense Category": , + "Edit-Expense Category": , "Payment Mode": , "Edit-Payment Mode": , "Expense Status": , @@ -58,24 +58,20 @@ const MasterModal = ({ modaldata, closeModal }) => { "Edit-Document Type": ( ), - "Services": ( - - ), - "Edit-Services": ( - - ), - "Manage-Services": ( - - ), + Services: , + "Edit-Services": , + "Manage-Services": , + "Payment Adjustment Head": , + "Edit-Payment Adjustment Head": }; return ( -<> -
    -

    {`${masterType, " ", modalType}`}

    +
    +
    +

    {`${(masterType, " ", modalType)}`}

    +
    + {modalComponents[modalType] || null}
    - { modalComponents[modalType] || null} - ); }; diff --git a/src/components/master/Services/ServicesGroups.jsx b/src/components/master/Services/ServicesGroups.jsx index b4262351..4cc94648 100644 --- a/src/components/master/Services/ServicesGroups.jsx +++ b/src/components/master/Services/ServicesGroups.jsx @@ -37,7 +37,6 @@ const ServiceGroups = ({ service }) => {
    -

    Manage Service

    {/* Service Header */} @@ -82,8 +81,8 @@ const ServiceGroups = ({ service }) => { ) : (
    - - {groups?.data?.map((group) => { + {!isLoading && groups?.data?.length === 0 && (

    No Group available.

    )} + {groups && groups?.data?.map((group) => { const isOpen = activeGroupId === group.id; return ( diff --git a/src/components/master/paymentAdjustmentHead/ManagePaymentHead.jsx b/src/components/master/paymentAdjustmentHead/ManagePaymentHead.jsx new file mode 100644 index 00000000..1afba4ab --- /dev/null +++ b/src/components/master/paymentAdjustmentHead/ManagePaymentHead.jsx @@ -0,0 +1,107 @@ +import React, { useEffect } from "react"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Label from "../../common/Label"; +import { useCreatePaymentAjustmentHead, useUpdatePaymentAjustmentHead } from "../../../hooks/masterHook/useMaster"; + +export const simpleFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), +}); + +const ManagePaymentHead = ({ data, onClose }) => { + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(simpleFormSchema), + defaultValues: { + name: "", + description: "", + }, + }); + + const {mutate:CreateAjustmentHead,isPending} = useCreatePaymentAjustmentHead(()=>{ + handleClose?.() + }); + const {mutate:UpdateAjustmentHead,isPending:isUpdating} = useUpdatePaymentAjustmentHead(()=>{ + handleClose?.() + }) + const onSubmit = (formData) => { + if(data){ + let id = data?.id; + const payload = { + ...formData, + id:id, + } + UpdateAjustmentHead({id:id,payload:payload}) + }else{ + let payload={ + ...formData + } + CreateAjustmentHead(payload) + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + description: data.description, + }); + } + }, [data]); + const handleClose = () => { + reset(); + onClose(); + }; + return ( +
    + +
    + + + {errors.name && ( +
    {errors.name.message}
    + )} +
    + +
    + + -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - - {/* Contact Us: End */} + +
    - - {/* / Sections:End */} - - {/* Footer: Start */} - - {/* Footer: End */} -
    + ); }; + export default LandingPage; diff --git a/src/pages/Home/LandingPageOld.css b/src/pages/Home/LandingPageOld.css new file mode 100644 index 00000000..16c8dbbc --- /dev/null +++ b/src/pages/Home/LandingPageOld.css @@ -0,0 +1,668 @@ +.section-py { + padding: 6.25rem 0; +} + +.section-py .heading { + font-size: 1.625rem; +} +@media (max-width: 1199.98px) { + .section-py { + padding: 4rem 0; + } +} +@media (max-width: 767.98px) { + .section-py { + padding: 3rem 0; + } +} + +.first-section-pt { + padding-top: 11.28rem; +} +@media (max-width: 1199.98px) { + .first-section-pt { + padding-top: 7.5rem; + } +} + +.card[class*="card-hover-border-"] { + transition: all 0.2s ease-in-out; +} + +.banner-bg-img { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + object-fit: cover; + object-position: left; +} + +.section-title-img { + height: 100%; + width: 120%; + inset-inline-start: -12%; + top: 10px; +} + +/* .light-style body { + background-color: #fff; +} */ + +.dark-style body { + background-color: #2b2c40; +} + +nav.layout-navbar { + /* backdrop-filter: unset; */ + /* background-color: transparent !important; */ + /* background-color: rgba(214, 36, 33, 0.88) !important; */ +} + +nav.layout-navbar::before { + position: absolute; + display: block; + block-size: 100%; + content: ""; + inline-size: 100%; + inset-block-start: 0; + inset-inline-start: 0; +} + +nav.layout-navbar .navbar.landing-navbar { + --bs-front-navbar-bg: rgba(var(--bs-paper-bg-rgb), 0.38); + --bs-front-navbar-border-color: rgba(var(--bs-paper-bg-rgb), 0.68); + border: 2px solid var(--bs-front-navbar-border-color); + background-color: var(--bs-front-navbar-bg); + margin-block-start: 0rem; + padding-block: 0.614rem; + transform: unset; + transition: all 0.2s ease-in-out; + border-radius: 0.375rem; +} + +nav.layout-navbar.navbar-active::after { + backdrop-filter: saturate(100%) blur(6px); + -webkit-backdrop-filter: saturate(100%) blur(6px); +} + +.navbar.landing-navbar { + box-shadow: none; + transition: all 0.2s ease-in-out; + transform: unset !important; + padding-top: 0.614rem; + padding-bottom: 0.614rem; + margin-top: 1rem; + border-width: 2px; + border-style: solid; + border-radius: 0.375rem; +} +.navbar.landing-navbar .navbar-nav .nav-link { + padding: 0.5rem 0.625rem; + margin-inline-end: 0.625rem; +} +@media (max-width: 1199.98px) { + .navbar.landing-navbar .navbar-nav .nav-link { + padding-left: 0.5rem; + padding-right: 0.5rem; + margin-inline-end: 0; + } +} +.navbar.landing-navbar .navbar-nav .nav-item:last-child .nav-link { + margin-inline-end: 0; +} +@media (min-width: 992px) { + .navbar.landing-navbar .navbar-nav .nav-item.mega-dropdown > .dropdown-menu { + max-width: 1300px; + inset-inline-start: 50% !important; + transform: translateX(-50%); + top: 100%; + } +} +@media (max-width: 991.98px) { + .navbar.landing-navbar .navbar-nav .nav-item.mega-dropdown > .dropdown-menu { + background: transparent; + box-shadow: none; + border: none; + } +} +.navbar.landing-navbar + .navbar-nav + .nav-item.mega-dropdown + > .dropdown-menu + .mega-dropdown-link { + padding-left: 0; + padding-right: 0; + margin: 0; + font-weight: 400; +} +.navbar.landing-navbar + .navbar-nav + .nav-item.mega-dropdown + > .dropdown-menu + .mega-dropdown-link + i { + font-size: 1rem; + font-weight: 700; + margin-top: -0.125rem; +} +.navbar.landing-navbar .navbar-nav .nav-item .nav-img-col, +.navbar.landing-navbar .navbar-nav .nav-item .nav-img-col img { + border-radius: 0.625rem; +} +@media (max-width: 991.98px) { + .navbar.landing-navbar .landing-menu-overlay { + position: fixed; + display: none; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(34, 48, 62, 0.78); + transition: all 0.2s ease-in-out; + z-index: 9998; + } + .navbar.landing-navbar .landing-nav-menu { + position: fixed; + display: block !important; + height: 100%; + max-width: 300px; + width: 80%; + padding: 1rem; + inset-inline-start: -100%; + top: 0; + overflow-y: auto; + transition: all 0.3s ease-in-out; + z-index: 9999; + } + .navbar.landing-navbar .landing-nav-menu.show { + inset-inline-start: 0; + } + .navbar.landing-navbar .landing-nav-menu.show ~ .landing-menu-overlay { + display: block; + } +} + +.light-style .layout-navbar .navbar.landing-navbar { + border-color: rgba(255, 255, 255, 100); + background: rgba(255, 255, 255, 50); +} +.light-style .layout-navbar .navbar.landing-navbar .navbar-nav .nav-link { + color: #384551; +} +.light-style + .layout-navbar + .navbar.landing-navbar + .navbar-nav + .show + > .nav-link, +.light-style + .layout-navbar + .navbar.landing-navbar + .navbar-nav + .active + > .nav-link, +.light-style .layout-navbar .navbar.landing-navbar .navbar-nav .nav-link.show, +.light-style + .layout-navbar + .navbar.landing-navbar + .navbar-nav + .nav-link.active { + color: #696cff !important; +} +.light-style + .layout-navbar + .navbar.landing-navbar + .navbar-nav + .nav-item.mega-dropdown + > .dropdown-menu + .mega-dropdown-link + i { + color: #646e78; +} +@media (max-width: 991.98px) { + .light-style .layout-navbar .navbar.landing-navbar .landing-nav-menu { + background-color: #fff; + } +} +.light-style .layout-navbar.navbar-active .navbar.landing-navbar { + background: #fff; + box-shadow: 0 0.125rem 0.375rem 0 rgba(34, 48, 62, 0.08); +} +.light-style .layout-navbar .menu-text { + color: #384551; +} + +.dark-style .layout-navbar .navbar.landing-navbar { + border-color: rgba(65, 65, 95, 0.68); + background-color: rgba(65, 65, 95, 0.38); +} +.dark-style .layout-navbar .navbar.landing-navbar .navbar-nav .nav-link { + color: #d5d5e2; +} +.dark-style .layout-navbar .navbar.landing-navbar .navbar-nav .show > .nav-link, +.dark-style + .layout-navbar + .navbar.landing-navbar + .navbar-nav + .active + > .nav-link, +.dark-style .layout-navbar .navbar.landing-navbar .navbar-nav .nav-link.show, +.dark-style .layout-navbar .navbar.landing-navbar .navbar-nav .nav-link.active { + color: #696cff !important; +} +.dark-style + .layout-navbar + .navbar.landing-navbar + .navbar-nav + .nav-item.mega-dropdown + > .dropdown-menu + .mega-dropdown-link + i { + color: #b2b2c4; +} +@media (max-width: 991.98px) { + .dark-style .layout-navbar .navbar.landing-navbar .landing-nav-menu { + background-color: #2b2c40; + } +} +.dark-style .layout-navbar .navbar .menu-text { + color: #d5d5e2; +} +.dark-style .layout-navbar.navbar-active .navbar.landing-navbar { + background: #2b2c40; + border-color: #2b2c40; + box-shadow: 0 0.125rem 0.375rem 0 rgba(20, 20, 29, 0.2); +} + +@media (min-width: 992px) { + [dir="rtl"] + .navbar.landing-navbar + .navbar-nav + .nav-item.mega-dropdown + > .dropdown-menu { + transform: translateX(50%); + } +} + +.landing-footer .footer-link, +.landing-footer .footer-text { + color: #fff; + opacity: 0.78; +} +.landing-footer .footer-title { + color: #fff; + opacity: 0.92; +} +.landing-footer .footer-bottom-text { + color: #d3d4dc; +} +.landing-footer .footer-bottom { + background-color: #f44336; +} +.landing-footer .footer-link { + transition: all 0.2s ease-in-out; +} +.landing-footer .footer-link:hover { + opacity: 1; +} +.landing-footer .footer-top { + padding-top: 1.3rem; + padding-bottom: 1.3rem; + border-top-left-radius: 1.75rem; + border-top-right-radius: 1.75rem; + background-color: #f44336; +} +@media (max-width: 767.98px) { + .landing-footer .footer-top { + padding: 3rem 0; + } +} +.landing-footer .footer-top .footer-bg { + object-position: center; +} +@media (min-width: 992px) { + .landing-footer .footer-logo-description { + max-width: 385px; + } +} +.landing-footer .footer-form { + max-width: 22.25rem; +} +.landing-footer .footer-form input { + background-color: transparent; + border-color: #4e4f6c; + color: #fff; +} +.landing-footer .footer-form input:hover:not([disabled]):not([focus]) { + border-color: #4e4f6c; +} +.landing-footer .footer-form input::placeholder { + color: rgba(255, 255, 255, 0.5); +} +.landing-footer .footer-form label { + color: #d5d5e2; +} + +.section-py { + padding: 6.25rem 0; +} +@media (max-width: 1199.98px) { + .section-py { + padding: 5rem 0; + } +} +@media (max-width: 767.98px) { + .section-py { + padding: 3rem 0; + } +} + +.landing-hero { + border-radius: 0 0 3.5rem 3.5rem; + padding-top: 8.2rem; +} +.landing-hero::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; +} +@media (min-width: 992px) { + .landing-hero .hero-text-box { + /* max-width: 34.375rem; */ + max-width: 70%; + margin: 0 auto; + } +} +.landing-hero .hero-title { + background: linear-gradient( + to right, + #28c76f 0%, + #5a4aff 47.92%, + #ff3739 100% + ); + background-size: 200% auto; + color: #384551; + font-size: calc(1.3875rem + 1.65vw); + background-clip: text; + line-height: 1.2; + text-fill-color: transparent; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shine 2s ease-in-out infinite alternate; +} +@media (min-width: 1200px) { + .landing-hero .hero-title { + font-size: 2.625rem; + } +} +.landing-hero .landing-hero-btn .hero-btn-item { + inset-inline-start: -94%; + top: 65%; +} +.landing-hero .hero-animation-img { + margin-bottom: -32rem; +} +.animation-img { + border: 1px soid red; +} +@media (max-width: 1199.98px) { + .landing-hero .hero-animation-img { + margin-bottom: -20rem; + } +} +@media (max-width: 575.98px) { + .landing-hero .hero-animation-img { + margin-bottom: -10rem; + } +} +.landing-hero .hero-animation-img .hero-dashboard-img { + width: 80%; + margin: 0 auto; + will-change: transform; + transform-style: preserve-3d; + transition: all 0.1s; +} +.landing-hero .hero-animation-img .hero-dashboard-img img { + width: 100%; +} + +.landing-hero-blank { + padding-top: 26rem; +} +@media (max-width: 1199.98px) { + .landing-hero-blank { + padding-top: 15rem; + } +} +@media (max-width: 575.98px) { + .landing-hero-blank { + padding-top: 7rem; + } +} + +@keyframes shine { + 0% { + background-position: 0% 50%; + } + 80% { + background-position: 50% 90%; + } + 100% { + background-position: 91% 100%; + } +} +.landing-features + .features-icon-wrapper + .features-icon-box + .features-icon-description { + max-width: 19.25rem; + margin: 0 auto; +} + +.landing-reviews { + border-top-left-radius: 3.75rem; + border-top-right-radius: 3.75rem; +} +.landing-reviews .swiper-reviews-carousel .swiper-button-prev, +.landing-reviews .swiper-reviews-carousel .swiper-button-next { + display: none; +} +.landing-reviews .swiper-reviews-carousel .swiper-slide { + height: auto; + padding: 0.8125rem; +} +.landing-reviews .swiper-reviews-carousel .client-logo { + height: 1.375rem; + object-fit: contain; +} +.landing-reviews .swiper-logo-carousel { + padding-bottom: 6.25rem; +} +.landing-reviews .swiper-logo-carousel .swiper { + max-width: 45rem; +} +.landing-reviews .swiper-logo-carousel .swiper .swiper-slide { + display: flex; + justify-content: center; +} +.landing-reviews .swiper-logo-carousel .swiper .client-logo { + max-height: 2.5rem; + max-width: 95%; + object-fit: contain; +} + +.landing-team .card, +.landing-team .card .team-image-box { + border-top-left-radius: 5.625rem; + border-top-right-radius: 1.25rem; +} +.landing-team .card .card-body { + border-bottom-left-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; +} +.landing-team .team-image-box { + height: 11.5625rem; +} +.landing-team .team-image-box .card-img-position { + height: 15rem; + transform: translateX(-50%); + max-width: 100%; + object-fit: cover; +} +@media (max-width: 991.98px) { + .landing-team .team-image-box .card-img-position { + height: 13rem; + } +} +@media (max-width: 575.98px) { + .landing-team .team-image-box { + height: 11rem; + } +} +.landing-team .card .team-media-icons i { + transition: all 0.2s ease-in-out; +} + +.landing-pricing { + border-radius: 3.75rem; +} +.landing-pricing .pricing-plans-item { + inset-inline-end: -56%; + bottom: -0.5rem; +} +@media (max-width: 767.98px) { + .landing-pricing .pricing-plans-item { + inset-inline-end: 0; + bottom: 1rem; + } +} +.landing-pricing .pricing-list .badge.badge-center { + width: 1rem; + height: 1rem; +} +.landing-pricing .pricing-list .badge.badge-center i { + margin-top: -5px; +} +.landing-pricing .price-yearly-toggle { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); +} +.landing-pricing .card .card-header, +.landing-pricing .card .card-body { + padding: 2rem; + padding-top: 3rem; +} + +.landing-faq { + border-top-left-radius: 3.75rem; + border-top-right-radius: 3.75rem; +} +.landing-faq .faq-image { + max-width: 20rem; + width: 80%; +} + +.landing-cta .cta-title { + font-size: 2.125rem; +} +@media (max-width: 767.98px) { + .landing-cta .cta-title { + font-size: 1.8rem; + } +} + +.landing-contact .text-heading { + overflow-wrap: anywhere; +} +.landing-contact .contact-img-box, +.landing-contact .contact-img-box .contact-img { + border-radius: 3.75rem 0.375rem 0.375rem 0.375rem; +} +.landing-contact .contact-img-box .contact-border-img { + inset-block-start: -2.5rem; + inset-inline-start: -2.8125rem; +} + +.light-style .landing-hero { + background: linear-gradient(138.18deg, #eae8fd 0%, #fce5e6 94.44%); +} +.light-style .landing-hero::after { + background-color: #fff; +} + +.dark-style .landing-hero { + background: #1e2130; +} +.dark-style .landing-hero::after { + background-color: #2b2c40; +} + +[dir="rtl"] .landing-reviews-btns { + display: flex; + justify-content: flex-end; + flex-direction: row-reverse; + gap: 1rem; +} +[dir="rtl"] .landing-team .team-image-box .card-img-position { + transform: translateX(50%) !important; +} +[dir="rtl"] .landing-pricing .switch .switch-label { + padding-right: 0; +} +[dir="rtl"] .landing-pricing .switch .switch-label:first-child { + padding-left: 0.5rem; +} +[dir="rtl"] .landing-pricing .switch .switch-input ~ .switch-label { + padding-right: 3rem; +} +[dir="rtl"] .landing-contact .contact-img-box { + border-radius: 0.375rem 3.75rem 0.375rem 0.375rem; +} +[dir="rtl"] .landing-contact .contact-img-box::before { + inset-block-start: -1.875rem; + inset-inline-start: -3.125rem; + transform: rotate(90deg); +} + +.swiper { + width: 100%; + height: 100%; + border-radius: 10px; +} + +.swiper-slide { + text-align: center; + font-size: 18px; + + /* Center slide text vertically */ + display: flex; + justify-content: center; + align-items: center; +} + +.swiper-slide img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.light-style .landing-hero { + background: linear-gradient(138.18deg, #eae8fd, #ede7e7 94.44%); +} + +.text-green { + color: #49bf3c !important; +} + +.text-blue { + color: var(--bs-blue); +} diff --git a/src/pages/Home/LandingPageOld.jsx b/src/pages/Home/LandingPageOld.jsx new file mode 100644 index 00000000..df546e60 --- /dev/null +++ b/src/pages/Home/LandingPageOld.jsx @@ -0,0 +1,1288 @@ +import { noop } from "@tanstack/react-query"; +import React, { useEffect, useMemo, useState } from "react"; + +import "./LandingPageOld.css"; +import { Link } from "react-router-dom"; + +import { Swiper, SwiperSlide } from "swiper/react"; +// import required modules +import { EffectFlip, Autoplay, Pagination, Navigation } from "swiper/modules"; +// Import Swiper styles +import "swiper/css"; +import "swiper/css/navigation"; +import SwaperSlideContent from "./SwaperSlideContent"; +import SwaperBlogContent from "./SwaperBlogContent"; +import SubscriptionPlans from "./SubscriptionPlans"; + +const swiperConfig = { + spaceBetween: 30, + centeredSlides: true, + rewind: true, + autoplay: { + delay: 3500, + disableOnInteraction: false, + }, + pagination: { + clickable: true, + }, + keyboard: { + enabled: true, + }, + navigation: false, + modules: [EffectFlip, Autoplay, Pagination, Navigation], + className: "mySwiper", +}; + +const LandingPageOld = () => { + const [swiperRef, setSwiperRef] = useState(null); + return ( +
    + + {/* Navbar: End */} + + {/* Sections:Start */} + +
    + {/* Hero: Start */} +
    +
    +
    +
    +

    + All-in-one platform to manage projects, people, and resources + seamlessly. +

    + {/*

    + Production-ready & easy to use Admin Template +
    + for Reliability and Customizability. +

    */} +
    + {/* + Join community + Join community arrow + */} + + Get Early Access + + + Request a Demo + +
    +
    + +
    +
    + {}} + onSwiper={(swiper) => {}} + > + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    + {/* Hero: End */} + + {/* Useful features: Start */} +
    +
    +
    + + Useful Features + +
    +

    + + From tasks to teams, documents to inventory — + {/* laptop charging */} + + everything your business needs in one place. +

    +

    + All-in-one platform to manage projects, people, and resources + seamlessly. +

    +
    +
    +
    + laptop charging +
    +
    Project & Task Management
    +

    + Plan, assign, and track projects and tasks seamlessly for + better team collaboration. +

    +
    +
    +
    + transition up +
    +
    Attendance & Leave Tracking
    +

    + Monitor employee attendance and manage leave requests with + ease. +

    +
    +
    +
    + edit +
    +
    Role-based Permissions
    +

    + Securely control access with customizable roles and + permissions. +

    +
    +
    +
    + 3d select solid +
    +
    Expense & Budget Tracking
    +

    + Keep projects on budget with real-time expense and cost + management. +

    +
    +
    +
    + user +
    +
    Reporting & Analytics
    +

    + Gain actionable insights through powerful reports and + analytics dashboards. +

    +
    +
    +
    + keyboard +
    +
    Document Management
    +

    + Organize, share, and access all your project and employee + documents in one place. +

    +
    +
    +
    + keyboard +
    +
    + Service Provider & Subcontractor Tracking +
    +

    + Manage multiple service providers and subcontractors + efficiently within projects. +

    +
    {" "} +
    +
    + keyboard +
    +
    Inventory Management
    +

    + Track materials, supplies, and assets — never run short again. +

    +
    {" "} +
    +
    +
    + keyboard +
    +
    Directory
    +

    + Your team, suppliers, vendors organized and connected in one + unified directory. +

    +
    {" "} +
    +
    + {/* Useful features: End */} + + {/* */} + + {/* */} + + {/* Pricing plans: Start */} +
    +
    +
    + + Pricing Plans + +
    +

    + + Tailored pricing plans + {/* laptop charging */} + + designed for you +

    +

    + No matter which plan you choose, you’ll get access to powerful + features. Choose the best plan to fit your needs. +

    + {/* */} + +
    +
    + {/* Pricing plans: End */} + + {/* Fun facts: Start */} +
    +
    +
    +
    +
    +
    + laptop +

    7.1k+

    +

    + Support Tickets +
    + Resolved +

    +
    +
    +
    +
    +
    +
    + laptop +

    50k+

    +

    + Join creatives +
    + community +

    +
    +
    +
    +
    +
    +
    + laptop +

    4.8/5

    +

    + Highly Rated +
    + Products +

    +
    +
    +
    +
    +
    +
    + laptop +

    100%

    +

    + Money Back +
    + Guarantee +

    +
    +
    +
    +
    +
    +
    + {/* Fun facts: End */} + + {/* FAQ: Start */} +
    +
    +
    + FAQ +
    +

    + Frequently Asked + + Questions + {/* laptop charging */} + +

    +

    + Browse through these FAQs to find answers to commonly asked + questions. +

    +
    +
    +
    + faq boy with logos +
    +
    +
    +
    +
    +

    + +

    + +
    +
    + A smart Project Management System designed to bring + teams, tasks, and timelines together in one place. With + AI-driven insights, role-based access, and seamless + reporting, it empowers organizations to deliver projects + faster and smarter. +
    +
    +
    +
    +

    + +

    +
    +
    + Yes, you have full flexibility to manage your + subscription. You can upgrade to a higher plan to unlock + more features, downgrade to a smaller plan if your needs + change, or cancel your subscription anytime. Plan + changes take effect instantly, and billing adjustments + are applied on a pro-rated basis. +
    +
    +
    +
    +

    + +

    +
    +
    + Security is at the core of OnFieldWork.com. We use + industry-standard encryption (SSL/TLS) to protect data + in transit and advanced encryption to safeguard data at + rest. Role-based access controls ensure that only + authorized users can access sensitive information. Our + system is hosted on secure, cloud-ready infrastructure + with regular backups, monitoring, and compliance with + best practices to keep your data safe and available at + all times. +
    +
    +
    +
    +

    + +

    +
    +
    + You can reach our support team anytime through the + in-app help center, email, or live chat. We also provide + a detailed knowledge base and FAQs to guide you through + common queries. For personalized assistance, our support + specialists are always ready to help you. +
    +
    +
    +
    +

    + +

    +
    +
    + OnFieldWork.com operate under a proprietary license + combined with a subscription model. This means customers + don’t own the software but are granted the right to + access and use it through the cloud under our Terms of + Service. Depending on the plan, licensing may be based + on users, features, or usage, and you can upgrade, + downgrade, or cancel at any time. non! +
    +
    +
    +
    +

    + +

    +
    +
    + Yes, OnFieldWork.com is designed to be flexible and + adaptable. You can customize workflows, user roles, + permissions, and reporting to match your organization’s + unique processes. Depending on your plan, we also + support advanced customization such as integrating with + third-party tools, adding custom fields, and tailoring + modules to fit your business requirements. +
    +
    +
    {" "} +
    +
    +
    +
    +
    + {/* FAQ: End */} + + {/* CTA: Start */} +
    + cta image +
    +
    + Contact US +
    + +
    +
    + hero elements +
    +
    +
    + {" "} +

    + + Let's Work + {/* laptop charging */} + + Together +

    +

    + Any question or remark? just write us a message +

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +

    Phone

    +
    + + +91 70288 83755 + +
    +
    +
    +
    +
    +
    +
    +
    + Ready to Get Started? +
    +
    + Start your project with a free trial +
    + {/* + Get Started + {" "} */} + + Request a Demo + +
    +
    +
    +
    +
    + {/* CTA: End */} + + {/* Contact Us: Start */} +
    +
    +
    + Contact US +
    +

    + + Let's work + laptop charging + + together +

    +

    + Any question or remark? just write us a message +

    +
    +
    +
    + contact border + contact customer service +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +

    Phone

    +
    + + +1234 568 963 + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Send a message

    +

    + If you would like to discuss anything related to payment, + account, licensing, +
    + partnerships, or have pre-sales questions, you’re at the + right place. +

    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + {/* Contact Us: End */} +
    + + {/* / Sections:End */} + + {/* Footer: Start */} + + {/* Footer: End */} +
    + ); +}; +export default LandingPageOld; diff --git a/src/pages/Home/MakeSubscription.jsx b/src/pages/Home/MakeSubscription.jsx new file mode 100644 index 00000000..359e6e31 --- /dev/null +++ b/src/pages/Home/MakeSubscription.jsx @@ -0,0 +1,171 @@ +import React, { useState, useMemo } from "react"; + +import SubscriptionLayout from "../../components/UserSubscription/SubscriptionLayout"; +import SubscriptionForm from "../../components/UserSubscription/SubscriptionForm"; +import ProcessedPayment from "../../components/UserSubscription/ProcessedPayment"; +import VerifiedPayment from "../../components/UserSubscription/VerifiedPayment"; +import SelectPlan from "../../components/UserSubscription/SelectPlan"; +import Review from "../../components/UserSubscription/Review"; + +const MakeSubscription = () => { + const [currentStep, setCurrentStep] = useState(1); + const [responsePayment, setResponsePayment] = useState(null); + + const [stepStatus, setStepStatus] = useState({ + 1: "pending", + 2: "pending", + 3: "pending", + 4: "pending", + 5: "pending", + }); + + const handleVerification = (resp) => { + setResponsePayment(resp); + if (resp?.success) { + setStepStatus((prev) => ({ ...prev, 4: "success" })); + setCurrentStep(5); + } else { + setStepStatus((prev) => ({ ...prev, 4: "failed" })); + } + }; + const handleNext = () => { + setStepStatus((prev) => ({ + ...prev, + [currentStep]: "success", + [currentStep + 1]: "pending", + })); + + setCurrentStep((prev) => prev + 1); + }; + + const checkOut_Steps = [ + { + name: "Client Info", + component: () => ( + + ), + }, + { + name: "Select Plan", + component: () => ( + + ), + }, + { + name: "Review", + component: () => ( + handleVerification(resp)} + resetPaymentStep={() => + setStepStatus((prev) => ({ ...prev, 4: "pending" })) + } + setCurrentStep={setCurrentStep} + setStepStatus={setStepStatus} + resetFormStep={() => { + setStepStatus((prev) => ({ ...prev, 1: "pending" })); + setCurrentStep(1); + }} + /> + ), + }, + { + name: "Payment", + component: () => ( + handleVerification(resp)} + resetPaymentStep={() => + setStepStatus((prev) => ({ ...prev, 4: "pending" })) + } + setCurrentStep={setCurrentStep} + setStepStatus={setStepStatus} + resetFormStep={() => { + setStepStatus((prev) => ({ ...prev, 1: "pending" })); + setCurrentStep(1); + }} + /> + ), + }, + { + name: "Verified", + component: () => ( + + ), + }, + ]; + + return ( + <> + + +
    + +
    + + ); +}; + +export default MakeSubscription; diff --git a/src/pages/Home/SubscriptionPlans.jsx b/src/pages/Home/SubscriptionPlans.jsx index 24be62f9..59ef4a7e 100644 --- a/src/pages/Home/SubscriptionPlans.jsx +++ b/src/pages/Home/SubscriptionPlans.jsx @@ -2,37 +2,26 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; import { Link } from "react-router-dom"; import PlanCardSkeleton from "./PlanCardSkeleton"; +import { useSubscription } from "../../hooks/useAuth"; const SubscriptionPlans = () => { const [plans, setPlans] = useState([]); const [frequency, setFrequency] = useState(1); + const { data, isLoading, isError, error } = useSubscription(frequency); const [loading, setLoading] = useState(false); - useEffect(() => { - const fetchPlans = async () => { - try { - setLoading(true); - const res = await axios.get( - `http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`, - { headers: { "Content-Type": "application/json" } } - ); - setPlans(res.data?.data || []); - } catch (err) { - console.error("Error fetching plans:", err); - } finally { - setLoading(false); - } - }; - fetchPlans(); - }, [frequency]); - const frequencyLabel = (freq) => { switch (freq) { - case 0: return "1 mo"; - case 1: return "3 mo"; - case 2: return "6 mo"; - case 3: return "1 yr"; - default: return "mo"; + case 0: + return "1 mo"; + case 1: + return "3 mo"; + case 2: + return "6 mo"; + case 3: + return "1 yr"; + default: + return "mo"; } }; @@ -41,38 +30,49 @@ const SubscriptionPlans = () => { {/* Frequency Switcher */}
    - {["Monthly", "Quarterly", "Half-Yearly", "Yearly"].map((label, idx) => ( - - ))} + {["Monthly", "Quarterly", "Half-Yearly", "Yearly"].map( + (label, idx) => ( + + ) + )}
    {/* Cards */}
    - {loading ? ( + {isLoading ? ( // Show 3 skeletons <> - ) : plans.length === 0 ? ( + ) : data?.length === 0 ? (
    No plans found
    + ) : isError ? ( +
    +

    {error.message}

    +

    {error.name}

    +
    ) : ( - plans.map((plan) => ( + data.map((plan) => (
    {/* Header */}
    -

    {plan.planName}

    +

    + {plan.planName} +

    {plan.description}

    @@ -80,7 +80,9 @@ const SubscriptionPlans = () => {

    {plan.currency?.symbol} {plan.price} - / {frequencyLabel(frequency)} + + / {frequencyLabel(frequency)} +

    @@ -100,13 +102,13 @@ const SubscriptionPlans = () => {
    Features
    -
      +
        {plan.features?.modules && Object.values(plan.features.modules).map((mod) => mod && mod.name ? (
      • {mod.enabled ? ( @@ -121,9 +123,15 @@ const SubscriptionPlans = () => { {/* Button */}
        + + Subscribe + Request a Demo @@ -133,7 +141,6 @@ const SubscriptionPlans = () => { )) )}
        -
    ); }; diff --git a/src/pages/Home/SubscriptionSummary.jsx b/src/pages/Home/SubscriptionSummary.jsx new file mode 100644 index 00000000..733ec81a --- /dev/null +++ b/src/pages/Home/SubscriptionSummary.jsx @@ -0,0 +1,117 @@ +import React from "react"; + +const SubscriptionSummary = () => { + const options = [ + { + id: 1, + title: "Design", + description: "Cake sugar plum fruitcake I love sweet roll jelly-o.", + svg: ( + + + + ), + }, + { + id: 2, + title: "Development", + description: "Cake sugar plum fruitcake I love sweet roll jelly-o.", + svg: ( + + + + ), + }, + { + id: 3, + title: "Native App", + description: "Cake sugar plum fruitcake I love sweet roll jelly-o.", + svg: ( + + + + ), + }, + ]; + + return ( +
    + {/* Section title aligned at start */} +
    +

    Summary

    +
    + +
    +
    +
    + {options.map((opt) => ( +
    +
    + +
    +
    + ))} +
    +
    + +
    + {/* Add your right-side content here */} +
    +
    +
    + + ); +}; + +export default SubscriptionSummary; diff --git a/src/pages/Home/SwaperSlideImages.jsx b/src/pages/Home/SwaperSlideImages.jsx new file mode 100644 index 00000000..9300b12c --- /dev/null +++ b/src/pages/Home/SwaperSlideImages.jsx @@ -0,0 +1,26 @@ +import { SwiperSlide } from "swiper/react"; + +const SwaperSlideImages = ({ + ImageUrl = "../../../public/assets/img/backgrounds/18.jpg", + Title = "", + Body = "", + ContentAlign = "right", +}) => { + return ( +
    + Card image cap + {/*
    +
    Card title
    +

    + Some quick example text to build on the card title and make up the + bulk of the card's content. +

    + + Go somewhere + +
    */} +
    + ); +}; + +export default SwaperSlideImages; diff --git a/src/pages/Organization/OrganizationPage.jsx b/src/pages/Organization/OrganizationPage.jsx index 21a28263..017245e9 100644 --- a/src/pages/Organization/OrganizationPage.jsx +++ b/src/pages/Organization/OrganizationPage.jsx @@ -6,14 +6,17 @@ import OrganizationsList from "../../components/Organization/OrganizationsList"; const OrganizationPage = () => { const { isOpen, orgData, startStep, onOpen, flowType } = useOrganizationModal(); - const [searchText, setSearchText] = useState("") + const [searchText, setSearchText] = useState(""); return (
    -
    +
    @@ -29,23 +32,24 @@ const OrganizationPage = () => {
    -
    +
    - - +
    + +
    ); }; diff --git a/src/pages/PaymentRequest/PaymentRequestPage.jsx b/src/pages/PaymentRequest/PaymentRequestPage.jsx new file mode 100644 index 00000000..7b7d2ff1 --- /dev/null +++ b/src/pages/PaymentRequest/PaymentRequestPage.jsx @@ -0,0 +1,186 @@ +import React, { createContext, useState, useEffect, useContext, useRef } from "react"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import GlobalModel from "../../components/common/GlobalModel"; +import ManagePaymentRequest from "../../components/PaymentRequest/ManagePaymentRequest"; +import { useFab } from "../../Context/FabContext"; +import PaymentRequestList from "../../components/PaymentRequest/PaymentRequestList"; +import PaymentRequestFilterPanel from "../../components/PaymentRequest/PaymentRequestFilterPanel"; +import { defaultPaymentRequestFilter } from "../../components/PaymentRequest/PaymentRequestSchema"; +import ViewPaymentRequest from "../../components/PaymentRequest/ViewPaymentRequest"; +import PreviewDocument from "../../components/Expenses/PreviewDocument"; +import MakeExpense from "../../components/PaymentRequest/MakeExpense"; + +export const PaymentRequestContext = createContext(); +export const usePaymentRequestContext = () => { + const context = useContext(PaymentRequestContext); + if (!context) { + throw new Error("usePaymentRequestContext must be used within PaymentRequestContext.Provider"); + } + return context; +}; + +const PaymentRequestPage = () => { + const [ManageRequest, setManageRequest] = useState({ IsOpen: null, RequestId: null }); + const [ViewRequest, setVieRequest] = useState({ view: false, requestId: null }); + const [filters, setFilters] = useState(defaultPaymentRequestFilter); + const [filterData, setFilterdata] = useState(null); + const [ViewDocument, setDocumentView] = useState({ IsOpen: false, Image: null }); + const [isExpenseGenerate, setIsExpenseGenerate] = useState({ IsOpen: null, RequestId: null }); + const [modalSize, setModalSize] = useState("md"); + const [search, setSearch] = useState(""); + const updatedRef = useRef(); + const { setOffcanvasContent, setShowTrigger } = useFab(); + + const contextValue = { + setManageRequest, + setVieRequest, + setDocumentView, + setModalSize, + setIsExpenseGenerate, + isExpenseGenerate, + }; + + const clearFilter = () => setFilters(defaultPaymentRequestFilter); + + useEffect(() => { + setShowTrigger(true); + setOffcanvasContent( + "Payment Request Filters", + + ); + + return () => { + setShowTrigger(false); + setOffcanvasContent("", null); + }; + }, []); + + const handleRemoveChip = (key, id) => { + setFilters((prev) => { + const updated = { ...prev }; + + if (Array.isArray(updated[key])) { + updated[key] = updated[key].filter((v) => v !== id); + setTimeout(() => updatedRef.current?.resetFieldValue(key, updated[key]), 0); + } else { + updated[key] = null; + setTimeout(() => updatedRef.current?.resetFieldValue(key, null), 0); + } + + return updated; + }); + }; + + return ( + +
    + {/* Breadcrumb */} + + + {/* Top Bar */} +
    +
    +
    +
    + setSearch(e.target.value)} + /> +
    + +
    + +
    +
    +
    +
    + + + {/* Add/Edit Modal */} + {ManageRequest.IsOpen && ( + setManageRequest({ IsOpen: null, expenseId: null })} + > + setManageRequest({ IsOpen: null, RequestId: null })} + /> + + )} + + {ViewRequest.view && ( + setVieRequest({ requestId: null, view: false })} + > + + + )} + {isExpenseGenerate.IsOpen && ( + setIsExpenseGenerate({ IsOpen: false, requestId: null })} + > + setIsExpenseGenerate({ IsOpen: false, requestId: null })} + /> + + )} + + {ViewDocument.IsOpen && ( + setDocumentView({ IsOpen: false, Image: null })} + > + + + )} +
    +
    + ); +}; + +export default PaymentRequestPage; diff --git a/src/pages/RecurringExpense/RecurringExpensePage.jsx b/src/pages/RecurringExpense/RecurringExpensePage.jsx new file mode 100644 index 00000000..12c07e10 --- /dev/null +++ b/src/pages/RecurringExpense/RecurringExpensePage.jsx @@ -0,0 +1,169 @@ +import React, { createContext, useState, useEffect, useContext } from "react"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import GlobalModel from "../../components/common/GlobalModel"; +import { useFab } from "../../Context/FabContext"; +import ManageRecurringExpense from "../../components/RecurringExpense/ManageRecurringExpense"; +import RecurringExpenseList from "../../components/RecurringExpense/RecurringExpenseList"; +import { PAYEE_RECURRING_EXPENSE } from "../../utils/constants"; +import { SearchRecurringExpenseSchema } from "../../components/RecurringExpense/RecurringExpenseSchema"; +import ViewRecurringExpense from "../../components/RecurringExpense/ViewRecurringExpense"; + +export const RecurringExpenseContext = createContext(); +export const useRecurringExpenseContext = () => { + const context = useContext(RecurringExpenseContext); + if (!context) { + throw new Error( + "useRecurringExpenseContext must be used within an ExpenseProvider" + ); + } + return context; +}; +const RecurringExpensePage = () => { + const [ManageRequest, setManageRequest] = useState({ + IsOpen: null, + RecurringId: null, + }); + const [viewRecurring, setViewRecurring] = useState({ + view: false, + recurringId: null, + }); + + const [selectedStatuses, setSelectedStatuses] = useState( + PAYEE_RECURRING_EXPENSE.map((s) => s.id) + ); + + const [search, setSearch] = useState(""); + + const contextValue = { + setManageRequest, + setViewRecurring, + }; + + const handleStatusChange = (id) => { + setSelectedStatuses((prev) => + prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id] + ); + }; + return ( + +
    + {/* Breadcrumb */} + + + {/* Top Bar */} +
    +
    +
    + {/* Left Column: Search + Filter */} +
    +
    + setSearch(e.target.value)} + /> + +
    + +
      + {PAYEE_RECURRING_EXPENSE.map(({ id, label }) => ( +
    • +
      + handleStatusChange(id)} + /> + +
      +
    • + ))} +
    +
    +
    +
    + + {/* Right Column: Add Button */} +
    + +
    +
    +
    +
    + + + + {ManageRequest.IsOpen && ( + + setManageRequest({ IsOpen: null, expenseId: null }) + } + > + + setManageRequest({ IsOpen: null, RecurringId: null }) + } + requestToEdit={ManageRequest.RecurringId} + /> + + )} + {viewRecurring.view && ( + + setViewRecurring({ IsOpen: null, recurringId: null }) + } + > + {/* + setViewRecurring({ IsOpen: null, recurringId: null }) + } + RecurringId={viewRecurring.recurringId} + /> */} + + + )} +
    +
    + ); +}; + +export default RecurringExpensePage; diff --git a/src/pages/ServiceProject/ServiceProjectDetail.jsx b/src/pages/ServiceProject/ServiceProjectDetail.jsx new file mode 100644 index 00000000..f8de3964 --- /dev/null +++ b/src/pages/ServiceProject/ServiceProjectDetail.jsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import ServiceProjectNav from "../../components/ServiceProject/ServiceProjectNav"; +import { ComingSoonPage } from "../Misc/ComingSoonPage"; +import ServiceProjectProfile from "../../components/ServiceProject/ServiceProjectProfile"; +import Jobs from "../../components/ServiceProject/Jobs"; +import ProjectTeam from "../../components/ServiceProject/ServiceProjectTeam/ProjectTeam"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { useParams } from "react-router-dom"; +import { useServiceProject } from "../../hooks/useServiceProject"; + +const ServiceProjectDetail = () => { + const { projectId } = useParams(); + const { + data: projectdata, + isLoading: isProjectLoading, + isProjectError, + } = useServiceProject(projectId); + + const [activePill, setActivePill] = useState( + sessionStorage.getItem("servicePrjectTab") || "profile" + ); + const handlePillClick = (pillKey) => { + setActivePill(pillKey); + localStorage.setItem("lastActiveProjectTab", pillKey); + }; + const renderContent = () => { + switch (activePill) { + case "profile": + return ; + case "teams": + return ; + case "jobs": + return ; + default: + return ; + } + }; + + return ( +
    + +
    +
    + +
    + {renderContent()} +
    +
    + ); +}; + +export default ServiceProjectDetail; diff --git a/src/pages/ServiceProject/ServiceProjectDisplay.jsx b/src/pages/ServiceProject/ServiceProjectDisplay.jsx new file mode 100644 index 00000000..6bb9299d --- /dev/null +++ b/src/pages/ServiceProject/ServiceProjectDisplay.jsx @@ -0,0 +1,98 @@ +import React, { useState } from "react"; +import { useProjectContext } from "../project/ProjectPage"; +import { + useActiveInActiveServiceProject, + useServiceProjects, +} from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import ProjectCard from "../../components/Project/ProjectCard"; +import Pagination from "../../components/common/Pagination"; +import GlobalModel from "../../components/common/GlobalModel"; +import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject"; +import { SpinnerLoader } from "../../components/common/Loader"; +import ServiceProjectCard from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectCard"; +import ServiceProjectList from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectList"; +import { useDebounce } from "../../utils/appUtils"; + +const ServiceProjectDisplay = ({ listView, selectedStatuses, searchTerm }) => { + const [currentPage, setCurrentPage] = useState(1); + + const { manageServiceProject, setManageServiceProject } = useProjectContext(); + const debouncedSearch = useDebounce(searchTerm, 500); + const { data, isLoading, isError, error } = useServiceProjects( + ITEMS_PER_PAGE, + currentPage, + debouncedSearch + ); + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + + const filteredProjects = data?.data?.filter(project => + selectedStatuses.includes(project?.status?.id) + ); + + if (isLoading) + return ( +
    + +
    + ); + + if (isError) + return ( +
    +

    {error.message}

    +
    + ); + return ( +
    +
    + {listView ? ( + + ) : filteredProjects?.length > 0 ? ( + filteredProjects?.map((project) => ( + + )) + + ):(

    No Service projects available

    )} + +
    + +
    + + {manageServiceProject?.isOpen && ( + + setManageServiceProject({ isOpen: false, project: null }) + } + > + + setManageServiceProject({ isOpen: false, project: null }) + } + /> + + )} +
    +
    + ); +}; + +export default ServiceProjectDisplay; diff --git a/src/pages/Tenant/SelfTenantDetails.jsx b/src/pages/Tenant/SelfTenantDetails.jsx index ea4b107d..2e3fbb38 100644 --- a/src/pages/Tenant/SelfTenantDetails.jsx +++ b/src/pages/Tenant/SelfTenantDetails.jsx @@ -3,7 +3,7 @@ import { useProfile } from "../../hooks/useProfile"; import TenantDetails from "./TenantDetails"; import { VIEW_TENANTS } from "../../utils/constants"; import { useNavigate } from "react-router-dom"; -import Loader from "../../components/common/Loader"; +import { SpinnerLoader } from "../../components/common/Loader"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; const SelfTenantDetails = () => { @@ -19,7 +19,7 @@ const SelfTenantDetails = () => { }, [isSelfTenantView, navigate]); if (loading || !tenantId) { - return ; + return ; } return ( diff --git a/src/pages/Tenant/TenantDetails.jsx b/src/pages/Tenant/TenantDetails.jsx index d799522e..8d744293 100644 --- a/src/pages/Tenant/TenantDetails.jsx +++ b/src/pages/Tenant/TenantDetails.jsx @@ -7,7 +7,7 @@ import { ComingSoonPage } from "../Misc/ComingSoonPage"; import GlobalModel from "../../components/common/GlobalModel"; import EditProfile from "../../components/Tenant/EditProfile"; import SubScriptionHistory from "../../components/Tenant/SubScriptionHistory"; -import Loader from "../../components/common/Loader"; +import { SpinnerLoader } from "../../components/common/Loader"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { MANAGE_TENANTS, SUPPER_TENANT } from "../../utils/constants"; @@ -71,8 +71,11 @@ const TenantDetails = ({ if (!activeTenantId) return
    No tenant selected.
    ; if (isLoading) return ( -
    - +
    +
    ); if (isError) @@ -110,14 +113,14 @@ const TenantDetails = ({ data={ iTSelf ? [ - { label: "Home", link: "/dashboard" }, - { label: "Tenant Details", link: null }, - ] + { label: "Home", link: "/dashboard" }, + { label: "Tenant Details", link: null }, + ] : [ - { label: "Home", link: "/dashboard" }, - { label: "Tenant", link: "/tenants" }, - { label: "Tenant Details", link: null }, - ] + { label: "Home", link: "/dashboard" }, + { label: "Tenant", link: "/tenants" }, + { label: "Tenant Details", link: null }, + ] } /> )} @@ -128,9 +131,8 @@ const TenantDetails = ({
  • -
    - - - Back to login - -
    +
    + + + Back to login + +
    {/* Footer Text */} -
  • ); }; -export default ForgotPasswordPage; \ No newline at end of file +export default ForgotPasswordPage; diff --git a/src/pages/authentication/LoginPage.jsx b/src/pages/authentication/LoginPage.jsx index 0015a605..6934b2e7 100644 --- a/src/pages/authentication/LoginPage.jsx +++ b/src/pages/authentication/LoginPage.jsx @@ -45,11 +45,11 @@ const LoginPage = () => { if (data.rememberMe) { localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("refreshToken", response.data.refreshToken); - removeSession("session") + removeSession("session"); } else { sessionStorage.setItem("jwtToken", response.data.token); sessionStorage.setItem("refreshToken", response.data.refreshToken); - removeSession("local") + removeSession("local"); } setLoading(false); navigate("/auth/switch/org"); @@ -78,25 +78,35 @@ const LoginPage = () => { }, [IsLoginWithOTP]); useEffect(() => { - const token = - localStorage.getItem("jwtToken") || - sessionStorage.getItem("jwtToken"); + const token = + localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken"); - if (token) { - navigate("/dashboard", { replace: true }); - } -}, []); + if (token) { + navigate("/dashboard", { replace: true }); + } + }, []); return ( -
    -
    -

    Welcome to PMS!

    +
    +
    + + +
    + Welcome to
    +

    + {" "} + OnField + Work + .com +

    +

    {IsLoginWithOTP ? "Enter your email to receive a one-time password (OTP)." : "Please sign in to your account and start the adventure"}

    -
    {/* Email */}
    @@ -219,7 +229,6 @@ const LoginPage = () => { )} - {/* Footer Text */} {!IsLoginWithOTP ? (

    diff --git a/src/pages/authentication/MainLogin.css b/src/pages/authentication/MainLogin.css new file mode 100644 index 00000000..dd6a759e --- /dev/null +++ b/src/pages/authentication/MainLogin.css @@ -0,0 +1,104 @@ +.display-header { + padding: 0px; + margin: 0px; + font-family: "Poppins", sans-serif; + color: #00324c; + font-size: 35px; + font-weight: 300; + line-height: 45px; + padding-bottom: 20px; +} + +.display-title { + padding: 0px; + margin: 0px; + font-family: "Poppins", sans-serif; + color: #00324c; + font-size: 20px; + font-weight: 300; + line-height: 45px; + padding-bottom: 20px; +} + +.display-body { + font-size: larger; + padding: 0px; + margin: 0px; + font-family: "Poppins", sans-serif; + color: #687377; + font-size: 16px; + font-weight: 300; +} + +ul li { + /* padding: 10px 10px 10px 0; */ +} + +li { + unicode-bidi: isolate; + margin: 0px; + display: list-item; +} + +ul.more-features-list { + font-size: 16px; + font-weight: 300; + list-style-type: none; + margin-block-start: 1em; + margin-block-end: 1em; + padding-inline-start: 40px; + unicode-bidi: isolate; + display: block; + + position: relative; + margin-left: 10px; + padding: 10px 0 0 0; + display: block; +} + +.list-item { + border-bottom: 1px #d6d7d7 solid; +} + +.autoplay-progress { + position: absolute; + right: 16px; + bottom: 16px; + z-index: 10; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: var(--swiper-theme-color); +} + +.autoplay-progress svg { + --progress: 0; + position: absolute; + left: 0; + top: 0px; + z-index: 10; + width: 100%; + height: 100%; + stroke-width: 4px; + stroke: var(--swiper-theme-color); + fill: none; + stroke-dashoffset: calc(125.6px * (1 - var(--progress))); + stroke-dasharray: 125.6; + transform: rotate(-90deg); +} + +.login-pg-swiper { + width: 80vw; /* 80% of viewport width */ + height: 70vh; /* 70% of viewport height */ + margin: auto; /* Center the swiper container */ +} + +.swiper-slide img.login-pg-img-swiper { + display: block; + width: 100%; /* Image takes full width of the slide (which is 80vw) */ + height: 100%; /* Image takes full height of the slide (which is 70vh) */ + object-fit: contain; /* Ensures the entire image is visible without stretching, maintaining aspect ratio */ +} diff --git a/src/pages/authentication/MainLogin.jsx b/src/pages/authentication/MainLogin.jsx index 03575f40..1de9f1f2 100644 --- a/src/pages/authentication/MainLogin.jsx +++ b/src/pages/authentication/MainLogin.jsx @@ -1,19 +1,293 @@ -import React from "react"; +import { React, useRef } from "react"; import LoginPage from "./LoginPage"; +import "./MainLogin.css"; const MainLogin = () => { return ( <>

    -
    -
    - Login image +
    + {/*
    */} +
    diff --git a/src/pages/authentication/RegisterPage.jsx b/src/pages/authentication/RegisterPage.jsx index 65b7d619..6e05cfdb 100644 --- a/src/pages/authentication/RegisterPage.jsx +++ b/src/pages/authentication/RegisterPage.jsx @@ -37,33 +37,36 @@ const registerSchema = z.object({ const RegisterPage = () => { const [registered, setRegristered] = useState(false); const [industries, setIndustries] = useState([]); - const [Loading,setLoading] = useState(false) + const [Loading, setLoading] = useState(false); const { register, handleSubmit, - formState: { errors },reset + formState: { errors }, + reset, } = useForm({ resolver: zodResolver(registerSchema), }); const onSubmit = async (data) => { try { - setLoading(true) + setLoading(true); const response = await MarketRepository.requestDemo(data); - showToast("Your request has been sent successfully. Please stay in touch!"); + showToast( + "Your request has been sent successfully. Please stay in touch!" + ); setRegristered(true); - setLoading(false) - reset() + setLoading(false); + reset(); } catch (error) { showToast(error.message, "error"); - setLoading(false) + setLoading(false); } }; useEffect(() => { fetchIndustries(); }, []); - useEffect(() => { }, [industries]); + useEffect(() => {}, [industries]); const fetchIndustries = async () => { try { @@ -76,11 +79,20 @@ const RegisterPage = () => { }; return ( <> -
    - -

    Adventure starts here

    +
    + + + +
    + +
    +
    Adventure starts here

    Make your app management easy and fun!

    { className="mb-2" onSubmit={handleSubmit(onSubmit)} > -
    -
    - - - {errors.organizatioinName && ( -
    - {errors.organizatioinName.message} -
    - )} -
    -
    - - - {errors.email && ( -
    - {errors.email.message} -
    - )} -
    +
    + + + {errors.organizatioinName && ( +
    + {errors.organizatioinName.message} +
    + )} +
    +
    + + + {errors.email && ( +
    + {errors.email.message} +
    + )} +
    - + {errors.contactPerson && (
    { - + {errors.contactNumber && (
    { - + {errors.about && (
    { - + {errors.oragnizationSize && (
    { - + {errors.industryId && (
    { privacy policy & terms -
    {errors.terms && (
    { aria-label="Click me " className="btn btn-primary d-grid w-100" > - {Loading ? "Please Wait..." :" Request Demo"} + {Loading ? "Please Wait..." : " Request Demo"} @@ -313,4 +323,4 @@ const RegisterPage = () => { ); }; -export default RegisterPage; \ No newline at end of file +export default RegisterPage; diff --git a/src/pages/authentication/SwitchTenant.jsx b/src/pages/authentication/SwitchTenant.jsx index 028414cb..b9747661 100644 --- a/src/pages/authentication/SwitchTenant.jsx +++ b/src/pages/authentication/SwitchTenant.jsx @@ -4,8 +4,7 @@ import { useAuthModal, useSelectTenant, useTenants } from "../../hooks/useAuth"; import { useProfile } from "../../hooks/useProfile"; import { useQueryClient } from "@tanstack/react-query"; import AuthRepository from "../../repositories/AuthRepository"; -import Loader from "../../components/common/Loader"; - +import { SpinnerLoader } from "../../components/common/Loader"; const SwitchTenant = () => { const queryClient = useQueryClient(); const { profile } = useProfile(); @@ -99,7 +98,7 @@ const SwitchTenant = () => {
    ); - return :contentBody} />; + return :contentBody} />; }; export default SwitchTenant; \ No newline at end of file diff --git a/src/pages/authentication/TenantSelectionPage.jsx b/src/pages/authentication/TenantSelectionPage.jsx index 18028902..5dac4e62 100644 --- a/src/pages/authentication/TenantSelectionPage.jsx +++ b/src/pages/authentication/TenantSelectionPage.jsx @@ -2,42 +2,50 @@ import { useEffect, useState } from "react"; import { useTenants, useSelectTenant, useLogout } from "../../hooks/useAuth.jsx"; import { Link, useNavigate } from "react-router-dom"; import Dashboard from "../../components/Dashboard/Dashboard.jsx"; -import Loader from "../../components/common/Loader.jsx"; +import { SpinnerLoader } from "../../components/common/Loader.jsx"; const TenantSelectionPage = () => { const [pendingTenant, setPendingTenant] = useState(null); const navigate = useNavigate(); - const { data, isLoading, isError, error } = useTenants(); + const { data, isLoading, isError } = useTenants(); const { mutate: chooseTenant, isPending } = useSelectTenant(() => { navigate("/dashboard"); }); - const handleTenantselection = (tenantId) => { + const { mutate: handleLogout, isPending: isLogouting } = useLogout(); + + const handleTenantSelection = (tenantId) => { setPendingTenant(tenantId); localStorage.setItem("ctnt", tenantId); chooseTenant(tenantId); }; - - const {mutate:handleLogout,isPending:isLogouting} = useLogout() - + // Auto-select if already stored useEffect(() => { - if (localStorage.getItem("ctnt")) { - chooseTenant(localStorage.getItem("ctnt")) + const storedTenant = localStorage.getItem("ctnt"); + if (storedTenant) { + chooseTenant(storedTenant); } - }, [navigate]); + }, []); + // Auto-select if only one tenant useEffect(() => { if (!isLoading && data?.data?.length === 1) { const tenant = data.data[0]; - handleTenantselection(tenant.id); + handleTenantSelection(tenant.id); } }, [isLoading, data]); - if (isLoading) return ; - - if (isLoading) { - return ; + // Show loader if: + // - initial loading + // - only one tenant (auto-selecting) + // - user manually selecting + if ( + isLoading || + isPending || + (data?.data?.length === 1 && pendingTenant !== null) + ) { + return ; } if (!data?.data?.length) { @@ -48,7 +56,6 @@ const TenantSelectionPage = () => { ); } - return (
    {/* Logo */} @@ -65,20 +72,24 @@ const TenantSelectionPage = () => {

    Welcome

    - Please select which dashboard you want to explore!!! + Please select which dashboard you want to explore!

    -
    handleLogout()}> - {isLogouting ? "Please Wait...":SignOut} +
    handleLogout()}> + {isLogouting ? ( + "Please Wait..." + ) : ( + + Sign Out + + )}
    -
    - {/* Card Section */} -
    + {/* Tenant Cards */} +
    {data?.data.map((tenant) => (
    - {/* Image */}
    { {/* Content */}
    - {/* Title */}

    {tenant?.name}

    - {/* Industry */}

    Industry:

    @@ -108,21 +117,19 @@ const TenantSelectionPage = () => {

    - {/* Description */} {tenant?.description && (

    {tenant?.description}

    )} - {/* Button */}
    @@ -131,8 +138,8 @@ const TenantSelectionPage = () => { ))}
    - ); }; -export default TenantSelectionPage; \ No newline at end of file +export default TenantSelectionPage; + diff --git a/src/pages/collections/CollectionPage.jsx b/src/pages/collections/CollectionPage.jsx new file mode 100644 index 00000000..32324133 --- /dev/null +++ b/src/pages/collections/CollectionPage.jsx @@ -0,0 +1,244 @@ +import React, { createContext, useContext, useState } from "react"; +import moment from "moment"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import CollectionList from "../../components/collections/CollectionList"; +import { useModal } from "../../hooks/useAuth"; +import { FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { DateRangePicker1 } from "../../components/common/DateRangePicker"; +import { isPending } from "@reduxjs/toolkit"; +import ConfirmModal from "../../components/common/ConfirmModal"; +import showToast from "../../services/toastService"; +import { useMarkedPaymentReceived } from "../../hooks/useCollections"; +import GlobalModel from "../../components/common/GlobalModel"; +import AddPayment from "../../components/collections/AddPayment"; +import ViewCollection from "../../components/collections/ViewCollection"; +import ManageCollection from "../../components/collections/ManageCollection"; +import PreviewDocument from "../../components/Expenses/PreviewDocument"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { + ADDPAYMENT_COLLECTION, + ADMIN_COLLECTION, + CREATE_COLLECTION, + EDIT_COLLECTION, + VIEW_COLLECTION, +} from "../../utils/constants"; +import AccessDenied from "../../components/common/AccessDenied"; + +const CollectionContext = createContext(); +export const useCollectionContext = () => { + const context = useContext(CollectionContext); + if (!context) { + window.location = "/dashboard"; + showToast("Out of Context Happend inside Collection Context", "warning"); + } + return context; +}; +const CollectionPage = () => { + const [viewCollection, setViewCollection] = useState(null); + const [makeCollection, setCollection] = useState({ + isOpen: false, + invoiceId: null, + }); + const [ViewDocument, setDocumentView] = useState({ + IsOpen: false, + Image: null, + }); + const [processedPayment, setProcessedPayment] = useState(null); + const [addPayment, setAddPayment] = useState({ + isOpen: false, + invoiceId: null, + }); + const [showPending, setShowPending] = useState(false); + const [searchText, setSearchText] = useState(""); + const isAdmin = useHasUserPermission(ADMIN_COLLECTION); + const canViewCollection = useHasUserPermission(VIEW_COLLECTION); + const canCreate = useHasUserPermission(CREATE_COLLECTION); + const canEditCollection = useHasUserPermission(EDIT_COLLECTION); + const canAddPayment = useHasUserPermission(ADDPAYMENT_COLLECTION); + const methods = useForm({ + defaultValues: { + fromDate: moment().subtract(180, "days").format("DD-MM-YYYY"), + toDate: moment().format("DD-MM-YYYY"), + }, + }); + const { watch } = methods; + const [fromDate, toDate] = watch(["fromDate", "toDate"]); + + const handleToggleActive = (e) => setShowPending(e.target.checked); + + const contextMassager = { + setProcessedPayment, + setCollection, + setAddPayment, + addPayment, + setViewCollection, + viewCollection, + setDocumentView, + }; + const { mutate: MarkedReceived, isPending } = useMarkedPaymentReceived(() => { + setProcessedPayment(null); + }); + const handleMarkedPayment = (payload) => { + MarkedReceived(payload); + }; + if ( + isAdmin === undefined || + canAddPayment === undefined || + canEditCollection === undefined || + canViewCollection === undefined || + canCreate === undefined + ) { + return
    Checking access...
    ; + } + + if ( + !isAdmin && + !canAddPayment && + !canEditCollection && + !canViewCollection && + !canCreate + ) { + return ( + + ); + } + return ( + +
    + + +
    +
    +
    +
    + + +
    +
    + +
    + setSearchText(e.target.value)} + placeholder="Search Collection" + className="form-control form-control-sm mt-2 mt-sm-0" + /> +
    + + + + + {(canCreate || isAdmin) && ( + + )} +
    +
    +
    +
    + + + + {makeCollection.isOpen && ( + setCollection({ isOpen: false, invoiceId: null })} + > + setCollection({ isOpen: false, invoiceId: null })} + /> + + )} + + {addPayment.isOpen && ( + setAddPayment({ isOpen: false, invoiceId: null })} + > + setAddPayment({ isOpen: false, invoiceId: null })} + /> + + )} + + {viewCollection && ( + setViewCollection(null)} + > + setViewCollection(null)} /> + + )} + + {ViewDocument.IsOpen && ( + setDocumentView({ IsOpen: false, Image: null })} + > + + + )} + + handleMarkedPayment(processedPayment?.invoiceId)} + onClose={() => setProcessedPayment(null)} + /> +
    +
    + ); +}; + +export default CollectionPage; diff --git a/src/pages/employee/EmployeeList.jsx b/src/pages/employee/EmployeeList.jsx index fc77ba1c..ba7a13ba 100644 --- a/src/pages/employee/EmployeeList.jsx +++ b/src/pages/employee/EmployeeList.jsx @@ -7,6 +7,7 @@ import Breadcrumb from "../../components/common/Breadcrumb"; import ManageEmp from "../../components/Employee/ManageRole"; import { useEmployeesAllOrByProjectId, + useEmployeesByOrganization, useSuspendEmployee, } from "../../hooks/useEmployees"; import { useProjectName, useProjects } from "../../hooks/useProjects"; @@ -36,6 +37,10 @@ import GlobalModel from "../../components/common/GlobalModel"; import usePagination from "../../hooks/usePagination"; import { setProjectId } from "../../slices/localVariablesSlice"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import Pagination from "../../components/common/Pagination"; +import handleEmployeeExport from "../../components/Employee/handleEmployeeExport"; +import { SpinnerLoader } from "../../components/common/Loader"; +import ManageReporting from "../../components/Employee/ManageReporting"; const EmployeeList = () => { const selectedProjectId = useSelector( @@ -46,15 +51,14 @@ const EmployeeList = () => { const dispatch = useDispatch(); const [showInactive, setShowInactive] = useState(false); - const [showAllEmployees, setShowAllEmployees] = useState(false); const Manage_Employee = useHasUserPermission(MANAGE_EMPLOYEES); - const { employees, loading, setLoading, error, recallEmployeeData } = - useEmployeesAllOrByProjectId( - showAllEmployees, - selectedProjectId, - showInactive - ); + const { + data: employees, + isLoading: loading, + error, + refetch: recallEmployeeData, + } = useEmployeesByOrganization(showInactive); const [employeeList, setEmployeeList] = useState([]); const [modelConfig, setModelConfig] = useState(); @@ -65,12 +69,13 @@ const EmployeeList = () => { const [searchText, setSearchText] = useState(""); const [filteredData, setFilteredData] = useState([]); const [showModal, setShowModal] = useState(false); - const [selectedEmployeeId, setSelecedEmployeeId] = useState(null); + const [selectedEmployeeId, setSelectedEmployeeId] = useState(null); + const [selectedEmployee, setSelectedEmployee] = useState(null); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedEmpFordelete, setSelectedEmpFordelete] = useState(null); + const [showManageReportingModal, setShowManageReportingModal] = useState(false); const [employeeLodaing, setemployeeLodaing] = useState(false); const ViewTeamMember = useHasUserPermission(VIEW_TEAM_MEMBERS); - const ViewAllEmployee = useHasUserPermission(VIEW_ALL_EMPLOYEES); const { mutate: suspendEmployee, isPending: empLodaing } = useSuspendEmployee( { setIsDeleteModalOpen, @@ -134,37 +139,28 @@ const EmployeeList = () => { const tableRef = useRef(null); const handleExport = (type) => { - if (!currentItems || currentItems.length === 0) return; - - switch (type) { - case "csv": - exportToCSV(currentItems, "employees"); - break; - case "excel": - exportToExcel(currentItems, "employees"); - break; - case "pdf": - exportToPDF(currentItems, "employees"); - break; - case "print": - printTable(tableRef.current); - break; - default: - break; - } + handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef); }; + + const handleAllEmployeesToggle = (e) => { const isChecked = e.target.checked; setShowInactive(false); - setShowAllEmployees(isChecked); + // setShowAllEmployees(isChecked); }; const handleEmployeeModel = (id) => { - setSelecedEmployeeId(id); + setSelectedEmployeeId(id); setShowModal(true); }; + const handleManageReporting = (employee) => { + setSelectedEmployee(employee); + setSelectedEmployeeId(employee.id); + setShowManageReportingModal(true); + }; + const handleOpenDelete = (employee) => { setSelectedEmpFordelete(employee); setIsDeleteModalOpen(true); @@ -176,12 +172,10 @@ const EmployeeList = () => { useEffect(() => { if (!loading && Array.isArray(employees)) { const sorted = [...employees].sort((a, b) => { - const nameA = `${a.firstName || ""}${a.middleName || ""}${ - a.lastName || "" - }`.toLowerCase(); - const nameB = `${b.firstName || ""}${b.middleName || ""}${ - b.lastName || "" - }`.toLowerCase(); + const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || "" + }`.toLowerCase(); + const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || "" + }`.toLowerCase(); return nameA?.localeCompare(nameB); }); @@ -205,25 +199,16 @@ const EmployeeList = () => { setCurrentPage((prevPage) => (prevPage !== 1 ? 1 : prevPage)); } - }, [loading, employees, selectedProjectId, showAllEmployees]); - - useEffect(() => { - if (!showAllEmployees) { - recallEmployeeData(showInactive, selectedProjectId); - } - }, [selectedProjectId, showInactive, showAllEmployees, recallEmployeeData]); + }, [loading, employees, selectedProjectId, showInactive]); const handler = useCallback( (msg) => { if (employees.some((item) => item.id == msg.employeeId)) { setEmployeeList([]); - recallEmployeeData( - showInactive, - showAllEmployees ? null : selectedProjectId - ); // Use selectedProjectId here + recallEmployeeData(showInactive); } }, - [employees, showInactive, showAllEmployees, selectedProjectId] // Add all relevant dependencies + [employees, showInactive, showInactive, selectedProjectId] // Add all relevant dependencies ); useEffect(() => { @@ -254,7 +239,19 @@ const EmployeeList = () => { setShowModal(false)} - IsAllEmployee={showAllEmployees} + /> + + )} + + {showManageReportingModal && ( + setShowManageReportingModal(false)} + > + setShowManageReportingModal(false)} /> )} @@ -268,9 +265,8 @@ const EmployeeList = () => { ? "Suspend Employee" : "Reactivate Employee" } - message={`Are you sure you want to ${ - selectedEmpFordelete?.isActive ? "suspend" : "reactivate" - } this employee?`} + message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate" + } this employee?`} onSubmit={(id) => suspendEmployee({ employeeId: id, @@ -293,59 +289,39 @@ const EmployeeList = () => { {ViewTeamMember ? ( //
    -
    +
    {/* Switches: All Employees + Inactive */}
    {/* All Employees Switch */} - {ViewAllEmployee && ( -
    - - -
    - )} {/* Show Inactive Employees Switch */} - {showAllEmployees && ( -
    - setShowInactive(e.target.checked)} - /> - -
    - )} + +
    + setShowInactive(e.target.checked)} + /> + +
    {/* Right side: Search + Export + Add Employee */} -
    +
    {/* Search Input - ALWAYS ENABLED */}
    - + {item.email ? ( {item.email} ) : ( - - - - + NA )} + @@ -599,9 +574,14 @@ const EmployeeList = () => { - - {moment(item.joiningDate)?.format("DD-MMM-YYYY")} + + {item.joiningDate ? ( + moment(item.joiningDate).format("DD-MMM-YYYY") + ) : ( + NA + )} + {showInactive ? ( { {" "} Manage Role + )} @@ -696,59 +688,15 @@ const EmployeeList = () => { ))} - -
    - - {/* Pagination */} - {!loading && displayData.length > ITEMS_PER_PAGE && ( - - )}
    + {displayData?.length > 0 && ( + + )}
    ) : (
    diff --git a/src/pages/master/MasterPage.jsx b/src/pages/master/MasterPage.jsx index 70554452..ad20fa84 100644 --- a/src/pages/master/MasterPage.jsx +++ b/src/pages/master/MasterPage.jsx @@ -103,7 +103,7 @@ const MasterPage = () => { if (menuErrorFlag || isMasterError) return ( -
    +

    Oops, an error occurred @@ -161,10 +161,10 @@ const MasterPage = () => { data={[{ label: "Home", link: "/dashboard" }, { label: "Masters" }]} /> -
    +
    @@ -228,8 +228,8 @@ const MasterPage = () => { handleModalData(selectedMaster, null, selectedMaster) } > - Add{" "} - {selectedMaster} + Add{" "} + {selectedMaster} )}
    diff --git a/src/pages/master/MasterTable.jsx b/src/pages/master/MasterTable.jsx index 8facfdb7..cb5fe881 100644 --- a/src/pages/master/MasterTable.jsx +++ b/src/pages/master/MasterTable.jsx @@ -92,7 +92,8 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => { {" "} {selectedMaster === "Activity" ? "Activity" : "Name"} - + + {" "} {selectedMaster === "Activity" ? "Unit" @@ -108,12 +109,12 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => { {currentItems.length > 0 ? ( currentItems.map((item, index) => ( - - + + {updatedColumns.map((col) => ( - + {col.key === "description" ? ( item[col.key] !== undefined && item[col.key] !== null ? ( @@ -133,7 +134,7 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => { )} ))} - + {(selectedMaster === "Application Role" || selectedMaster === "Work Category") && item?.isSystem ? ( diff --git a/src/pages/project/ProjectDetails.jsx b/src/pages/project/ProjectDetails.jsx index 8d2f8277..298e95fb 100644 --- a/src/pages/project/ProjectDetails.jsx +++ b/src/pages/project/ProjectDetails.jsx @@ -2,12 +2,12 @@ import React, { useState, useEffect, useCallback } from "react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; -import ProjectOverview from "../../components/Project/ProjectOverview"; +import ProjectOverview from "../../components/Project/ProjectStatistics"; import AboutProject from "../../components/Project/AboutProject"; import ProjectNav from "../../components/Project/ProjectNav"; import Teams from "../../components/Project/Team/Teams"; import ProjectInfra from "../../components/Project/ProjectInfra"; -import Loader from "../../components/common/Loader"; +import { SpinnerLoader } from "../../components/common/Loader"; import WorkPlan from "../../components/Project/WorkPlan"; import Breadcrumb from "../../components/common/Breadcrumb"; import { useSelectedProject } from "../../slices/apiDataManager"; @@ -15,12 +15,12 @@ import { useProjectDetails, useProjectName } from "../../hooks/useProjects"; import { ComingSoonPage } from "../Misc/ComingSoonPage"; import eventBus from "../../services/eventBus"; import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart"; -import AttendanceOverview from "../../components/Dashboard/AttendanceChart"; +import AttendanceOverview from "../../components/Dashboard/AttendanceOverview"; import { setProjectId } from "../../slices/localVariablesSlice"; import ProjectDocuments from "../../components/Project/ProjectDocuments"; import ProjectSetting from "../../components/Project/ProjectSetting"; import DirectoryPage from "../Directory/DirectoryPage"; -import { useProjectAccess } from "../../hooks/useProjectAccess"; +import { useProjectAccess } from "../../hooks/useProjectAccess"; import "./ProjectDetails.css"; import ProjectOrganizations from "../../components/Project/ProjectOrganizations"; @@ -65,7 +65,14 @@ const ProjectDetails = () => { }; if (projectLoading || permsLoading || !projects_Details) { - return ; + return ( +
    + +
    + ); } const renderContent = () => { @@ -77,7 +84,7 @@ const ProjectDetails = () => {
    -
    +
    diff --git a/src/pages/project/ProjectPage.jsx b/src/pages/project/ProjectPage.jsx index 4fdccc30..234e3838 100644 --- a/src/pages/project/ProjectPage.jsx +++ b/src/pages/project/ProjectPage.jsx @@ -1,14 +1,22 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; import Breadcrumb from "../../components/common/Breadcrumb"; -import { ITEMS_PER_PAGE, MANAGE_PROJECT, PROJECT_STATUS } from "../../utils/constants"; +import { + ITEMS_PER_PAGE, + MANAGE_PROJECT, + PROJECT_STATUS, +} from "../../utils/constants"; import ProjectListView from "../../components/Project/ProjectListView"; import GlobalModel from "../../components/common/GlobalModel"; import ManageProjectInfo from "../../components/Project/ManageProjectInfo"; import ProjectCardView from "../../components/Project/ProjectCardView"; import usePagination from "../../hooks/usePagination"; import { useProjects } from "../../hooks/useProjects"; -import Loader from "../../components/common/Loader"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { SpinnerLoader } from "../../components/common/Loader"; +import { useServiceProjects } from "../../hooks/useServiceProject"; +import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject"; +import ProjectsDisplay from "./ProjectsDisplay"; +import ServiceProjectDisplay from "../ServiceProject/ServiceProjectDisplay"; const ProjectContext = createContext(); export const useProjectContext = () => { @@ -22,41 +30,28 @@ export const useProjectContext = () => { const ProjectPage = () => { const [manageProject, setMangeProject] = useState({ isOpen: false, - Project: null, + project: null, }); - + const [manageServiceProject, setManageServiceProject] = useState({ + isOpen: false, + project: null, + }); + const dropdownRef = useRef(null); const [projectList, setProjectList] = useState([]); const [listView, setListView] = useState(false); const [searchTerm, setSearchTerm] = useState(""); + const [coreProjects, setCoreProjects] = useState(() => { + const storedValue = sessionStorage.getItem("whichProjectDisplay"); + return storedValue === "true"; + }); const HasManageProject = useHasUserPermission(MANAGE_PROJECT); + const [currentPage, setCurrentPage] = useState(1); + const [open, setOpen] = useState(false); const [selectedStatuses, setSelectedStatuses] = useState( PROJECT_STATUS.map((s) => s.id) ); - - const { data, isLoading, isError, error } = useProjects(); - - const contextDispatcher = { - setMangeProject, - }; - - const filteredProjects = projectList.filter((project) => { - const matchesStatus = selectedStatuses.includes(project.projectStatusId); - const matchesSearch = project.name - .toLowerCase() - .includes(searchTerm.toLowerCase()); - return matchesStatus && matchesSearch; - }); - - const totalPages = Math.ceil(filteredProjects.length / ITEMS_PER_PAGE); - - const { currentItems, currentPage, paginate, setCurrentPage } = usePagination( - filteredProjects, - ITEMS_PER_PAGE - ); - const handleStatusChange = (statusId) => { - setCurrentPage(1); setSelectedStatuses((prev) => prev.includes(statusId) ? prev.filter((id) => id !== statusId) @@ -64,40 +59,28 @@ const ProjectPage = () => { ); }; - const sortingProject = (projects) => { - if (!isLoading && Array.isArray(projects)) { - const grouped = {}; + const contextDispatcher = { + setMangeProject, + setManageServiceProject, + manageProject, + manageServiceProject, + }; - projects.forEach((project) => { - const statusId = project.projectStatusId; - if (!grouped[statusId]) grouped[statusId] = []; - grouped[statusId].push(project); - }); - - const sortedGrouped = selectedStatuses - .filter((statusId) => grouped[statusId]) - .flatMap((statusId) => - grouped[statusId].sort((a, b) => - a.name.toLowerCase().localeCompare(b.name.toLowerCase()) - ) - ); - - setProjectList((prev) => { - const isSame = JSON.stringify(prev) === JSON.stringify(sortedGrouped); - return isSame ? prev : sortedGrouped; - }); - } + const handleToggleProject = (value) => { + setCoreProjects(value); + sessionStorage.setItem("whichProjectDisplay", String(value)); }; useEffect(() => { - if (!isLoading && data) { - sortingProject(data); - } - }, [data, isLoading, selectedStatuses]); + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); - - if(isLoading) return
    - if(isError) return

    {error.message}

    return (
    @@ -109,10 +92,36 @@ const ProjectPage = () => { />
    -
    -
    -
    -
    +
    +
    + {/* LEFT SIDE — DATE TOGGLE BUTTONS */} +
    +
    + {/* Service Project Button */} + + {/* Organization Project Button */} + +
    +
    + + {/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */} +
    + {/* Search */} +
    { />
    -
    + {/* Card/List Buttons */} +
    +
    -
    -
    -
      - {PROJECT_STATUS.map(({ id, label }) => ( -
    • -
      - handleStatusChange(id)} - /> - -
      -
    • - ))} -
    -
    -
    + -
    - {HasManageProject && ( )} + {selectedStatuses.length !== PROJECT_STATUS.length && ( + + {PROJECT_STATUS.length - selectedStatuses.length} + + )} +
    + + {open && ( +
      e.stopPropagation()} // IMPORTANT + > + {PROJECT_STATUS.map(({ id, label }) => ( +
    • +
      + e.stopPropagation()} // IMPORTANT + onChange={() => handleStatusChange(id)} + /> + +
      +
    • + ))} +
    + )} +
    + + + + + {HasManageProject && ( + + )}
    - {/* Project Render here */} - {listView ? ( - ) : ( - - )} - - {/* ------------------ */} - - {/* Project Manage UPdate or create */} - - {manageProject.isOpen && ( - setMangeProject({ isOpen: false, Project: null })} - > - setMangeProject({ isOpen: false, Project: null })} - /> - + )}
    diff --git a/src/pages/project/ProjectsDisplay.jsx b/src/pages/project/ProjectsDisplay.jsx new file mode 100644 index 00000000..d01228c8 --- /dev/null +++ b/src/pages/project/ProjectsDisplay.jsx @@ -0,0 +1,135 @@ +import React, { useEffect, useState } from "react"; +import ProjectListView from "../../components/Project/ProjectListView"; +import ProjectCardView from "../../components/Project/ProjectCardView"; +import GlobalModel from "../../components/common/GlobalModel"; +import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject"; +import { useProjectContext } from "./ProjectPage"; +import { SpinnerLoader } from "../../components/common/Loader"; +import { useProjects } from "../../hooks/useProjects"; +import { useServiceProjects } from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants"; +import usePagination from "../../hooks/usePagination"; +import ManageProjectInfo from "../../components/Project/ManageProjectInfo"; +import { useDebounce } from "../../utils/appUtils"; + +const ProjectsDisplay = ({ + listView, + searchTerm, + selectedStatuses, + handleStatusChange, +}) => { + const [currentPage, setCurrentPage] = useState(1); + const { + manageProject, + manageServiceProject, + setMangeProject, + setManageServiceProject, + } = useProjectContext(); + + const [projectList, setProjectList] = useState([]); + const debouncedSearch = useDebounce(searchTerm, 500); + const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1, debouncedSearch); + + const filteredProjects = + data?.data?.filter((project) => { + const statusId = + project.projectStatusId ?? project?.status?.id ?? project?.statusId; + + const matchesStatus = selectedStatuses.includes(statusId); + + const matchesSearch = project?.name + ?.toLowerCase() + ?.includes(searchTerm?.toLowerCase()); + + return matchesStatus && matchesSearch; + }) ?? []; + + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + + const sortingProject = (projects) => { + if (!isLoading && Array.isArray(projects)) { + const grouped = {}; + + projects.forEach((project) => { + const statusId = project.projectStatusId ?? project?.status?.id; + if (!grouped[statusId]) grouped[statusId] = []; + grouped[statusId].push(project); + }); + + const sortedGrouped = selectedStatuses + .filter((statusId) => grouped[statusId]) + .flatMap((statusId) => + grouped[statusId].sort((a, b) => + a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()) + ) + ); + + setProjectList((prev) => { + const isSame = JSON.stringify(prev) === JSON.stringify(sortedGrouped); + return isSame ? prev : sortedGrouped; + }); + } + }; + + useEffect(() => { + if (!isLoading && data?.data) { + sortingProject(data.data); + } + }, [data?.data, isLoading, selectedStatuses]); + + if (isLoading) + return ( +
    + +
    + ); + + if (isError) + return ( +
    +

    {error.message}

    +
    + ); + + return ( +
    + {listView ? ( + + ) : ( + + )} + + {manageProject?.isOpen && ( + setMangeProject({ isOpen: false, Project: null })} + > + setMangeProject({ isOpen: false, Project: null })} + /> + + )} +
    + ); +}; + +export default ProjectsDisplay; diff --git a/src/pages/project/ProjectsDisplayGround.jsx b/src/pages/project/ProjectsDisplayGround.jsx new file mode 100644 index 00000000..43c5bf08 --- /dev/null +++ b/src/pages/project/ProjectsDisplayGround.jsx @@ -0,0 +1,154 @@ +import React, { useEffect, useState } from "react"; +import ProjectListView from "../../components/Project/ProjectListView"; +import ProjectCardView from "../../components/Project/ProjectCardView"; +import GlobalModel from "../../components/common/GlobalModel"; +import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject"; +import { useProjectContext } from "./ProjectPage"; +import { SpinnerLoader } from "../../components/common/Loader"; +import { useProjects } from "../../hooks/useProjects"; +import { useServiceProjects } from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants"; +import usePagination from "../../hooks/usePagination"; + +const ProjectsDisplayGround = ({ listView, searchTerm }) => { + const { + manageProject, + manageServiceProject, + setMangeProject, + setManageServiceProject, + } = useProjectContext(); + const [projectList, setProjectList] = useState([]); + + const [selectedStatuses, setSelectedStatuses] = useState( + PROJECT_STATUS.map((s) => s.id) + ); + + const contextDispatcher = { + setMangeProject, + setManageServiceProject, + }; + const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1); + const filteredProjects = data?.data?.filter((project) => { + const matchesStatus = selectedStatuses.includes(project.projectStatusId); + const matchesSearch = project?.name + ?.toLowerCase() + ?.includes(searchTerm?.toLowerCase()); + return matchesStatus && matchesSearch; + }); + + const totalPages = Math.ceil(filteredProjects?.length / ITEMS_PER_PAGE); + + const { currentItems, currentPage, paginate, setCurrentPage } = usePagination( + filteredProjects, + ITEMS_PER_PAGE + ); + + const handleStatusChange = (statusId) => { + setCurrentPage(1); + setSelectedStatuses((prev) => + prev.includes(statusId) + ? prev.filter((id) => id !== statusId) + : [...prev, statusId] + ); + }; + + const sortingProject = (projects) => { + if (!isLoading && Array.isArray(projects)) { + const grouped = {}; + + projects.forEach((project) => { + const statusId = project.projectStatusId ?? project?.status?.id; + if (!grouped[statusId]) grouped[statusId] = []; + grouped[statusId].push(project); + }); + + const sortedGrouped = selectedStatuses + .filter((statusId) => grouped[statusId]) + .flatMap((statusId) => + grouped[statusId].sort((a, b) => + a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()) + ) + ); + + setProjectList((prev) => { + const isSame = JSON.stringify(prev) === JSON.stringify(sortedGrouped); + return isSame ? prev : sortedGrouped; + }); + } + }; + useEffect(() => { + if (!isLoading && data) { + sortingProject([data.data]); + } + }, [data?.data, isLoading, selectedStatuses]); + + if (isLoading) + return ( +
    + +
    + ); + + if (isError) + return ( +
    +

    {error.message}

    +
    + ); + return ( +
    + {/* Project Render here */} + {listView ? ( + + ) : ( + + )} + + {/* Project Manage UPdate or create */} + {manageProject?.isOpen && ( + setMangeProject({ isOpen: false, Project: null })} + > + setMangeProject({ isOpen: false, Project: null })} + /> + + )} + + {/* Servicer Project */} + + {manageServiceProject?.isOpen && ( + + setManageServiceProject({ isOpen: false, Project: null }) + } + > + + + )} +
    + ); +}; + +export default ProjectsDisplayGround; diff --git a/src/repositories/AuthRepository.jsx b/src/repositories/AuthRepository.jsx index 700020fd..dcc2c0dc 100644 --- a/src/repositories/AuthRepository.jsx +++ b/src/repositories/AuthRepository.jsx @@ -10,6 +10,10 @@ const AuthRepository = { verifyOTP: (data) => api.postPublic("/api/auth/login-otp", data), register: (data) => api.postPublic("/api/auth/register", data), sendMail: (data) => api.postPublic("/api/auth/sendmail", data), + getSubscription:(frequency)=> api.getPublic(`/api/market/list/subscription-plan?frequency=${frequency}`), + createSuscription:(data)=>api.post(`/api/Tenant/self/create`,data), // this will put entry inside enquiry table + selfCreateSubscription:(data)=>api.post(`/api/Tenant/self/subscription`,data), + // Protected routes (require auth token) logout: (data) => api.post("/api/auth/logout", data), @@ -17,7 +21,9 @@ const AuthRepository = { changepassword: (data) => api.post("/api/auth/change-password", data), appmenu: () => api.get('/api/appmenu/get/menu'), selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`), - getTenantList: () => api.get("/api/Auth/get/user/tenants"), + getTenantList: () => api.get("/api/Auth/get/user/tenants"), + + // }; diff --git a/src/repositories/ColllectionRepository.jsx b/src/repositories/ColllectionRepository.jsx new file mode 100644 index 00000000..04db4369 --- /dev/null +++ b/src/repositories/ColllectionRepository.jsx @@ -0,0 +1,49 @@ +import { api } from "../utils/axiosClient"; +import { DirectoryRepository } from "./DirectoryRepository"; + +export const CollectionRepository = { + createNewCollection: (data) => + api.post(`/api/Collection/invoice/create`, data), + updateCollection: (id, data) => { + api.put(`/api/Collection/invoice/edit/${id}`, data) + }, + // getCollections: (pageSize, pageNumber,fromDate,toDate, isPending,isActive,projectId, searchString) => { + // let url = `/api/Collection/invoice/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isPending=${isPending}&isActive=${isActive}&searchString=${searchString}`; + + // const params = []; + // if (fromDate) params.push(`fromDate=${fromDate}`); + // if (toDate) params.push(`toDate=${toDate}`); + // if(projectId) params.push(`projectId=${projectId}`) + + // if (params.length > 0) { + // url += `&${params.join("&")}`; + // } + // return api.get(url); + // }, + + getCollections: (projectId, searchString, fromDate, toDate, pageSize, pageNumber, isActive, isPending) => { + let url = `/api/Collection/invoice/list`; + const params = []; + + if (projectId) params.push(`projectId=${projectId}`); + if (searchString) params.push(`search=${searchString}`); + if (fromDate) params.push(`dateFrom=${fromDate}`); + if (toDate) params.push(`dateTo=${toDate}`); + if (pageSize) params.push(`pageSize=${pageSize}`); + if (pageNumber) params.push(`pageNumber=${pageNumber}`); + if (isActive) params.push(`isActive=${isActive}`); + if (isPending) params.push(`isPending=${isPending}`); + + if (params.length > 0) { + url += "?" + params.join("&"); + } + + return api.get(url); + }, + + makeReceivePayment: (data) => api.post(`/api/Collection/invoice/payment/received`, data), + markPaymentReceived: (invoiceId) => api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`), + getCollection: (id) => api.get(`/api/Collection/invoice/details/${id}`), + addComment: (data) => api.post(`/api/Collection/invoice/add/comment`, data) +}; + diff --git a/src/repositories/EmployeeRepository.jsx b/src/repositories/EmployeeRepository.jsx index d257825b..256186b7 100644 --- a/src/repositories/EmployeeRepository.jsx +++ b/src/repositories/EmployeeRepository.jsx @@ -10,18 +10,21 @@ const EmployeeRepository = { updateEmployee: (id, data) => api.put(`/users/${id}`, data), // deleteEmployee: ( id ) => api.delete( `/users/${ id }` ), getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`), - deleteEmployee: (id,active) => api.delete(`/api/employee/${id}?active=${active}`), - getEmployeeName: (projectId, search,allEmployee) => { - const params = new URLSearchParams(); + deleteEmployee: (id, active) => api.delete(`/api/employee/${id}?active=${active}`), + getEmployeeName: (projectId, search, allEmployee,employeeId) => { + const params = new URLSearchParams(); - if (projectId) params.append("projectId", projectId); - if (search) params.append("searchString", search); - if(allEmployee) params.append("allEmployee",allEmployee) + if (projectId) params.append("projectId", projectId); + if (search) params.append("searchString", search); + if (allEmployee) params.append("allEmployee", allEmployee); + if (employeeId) params.append("employeeId", employeeId); - const query = params.toString(); - return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`); -} + const query = params.toString(); + return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`); + }, + getOrganizaionHierarchy: (employeeId) => api.get(`/api/organization/hierarchy/list/${employeeId}`), + manageOrganizationHierarchy: (employeeId, data) => api.post(`/api/organization/hierarchy/manage/${employeeId}`, data), }; export default EmployeeRepository; diff --git a/src/repositories/ExpsenseRepository.jsx b/src/repositories/ExpsenseRepository.jsx index ef15940a..6be424b0 100644 --- a/src/repositories/ExpsenseRepository.jsx +++ b/src/repositories/ExpsenseRepository.jsx @@ -1,24 +1,80 @@ import { api } from "../utils/axiosClient"; - const ExpenseRepository = { - GetExpenseList: ( pageSize, pageNumber, filter,searchString ) => { + //#region Expense + GetExpenseList: (pageSize, pageNumber, filter, searchString) => { const payloadJsonString = JSON.stringify(filter); - - - - return api.get(`/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`); + return api.get( + `/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}` + ); }, - - GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`), - CreateExpense:(data)=>api.post("/api/Expense/create",data), - UpdateExpense:(id,data)=>api.put(`/api/Expense/edit/${id}`,data), - DeleteExpense:(id)=>api.delete(`/api/Expense/delete/${id}`), + GetExpenseDetails: (id) => api.get(`/api/Expense/details/${id}`), + CreateExpense: (data) => api.post("/api/Expense/create", data), + UpdateExpense: (id, data) => api.put(`/api/Expense/edit/${id}`, data), + DeleteExpense: (id) => api.delete(`/api/Expense/delete/${id}`), + ActionOnExpense: (data) => api.post("/api/expense/action", data), + GetExpenseFilter: () => api.get("/api/Expense/filter"), - ActionOnExpense:(data)=>api.post('/api/expense/action',data), - - GetExpenseFilter:()=>api.get('/api/Expense/filter') + //#endregion -} + + + //#region Payment Request + GetPaymentRequestList: ( + pageSize, + pageNumber, + filter, + isActive, + searchString + ) => { + const payloadJsonString = JSON.stringify(filter); + return api.get( + `/api/Expense/get/payment-requests/list?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}` + ); + }, + CreatePaymentRequest: (data) => + api.post("/api/expense/payment-request/create", data), + UpdatePaymentRequest: (id, data) => + api.put(`/api/Expense/payment-request/edit/${id}`, data), + GetPaymentRequest: (id) => + api.get(`/api/Expense/get/payment-request/details/${id}`), + GetPaymentRequestFilter: () => api.get("/api/Expense/payment-request/filter"), + ActionOnPaymentRequest: (data) => + api.post("/api/Expense/payment-request/action", data), + DeletePaymentRequest: () => api.get("delete here come"), + CreatePaymentRequestExpense: (data) => + api.post("/api/Expense/payment-request/expense/create", data), + GetPayee: () => api.get('/api/Expense/payment-request/payee'), + //#endregion + + + + //#region Recurring Expense + GetRecurringExpenseList: (pageSize, pageNumber, filter, isActive, searchString) => { + const payloadJsonString = JSON.stringify(filter); + return api.get( + `/api/expense/get/recurring-payment/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&isActive=${isActive}&searchString=${searchString}` + ); + }, + CreateRecurringExpense: (data) => + api.post("/api/Expense/recurring-payment/create", data), + UpdateRecurringExpense: (id, data) => + api.put(`/api/Expense/recurring-payment/edit/${id}`, data), + GetRecurringExpense: (id) => + api.get(`/api/Expense/get/recurring-payment/details/${id}`), + //#endregion + + + + + //#region Advance Payment + GetTranctionList: (employeeId) => + api.get(`/api/Expense/get/transactions/${employeeId}`), + getAllTranctionList: (searchString) => + api.get(`/api/Expense/get/advance-payment/employee/list?searchString=${searchString}`), + //#endregion + + +}; export default ExpenseRepository; diff --git a/src/repositories/GlobalRepository.jsx b/src/repositories/GlobalRepository.jsx index d3367fa6..2516020a 100644 --- a/src/repositories/GlobalRepository.jsx +++ b/src/repositories/GlobalRepository.jsx @@ -3,12 +3,12 @@ import { api } from "../utils/axiosClient"; const GlobalRepository = { getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => { let params; - if(projectId == null){ + if (projectId == null) { params = new URLSearchParams({ days: days.toString(), FromDate, }); - }else{ + } else { params = new URLSearchParams({ days: days.toString(), FromDate, @@ -18,31 +18,75 @@ const GlobalRepository = { return api.get(`/api/Dashboard/Progression?${params.toString()}`); }, + getProjectCompletionStatus:()=>api.get(`/api/Dashboard/project-completion-status`), - getDashboardAttendanceData: ( date,projectId ) => { - return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`); -}, + getDashboardAttendanceData: (date, projectId) => { + + return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`); + }, getDashboardProjectsCardData: () => { return api.get(`/api/Dashboard/projects`); }, - + getDashboardTeamsCardData: (projectId) => { - const url = projectId - ? `/api/Dashboard/teams?projectId=${projectId}` - : `/api/Dashboard/teams`; - return api.get(url); -}, + const url = projectId + ? `/api/Dashboard/teams?projectId=${projectId}` + : `/api/Dashboard/teams`; + return api.get(url); + }, getDashboardTasksCardData: (projectId) => { - const url = projectId - ? `/api/Dashboard/tasks?projectId=${projectId}` - : `/api/Dashboard/tasks`; - return api.get(url); -}, + const url = projectId + ? `/api/Dashboard/tasks?projectId=${projectId}` + : `/api/Dashboard/tasks`; + return api.get(url); + }, + + getExpenseData: (projectId, startDate, endDate) => { + let url = `api/Dashboard/expense/type` + const queryParams = []; + if (projectId) { + queryParams.push(`projectId=${projectId}`); + } + if (startDate) { + queryParams.push(`startDate=${startDate}`); + } + if (endDate) { + queryParams.push(`endDate=${endDate}`); + } + + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + return api.get(url); + }, + + getExpenseStatus: (projectId) => api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}` : ""}`), + + getExpenseDataByProject: (projectId, categoryId, months) => { + let url = `api/Dashboard/expense/monthly` + const queryParams = []; + if (projectId) { + queryParams.push(`projectId=${projectId}`); + } + if (categoryId) { + queryParams.push(`categoryId=${categoryId}`); + } + if (months) { + queryParams.push(`months=${months}`); + } + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + return api.get(url); + }, + + getAttendanceOverview: (projectId, days) => api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`) + - getAttendanceOverview:(projectId,days)=>api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`) }; + export default GlobalRepository; diff --git a/src/repositories/MastersRepository.jsx b/src/repositories/MastersRepository.jsx index 3e9fac8f..72b3e2a0 100644 --- a/src/repositories/MastersRepository.jsx +++ b/src/repositories/MastersRepository.jsx @@ -50,7 +50,10 @@ export const MasterRespository = { "Contact Category": (id) => api.delete(`/api/master/contact-category/${id}`), "Contact Tag": (id) => api.delete(`/api/master/contact-tag/${id}`), "Expense Type": (id, isActive) => - api.delete(`/api/Master/expenses-type/delete/${id}`, (isActive = false)), + api.delete( + `/api/Master/expenses-category/delete/${id}`, + (isActive = false) + ), "Payment Mode": (id, isActive) => api.delete(`/api/Master/payment-mode/delete/${id}`, (isActive = false)), "Expense Status": (id, isActive) => @@ -58,6 +61,11 @@ export const MasterRespository = { "Document Type": (id) => api.delete(`/api/Master/document-type/delete/${id}`), "Document Category": (id) => api.delete(`/api/Master/document-category/delete/${id}`), + "Payment Adjustment Head": (id, isActive) => + api.delete( + `/api/Master/payment-adjustment-head/delete/${id}`, + (isActive = false) + ), getWorkCategory: () => api.get(`/api/master/work-categories`), createWorkCategory: (data) => api.post(`/api/master/work-category`, data), @@ -77,10 +85,11 @@ export const MasterRespository = { getAuditStatus: () => api.get("/api/Master/work-status"), - getExpenseType: () => api.get("/api/Master/expenses-types"), - createExpenseType: (data) => api.post("/api/Master/expenses-type", data), - updateExpenseType: (id, data) => - api.put(`/api/Master/expenses-type/edit/${id}`, data), + getExpenseCategories: () => api.get("/api/Master/expenses-categories"), + createExpenseCategory: (data) => + api.post("/api/Master/expenses-category", data), + updateExpenseCategory: (id, data) => + api.put(`/api/Master/expenses-category/edit/${id}`, data), getPaymentMode: () => api.get("/api/Master/payment-modes"), createPaymentMode: (data) => api.post(`/api/Master/payment-mode`, data), @@ -124,10 +133,28 @@ export const MasterRespository = { api.put(`/api/Master/activity-group/edit/${serviceId}`, data), getActivitesByGroup: (activityGroupId) => api.get(`api/master/activities?activityGroupId=${activityGroupId}`), - deleteActivityGroup:(id)=>api.delete(`/api/Master/activity-group/delete/${id}`), + deleteActivityGroup: (id) => + api.delete(`/api/Master/activity-group/delete/${id}`), - - deleteActivity:(id)=>api.delete(`/api/Master/activity/delete/${id}`), + deleteActivity: (id) => api.delete(`/api/Master/activity/delete/${id}`), getOrganizationType: () => api.get("/api/Master/organization-type/list"), + + getPaymentAdjustmentHead: (isActive) => + api.get(`/api/Master/payment-adjustment-head/list?isActive=${isActive}`), + createPaymentAjustmentHead: (data) => + api.post(`/api/Master/payment-adjustment-head`, data), + updatePaymentAjustmentHead: (id, data) => + api.put(`/api/Master/payment-adjustment-head/edit/${id}`, data), + + getCurrencies: () => api.get(`/api/Master/currencies/list`), + + getRecurringStatus: () => api.get(`/api/Master/recurring-status/list`), + // Service Job JobTickets Status + getJobStatus: (statusId,projectId) => + api.get( + `/api/Master/job-status/list?statusId=${statusId}&projectId=${projectId}` + ), + + getTeamRole: () => api.get(`/api/Master/team-roles/list`), }; diff --git a/src/repositories/PaymentRepository.jsx b/src/repositories/PaymentRepository.jsx new file mode 100644 index 00000000..7aa877bf --- /dev/null +++ b/src/repositories/PaymentRepository.jsx @@ -0,0 +1,7 @@ +import { api } from "../utils/axiosClient"; + + +export const PaymentRepository = { + makePayment: (data) => api.post(`/api/Payment/create-order`,data), + verifyPayment: (data) => api.post(`/api/Payment/verify-payment`,data), +}; diff --git a/src/repositories/ProjectRepository.jsx b/src/repositories/ProjectRepository.jsx index 2daf73fd..6759b885 100644 --- a/src/repositories/ProjectRepository.jsx +++ b/src/repositories/ProjectRepository.jsx @@ -1,17 +1,25 @@ import { api } from "../utils/axiosClient"; const ProjectRepository = { - getProjectList: () => api.get("/api/project/list"), + + getProjectList: (pageSize, pageNumber,searchString) => + api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`), getProjectByprojectId: (projetid) => api.get(`/api/project/details/${projetid}`), - getProjectAllocation: (projectId, serviceId, organizationId, employeeStatus) => { + getProjectAllocation: ( + projectId, + serviceId, + organizationId, + employeeStatus + ) => { let url = `/api/project/allocation/${projectId}`; const params = []; if (organizationId) params.push(`organizationId=${organizationId}`); if (serviceId) params.push(`serviceId=${serviceId}`); - if (employeeStatus !== undefined) params.push(`includeInactive=${employeeStatus}`); + if (employeeStatus !== undefined) + params.push(`includeInactive=${employeeStatus}`); if (params.length > 0) { url += `?${params.join("&")}`; @@ -20,7 +28,6 @@ const ProjectRepository = { return api.get(url); }, - getEmployeesByProject: (projectId) => api.get(`/api/Project/employees/get/${projectId}`), @@ -40,10 +47,13 @@ const ProjectRepository = { api.get(`/api/project/allocation-histery/${id}`), updateProjectsByEmployee: (id, data) => api.post(`/api/project/assign-projects/${id}`, data), - projectNameList: () => api.get("/api/project/list/basic"), + projectNameList: (provideAll) => + api.get(`/api/project/list/basic?provideAll=${provideAll}`), + projectNameListAll: (searchString) => + api.get(`/api/project/list/basic/all?searchString=${searchString}`), getProjectDetails: (id) => api.get(`/api/project/details/${id}`), - + getProjectInfraByproject: (projectId, serviceId) => { let url = `/api/project/infra-details/${projectId}`; if (serviceId) { @@ -85,6 +95,8 @@ const ProjectRepository = { api.get(`/api/Project/get/assigned/services/${projectId}`), getProjectAssignedOrganizations: (projectId) => api.get(`/api/Project/get/assigned/organization/${projectId}`), + getProjectAssignedOrganizationsName: (projectId) => + api.get(`/api/Project/get/assigned/organization/dropdown/${projectId}`), getEmployeeForTaskAssign: (projectId, serviceId, organizationId) => { let url = `/api/Project/get/task/team/${projectId}`; diff --git a/src/repositories/ServiceProjectRepository.jsx b/src/repositories/ServiceProjectRepository.jsx new file mode 100644 index 00000000..ff3b9d3b --- /dev/null +++ b/src/repositories/ServiceProjectRepository.jsx @@ -0,0 +1,60 @@ +import { isAction } from "@reduxjs/toolkit"; +import { api } from "../utils/axiosClient"; + +export const ServiceProjectRepository = { + //#region Service Project + CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data), + GetServiceProjects: (pageSize, pageNumber,searchString) => + api.get( + `/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}` + ), + GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`), + UpdateServiceProject: (id, data) => + api.put(`/api/ServiceProject/edit/${id}`, data), + DeleteServiceProject: (id, isActive = false) => + api.delete(`/api/ServiceProject/delete/${id}?isActive=${isActive}`), + AllocateEmployee: (data) => + api.post(`/api/ServiceProject/manage/allocation`, data), + GetAllocatedEmployees: (projectId, isActive) => + api.get( + `/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} ` + ), + //#endregion + + //#region Job + + CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data), + GetJobList: (pageSize, pageNumber, isActive, projectId,isArchive) => + api.get( + `/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}&isArchive=${isArchive}` + ), + GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`), + AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data), + GetJobComment: (jobTicketId, pageSize, pageNumber) => + api.get( + `/api/ServiceProject/job/comment/list?jobTicketId=${jobTicketId}&pageSize=${pageSize}&pageNumber=${pageNumber}` + ), + GetJobTags: () => api.get(`/api/ServiceProject/job/tag/list`), + UpdateJob: (id, patchData) => + api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, { + "Content-Type": "application/json-patch+json", + }), + //#endregion + + //#region Project Branch + CreateBranch: (data) => api.post(`/api/ServiceProject/branch/create`, data), + UpdateBranch: (id, data) => + api.put(`/api/ServiceProject/branch/edit/${id}`, data), + + GetBranchList: (projectId, isActive, pageSize, pageNumber, searchString) => { + return api.get( + `/api/ServiceProject/branch/list/${projectId}?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}` + ); + }, + + GetBranchDetail: (id) => api.get(`/api/ServiceProject/branch/details/${id}`), + DeleteBranch: (id, isActive = false) => + api.delete(`/api/ServiceProject/branch/delete/${id}?isActive=${isActive}`), + + GetBranchTypeList: () => api.get(`/api/serviceproject/branch-type/list`), +}; diff --git a/src/repositories/TaskRepository.jsx b/src/repositories/TaskRepository.jsx index 9afc47a0..06e6a8fc 100644 --- a/src/repositories/TaskRepository.jsx +++ b/src/repositories/TaskRepository.jsx @@ -16,6 +16,8 @@ export const TasksRepository = { return api.get(url); }, + getTaskListFilter:(projectId)=>api.get(`/api/task/filter/${projectId}`), + getTaskById: (id) => api.get(`/api/task/get/${id}`), reportTask: (data) => api.post("api/task/report", data), taskComments: (data) => api.post("api/task/comment", data), diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index c0787e95..dc556a6e 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -53,6 +53,15 @@ import DailyProgrssReport from "../pages/DailyProgressReport/DailyProgrssReport" import ProjectPage from "../pages/project/ProjectPage"; import { ComingSoonPage } from "../pages/Misc/ComingSoonPage"; import ImageGalleryPage from "../pages/Gallary/ImageGallaryPage"; +import CollectionPage from "../pages/collections/CollectionPage"; +import SubscriptionSummary from "../pages/Home/SubscriptionSummary"; +import MakeSubscription from "../pages/Home/MakeSubscription"; +import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage"; +import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage"; +import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage"; +import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail"; +import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob"; +import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1"; const router = createBrowserRouter( [ { @@ -72,6 +81,10 @@ const router = createBrowserRouter( ], }, { path: "/auth/switch/org", element: }, + { + path: "/auth/subscripe/:frequency/:planId", + element: , + }, { element: , errorElement: , @@ -83,6 +96,9 @@ const router = createBrowserRouter( { path: "/projects", element: }, { path: "/projects/details", element: }, { path: "/project/manage/:projectId", element: }, + { path: "/service-projects/:projectId", element: }, + {path:"/service/job",element:}, + { path: "/employees", element: }, { path: "/employee/:employeeId", element: }, // { path: "/employee/manage", element: }, @@ -90,11 +106,20 @@ const router = createBrowserRouter( { path: "/directory", element: }, { path: "/inventory", element: }, { path: "/activities/attendance", element: }, - { path: "/activities/records/:projectId?", element: }, + { + path: "/activities/records/:projectId?", + element: , + }, { path: "/activities/task", element: }, { path: "/activities/reports", element: }, { path: "/gallary", element: }, + { path: "/expenses/:status?/:project?", element: }, { path: "/expenses", element: }, + { path: "/payment-request", element: }, + { path: "/recurring-payment", element: }, + { path: "/advance-payment", element: }, + { path: "/advance-payment/:employeeId", element: }, + { path: "/collection", element: }, { path: "/masters", element: }, { path: "/tenants", element: }, { path: "/tenants/new-tenant", element: }, diff --git a/src/router/ProtectedRoute.jsx b/src/router/ProtectedRoute.jsx index 9179d43d..2a741df6 100644 --- a/src/router/ProtectedRoute.jsx +++ b/src/router/ProtectedRoute.jsx @@ -24,7 +24,6 @@ const validateToken = async () => { sessionStorage.getItem("refreshToken"); if (!refreshTokenStored){ - console.log("no refrh tokem"); removeSession() return false }; diff --git a/src/services/signalRService.js b/src/services/signalRService.js index 05ff808a..067dc1a2 100644 --- a/src/services/signalRService.js +++ b/src/services/signalRService.js @@ -23,8 +23,11 @@ export function startSignalR(loggedUser) { transport: signalR.HttpTransportType.LongPolling, withCredentials: false, }) + // .withKeepAliveInterval(30000) + // .withServerTimeout(30000) .withAutomaticReconnect() .build(); + connection.serverTimeoutInMilliseconds = 30000; // 60 seconds const todayDate = new Date(); const today = new Date( Date.UTC(todayDate.getFullYear(), todayDate.getMonth(), todayDate.getDate()) @@ -40,6 +43,14 @@ export function startSignalR(loggedUser) { // ---- Handlers for invalidate or remove ---- const queryInvalidators = { + Payment_Request: () => { + queryClient.invalidateQueries({ queryKey: ["paymentRequestList"] }), + queryClient.invalidateQueries({ queryKey: ["paymentRequest"] }); + }, + Recurring_Payment: () => { + queryClient.invalidateQueries({ queryKey: ["recurringExpenseList"] }), + queryClient.invalidateQueries({ queryKey: ["recurringExpense"] }); + }, Expanse: () => { queryClient.invalidateQueries({ queryKey: ["Expenses"] }), queryClient.invalidateQueries({ queryKey: ["Expense"] }); @@ -79,7 +90,8 @@ export function startSignalR(loggedUser) { if (today === checkIn) eventBus.emit("attendance", data); const onlyDate = Number(checkIn.substring(8, 10)); - const afterTwoDay = checkIn.substring(0,8) + (onlyDate + 2).toString().padStart(2, "0"); + const afterTwoDay = + checkIn.substring(0, 8) + (onlyDate + 2).toString().padStart(2, "0"); if ( afterTwoDay <= today && (response.activity === 4 || response.activity === 5) @@ -131,6 +143,27 @@ export function startSignalR(loggedUser) { ) { emitters.image_gallery(); } + + // --- Service Project ---------------- + + if (keyword === "Service_Project") { + queryClient.invalidateQueries(["serviceProjects"]); + queryClient.invalidateQueries(["serviceProject"]); + } + if (keyword === "Project_Branch") { + queryClient.invalidateQueries(["branches"]); + } + + if (keyword === "Service_Project_Allocation") { + queryClient.invalidateQueries(["serviceProjectTeam"]); + } + if (keyword === "Job_Ticket") { + queryClient.invalidateQueries(["serviceProjectJobs"]); + queryClient.invalidateQueries(["service-job"]); + } + if (keyword === "Job_Ticket_Comment") { + queryClient.invalidateQueries(["jobComments"]); + } }); connection.start(); diff --git a/src/slices/localVariablesSlice.jsx b/src/slices/localVariablesSlice.jsx index b520b7e5..2e2951ba 100644 --- a/src/slices/localVariablesSlice.jsx +++ b/src/slices/localVariablesSlice.jsx @@ -11,6 +11,9 @@ const localVariablesSlice = createSlice({ SelectedOrg: "", }, + // PopUp + popups: {}, + // Modal for all simple pass Name modals: { @@ -31,6 +34,14 @@ const localVariablesSlice = createSlice({ AuthModal: { isOpen: false, }, + + selfTenant: { + tenantEnquireId: null, + planId: null, + details: null, + frequency: null, + paymentDetailId: null, + }, }, reducers: { changeMaster: (state, action) => { @@ -86,16 +97,52 @@ const localVariablesSlice = createSlice({ openModal: (state, action) => { const { modalType, data } = action.payload; - state.modals[modalType] = { isOpen: true, ...data }; + + state.modals[modalType] = { + isOpen: true, + data: data ?? {}, + }; }, + closeModal: (state, action) => { const { modalType } = action.payload; - state.modals[modalType] = { ...state.modals[modalType], isOpen: false }; + state.modals[modalType] = { + isOpen: false, + data: null, + }; }, + toggleModal: (state, action) => { const { modalType } = action.payload; - state.modals[modalType].isOpen = !state.modals[modalType].isOpen; + const modal = state.modals[modalType] || {}; + modal.isOpen = !modal.isOpen; }, + + setSelfTenant: (state, action) => { + state.selfTenant.tenantEnquireId = + action.payload.tenantEnquireId ?? state.selfTenant.tenantEnquireId; + state.selfTenant.planId = + action.payload.planId ?? state.selfTenant.planId; + state.selfTenant.details = + action.payload.details ?? state.selfTenant.details; + state.selfTenant.frequency = + action.payload.frequency ?? state.selfTenant.frequency; + state.selfTenant.paymentDetailId = + action.payload.paymentDetailId ?? state.selfTenant.paymentDetailId; + }, + + openPopup: (state, action) => { + const id = action.payload; + state.popups[id] = true; + }, + closePopup: (state, action) => { + const id = action.payload; + state.popups[id] = false; + }, + togglePopup: (state, action) => { + const id = action.payload; + state.popups[id] = !state.popups[id]; + } }, }); @@ -110,6 +157,10 @@ export const { toggleOrgModal, openAuthModal, closeAuthModal, - setOrganization,openModal, closeModal, toggleModal + setOrganization, + openModal, + closeModal, + toggleModal, + setSelfTenant,openPopup, closePopup, togglePopup } = localVariablesSlice.actions; export default localVariablesSlice.reducer; diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js index 0611d9c8..7daa6f5e 100644 --- a/src/utils/appUtils.js +++ b/src/utils/appUtils.js @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { format, parseISO } from "date-fns"; +import { parseISO, formatISO } from "date-fns"; export const formatFileSize = (bytes) => { if (bytes < 1024) return bytes + " B"; else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"; @@ -34,8 +34,24 @@ export const getColorNameFromHex = (hex) => { } } - return null; // + return null; }; +export const getJobStatusBadge = (statusId) => { + if (!statusId) return "bg-label-secondary"; + + const map = { + "32d76a02-8f44-4aa0-9b66-c3716c45a918": "bg-label-primary", // New + "cfa1886d-055f-4ded-84c6-42a2a8a14a66": "bg-label-info", // Assigned + "5a6873a5-fed7-4745-a52f-8f61bf3bd72d": "bg-label-warning", // In Progress + "aab71020-2fb8-44d9-9430-c9a7e9bf33b0": "bg-label-label-dark", // Review + "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7": "bg-label-success", // Done + "3ddeefb5-ae3c-4e10-a922-35e0a452bb69": "bg-label-secondary", // Closed + "75a0c8b8-9c6a-41af-80bf-b35bab722eb2": "bg-label-danger", // On Hold + }; + + return map[statusId] || "bg-label-secondary"; +}; + export const useDebounce = (value, delay = 500) => { const [debouncedValue, setDebouncedValue] = useState(value); @@ -51,13 +67,15 @@ export const useDebounce = (value, delay = 500) => { export const getIconByFileType = (type = "") => { const lower = type.toLowerCase(); - if (lower === "application/pdf") return "bxs-file-pdf"; - if (lower.includes("word")) return "bxs-file-doc"; + if (lower === "application/pdf") return "bxs-file-pdf text-danger"; + if (lower.includes("word")) return "bxs-file-doc text-primary text-primry"; if (lower.includes("excel") || lower.includes("spreadsheet")) - return "bxs-file-xls"; + return "bxs-file-xls text-primry"; if (lower === "image/png") return "bxs-file-png"; - if (lower === "image/jpeg" || lower === "image/jpg") return "bxs-file-jpg"; - if (lower.includes("zip") || lower.includes("rar")) return "bxs-file-archive"; + if (lower === "image/jpeg" || lower === "image/jpg") + return "bxs-file-jpg text-primry"; + if (lower.includes("zip") || lower.includes("rar")) + return "bxs-file-archive text-secondary"; return "bx bx-file"; }; @@ -70,25 +88,177 @@ export const normalizeAllowedContentTypes = (allowedContentType) => { return []; }; export function localToUtc(dateString) { -if (!dateString || typeof dateString !== "string") return null; + if (!dateString || typeof dateString !== "string") return null; -const parts = dateString.trim().split("-"); -if (parts.length !== 3) return null; + const parts = dateString.trim().split("-"); + if (parts.length !== 3) return null; -let day, month, year; + let day, month, year; -if (parts[0].length === 4) { -// Format: yyyy-mm-dd -[year, month, day] = parts; -} else { -// Format: dd-mm-yyyy -[day, month, year] = parts; + if (parts[0].length === 4) { + // Format: yyyy-mm-dd + [year, month, day] = parts; + } else { + // Format: dd-mm-yyyy + [day, month, year] = parts; + } + + if (!day || !month || !year) return null; + + const date = new Date( + Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0) + ); + return isNaN(date.getTime()) ? null : date.toISOString(); +} + +export const formatCurrency = (amount, currency = "INR", locale = "en-US") => { + return new Intl.NumberFormat(locale, { + style: "currency", + notation: "compact", + compactDisplay: "short", + currency: currency, + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(amount); +}; + +export const countDigit = (num) => { + return Math.abs(num).toString().length; +}; +export const formatFigure = ( + amount, + { + type = "number", + currency = "INR", + locale = "en-US", + notation = "standard", // standard or compact + compactDisplay = "short", + minimumFractionDigits = 0, + maximumFractionDigits = 2, + } = {} +) => { + if (amount == null || isNaN(amount)) return "-"; + + const formatterOptions = { + style: + type === "currency" + ? "currency" + : type === "percent" + ? "percent" + : "decimal", + notation: notation, + compactDisplay, + minimumFractionDigits, + maximumFractionDigits, + }; + + if (type === "currency") { + formatterOptions.currency = currency; + } + + return new Intl.NumberFormat(locale, formatterOptions).format(amount); +}; + +export const frequencyLabel = ( + freq, + isLong = false, + isMonthRequired = false +) => { + const frequency = parseInt(freq, 10); + switch (frequency) { + case 0: + if (isLong && isMonthRequired) { + return { planDurationInString: "1 Month", planDurationInInt: 1 }; + } + if (isLong) { + return "1 Month"; + } else { + return "1 mon"; + } + case 1: + if (isLong && isMonthRequired) { + return { + planDurationInString: "Quarterly (3 Months)", + planDurationInInt: 3, + }; + } + if (isLong) { + return "Quarterly (3 Months)"; + } else { + return "3 mon"; + } + case 2: + if (isLong && isMonthRequired) { + return { planDurationInString: "6 Month", planDurationInInt: 6 }; + } + if (isLong) { + return "6 Month"; + } else { + return "6 mon"; + } + case 3: + if (isLong && isMonthRequired) { + return { planDurationInString: "1 Year", planDurationInInt: 12 }; + } + if (isLong) { + return "1 Year"; + } else { + return "1 yr"; + } + default: + return isLong ? "Unknown" : "N/A"; + } +}; + +const badgeColors = ["primary", "secondary", "success", "warning", "info"]; + +let colorIndex = 0; + +export function getNextBadgeColor(type = "label") { + const color = badgeColors[colorIndex]; + colorIndex = (colorIndex + 1) % badgeColors.length; + return `rounded-pill text-bg-${color}`; +} + +export function daysLeft(startDate, dueDate) { + if (!startDate || !dueDate) { + return { days: null, color: "label-secondary" }; + } + + const start = new Date(startDate); + const due = new Date(dueDate); + + const today = new Date(); + const diffTime = due.getTime() - today.getTime(); + const days = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + let color = "label-primary"; // default + + if (days < 0) { + color = "label-danger"; // overdue → red + } else if (days <= 15) { + color = "label-warning"; // near due → yellow + } else { + color = "label-primary"; // safe range + } + + return { days, color }; +} + +export function calculateTDSPercentage(baseAmount = 0, taxAmount = 0, tdsPercentage = 0) { + baseAmount = Number(baseAmount) || 0; + taxAmount = Number(taxAmount) || 0; + tdsPercentage = Number(tdsPercentage) || 0; + + const grossAmount = baseAmount + taxAmount; + const tdsAmount = (baseAmount * tdsPercentage) / 100; + const netPayable = grossAmount - tdsAmount; + + return { + grossAmount, + tdsAmount, + netPayable, + }; } -if (!day || !month || !year) return null; -const date = new Date( -Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0) -); -return isNaN(date.getTime()) ? null : date.toISOString(); -} \ No newline at end of file diff --git a/src/utils/axiosClient.jsx b/src/utils/axiosClient.jsx index bf26500e..40981633 100644 --- a/src/utils/axiosClient.jsx +++ b/src/utils/axiosClient.jsx @@ -72,7 +72,9 @@ axiosClient.interceptors.response.use( if (status === 401 && !isRefreshRequest) { originalRequest._retry = true; - const refreshToken = localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken"); + const refreshToken = + localStorage.getItem("refreshToken") || + sessionStorage.getItem("refreshToken"); if ( !refreshToken || @@ -87,7 +89,9 @@ axiosClient.interceptors.response.use( try { // Refresh token call const res = await axiosClient.post("/api/Auth/refresh-token", { - token: localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken"), + token: + localStorage.getItem("jwtToken") || + sessionStorage.getItem("jwtToken"), refreshToken, }); @@ -144,6 +148,11 @@ export const api = { headers: { ...customHeaders }, authRequired: false, }), + getPublic: (url, data = {}, customHeaders = {}) => + apiRequest("get", url, data, { + headers: { ...customHeaders }, + authRequired: false, + }), // Authenticated routes get: (url, params = {}, customHeaders = {}) => @@ -169,6 +178,12 @@ export const api = { headers: { ...customHeaders }, authRequired: true, }), + patch: (url, data = {}, customHeaders = {}) => + apiRequest("patch", url, data, { + headers: { ...customHeaders }, + authRequired: true, + }), + }; // Redirect helper diff --git a/src/utils/blockUI.js b/src/utils/blockUI.js new file mode 100644 index 00000000..b516e059 --- /dev/null +++ b/src/utils/blockUI.js @@ -0,0 +1,32 @@ +export const blockUI = (message = 'Please wait...') => { + if (window.$ && window.$.blockUI) { + window.$.blockUI({ + message: ` +
    +
    +
    +
    +
    +
    +
    +
    +

    ${message}

    +
    `, + css: { + backgroundColor: 'transparent', + border: '0', + color: '#fff', + }, + overlayCSS: { + opacity: 0.5, + cursor: 'wait', + }, + }); + } +}; + +export const unblockUI = () => { + if (window.$ && window.$.unblockUI) { + window.$.unblockUI(); + } +}; diff --git a/src/utils/constants.jsx b/src/utils/constants.jsx index 62c686c9..1ca271ec 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -1,3 +1,8 @@ +export const BASE_URL = process.env.VITE_BASE_URL; + +// export const BASE_URL = "https://api.marcoaiot.com"; + + export const THRESH_HOLD = 48; // hours export const DURATION_TIME = 10; // minutes export const ITEMS_PER_PAGE = 20; @@ -59,14 +64,17 @@ export const APPROVE_EXPENSE = "eaafdd76-8aac-45f9-a530-315589c6deca"; export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; -export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; +export const EXPENSE_MANAGE = "bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3"; -export const EXPENSE_REJECTEDBY = [ - "d1ee5eec-24b6-4364-8673-a8f859c60729", - "965eda62-7907-4963-b4a1-657fb0b2724b", -]; -export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"; + +// --------------------------------Collection---------------------------- + +export const ADMIN_COLLECTION = "dbf17591-09fe-4c93-9e1a-12db8f5cc5de"; +export const VIEW_COLLECTION = "c8d7eea5-4033-4aad-9ebe-76de49896830"; +export const CREATE_COLLECTION = "b93141fd-dbd3-4051-8f57-bf25d18e3555"; +export const EDIT_COLLECTION = "455187b4-fef1-41f9-b3d0-025d0b6302c3"; +export const ADDPAYMENT_COLLECTION = "061d9ccd-85b4-4cb0-be06-2f9f32cebb72"; // ----------------------------Tenant------------------------- export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b"; @@ -83,9 +91,16 @@ export const DOWNLOAD_DOCUMENT = "404373d0-860f-490e-a575-1c086ffbce1d"; export const VERIFY_DOCUMENT = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0"; // -------------------Application Role------------------------------ -// 1 - Expense Manage +// 1 - Expense Manage- Status +export const EXPENSE_REJECTEDBY = [ + "965eda62-7907-4963-b4a1-657fb0b2724b", + "d1ee5eec-24b6-4364-8673-a8f859c60729", +]; +export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"; export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7"; - +export const EXPENSE_CREATE = "b8586f67-dc19-49c3-b4af-224149efe1d3" +export const INR_CURRENCY_CODE = "78e96e4a-7ce0-4164-ae3a-c833ad45ec2c"; +export const EXPENSE_PROCESSED = "61578360-3a49-4c34-8604-7b35a3787b95"; export const TENANT_STATUS = [ { id: "62b05792-5115-4f99-8ff5-e8374859b191", name: "Active" }, { id: "c0b5def8-087e-4235-b3a4-8e2f0ed91b94", name: "In Active" }, @@ -140,8 +155,90 @@ export const PROJECT_STATUS = [ label: "Completed", }, ]; + +export const DEFAULT_CURRENCY = "78e96e4a-7ce0-4164-ae3a-c833ad45ec2c"; + +export const EXPENSE_STATUS = { + daft: "297e0d8f-f668-41b5-bfea-e03b354251c8", + review_pending: "6537018f-f4e9-4cb3-a210-6c3b2da999d7", + payment_pending: "f18c5cfd-7815-4341-8da2-2c2d65778e27", + approve_pending: "4068007f-c92f-4f37-a907-bc15fe57d4d8", + payment_processed: "61578360-3a49-4c34-8604-7b35a3787b95", + payment_done: "b8586f67-dc19-49c3-b4af-224149efe1d3", +} + +export const UUID_REGEX = + /^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + +export const ALLOW_PROJECTSTATUS_ID = [ + "603e994b-a27f-4e5d-a251-f3d69b0498ba", + "cdad86aa-8a56-4ff4-b633-9c629057dfef", + "b74da4c2-d07e-46f2-9919-e75e49b12731", +]; + export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000"; -export const BASE_URL = process.env.VITE_BASE_URL; +export const FREQUENCY_FOR_RECURRING = { + 0: "Monthly", + 1: "Quarterly", + 2: "Half-Yearly", + 3: "Yearly", + 4: "Daily", + 5: "Weekly" +}; -// export const BASE_URL = "https://api.marcoaiot.com"; +export const PAYEE_RECURRING_EXPENSE = [ + { + id: "da462422-13b2-45cc-a175-910a225f6fc8", + label: "Active", + }, + { + id: "306856fb-5655-42eb-bf8b-808bb5e84725", + label: "Completed", + }, + { + id: "3ec864d2-8bf5-42fb-ba70-5090301dd816", + label: "De-Activited", + }, + { + id: "8bfc9346-e092-4a80-acbf-515ae1ef6868", + label: "Paused", + }, +]; + + +//#region Service Project and Jobs +export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69" + +//#endregion + +export const JOBS_STATUS_IDS = [ + { + id: "32d76a02-8f44-4aa0-9b66-c3716c45a918", + label: "New", + }, + { + id: "cfa1886d-055f-4ded-84c6-42a2a8a14a66", + label: "Assigned", + }, + { + id: "5a6873a5-fed7-4745-a52f-8f61bf3bd72d", + label: "In Progress", + }, + { + id: "aab71020-2fb8-44d9-9430-c9a7e9bf33b0", + label: "Work Done", + }, + { + id: "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7", + label: "Review Done", + }, + { + id: "3ddeefb5-ae3c-4e10-a922-35e0a452bb69", + label: "Closed", + }, + { + id: "75a0c8b8-9c6a-41af-80bf-b35bab722eb2", + label: "On Hold", + }, +]; \ No newline at end of file diff --git a/src/utils/dateUtils.jsx b/src/utils/dateUtils.jsx index 388f7563..b5f59537 100644 --- a/src/utils/dateUtils.jsx +++ b/src/utils/dateUtils.jsx @@ -51,7 +51,7 @@ export const convertShortTime = (dateString) => { }; export const timeElapsed = (checkInTime, timeElapsedInHours) => { - const checkInDate = new Date( checkInTime.split( "T" )[ 0 ] ); + const checkInDate = new Date(checkInTime.split("T")[0]); const currentTime = new Date(); @@ -72,7 +72,7 @@ export const checkIfCurrentDate = (dateString) => { return currentDate?.getTime() === inputDate?.getTime(); }; -export const formatNumber = (num) => { +export const formatNumber = (num) => { if (num == null || isNaN(num)) return "NA"; return Number.isInteger(num) ? num : num.toFixed(2); }; @@ -84,15 +84,25 @@ export const formatUTCToLocalTime = (datetime, timeRequired = false) => { : moment.utc(datetime).local().format("DD MMM YYYY"); }; -export const getCompletionPercentage = (completedWork, plannedWork)=> { +export const getCompletionPercentage = (completedWork, plannedWork) => { if (!plannedWork || plannedWork === 0) return 0; const percentage = (completedWork / plannedWork) * 100; const clamped = Math.min(Math.max(percentage, 0), 100); - return clamped.toFixed(2); + return clamped.toFixed(2); } -export const getTenantStatus =(statusId)=>{ - return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary" +export const formatDate_DayMonth = (monthName, year) => { + if (!monthName || !year) return ""; + try { + const shortMonth = monthName.substring(0, 3); + return `${shortMonth} ${year}`; + } catch { + return ""; + } +}; + +export const getTenantStatus = (statusId) => { + return ActiveTenant === statusId ? " bg-label-success" : "bg-label-secondary" } \ No newline at end of file diff --git a/src/utils/tableExportUtils.jsx b/src/utils/tableExportUtils.jsx index 90a1306a..768ff5ac 100644 --- a/src/utils/tableExportUtils.jsx +++ b/src/utils/tableExportUtils.jsx @@ -40,112 +40,57 @@ export const exportToExcel = (data, fileName = "data") => { * @param {Array} data - Array of objects to export * @param {string} fileName - File name for the PDF (optional) */ -export const exportToPDF = async (data, fileName = "data") => { +const sanitizeText = (text) => { + if (!text) return ""; + // Replace all non-ASCII characters with "?" or remove them + return text.replace(/[^\x00-\x7F]/g, "?"); +}; + +export const exportToPDF = async (data, fileName = "data", columns = null, options = {}) => { if (!data || data.length === 0) return; - // Create a new PDF document const pdfDoc = await PDFDocument.create(); + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); - // Set up the font - const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Use Helvetica font - - // Calculate column widths dynamically based on data content - const headers = Object.keys(data[0]); - const rows = data.map(item => headers.map(header => item[header] || '')); + // Default options + const { + columnWidths = [], // array of widths per column + fontSizeHeader = 12, + fontSizeRow = 10, + rowHeight = 25, + } = options; - const getMaxColumnWidth = (columnIndex) => { - let maxWidth = font.widthOfTextAtSize(headers[columnIndex], 12); - rows.forEach(row => { - const cellText = row[columnIndex].toString(); - maxWidth = Math.max(maxWidth, font.widthOfTextAtSize(cellText, 10)); + const pageWidth = 1000; + const pageHeight = 600; + let page = pdfDoc.addPage([pageWidth, pageHeight]); + const margin = 30; + let y = pageHeight - margin; + + const headers = columns || Object.keys(data[0]); + + // Draw headers + headers.forEach((header, i) => { + const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150); + page.drawText(header, { x, y, font, size: fontSizeHeader }); + }); + y -= rowHeight; + + // Draw rows + data.forEach(row => { + headers.forEach((header, i) => { + const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150); + const text = row[header] || ''; + page.drawText(text, { x, y, font, size: fontSizeRow }); }); - return maxWidth + 10; // Padding for better spacing - }; + y -= rowHeight; - const columnWidths = headers.map((_, index) => getMaxColumnWidth(index)); - const tableX = 30; // X-coordinate for the table start - const rowHeight = 20; // Height of each row (can be adjusted) - const maxPageHeight = 750; // Max available height for content (before a new page is added) - const pageMargin = 30; // Margin from the top of the page - - let tableY = maxPageHeight; // Start Y position for the table - const maxPageWidth = 600; // Max available width for content (before a new page is added) - - // Add the headers and rows to the page - const addHeadersToPage = (page, scaleFactor) => { - let xPosition = tableX; - headers.forEach((header, index) => { - page.drawText(header, { - x: xPosition, - y: tableY, - font, - size: 12 * scaleFactor, // Scale the header font size - color: rgb(0, 0, 0), - }); - xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling - }); - tableY -= rowHeight; // Move down after adding headers - }; - - // Add a new page and reset the table position - const addNewPage = (scaleFactor) => { - const page = pdfDoc.addPage([600, 800]); - tableY = maxPageHeight; // Reset Y position for the new page - addHeadersToPage(page, scaleFactor); // Re-add headers to the new page - return page; - }; - - // Create the first page and add headers - let page = pdfDoc.addPage([600, 800]); - - // Check if the content fits within the page width, scale if necessary - const checkPageWidth = (row) => { - let totalWidth = columnWidths.reduce((acc, width) => acc + width, 0); - let scaleFactor = 1; - if (totalWidth > maxPageWidth) { - scaleFactor = maxPageWidth / totalWidth; // Scale down if necessary + if (y < margin) { + page = pdfDoc.addPage([pageWidth, pageHeight]); + y = pageHeight - margin; } - - return scaleFactor; - }; - - // Function to check for page breaks when adding a new row - const checkPageBreak = () => { - if (tableY - rowHeight < pageMargin) { - page = addNewPage(scaleFactor); // Add a new page if there is no space for the next row - } - }; - - // Add rows to the PDF with pagination and horizontal scaling - rows.forEach(row => { - checkPageBreak(); // Check for page break before adding each row - - const scaleFactor = checkPageWidth(row); // Get the scaling factor for the row - - // Add headers to the first page and each new page with the same scale factor - if (tableY === maxPageHeight) { - addHeadersToPage(page, scaleFactor); // Add headers only on the first page - } - - let xPosition = tableX; - row.forEach((value, index) => { - page.drawText(value.toString(), { - x: xPosition, - y: tableY, - font, - size: 10 * scaleFactor, // Scale the font size - color: rgb(0, 0, 0), - }); - xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling - }); - - tableY -= rowHeight; // Move down to the next row position }); - // Serialize the document to bytes const pdfBytes = await pdfDoc.save(); - - // Trigger a download of the PDF const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); @@ -153,15 +98,110 @@ export const exportToPDF = async (data, fileName = "data") => { link.click(); }; + + + + +/** + * Export JSON data to PDF in a card-style format + * @param {Array} data - Array of objects to export + * @param {string} fileName - File name for the PDF (optional) + */ +export const exportToPDF1 = async (data, fileName = "data") => { + if (!data || data.length === 0) return; + + const pdfDoc = await PDFDocument.create(); + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + const pageWidth = 600; + const pageHeight = 800; + const margin = 30; + const cardSpacing = 20; + const cardPadding = 10; + let page = pdfDoc.addPage([pageWidth, pageHeight]); + let y = pageHeight - margin; + + for (const item of data) { + const title = item.ContactName || ""; + const subtitle = `by ${item.CreatedBy || ""} on ${item.CreatedAt || ""}`; + const body = item.Note || ""; + + const cardHeight = 80 + (body.length / 60) * 14; // approximate height for body text + + if (y - cardHeight < margin) { + page = pdfDoc.addPage([pageWidth, pageHeight]); + y = pageHeight - margin; + } + + // Draw card border + page.drawRectangle({ + x: margin, + y: y - cardHeight, + width: pageWidth - 2 * margin, + height: cardHeight, + borderColor: rgb(0.7, 0.7, 0.7), + borderWidth: 1, + color: rgb(1, 1, 1), + }); + + // Draw title + page.drawText(title, { + x: margin + cardPadding, + y: y - 20, + font: boldFont, + size: 12, + color: rgb(0.1, 0.1, 0.1), + }); + + // Draw subtitle + page.drawText(subtitle, { + x: margin + cardPadding, + y: y - 35, + font, + size: 10, + color: rgb(0.4, 0.4, 0.4), + }); + + // Draw body text (wrap manually) + const lines = body.match(/(.|[\r\n]){1,80}/g) || []; + lines.forEach((line, i) => { + page.drawText(line, { + x: margin + cardPadding, + y: y - 50 - i * 12, + font, + size: 10, + color: rgb(0.2, 0.2, 0.2), + }); + }); + + y -= cardHeight + cardSpacing; + } + + const pdfBytes = await pdfDoc.save(); + const blob = new Blob([pdfBytes], { type: 'application/pdf' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${fileName}.pdf`; + link.click(); +}; + + + + /** * Print the HTML table by accepting the table element or a reference. * @param {HTMLElement} table - The table element (or ref) to print */ export const printTable = (table) => { if (table) { - const newWindow = window.open("", "", "width=600,height=600"); // Open a new window + const clone = table.cloneNode(true); + // Remove last column (Actions) from all rows + clone.querySelectorAll("tr").forEach((row) => { + row.removeChild(row.lastElementChild); + }); - // Inject styles for the table and body + const newWindow = window.open("", "", "width=600,height=600"); newWindow.document.write("Print Table"); const style = document.createElement('style'); style.innerHTML = ` @@ -171,16 +211,14 @@ export const printTable = (table) => { th { background-color: #f2f2f2; } `; newWindow.document.head.appendChild(style); - + newWindow.document.write(""); - newWindow.document.write(table.outerHTML); // Write the table HTML to the new window + newWindow.document.write(clone.outerHTML); newWindow.document.write(""); - - newWindow.document.close(); // Close the document stream - - // Wait for the document to load before triggering print + newWindow.document.close(); newWindow.onload = () => { - newWindow.print(); // Trigger the print dialog after the content is loaded + newWindow.print(); }; } }; +