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 5b780c72..b05f71c1 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -3,6 +3,37 @@ --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); @@ -25,13 +56,102 @@ font-size: 2rem; .text-md-b { font-weight: normal; } -.cursor-wait{ - cursor:wait; +.stepper-container { + position: relative; } -.cursor-notallowed { - cursor: not-allowed; + +.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 */ @@ -146,7 +266,10 @@ font-weight: normal; .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; } /* ========================== @@ -301,6 +424,35 @@ font-weight: normal; .w-10-xl{ width: 2.5rem; } } -.cursor-not-allowed{ - cursor: not-allowed; + +/* ------------------------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 fcb1fcc4..4559ac2d 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.875rem; --bs-body-font-weight: 400; --bs-body-line-height: 1.375; --bs-body-color: #646e78; @@ -32577,9 +32578,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) } @@ -32591,4 +32590,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 bb217139..8ef00bcd 100644 --- a/src/ModalProvider.jsx +++ b/src/ModalProvider.jsx @@ -5,21 +5,24 @@ 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:isCollectionNew} = useModal("newCollection"); + const { isOpen: isChangePass } = useModal("ChangePassword"); + const { isOpen: isCollectionNew } = useModal("newCollection"); + const { isOpen: isServiceTeamAllocation } = useModal("ServiceTeamAllocation"); return ( <> {isOpen && } {isAuthOpen && } - {isChangePass && } - {isCollectionNew && } + {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 52d022e3..253e9f21 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) => ( - - + + + - + {dates.map((date, idx) => ( - {roles.map((role) => ( {tableData.map((row, idx) => { const value = row[role]; - const cellStyle = - value > 0 ? { backgroundColor: "#d5d5d5" } : {}; + const cellStyle = value > 0 ? { backgroundColor: "#d5d5d5" } : {}; return ( + + {data?.map((project) => ( + + {projectColumns.map((col) => ( + + ))} + + + ))} + +
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 b7ddfaff..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 ? ( + <> diff --git a/src/components/Activities/AttendcesLogs.jsx b/src/components/Activities/AttendcesLogs.jsx index 4660f956..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"; @@ -16,6 +16,7 @@ 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); @@ -174,49 +175,51 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { return ( <>
-
- {/* Date Range Picker */} -
- -
- - {/* Pending Attendance Switch */} -
- setShowPending(e.target.checked)} - /> - -
+ {/* Left Side - Date Picker */} +
+
+ {/* Right Side - Pending Attendance Switch */} +
+ setShowPending(e.target.checked)} + /> + +
+
{isLoading ? (
-

Loading...

+
) : filteredSearchData?.length > 0 ? ( +
@@ -255,8 +258,8 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { className="table-row-header" > diff --git a/src/components/Activities/InfraPlanning.jsx b/src/components/Activities/InfraPlanning.jsx index e7a4003a..08f03a93 100644 --- a/src/components/Activities/InfraPlanning.jsx +++ b/src/components/Activities/InfraPlanning.jsx @@ -24,8 +24,7 @@ import { useProfile } from "../../hooks/useProfile"; import { refreshData, setProjectId } from "../../slices/localVariablesSlice"; import InfraTable from "../Project/Infrastructure/InfraTable"; import { useSelectedProject } from "../../slices/apiDataManager"; -import Loader from "../common/Loader"; - +import { SpinnerLoader } from "../common/Loader"; const InfraPlanning = () => { const { profile: LoggedUser, refetch: fetchData } = useProfile(); const dispatch = useDispatch(); @@ -57,7 +56,14 @@ const InfraPlanning = () => { } if (isLoading) { - return ; + return ( +
+ +
+ ); } if (isFetched && (!projectInfra || projectInfra.length === 0)) { diff --git a/src/components/Activities/Regularization.jsx b/src/components/Activities/Regularization.jsx index 954820c2..8e0ff865 100644 --- a/src/components/Activities/Regularization.jsx +++ b/src/components/Activities/Regularization.jsx @@ -15,6 +15,7 @@ import { import { useQueryClient } from "@tanstack/react-query"; import Pagination from "../../components/common/Pagination"; import { useNavigate } from "react-router-dom"; +import { SpinnerLoader } from "../common/Loader"; const Regularization = ({ handleRequest, @@ -106,15 +107,15 @@ const Regularization = ({ return (
{loading ? (
-

Loading...

+
) : currentItems?.length > 0 ? (
- - {moment(currentDate).format("DD-MM-YYYY")} + + {formatUTCToLocalTime(currentDate)}
diff --git a/src/components/AdvancePayment/AdvancePaymentList.jsx b/src/components/AdvancePayment/AdvancePaymentList.jsx new file mode 100644 index 00000000..e9113b48 --- /dev/null +++ b/src/components/AdvancePayment/AdvancePaymentList.jsx @@ -0,0 +1,234 @@ + +import React, { useEffect, useMemo } from "react"; +import { useExpenseTransactions } from "../../hooks/useExpense"; +import Error from "../common/Error"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Loader, { SpinnerLoader } from "../common/Loader"; +import { useForm, useFormContext } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { employee } from "../../data/masters"; +import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage"; +import { formatFigure } from "../../utils/appUtils"; + +const AdvancePaymentList = ({ employeeId }) => { + const { setBalance } = useAdvancePaymentContext(); + const { data, isError, isLoading, error, isFetching } = + useExpenseTransactions(employeeId, { enabled: !!employeeId }); + + const records = Array.isArray(data) ? data : []; + + let currentBalance = 0; + const rowsWithBalance = records.map((r) => { + const isCredit = r.amount > 0; + const credit = isCredit ? r.amount : 0; + const debit = !isCredit ? Math.abs(r.amount) : 0; + currentBalance += credit - debit; + return { + id: r.id, + description: r.title || "-", + projectName: r.project?.name || "-", + createdAt: r.createdAt, + credit, + debit, + financeUId: r.financeUId, + balance: currentBalance, + }; + }); + + useEffect(() => { + if (!employeeId) { + setBalance(null); + return; + } + + if (rowsWithBalance.length > 0) { + setBalance(rowsWithBalance[rowsWithBalance.length - 1].balance); + } else { + setBalance(0); + } + }, [employeeId, data, setBalance]); + + if (!employeeId) { + return ( +
+

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/Charts/HorizontalBarChart.jsx b/src/components/Charts/HorizontalBarChart.jsx index 5a02c98f..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 2af3636d..8394a158 100644 --- a/src/components/DailyProgressRport/TaskReportFilterPanel.jsx +++ b/src/components/DailyProgressRport/TaskReportFilterPanel.jsx @@ -54,6 +54,7 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
{ Total Pending{" "} This shows the total pending tasks for each activity on that date.

} > @@ -213,6 +214,7 @@ const TaskReportList = () => { Reported/Planned{" "} This shows the reported versus planned tasks for each activity on that date.

} > 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 { ))}
{role} {value} diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 4c09056a..ad5ead8c 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -12,11 +12,11 @@ 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 = () => { @@ -29,16 +29,16 @@ const Dashboard = () => {
{isAllProjectsSelected && (
- +
)}
- +
- +
{isAllProjectsSelected && ( @@ -46,32 +46,31 @@ const Dashboard = () => {
)} - - {!isAllProjectsSelected && ( -
- -
- )} -
-
-
- + {!isAllProjectsSelected && ( +
+
-
+ )} + {!isAllProjectsSelected && ( +
+ +
+ )}
- {!isAllProjectsSelected && ( -
- + +
+
+
- )} +
diff --git a/src/components/Dashboard/ExpenseAnalysis.jsx b/src/components/Dashboard/ExpenseAnalysis.jsx index f398e8b5..3c6b0d08 100644 --- a/src/components/Dashboard/ExpenseAnalysis.jsx +++ b/src/components/Dashboard/ExpenseAnalysis.jsx @@ -6,11 +6,13 @@ 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, loading } = useProjectName(); + const { projectNames } = useProjectName(); const methods = useForm({ defaultValues: { startDate: "", endDate: "" }, @@ -49,7 +51,7 @@ const ExpenseAnalysis = () => { labels, legend: { show: false }, dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` }, - colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"], + colors: flatColors, plotOptions: { pie: { donut: { @@ -78,88 +80,99 @@ const ExpenseAnalysis = () => { return ( <> - -
-
-
Expense Breakdown
- {/*

Category Wise Expense Breakdown

*/} -

{projectName}

-
- -
- - - -
+
+
+
Expense Breakdown
+

{projectName}

- {/* Card body */} -
- {isLoading && ( -
- Loading... -
- )} +
+ + + +
+
- {!isLoading && report.length === 0 && ( -
No data found
- )} +
+ {isLoading && ( +
+ +
+ )} - {!isLoading && report.length > 0 && ( - <> - {isFetching && ( -
- Loading... -
- )} + {!isLoading && report.length === 0 && ( +
+ No data found +
+ )} -
+ {!isLoading && report.length > 0 && ( + <> + {isFetching && ( +
+ Loading... +
+ )} + +
+ {/* Chart Column */} +
-
-
+ {/* Data/Legend Column */} +
+
{report.map((item, idx) => (
-
- + + {item.projectName} + + - - -
-
- {item.projectName} - {formatCurrency(item.totalApprovedAmount)}
+ ))}
- - )} -
- - {/* Header */} - +
+ + )} +
); }; diff --git a/src/components/Dashboard/ExpenseByProject.jsx b/src/components/Dashboard/ExpenseByProject.jsx index 96e3fb56..c589bc45 100644 --- a/src/components/Dashboard/ExpenseByProject.jsx +++ b/src/components/Dashboard/ExpenseByProject.jsx @@ -1,12 +1,13 @@ import React, { useState, useEffect } from "react"; import Chart from "react-apexcharts"; -import { useExpenseType } from "../../hooks/masterHook/useMaster"; import { useSelector } from "react-redux"; import { useExpenseDataByProject } from "../../hooks/useDashboard_Data"; import { formatCurrency } from "../../utils/appUtils"; import { formatDate_DayMonth } from "../../utils/dateUtils"; import { useProjectName } from "../../hooks/useProjects"; import { useSelectedProject } from "../../slices/apiDataManager"; +import { SpinnerLoader } from "../common/Loader"; +import { useExpenseCategory } from "../../hooks/masterHook/useMaster"; const ExpenseByProject = () => { const projectId = useSelector((store) => store.localVariables.projectId); @@ -18,7 +19,7 @@ const ExpenseByProject = () => { const [chartData, setChartData] = useState({ categories: [], data: [] }); const selectedProject = useSelectedProject(); - const { ExpenseTypes, loading: typeLoading } = useExpenseType(); + const {expenseCategories , loading: typeLoading } = useExpenseCategory(); const { data: expenseApiData, isLoading } = useExpenseDataByProject( projectId, @@ -49,7 +50,7 @@ const ExpenseByProject = () => { const getSelectedTypeName = () => { if (!selectedType) return "All Types"; - const found = ExpenseTypes.find((t) => t.id === selectedType); + const found = expenseCategories?.find((t) => t.id === selectedType); return found ? found.name : "All Types"; }; @@ -63,6 +64,12 @@ const ExpenseByProject = () => { categories: chartData.categories, labels: { style: { fontSize: "12px" }, rotate: -45 }, }, + yaxis: { + labels: { + formatter: (val) => formatCurrency(val), + style: { fontSize: "12px", colors: "#555" }, + }, + }, tooltip: { y: { formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`, @@ -82,15 +89,15 @@ const ExpenseByProject = () => { ]; return ( -
+
{/* Header */}
-
+
Monthly Expense -

{projectName}

-
+
diff --git a/src/components/Dashboard/ExpenseStatus.jsx b/src/components/Dashboard/ExpenseStatus.jsx index d6fefe7d..a45fb343 100644 --- a/src/components/Dashboard/ExpenseStatus.jsx +++ b/src/components/Dashboard/ExpenseStatus.jsx @@ -103,7 +103,7 @@ const ExpenseStatus = () => {
= 3 ? "text-xl" : "text-2xl" + className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-xl" } text-gray-500`} > {item?.count || 0} @@ -122,7 +122,7 @@ const ExpenseStatus = () => { {isManageExpense && (
handleNavigate(EXPENSE_STATUS.process_pending)} + onClick={() => handleNavigate(EXPENSE_STATUS.payment_processed)} >
{
3 ? "text-" : "text-3xl" + className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-xl" : "text-3xl" } text-md`} > {formatCurrency(data?.totalAmount || 0)} diff --git a/src/components/Dashboard/ProjectCompletionChart.jsx b/src/components/Dashboard/ProjectCompletionChart.jsx index decf7918..b8c656b8 100644 --- a/src/components/Dashboard/ProjectCompletionChart.jsx +++ b/src/components/Dashboard/ProjectCompletionChart.jsx @@ -1,15 +1,16 @@ -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"; const ProjectCompletionChart = () => { - const { data: projects = [], isLoading: loading, isError, error } = useProjects(); - + const [currentPage, setCurrentPage] = useState(1); + const { data: projects, isLoading: loading, isError, error } = useProjects(50,currentPage); // Bar chart logic - const projectNames = projects?.map((p) => p.name) || []; + const projectNames = projects?.data?.map((p) => p.name) || []; const projectProgress = - projects?.map((p) => { + projects?.data?.map((p) => { const completed = p.completedWork || 0; const planned = p.plannedWork || 1; const percent = planned ? (completed / planned) * 100 : 0; @@ -24,7 +25,7 @@ const ProjectCompletionChart = () => {

Projects Completion Status

-
+
{
) : ( -
+
{/* Total Tasks */} -
+

{formatFigure(tasksCardData?.totalTasks ?? 0, { notation: "compact", @@ -56,7 +56,7 @@ const TasksCard = () => {

{/* Completed Tasks */} -
+

{formatFigure(tasksCardData?.completedTasks ?? 0, { notation: "compact", 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/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/Documents/DocumentFilterPanel.jsx b/src/components/Documents/DocumentFilterPanel.jsx index edc613e1..04f54956 100644 --- a/src/components/Documents/DocumentFilterPanel.jsx +++ b/src/components/Documents/DocumentFilterPanel.jsx @@ -13,230 +13,236 @@ import { useParams } from "react-router-dom"; const DocumentFilterPanel = forwardRef( ({ entityTypeId, onApply, setFilterdata }, ref) => { - const [resetKey, setResetKey] = useState(0); - const { status } = useParams(); + const [resetKey, setResetKey] = useState(0); + const { status } = useParams(); - const { data, isError, isLoading, error } = - useDocumentFilterEntities(entityTypeId); + const { data, isError, isLoading, error } = + useDocumentFilterEntities(entityTypeId); - //changes + useEffect(() => { + return () => { + closePanel(); + }; + }, []); - const dynamicDocumentFilterDefaultValues = useMemo(() => { - return { - ...DocumentFilterDefaultValues, - uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [], - documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [], - documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [], - documentTagIds: DocumentFilterDefaultValues.documentTagIds || [], - startDate: DocumentFilterDefaultValues.startDate, - endDate: DocumentFilterDefaultValues.endDate, + //changes + + const dynamicDocumentFilterDefaultValues = useMemo(() => { + return { + ...DocumentFilterDefaultValues, + uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [], + documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [], + documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [], + documentTagIds: DocumentFilterDefaultValues.documentTagIds || [], + startDate: DocumentFilterDefaultValues.startDate, + endDate: DocumentFilterDefaultValues.endDate, + }; + + }, [status]); + + const methods = useForm({ + resolver: zodResolver(DocumentFilterSchema), + defaultValues: dynamicDocumentFilterDefaultValues, + }); + + const { handleSubmit, reset, setValue, watch } = methods; + + // Watch values from form + const isUploadedAt = watch("isUploadedAt"); + const isVerified = watch("isVerified"); + + // Close the offcanvas (bootstrap specific) + const closePanel = () => { + document.querySelector(".offcanvas.show .btn-close")?.click(); }; - }, [status]); + 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 + })); - const methods = useForm({ - resolver: zodResolver(DocumentFilterSchema), - defaultValues: dynamicDocumentFilterDefaultValues, - }); - - const { handleSubmit, reset, setValue, watch } = methods; - - // Watch values from form - const isUploadedAt = watch("isUploadedAt"); - const isVerified = watch("isVerified"); - - // Close the offcanvas (bootstrap specific) - const closePanel = () => { - document.querySelector(".offcanvas.show .btn-close")?.click(); - }; - - useImperativeHandle(ref, () => ({ - resetFieldValue: (name, value) => { - if (value !== undefined) { - setValue(name, value); - } else { - reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] }); + //changes + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); } - }, - getValues: methods.getValues, // optional, to read current filter state - })); + }, [data, setFilterdata]); - //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 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(); + }; - const onClear = () => { - reset(DocumentFilterDefaultValues); - setResetKey((prev) => prev + 1); - onApply(DocumentFilterDefaultValues); - // closePanel(); - }; + if (isLoading) return
Loading...
; + if (isError) + return
Error: {error?.message || "Something went wrong!"}
; - if (isLoading) return
Loading...
; - if (isError) - return
Error: {error?.message || "Something went wrong!"}
; - - const { - uploadedBy = [], - documentCategory = [], - documentType = [], - documentTag = [], - } = data?.data || {}; + const { + uploadedBy = [], + documentCategory = [], + documentType = [], + documentTag = [], + } = data?.data || {}; - return ( - -
- {/* Date Range Section */} -
-
- -
- - + 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/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 686d820c..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(); @@ -82,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 && ( @@ -179,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/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/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 index 66c8a376..77bec83e 100644 --- a/src/components/Expenses/ExpenseFilterChips.jsx +++ b/src/components/Expenses/ExpenseFilterChips.jsx @@ -4,21 +4,19 @@ 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, + name: list?.find((item) => item.id === id)?.name || id, })); chips.push({ key, label, items }); }; - buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds"); buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds"); buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById"); buildGroup(filters.statusIds, filterData.status, "Status", "statusIds"); - buildGroup(filters.ExpenseTypeIds, filterData.expensesType, "Category", "ExpenseTypeIds"); + buildGroup(filters.expenseCategoryIds, filterData.expenseCategory, "Category", "expenseCategoryIds"); if (filters.startDate || filters.endDate) { const start = filters.startDate @@ -40,44 +38,47 @@ const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => { if (!filterChips.length) return null; return ( -
    -
    -
    - {filterChips.map((chip) => ( -
    - {/* Chip Label */} - {chip.label}: +
    +
    +
    + {filterChips.map((chip) => ( +
    + {/* Chip Label */} + {chip.label}: - {/* Chip Items */} -
    - {chip.items.map((item) => ( - - {item.name} -
    -
    - ))} -
    -
    + {/* Chip Items */} +
    + {chip.items.map((item) => ( + + {item.name} +
    + ))} +
    +
    +
    + + + ); }; diff --git a/src/components/Expenses/ExpenseFilterPanel.jsx b/src/components/Expenses/ExpenseFilterPanel.jsx index 3f0085c3..61583902 100644 --- a/src/components/Expenses/ExpenseFilterPanel.jsx +++ b/src/components/Expenses/ExpenseFilterPanel.jsx @@ -31,7 +31,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata } { id: "submittedBy", name: "Submitted By" }, { id: "project", name: "Project" }, { id: "paymentMode", name: "Payment Mode" }, - { id: "expensesType", name: "Expense Category" }, + { id: "expenseCategory", name: "Expense Category" }, { id: "createdAt", name: "Submitted Date" }, ].sort((a, b) => a.name.localeCompare(b.name)); }, []); @@ -46,12 +46,12 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata } projectIds: defaultFilter.projectIds || [], createdByIds: defaultFilter.createdByIds || [], paidById: defaultFilter.paidById || [], - ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [], + expenseCategoryIds: defaultFilter.expenseCategoryIds || [], isTransactionDate: defaultFilter.isTransactionDate ?? true, startDate: defaultFilter.startDate, endDate: defaultFilter.endDate, }; - }, [status]); + }, [status, selectedProjectId]); const methods = useForm({ resolver: zodResolver(SearchSchema), @@ -96,7 +96,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata } endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(), }); handleGroupBy(selectedGroup.id); - closePanel(); + // closePanel(); }; const onClear = () => { @@ -105,7 +105,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata } setSelectedGroup(groupByList[0]); onApply(defaultFilter); handleGroupBy(groupByList[0].id); - closePanel(); + // closePanel(); if (status) { navigate("/expenses", { replace: true }); } @@ -145,10 +145,9 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata } handleGroupBy, selectedGroup.id, appliedStatusId, - selectedProjectId, // ✅ Added dependency + selectedProjectId, ]); - if (isLoading || isFetching) return ; if (isError && isFetched) return
    Something went wrong Here- {error.message}
    ; @@ -181,7 +180,6 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
    - @@ -215,9 +214,9 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata } valueKey="id" /> item.name} valueKey="id" /> diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 6c5c2d74..d1576667 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -12,6 +12,7 @@ import { } from "../../utils/constants"; import { formatCurrency, + formatFigure, getColorNameFromHex, useDebounce, } from "../../utils/appUtils"; @@ -76,7 +77,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { switch (field) { case "transactionDate": - key = item?.transactionDate?.split("T")[0]; + key = formatUTCToLocalTime(item?.transactionDate); displayField = "Transaction Date"; break; case "status": @@ -97,8 +98,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { 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": @@ -128,9 +129,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { align: "text-start mx-2", }, { - key: "expensesType", - label: "Expense Type", - getValue: (e) => e.expensesType?.name || "N/A", + key: "expenseCategory", + label: "Expense Category", + getValue: (e) => e.expenseCategory?.name || "N/A", align: "text-start", }, { @@ -144,9 +145,8 @@ 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) => (
    { lastName={e.createdBy?.lastName} /> - {`${e.createdBy?.firstName ?? ""} ${ - e.createdBy?.lastName ?? "" - }`.trim() || "N/A"} + {`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? "" + }`.trim() || "N/A"}
    ), @@ -175,7 +174,15 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { { key: "amount", label: "Amount", - getValue: (e) => <>{formatCurrency(e?.amount)}, + getValue: (e) => ( + <> + {" "} + {formatFigure(e?.amount, { + type: "currency", + currency: e?.currency?.currencyCode, + })} + + ), isAlwaysVisible: true, align: "text-end", }, @@ -185,17 +192,25 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { align: "text-center", getValue: (e) => ( {e.status?.name || "Unknown"} ), }, ]; - - if (isInitialLoading && !data) return ; + const headers = [ + "Expense Category", + "Payment Mode", + "Submitted By", + "Submitted", + "Amount", + "Status", + "Action", + ]; + if (isInitialLoading && !data) + return ; if (isError) return
    {error?.message}
    ; const grouped = groupBy @@ -219,6 +234,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId ); }; + return ( <> {IsDeleteModalOpen && ( @@ -255,10 +271,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { (col.isAlwaysVisible || groupBy !== col.key) && (
  • ) )} @@ -273,7 +289,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { ) )} @@ -384,15 +414,15 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { )}
    -
    {col.label}
    +
    {col.label}
    -
    +
    {" "} {displayField} :{" "} @@ -293,16 +309,33 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { (col.isAlwaysVisible || groupBy !== col.key) && (
    - {col.customRender - ? col.customRender(expense) - : col.getValue(expense)} +
    + {col.customRender + ? col.customRender(expense) + : col.getValue(expense)} +
    -
    +
    @@ -312,61 +345,58 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { }) } > - {canDetetExpense(expense) && - canEditExpense(expense) && ( -
    - +
      +
    • + setManageExpenseModal({ + IsOpen: true, + expenseId: expense.id, + }) + } > - - -
        - {canDetetExpense(expense) && ( -
      • - setManageExpenseModal({ - IsOpen: true, - expenseId: expense.id, - }) - } - > - - - - Modify - - -
      • - )} + + + + Modify + + + - {canDetetExpense(expense) && ( -
      • { - setIsDeleteModalOpen(true); - setDeletingId(expense.id); - }} - > - - - - Delete - - -
      • - )} -
      -
    - )} + {canDetetExpense(expense) && ( +
  • { + setIsDeleteModalOpen(true); + setDeletingId(expense.id); + }} + > + + + + Delete + + +
  • + )} + +
    + )}
    - {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..d595421d 100644 --- a/src/components/Expenses/ManageExpense.jsx +++ b/src/components/Expenses/ManageExpense.jsx @@ -3,12 +3,13 @@ import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { defaultExpense, ExpenseSchema } from "./ExpenseSchema"; import { formatFileSize, localToUtc } from "../../utils/appUtils"; -import { useProjectName } from "../../hooks/useProjects"; +import { 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,8 @@ import DatePicker from "../common/DatePicker"; import ErrorPage from "../../pages/ErrorPage"; import Label from "../common/Label"; import EmployeeSearchInput from "../common/EmployeeSearchInput"; +import Filelist from "./Filelist"; +import { DEFAULT_CURRENCY } from "../../utils/constants"; const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const { @@ -36,14 +39,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { isLoading, error: ExpenseErrorLoad, } = useExpense(expenseToEdit); - const [ExpenseType, setExpenseType] = useState(); + const [expenseCategory, setExpenseCategory] = 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 +68,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { error, isError: isProjectError, } = useProjectName(); - + const { + data: currencies, + isLoading: currencyLoading, + error: currencyError, + } = useCurrencies(); const { PaymentModes, loading: PaymentModeLoading, @@ -81,6 +88,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { isLoading: EmpLoading, isError: isEmployeeError, } = useEmployeesNameByProject(selectedproject); + const files = watch("billAttachments"); const onFileChange = async (e) => { const newFiles = Array.from(e.target.files); @@ -146,7 +154,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { 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 +165,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, @@ -192,11 +201,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(); @@ -237,30 +248,30 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
    -
    @@ -295,34 +306,10 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { )} -
    - @@ -435,25 +423,53 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { {errors.gstNumber.message} )}
    - - {ExpenseType?.noOfPersonsRequired && ( -
    - - - {errors.noOfPersons && ( - - {errors.noOfPersons.message} - - )} -
    - )} + +
    +
    + + + {errors.currencyId && ( + {errors.currencyId.message} + )} +
    + {expenseCategory?.noOfPersonsRequired && ( +
    + + + {errors.noOfPersons && ( + + {errors.noOfPersons.message} + + )} +
    + )} +
    @@ -510,40 +526,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => { )} {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 +566,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 31f50991..7ea0c3ec 100644 --- a/src/components/Expenses/PreviewDocument.jsx +++ b/src/components/Expenses/PreviewDocument.jsx @@ -1,137 +1,54 @@ -import { useState, useRef ,useEffect} from "react"; +import { useState } from "react"; const PreviewDocument = ({ imageUrl }) => { const [loading, setLoading] = useState(true); const [rotation, setRotation] = useState(0); - const [zoom, setZoom] = useState(1); - const [position, setPosition] = useState({ x: 0, y: 0 }); - const [isDragging, setIsDragging] = useState(false); - const [startPos, setStartPos] = useState({ x: 0, y: 0 }); - const containerRef = useRef(null); - - // Zoom handlers - const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.2, 3)); - const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.2, 0.5)); - - // Mouse wheel zoom - const handleWheel = (e) => { - e.preventDefault(); - const delta = e.deltaY > 0 ? -0.1 : 0.1; - setZoom((prev) => Math.min(Math.max(prev + delta, 0.5), 3)); - }; - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - container.addEventListener("wheel", handleWheel, { passive: false }); - - return () => { - container.removeEventListener("wheel", handleWheel); - }; - }, []); - const handleMouseDown = (e) => { - if (zoom <= 1) return; - setIsDragging(true); - setStartPos({ - x: e.clientX - position.x, - y: e.clientY - position.y, - }); - }; - - const handleMouseMove = (e) => { - if (!isDragging) return; - setPosition({ - x: e.clientX - startPos.x, - y: e.clientY - startPos.y, - }); - }; - - const handleMouseUp = () => setIsDragging(false); - const handleMouseLeave = () => setIsDragging(false); - - const handleReset = () => { - setRotation(0); - setZoom(1); - setPosition({ x: 0, y: 0 }); - }; return ( - <> -
    + <> +
    setRotation((prev) => prev + 90)} > - - -
    +
    + + {loading && ( +
    Loading...
    + )} -
    1 ? (isDragging ? "grabbing" : "grab") : "default", - userSelect: "none", - position: "relative", - }} - > - {loading && ( -
    - Loading... -
    - )} +
    Preview setLoading(false)} + alt="Full View" + className="img-fluid" style={{ - transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${zoom})`, - transition: isDragging ? "none" : "transform 0.3s ease", + maxHeight: "80vh", objectFit: "contain", - maxWidth: "100%", - maxHeight: "100%", display: loading ? "none" : "block", - pointerEvents: "none", + transform: `rotate(${rotation}deg)`, + transition: "transform 0.3s ease", }} + onLoad={() => setLoading(false)} />
    - {/*
    +
    -
    */} - +
    +
    + ); }; export default PreviewDocument; - - - diff --git a/src/components/Expenses/ViewExpense.jsx b/src/components/Expenses/ViewExpense.jsx index cd608dfe..8c032fc0 100644 --- a/src/components/Expenses/ViewExpense.jsx +++ b/src/components/Expenses/ViewExpense.jsx @@ -10,6 +10,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema"; import { useExpenseContext } from "../../pages/Expense/ExpensePage"; import { + calculateTDSPercentage, + formatCurrency, + formatFigure, getColorNameFromHex, getIconByFileType, localToUtc, @@ -18,6 +21,7 @@ import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { EXPENSE_REJECTEDBY, + EXPENSE_STATUS, PROCESS_EXPENSE, REVIEW_EXPENSE, } from "../../utils/constants"; @@ -42,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, @@ -50,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 || [] ); @@ -107,366 +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 && ( -
    -
    -
    + {col.getValue + ? col.getValue(project) + : project[col.key] || "N/A"} + + +
    - {isLoading && ( -
    - {" "} - {isLoading &&

    Loading...

    } - {!isLoading && filteredProjects.length === 0 && ( -

    No projects found.

    - )} -
    - )} - {!isLoading && currentItems.length === 0 && ( -
    -

    No projects found.

    -
    - )} - {!isLoading && totalPages > 1 && ( - - )} +
    ); }; diff --git a/src/components/Project/ProjectOrganization/ProjectAssignedOrgs.jsx b/src/components/Project/ProjectOrganization/ProjectAssignedOrgs.jsx index c4aec957..e8281dfe 100644 --- a/src/components/Project/ProjectOrganization/ProjectAssignedOrgs.jsx +++ b/src/components/Project/ProjectOrganization/ProjectAssignedOrgs.jsx @@ -2,6 +2,7 @@ import React from "react"; import { useProjectAssignedOrganizations } from "../../../hooks/useProjects"; import { useSelectedProject } from "../../../slices/apiDataManager"; import { formatUTCToLocalTime } from "../../../utils/dateUtils"; +import { SpinnerLoader } from "../../common/Loader"; const ProjectAssignedOrgs = () => { const selectedProject = useSelectedProject(); @@ -25,12 +26,12 @@ const ProjectAssignedOrgs = () => { ), align: "text-start", }, - { + { key: "service", label: "Service Name", getValue: (org) => (
    - {org?.service?.name} + {org?.service?.name}
    ), align: "text-start", @@ -49,7 +50,7 @@ const ProjectAssignedOrgs = () => { align: "text-center", }, - { + { key: "organizationType", label: "Organization Type", getValue: (org) => ( @@ -62,7 +63,7 @@ const ProjectAssignedOrgs = () => { ), align: "text-center", }, - { + { key: "assignedDate", label: "Assigned Date", getValue: (org) => ( @@ -78,7 +79,12 @@ const ProjectAssignedOrgs = () => { }, ]; - if (isLoading) return
    Loading...
    ; + if (isLoading) + return ( +
    + +
    + ); if (isError) return
    {error.message}
    ; return ( diff --git a/src/components/Project/ProjectOrganizations.jsx b/src/components/Project/ProjectOrganizations.jsx index b5a91fa6..7e5bfce5 100644 --- a/src/components/Project/ProjectOrganizations.jsx +++ b/src/components/Project/ProjectOrganizations.jsx @@ -21,7 +21,7 @@ const ProjectOrganizations = () => { -
    +
    diff --git a/src/components/Project/ProjectPermission.jsx b/src/components/Project/ProjectPermission.jsx index fa6bf1d2..a043603e 100644 --- a/src/components/Project/ProjectPermission.jsx +++ b/src/components/Project/ProjectPermission.jsx @@ -104,7 +104,6 @@ const hasChanges = permission: payloadPermissions, }; - console.log("Final payload:", payload); updatePermission(payload); }; diff --git a/src/components/Project/ProjectOverview.jsx b/src/components/Project/ProjectStatistics.jsx similarity index 62% rename from src/components/Project/ProjectOverview.jsx rename to src/components/Project/ProjectStatistics.jsx index ee665144..7ec5dbd3 100644 --- a/src/components/Project/ProjectOverview.jsx +++ b/src/components/Project/ProjectStatistics.jsx @@ -9,7 +9,7 @@ import { import ReactApexChart from "react-apexcharts"; import Chart from "react-apexcharts"; -const ProjectOverview = ({ project }) => { +const ProjectStatistics = ({ project }) => { const { data } = useProjects(); const [current_project, setCurrentProject] = useState( data?.find((pro) => pro.id == project) @@ -133,10 +133,10 @@ const ProjectOverview = ({ project }) => { }, }, stroke: { - lineCap: "round", + lineCap: "round", }, - labels: ["Progress"], - series: [percentage], + labels: ["Progress"], + series: [percentage], }; }; const [radialPercentage, setRadialPercentage] = useState(75); // Initial percentage @@ -165,7 +165,7 @@ const ProjectOverview = ({ project }) => { }, [selectedProject]); return ( -
    +
    {" "} @@ -173,78 +173,78 @@ const ProjectOverview = ({ project }) => { Project Statistics
    -
    -
      -
    • -
      - {/* Centered Chart */} -
      -
      - -
      -
      - - {/* Info Section */} -
      -
      - {/* Tasks Planned */} -
      -
      - - - +
      +
        +
      • +
        + {/* Centered Chart */} +
        +
        + +
        -
        - Tasks Planned -
        - {FormattedNumber(current_project?.plannedWork)} -
        + + {/* Info Section */} +
        +
        + {/* Tasks Planned */} +
        +
        + + + +
        +
        + Tasks Planned +
        + {FormattedNumber(current_project?.plannedWork)} +
        +
        +
        + + {/* Tasks Completed */} +
        +
        + + + +
        +
        + Tasks Completed +
        + {FormattedNumber(current_project?.completedWork)} +
        +
        +
        + + {/* Team Size */} +
        +
        + + + +
        +
        + Current Team Size +
        + {FormattedNumber(current_project?.teamSize)} +
        +
        +
        +
        - - {/* Tasks Completed */} -
        -
        - - - -
        -
        - Tasks Completed -
        - {FormattedNumber(current_project?.completedWork)} -
        -
        -
        - - {/* Team Size */} -
        -
        - - - -
        -
        - Current Team Size -
        - {FormattedNumber(current_project?.teamSize)} -
        -
        -
        -
        -
      +
    • +
    - - -
    ); }; -export default ProjectOverview; \ No newline at end of file +export default ProjectStatistics; \ No newline at end of file diff --git a/src/components/Project/Team/Teams.jsx b/src/components/Project/Team/Teams.jsx index 31d27efa..dfd4a86a 100644 --- a/src/components/Project/Team/Teams.jsx +++ b/src/components/Project/Team/Teams.jsx @@ -21,6 +21,7 @@ import { import { useSelectedProject } from "../../../slices/apiDataManager"; import GlobalModel from "../../common/GlobalModel"; import TeamAssignToProject from "./TeamAssignToProject"; +import { SpinnerLoader } from "../../common/Loader"; const Teams = () => { const selectedProject = useSelectedProject(); @@ -158,16 +159,8 @@ const Teams = () => {
    {!servicesLoading && ( <> - {(!assignedServices || assignedServices.length === 0) && ( - - Not Service Assigned - - )} - {assignedServices?.length === 1 && ( - - {assignedServices[0].name} - +
    {assignedServices[0].name}
    )} {assignedServices?.length > 1 && ( @@ -232,7 +225,11 @@ const Teams = () => {
    - {employeeLodaing &&

    Loading..

    } + {employeeLodaing && ( +
    + +
    + )} {projectEmployees && projectEmployees.length > 0 && ( @@ -290,8 +287,8 @@ const Teams = () => { )} diff --git a/src/components/RecurringExpense/ManageRecurringExpense.jsx b/src/components/RecurringExpense/ManageRecurringExpense.jsx new file mode 100644 index 00000000..aad3c468 --- /dev/null +++ b/src/components/RecurringExpense/ManageRecurringExpense.jsx @@ -0,0 +1,584 @@ +import React, { useEffect, useState } from "react"; +import Label from "../common/Label"; +import { Controller, useForm } from "react-hook-form"; +import { + useCurrencies, + useExpenseCategory, + useRecurringStatus, +} from "../../hooks/masterHook/useMaster"; +import DatePicker from "../common/DatePicker"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + defaultRecurringExpense, + PaymentRecurringExpense, +} from "./RecurringExpenseSchema"; +import { + FREQUENCY_FOR_RECURRING, + INR_CURRENCY_CODE, +} from "../../utils/constants"; +import { useProjectName } from "../../hooks/useProjects"; +import { + useCreateRecurringExpense, + usePayee, + useRecurringExpenseDetail, + useUpdateRecurringExpense, +} from "../../hooks/useExpense"; +import InputSuggestions from "../common/InputSuggestion"; +import { useEmployeesName } from "../../hooks/useEmployees"; +import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag"; +import HoverPopup from "../common/HoverPopup"; + +const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => { + const { + data, + isLoading, + isError, + error: requestError, + } = useRecurringExpenseDetail(requestToEdit); + const { data: employees } = useEmployeesName(null, null, true); + //APIs + const { + projectNames, + loading: projectLoading, + error, + isError: isProjectError, + } = useProjectName(); + const { + data: currencyData, + isLoading: currencyLoading, + isError: currencyError, + } = useCurrencies(); + const { + data: statusData, + isLoading: statusLoading, + isError: statusError, + } = useRecurringStatus(); + const { + data: Payees, + isLoading: isPayeeLoaing, + isError: isPayeeError, + error: payeeError, + } = usePayee(); + const { + expenseCategories, + loading: ExpenseLoading, + error: ExpenseError, + } = useExpenseCategory(); + + const schema = PaymentRecurringExpense(); + const { + register, + control, + watch, + handleSubmit, + setValue, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: defaultRecurringExpense, + }); + const handleClose = () => { + reset(); + closeModal(); + }; + + const { mutate: CreateRecurringExpense, isPending: createPending } = + useCreateRecurringExpense(() => { + handleClose(); + }); + const { mutate: RecurringExpenseUpdate, isPending } = + useUpdateRecurringExpense(() => handleClose()); + const handleEmailGetting = (userArray = []) => { + if (!Array.isArray(userArray) || userArray.length === 0) return []; + + return userArray + .map((empId) => { + const foundUser = employees?.data?.find((user) => user.id === empId); + return foundUser?.email || null; + }) + .filter(Boolean) + .join(","); + }; + useEffect(() => { + if (requestToEdit && data) { + reset({ + title: data.title || "", + description: data.description || "", + payee: data.payee || "", + notifyTo: data.notifyTo ? data.notifyTo.map((usr) => usr.id) : [], + currencyId: data.currency.id || "", + amount: data.amount || "", + strikeDate: data.strikeDate?.slice(0, 10) || "", + projectId: data.project.id || "", + paymentBufferDays: data.paymentBufferDays || "", + // numberOfIteration: data.numberOfIteration || "", + endDate: data.endDate?.slice(0, 10) || "", + expenseCategoryId: data.expenseCategory.id || "", + statusId: data.status.id || "", + frequency: data.frequency || "", + isVariable: data.isVariable || false, + }); + } + }, [data, reset]); + + useEffect(() => { + if (!requestToEdit && currencyData && currencyData.length > 0) { + const inrCurrency = currencyData.find((c) => c.id === INR_CURRENCY_CODE); + if (inrCurrency) { + setValue("currencyId", INR_CURRENCY_CODE, { shouldValidate: true }); + } + } + }, [currencyData, requestToEdit, setValue]); + + const StrikeDate = watch("strikeDate") + + const onSubmit = (fromdata) => { + let payload = { + ...fromdata, + strikeDate: fromdata.strikeDate + ? new Date(fromdata.strikeDate).toISOString() + : null, + endDate: fromdata.endDate + ? new Date(fromdata.endDate).toISOString() + : null, + notifyTo: handleEmailGetting(fromdata.notifyTo), + }; + if (requestToEdit) { + const editPayload = { ...payload, id: data.id }; + RecurringExpenseUpdate({ id: data.id, payload: editPayload }); + } else { + CreateRecurringExpense(payload); + } + }; + + return ( +
    +
    + {requestToEdit + ? "Update Expense Recurring " + : "Create Expense Recurring"} +
    + + {/* Project and Category */} +
    +
    + + + {errors.projectId && ( + {errors.projectId.message} + )} +
    + +
    + + + {errors.expenseCategoryId && ( + + {errors.expenseCategoryId.message} + + )} +
    +
    + + {/* Title and Is Variable */} +
    +
    + + + {errors.title && ( + {errors.title.message} + )} +
    + +
    +
    + + + Choose whether the payment amount varies or remains fixed each cycle. +
    + Is Variable: Amount changes per cycle. +
    + Fixed: Amount stays constant. +

    + } + > + +
    +
    + + ( +
    +
    + field.onChange(true)} + /> + +
    + +
    + field.onChange(false)} + /> + +
    +
    + )} + /> + + {errors.isVariable && ( + {errors.isVariable.message} + )} +
    + +
    + + {/* Date and Amount */} +
    +
    + + + {errors.strikeDate && ( + {errors.strikeDate.message} + )} +
    + +
    + + + {errors.amount && ( + {errors.amount.message} + )} +
    +
    + + {/* Payee and Currency */} +
    +
    + + + setValue("payee", val, { shouldValidate: true }) + } + error={errors.payee?.message} + placeholder="Select or enter payee" + /> +
    + +
    + + + {errors.currencyId && ( + {errors.currencyId.message} + )} +
    +
    + + {/* Frequency To and Status Id */} +
    +
    +
    + + + Defines how often payments or billing occur, such as monthly, quarterly, or annually. +

    + } + > + +
    +
    + + + + {errors.frequency && ( + {errors.frequency.message} + )} +
    + +
    + + + {errors.statusId && ( + {errors.statusId.message} + )} +
    +
    + + {/* Payment Buffer Days and End Date */} +
    + +
    +
    + + + Number of extra days allowed after the due date before payment is considered late. +

    + } + > + +
    +
    + + + + {errors.paymentBufferDays && ( + + {errors.paymentBufferDays.message} + + )} +
    + + +
    +
    + + + The date when the last payment in the recurrence occurs. +

    + } + > + +
    +
    + + + + {errors.endDate && ( + {errors.endDate.message} + )} +
    + +
    + + +
    +
    + + + {/* */} + + {errors.notifyTo && ( + {errors.notifyTo.message} + )} +
    +
    + + {/* Description */} +
    +
    + + + {errors.description && ( + + {errors.description.message} + + )} +
    +
    + +
    + + +
    + +
    + ); +}; + +export default ManageRecurringExpense; diff --git a/src/components/RecurringExpense/RecurringExpenseFilterPanel.jsx b/src/components/RecurringExpense/RecurringExpenseFilterPanel.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/RecurringExpense/RecurringExpenseList.jsx b/src/components/RecurringExpense/RecurringExpenseList.jsx new file mode 100644 index 00000000..338a3f38 --- /dev/null +++ b/src/components/RecurringExpense/RecurringExpenseList.jsx @@ -0,0 +1,303 @@ +import React, { useState } from "react"; +import { + EXPENSE_DRAFT, + EXPENSE_REJECTEDBY, + FREQUENCY_FOR_RECURRING, + ITEMS_PER_PAGE, + PAYEE_RECURRING_EXPENSE, +} from "../../utils/constants"; +import { formatCurrency, useDebounce } from "../../utils/appUtils"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { ExpenseTableSkeleton } from "../Expenses/ExpenseSkeleton"; +import ConfirmModal from "../common/ConfirmModal"; +import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import Error from "../common/Error"; +import { useRecurringExpenseContext } from "../../pages/RecurringExpense/RecurringExpensePage"; +import { useRecurringExpenseList } from "../../hooks/useExpense"; +import Pagination from "../common/Pagination"; +import { SpinnerLoader } from "../common/Loader"; + +const RecurringExpenseList = ({ search, filterStatuses }) => { + const { setManageRequest, setVieRequest, setViewRecurring } = + useRecurringExpenseContext(); + const navigate = useNavigate(); + const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [deletingId, setDeletingId] = useState(null); + + const SelfId = useSelector( + (store) => store?.globalVariables?.loginUser?.employeeInfo?.id + ); + + const statusColorMap = { + "da462422-13b2-45cc-a175-910a225f6fc8": "primary", // Active + "306856fb-5655-42eb-bf8b-808bb5e84725": "success", // Completed + "3ec864d2-8bf5-42fb-ba70-5090301dd816": "danger", // De-Activated + "8bfc9346-e092-4a80-acbf-515ae1ef6868": "warning", // Paused + }; + + const recurringExpenseColumns = [ + { + key: "expenseCategory", + label: "Category", + align: "text-start", + getValue: (e) => e?.expenseCategory?.name || "N/A", + }, + { + key: "title", + label: "Title", + align: "text-start", + getValue: (e) => e?.title || "N/A", + }, + { + key: "payee", + label: "Payee", + align: "text-start", + getValue: (e) => e?.payee || "N/A", + }, + { + key: "frequency", + label: "Frequency", + align: "text-start", + getValue: (e) => + e?.frequency !== undefined && e?.frequency !== null + ? FREQUENCY_FOR_RECURRING[e.frequency] || "N/A" + : "N/A", + }, + { + key: "amount", + label: "Amount", + align: "text-end", + getValue: (e) => + e?.amount + ? `${ + e?.currency?.symbol ? e.currency.symbol + " " : "" + }${e.amount.toLocaleString()}` + : "N/A", + }, + { + key: "createdAt", + label: "Next Generation Date", + align: "text-center", + getValue: (e) => + e?.createdAt ? formatUTCToLocalTime(e.createdAt) : "N/A", + }, + { + key: "status", + label: "Status", + align: "text-center", + getValue: (e) => { + const color = statusColorMap[e?.status?.id] || "secondary"; + const label = PAYEE_RECURRING_EXPENSE.find( + (s) => s.id === e?.status?.id + )?.label; + return ( + + {label || e?.status?.name || "N/A"} + + ); + }, + }, + ]; + + const [currentPage, setCurrentPage] = useState(1); + const debouncedSearch = useDebounce(search, 500); + + const { data, isLoading, isError, error, isRefetching, refetch } = + useRecurringExpenseList( + ITEMS_PER_PAGE, + currentPage, + {}, + true, + debouncedSearch + ); + + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + + if (isError) { + return ; + } + + const header = [ + "Category", + "Title", + "Amount", + "Payee", + "Frequency", + "Next Generation", + "Status", + "Action", + ]; + + if (isLoading) return ; + + const canEditExpense = (recurringExpense) => { + // return ( + // (recurringExpense?.expenseStatus?.id === EXPENSE_DRAFT || + // EXPENSE_REJECTEDBY.includes(recurringExpense?.expenseStatus.id)) && + // recurringExpense?.createdBy?.id === SelfId + // ); + }; + + const canDeleteExpense = (request) => { + return ( + request?.expenseStatus?.id === EXPENSE_DRAFT && + request?.createdBy?.id === SelfId + ); + }; + + const filteredData = data?.data?.filter((item) => + filterStatuses.includes(item?.status?.id) + ); + + const handleDelete = (id) => { + setDeletingId(id); + DeleteExpense( + { id }, + { + onSettled: () => { + setDeletingId(null); + setIsDeleteModalOpen(false); + }, + } + ); + }; + + return ( + <> + {IsDeleteModalOpen && ( + setIsDeleteModalOpen(false)} + paramData={deletingId} + /> + )} + +
    +
    +
    + {Array.isArray(filteredData) && filteredData.length > 0 && ( +
    {emp.reAllocationDate ? moment(emp.reAllocationDate).format( - "DD-MMM-YYYY" - ) + "DD-MMM-YYYY" + ) : "Present"}
    + + + {recurringExpenseColumns.map((col) => ( + + ))} + + + + + + {filteredData?.length > 0 ? ( + filteredData?.map((recurringExpense) => ( + + {recurringExpenseColumns.map((col) => ( + + ))} + + + )) + ) : ( + + + + )} + +
    + {col.label} + Action
    + {col?.customRender + ? col?.customRender(recurringExpense) + : col?.getValue(recurringExpense)} + +
    + + setViewRecurring({ + recurringId: recurringExpense?.id, + view: true, + }) + } + > + +
    + +
      +
    • + setManageRequest({ + IsOpen: true, + RecurringId: recurringExpense?.id, + }) + } + > + + + Modify + +
    • + +
    • { + setIsDeleteModalOpen(true); + setDeletingId(recurringExpense.id); + }} + > + + + Delete + +
    • +
    +
    +
    +
    + )} + {!filteredData || + filteredData.length === 0 + && ( +
    + {isError ? (

    {error.message}

    ):(

    No Recurring Expense Found

    )} +
    + )} +
    + + + {/* Pagination */} + + + + ); +}; + +export default RecurringExpenseList; diff --git a/src/components/RecurringExpense/RecurringExpenseSchema.js b/src/components/RecurringExpense/RecurringExpenseSchema.js new file mode 100644 index 00000000..a2feb508 --- /dev/null +++ b/src/components/RecurringExpense/RecurringExpenseSchema.js @@ -0,0 +1,113 @@ +import { boolean, z } from "zod"; +import { INR_CURRENCY_CODE } from "../../utils/constants"; + +export const PaymentRecurringExpense = () => { + return z.object({ + title: z.string().min(1, { message: "Title is required" }).transform((val) => val.trim()), + description: z.string().min(1, { message: "Description is required" }).transform((val) => val.trim()), + payee: z.string().min(1, { message: "Payee name is required" }).transform((val) => val.trim()), + notifyTo: z.array(z.string()).min(1,"Please select at lest one user"), + currencyId: z + .string() + .min(1, { message: "Currency is required" }) + .transform((val) => val.trim()), + + amount: z + .number({ + required_error: "Amount is required", + invalid_type_error: "Amount must be a number", + }) + .min(1, { message: "Amount must be greater than 0" }) + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + + strikeDate: z + .string() + .min(1, { message: "Strike Date is required" }) + .refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date format", + }) + .transform((val) => val.trim()), + + projectId: z + .string() + .min(1, { message: "Project is required" }) + .transform((val) => val.trim()), + + paymentBufferDays: z + .number({ + required_error: "Buffer days is required", + invalid_type_error: "Buffer days must be a number", + }) + .min(0, { message: "Buffer days cannot be negative" }), + + endDate: z + .string() + .min(1, { message: "End Date is required" }) + .refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date format", + }) + .transform((val) => val.trim()), + + expenseCategoryId: z + .string() + .min(1, { message: "Expense Category is required" }) + .transform((val) => val.trim()), + + statusId: z + .string() + .min(1, { message: "Please select a status" }) + .transform((val) => val.trim()), + + frequency: z + .number({ + required_error: "Frequency is required", + invalid_type_error: "Frequency must be a number", + }) + .refine((val) => [0, 1, 2, 3, 4, 5].includes(val), { + message: "Invalid frequency selected", + }), + + isVariable: z.boolean().optional(), + }); +}; + + +export const defaultRecurringExpense = { + title: "", + description: "", + payee: "", + notifyTo: [], + currencyId: "", + amount: 0, + strikeDate: "", + projectId: "", + paymentBufferDays: 0, + endDate: "", + expenseCategoryId: "", + statusId: "", + frequency: 1, + isVariable: true, +}; + + +export const SearchRecurringExpenseSchema = z.object({ + title: z.array(z.string()).optional(), + description: z.array(z.string()).optional(), + payee: z.array(z.string()).optional(), + notifyTo: z.array(z.string()).optional(), + currencyId: z.array(z.string()).optional(), + amount: z.array(z.string()).optional(), + strikeDate: z.string().optional(), + projectId: z.string().optional(), + paymentBufferDays: z.string().optional(), + numberOfIteration: z.string().optional(), + expenseCategoryId: z.string().optional(), + statusId: z.string().optional(), + frequency: z.string().optional(), + isVariable: z.string().optional(), +}); + + + diff --git a/src/components/RecurringExpense/RecurringRexpenseList.jsx b/src/components/RecurringExpense/RecurringRexpenseList.jsx new file mode 100644 index 00000000..ef88ae00 --- /dev/null +++ b/src/components/RecurringExpense/RecurringRexpenseList.jsx @@ -0,0 +1,342 @@ +import React, { useState } from "react"; +import { + EXPENSE_DRAFT, + EXPENSE_REJECTEDBY, + ITEMS_PER_PAGE, +} from "../../utils/constants"; +import { + formatCurrency, + getColorNameFromHex, + useDebounce, +} from "../../utils/appUtils"; +import { usePaymentRequestList } from "../../hooks/useExpense"; +import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils"; +import Avatar from "../../components/common/Avatar"; +import { ExpenseTableSkeleton } from "../Expenses/ExpenseSkeleton"; +import ConfirmModal from "../common/ConfirmModal"; +import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import Error from "../common/Error"; +import { useRecurringExpenseContext } from "../../pages/RecurringExpense/RecurringExpensePage"; + +const RecurringExpenseList = ({ filters, groupBy = "submittedBy", search }) => { + const { setManageRequest, setVieRequest } = useRecurringExpenseContext(); + const navigate = useNavigate(); + const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [deletingId, setDeletingId] = useState(null); + const SelfId = useSelector( + (store) => store?.globalVariables?.loginUser?.employeeInfo?.id + ); + const groupByField = (items, field) => { + return items.reduce((acc, item) => { + let key; + let displayField; + + switch (field) { + case "transactionDate": + key = item?.transactionDate?.split("T")[0]; + displayField = "Transaction Date"; + break; + case "status": + key = item?.status?.displayName || "Unknown"; + displayField = "Status"; + break; + case "submittedBy": + key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? "" + }`.trim(); + displayField = "Submitted By"; + break; + case "project": + key = item?.project?.name || "Unknown Project"; + displayField = "Project"; + break; + case "paymentMode": + key = item?.paymentMode?.name || "Unknown Mode"; + displayField = "Payment Mode"; + break; + case "expensesType": + key = item?.expensesType?.name || "Unknown Type"; + displayField = "Expense Category"; + break; + case "createdAt": + key = item?.createdAt?.split("T")[0] || "Unknown Date"; + displayField = "Created Date"; + break; + default: + key = "Others"; + displayField = "Others"; + } + + const groupKey = `${field}_${key}`; // unique key for object property + if (!acc[groupKey]) { + acc[groupKey] = { key, displayField, items: [] }; + } + + acc[groupKey].items.push(item); + return acc; + }, {}); + }; + + const paymentRequestColumns = [ + { + key: "paymentRequestUID", + label: "Template Name", + align: "text-start mx-2", + getValue: (e) => e.paymentRequestUID || "N/A", + }, + { + key: "title", + label: "Frequency", + align: "text-start", + getValue: (e) => e.title || "N/A", + }, + { + key: "createdAt", + label: "Next Generation Date", + align: "text-start", + getValue: (e) => formatUTCToLocalTime(e?.createdAt), + }, + { + key: "createdAt", + label: "Status", + align: "text-start", + getValue: (e) => formatUTCToLocalTime(e?.createdAt), + }, + + ]; + + const [currentPage, setCurrentPage] = useState(1); + const debouncedSearch = useDebounce(search, 500); + + const { data, isLoading, isError, error, isRefetching, refetch } = + usePaymentRequestList( + ITEMS_PER_PAGE, + currentPage, + filters, + true, + debouncedSearch + ); + + const paymentRequestData = data?.data || []; + const totalPages = data?.data?.totalPages || 1; + + if (isError) { + return ; + } + const header = [ + "Request ID", + "Request Title", + "Submitted By", + "Submitted On", + "Amount", + "Status", + "Action", + ]; + if (isLoading) return ; + + const grouped = groupBy + ? groupByField(data?.data ?? [], groupBy) + : { All: data?.data ?? [] }; + const IsGroupedByDate = [ + { key: "transactionDate", displayField: "Transaction Date" }, + { key: "createdAt", displayField: "created Date" }, + ]?.includes(groupBy); + + const canEditExpense = (paymentRequest) => { + return ( + (paymentRequest?.expenseStatus?.id === EXPENSE_DRAFT || + EXPENSE_REJECTEDBY.includes(paymentRequest?.expenseStatus.id)) && + paymentRequest?.createdBy?.id === SelfId + ); + }; + const canDetetExpense = (request) => { + return ( + request?.expenseStatus?.id === EXPENSE_DRAFT && + request?.createdBy?.id === SelfId + ); + }; + + const handleDelete = (id) => { + setDeletingId(id); + DeleteExpense( + { id }, + { + onSettled: () => { + setDeletingId(null); + setIsDeleteModalOpen(false); + }, + } + ); + }; + + return ( + <> + {IsDeleteModalOpen && ( + setIsDeleteModalOpen(false)} + // loading={isPending} + paramData={deletingId} + /> + )} +
    +
    + + + + {paymentRequestColumns.map((col) => ( + + ))} + + + + + + {Object.keys(grouped).length > 0 ? ( + Object.values(grouped).map(({ key, displayField, items }) => ( + + + + + {items?.map((paymentRequest) => ( + + {paymentRequestColumns.map( + (col) => + (col.isAlwaysVisible || groupBy !== col.key) && ( + + ) + )} + + + ))} + + )) + ) : ( + + + + )} + +
    + {col.label} + Action
    +
    + {" "} + + {displayField} :{" "} + {" "} + + {IsGroupedByDate ? formatUTCToLocalTime(key) : key} + +
    +
    + {col?.customRender + ? col?.customRender(paymentRequest) + : col?.getValue(paymentRequest)} + +
    + + setVieRequest({ + requestId: paymentRequest.id, + view: true, + }) + } + > + {canEditExpense(paymentRequest) && ( +
    + +
      +
    • + setManageRequest({ + IsOpen: true, + RequestId: paymentRequest.id, + }) + } + > + + + + Modify + + +
    • + + {canDetetExpense(paymentRequest) && ( +
    • { + setIsDeleteModalOpen(true); + setDeletingId(paymentRequest.id); + }} + > + + + + Delete + + +
    • + )} +
    +
    + )} +
    +
    +
    +

    No Request Found

    +
    +
    +
    + + {/* Pagination */} + {totalPages > 1 && ( +
    + +
    + )} +
    + + ); +}; + +export default RecurringExpenseList; diff --git a/src/components/RecurringExpense/ViewRecurringExpense.jsx b/src/components/RecurringExpense/ViewRecurringExpense.jsx new file mode 100644 index 00000000..5c9d2baa --- /dev/null +++ b/src/components/RecurringExpense/ViewRecurringExpense.jsx @@ -0,0 +1,317 @@ +import React from "react"; +import { useRecurringExpenseDetail } from "../../hooks/useExpense"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { formatFigure, getColorNameFromHex } from "../../utils/appUtils"; +import Avatar from "../common/Avatar"; +import { FREQUENCY_FOR_RECURRING } from "../../utils/constants"; +import { ExpenseDetailsSkeleton } from "../Expenses/ExpenseSkeleton"; + +const ViewRecurringExpense = ({ RecurringId }) => { + const { data, isLoading, isError, error, isFetching } = + useRecurringExpenseDetail(RecurringId); + + const statusColorMap = { + "da462422-13b2-45cc-a175-910a225f6fc8": "primary", // Active + "306856fb-5655-42eb-bf8b-808bb5e84725": "success", // Completed + "3ec864d2-8bf5-42fb-ba70-5090301dd816": "danger", // De-Activated + "8bfc9346-e092-4a80-acbf-515ae1ef6868": "warning", // Paused + }; + if (isLoading) return ; + + return ( +
    +
    +
    Recurring Expense Details
    +
    +
    + {/*
    */} +
    + {/* Row 1 Recurring Id and Status */} + +
    + {data?.recurringPaymentUID} + + {data?.status?.name || "N/A"} + +
    + + {/* Row 2 Category*/} + +
    +
    + +
    + {data?.expenseCategory?.name || "N/A"} +
    +
    +
    + + {/* Row 3 Amount and Project */} + +
    +
    + +
    + {data?.amount != null + ? `${data?.currency?.symbol ?? "¥"} ${Number( + data.amount + ).toFixed(2)} ${data?.currency?.currencyCode ?? "CN"}` + : "N/A"} +
    +
    +
    + +
    +
    + +
    {data?.project?.name || "N/A"}
    +
    +
    + + {/* Row 4 Created At and Title*/} + +
    +
    + + {/*
    + {formatUTCToLocalTime(data?.createdAt, true)} +
    */} + +
    + {data?.createdAt + ? formatUTCToLocalTime(data.createdAt, true) + : "N/A"} +
    +
    +
    + +
    +
    + +
    {data?.title || "N/A"}
    +
    +
    + + {/* Row 5 Payee and Notify*/} + +
    +
    + +
    {data?.payee || "N/A"}
    +
    +
    + +
    +
    + + +
    + {data?.notifyTo?.length > 0 + ? data.notifyTo?.map((user, index) => ( + + {user.email} + {index < data?.notifyTo?.length - 1 && ", "} + + )) + : "N/A"} +
    +
    +
    + + {/* Row 6 Strike Date*/} + +
    +
    + + {/*
    + {formatUTCToLocalTime(data?.strikeDate)} +
    */} +
    + {data?.strikeDate + ? formatUTCToLocalTime(data.strikeDate, true) + : "N/A"} +
    +
    +
    + + {/* Row 7 Frequency and Buffer Days*/} + +
    +
    + +
    + {data?.frequency !== undefined + ? FREQUENCY_FOR_RECURRING[data.frequency] + : "N/A"} +
    +
    +
    + +
    +
    + +
    + {data?.paymentBufferDays || "N/A"} +
    +
    +
    + + {/* Row 8 Updated At and Number of Iteration*/} + +
    +
    + +
    + {data?.updatedAt + ? formatUTCToLocalTime(data.updatedAt, true) + : "N/A"} +
    +
    +
    + +
    +
    + +
    + {data?.endDate + ? formatUTCToLocalTime(data.endDate, true) + : "N/A"} +
    +
    +
    + + {/* Row 9 Created By and Updated By*/} + +
    +
    + + + + {`${data?.createdBy?.firstName ?? ""} ${data?.createdBy?.lastName ?? "" + }`.trim() || "N/A"} + +
    +
    + {data?.updatedBy && ( +
    +
    + + + <> + + + {`${data.updatedBy.firstName ?? ""} ${data.updatedBy.lastName ?? "" + }`.trim() || "N/A"} + + +
    +
    + )} + + {/* Row 10 Description */} + +
    + +
    + {data?.description || "N/A"} +
    +
    +
    + {/*
    */} +
    +
    + ); +}; + +export default ViewRecurringExpense; diff --git a/src/components/ServiceProject/ChangeStatus.jsx b/src/components/ServiceProject/ChangeStatus.jsx new file mode 100644 index 00000000..49b2fc70 --- /dev/null +++ b/src/components/ServiceProject/ChangeStatus.jsx @@ -0,0 +1,100 @@ +import SelectField from "../common/Forms/SelectField"; +import { useJobStatus } from "../../hooks/masterHook/useMaster"; +import { SpinnerLoader } from "../common/Loader"; +import Error from "../common/Error"; +import { z } from "zod"; +import { + AppFormController, + AppFormProvider, + useAppForm, +} from "../../hooks/appHooks/useAppForm"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useDispatch, useSelector } from "react-redux"; +import { closePopup } from "../../slices/localVariablesSlice"; +import { useUpdateServiceProjectJob } from "../../hooks/useServiceProject"; + +export const ChangeStatusSchema = z.object({ + statusId: z.string().min(1, { message: "Please select status" }), +}); +const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => { + const { data, isLoading, isError, error } = useJobStatus(statusId, projectId); + const dispatch = useDispatch(); + const methods = useAppForm({ + resolver: zodResolver(ChangeStatusSchema), + defaultValues: { statusId: "" }, + }); + + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = methods; + + const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => { + // handleClose(); + }); + const onSubmit = (formData) => { + const payload = + [ + { + op: "replace", + path: "/statusId", + value: formData.statusId, + }, + ]; + + + UpdateStatus({ id: jobId, payload }); + }; + + const handleClose = () => { + dispatch(closePopup(popUpId)); + }; + return ( + +
    +
    + ( + + )} + /> + + {errors.statusId && ( + {errors.statusId.message} + )} +
    + +
    + + + +
    +
    +
    + ); +}; + +export default ChangeStatus; diff --git a/src/components/ServiceProject/JobComments.jsx b/src/components/ServiceProject/JobComments.jsx new file mode 100644 index 00000000..84d31665 --- /dev/null +++ b/src/components/ServiceProject/JobComments.jsx @@ -0,0 +1,216 @@ +import React, { useEffect, useState } from "react"; +import Avatar from "../common/Avatar"; +import { useAppForm } from "../../hooks/appHooks/useAppForm"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { JobCommentSchema } from "./ServiceProjectSchema"; +import { + useAddCommentJob, + useJobComments, +} from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Filelist from "../Expenses/Filelist"; +import { formatFileSize, getIconByFileType } from "../../utils/appUtils"; + +const JobComments = ({ data }) => { + const { + register, + handleSubmit, + watch, + reset, + setValue, + formState: { errors }, + } = useAppForm({ + resolver: zodResolver(JobCommentSchema), + defaultValues: { comment: "", attachments: [] }, + }); + + const { + data: comments, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useJobComments(data?.id, ITEMS_PER_PAGE, 1); + const jobComments = comments?.pages?.flatMap((p) => p?.data ?? []) ?? []; + + const { mutate: AddComment, isPending } = useAddCommentJob(() => reset()); + const onSubmit = (formData) => { + formData.jobTicketId = data?.id; + AddComment(formData); + }; + + useEffect(() => { + document.documentElement.style.setProperty("--sticky-top", `-25px`); + }, []); + + const files = watch("attachments"); + const onFileChange = async (e) => { + const newFiles = Array.from(e.target.files); + if (newFiles.length === 0) return; + + const existingFiles = Array.isArray(watch("attachments")) + ? watch("attachments") + : []; + + const parsedFiles = await Promise.all( + newFiles.map(async (file) => { + const base64Data = await toBase64(file); + return { + fileName: file.name, + base64Data, + contentType: file.type, + fileSize: file.size, + description: "", + isActive: true, + }; + }) + ); + + const combinedFiles = [ + ...existingFiles, + ...parsedFiles.filter( + (newFile) => + !existingFiles?.some( + (f) => + f.fileName === newFile.fileName && f.fileSize === newFile.fileSize + ) + ), + ]; + + setValue("attachments", combinedFiles, { + shouldDirty: true, + shouldValidate: true, + }); + }; + + const toBase64 = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result.split(",")[1]); + reader.onerror = (error) => reject(error); + }); + const removeFile = (index) => { + const newFiles = files.filter((_, i) => i !== index); + setValue("attachments", newFiles, { shouldValidate: true }); + }; + return ( +
    +
    +
    Add Comment
    + +
    +
    + + +
    + + + {errors?.comment && ( + + {errors?.comment?.message} + + )} +
    +
    +
    + {files?.length > 0 && ( + + )} +
    +
    +
    document.getElementById("attachments").click()} + className="cursor-pointer" + style={{ whiteSpace: "nowrap" }} + > + { + onFileChange(e); + e.target.value = ""; + }} + /> + + Add Attachment +
    + + +
    +
    +
    +
    +
    + {jobComments?.map((item) => { + const user = item?.createdBy; + + return ( +
    +
    + +
    +
    + + {user?.firstName} {user?.lastName} + + + {formatUTCToLocalTime(item?.createdAt, true)} + +
    +
    + {user?.jobRoleName} +
    +
    +

    {item.comment}

    +
    + {item.attachments?.map((file) => ( +
    + +
    +

    {file.fileName}

    + + {formatFileSize(file.fileSize)} + +
    +
    + ))} +
    +
    +
    +
    +
    + ); + })} +
    +
    +
    + ); +}; + +export default JobComments; diff --git a/src/components/ServiceProject/JobList.jsx b/src/components/ServiceProject/JobList.jsx new file mode 100644 index 00000000..8a7f1639 --- /dev/null +++ b/src/components/ServiceProject/JobList.jsx @@ -0,0 +1,178 @@ +import React, { useState } from "react"; +import { + daysLeft, + getJobStatusBadge, + getNextBadgeColor, +} from "../../utils/appUtils"; +import { useServiceProjectJobs } from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { SpinnerLoader } from "../common/Loader"; +import { useParams } from "react-router-dom"; +import ProjectPage from "../../pages/project/ProjectPage"; +import { useServiceProjectJobContext } from "./Jobs"; + +const JobList = () => { + const { setSelectedJob, setManageJob } = useServiceProjectJobContext(); + const { projectId } = useParams(); + const { data, isLoading, isError, error } = useServiceProjectJobs( + ITEMS_PER_PAGE, + 1, + true, + projectId + ); + + const jobGrid = [ + { + key: "jobTicketUId", + label: "Job Id", + getValue: (e) => {e?.jobTicketUId || "N/A"}, + align: "text-start", + }, + { + key: "title", + label: "Title", + getValue: (e) => ( + setSelectedJob({ showCanvas: true, job: e?.id })} + > + {e?.title} + + ), + isAlwaysVisible: true, + className: "text-start", + }, + + { + key: "dueDate", + label: "Due On", + getValue: (e) => formatUTCToLocalTime(e.startDate), + isAlwaysVisible: true, + className: "text-start d-none d-sm-table-cell", + }, + { + key: "status", + label: "Status", + getValue: (e) => { + return ( + + {e?.status?.displayName} + + ); + }, + isAlwaysVisible: true, + className: "text-start d-none d-sm-table-cell", + }, + { + key: "daysLeft", + label: "Days Left", + getValue: (e) => { + const { days, color } = daysLeft(e.startDate, e.dueDate); + + return ( + + {days !== null ? `${days} days` : "N/A"} + + ); + }, + isAlwaysVisible: true, + className: "text-start d-none d-sm-table-cell", + }, + ]; + + return ( +
    + + + + {jobGrid.map((col) => ( + + ))} + + + + + + {Array.isArray(data?.data) && data.data.length > 0 ? ( + data.data.map((row, i) => ( + + {jobGrid.map((col) => ( + + ))} + + + )) + ) : ( + + + + )} + +
    +
    {col.label}
    +
    + Actions +
    + setSelectedJob({ showCanvas: true, job: row?.id }) + } + > + {col.getValue(row)} + +
    + +
    + + + <> + + +
    +
    +
    + {isLoading ? : "Not Found Jobs."} +
    +
    + ); +}; + +export default JobList; diff --git a/src/components/ServiceProject/JobStatusLog.jsx b/src/components/ServiceProject/JobStatusLog.jsx new file mode 100644 index 00000000..88ff3562 --- /dev/null +++ b/src/components/ServiceProject/JobStatusLog.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import Avatar from "../common/Avatar"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; + +const JobStatusLog = ({ data }) => { + return ( +
    +
    +
    + {data?.map((item) => ( +
    +
    +
    + + {item.nextStatus?.displayName ?? + item.status?.displayName ?? + "Status"} + +
    + + + {formatUTCToLocalTime(item?.updatedAt,true)} + +
    +
    + +
    +
    + + {item.updatedBy?.firstName} {item.updatedBy?.lastName} + + + {/* {formatUTCToLocalTime(item?.createdAt, true)} */} + +
    +
    + {item?.updatedBy?.jobRoleName} +
    +
    +

    {item.comment}

    +
    +
    +
    +
    + ))} +
    +
    +
    + ); +}; + +export default JobStatusLog; diff --git a/src/components/ServiceProject/Jobs.jsx b/src/components/ServiceProject/Jobs.jsx new file mode 100644 index 00000000..d6339174 --- /dev/null +++ b/src/components/ServiceProject/Jobs.jsx @@ -0,0 +1,81 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; +import JobList from "./JobList"; +import { useNavigate } from "react-router-dom"; +import { useServiceProjects } from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import OffcanvasComponent from "../common/OffcanvasComponent"; +import showToast from "../../services/toastService"; +import ManageJob from "./ManageJob"; +import ManageJobTicket from "./ManageJobTicket"; +import GlobalModel from "../common/GlobalModel"; +import PreviewDocument from "../Expenses/PreviewDocument"; + +export const JonContext = createContext(); +export const useServiceProjectJobContext = () => { + const context = useContext(JonContext); + if (!context) { + showToast("Something went wrong", "warning"); + window.location = "/dashboard"; + } + return context; +}; +const Jobs = () => { + const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null }); + const [showCanvas, setShowCanvas] = useState(false); + const [selectedProject, setSelectedProject] = useState(null); + const [selectJob, setSelectedJob] = useState({ + showCanvas: false, + job: null, + }); + const navigate = useNavigate(); + const { data } = useServiceProjects(ITEMS_PER_PAGE, 1); + + const contextProvider = { + setSelectedJob, + setSelectedProject, + setManageJob, + manageJob, + }; + return ( + <> + + setSelectedJob({ showCanvas: false, job: null })} + > + + + setManageJob({ isOpen: false, jobId: null })} + > + + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    + + ); +}; + +export default Jobs; diff --git a/src/components/ServiceProject/ManageJob.jsx b/src/components/ServiceProject/ManageJob.jsx new file mode 100644 index 00000000..c0d9223e --- /dev/null +++ b/src/components/ServiceProject/ManageJob.jsx @@ -0,0 +1,271 @@ +import React, { useEffect, useState } from "react"; +import Breadcrumb from "../common/Breadcrumb"; +import Label from "../common/Label"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { defaultJobValue, jobSchema } from "./ServiceProjectSchema"; +import { + useCreateServiceProjectJob, + useJobTags, + useServiceProjectJobDetails, + useServiceProjects, + useUpdateServiceProjectJob, +} from "../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import DatePicker from "../common/DatePicker"; +import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag"; +import TagInput from "../common/TagInput"; +import { localToUtc } from "../../utils/appUtils"; +import SelectField from "../common/Forms/SelectField"; +import { + AppFormController, + AppFormProvider, + useAppForm, +} from "../../hooks/appHooks/useAppForm"; +import { useParams } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { useJobStatus } from "../../hooks/masterHook/useMaster"; +import { useServiceProjectJobContext } from "./Jobs"; + +const ManageJob = ({ Job }) => { + const { setManageJob, setSelectedJob } = useServiceProjectJobContext(); + const { projectId } = useParams(); + const dispatch = useDispatch(); + const methods = useAppForm({ + resolver: zodResolver(jobSchema), + defaultValues: defaultJobValue, + }); + const { + register, + control, + watch, + handleSubmit, + reset, + formState: { errors }, + } = methods; + + const { + data: JobTags, + isLoading: isTagLoading, + isError: isTagError, + error: tagError, + } = useJobTags(); + const { + data: JobData, + isLoading: isJobLoading, + isError: isJobError, + error: jobError, + } = useServiceProjectJobDetails(Job); + + const { data, isLoading, isError, error } = useJobStatus( + JobData?.status?.id, + projectId + ); + const { mutate: CreateJob, isPending } = useCreateServiceProjectJob(() => { + reset(); + }); + const { mutate: UpdateJob, isPending: Updating } = useUpdateServiceProjectJob( + () => { + setManageJob({ isOpen: false, jobId: null }); + setSelectedJob({ showCanvas: true, job: Job }); + } + ); + const onSubmit = (formData) => { + if (Job) { + const existingEmployeeIds = JobData?.assignees?.map((e) => e.id) || []; + + const oldEmployees = JobData.assignees.map((e) => ({ + employeeId: e.id, + isActive: formData.assignees.includes(e.id), + })); + + const newEmployees = formData.assignees + .filter((id) => !existingEmployeeIds.includes(id)) + .map((id) => ({ + employeeId: id, + isActive: true, + })); + + const updatedEmployees = [...oldEmployees, ...newEmployees]; + + const payload = [ + { + op: "replace", + path: "/title", + value: formData.title, + }, + { + op: "replace", + path: "/description", + value: formData.description, + }, + { + op: "replace", + path: "/startDate", + value: localToUtc(formData.startDate) ?? JobData.startDate, + }, + { + op: "replace", + path: "/dueDate", + value: localToUtc(formData.dueDate) ?? JobData.dueDate, + }, + { + op: "replace", + path: "/tags", + value: formData.tags, + }, + + { + op: "replace", + path: "/assignees", + value: updatedEmployees, + }, + { + op: "replace", + path: "/statusId", + value: formData.statusId, + }, + ]; + UpdateJob({ id: Job, payload }); + } else { + formData.assignees = formData.assignees.map((emp) => ({ + employeeId: emp, + isActive: true, + })); + + formData.startDate = localToUtc(formData.startDate); + formData.dueDate = localToUtc(formData.dueDate); + formData.projectId = projectId; + + CreateJob(formData); + } + }; + + useEffect(() => { + if (!JobData && !Job) { + reset({ + ...defaultJobValue, + projectId: projectId, + }); + return; + } + + if (!JobData || !Job) return; + + const assignedEmployees = (JobData.assignees || []).map((e) => e.id); + + reset({ + title: JobData.title ?? "", + description: JobData.description ?? "", + projectId: JobData.project?.id ?? projectId, + assignees: assignedEmployees, + startDate: JobData.startDate ?? null, + dueDate: JobData.dueDate ?? null, + tags: JobData.tags ?? [], + statusId: JobData.status.id, + }); + }, [JobData, Job, projectId]); + return ( +
    + +
    +
    + + +
    + +
    + + +
    +
    + + +
    +
    + + +
    + {Job && ( +
    + ( + + )} + /> + + {errors.statusId && ( + {errors.statusId.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 67f873a8..59ef4a7e 100644 --- a/src/pages/Home/SubscriptionPlans.jsx +++ b/src/pages/Home/SubscriptionPlans.jsx @@ -10,8 +10,6 @@ const SubscriptionPlans = () => { const { data, isLoading, isError, error } = useSubscription(frequency); const [loading, setLoading] = useState(false); - - const frequencyLabel = (freq) => { switch (freq) { case 0: @@ -58,7 +56,7 @@ const SubscriptionPlans = () => { - ) : data.length === 0 ? ( + ) : data?.length === 0 ? (
    No plans found
    ) : isError ? (
    @@ -104,13 +102,13 @@ const SubscriptionPlans = () => {
    Features
    -
      +
        {plan.features?.modules && Object.values(plan.features.modules).map((mod) => mod && mod.name ? (
      • {mod.enabled ? ( @@ -125,9 +123,15 @@ const SubscriptionPlans = () => { {/* Button */}
        + + Subscribe + Request a Demo 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 95629d87..017245e9 100644 --- a/src/pages/Organization/OrganizationPage.jsx +++ b/src/pages/Organization/OrganizationPage.jsx @@ -6,12 +6,15 @@ 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,27 +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..1d59f1f2 --- /dev/null +++ b/src/pages/ServiceProject/ServiceProjectDisplay.jsx @@ -0,0 +1,88 @@ +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"; + +const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => { + const [currentPage, setCurrentPage] = useState(1); + + const { manageServiceProject, setManageServiceProject } = useProjectContext(); + const { data, isLoading, isError, error } = useServiceProjects( + ITEMS_PER_PAGE, + currentPage + ); + 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 ? ( +

    List

    + ) : ( + filteredProjects?.map((project) => ( + + )) + + )} + +
    + +
    + + {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 b8a5398d..5dac4e62 100644 --- a/src/pages/authentication/TenantSelectionPage.jsx +++ b/src/pages/authentication/TenantSelectionPage.jsx @@ -2,7 +2,7 @@ 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); @@ -45,7 +45,7 @@ const TenantSelectionPage = () => { isPending || (data?.data?.length === 1 && pendingTenant !== null) ) { - return ; + return ; } if (!data?.data?.length) { diff --git a/src/pages/collections/CollectionPage.jsx b/src/pages/collections/CollectionPage.jsx index 298bde19..0dbe446c 100644 --- a/src/pages/collections/CollectionPage.jsx +++ b/src/pages/collections/CollectionPage.jsx @@ -101,7 +101,7 @@ const CollectionPage = () => { ) { return ( ); } @@ -109,18 +109,22 @@ const CollectionPage = () => {
    -
    -
    -
    +
    +
    + {/* Left side: Date Picker + Show Pending (stacked on mobile) */} +
    - + -
    -
    -
    + +
    { onChange={(e) => setShowPending(e.target.checked)} />
    -
    -
    - {" "} - setSearchText(e.target.value)} - placeholder="Search Collection" - className="form-control form-control-sm" - /> -
    + {/* Right side: Search + Add Button */} +
    + setSearchText(e.target.value)} + placeholder="Search Collection" + className="form-control form-control-sm w-auto" + /> + {(canCreate || isAdmin) && (
    @@ -468,12 +491,15 @@ const EmployeeList = () => { {loading && ( - -

    Loading...

    + +
    + +
    )} + {!loading && displayData?.length === 0 && (!searchText) ? ( @@ -629,6 +655,18 @@ const EmployeeList = () => { {" "} Manage Role + )} @@ -649,16 +687,16 @@ const EmployeeList = () => { ))} - +
    - {displayData?.length > 0 && ( - - )} + {displayData?.length > 0 && ( + + )}
    ) : (
    diff --git a/src/pages/project/ProjectDetails.jsx b/src/pages/project/ProjectDetails.jsx index fe22d913..926d880b 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"; @@ -66,7 +66,14 @@ const ProjectDetails = () => { }; if (projectLoading || permsLoading || !projects_Details) { - return ; + return ( +
    + +
    + ); } const renderContent = () => { diff --git a/src/pages/project/ProjectPage.jsx b/src/pages/project/ProjectPage.jsx index a85d6f58..d9557f48 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 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,26 @@ export const useProjectContext = () => { const ProjectPage = () => { const [manageProject, setMangeProject] = useState({ isOpen: false, - Project: null, + project: null, + }); + const [manageServiceProject, setManageServiceProject] = useState({ + isOpen: false, + project: 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 [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 +57,20 @@ const ProjectPage = () => { ); }; - const sortingProject = (projects) => { - if (!isLoading && Array.isArray(projects)) { - const grouped = {}; - - 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 contextDispatcher = { + setMangeProject, + setManageServiceProject, + manageProject, + manageServiceProject, }; - useEffect(() => { - if (!isLoading && data) { - sortingProject(data); - } - }, [data, isLoading, selectedStatuses]); + + const handleToggleProject = (value) => { + setCoreProjects(value); + sessionStorage.setItem("whichProjectDisplay", String(value)); + }; - if (isLoading) return
    - if (isError) return

    {error.message}

    return (
    @@ -109,10 +82,39 @@ const ProjectPage = () => { />
    -
    -
    -
    -
    +
    +
    + {/* LEFT SIDE — DATE TOGGLE BUTTONS */} +
    +
    + {/* Service Project Button */} + + {/* Organization Project Button */} + + +
    +
    + + + {/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */} +
    + + {/* Search */} +
    { />
    -
    + {/* Card/List Buttons */} +
    +
    -
    + {/* Dropdown Filter */} +
    +
      {PROJECT_STATUS.map(({ id, label }) => (
    • handleStatusChange(id)} @@ -176,57 +175,31 @@ const ProjectPage = () => { ))}
    -
    - {HasManageProject && ( -
    + {HasManageProject && ( -
    - )} + )} +
    - {/* Project Render here */} - {listView ? ( - - ) : ( - - )} - - {/* ------------------ */} - - {/* Project Manage UPdate or create */} - - {manageProject.isOpen && ( - setMangeProject({ isOpen: false, Project: null })} - > - setMangeProject({ isOpen: false, Project: null })} - /> - - )} + {coreProjects ? : }
    ); diff --git a/src/pages/project/ProjectsDisplay.jsx b/src/pages/project/ProjectsDisplay.jsx new file mode 100644 index 00000000..db3deaec --- /dev/null +++ b/src/pages/project/ProjectsDisplay.jsx @@ -0,0 +1,134 @@ +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"; + +const ProjectsDisplay = ({ + listView, + searchTerm, + selectedStatuses, + handleStatusChange, +}) => { + const [currentPage, setCurrentPage] = useState(1); + const { + manageProject, + manageServiceProject, + setMangeProject, + setManageServiceProject, + } = useProjectContext(); + + const [projectList, setProjectList] = useState([]); + + const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1); + + 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 ba68946e..dcc2c0dc 100644 --- a/src/repositories/AuthRepository.jsx +++ b/src/repositories/AuthRepository.jsx @@ -11,6 +11,9 @@ const AuthRepository = { 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), diff --git a/src/repositories/ColllectionRepository.jsx b/src/repositories/ColllectionRepository.jsx index 8eb512da..04db4369 100644 --- a/src/repositories/ColllectionRepository.jsx +++ b/src/repositories/ColllectionRepository.jsx @@ -4,26 +4,46 @@ 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) + 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}`; + // 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 (fromDate) params.push(`fromDate=${fromDate}`); - if (toDate) params.push(`toDate=${toDate}`); - if(projectId) params.push(`projectId=${projectId}`) + + 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("&")}`; + url += "?" + params.join("&"); } - return api.get(url); + + 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) + 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..da51038c 100644 --- a/src/repositories/EmployeeRepository.jsx +++ b/src/repositories/EmployeeRepository.jsx @@ -10,18 +10,20 @@ 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) => { + 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) - 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..5e30bf76 100644 --- a/src/repositories/ExpsenseRepository.jsx +++ b/src/repositories/ExpsenseRepository.jsx @@ -1,24 +1,78 @@ 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}`), + //#endregion + + +}; export default ExpenseRepository; diff --git a/src/repositories/MastersRepository.jsx b/src/repositories/MastersRepository.jsx index d197c11c..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,7 +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)), + "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), @@ -78,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), @@ -134,6 +142,19 @@ export const MasterRespository = { 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) + 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 06d33c58..86e59955 100644 --- a/src/repositories/ProjectRepository.jsx +++ b/src/repositories/ProjectRepository.jsx @@ -1,17 +1,24 @@ import { api } from "../utils/axiosClient"; const ProjectRepository = { - getProjectList: () => api.get("/api/project/list"), + getProjectList: (pageSize, pageNumber) => + api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}`), 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 +27,6 @@ const ProjectRepository = { return api.get(url); }, - getEmployeesByProject: (projectId) => api.get(`/api/Project/employees/get/${projectId}`), @@ -40,10 +46,13 @@ const ProjectRepository = { api.get(`/api/project/allocation-histery/${id}`), updateProjectsByEmployee: (id, data) => api.post(`/api/project/assign-projects/${id}`, data), - projectNameList: (provideAll) => api.get(`/api/project/list/basic?provideAll=${provideAll}`), + 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,7 +94,7 @@ const ProjectRepository = { api.get(`/api/Project/get/assigned/services/${projectId}`), getProjectAssignedOrganizations: (projectId) => api.get(`/api/Project/get/assigned/organization/${projectId}`), - getProjectAssignedOrganizationsName: (projectId) => + getProjectAssignedOrganizationsName: (projectId) => api.get(`/api/Project/get/assigned/organization/dropdown/${projectId}`), getEmployeeForTaskAssign: (projectId, serviceId, organizationId) => { diff --git a/src/repositories/ServiceProjectRepository.jsx b/src/repositories/ServiceProjectRepository.jsx new file mode 100644 index 00000000..94ef3c8e --- /dev/null +++ b/src/repositories/ServiceProjectRepository.jsx @@ -0,0 +1,38 @@ +import { api } from "../utils/axiosClient"; + +export const ServiceProjectRepository = { + CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data), + GetServiceProjects: (pageSize, pageNumber) => + api.get( + `/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}` + ), + 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} ` + ), + //#region Job + + CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data), + GetJobList: (pageSize, pageNumber, isActive, projectId) => + api.get( + `/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}` + ), + 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", + }), +}; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 51cc761e..762264ca 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -54,8 +54,13 @@ 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 DemoBOQGrid from "../services/pmsGrid/BasicTable"; - +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/ManageJob"; const router = createBrowserRouter( [ { @@ -77,6 +82,10 @@ const router = createBrowserRouter( { path: "/auth/switch/org", element: }, { path: "/help/docs", element: }, + { + path: "/auth/subscripe/:frequency/:planId", + element: , + }, { element: , errorElement: , @@ -88,6 +97,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: }, @@ -95,12 +107,18 @@ 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: "/collection", element: }, { path: "/masters", element: }, { path: "/tenants", 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..9b96b95a 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,24 @@ export function startSignalR(loggedUser) { ) { emitters.image_gallery(); } + + // --- Service Project ---------------- + + if (keyword === "Service_Project") { + queryClient.invalidateQueries(["serviceProjects"]); + queryClient.invalidateQueries(["serviceProject"]); + } + + 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 b718a3bc..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"; }; @@ -69,49 +87,6 @@ export const normalizeAllowedContentTypes = (allowedContentType) => { return allowedContentType.split(","); return []; }; - -/** - * Flexible number formatter for currency, numbers, or percentages. - * - * @param {number} amount - The value to format. - * @param {Object} options - Formatting options. - * @param {"currency"|"number"|"percent"} [options.type="number"] - Type of format. - * @param {string} [options.currency="INR"] - Currency code (only used when type="currency"). - * @param {string} [options.locale="en-US"] - Locale for formatting. - * @param {"short"|"long"|"standard"} [options.notation="compact"] - Display style for large numbers. - * @param {number} [options.minimumFractionDigits=0] - Minimum decimal places. - * @param {number} [options.maximumFractionDigits=2] - Maximum decimal places. - * @returns {string} Formatted number string. - */ -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 function localToUtc(dateString) { if (!dateString || typeof dateString !== "string") return null; @@ -150,3 +125,140 @@ export const formatCurrency = (amount, currency = "INR", locale = "en-US") => { 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, + }; +} + + diff --git a/src/utils/axiosClient.jsx b/src/utils/axiosClient.jsx index 0f2f14a2..40981633 100644 --- a/src/utils/axiosClient.jsx +++ b/src/utils/axiosClient.jsx @@ -178,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 22061add..2375b010 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -1,12 +1,13 @@ +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; export const OTP_EXPIRY_SECONDS = 300; // OTP time -export const BASE_URL = process.env.VITE_BASE_URL; - -// export const BASE_URL = "https://api.marcoaiot.com"; - export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323"; export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"; @@ -49,7 +50,7 @@ export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda"; export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5"; export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb"; -// ========================Finance========================================================= + // -----------------------Expense---------------------------------------- export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116"; @@ -63,7 +64,9 @@ 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"; + + // --------------------------------Collection---------------------------- @@ -73,15 +76,6 @@ 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"; -// ========================================================================================== - -export const EXPENSE_REJECTEDBY = [ - "d1ee5eec-24b6-4364-8673-a8f859c60729", - "965eda62-7907-4963-b4a1-657fb0b2724b", -]; - -export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8"; - // ----------------------------Tenant------------------------- export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b"; export const MANAGE_TENANTS = "00e20637-ce8d-4417-bec4-9b31b5e65092"; @@ -97,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" }, @@ -155,12 +156,58 @@ export const PROJECT_STATUS = [ }, ]; -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", - process_pending:"61578360-3a49-4c34-8604-7b35a3787b95" +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 FREQUENCY_FOR_RECURRING = { + 0: "Monthly", + 1: "Quarterly", + 2: "Half-Yearly", + 3: "Yearly", + 4: "Daily", + 5: "Weekly" +}; + +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 \ No newline at end of file