{"version":3,"file":"index-DkHGYlXF.js","sources":["../../src/config/config.ts","../../src/hooks/usePageTracking.ts","../../src/utilities/handleLocalStorage.ts","../../src/utilities/handleErrorMessage.ts","../../src/api/tokenRefresher.ts","../../src/api/constants.ts","../../src/api/errorHandling.ts","../../src/api/fetchHandler.ts","../../src/utilities/strings/handleStringCapitalization.ts","../../src/utilities/strings/handleExtractFileExtension.ts","../../src/hooks/useErrorReporting.ts","../../src/components/Loader/Loader.tsx","../../src/components/Button/Button.tsx","../../src/components/Form/FormInput.tsx","../../src/utilities/strings/handleCheckStringForHTML.ts","../../src/schemas/constants.ts","../../src/schemas/regexes.ts","../../src/schemas/AuthenticationSchemas.ts","../../src/pages/Authentication/ForgotPassword.tsx","../../src/hooks/useExtractSearchParameters.ts","../../src/components/Form/FormCheckbox.tsx","../../src/components/Modal/ModalBackdrop.tsx","../../src/components/Modal/ModalInactiveUser.tsx","../../src/components/Form/FormPasswordInput.tsx","../../src/pages/Authentication/Login.tsx","../../src/pages/Authentication/ResetPassword.tsx","../../src/pages/Authentication/Autologin.tsx","../../src/assets/images/fch-full-logo.png","../../src/layout/LayoutUnauthenticated.tsx","../../src/routes/authentication.tsx","../../src/components/Content/ContentHeader.tsx","../../src/hooks/usePreventBodyScroll.ts","../../src/hooks/useOnEscapeKey.ts","../../src/components/Modal/Modal.tsx","../../src/components/Card/Card.tsx","../../src/components/SkeletonPlaceholders/TableSkeletonPlaceholder.tsx","../../src/hooks/useWindowResize.ts","../../src/components/Pagination/Pagination.tsx","../../src/components/Table/TableNoDataMessage.tsx","../../src/components/Table/Table.tsx","../../src/utilities/handlePermissionCheck.ts","../../src/components/Wrappers/PermissionCheckComponentWrapper.tsx","../../src/api/Users/Users.ts","../../src/api/Invitation/Invitation.ts","../../src/utilities/strings/handleFullnameCombination.ts","../../src/pages/Account/Users/UsersTable.tsx","../../src/hooks/useDebounce.ts","../../src/components/Inputs/InputFieldSearch.tsx","../../src/pages/Account/Users/Users.tsx","../../src/schemas/UserSchemas.ts","../../src/hooks/useBackNavigation.ts","../../src/pages/Account/ProfileSettings.tsx","../../src/api/Tours/Tours.ts","../../src/components/ProductTour/constants.tsx","../../src/providers/tour-context.tsx","../../src/hooks/useOnClickOutside.ts","../../src/components/CustomScrollbars/CustomScrollbars.tsx","../../src/constants/framer.ts","../../src/components/Dropdown/Dropdown.tsx","../../src/components/Form/FormDropdown.tsx","../../src/assets/images/icons/appointment-delete-icon.svg?react","../../src/components/Form/FormInputBubbles.tsx","../../src/interfaces/global.ts","../../src/schemas/AccountSchemas.ts","../../src/api/Groups/Groups.ts","../../src/pages/Account/Users/UsersNew.tsx","../../src/components/Form/FormInputSideLabel.tsx","../../src/pages/Account/Users/UsersEdit.tsx","../../src/pages/Account/statics.ts","../../src/pages/Account/Resources.tsx","../../src/api/Alerts/Alerts.ts","../../src/utilities/dates/handleDateAsTimestamp.ts","../../src/pages/Account/ManageAlerts.tsx","../../src/api/VideoConferencing/VideoConferencing.ts","../../src/pages/Account/VideoConferencing.tsx","../../src/api/Company/AdminCompany.ts","../../src/components/Tooltip/Tooltip.tsx","../../src/components/Dropdown/DropdownMultiselect.tsx","../../src/api/Users/AdminUsers.ts","../../src/pages/Account/Users/Admin/AdminUsersTable.tsx","../../src/pages/Account/Users/Admin/AdminUsers.tsx","../../src/components/Form/FormDropdownMultiselect.tsx","../../src/hooks/useScrollToActiveElement.ts","../../src/components/Dropdown/DropdownSearchable.tsx","../../src/components/Form/FormDropdownSearchable.tsx","../../src/api/Roles/AdminRoles.ts","../../src/pages/Account/Users/Admin/hooks/useAdminUsersCompanies.ts","../../src/api/Privileges/Privileges.ts","../../src/pages/Account/Users/Admin/AdminUsersNew.tsx","../../src/pages/Account/Users/Admin/AdminUsersEdit.tsx","../../src/assets/images/fch-logo.png","../../src/assets/images/icons/applications-icon.svg?react","../../src/assets/images/icons/appointments-icon.svg?react","../../src/assets/images/icons/communication-icon.svg?react","../../src/assets/images/icons/clients-icon.svg?react","../../src/assets/images/icons/messages-icon.svg?react","../../src/assets/images/icons/ad-manager-icon.svg?react","../../src/assets/images/icons/privileges-icon.svg?react","../../src/assets/images/icons/reports-icon.svg?react","../../src/assets/images/icons/job-board-icon.svg?react","../../src/assets/images/icons/send-sbca-request-icon.svg?react","../../src/assets/images/icons/email-templates-icon.svg?react","../../src/assets/images/icons/video-conference-icon.svg?react","../../src/assets/images/icons/resources-icon.svg?react","../../src/components/Header/statics.tsx","../../src/components/Header/HeaderDropdownGroup.tsx","../../src/components/Header/HeaderDropdownAccount.tsx","../../src/components/Header/HeaderDropdownAdminMenu.tsx","../../src/assets/images/icons/briefcase.svg?react","../../src/components/Header/HeaderDropdownWebsite.tsx","../../src/assets/images/icons/bell.svg?react","../../src/api/Articles/Articles.ts","../../src/utilities/dates/handleDistanceToNow.ts","../../src/components/Articles/ArticleNotification.tsx","../../src/components/Articles/ArticlesNotificationsSkeleton.tsx","../../src/components/Articles/ArticlesNotificationsMenu.tsx","../../src/api/Company/Company.ts","../../src/components/Header/utils/handleRedirectOnActiveCompanyChange.ts","../../src/components/Header/HeaderCompaniesMenu.tsx","../../src/components/Dropdown/DropdownActions.tsx","../../src/components/Chat/ChatMessagesSort.tsx","../../src/api/Chat/Chat.ts","../../src/components/Chat/ChatActions.tsx","../../src/utilities/strings/handleUserInitials.ts","../../src/components/Chat/ChatAvatar.tsx","../../src/components/Chat/ChatMessageListItem.tsx","../../src/components/Chat/Skeleton/SkeletonChatMesageListItem.tsx","../../src/assets/images/icons/sms-icon.svg?react","../../src/components/Chat/constants.ts","../../src/utilities/data/handleMoveArrayItem.ts","../../src/hooks/useVisualViewportResize.ts","../../src/assets/images/icons/chat-panel-minimize-icon.svg?react","../../src/components/Chat/Conversation/ChatConversationMessage.tsx","../../src/components/Chat/Conversation/ChatConversationMessageGroup.tsx","../../src/utilities/numbers/handleCalculatePercentage.ts","../../src/components/Chat/utils/handleChatMessageProgress.ts","../../src/assets/images/icons/chat-send-icon.svg?react","../../src/components/Chat/Conversation/ChatConversationForm.tsx","../../src/components/Chat/Conversation/ChatConversationMessageSkeleton.tsx","../../src/config/chat-socket.ts","../../src/components/Chat/Panel/ChatPanelConversation.tsx","../../src/components/Chat/Panel/ChatPanel.tsx","../../src/components/Chat/ChatBubble.tsx","../../src/components/Chat/ChatBubbleExtrasItem.tsx","../../src/components/Chat/ChatBubbleExtras.tsx","../../src/components/Chat/ChatWrapper/ChatContextWrapper.tsx","../../src/components/Chat/ChatMessagesDropdown.tsx","../../src/assets/images/icons/header-need-help-icon.svg?react","../../src/components/Header/HeaderDropdownHelp.tsx","../../src/assets/images/icons/career-pages-icon.svg?react","../../src/components/Header/HeaderCareerPages.tsx","../../src/components/Header/Header.tsx","../../src/assets/images/fch-full-logo-white.png","../../src/components/Footer/Footer.tsx","../../src/api/Marketing/Banners.ts","../../src/hooks/useInterval.ts","../../src/components/Banner/MarketingBanner.tsx","../../src/layout/LayoutAuthenticated.tsx","../../src/pages/UserInvitation/UserInvitationAccept.tsx","../../src/templates/emails/initial-interview-request.ts","../../src/templates/emails/job-offer-email-cover-note.ts","../../src/templates/emails/rejection-email-no-interview.ts","../../src/templates/emails/rejection-email-post-interview.ts","../../src/templates/emails/second-interview-request.ts","../../src/templates/emails/index.ts","../../src/pages/Account/EmailTemplates.tsx","../../src/routes/account.tsx","../../src/hooks/useDefaultViewSelection.ts","../../src/components/Applications/hooks/useApplicationsPositionFiltering.ts","../../src/components/Applications/hooks/useApplicationsSorting.ts","../../src/components/Applications/utils/handleApplicationsWithResume.ts","../../src/api/Applications/Applications.ts","../../src/utilities/strings/handleApplicantCityAndState.ts","../../src/templates/spreadsheet/DefaultReportCSVTemplate.ts","../../src/templates/spreadsheet/DefaultReportExcelTemplate.ts","../../src/templates/spreadsheet/SummaryReportExcelTemplate.ts","../../src/utilities/data/handleInitiateSpreadsheetGenerator.ts","../../src/utilities/strings/handlePhoneStringSanitization.ts","../../src/utilities/strings/handleCSVExportPhoneFormatting.ts","../../src/components/Applications/utils/handleExportSelectedCSV.ts","../../src/components/Applications/Buckets/useFindTargetedBucket.ts","../../src/utilities/data/handleDataPagination.ts","../../src/api/Buckets/Buckets.ts","../../src/components/Buckets/hooks/useVisibleBuckets.ts","../../src/components/Buckets/BucketItem.tsx","../../src/schemas/BucketsSchemas.ts","../../src/components/Buckets/BucketsCreateNewModal.tsx","../../src/components/Buckets/BucketsDropdown.tsx","../../src/components/Buckets/BucketsLoadingPlaceholder.tsx","../../src/components/Buckets/BucketsRefetch.tsx","../../src/components/Buckets/BucketsDeleteModal.tsx","../../src/components/Buckets/BucketsEditCustomBucketModal.tsx","../../src/components/Buckets/BucketsDraggableItem.tsx","../../src/components/Buckets/BucketsDragOverlayItem.tsx","../../src/components/Banner/Banner.tsx","../../src/components/Buckets/BucketsEditModal.tsx","../../src/components/ProductTour/ProductTourTooltip.tsx","../../src/components/ProductTour/ProductTourWrapper.tsx","../../src/components/ProductTour/useShouldShowTour.ts","../../src/components/Buckets/Buckets.tsx","../../src/components/Applications/hooks/useApplicationResumeModal.ts","../../src/assets/images/icons/applications-calendar-icon.svg?react","../../src/assets/images/icons/applications-comment-icon.svg?react","../../src/components/Applications/ApplicationAppointmentsAndComments.tsx","../../src/components/Applications/utils/handleConvertSBCARatingToClassname.ts","../../src/components/Form/FormTextarea.tsx","../../src/schemas/ApplicationsSchemas.ts","../../src/components/Applications/modals/ApplicationModalSuccessfulRequest.tsx","../../src/components/Applications/modals/ApplicationModalSBCA.tsx","../../src/assets/images/icons/assessment-ai.svg?react","../../src/components/Applications/ApplicationSBCA.tsx","../../src/components/Datepicker/Datepicker.tsx","../../src/components/Applications/ApplicationDateSection.tsx","../../src/components/Applications/ApplicationCardHeader.tsx","../../src/components/Applications/ApplicationContact.tsx","../../src/components/Applications/ApplicationDetails.tsx","../../src/components/Applications/ApplicationPhoto.tsx","../../src/components/Applications/ApplicationRating.tsx","../../src/components/Applications/Socials/ApplicationSocials.tsx","../../src/components/Applications/ApplicationSourceAdmin.tsx","../../src/components/Applications/ApplicationSourceUser.tsx","../../src/assets/images/icons/applications-archive-icon.svg?react","../../src/components/Applications/Buckets/ApplicationArchive.tsx","../../src/assets/images/icons/applications-favorite-icon.svg?react","../../src/components/Applications/Buckets/ApplicationFavorite.tsx","../../src/assets/images/icons/applications-message-icon.svg?react","../../src/components/Applications/Socials/ApplicationSendText.tsx","../../src/assets/images/icons/applications-resume-icon.svg?react","../../src/components/Applications/Socials/ApplicationResume.tsx","../../src/components/Applications/modals/ApplicationModalResumeRequest.tsx","../../src/assets/images/icons/applications-linkedin-icon.svg?react","../../src/components/Applications/Socials/ApplicationLinkedIn.tsx","../../src/assets/images/icons/applications-camera-icon.svg?react","../../src/components/Applications/Socials/ApplicationScheduleMeeting.tsx","../../src/api/Appointments/Appointments.ts","../../src/components/Applications/ScheduleMeeting/InPersonAppointment.tsx","../../src/components/Applications/ScheduleMeeting/MeetNow.tsx","../../src/api/Timezones/Timezones.ts","../../src/components/Applications/ScheduleMeeting/ScheduleVideoMeeting.tsx","../../src/components/Inputs/RadioButton.tsx","../../src/components/Applications/ScheduleMeeting/VideoConferecing.tsx","../../src/assets/images/icons/applications-in-person-appointment-icon.svg?react","../../src/components/Applications/ScheduleMeeting/ScheduleMeetingMenu.tsx","../../src/assets/images/icons/applications-email-icon.svg?react","../../src/utilities/strings/handleEncodeEmailTemplate.ts","../../src/components/Alert/Alert.tsx","../../src/components/Applications/modals/ApplicationEmailTemplatesModal.tsx","../../src/components/Applications/Socials/ApplicationSendEmail.tsx","../../src/components/Applications/ApplicationCardList.tsx","../../src/components/Applications/ApplicationCardGrid.tsx","../../src/components/Inputs/Checkbox.tsx","../../src/components/Applications/Buckets/orderBuckets.ts","../../src/components/Applications/Buckets/filterGroupedApplications.ts","../../src/components/Applications/Buckets/ApplicationBulkBucketMovement.tsx","../../src/components/Applications/statics.tsx","../../src/components/Applications/ApplicationFilters.tsx","../../src/components/Loader/LoaderRefetch.tsx","../../src/components/Applications/Skeletons/ApplicationCardGridSkeletons.tsx","../../src/components/Applications/Skeletons/ApplicationCardListSkeletons.tsx","../../src/assets/images/icons/list-view.svg?react","../../src/assets/images/icons/grid-view.svg?react","../../src/components/ViewActions/ViewActions.tsx","../../src/components/Applications/ApplicationsSort.tsx","../../src/components/Applications/ApplicationsSelectionIndicator.tsx","../../src/api/fileDownloadHandler.ts","../../src/assets/images/icons/info-icon.svg?react","../../src/components/Applications/modals/ApplicationModalResumePreview.tsx","../../src/components/Applications/ApplicationsHideSelection.tsx","../../src/components/ProductTour/utilities.ts","../../src/components/ProductTour/tours-data/handler.ts","../../src/components/Toggle/Toggle.tsx","../../src/pages/Applications/Applications.tsx","../../src/assets/images/icons/applications-email-application-icon.svg?react","../../src/assets/images/icons/applications-print-application-icon.svg?react","../../src/assets/images/icons/applications-refresh-page-icon.svg?react","../../src/components/Applications/Buckets/ApplicationBucketMovement.tsx","../../src/assets/images/icons/appointment-in-person-icon.svg?react","../../src/components/Appointments/utils/handleAppointmentDisplayedTime.ts","../../src/components/Appointments/modals/EditInPersonAppointment.tsx","../../src/components/Appointments/modals/EditVideoMeetingAppointment.tsx","../../src/components/Appointments/modals/DeleteAppointment.tsx","../../src/assets/images/icons/appointment-calendar-icon.svg?react","../../src/assets/images/icons/edit-icon.svg?react","../../src/utilities/data/handleDownloadBlob.ts","../../src/components/Appointments/AppointmentsActions.tsx","../../src/components/Appointments/AppointmentsListItem.tsx","../../src/api/Applications/Comments/Comments.ts","../../src/components/Applications/ApplicationComment.tsx","../../src/components/Applications/ApplicationCommentMenu.tsx","../../src/pages/Applications/Skeleton/ApplicationDetailsSkeleton.tsx","../../src/components/Applications/modals/ApplicationEmailModal.tsx","../../src/components/Applications/ApplicationVerifiedFirst.tsx","../../src/components/Accordion/Accordion.tsx","../../src/features/Applications/ApplicationExtraInformation.tsx","../../src/components/Modal/ModalApplicationNotInCompany.tsx","../../src/features/Applications/history-log.ts","../../src/pages/ErrorBoundary/ComponentErrorBoundary.tsx","../../src/features/Applications/ApplicationHistory.tsx","../../src/components/Applications/ApplicationsDetailsAdminActions.tsx","../../src/hooks/useIsInViewport.ts","../../src/assets/images/icons/sbca-caregiver-icon.svg?react","../../src/assets/images/icons/sbca-thinker-icon.svg?react","../../src/assets/images/icons/sbca-processor-icon.svg?react","../../src/components/Progress/ProgressCircle.tsx","../../src/components/Applications/ApplicationSBCAResult.tsx","../../src/components/Markdown/Markdown.tsx","../../src/components/Applications/ApplicationSBCADetails.tsx","../../src/features/Applications/ApplicationGeneratedInterviewQuestions.tsx","../../src/features/Applications/ApplicantSummary.tsx","../../src/api/fetchHandlerUpload.ts","../../src/api/HRCompliantApplication/HRCompliantApplication.ts","../../src/assets/images/icons/request-custom-application-inactive-icon.svg?react","../../src/components/Applications/ApplicationSendHRCompliantForm.tsx","../../src/pages/Applications/ApplicationsDetails.tsx","../../src/assets/images/fch-full-logo.svg?react","../../src/pages/Applications/ApplicationPrint.tsx","../../src/routes/applications.tsx","../../src/components/Appointments/modals/AppointmentResumePreview.tsx","../../src/components/Appointments/AppointmentsCard.tsx","../../src/components/Appointments/skeletons/AppointmentsCardSkeleton.tsx","../../src/pages/Appointments/Appointments.tsx","../../src/routes/appointments.tsx","../../src/components/Articles/ArticlesOthers.tsx","../../src/pages/Articles/Skeleton/ArticleDetails.tsx","../../src/components/Error/ErrorNotFound.tsx","../../src/pages/Articles/ArticleDetails.tsx","../../src/components/Articles/ArticleCardGrid.tsx","../../src/components/Articles/ArticleCardList.tsx","../../src/pages/Articles/Skeleton/Articles.tsx","../../src/pages/Articles/Articles.tsx","../../src/utilities/handleFormDataFields.ts","../../src/pages/Articles/constants.ts","../../src/api/Articles/AdminArticles.ts","../../src/utilities/handleCheckIfChromeAndroid.ts","../../src/components/Form/FormUpload.tsx","../../src/components/WYSIWYG/custom-plugins/customImagePlugin.ts","../../src/components/WYSIWYG/utils/customImageUpload.ts","../../src/components/WYSIWYG/utils/handleLinkTargetOnLinkPaste.ts","../../src/components/WYSIWYG/TextEditor.tsx","../../src/pages/Articles/Skeleton/ArticlesManagement.tsx","../../src/schemas/ArticlesSchemas.ts","../../src/pages/Articles/ArticlesEdit.tsx","../../src/components/Tabs/Tabs.tsx","../../src/components/Articles/ArticlesOverviewTable.tsx","../../src/pages/Articles/Skeleton/ArticlesOverview.tsx","../../src/pages/Articles/ArticlesOverview.tsx","../../src/routes/articles.tsx","../../src/api/Assessment/Assessment.ts","../../src/components/Assessment/AssessmentData/AssessmentScoreSummary.tsx","../../src/assets/images/icons/assessment-warning.svg?react","../../src/components/Assessment/AssessmentData/AssessmentWarning.tsx","../../src/assets/images/icons/assessment-user.svg?react","../../src/assets/images/icons/assessment-users.svg?react","../../src/assets/images/icons/assessment-briefcase.svg?react","../../src/components/Assessment/AssessmentData/AssessmentCommentary.tsx","../../src/components/Assessment/AssessmentSection.tsx","../../src/components/Progress/ProgressBar.tsx","../../src/components/Assessment/AssessmentProgressBox.tsx","../../src/components/Assessment/AssessmentData/AssessmentProgressBarCard.tsx","../../src/components/Assessment/AssessmentCard.tsx","../../src/components/Assessment/AssessmentData/AssessmentProgressBarDrivenCard.tsx","../../src/components/Assessment/AssessmentData/statics.ts","../../src/assets/images/icons/sbca-report-check.svg?react","../../src/components/Assessment/AssessmentData/AssessmentProgressBarCircleCard.tsx","../../src/components/Assessment/AssessmentData/AssessmentProgressBarSummaryCard.tsx","../../src/components/Assessment/Modal/AssessmentModalSuccessfulRequest.tsx","../../src/components/Assessment/Modal/AssessmentEmailModal.tsx","../../src/assets/images/icons/assessment-mail.svg?react","../../src/assets/images/icons/assessment-print.svg?react","../../src/assets/images/icons/assessment-company.svg?react","../../src/assets/images/icons/assessment-tag.svg?react","../../src/assets/images/icons/assessment-report.svg?react","../../src/assets/images/icons/assessment-submitted.svg?react","../../src/components/Assessment/AssessmentHeader.tsx","../../src/pages/Assessment/Skeleton/AssessmentResultsSkeleton.tsx","../../src/components/Assessment/AssessmentData/AssessmentAISummary.tsx","../../src/pages/Assessment/AssessmentResults.tsx","../../src/pages/Assessment/AssessmentResultsPrint.tsx","../../src/layout/LayoutAssessment.tsx","../../src/components/Form/FormPhoneInput.tsx","../../src/api/SBCA/SbcaTypes.ts","../../src/pages/Assessment/AssessmentRequest.tsx","../../src/routes/assessment.tsx","../../src/api/CareerPages/CareerPages.ts","../../src/pages/Career/CareerPageGraphics.tsx","../../src/components/CareerPages/Form/GeneralSection.tsx","../../src/pages/Career/statics.ts","../../src/components/CareerPages/Form/HighlightsSection.tsx","../../src/components/CareerPages/Form/FeaturedHighlightsSection.tsx","../../src/components/CareerPages/Form/BenefitsSection.tsx","../../src/pages/Career/CareerPagesSkeleton.tsx","../../src/schemas/CareerPages.ts","../../src/pages/Career/utils/handleCareerPagesOrdinalityDataSorting.ts","../../src/pages/Career/utils/handleCareerPagesFilteredHighlightsData.ts","../../src/utilities/forms/formik.ts","../../src/pages/Career/CareerPages.tsx","../../src/routes/career.tsx","../../src/components/Chat/Conversation/ChatConversation.tsx","../../src/pages/Chats/Chats.tsx","../../src/routes/chat.tsx","../../src/api/Clients/AdminClients.ts","../../src/utilities/strings/handleStripProtocolFromLinks.ts","../../src/pages/Admin/Clients/AdminClientsTable.tsx","../../src/pages/Admin/Clients/AdminClients.tsx","../../src/schemas/ClientsSchemas.ts","../../src/pages/Admin/Clients/statics.ts","../../src/constants/countries.ts","../../src/pages/Admin/Clients/AdminClientsNew.tsx","../../src/pages/Admin/Clients/AdminClientsEdit.tsx","../../src/routes/clients.tsx","../../src/api/JobAds/JobAds.ts","../../src/pages/JobAds/constants.ts","../../src/components/JobAds/JobAdsTable.tsx","../../src/components/JobAds/JobAdsLatestCard.tsx","../../src/components/JobAds/JobAdHeader.tsx","../../src/pages/JobAds/Skeleton/JobAdsOverviewSkeleton.tsx","../../src/components/JobAds/JobAdsClientsTable.tsx","../../src/components/JobAds/JobAdsOverviewSearch.tsx","../../src/pages/JobAds/JobAdsOverview.tsx","../../src/pages/JobAds/JobAdsListAds.tsx","../../src/api/JobAds/JobAdsClients.ts","../../src/pages/JobAds/Clients/JobAdsClients.tsx","../../src/utilities/strings/handleStripStringFromLastCharacter.ts","../../src/assets/images/job-ads-client-default-logo.png","../../src/pages/JobAds/Clients/JobAdsClientDetails.tsx","../../src/components/JobAds/Details/JobAdStatusUpdateActions.tsx","../../src/pages/JobAds/Skeleton/JobAdDetailsSkeleton.tsx","../../src/components/JobAds/Details/JobAdInformation.tsx","../../src/api/JobAds/JobAdsComments.ts","../../src/components/JobAds/Details/JobBoardsForms/constants.ts","../../src/schemas/JobAdSchemas.ts","../../src/components/JobAds/Details/JobAdComments.tsx","../../src/components/JobAds/Details/JobAdLivePreview.tsx","../../src/components/JobAds/Details/JobAdLinks.tsx","../../src/components/JobAds/Details/JobAdHistory.tsx","../../src/components/JobAds/Details/JobAdSettings.tsx","../../src/components/JobAds/Details/JobBoardActions.tsx","../../src/components/JobAds/Details/JobBoardPostingHistory.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/AppcastDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/AppcastForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/LinkedInDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/LinkedInForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/SnagAJobDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/SnagAJobForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/CraigslistForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/IndeedDropdown.ts","../../src/components/Form/FormInputWithElements.tsx","../../src/components/JobAds/Details/JobBoardsForms/IndeedForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/ZipRecruiterDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/ScreeningQuestions.ts","../../src/components/JobAds/Details/JobBoardsForms/ZipRecruiterForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/CareerBuilderDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/CareerBuilderForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/TalentDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/TalentForm.tsx","../../src/components/JobAds/Details/JobBoardsForms/dropdown-data/FirstChoiceDropdowns.ts","../../src/components/JobAds/Details/JobBoardsForms/FirstChoiceForm.tsx","../../src/components/JobAds/Details/JobBoards.tsx","../../src/components/JobAds/Details/JobAdCraigslistEmbed.tsx","../../src/pages/JobAds/JobAdDetails.tsx","../../src/api/JobAds/JobAdsLibraries.ts","../../src/components/JobAds/Edit/JobAdAddress.tsx","../../src/components/WYSIWYG/utils/extractTextEditorToolbarButtonNames.ts","../../src/components/JobAds/Edit/generate-ad-template.ts","../../src/components/JobAds/Edit/JobAdEditor.tsx","../../src/components/JobAds/Edit/JobAdFormSettings.tsx","../../src/components/JobAds/Edit/JobAdInformation.tsx","../../src/components/JobAds/Edit/JobAdLibrary.tsx","../../src/pages/JobAds/Skeleton/JobAdsEditSkeleton.tsx","../../src/components/JobAds/Edit/JobAdAiTemplate.tsx","../../src/pages/JobAds/JobAdsEdit.tsx","../../src/pages/JobAds/JobAdPreview.tsx","../../src/routes/job-ads.tsx","../../src/api/Logs/Logs.ts","../../src/pages/Logs/SMSLogDetails.tsx","../../src/pages/Logs/EmailLogDetails.tsx","../../src/pages/Logs/EmailLogs.tsx","../../src/pages/Logs/SMSLogs.tsx","../../src/pages/Logs/Logs.tsx","../../src/routes/logs.tsx","../../src/pages/Privileges/Skeletons/PrivilegesSkeleton.tsx","../../src/pages/Privileges/Privileges.tsx","../../src/pages/Privileges/utils.ts","../../src/pages/Privileges/Skeletons/PrivilegesCrudSkeleton.tsx","../../src/schemas/PrivilegesSchemas.ts","../../src/pages/Privileges/PrivilegesCrud.tsx","../../src/pages/Privileges/PrivilegesGroups.tsx","../../src/pages/Privileges/Skeletons/PrivilegesUserSkeleton.tsx","../../src/pages/Privileges/PrivilegesUserUpdates.tsx","../../src/routes/privileges.tsx","../../src/api/Reports/Reports.ts","../../src/utilities/data/handleInitiatePDFWorker.ts","../../src/pages/Reports/hooks/useReportDateRangeHandling.ts","../../src/pages/Reports/constants.ts","../../src/pages/Reports/AdActivation.tsx","../../src/components/Charts/PieChart.tsx","../../src/components/Charts/constants.ts","../../src/pages/Reports/AdPerformance.tsx","../../src/pages/Reports/ApplicantCount.tsx","../../src/pages/Reports/ExpiringAds.tsx","../../src/pages/Reports/Hiring.tsx","../../src/pages/Reports/IndeedSponsored.tsx","../../src/utilities/strings/handleDateRangeString.ts","../../src/pages/Reports/InternalHiring.tsx","../../src/pages/Reports/JobBoardStatus.tsx","../../src/pages/Reports/ZipRecruiterSponsored.tsx","../../src/pages/Reports/Reports.tsx","../../src/routes/reports.tsx","../../src/api/JobPositions/JobPositions.ts","../../src/features/JobPositions/components/PositionsSelectionIndicator.tsx","../../src/schemas/JobPositionSchemas.ts","../../src/features/JobPositions/CompanyPositions.tsx","../../src/features/JobPositions/DefaultPositions.tsx","../../src/pages/JobPositions/JobPositions.tsx","../../src/routes/job-positions.tsx","../../src/pages/ErrorPages/ForbiddenAccess.tsx","../../src/pages/ErrorPages/NotFound.tsx","../../src/routes/error-pages.tsx","../../src/api/OnlineApplication/OnlineApplication.tsx","../../src/assets/images/icons/phone-icon.svg?react","../../src/assets/images/icons/linkedin-icon.svg?react","../../src/components/Form/FormDatetimePicker.tsx","../../src/pages/OnlineApplication/form-builder/constants.tsx","../../src/schemas/OnlineApplicationSchemas.ts","../../src/pages/OnlineApplication/Assessment/handleGenerateAssessmentQuestions.ts","../../src/components/OnlineApplications/OnlineApplicationColorPicker.tsx","../../src/components/Form/FormInputSlider.tsx","../../src/components/OnlineApplications/OnlineApplicationAssessmentGroupQuestion.tsx","../../src/components/OnlineApplications/OnlineApplicationAssessmentQuestion.tsx","../../src/components/OnlineApplications/OnlnieApplicationFooter.tsx","../../src/internationalization/locales/en/pages/index.ts","../../src/internationalization/locales/es/pages/index.ts","../../src/internationalization/i18n.ts","../../src/pages/OnlineApplication/Assessment/constants.ts","../../src/assets/images/comodo_secure_white.png","../../src/assets/images/icons/flag-us-icon.svg?react","../../src/assets/images/icons/flag-es-icon.svg?react","../../src/pages/OnlineApplication/Assessment/OnlineApplicationAssessment.tsx","../../src/pages/OnlineApplication/Assessment/OnlineApplicationAssessmentThankYou.tsx","../../src/components/OnlineApplications/TermsOfService.tsx","../../src/pages/OnlineApplication/form-builder/handleFormSelectFieldOptions.ts","../../src/pages/OnlineApplication/form-builder/handleFormFieldComponentProps.tsx","../../src/pages/OnlineApplication/form-builder/handleFormFieldSizes.ts","../../src/pages/OnlineApplication/form-builder/handleAllowedDates.ts","../../src/pages/OnlineApplication/interfaces.ts","../../src/pages/OnlineApplication/form-builder/utilities.ts","../../src/components/OnlineApplications/OnlineApplicationArraySection.tsx","../../src/pages/OnlineApplication/form-builder/handleFormArraySectionsFieldValidationSchemas.ts","../../src/pages/OnlineApplication/form-builder/handleFormFieldsInitialValues.ts","../../src/pages/OnlineApplication/form-builder/handleRegularFormFieldsValidation.ts","../../src/assets/images/fch-logo-disabled-client.svg","../../src/assets/images/icons/client-disabled-phone-icon.svg","../../src/assets/images/icons/client-disabled-email-icon.svg","../../src/assets/images/fch-graphic.png","../../src/components/OnlineApplications/OnlineApplicationDisabledClient.tsx","../../src/pages/OnlineApplication/OnlineApplication.tsx","../../src/pages/OnlineApplication/OnlineApplicationThankYou.tsx","../../src/schemas/HRCompliantSchemas.ts","../../src/pages/OnlineApplication/OnlineApplicationUploadHRCompliantForm.tsx","../../src/pages/OnlineApplication/OnlineApplicationUploadPhoto.tsx","../../src/pages/OnlineApplication/OnlineApplicationUploadResume.tsx","../../src/pages/OnlineApplication/OnlineApplicationUploadThankYou.tsx","../../src/routes/online-application.tsx","../../src/pages/VideoConferencing/VideoConferencingPublic.tsx","../../src/routes/video-conferencing.tsx","../../src/pages/UserInvitation/UserInvitationRegister.tsx","../../src/pages/UserInvitation/UserInvitationStatusCheck.tsx","../../src/routes/invitations.tsx","../../src/pages/Maintenance/MaintenanceMode.tsx","../../src/routes/maintenance.tsx","../../src/components/WYSIWYG/TextEditorConfigurable.tsx","../../src/components/Table/TableDnD.tsx","../../src/pages/Marketing/Banners.tsx","../../src/routes/marketing.tsx","../../src/pages/Tours/Applicants/TourAiSummary.tsx","../../src/routes/tours.tsx","../../src/pages/HRCompliant/HRCompliantFormCompanyUpload.tsx","../../src/routes/hr-compliant-application.tsx","../../src/routes/index.ts","../../src/utilities/handleCheckVisitedPageAccessType.ts","../../src/api/User/User.ts","../../src/constants/users.ts","../../src/providers/auth-context.tsx","../../src/api/Authentication/Authentication.ts","../../src/hooks/useMetaViewportTag.ts","../../src/components/Modal/ModalEnableCookies.tsx","../../src/components/Wrappers/PermissonCheckPageWrapper.tsx","../../src/App.tsx","../../src/components/ScrollRestoration/ScrollRestoration.tsx","../../src/config/rollbar.ts","../../src/providers/query-client.tsx","../../src/pages/ErrorBoundary/ErrorBoundary.tsx","../../src/main.tsx"],"sourcesContent":["export const API_BASE_URL: string = import.meta.env.VITE_BASE_URL;\nexport const SOCKET_URL: string = import.meta.env.VITE_SOCKET_URL;\nexport const TINY_MCE_WYSIWYG_EDITOR_KEY: string = import.meta.env.VITE_TINY_MCE_KEY;\nexport const GOOGLE_RECAPTCHA_KEY: string = import.meta.env.VITE_GOOGLE_RECAPTCHA_KEY;\nexport const GOOGLE_ANALYTICS_ID: string = import.meta.env.VITE_GOOGLE_ANALYTICS_ID;\n","import { useEffect } from \"react\";\nimport ReactGA from \"react-ga4\";\nimport { useLocation } from \"react-router\";\nimport { GOOGLE_ANALYTICS_ID } from \"../config/config\";\n\n/**\n *\n * Google Analytics Tag \n * Creates a Google Analytics session and emits an event on every page change. \n * \n */\nexport const usePageTracking = () => {\n // Prevent initializing the GA Tag if on \"dev\" environment or if there's no valid ID to be used\n if (import.meta.env.DEV || !GOOGLE_ANALYTICS_ID) return;\n\n ReactGA.initialize(GOOGLE_ANALYTICS_ID);\n\n const location = useLocation();\n\n useEffect(() => {\n ReactGA.send({ hitType: \"pageview\", page: location.pathname, title: \"FirstChoice Hiring\" });\n }, [location]);\n};\n","type LocalStorageKeys =\n | \"accessToken\"\n | \"refreshToken\"\n | \"expiresIn\"\n | \"autologin\"\n | \"fch-chats\"\n | \"fch-email\"\n | \"dashboard_view_mode\"\n | \"activeProductTour\";\n\n/**\n *\n * This object represents a mapping between the specific local storage keys that are being\n * used in the application and the type of action that will be taken\n * when obtaining or storing the value in local storage.\n *\n * A \"parse\" action (e.g. for `expiresIn`) represents the following:\n *\n * - before obtaining an item from local storage, that is associated with the \"parse\" action,\n * the \"JSON.parse\" method will be called before returning the value\n *\n * - before storing an item into local storage, that is associated with the \"parse\" action,\n * the \"JSON.stringify\" method will be called before the value is stored\n *\n * A \"non-parse\" action represents that the item that was obtained, or the item that is to be stored, will be\n * in its original form without any additional steps.\n *\n */\nconst LocalStorageActionMappings: Record = {\n accessToken: \"no-parse\",\n refreshToken: \"no-parse\",\n expiresIn: \"parse\",\n autologin: \"parse\",\n \"fch-chats\": \"parse\",\n \"fch-email\": \"no-parse\",\n dashboard_view_mode: \"no-parse\",\n activeProductTour: \"no-parse\",\n};\n\nclass LocalStorageUtilities {\n /**\n *\n * Utility function that checks if Local Storage API is available at all in the browser,\n * in order to prevent \"Insecure Operations\" errors when this API is not available.\n *\n * Code taken from MDN: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage\n *\n */\n private checkExistanceOfLocalStorage() {\n let storage;\n try {\n storage = window[\"localStorage\"];\n const storageTest = \"__storage_test__\";\n storage.setItem(storageTest, storageTest);\n storage.removeItem(storageTest);\n return true;\n } catch (e) {\n return (\n e instanceof DOMException &&\n // everything except Firefox\n (e.code === 22 ||\n // Firefox\n e.code === 1014 ||\n // test name field too, because code might not be present\n // everything except Firefox\n e.name === \"QuotaExceededError\" ||\n // Firefox\n e.name === \"NS_ERROR_DOM_QUOTA_REACHED\") &&\n // acknowledge QuotaExceededError only if there's something already stored\n storage &&\n storage.length !== 0\n );\n }\n }\n\n /**\n * Checks if \"Local Storage\" API is available to work with and returns a `boolean`\n * based on its availability that is then used as a safe-guard within the other methods.\n *\n * @returns Boolean indicating if the API is available or not.\n *\n **/\n private checkLocalStorageAvailability(): boolean {\n if (!this.checkExistanceOfLocalStorage() || !navigator.cookieEnabled || !window.localStorage) {\n return false;\n }\n\n return true;\n }\n\n /**\n *\n * Obtain the targeted key from local storage, including additional functionality:\n *\n * - check for availability of local storage API\n * - check for availabilty of stored items to work with\n * - parsing of stored value that will be obtained from local storage (if needed)\n *\n * @param key Specific key used in the application that we want to obtain from local storage\n *\n */\n public getItem(key: LocalStorageKeys) {\n // Exit function if local storage is not available\n if (!this.checkLocalStorageAvailability()) return;\n\n // Exit function if there are no items to work with in local storage\n if (!window.localStorage.length) return;\n\n // Obtain the value of the specific key that was saved in local storage,\n // and if it cannot be found exit function returning default value\n const storedValue = window.localStorage.getItem(key);\n if (!storedValue) return \"\";\n\n try {\n if (LocalStorageActionMappings[key] === \"parse\") {\n return JSON.parse(storedValue);\n } else {\n return storedValue;\n }\n } catch (error) {\n throw new Error(\"Local storage utility function - JSON.parse action - failed\");\n }\n }\n\n /**\n *\n * Save a specific key/value pair in local storage, with additional functionality:\n *\n * - check for availability of local storage API\n * - stringifies the value before storing it in local storage (if needed)\n *\n * @param key Specific key to be used in the application that we want to save in local storage\n * @param value Specific value that will be used in the application (e.g. tokens)\n *\n */\n public saveItem(key: LocalStorageKeys, value: unknown) {\n // Exit function if local storage is not available\n if (!this.checkLocalStorageAvailability()) return;\n\n // Exit function if there's no value to work with\n if (!value) return;\n\n try {\n if (LocalStorageActionMappings[key] === \"parse\") {\n window.localStorage.setItem(key, JSON.stringify(value));\n } else {\n window.localStorage.setItem(key, value as string);\n }\n } catch (error: any) {\n throw new Error(error);\n }\n }\n\n /**\n *\n * Remove a specific key from local storage.\n *\n * Includes check for availability of local storage API.\n *\n * @param key Specific key that was being used in the application prior its deletion\n *\n */\n public removeItem(key: LocalStorageKeys) {\n // Exit function if local storage is not available\n if (!this.checkLocalStorageAvailability()) return;\n\n window.localStorage.removeItem(key);\n }\n\n /**\n *\n * Clears out the local storage entirely.\n *\n */\n public clear() {\n window.localStorage.clear();\n }\n}\n\nexport const LocalStorageActions = new LocalStorageUtilities();\n","/**\n * Utility function for handling the error messages in the try/catch blocks\n * @param error The thrown error that was caught in the \"catch\" block\n * @returns Either the error's message (if it exists; for regular errors)\n * or the error itself as a string (for irregular errors)\n */\nexport default function handleErrorMessage(error: unknown): string {\n const typecastedError = error as Error;\n return typecastedError?.message ? typecastedError.message : String(error);\n}\n","import { API_BASE_URL } from \"../config/config\";\nimport { AuthenticationAPIResponse } from \"./types\";\n\n// Utilities\nimport { LocalStorageActions } from \"../utilities/handleLocalStorage\";\nimport handleErrorMessage from \"../utilities/handleErrorMessage\";\n\n/**\n *\n * Utility function for sending a request to the server\n * which should generate new access and refresh tokens that\n * guarantee usage of the application as long as the user is authenticated\n *\n */\nexport async function tokenRefresher() {\n // Get the tokens from local storage\n const accessToken = LocalStorageActions.getItem(\"accessToken\");\n const refreshToken = LocalStorageActions.getItem(\"refreshToken\");\n const tokenExpiresIn = LocalStorageActions.getItem(\"expiresIn\") || 0;\n const isAutologin = LocalStorageActions.getItem(\"autologin\") || false;\n\n try {\n // If user used the \"autologin\" feature, do not try to refresh any tokens\n if (isAutologin) return { accessToken };\n\n // If user manually logged in, but there are no tokens to work with, throw an error\n if (!accessToken || !refreshToken) {\n throw new Error(\"Could not refresh the access token.\");\n }\n\n // If there's a valid access token, and the expiry date is not yet reached,\n // just return the access token instead of sending additional requests\n if (accessToken && tokenExpiresIn) {\n const currentTimestamp: number = new Date().getTime();\n\n // We should check the expiration date of the token\n // 10 minutes before it is set to expire\n const tokenExpirationEarlyCheck: number = tokenExpiresIn - 1000 * 60 * 10;\n\n // If the current timestamp hasn't yet reached the early expiration check,\n // just return the existing access token that we already have in local storage\n if (tokenExpirationEarlyCheck >= currentTimestamp) {\n return { accessToken, refreshToken };\n }\n }\n\n // Send a request to the API to obtain the new tokens\n const response = await fetch(`${API_BASE_URL}/auth/refresh-token`, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n\n const { access_token, refresh_token, expires_in }: AuthenticationAPIResponse =\n await response.json();\n\n // Throw an error if the response had some issue\n if (!response.ok) throw new Error(\"Could not refresh the access token. Please try again.\");\n\n // Save the tokens and expiration timestamp values in local storage\n // after successfully refreshing the active access token\n handleAuthenticationTokensInLocalStorage(access_token, refresh_token, expires_in);\n\n return { access_token, refresh_token };\n } catch (error) {\n const errorMessage: string = handleErrorMessage(error);\n\n // In case of an error when trying to refresh the access token,\n // which at the same time means the user should not be authenticated anymore,\n // remove the existing token values from storage and reload page\n handleAuthenticationTokensRemovalFromStorage();\n\n // Redirect the user to the login page containing info about the message to be shown\n const redirectURL = `${location.pathname}${location.search}`;\n location.replace(`/login/?message=expired_token&redirectTo=${encodeURIComponent(redirectURL)}`);\n\n throw new Error(errorMessage);\n }\n}\n\n/**\n *\n * Handle saving the received tokens and expiration timestamp into local storage\n *\n * @param accessToken The access token value received from the API upon login / token refresh.\n * @param refreshToken The refresh token value received from the API upon login / token refresh.\n * @param expiresIn The expiration timestamp (representing `in how much time will the token expire`)\n * received from the API upon login / token refresh.\n *\n */\nexport function handleAuthenticationTokensInLocalStorage(\n accessToken: string,\n refreshToken: string,\n expiresIn: number,\n autologin?: boolean,\n) {\n // Save the newly issued tokens in local storage\n LocalStorageActions.saveItem(\"accessToken\", accessToken);\n LocalStorageActions.saveItem(\"refreshToken\", refreshToken);\n\n // If user used the \"autologin\" feature, save the value in local storage so it can be reused\n if (autologin) LocalStorageActions.saveItem(\"autologin\", autologin);\n\n // Save the expiration date in local storage as a sum\n // of the current timestamp and the expiration timestamp received from the API\n const tokenExpirationSum: number = new Date().getTime() + expiresIn * 1000;\n LocalStorageActions.saveItem(\"expiresIn\", tokenExpirationSum);\n}\n\n/**\n *\n * Handle removal of tokens and expiration timestamp that are saved in local storage.\n * This function will be triggered in the following scenarios:\n * - Upon manual `logout` by the user.\n * - Upon error scenario in the `tokenRefresher` function, failing to refresh the\n * currently active access token.\n *\n */\nexport function handleAuthenticationTokensRemovalFromStorage() {\n LocalStorageActions.removeItem(\"accessToken\");\n LocalStorageActions.removeItem(\"refreshToken\");\n LocalStorageActions.removeItem(\"expiresIn\");\n LocalStorageActions.removeItem(\"autologin\");\n}\n","export const API_RESPONSE_ERROR_MESSAGES: Record = {\n 400: \"Bad request to the API\",\n 401: \"Unauthenticated request to the API\",\n 403: \"You have no authorization for this action\",\n 404: \"Requested resource was not found.\",\n 405: \"The used request method is not allowed.\",\n 422: \"Form data is invalid. Please double check the form fields.\",\n};\n","import { API_RESPONSE_ERROR_MESSAGES } from \"./constants\";\nimport { FetchHandlerRequestBody } from \"./types\";\n\n/** Type definition for a custom error object that will be thrown when the response from the API is not OK. */\nexport type FetchErrorType = {\n message: string;\n originalRequest: FetchHandlerRequestBody;\n response: {\n status: number;\n statusText: string;\n url: string;\n type: string;\n data: {\n message: string;\n exception: string;\n line: string | number;\n };\n stack: string;\n };\n};\n\nexport type FetchErrorResponseType = {\n status: number;\n statusText: string;\n url: string;\n type: string;\n};\n\n/**\n *\n * Handler of the message that will be shown in the UI and logged in the error tracking service\n * based on the received response `status` and `message` from the API.\n *\n * If there's no specific `message` received in the response, uses the status-to-message mapping, to show pre-defined message.\n * If received status is not matched in the pre-defined list, use a generic message.\n *\n * @param status Status returned in the response from the API\n * @param message Message returned in the response from the API\n *\n * @returns String based message to be used for the fetch error handler.\n *\n */\nexport function handleFetchErrorMessage(status: number, message: string | undefined): string {\n if (message) {\n return message;\n } else {\n return API_RESPONSE_ERROR_MESSAGES[status] || \"Something went wrong!\";\n }\n}\n\n/**\n *\n * Handler for throwing custom error objects with specific details\n * for any erroneous response that we get from the API when using \"fetch\"\n *\n * @param response The response object from the API\n * @param data Object containing the data as received from the API\n *\n * @throws Custom error object\n *\n */\nexport function handleFetchError(\n response: FetchErrorResponseType,\n data: any,\n originalRequest: FetchHandlerRequestBody,\n) {\n // Error message that will be shown in the UI and logged in error tracking service\n const message: string = handleFetchErrorMessage(response.status, data.error || data.message);\n\n // Object to be thrown as an error for erroneous responses\n const error: FetchErrorType = {\n message,\n originalRequest,\n response: {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n type: response.type,\n stack: new Error(message)?.stack ?? \"N/A\",\n data: {\n message: data.message ?? \"N/A\",\n exception: data.exception ?? \"N/A\",\n line: data.line ?? \"N/A\",\n },\n },\n };\n\n // Create a form data object that will be sent to the server\n const formData = new FormData();\n\n // Default fields to be sent to the debugging server\n formData.append(\"error_message\", message);\n formData.append(\"status\", JSON.stringify(response.status));\n formData.append(\"statusText\", response.statusText);\n formData.append(\"url\", response.url);\n formData.append(\"type\", response.type);\n\n // Append the fields received from the file upload form data\n // before sending request to the debugging server, only if we have a body from the original request\n if (originalRequest.body && originalRequest.body instanceof FormData) {\n for (const [key, val] of originalRequest.body) {\n formData.append(key, val);\n }\n }\n\n throw error;\n}\n","import { API_BASE_URL } from \"../config/config\";\nimport { FetchHandlerMethods, FetchHandlerRequestBody, FetchHandlerURL } from \"./types\";\nimport { handleAuthenticationTokensRemovalFromStorage } from \"./tokenRefresher\";\nimport { handleFetchError } from \"./errorHandling\";\nimport { LocalStorageActions } from \"../utilities/handleLocalStorage\";\n\n/**\n *\n * Utility function for handling all fetch calls in the application\n * @param method The method that will be used in the API request\n * @param url The URL to which the API request will be sent\n * @param body The body (optional parameter) that will be sent in the API request\n *\n * @returns Processed data from the API response, or throws an error if something went wrong.\n *\n */\nexport default async function fetchHandler(\n method: FetchHandlerMethods = \"GET\",\n url: FetchHandlerURL,\n body?: FetchHandlerRequestBody,\n) {\n // Reads the saved access token\n const accessToken = LocalStorageActions.getItem(\"accessToken\");\n\n const response = await fetch(`${API_BASE_URL}/${url}`, {\n method,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...(accessToken && { Authorization: `Bearer ${accessToken}` }),\n },\n ...(body && { body: JSON.stringify(body) }),\n });\n\n // If the response status is 204, just exit the function\n if (response.status === 204) return;\n\n // If the response status is 401, logout the unauthenticated user\n if (response.status === 401) {\n // Remove tokens and expiration time from local storage\n handleAuthenticationTokensRemovalFromStorage();\n\n // Redirect the user to the login page\n location.replace(\"/login/\");\n\n return null;\n }\n\n // Parse the response that is returned by the API\n const data = await response.json();\n\n // In case there's an error, display the message received\n // in the API response, or fallback to a standardized error message\n if (!response.ok) handleFetchError(response, data, { body });\n\n // If everything is OK, return the received data\n return data;\n}\n","/**\n * Capitalizes the first letter of a given string\n * @param initialString The initial string value as received from external source (e.g. API)\n * @param splitters An array of strings representing splitters used to split the initial value (e.g. empty spaces, '-', ',', etc.)\n * @returns A string firstly split into the provided delimiters (splitters) and capitalized each chunk.\n */\nexport function handleStringCapitalization(\n initialString: string,\n splitters: string[] = [\"\"],\n): string {\n // Exit function and return empty string if there's no string to be capitalized\n if (!initialString) return \"\";\n\n let capitalized = initialString;\n\n // For each provided splitter, capitalize the contained substring\n splitters.forEach(splitter => {\n capitalized = capitalized\n .split(splitter)\n .map(subString => {\n // If substring is empty after splitting, skip the chunk and do not include it back in the list of items to be joined\n if (!subString) return;\n return subString[0].toUpperCase() + subString.slice(1);\n })\n .join(splitter);\n });\n\n return capitalized;\n}\n","/**\n *\n * Utility function for extracting the extension of the selected file,\n * based on the received `filename` value for the selection that was made.\n *\n * For example, for selected `test.docx` file, the utility function\n * will extract its extension, and return only `docx` as the value.\n *\n * @param filename The name of the file that was selected\n *\n * @returns The file's extension\n *\n */\nexport default function handleExtractFileExtension(filename: string) {\n // Exit function if there's no valid filename value provided\n if (!filename) return \"N/A\";\n\n const splitFilename = filename.split(\".\");\n const filenameExtension: string = splitFilename[splitFilename.length - 1];\n\n // We always convert the filename extension to lowercase\n // to ensure edge cases such as JPG vs jpg not being handled correctly.\n return filenameExtension.toLowerCase();\n}\n","import { useRollbar } from \"@rollbar/react\";\nimport { toast } from \"react-toastify\";\n\n// Utilities\nimport handleErrorMessage from \"../utilities/handleErrorMessage\";\nimport handleExtractFileExtension from \"../utilities/strings/handleExtractFileExtension\";\nimport { FetchErrorType } from \"../api/errorHandling\";\n\ntype RollbarLogLevel = \"critical\" | \"error\" | \"warn\" | \"info\";\ntype RollbarPayloadDetails = Record;\n\n/**\n *\n * Hook for handling sending specific errors from\n * `catch` blocks to Rollbar for better organization.\n */\nexport default function useErrorReporting() {\n const rollbar = useRollbar();\n\n const handleRollbarError = (\n message: string,\n error: unknown | Error,\n extraDetails: RollbarPayloadDetails = {},\n level: RollbarLogLevel = \"error\",\n ) => {\n // Typecast the received custom error object\n const errorTypecasted: FetchErrorType = error as FetchErrorType;\n\n // Extract only the 'message' of the received error object\n const extractedErrorMessage: string = handleErrorMessage(errorTypecasted.message);\n\n // Display toast notification in the UI\n toast.error(extractedErrorMessage);\n\n // Map the received extra details to be included in the rollbar log\n // so we can handle including extra information for fields that represent file uploads\n const mappedExtraDetailsObject: RollbarPayloadDetails = {};\n Object.entries(extraDetails).forEach(property => {\n const [key, value] = property;\n\n // If the extra details field is a file or a blob, include extra information to be sent to rollbar\n if (value instanceof File) {\n mappedExtraDetailsObject[key] = {\n file_size: value.size,\n file_name: value.name,\n file_extension: handleExtractFileExtension(value.name),\n };\n } else {\n mappedExtraDetailsObject[key] = value;\n }\n });\n\n // Include erroneous response details\n mappedExtraDetailsObject[\"response_error\"] = { ...errorTypecasted };\n\n // Report issue to Rollbar\n rollbar[level](`${message} - ${extractedErrorMessage}`, mappedExtraDetailsObject);\n };\n\n return handleRollbarError;\n}\n","import { LoaderProps } from \"./interfaces\";\n\nconst Loader = ({\n size = \"sm\",\n speed = \"medium\",\n modifierWrapper = \"\",\n modifier = \"\",\n style = {},\n}: LoaderProps) => {\n return (\n
\n \n
\n );\n};\n\nexport default Loader;\n","import Loader from \"../Loader/Loader\";\nimport { ButtonProps } from \"./interfaces\";\n\nconst Button = ({\n type = \"submit\",\n isLoading = false,\n modifierClass = \"\",\n loaderPositioning = \"right\",\n isDisabled = false,\n ...props\n}: ButtonProps) => {\n return (\n \n
\n <>\n {props.children}\n\n {isLoading && }\n \n
\n \n );\n};\n\nexport default Button;\n","// Utilities & Hooks\nimport { useEffect, useRef } from \"react\";\nimport { getIn } from \"formik\";\nimport { useTranslation } from \"react-i18next\";\n\n// Interfaces\nimport { FormInputProps } from \"./interfaces\";\n\nconst FormInput = ({\n form,\n field,\n label = \"\",\n isRequired = false,\n modifierClass = \"\",\n description = \"\",\n size = \"full\",\n tooltip = null,\n shouldAutofocus = false,\n ...props\n}: FormInputProps) => {\n // Handle Formik errors\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===============================\n TRIM FORM FIELD VALUE\n ================================*/\n const handleOnBlur = (event: React.FocusEvent) => {\n const trimmedValue: string = event.target.value.trim();\n\n // Update the form value to which this field corresponds to\n form.setFieldValue(field.name, trimmedValue);\n\n // Trigger internal Formik 'onBlur' events for the field\n field.onBlur(event);\n };\n\n // Automtaically focus on the input field\n const inputRef = useRef(null);\n useEffect(() => {\n if (!shouldAutofocus || !inputRef || !inputRef.current) return;\n inputRef.current.focus();\n }, [shouldAutofocus]);\n\n /*================================\n INTERNATIONALIZATION\n =================================*/\n const { t } = useTranslation();\n\n return (\n
\n {label && (\n \n {label}\n \n )}\n\n
\n \n\n {/* TOOLTIP ICON WITHIN THE INPUT */}\n {tooltip &&
{tooltip}
}\n
\n {description &&

{description}

}\n\n {/* DISPLAY ERROR MESSAGES */}\n {errors && touched &&

{t(errors)}

}\n
\n );\n};\n\nexport default FormInput;\n","/**\n *\n * Check if the string that is received as a parameter\n * contains valid HTML tags within it.\n *\n * Example usage: Used for validation schemas test cases where we\n * throw a validation error if the string has a valid HTML contained within it,\n * such as \"

Example text

\". Note that \"

node.nodeType === 1);\n}\n","/** Pre-defined text to be used as validation error message when there are HTML tags within a received text */\nexport const SCHEMAS_NO_HTML_MESSAGE_TEXT: string = \"HTML code is not allowed!\";\n\n/** Pre-defined text to be used as validation error message for passwords */\nexport const SCHEMAS_PASSWORD_MESSAGE: string =\n \"Your password must be at least 8 characters long. It is recommended that you use mixture of letters, numbers and special characters.\";\n\n/** Type definition for \"value\" parameter used in validation tests for file upload related fields */\nexport type SchemasFileValueTestValidation = {\n name: string;\n size: number;\n};\n","export const PASSWORD_REGEX_PATTERN: RegExp =\n /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*()-=[\\]{}\\\\|'\";:/?.,<>~`])([a-zA-Z0-9!@#$%^&*()-=[\\]{}\\\\|'\";:/?.,<>~`]+){8,}$/;\n\nexport const EMAIL_REGEX_PATTERN: RegExp =\n /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n\nexport const YOUTUBE_URL_REGEX_PATTERN: RegExp =\n /https:\\/\\/(www\\.youtube\\.com\\/watch\\?v|youtu\\.be\\/|www\\.youtube\\.com\\/embed\\/|youtube.com\\/watch\\?v)/;\n\nexport const SLUG_REGEX_PATTERN: RegExp = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport const WEBSITE_URL_REGEX_PATTERN: RegExp =\n /^(https?:\\/\\/)?(www\\.)?([a-zA-Z0-9]+(-?[a-zA-Z0-9])*\\.)+[\\w]{2,}(\\/\\S*)?$/;\n\nexport const LINKED_IN_URL_REGEX_PATTERN: RegExp =\n /^(http(s)?:\\/\\/)?([\\w]+\\.)?((linkedin\\.com)|(linked\\.in))\\/(mwlite\\/)?(in|pub|profile)\\/.*/gi;\n","import * as Yup from \"yup\";\nimport { handleCheckStringForHTML } from \"../utilities/strings/handleCheckStringForHTML\";\nimport { SCHEMAS_NO_HTML_MESSAGE_TEXT, SCHEMAS_PASSWORD_MESSAGE } from \"./constants\";\n\n// SCHEMA REGEX\nimport { PASSWORD_REGEX_PATTERN } from \"./regexes\";\n\nexport const AUTHENTICATION_LOGIN_SCHEMA = Yup.object().shape({\n email: Yup.string()\n .email(\"Please enter a valid email address\")\n .required(\"Please enter your email\"),\n password: Yup.string().required(\"Please enter your password\"),\n});\n\nexport const AUTHENTICATION_REGISTER_SCHEMA = Yup.object().shape({\n first_name: Yup.string()\n .required(\"Please enter your first name\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"auth-register-fname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n last_name: Yup.string()\n .required(\"Please enter your last name\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"auth-register-lname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n password: Yup.string()\n .matches(PASSWORD_REGEX_PATTERN, {\n message: SCHEMAS_PASSWORD_MESSAGE,\n })\n .min(8, \"Your password needs to be at least 8 characters long.\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .required(\"Please enter your password\"),\n password_confirmation: Yup.string().when(\"password\", {\n is: (password: string) => !!password,\n then: schema =>\n schema\n .oneOf([Yup.ref(\"password\")], \"Passwords do not match.\")\n .required(\"Please confirm the password\"),\n }),\n});\n\nexport const AUTHENTICATION_FORGOT_PASSWORD_SCHEMA = Yup.object().shape({\n email: Yup.string()\n .email(\"Please enter a valid email\")\n .required(\"Please enter your email address.\"),\n});\n\nexport const AUTHENTICATION_RESET_PASSWORD_SCHEMA = Yup.object().shape({\n password: Yup.string()\n .matches(PASSWORD_REGEX_PATTERN, {\n message: SCHEMAS_PASSWORD_MESSAGE,\n })\n .min(8, \"Your password needs to be at least 8 characters long.\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .required(\"Please enter your password\"),\n password_confirmation: Yup.string().when(\"password\", {\n is: (password: string) => (password && password.length > 0 ? true : false),\n then: schema =>\n schema\n .oneOf([Yup.ref(\"password\")], \"Passwords do not match.\")\n .required(\"Please confirm the password\"),\n }),\n});\n","// Utilities & Hooks\nimport { Field, Form, Formik } from \"formik\";\nimport { useAuthenticationForgotPassword } from \"../../api/Authentication/Authentication\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport { Link } from \"react-router\";\nimport Button from \"../../components/Button/Button\";\nimport FormInput from \"../../components/Form/FormInput\";\n\n// Schemas\nimport { AUTHENTICATION_FORGOT_PASSWORD_SCHEMA } from \"../../schemas/AuthenticationSchemas\";\n\n// Interfaces\nimport { ForgotPasswordState } from \"./interfaces\";\n\nconst ForgotPassword = () => {\n const errorReporting = useErrorReporting();\n const forgotPassword = useAuthenticationForgotPassword();\n\n // Send a request to the API so the user can reset the password\n const handleForgotPassword = async ({ email }: ForgotPasswordState) => {\n try {\n await forgotPassword.mutateAsync(email);\n } catch (error) {\n errorReporting(\"Failed 'Forgot Password' action.\", error, { email }, \"critical\");\n }\n };\n\n return (\n <>\n

Reset Password

\n\n \n
\n \n\n \n {forgotPassword.isPending ? \"Sending Email...\" : \"Request Password Reset\"}\n \n\n
\n \n Go Back\n \n
\n \n \n\n
\n

\n \n Having trouble resetting your password?\n \n Call us at (877) 449-7595\n

\n\n

\n Note: Please contact your local IT person or\n department to verify that the domain name firstchoicehiring.com is white-listed in your\n company's email server.\n

\n
\n \n );\n};\n\nexport default ForgotPassword;\n","import { useSearchParams } from \"react-router\";\n\n/**\n *\n * Hook for extracting the query parameters from\n * the URL that the user visited\n *\n */\nexport function useExtractSearchParameters(): [Record, Function] {\n const [searchParameters, setSearchParams] = useSearchParams();\n const searchParams = Object.fromEntries(new URLSearchParams(searchParameters));\n\n return [searchParams, setSearchParams];\n}\n\n/**\n *\n * Utility function for merging the extracted\n * search parameters into a single string to be\n * then used for constructing navigation links.\n *\n * @param params Key/value pairs representing the existing URL parameters.\n *\n * @returns A single string containing all of the existing search parameters joint together.\n *\n */\nexport function handleMergeSearchParameters(params: Record): string {\n if (!params || !Object.entries(params).length) return \"\";\n\n let parameters: string = \"\";\n Object.entries(params).forEach(param => {\n const [key, value] = param;\n\n if (parameters.startsWith(\"?\")) {\n parameters += `&${key}=${value}`;\n } else {\n parameters += `?${key}=${value}`;\n }\n });\n\n return parameters;\n}\n","import React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\n// Utilities\nimport { getIn } from \"formik\";\n\n// Interfaces\nimport { FormCheckboxProps } from \"./interfaces\";\n\nconst FormCheckbox = ({\n form,\n field,\n label,\n handleCheckboxNonFormAction,\n modifierClass = \"\",\n labelModifierClass = \"\",\n ...props\n}: FormCheckboxProps) => {\n // Handle Formik errors\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*================================\n HANDLE CHECKBOX CHANGES\n =================================*/\n const handleCheckbox = (event: React.ChangeEvent) => {\n // If a non-form action function is received as a prop execeute that\n // Otherwise trigger updating the specified form field's value\n if (handleCheckboxNonFormAction) {\n handleCheckboxNonFormAction(event);\n } else {\n form.setFieldValue(field.name, event.currentTarget.checked);\n }\n };\n\n /*================================\n INTERNATIONALIZATION\n =================================*/\n const { t } = useTranslation();\n\n return (\n \n \n \n\n {errors && touched &&
{t(errors)}
}\n \n );\n};\n\nexport default FormCheckbox;\n","import { ModalBackdropProps } from \"./interfaces\";\nimport { motion } from \"framer-motion\";\n\nconst ModalBackdrop = ({ overlayModifierClass = \"\", handleCloseModal }: ModalBackdropProps) => {\n return (\n \n );\n};\n\nexport default ModalBackdrop;\n","// Components\nimport { motion, Variants } from \"framer-motion\";\nimport Button from \"../Button/Button\";\nimport ModalBackdrop from \"./ModalBackdrop\";\n\n// Assets\nimport { FaRegUserCircle as UserIcon } from \"react-icons/fa\";\n\nconst FRAMER_MOTION_INACTIVE_USER_MODAL: Variants = {\n initial: {\n opacity: 0,\n translateX: \"-50%\",\n translateY: 0,\n },\n animate: {\n opacity: 1,\n translateX: \"-50%\",\n translateY: \"-50%\",\n },\n exit: {\n opacity: 0,\n translateX: \"-50%\",\n translateY: 0,\n },\n};\n\nconst FRAMER_MOTION_INACTIVE_USER_MODAL_ICON: Variants = {\n initial: {\n opacity: 0,\n scale: 0,\n y: 20,\n },\n animate: {\n scale: 1,\n opacity: 1,\n y: 0,\n },\n};\n\nconst FRAMER_MOTION_INACTIVE_USER_MODAL_ELEMENTS: Variants = {\n initial: {\n opacity: 0,\n y: 20,\n },\n animate: {\n opacity: 1,\n y: 0,\n },\n};\n\nconst ModalInactiveUser = ({ handleModalState }: { handleModalState: () => void }) => {\n return (\n
\n \n\n \n \n \n \n\n \n Password Reset Required\n \n\n \n We've been busy improving FirstChoice Hiring! We just sent you an email.
\n Check your inbox now for your password reset message.\n
\n
\n Need help? Contact Support!\n
\n
\n \n \n (877) 449-7595\n \n or\n \n support@proexel.com\n \n \n
\n\n \n \n
\n );\n};\n\nexport default ModalInactiveUser;\n","import { getIn } from \"formik\";\nimport { FormPasswordInputProps } from \"./interfaces\";\nimport { useState } from \"react\";\n\n// Assets\nimport { BiHide as HidePasswordIcon, BiShow as ShowPasswordIcon } from \"react-icons/bi\";\n\nconst FormPasswordInput = ({\n form,\n field,\n label = \"\",\n modifierClass = \"\",\n description = \"\",\n isRequired = false,\n size = \"full\",\n ...props\n}: FormPasswordInputProps) => {\n // Handle Formik errors\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===============================\n TRIM FORM FIELD VALUE\n ================================*/\n const handleOnBlur = (event: React.FocusEvent) => {\n const trimmedValue: string = event.target.value.trim();\n\n // Update the form value to which this field corresponds to\n form.setFieldValue(field.name, trimmedValue);\n\n // Trigger internal Formik 'onBlur' events for the field\n field.onBlur(event);\n };\n\n /*==============================\n TOGGLE PASSWORD VISIBILITY\n ===============================*/\n const [passwordVisibility, setPasswordVisibility] = useState(false);\n const handlePasswordVisibility = () => setPasswordVisibility(!passwordVisibility);\n\n return (\n \n {label && (\n \n {label}\n \n )}\n\n
\n \n\n \n {passwordVisibility ? (\n \n ) : (\n \n )}\n \n
\n\n {/* DESCRIPTION OF THE FIELD */}\n {description &&

{description}

}\n\n {/* DISPLAY ERROR MESSAGES */}\n {errors && touched &&

{errors}

}\n \n );\n};\n\nexport default FormPasswordInput;\n","// Utilities & Hooks\nimport { useEffect, useState, ChangeEvent } from \"react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useExtractSearchParameters } from \"../../hooks/useExtractSearchParameters\";\nimport { cloneDeep } from \"lodash-es\";\nimport { LocalStorageActions } from \"../../utilities/handleLocalStorage\";\nimport { useAuthenticationLogin } from \"../../api/Authentication/Authentication\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport { Link, useLocation, useNavigate } from \"react-router\";\nimport { AnimatePresence } from \"framer-motion\";\nimport Button from \"../../components/Button/Button\";\nimport FormInput from \"../../components/Form/FormInput\";\nimport FormCheckbox from \"../../components/Form/FormCheckbox\";\nimport ModalInactiveUser from \"../../components/Modal/ModalInactiveUser\";\nimport FormPasswordInput from \"../../components/Form/FormPasswordInput\";\n\n// Schemas\nimport { AUTHENTICATION_LOGIN_SCHEMA } from \"../../schemas/AuthenticationSchemas\";\n\n// Interfaces\nimport { LoginFormState } from \"./interfaces\";\nimport { RouterLocationStateRedirectTo } from \"../../interfaces/global\";\n\nconst Login = () => {\n const location = useLocation();\n const navigate = useNavigate();\n const errorReporting = useErrorReporting();\n const [searchParametersObject] = useExtractSearchParameters();\n\n const [loginState, setLoginState] = useState({\n email: \"\",\n password: \"\",\n });\n\n /*==============================\n LOGIN USER\n ===============================*/\n const [showInactiveUserModal, setShowInactiveUserModal] = useState(false);\n const loginUser = useAuthenticationLogin();\n\n const handleLogin = async (credentials: LoginFormState) => {\n try {\n const { is_enabled } = await loginUser.mutateAsync(credentials);\n\n // If the user is not enabled, show info modal\n if (is_enabled === false) {\n setShowInactiveUserModal(true);\n return;\n }\n\n // Redirect the user to the page that they tried accessing\n // but couldn't because they weren't logged in at the moment\n const locationState = location.state as RouterLocationStateRedirectTo;\n\n if (locationState && locationState.redirectTo) navigate(locationState.redirectTo);\n\n // If the redirection is coming from an expired token, read the redirection\n // URL & any filters if present from the search params\n const copiedParams = cloneDeep(searchParametersObject);\n delete copiedParams[\"message\"];\n const parametersEntries = Object.entries(copiedParams);\n\n const finalparams = parametersEntries\n .join(\"&\")\n .replaceAll(\",\", \"=\")\n .replace(\"redirectTo=\", \"\");\n\n if (searchParametersObject[\"redirectTo\"]) navigate(finalparams);\n } catch (error) {\n errorReporting(\"User login failed\", error, { email: credentials.email }, \"critical\");\n }\n };\n\n /*==============================\n REMEMBER ME\n\n Saves the email in local storage\n and populates the input field with its value\n ===============================*/\n const [isRememberMeChecked, setIsRememberMeChecked] = useState(false);\n\n const handleRememberMe = (email: string, checked: boolean) => {\n // Either update or remove the 'fch-email' value in local storage\n if (checked) {\n LocalStorageActions.saveItem(\"fch-email\", email);\n } else {\n LocalStorageActions.removeItem(\"fch-email\");\n }\n\n setIsRememberMeChecked(checked);\n };\n\n // Pre-populate the email field with if a saved value exists\n useEffect(() => {\n const loginStateCopy = { ...loginState };\n const rememberedEmail: string = LocalStorageActions.getItem(\"fch-email\");\n\n if (rememberedEmail) {\n setLoginState({ ...loginStateCopy, email: rememberedEmail });\n setIsRememberMeChecked(true);\n }\n }, []);\n\n /*==============================\n RESET ALL CACHED DATA\n WHEN THIS PAGE COMPONENT MOUNTS\n ===============================*/\n const queryClient = useQueryClient();\n useEffect(() => {\n queryClient.clear();\n }, []);\n\n /*============================\n HANDLE THE DISPLAYED\n LOGIN MESSAGE\n =============================*/\n const [loginMessage, setLoginMessage] = useState(\"Please Login\");\n\n useEffect(() => {\n // If there's no valid location's state, exit function\n if (!location.state) return;\n\n // Extract the \"redirectTo\" state property, that contains\n // the URL to which the user will be redirected after successful login\n const { redirectTo } = location.state as Record<\"redirectTo\", string>;\n\n // If the redirect URL is the page for accepting an invitation,\n // show a specific message to the user informing them what they need to do.\n if (redirectTo.startsWith(\"/account/invitation/\")) {\n setLoginMessage(\n \"You need to be logged in to \\n accept this invitation. \\n\\n Please log in below.\",\n );\n }\n }, [location.state]);\n\n // Check for existing URL parameters that include the redirect reason\n // so we can display the correct message to the user in the UI\n // NOTE: In case the number of scenarios where we need to show a message increases, we can refactor this.\n useEffect(() => {\n if (searchParametersObject?.message === \"expired_token\") {\n setLoginMessage(\n <>\n Your session has timed out\n Please login again to access the application\n ,\n );\n }\n }, []);\n\n return (\n <>\n

{loginMessage}

\n\n \n {({ values }) => (\n
\n \n\n \n\n ) => {\n handleRememberMe(values.email, event.target.checked);\n }}\n component={FormCheckbox}\n />\n\n \n {loginUser.isPending ? \"Signing In...\" : \"Sign In\"}\n \n \n )}\n \n\n
\n \n Forgot Password?\n \n
\n\n
\n

Need Technical Support?

\n Call us at (877) 449-7595\n
\n\n
\n

Need Sales Assistance?

\n Call us at (888) 990-6451\n
\n\n \n {showInactiveUserModal ? (\n setShowInactiveUserModal(false)} />\n ) : null}\n \n \n );\n};\n\nexport default Login;\n","// Utilities & Hooks\nimport { Field, Form, Formik } from \"formik\";\nimport { useExtractSearchParameters } from \"../../hooks/useExtractSearchParameters\";\nimport { useAuthenticationResetPassword } from \"../../api/Authentication/Authentication\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport { Link } from \"react-router\";\nimport Button from \"../../components/Button/Button\";\nimport FormPasswordInput from \"../../components/Form/FormPasswordInput\";\n\n// Schemas\nimport { AUTHENTICATION_RESET_PASSWORD_SCHEMA } from \"../../schemas/AuthenticationSchemas\";\n\n// Interfaces\nimport { ResetPasswordState } from \"./interfaces\";\n\nconst ResetPassword = () => {\n const errorReporting = useErrorReporting();\n\n /*==============================\n EXTRACT QUERY PARAMETERS\n ===============================*/\n const [searchParametersObject] = useExtractSearchParameters();\n\n /*==============================\n RESET PASSWORD\n ===============================*/\n const resetPassword = useAuthenticationResetPassword();\n\n // Send a request to the API to reset the password\n const handleResetPassword = async ({ password, password_confirmation }: ResetPasswordState) => {\n // If there's no available \"token\" query parameter,\n // exit function and prevent sending request to the API\n if (!searchParametersObject[\"token\"]) return;\n\n try {\n await resetPassword.mutateAsync({\n token: searchParametersObject[\"token\"],\n password,\n password_confirmation,\n });\n } catch (error) {\n errorReporting(\n \"Failed 'Reset Password' action\",\n error,\n {\n reset_password_token: searchParametersObject[\"token\"],\n },\n \"critical\",\n );\n }\n };\n\n return (\n <>\n

Reset Your Password

\n\n \n
\n \n\n \n\n \n {resetPassword.isPending ? \"Resetting Password...\" : \"Reset Password\"}\n \n\n
\n \n Go Back\n \n
\n \n \n\n
\n

\n \n Having trouble processing your reset request?\n \n Call us at 1-877-449-7595 for help.\n

\n

\n Note: Please contact your local IT person or\n department to verify that the domain names firstchoicehiring.net, and\n firstchoicehiring.com are white-listed in your company's email server.\n

\n
\n \n );\n};\n\nexport default ResetPassword;\n","// Utilities & Hooks\nimport { Link, useNavigate } from \"react-router\";\nimport { useExtractSearchParameters } from \"../../hooks/useExtractSearchParameters\";\nimport { useAuthenticationAutologin } from \"../../api/Authentication/Authentication\";\nimport { LocalStorageActions } from \"../../utilities/handleLocalStorage\";\nimport { toast } from \"react-toastify\";\nimport { useEffect } from \"react\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport Loader from \"../../components/Loader/Loader\";\n\nconst Autologin = () => {\n const { handleAuthenticationTokens } = useAuth();\n const [searchParametersObject] = useExtractSearchParameters();\n const navigate = useNavigate();\n const existingAccessToken = LocalStorageActions.getItem(\"accessToken\");\n const errorReporting = useErrorReporting();\n\n // Get the email from the search params & replace space character with \"+\"\n const formattedEmail = searchParametersObject?.email?.replaceAll(\" \", \"+\") || \"\";\n\n // Send request to obtain new tokens from the used autologin link\n const { data, error, isPending, isSuccess } = useAuthenticationAutologin(\n formattedEmail,\n searchParametersObject.token,\n existingAccessToken,\n );\n\n // Updated authentication details with what is received from requested autologin\n // If an error occurs or if there's no valid access token, redirect to forbidden page\n useEffect(() => {\n if (!data) return;\n\n if (!data.access_token) {\n errorReporting(\"No token(s) received from the API\", null);\n navigate(\"/403\");\n return;\n }\n\n handleAuthenticationTokens(\n data.access_token,\n data.refresh_token,\n data.expires_in,\n data.autologin,\n );\n }, [data]);\n\n // If there are any authentication values saved in local storage, redirect user to homepage\n useEffect(() => {\n if (!existingAccessToken) return;\n\n navigate(\"/applications/\");\n toast.info(\n \"You're already logged in. Try using the Autologin link in another browser or in incognito mode.\",\n { ariaLabel: \"autologin-info-notification\" },\n );\n }, []);\n\n // If the autologin request has successfully been completed, redirect user to homepage\n useEffect(() => {\n if (!isSuccess) return;\n navigate(\"/applications/\");\n }, [isSuccess]);\n\n if (error) {\n navigate(\"/403\");\n return null;\n }\n\n // If there are missing \"email\" and/or \"token\" search parameters,\n // use a fallback UI to inform the user about the required fields\n if (!searchParametersObject.email || !searchParametersObject.token) {\n return (\n
\n

Missing Autologin Credentials

\n

\n Please make sure that there are valid \"email\" and \"token\" credentials included in the URL.\n

\n \n Go Back\n \n
\n );\n }\n\n return (\n
\n

Logging In as:

\n

{formattedEmail}

\n {isPending ? : null}\n
\n );\n};\n\nexport default Autologin;\n","export default \"__VITE_ASSET__Cs3WbKl8__\"","// Assets\nimport FCHLogo from \"../assets/images/fch-full-logo.png\";\n\nconst LayoutUnauthenticated = ({ children }: { children: React.ReactNode }) => {\n return (\n
\n
\n
\n

We do all the work so you can focus on yours.

\n
\n
\n \n
\n \"FirstChoice\n\n {children}\n
\n
\n \n );\n};\n\nexport default LayoutUnauthenticated;\n","// Interfaces\nimport { ApplicationRouteProps } from \"./interfaces\";\n\n// Pages\nimport ForgotPassword from \"../pages/Authentication/ForgotPassword\";\nimport Login from \"../pages/Authentication/Login\";\nimport ResetPassword from \"../pages/Authentication/ResetPassword\";\nimport Autologin from \"../pages/Authentication/Autologin\";\nimport LayoutUnauthenticated from \"../layout/LayoutUnauthenticated\";\n\nexport const ROUTES_AUTHENTICATION: ApplicationRouteProps[] = [\n {\n path: \"/login\",\n element: (\n \n \n \n ),\n permissions: [\"*\"],\n type: \"auth\",\n },\n {\n path: \"/forgot-password\",\n element: (\n \n \n \n ),\n permissions: [\"*\"],\n type: \"auth\",\n },\n {\n path: \"/reset-password\",\n element: (\n \n \n \n ),\n permissions: [\"*\"],\n type: \"auth\",\n },\n {\n path: \"/autologin\",\n element: ,\n permissions: [\"*\"],\n type: \"public\",\n },\n];\n","import { ContentHeaderProps } from \"./interfaces\";\n\nconst ContentHeader = ({ title, children, modifierClass = \"\" }: ContentHeaderProps) => {\n return (\n
\n
{title}
\n {children &&
{children}
}\n
\n );\n};\n\nexport default ContentHeader;\n","import { useEffect } from \"react\";\n\n/**\n *\n * Hook that will add a class to the `body` HTML element\n * that will prevent the page from being scrolled.\n *\n * Example use case for this hook is when the `ModalAction` component\n * is mounted and rendered in the UI, that will add the class to the body element\n * and prevent it from being scrolled. And when the component gets unmounted,\n * the class is removed from the `body` element making the page scrollable again.\n *\n */\nexport function usePreventBodyScroll() {\n useEffect(() => {\n // Add the class that will be used to prevent scrolling in the body element\n document.body.classList.add(\"prevent-scroll\");\n\n // On component unmount, remove the class from the body\n return () => document.body.classList.remove(\"prevent-scroll\");\n }, []);\n}\n","import { RefObject, useEffect } from \"react\";\n\n/**\n *\n * Trigger component-related functionality when \"Escape\" key is pressed by the user.\n *\n * @param ref The referenced HTML element\n * @param callback The function to be triggered\n *\n */\nexport function useOnEscapeKey(ref: RefObject, callback: () => void) {\n // Add event listener when component mounts and remove it when it unmounts\n useEffect(() => {\n // Checks if the referenced element exists in the DOM,\n // and then checks if the referenced element is the currently focused one, or a parent of the focused element\n if (\n ref &&\n ref.current &&\n document.activeElement &&\n (document.activeElement === ref.current ||\n document.activeElement.contains(ref.current) ||\n ref.current.contains(document.activeElement))\n ) {\n // Add event listener to ref since it's focused\n ref.current.addEventListener(\"keyup\", event => handleEscapeKey(event), false);\n }\n\n return () => {\n ref.current?.removeEventListener(\"keyup\", handleEscapeKey, false);\n };\n }, [document.activeElement]);\n\n const handleEscapeKey = (event: KeyboardEvent) => {\n event.preventDefault();\n if (event.key === \"Escape\") callback();\n };\n}\n","// Assets\nimport { MdClose as CloseIcon } from \"react-icons/md\";\n\n// Interfaces\nimport { ModalProps } from \"./interfaces\";\n\n// Hooks\nimport { usePreventBodyScroll } from \"../../hooks/usePreventBodyScroll\";\nimport { useRef } from \"react\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport { motion } from \"framer-motion\";\nimport ModalBackdrop from \"./ModalBackdrop\";\n\nconst Modal = ({\n title,\n text = \"\",\n modifierClass = \"\",\n overlayModifierClass = \"\",\n children,\n testID = \"modal\",\n handleCloseModal,\n}: ModalProps) => {\n // Prevent the body of the page from scrolling when\n // the modal component is displayed in the UI.\n usePreventBodyScroll();\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n const modalRef = useRef(null);\n useOnEscapeKey(modalRef, handleCloseModal);\n\n return (\n <>\n \n\n \n \n\n
{title}
\n {text &&
{text}
}\n\n {children}\n \n \n );\n};\n\nexport default Modal;\n","import { CardProps } from \"./interfaces\";\n\nconst Card = ({ children, modifierClass = \"\" }: CardProps) => {\n return
{children}
;\n};\n\nexport default Card;\n","import Skeleton from \"react-loading-skeleton\";\nimport Card from \"../Card/Card\";\n\nconst TableSkeletonPlaceholder = ({ modifierClass = \"\" }: { modifierClass?: string }) => {\n return (\n \n \n \n \n {[...Array(4).keys()].map((index: number) => {\n return (\n \n );\n })}\n \n \n \n {[...Array(4).keys()].map((_, index: number) => {\n return (\n \n <>\n {[...Array(4).keys()].map(index => {\n return (\n \n \n \n );\n })}\n \n \n );\n })}\n \n
\n \n \n \n
\n
\n );\n};\n\nexport default TableSkeletonPlaceholder;\n","import { useEffect, useState } from \"react\";\nimport debounce from \"lodash/debounce\";\n\n/**\n * Custom hook used for extracting window's width after resize or on page load.\n *\n * @param delay Delay used for the debounce. Defaults to `700ms`\n * @param defaultWidth An optional argument that is used for pre-defining the\n * starting window width size. Defaults to `outerWidth` of the current window.\n *\n * @returns The outer's window width number value.\n *\n */\nfunction useWindowResize(delay = 700, defaultWidth = window.outerWidth) {\n const [windowWidth, setWindowWidth] = useState(defaultWidth);\n\n useEffect(() => {\n const handleResize = () => setWindowWidth(window.outerWidth);\n const debouncedHandleResize = debounce(handleResize, delay);\n\n window.addEventListener(\"resize\", debouncedHandleResize);\n\n // Clean up\n return () => {\n window.removeEventListener(\"resize\", debouncedHandleResize);\n };\n }, [delay]);\n\n return [windowWidth];\n}\n\nexport default useWindowResize;\n","import { useExtractSearchParameters } from \"../../hooks/useExtractSearchParameters\";\nimport { useEffect, useState } from \"react\";\nimport ReactPaginate from \"react-paginate\";\n\n// Assets\nimport { FaAngleRight as PaginationRightIcon } from \"react-icons/fa\";\nimport { FaAngleLeft as PaginationLeftIcon } from \"react-icons/fa\";\n\n// Interfaces\nimport { PaginationProps } from \"./interfaces\";\n\nconst Pagination = ({\n pageCount,\n currentPage = 0,\n containerClassName = \"pagination pagination--hide-prevnext\",\n shouldHandlePageRouteParameter = true,\n handlePageChange,\n}: PaginationProps) => {\n const [currentPageState, setCurrentPageState] = useState(currentPage);\n const [searchParams, setSearchParams] = useExtractSearchParameters();\n\n // Reflect any external updates to the current page state\n useEffect(() => {\n setCurrentPageState(currentPage);\n }, [currentPage]);\n\n // Handle page selection and search parameters updates\n const handlePagination = (selected: { selected: number }) => {\n // Trigger the received callback prop for handling page changes\n handlePageChange(selected);\n\n // Exit function if component shouldn't handle the search parameters\n if (!shouldHandlePageRouteParameter) return;\n\n // Update the state of the search parameters object to include the page\n // We intentionally increase + 1 to the selected page, so we can represent it\n // normally in the URL, because the pagination uses 0-based index.\n setSearchParams({ ...searchParams, page: selected.selected + 1 });\n };\n\n // Pre-select the received page based on received search parameter when component mounts\n useEffect(() => {\n if (searchParams.page && shouldHandlePageRouteParameter) {\n const pageNumber = parseInt(searchParams.page);\n\n // If invalid page number is received (e.g. NaN), exit function\n if (!pageNumber || pageNumber < 0) return;\n\n // Update the local state for the selected page and trigger received callback\n // We're decreasing the page number here (opposite from when we set the search parameter trough selection)\n // so it can correctly reflect the page state, because the package reads `page=0` as the `page=1`\n if (pageNumber > pageCount) {\n // If the received page number is higher than the total pages count, default to last page\n setCurrentPageState(pageCount - 1);\n handlePageChange({ selected: pageCount - 1 });\n } else {\n setCurrentPageState(pageNumber - 1);\n handlePageChange({ selected: pageNumber - 1 });\n }\n }\n\n // Reset the pagination to the first page if no page URL parameter was received and we're not at the first page,\n // which can be caused if using the 'Go Back' browser button to go back to the point\n // where the page was first visited, before interacting with the pagination\n if (!searchParams.page && currentPageState !== 0) {\n setCurrentPageState(0);\n handlePageChange({ selected: 0 });\n }\n }, [searchParams.page]);\n\n return (\n : null}\n nextLabel={currentPageState + 1 !== pageCount ? : null}\n breakLabel={\"...\"}\n breakClassName={\"pagination--break\"}\n pageCount={pageCount}\n marginPagesDisplayed={1}\n pageRangeDisplayed={3}\n onPageChange={handlePagination}\n containerClassName={containerClassName}\n activeClassName={\"active\"}\n forcePage={currentPageState}\n disableInitialCallback={true}\n />\n );\n};\n\nexport default Pagination;\n","// Components\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport Loader from \"../Loader/Loader\";\n\n// Interfaces\nimport { TableNoDataMessageProps } from \"./interfaces\";\n\nconst TableNoDataMessage = ({ message, isRefetching = false }: TableNoDataMessageProps) => {\n return (\n <>\n \n {!isRefetching ? (\n \n
\n {message}\n
\n \n ) : null}\n
\n\n \n {isRefetching ? (\n \n
\n Fetching data...\n
\n {isRefetching ? : null}\n \n ) : null}\n
\n \n );\n};\n\nexport default TableNoDataMessage;\n","// TanStack Table components and utilities\nimport {\n Column,\n ColumnPinningState,\n ExpandedState,\n flexRender,\n getCoreRowModel,\n getExpandedRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n PaginationState,\n Row,\n RowData,\n SortingState,\n useReactTable,\n} from \"@tanstack/react-table\";\n\n// Utilities & Hooks\nimport { useEffect, useState } from \"react\";\nimport { useExtractSearchParameters } from \"../../hooks/useExtractSearchParameters\";\nimport useWindowResize from \"../../hooks/useWindowResize\";\n\n// Components\nimport Loader from \"../Loader/Loader\";\nimport Pagination from \"../Pagination/Pagination\";\n\n// Interfaces\nimport { TableColumnMetaProperties, TableModifiedData, TableProps } from \"./interfaces\";\nimport { type CSSProperties } from \"react\";\n\n// Assets\nimport {\n FaSort as SortDefaultIcon,\n FaSortDown as SortDescendingIcon,\n FaSortUp as SortAscendingIcon,\n} from \"react-icons/fa\";\nimport TableNoDataMessage from \"./TableNoDataMessage\";\n\n// Extending the interface for the table meta field, used for conditional row styles\ndeclare module \"@tanstack/table-core\" {\n interface TableMeta {\n getRowStyles: (row: Row) => CSSProperties;\n }\n}\n\nconst Table = ({\n data,\n columns,\n isRefetching,\n noDataMessage = \"No data available\",\n paginationPageSize = 10,\n title = \"\",\n modifierClass = \"\",\n shouldHandlePageRouteParameter = true,\n shouldShowSummarizedData = false,\n handleExportPaginationState,\n handleExportData,\n}: TableProps) => {\n /*=======================\n SORTING HANDLING\n ========================*/\n const [sorting, setSorting] = useState([]);\n\n /*=======================\n PAGINATION HANDLING\n ========================*/\n const [pagination, setPagination] = useState({\n pageIndex: 0,\n pageSize: paginationPageSize,\n });\n\n // NOTE: Extraction of pagination state outside of the table component, used for slicing the extracted table data\n // TODO: Refactor extraction approach\n useEffect(() => {\n if (!handleExportPaginationState) return;\n\n handleExportPaginationState(pagination);\n }, [pagination]);\n\n useEffect(() => {\n const PAGINATION_UPDATE = { ...pagination, pageSize: paginationPageSize };\n setPagination(PAGINATION_UPDATE);\n }, [paginationPageSize]);\n\n /*==================================\n HANDLE TABLE'S PAGE AS \n ROUTE PARAMETER FOR DIRECT ACCESS\n ===================================*/\n const [searchParams, setSearchParams] = useExtractSearchParameters();\n\n /*==============================================\n MODIFIED DATA FOR DATA SUMMARIZATION FOOTER\n ===============================================*/\n const [modifiedData, setModifiedData] = useState({\n data: [],\n summarizedData: null,\n });\n\n // In case there should be a summarization footer\n // separate the main data & the footer data in the 'modifiedData' state\n useEffect(() => {\n setModifiedData({\n data: shouldShowSummarizedData ? data.slice(0, data.length - 1) : data,\n summarizedData: shouldShowSummarizedData ? data[data.length - 1] : null,\n });\n }, [data]);\n\n // Expanded rows state, supply a 'subRows' array in the data to make a row expandable\n const [expanded, setExpanded] = useState({});\n\n // Reset the expanded state upon data change\n useEffect(() => {\n setExpanded({});\n }, [data]);\n\n // To pin a sticky column, supply the 'isSticky:true' field in the ColumnDef and an unique column id\n // Note: Only sticky pinned right aligned columns supported for now\n // TODO: Extend the 'ColumnDef' interface for better typesafety\n const [columnPinning, setColumnPinning] = useState({\n left: [],\n right: [],\n });\n\n // Use the 'windowWidth' to 'unsticky' the columns on small screens\n const [windowWidth] = useWindowResize(300);\n\n useEffect(() => {\n if (windowWidth < 768) {\n setColumnPinning({ left: [], right: [] });\n } else {\n setColumnPinning({\n left: [],\n right: columns.filter((column: any) => column.isSticky).map((column: any) => column.id),\n });\n }\n }, [columns, windowWidth]);\n\n /*=======================\n TABLE INITIALIZATION\n ========================*/\n const table = useReactTable({\n data: modifiedData.data,\n columns,\n state: { sorting, pagination, expanded, columnPinning },\n meta: {\n // Apply conditional row styles here based on row state or data\n getRowStyles: (row: Row) => {\n // Custom coloring & border for expanded rows and custom border for main expanded row\n if (row.getCanExpand() || row.depth === 1) {\n return {\n borderLeft:\n row.getIsExpanded() || row.depth === 1\n ? \"3px solid #0166a7\"\n : \"3px solid transparent\",\n backgroundColor:\n row.depth === 1 ? \"#f7f7f7\" : row.getIsExpanded() ? \"#c3dbea\" : \"white\",\n fontWeight: row.getIsExpanded() ? \"700\" : \"\",\n };\n }\n\n return {};\n },\n },\n getCoreRowModel: getCoreRowModel(),\n onPaginationChange: setPagination,\n getPaginationRowModel: getPaginationRowModel(),\n onSortingChange: setSorting,\n getSortedRowModel: getSortedRowModel(),\n\n onExpandedChange: setExpanded,\n getSubRows: row => row.subRows,\n getExpandedRowModel: getExpandedRowModel(),\n paginateExpandedRows: false,\n\n onColumnPinningChange: setColumnPinning,\n });\n\n /*====================\n EXPORT TABLE DATA\n =====================*/\n useEffect(() => {\n // Exit if no handler is provided\n if (!handleExportData) return;\n\n const mappedTableData = table.getSortedRowModel().rows.map(entity => {\n return entity.original;\n });\n\n // Append the removed last element in case the\n // 'shouldShowSummarizedData' prop is recieved before exporting data\n if (shouldShowSummarizedData) {\n mappedTableData.push(data[data.length - 1]);\n }\n // Call handler from prop with current table data sorting\n handleExportData(mappedTableData);\n }, [table.getSortedRowModel()]);\n\n // Handle the table pagination and \"page\" URL parameter updates\n // in order to stay consistent and always use the correct value\n useEffect(() => {\n const totalTablePagesCount: number = table.getPageCount();\n\n // Early exit if there are no pages available for the table yet\n if (!totalTablePagesCount) return;\n\n // Early exit if theres no \"page\" route parameter or if pagination shouldn't be handled trough it\n if (!shouldHandlePageRouteParameter || !searchParams.page) return;\n\n // Parse the received \"page\" parameter value to a number\n const parsedPageParameter = parseInt(searchParams.page);\n\n // If the \"page\" parameter is not a valid number value or it is a negative number\n // then update the parameter to \"page=1\" which will also reset the internal table page index\n if (!parsedPageParameter || parsedPageParameter < 0) {\n table.resetPageIndex();\n setSearchParams({ ...searchParams, page: 1 }, { replace: true });\n return;\n }\n\n // We normalize the received page parameter by subtracting one page\n // so it can correctly match the pagination component and the table page, as the pagination uses 0-based index\n const normalizedPageParameter: number = parsedPageParameter - 1;\n\n // Do not update the table's state if the received page parameter\n // matches the currently opened page in the table\n if (normalizedPageParameter === table.getState().pagination.pageIndex) return;\n\n // If the \"page\" URL parameter is higher in value than the total number of table pages - default to last page\n // Otherwise update the table to the page that was received as URL parameter\n let updatedPageParameter = parsedPageParameter;\n if (normalizedPageParameter > totalTablePagesCount) {\n table.setPageIndex(totalTablePagesCount - 1);\n updatedPageParameter = totalTablePagesCount;\n } else {\n table.setPageIndex(parsedPageParameter - 1);\n }\n\n // Update the used \"page\" route parameter\n setSearchParams({ ...searchParams, page: updatedPageParameter }, { replace: true });\n }, [table.getPageCount()]);\n\n // Handler for overwriting original styling & applying customs styles for sticky positioned columns\n const getPinnedStickyStyle = (column: Column): CSSProperties => {\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n return {\n boxShadow: isLastLeftPinnedColumn\n ? \"-2px 0 3px -2px gray inset\"\n : isFirstRightPinnedColumn\n ? \"2px 0 2px -2px gray inset\"\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n // NOTE: The minus 20px below is to compensate for the table-wrapper css class padding\n right: isPinned === \"right\" ? `${column.getAfter(\"right\") - 20}px` : undefined,\n position: isPinned ? \"sticky\" : \"relative\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : 0,\n };\n };\n\n return (\n <>\n
\n {title &&

{title}

}\n\n {data.length > 0 && isRefetching ? (\n \n ) : null}\n\n \n \n {table.getHeaderGroups().map(headerGroup => (\n \n {headerGroup.headers.map(header => (\n \n \n {flexRender(header.column.columnDef.header, header.getContext())}\n\n {header.column.getCanSort() && (\n
\n {{\n asc: ,\n desc: ,\n }[header.column.getIsSorted() as string] ?? }\n
\n )}\n \n \n ))}\n
\n ))}\n \n\n {data.length > 0 ? (\n <>\n \n {table.getRowModel().rows.map(row => (\n \n {row.getVisibleCells().map(cell => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n \n ))}\n \n\n {shouldShowSummarizedData && modifiedData.summarizedData ? (\n \n {table.getFooterGroups().map(footerGroup => (\n \n {Object.keys(modifiedData.summarizedData).map((_footerKey, index) => {\n return (\n \n );\n })}\n \n ))}\n \n ) : null}\n \n ) : null}\n
\n {modifiedData.summarizedData[footerGroup.headers[index].id]}\n
\n\n {data.length === 0 ? (\n \n ) : null}\n
\n\n {table.getPageCount() > 1 && (\n
\n table.setPageIndex(selected)}\n shouldHandlePageRouteParameter={shouldHandlePageRouteParameter}\n />\n
\n )}\n \n );\n};\n\nexport default Table;\n","import { UserAccessPermissions } from \"../interfaces/permissions\";\nimport { useAuth } from \"../providers/auth-context\";\n\n/**\n *\n * Utility function that reads the list of the logged-in user's permissions\n * and checks if the permission(s) received as parameter exists in that list\n *\n * @param expectedPermissions An array of strings representing a set of pre-defined\n * permissions that the user needs to have in order to be granted access.\n *\n * @param requireAllExpectedPermissions A boolean indicating if the user must have all of\n * the expected permissions that were passed trough the `expectedPermissions` argument.\n *\n * The `expectedPermissions` defaults to `*` which is used as a wildcard for components\n * and pages that do not have any permission-based restrictions.\n *\n * If the user is a \"Super Admin\" then access is granted by default.\n *\n * @returns A boolean indicating if the user has the necessary permission(s) to\n * view or access the component or the page.\n *\n */\nexport default function handlePermissionCheck(\n expectedPermissions: UserAccessPermissions[] = [\"*\"],\n requireAllExpectedPermissions: boolean = false,\n): boolean {\n const { user } = useAuth();\n\n // If the user has a \"Super Admin\" role, then everything is permitted\n if (user.role === \"Super Admin\") return true;\n\n // If an \"*\" was provided as the \"specificPermission\" parameter,\n // then that means that the component / route does not have any\n // permission-based access associated with it\n if (expectedPermissions.includes(\"*\")) return true;\n\n // Make sure the user has all of the expected permissions if all are required.\n // Otherwise, make sure that the user has at least one of the expected permissions before granting access.\n const arrayMethodToBeCalled: \"every\" | \"some\" = requireAllExpectedPermissions ? \"every\" : \"some\";\n\n // If the user does not have a \"Super Admin\" role and the wildcard (*) permission is not part of the expected permissions,\n // then check if the expected permissions passed as argument exist in the list of received permissions of the user from the API\n const isAccessGranted: boolean = expectedPermissions[arrayMethodToBeCalled](\n expectedPermission => {\n return user.permissions.includes(expectedPermission);\n },\n );\n\n return isAccessGranted;\n}\n","import { UserAccessPermissions } from \"../../interfaces/permissions\";\nimport handlePermissionCheck from \"../../utilities/handlePermissionCheck\";\n\ninterface PermissionCheckComponenteWrapperProps {\n children: React.ReactNode;\n permissions: UserAccessPermissions[];\n\n /** Requires that the user has `all` of the expected permissions before granting access */\n requireAllPermissions?: boolean;\n}\n\n/**\n *\n * Component that checks if the user has access and is allowed to see and interact\n * with a specific component that is supposed to be rendered on the page.\n *\n * This is being used in each individual page and/or component, to control user interactions\n * based on the permissions that the user has.\n *\n */\nconst PermissionCheckComponentWrapper = ({\n children,\n permissions,\n requireAllPermissions = false,\n}: PermissionCheckComponenteWrapperProps) => {\n return <>{handlePermissionCheck(permissions, requireAllPermissions) ? children : null};\n};\n\nexport default PermissionCheckComponentWrapper;\n","// Utilities & Hooks\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"react-toastify\";\nimport fetchHandler from \"../fetchHandler\";\n\n// Interfaces\nimport {\n UsersListResponseFields,\n UserSpecificResponseFields,\n UsersEditRequestFields,\n UsersAddFormikFields,\n UsersResponseFields,\n} from \"./interfaces\";\nimport { useAuth } from \"../../providers/auth-context\";\n\n/**\n *\n * Get all users that exist within the currently selected\n * (active) company.\n *\n */\nexport function useUsersGet() {\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useQuery({\n queryKey: [\"users-active-company\", companySlug],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `company/${companySlug}/users`)) as UsersListResponseFields;\n },\n enabled: !!companySlug,\n meta: {\n errorMessage: \"Failed getting list of existing users\",\n },\n });\n}\n\n/**\n *\n * Get a list of all the users that exist in the currently selected\n * active company. This endpoint has no specific permission requirements\n * and will be used for generating the lists in dropdown menues across the application\n * (e.g. when scheduling a video meeting, sending an application trough email, etc.)\n *\n *\n */\nexport function useUsersGetList() {\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useQuery({\n queryKey: [\"users-list\", companySlug],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `company/${companySlug}/users/list`,\n )) as UsersListResponseFields;\n },\n enabled: !!companySlug,\n meta: {\n errorMessage: \"Failed getting list of existing users\",\n },\n });\n}\n\n/**\n *\n * Get the details of the targeted user that belongs to the currently active company\n *\n * TODO: Read the `companySlug` from within the hook.\n *\n * @param companySlug The logged in user's \"active_company\" slug value\n * @param userID The ID of the targeted user whose details we need to get\n */\nexport function useUsersGetSpecific(\n companySlug: string | null | undefined,\n userID: string | undefined,\n) {\n return useQuery({\n queryKey: [\"users-active-company-user\", companySlug, userID],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `company/${companySlug}/users/${userID}`,\n )) as UserSpecificResponseFields;\n },\n enabled: !!companySlug && !!userID,\n meta: {\n errorMessage: `Failed getting details for targeted user with ID ${userID}`,\n },\n });\n}\n\n/**\n *\n * Add a new user to the company\n *\n */\nexport function useUsersAddToCompany() {\n const queryClient = useQueryClient();\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useMutation({\n mutationFn: ({ email, group_id }: UsersAddFormikFields) => {\n return fetchHandler(\"POST\", `company/${companySlug}/users`, { email, group_id });\n },\n onSuccess: () => toast.success(\"Successfully added user to company!\"),\n onError: error => error,\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"users-active-company\", companySlug] });\n queryClient.invalidateQueries({ queryKey: [\"users-list\", companySlug] });\n },\n });\n}\n\n/**\n *\n * Update the targeted user's details\n *\n */\nexport function useUsersEditSpecific() {\n const queryClient = useQueryClient();\n\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useMutation({\n mutationFn: ({ userDetails, userId }: UsersEditRequestFields) => {\n return fetchHandler(\"PUT\", `company/${companySlug}/users/${userId}`, userDetails);\n },\n onMutate: ({ userDetails, userId }) => {\n // Get the data stored in react-query's cache for the user that is being edited\n const editedUserDetails = queryClient.getQueryData([\n \"users-active-company-user\",\n companySlug,\n userId,\n ]);\n\n // Optimistically update the details of the user that is being edited\n queryClient.setQueryData([\"users-active-company-user\", companySlug, userId], userDetails);\n\n // Show success notification\n toast.success(\"User details updated successfully!\", {\n toastId: \"users-edit-user\",\n });\n\n // Return the cached data (before mutation) in the context provider so we can fallback to it\n return editedUserDetails;\n },\n onError: (error, { userId }, editedUserDetails) => {\n // Dismiss the success notification from the UI first\n toast.dismiss(\"users-edit-user\");\n\n queryClient.setQueryData(\n [\"users-active-company-user\", companySlug, userId],\n editedUserDetails,\n );\n return error;\n },\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"users-active-company\"] });\n },\n });\n}\n\n/**\n *\n * Delete the targeted user from the currently active company\n *\n */\nexport function useUsersRemoveSpecific() {\n const queryClient = useQueryClient();\n\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useMutation({\n mutationFn: async (userId: number) => {\n return await fetchHandler(\"DELETE\", `company/${companySlug}/users/${userId}`);\n },\n onMutate: userId => {\n const previousUsersState = queryClient.getQueryData([\n \"users-active-company\",\n companySlug,\n ]) as UsersListResponseFields;\n\n // Filter out the cached data, removing the targeted user\n queryClient.setQueryData(\n [\"users-active-company\", companySlug],\n users => {\n // Do not update the cached data if the \"users\" data is undefined\n if (!users) return;\n\n return users.filter((user: UsersResponseFields) => user.id !== userId);\n },\n );\n\n // Show notification in the UI\n toast.success(\"User removed successfully!\", {\n toastId: \"users-remove-user\",\n });\n\n return previousUsersState;\n },\n onError: (error, _variables, previousUsersState) => {\n // Dismiss the success notification from the UI first\n toast.dismiss(\"users-remove-user\");\n\n queryClient.setQueryData([\"users-active-company\", companySlug], previousUsersState);\n return error;\n },\n });\n}\n","import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"react-toastify\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport fetchHandler from \"../fetchHandler\";\nimport {\n InvitationCompleteRegistrationPayloadFields,\n InvitationInfoResponseFields,\n InvitationStatusInfoResponseFields,\n} from \"./interfaces\";\nimport { UsersListResponseFields } from \"../Users/interfaces\";\n\n/**\n *\n * Get the invitation's info detail\n *\n * @param invitationID The encrypted string representing the invitation's ID\n *\n */\nexport function useInvitationGetInfo(invitationID: string | undefined) {\n return useQuery({\n queryKey: [\"invitation-info\", invitationID],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `auth/invitation/${invitationID}`,\n )) as InvitationInfoResponseFields;\n },\n enabled: !!invitationID,\n retryDelay: 0,\n retry: 1,\n meta: {\n errorMessage: \"Failed getting invitation's info details \",\n },\n });\n}\n\n/**\n *\n * Get the status of the received invitation, whether the\n * user already exists in the system or not, and redirect based on\n * that response\n *\n * @param invitationID The encrypted string representing the invitation's ID\n *\n */\nexport function useInvitationGetStatus(invitationID: string | undefined) {\n return useQuery({\n queryKey: [\"invitation-status-info\", invitationID],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `auth/invitation/${invitationID}/status`,\n )) as InvitationStatusInfoResponseFields;\n },\n enabled: !!invitationID,\n retryDelay: 0,\n retry: 1,\n gcTime: 0,\n meta: {\n errorMessage: \"Fauled getting the invitation status\",\n },\n });\n}\n\n/**\n *\n * Send a request accepting the received invitation\n *\n */\nexport function useInvitationAccept() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (invitationID: string) => {\n return await fetchHandler(\"POST\", `auth/invitation/${invitationID}/join-company`);\n },\n onError: error => error,\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"companies\"] });\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n },\n });\n}\n\n/**\n *\n * Complete the registration process after the user has\n * been invited to join a specific company\n *\n */\nexport function useInvitationCompleteRegistration() {\n return useMutation({\n mutationFn: async (registrationDetails: InvitationCompleteRegistrationPayloadFields) => {\n return await fetchHandler(\n \"POST\",\n `auth/invitation/${registrationDetails.invitationID}/join-company/new-user`,\n {\n first_name: registrationDetails.first_name,\n last_name: registrationDetails.last_name,\n password: registrationDetails.password,\n password_confirmation: registrationDetails.password_confirmation,\n },\n );\n },\n onError: error => error,\n });\n}\n\n/**\n *\n * Resend an invitation to a user who is in a \"pending\" state\n *\n */\nexport function useInvitationResend() {\n // Read the currently active company's slug\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useMutation({\n mutationFn: async (userID: number) => {\n return await fetchHandler(\"POST\", `company/${companySlug}/invitation/${userID}/resend`);\n },\n onSuccess: () => toast.success(\"Invitation email has been resent!\"),\n onError: error => error,\n });\n}\n\n/**\n *\n * Delete an invitation that was previously sent to a user\n *\n */\nexport function useInvitationDelete() {\n const queryClient = useQueryClient();\n\n // Read the currently active company's slug\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useMutation({\n mutationFn: async (userID: number) => {\n return await fetchHandler(\"DELETE\", `company/${companySlug}/invitation/${userID}`);\n },\n onMutate: userID => {\n // Get the cached data for listed users in the active company\n const previousUsersState = queryClient.getQueryData([\n \"users-active-company\",\n companySlug,\n ]) as UsersListResponseFields;\n\n // Filter out the cached data, removing the targeted user\n queryClient.setQueryData(\n [\"users-active-company\", companySlug],\n users => {\n // Do not update the cached data if the \"users\" data is undefined\n if (!users) return;\n\n return users.filter(user => user.id !== userID);\n },\n );\n\n // Show notification in the UI\n toast.success(\"Invitation has been deleted successfully!\", {\n toastId: \"invitation-delete\",\n });\n\n return previousUsersState;\n },\n onError: (error, _variables, previousUsersState) => {\n // Dismiss success notification from the UI first\n toast.dismiss(\"invitation-delete\");\n\n // Fallback to state before triggering the API call, if the request was erroneous\n queryClient.setQueryData([\"users-active-company\", companySlug], previousUsersState);\n return error;\n },\n });\n}\n","type ExpectedNameFields = Record<\"first_name\" | \"middle_name\" | \"last_name\", string | null>;\ntype DataObject = ExpectedNameFields | Record | null;\n\n/**\n *\n * Utility function for joining the `first` and `last` names\n * of the user/applicant, into a single `fullname` string.\n *\n * @param dataObject The data object that is supposed to contain the\n * `first_name` and `last_name` values.\n *\n * @returns A string representing the fullname of the user/applicant\n */\nexport default function handleFullnameCombination(dataObject: DataObject): string {\n // If no object, or object with no entries, is received, then return default value\n if (!dataObject || !Object.entries(dataObject)) return \"N/A\";\n\n // Check if the received data object has the name properties\n if (\n !Object.prototype.hasOwnProperty.call(dataObject, \"first_name\") ||\n !Object.prototype.hasOwnProperty.call(dataObject, \"last_name\")\n )\n return \"N/A\";\n\n // If the values exist, combine them into a full name and return that as a string\n const { first_name, last_name } = dataObject;\n\n let fullname: string = `${first_name}`;\n\n // Append last name if present and if not received as 'N/A' from server\n if (last_name && last_name.toLowerCase() !== \"n/a\") fullname += ` ${last_name}`;\n\n return fullname;\n}\n","// Components\nimport { AnimatePresence } from \"framer-motion\";\nimport { Link } from \"react-router\";\nimport Button from \"../../../components/Button/Button\";\nimport Modal from \"../../../components/Modal/Modal\";\nimport TableSkeletonPlaceholder from \"../../../components/SkeletonPlaceholders/TableSkeletonPlaceholder\";\nimport Table from \"../../../components/Table/Table\";\nimport PermissionCheckComponentWrapper from \"../../../components/Wrappers/PermissionCheckComponentWrapper\";\n\n// Interfaces\nimport { UsersResponseFields } from \"../../../api/Users/interfaces\";\nimport { UsersListMappedFields } from \"./interfaces\";\n\n// Utilities & Hooks\nimport { matchSorter } from \"match-sorter\";\nimport { createColumnHelper } from \"@tanstack/react-table\";\nimport { format, parseJSON } from \"date-fns\";\nimport { useMemo, useState } from \"react\";\nimport { useUsersRemoveSpecific, useUsersGet } from \"../../../api/Users/Users\";\nimport { useAuth } from \"../../../providers/auth-context\";\nimport { useInvitationDelete, useInvitationResend } from \"../../../api/Invitation/Invitation\";\nimport useErrorReporting from \"../../../hooks/useErrorReporting\";\nimport handleFullnameCombination from \"../../../utilities/strings/handleFullnameCombination\";\n\nconst UsersTable = ({ searchedUsers }: { searchedUsers: string }) => {\n const errorReporting = useErrorReporting();\n\n /*=========================\n GENERATE TABLE COLUMNS\n ==========================*/\n const COLUMN_HELPER = createColumnHelper();\n const USERS_TABLE_COLUMNS = [\n COLUMN_HELPER.accessor(\"name\", {\n header: () => Name,\n cell: data => {data.getValue() || \"N/A\"},\n size: 100,\n }),\n COLUMN_HELPER.accessor(\"title\", {\n header: () => Title,\n size: 50,\n cell: data => {data.getValue() || \"N/A\"},\n }),\n COLUMN_HELPER.accessor(\"email\", {\n header: () => E-mail,\n cell: data => {data.getValue() || \"N/A\"},\n size: 90,\n }),\n COLUMN_HELPER.accessor(\"group\", {\n header: () => User Group,\n cell: data => {data.getValue() || \"N/A\"},\n size: 30,\n }),\n COLUMN_HELPER.accessor(\"last_login\", {\n header: () => Last Login,\n cell: data => (\n \n {data.getValue()\n ? format(parseJSON(data.getValue().toString()), \"dd MMM yyyy HH:mm\")\n : \"N/A\"}\n \n ),\n size: 50,\n }),\n COLUMN_HELPER.accessor(\"status\", {\n header: () => Status,\n cell: data => {data.getValue()},\n size: 30,\n }),\n COLUMN_HELPER.accessor(\"id\", {\n header: () => Action,\n enableSorting: false,\n meta: {\n headerModifierClass: \"justify-content-end\",\n },\n cell: data => (\n
\n {data.row.original.status === \"active\" ? (\n <>\n \n \n View Messages\n \n \n\n {/* If the user does not belong to any group (Account Manager or Super Admin, do not show buttons) */}\n {data.row.original.group ? (\n <>\n \n \n Edit\n \n \n\n \n {\n handleOpenActionModal(data.row.original, \"delete-user\");\n }}\n >\n Remove\n \n \n \n ) : null}\n \n ) : (\n <>\n handleInvitationResend(data.getValue())}\n isLoading={resendInvitationID === data.getValue() && invitationResend.isPending}\n isDisabled={resendInvitationID === data.getValue() && invitationResend.isPending}\n >\n Resend Invitation\n \n\n \n {\n handleOpenActionModal(data.row.original, \"delete-invitation\");\n }}\n >\n Delete\n \n \n \n )}\n
\n ),\n }),\n ];\n\n /*=========================\n GET ALL USERS FROM\n THE CURRENTLY ACTIVE COMPANY\n ==========================*/\n const { user } = useAuth();\n\n // Read the currently active company value from the authentication context\n // or default to a specified value if it doesn't exists\n const { data, isPending, isFetching } = useUsersGet();\n\n const users = useMemo(() => {\n // Exit function and return default value if there's no data\n if (!data || !data.length || isPending) return [];\n\n let mappedUsers: UsersListMappedFields[] = data.map((user: UsersResponseFields) => {\n return {\n id: user.id,\n name: handleFullnameCombination(user),\n title: user.title,\n email: user.email || \"N/A\",\n last_login: user.last_login || 0,\n group: user.group ?? \"\",\n status: user.status,\n };\n });\n\n // When user searches, filter out the list of users displayed in the table\n if (searchedUsers) {\n mappedUsers = matchSorter(mappedUsers, searchedUsers, {\n keys: [\"name\", \"email\", { key: \"group\", threshold: matchSorter.rankings.EQUAL }],\n threshold: matchSorter.rankings.CONTAINS,\n });\n }\n\n return mappedUsers;\n }, [data, searchedUsers]);\n\n /*=========================\n MODALS\n ==========================*/\n const [targetedUser, setTargetedUser] = useState(null);\n const [isUserDeleteModalOpen, setIsUserDeleteModalOpen] = useState(false);\n const [isInvitationDeleteModalOpen, setIsInvitationDeleteModalOpen] = useState(false);\n\n const handleOpenActionModal = (\n user: UsersListMappedFields,\n modal: \"delete-user\" | \"delete-invitation\",\n ) => {\n setTargetedUser(user);\n\n if (modal === \"delete-user\") {\n setIsUserDeleteModalOpen(true);\n } else {\n setIsInvitationDeleteModalOpen(true);\n }\n };\n\n const handleResetModalState = () => {\n setTargetedUser(null);\n setIsUserDeleteModalOpen(false);\n setIsInvitationDeleteModalOpen(false);\n };\n\n /*====================================\n DELETE THE TARGETED USER\n FROM THE CURRENTLY ACTIVE COMPANY\n =====================================*/\n const userRemove = useUsersRemoveSpecific();\n\n const handleUserRemove = async () => {\n // Exit the function if there's no targeted user\n // or a valid active company's \"slug\" value available yet\n if (targetedUser === null || !user.active_company.slug) return;\n\n try {\n // Close the modal and reset the targeted user's data\n handleResetModalState();\n\n await userRemove.mutateAsync(targetedUser.id);\n } catch (error) {\n errorReporting(\"Failed deleting user\", error, {\n user_id: targetedUser.id,\n company_slug: user.active_company.slug,\n });\n }\n };\n\n /*======================================\n USER INVITATIONS\n\n Actions can be triggered only\n if the user is in a \"pending\" state\n ======================================*/\n const [resendInvitationID, setResendInvitationID] = useState(null);\n\n const invitationResend = useInvitationResend();\n const invitationDelete = useInvitationDelete();\n\n const handleInvitationResend = async (userID: number) => {\n // ID to be used to control the loading & disabled state of the\n // \"Resend Invitation\" button states, so only the button which we clicked will be affected\n setResendInvitationID(userID);\n\n try {\n await invitationResend.mutateAsync(userID);\n\n // Reset the selected invitation ID\n setResendInvitationID(null);\n\n // Close the modal after the invitaion was successfully deleted\n handleResetModalState();\n } catch (error) {\n errorReporting(\"Failed resending user invitation\", error, { user_id: userID });\n }\n };\n\n const handleInvitationDelete = async () => {\n // Exit the function if there's no targeted user\n // or a valid active company's \"slug\" value available yet\n if (targetedUser === null || !user.active_company.slug) return;\n\n try {\n // Close the modal and reset the targeted user's data\n handleResetModalState();\n\n await invitationDelete.mutateAsync(targetedUser.id);\n } catch (error) {\n errorReporting(\"Failed deleting user invitation\", error, { user_id: targetedUser.id });\n }\n };\n\n return (\n <>\n {isPending ? (\n \n ) : (\n \n )}\n\n \n {isUserDeleteModalOpen && (\n \n This action will delete {targetedUser?.name}{\" \"}\n from the company. This action is{\" \"}\n irreversible. Do you want to continue?\n \n }\n modifierClass=\"modal--md modal--fixated\"\n handleCloseModal={handleResetModalState}\n >\n
\n \n\n \n Yes, Remove User\n \n
\n \n )}\n
\n\n \n {isInvitationDeleteModalOpen && (\n \n This action will delete the invitation that was sent to{\" \"}\n {targetedUser?.email} .This action is{\" \"}\n irreversible. Do you want to continue?\n \n }\n modifierClass=\"modal--md modal--fixated\"\n handleCloseModal={handleResetModalState}\n >\n
\n \n\n \n Yes, Delete Invitation\n \n
\n \n )}\n
\n \n );\n};\n\nexport default UsersTable;\n","import { useState, useEffect } from \"react\";\n\n/**\n * Hook for debouncing (delaying) a passed value\n * @param value The updated value that is to be delayed\n * @param delay The amount of delay to apply, expressed in milliseconds\n * @returns The updated value returned after the delay\n */\nexport default function useDebounce(value: string | number, delay: number = 500) {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n // Set the debounced value after given delay\n const handler = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n // Clear the debounce / timeout\n return () => clearTimeout(handler);\n }, [value]);\n\n return debouncedValue;\n}\n","// Hooks\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { useExtractSearchParameters } from \"../../hooks/useExtractSearchParameters\";\nimport useDebounce from \"../../hooks/useDebounce\";\n\n// Interfaces\nimport { InputFieldSearchProps } from \"./interfaces\";\n\n// Assets\nimport { BiSearchAlt as SearchIcon } from \"react-icons/bi\";\n\nconst InputFieldSearch: React.FC = ({\n modifierClass = \"\",\n placeholder,\n handleOnSearch,\n predefinedSearchValue = \"\",\n size = \"full\",\n disabled = false,\n}) => {\n const [searchParams, setSearchParams] = useExtractSearchParameters();\n\n /*=================================\n HANDLE SEARCHED VALUES\n =================================*/\n const [searchedValue, setSearchedValue] = useState(() => {\n return searchParams.search || predefinedSearchValue;\n });\n const [previousSearchedValue, setPreviousSearchedValue] = useState(\"\");\n const debouncedSearch: string = useDebounce(searchedValue);\n\n const handleSearchValue = (event: React.ChangeEvent) => {\n if (disabled) return;\n\n setSearchedValue(event.target.value);\n };\n\n // Anytime the debounced searched value is updated,\n // trigger the received handler for searching the data\n useEffect(() => {\n // Set the search parameters with the latest searched value\n if (debouncedSearch) {\n setSearchParams(\n {\n ...searchParams,\n search: debouncedSearch,\n\n // Reset pagination parameter (if it was previously present)\n ...(searchParams.page && { page: 1 }),\n },\n { replace: true },\n );\n } else {\n delete searchParams.search;\n setSearchParams(searchParams, { replace: true });\n }\n\n // Trim the searched value before triggering the callback\n const trimmedSearch: string = debouncedSearch.trim();\n\n // Trigger received callback\n handleOnSearch(trimmedSearch);\n }, [debouncedSearch]);\n\n /*=================================\n CLEAR SEARCHED VALUE ON \n 'SEARCH' URL PARAMETER CHANGE\n =================================*/\n useEffect(() => {\n const { search } = searchParams;\n\n // If a 'search' URL parameter is present, update the previously searched state\n if (search) setPreviousSearchedValue(search);\n\n // If a 'search' URL parameter is not present, but we have a previously searched value in the input field, clear it\n if (!search && previousSearchedValue) {\n setSearchedValue(\"\");\n setPreviousSearchedValue(\"\");\n }\n }, [searchParams]);\n\n /*=================================\n FOCUS ON INPUT FIELD WHEN\n CLICKED ON SEARCH ICON\n =================================*/\n const searchRef = useRef(null);\n const handleFocusOnInput = () => {\n if (!searchRef.current || disabled) return;\n searchRef.current.focus();\n };\n\n return (\n
\n
\n \n ) => handleSearchValue(event)}\n onBlur={event => setSearchedValue(event.target.value.trim())}\n disabled={disabled}\n />\n
\n
\n );\n};\nexport default InputFieldSearch;\n","// Hooks\nimport { useState } from \"react\";\n\n// Components\nimport { Link } from \"react-router\";\nimport ContentHeader from \"../../../components/Content/ContentHeader\";\nimport UsersTable from \"./UsersTable\";\nimport PermissionCheckComponentWrapper from \"../../../components/Wrappers/PermissionCheckComponentWrapper\";\nimport InputFieldSearch from \"../../../components/Inputs/InputFieldSearch\";\n\nconst Users = () => {\n /*==========================\n SEARCH TROUGH THE LIST\n OF USERS IN THE TABLE\n ===========================*/\n const [searchedUsers, setSearchedUsers] = useState(\"\");\n\n return (\n
\n \n \n Add User\n \n }\n modifierClass=\"content__header--no-underline mb--10\"\n >\n setSearchedUsers(search)}\n />\n \n \n\n \n
\n );\n};\n\nexport default Users;\n","import * as Yup from \"yup\";\nimport { SCHEMAS_PASSWORD_MESSAGE } from \"./constants\";\n\n// SCHEMA REGEX\nimport { PASSWORD_REGEX_PATTERN } from \"./regexes\";\n\n/*========================\n USER: PROFILE SETTINGS\n=========================*/\nexport const USER_PROFILE_SETTINGS_UPDATE = Yup.object().shape({\n current_password: Yup.string()\n .required(\"Please enter your current password\")\n .matches(PASSWORD_REGEX_PATTERN, SCHEMAS_PASSWORD_MESSAGE)\n .min(8, SCHEMAS_PASSWORD_MESSAGE)\n .max(50, \"Maximum of 50 characters allowed!\"),\n new_password: Yup.string()\n .required(\"Please enter a new password\")\n .min(8, \"Your password is too short.\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .matches(PASSWORD_REGEX_PATTERN, SCHEMAS_PASSWORD_MESSAGE),\n new_password_confirmation: Yup.string()\n .required(\"Please confirm the password\")\n .oneOf([Yup.ref(\"new_password\")], \"Your passwords do not match.\")\n .required(\"Please confirm the password\")\n .max(50, \"Maximum of 50 characters allowed!\"),\n});\n","import { useEffect, useState } from \"react\";\nimport { useNavigate } from \"react-router\";\n\n/**\n * Custom utility function that handles back navigation\n *\n * This function is used as a failsafe against empty session history back navigation\n * If the session history contains items from the same domain, we use the\n * native 'navigate(-1)', else we navigate to the provided fallback route\n *\n * @returns 'handleNavigateBack' - The navigation handler that accepts a fallback route as parameters\n *\n */\nexport const useBackNavigation = () => {\n const navigate = useNavigate();\n\n const [referrerIsSameDomain, setReferrerIsSameDomain] = useState();\n\n useEffect(() => {\n // If the user navigated to the current page through the app OR it's just a page refresh the document.referrer will be empty\n // We use this to determine whether we should use the native 'navigate(-1)' or use the fallback route\n if (!document.referrer || new URL(document.referrer).hostname === window.location.hostname) {\n setReferrerIsSameDomain(true);\n } else {\n setReferrerIsSameDomain(false);\n }\n }, []);\n\n // The handler checks whether there are previous elements in the history stack AND whether the\n // referrer is from the same domain, based on this we chose whether we go back the specified number of pages\n // which defaults to 1 previous page or the fallback route\n const handleNavigateBack = (fallback: string) => {\n if (window.history.state && window.history.state.idx > 1 && referrerIsSameDomain) {\n navigate(-1);\n } else {\n navigate(fallback);\n }\n };\n\n return handleNavigateBack;\n};\n","// Utilities & hooks\nimport { useUserUpdateProfileSettings } from \"../../api/User/User\";\nimport { USER_PROFILE_SETTINGS_UPDATE } from \"../../schemas/UserSchemas\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport { useBackNavigation } from \"../../hooks/useBackNavigation\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport { Formik, Form, Field } from \"formik\";\nimport Card from \"../../components/Card/Card\";\nimport Button from \"../../components/Button/Button\";\nimport ContentHeader from \"../../components/Content/ContentHeader\";\nimport Skeleton from \"react-loading-skeleton\";\nimport FormPasswordInput from \"../../components/Form/FormPasswordInput\";\n\n// Interfaces\nimport { UserUpdateProfileSettingsPayload } from \"../../api/User/interfaces\";\n\nconst ProfileSettings = () => {\n const { user } = useAuth();\n const errorReporting = useErrorReporting();\n\n /*==============================\n UPDATE SETTINGS\n ===============================*/\n const updateProfileSettings = useUserUpdateProfileSettings();\n\n const handleUpdateProfileSettings = async (values: UserUpdateProfileSettingsPayload) => {\n try {\n await updateProfileSettings.mutateAsync(values);\n } catch (error) {\n errorReporting(\"Failed updating profile settings\", error, { ...values });\n }\n };\n\n /*==============================\n CUSTOM BACK NAVIGATION\n ===============================*/\n const handleNavigateBack = useBackNavigation();\n\n return (\n
\n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
NameEmail
\n {!user.first_name ? (\n \n ) : (\n <>\n {user.first_name} {user.last_name}\n \n )}\n \n {!user.email ? : user.email}\n
\n
\n\n

\n Did you know?\n Security experts agree you should change your password a minimum of every 90 days.\n

\n\n \n
\n \n\n \n\n \n\n
\n {user.active_company.slug ? (\n handleNavigateBack(\"/applications/\")}\n >\n Cancel\n \n ) : null}\n\n \n Submit\n \n
\n \n \n
\n
\n );\n};\nexport default ProfileSettings;\n","import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport fetchHandler from \"../fetchHandler\";\nimport { ProductTourResponseFields } from \"./interfaces\";\n\n/**\n *\n * Mark the provided tour as finished in the user object\n *\n */\nexport const useMarkFinishTour = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (tourID: number) => {\n return await fetchHandler(\"POST\", `product-tours/${tourID}/users`);\n },\n onError: error => error,\n onSettled: () => queryClient.invalidateQueries({ queryKey: [\"user-profile\"] }),\n });\n};\n\n/**\n *\n * Get all availbale tours for the current user\n *\n */\nexport const useGetAvailableTours = () => {\n return useQuery({\n queryKey: [\"product-tours\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"product-tours\")) as ProductTourResponseFields[];\n },\n });\n};\n","import { ProductTourDetails } from \"./interfaces\";\n\nexport const PRODUCT_TOURS_DATA: ProductTourDetails[] = [\n {\n tourId: \"buckets-improved-workflow\",\n type: \"manual\",\n steps: [\n {\n title: \"New, Improved Workflow\",\n target: \".buckets__container\",\n content: (\n
\n We've improved the layout of your folders to better organize the path to Happy Hiring!\n \n Note: You can also use \"Left\" and \"Right\" arrow keys on the keyboard for navigation.\n \n
\n ),\n disableBeacon: true,\n stepName: \"buckets container showcase\",\n },\n {\n title: \"Current Folder\",\n target: \".js-bucket-current\",\n content: (\n
\n All your new applications appear in the Current folder as soon as they are received!\n
\n ),\n disableBeacon: true,\n stepName: \"buckets current showcase\",\n },\n {\n title: \"Favorites Folder\",\n target: \".js-bucket-favorites\",\n content: (\n
\n Found the perfect application? Move them to the Favorites and get that interview\n scheduled!\n
\n ),\n disableBeacon: true,\n stepName: \"buckets favorites showcase\",\n },\n {\n title: \"Interviewed Folder\",\n target: \".js-bucket-interviewed\",\n content: (\n
\n Interview complete? Here you can make notes, give a rating, even share with a colleague.\n It's decision time!\n
\n ),\n disableBeacon: true,\n stepName: \"buckets interviewed showcase\",\n },\n {\n title: \"Offer Pending\",\n target: \".js-bucket-offer_pending\",\n content: (\n
\n Made an Offer? Excellent! You're on the path to Happy Hiring! Move those applicants here\n and keep the ball rolling!\n
\n ),\n disableBeacon: true,\n stepName: \"buckets offer pending showcase\",\n },\n {\n title: \"Hired Folder\",\n target: \".js-bucket-hired\",\n content: (\n
\n Just made a great hire? Awesome! Move them here and get that process started. You're\n building a winning team!\n
\n ),\n disableBeacon: true,\n stepName: \"buckets hired showcase\",\n },\n {\n title: \"Archived Folder\",\n target: \".js-bucket-archived\",\n content: (\n
\n Not a perfect fit? No worries! You can move these applications to the Archived folder.\n
\n ),\n disableBeacon: true,\n stepName: \"buckets archived showcase\",\n },\n ],\n },\n];\n","// Utilities & Hooks\nimport React, { createContext, useContext, useEffect, useMemo, useState } from \"react\";\nimport { useMarkFinishTour } from \"../api/Tours/Tours\";\nimport { useAuth } from \"./auth-context\";\nimport { useNavigate, useNavigationType } from \"react-router\";\nimport { LocalStorageActions } from \"../utilities/handleLocalStorage\";\nimport useErrorReporting from \"../hooks/useErrorReporting\";\n\n// Constants\nimport { PRODUCT_TOURS_DATA } from \"../components/ProductTour/constants\";\n\n// Components\nimport Modal from \"../components/Modal/Modal\";\nimport Button from \"../components/Button/Button\";\n\n// Interfaces\nimport { ProductTourDetails, ProductTourIds } from \"../components/ProductTour/interfaces\";\nimport {\n handleMergeSearchParameters,\n useExtractSearchParameters,\n} from \"../hooks/useExtractSearchParameters\";\n\ninterface TourContextProps {\n activeTour: ProductTourDetails | null;\n isTourRunning: boolean;\n tourCurrentStep: number;\n tourCurrentStepName: string;\n handleTourStart: (tourId: ProductTourIds) => void;\n handlePromptTourEnd: () => void;\n handleTourEnd: () => void;\n handleTourNextStep: (transitionTimeout: number, callback?: () => void) => void;\n handleTourPreviousStep: (transitionTimeout: number, callback?: () => void) => void;\n handlePrepStep: (stepName: string, callback: () => void, timeout: number) => void;\n handleDynamicTour: (dynamicTour: ProductTourDetails) => void;\n handleClearActiveTour: () => void;\n handleResumeTour: () => void;\n handlePauseTour: () => void;\n}\n\nconst TourContext = createContext({\n activeTour: null,\n isTourRunning: false,\n tourCurrentStep: 0,\n tourCurrentStepName: \"\",\n handleTourStart: () => undefined,\n handlePromptTourEnd: () => undefined,\n handleTourEnd: () => undefined,\n handleTourNextStep: () => undefined,\n handleTourPreviousStep: () => undefined,\n handlePrepStep: () => undefined,\n handleDynamicTour: () => undefined,\n handleClearActiveTour: () => undefined,\n handleResumeTour: () => undefined,\n handlePauseTour: () => undefined,\n});\n\nconst TourContextWrapper = ({ children }: { children: React.ReactNode }) => {\n // for some reason we must include this\n // TODO: check if it causes issues with something else - very important\n window.global = globalThis;\n\n const { user, handleUpdateUserDetails } = useAuth();\n const [searchParametersObject] = useExtractSearchParameters();\n const navigationType = useNavigationType();\n const navigate = useNavigate();\n\n // Manual handling of the tours\n const [activeTour, setActiveTour] = useState(null);\n const [isTourRunning, setIsTourRunning] = useState(false);\n const [tourCurrentStep, setTourCurrentStep] = useState(0);\n const [showCancelTourModal, setShowCancelTourModal] = useState(false);\n\n // Sync between step index & step name state\n const tourCurrentStepName = useMemo(() => {\n // find the name of the step from the current tour\n // corresponding to the the current step index\n const tourStepName: string = activeTour?.steps[tourCurrentStep]?.stepName || \"\";\n\n return tourStepName;\n }, [tourCurrentStep]);\n\n /**\n *\n * Trigger static product tours that use pre-defined steps with static data.\n *\n * @param tourId The ID of the static tour.\n */\n const handleTourStart = (tourId: ProductTourIds) => {\n const matchingTour: ProductTourDetails | undefined = PRODUCT_TOURS_DATA.find(\n tour => tour.tourId === tourId,\n );\n\n // Exit function if no match was found\n if (!matchingTour) return;\n\n // Play the tour and save it in local storage\n LocalStorageActions.saveItem(\"activeProductTour\", matchingTour.tourId);\n setActiveTour(matchingTour);\n setIsTourRunning(true);\n };\n\n /**\n *\n * Trigger dynamic product tours that depend on some\n * data that is received from the API.\n *\n * @param dynamicTourData The data to be used for the dynamic tour\n */\n const handleDynamicTour = (dynamicTourData: ProductTourDetails) => {\n if (activeTour || !dynamicTourData) return;\n\n setActiveTour(dynamicTourData);\n setIsTourRunning(true);\n };\n\n // Execute prompt tour end\n const handlePromptTourEnd = () => setShowCancelTourModal(true);\n\n // Execute tour end\n const markFinishedTour = useMarkFinishTour();\n const errorReporting = useErrorReporting();\n\n const handleTourEnd = () => {\n if (!activeTour) return;\n setShowCancelTourModal(false);\n\n setIsTourRunning(false);\n setActiveTour(null);\n setTourCurrentStep(0);\n\n // Remove the active tour from local storage after closing it\n LocalStorageActions.removeItem(\"activeProductTour\");\n\n // If there are no unwatched product tours, do not try to send\n // a request to the API when cancelling the current tour\n if (!user || !user.unwatched_product_tours.length) {\n handleRedirectAfterTourEnd(activeTour.tourId);\n return;\n }\n\n /*============================================\n Send request to the API to mark the tour \n that is being closed as read.\n ============================================*/\n const tourIdToBeMarkedAsWatched = user.unwatched_product_tours.find(\n unwatchedTour => unwatchedTour.name === activeTour.tourId,\n )?.id;\n\n if (!tourIdToBeMarkedAsWatched) {\n handleRedirectAfterTourEnd(activeTour.tourId);\n return;\n }\n\n try {\n markFinishedTour.mutate(tourIdToBeMarkedAsWatched);\n\n // Manually update the user details objects to clear out the list of unwatched product tours\n // Note: This is so we can easily close the product tour window after ending the tour\n handleUpdateUserDetails({ unwatched_product_tours: [] });\n handleRedirectAfterTourEnd(activeTour.tourId);\n } catch (error) {\n errorReporting(\"Failed marking current tour as finished\", error);\n }\n };\n\n // Where should the user be redirected after ending the tour\n\n const handleRedirectAfterTourEnd = (tourId: ProductTourIds) => {\n const tourNavigationParameters = handleMergeSearchParameters(searchParametersObject);\n\n // Todo: In case the number of tours increases,\n // change this to not use if / else (or switch) statements\n if (tourId === \"applicants-ai-summary\") navigate(`/applications/${tourNavigationParameters}`);\n };\n\n const handleTourNextStep = (transitionTimeout: number = 500, callback?: () => void) => {\n // Prevent any actions if there is no active tour or tour is paused\n if (!activeTour) return;\n\n // Check if current step is last step, if so dont update counter\n const isLastStepOfTour: boolean = activeTour.steps.length - 1 === tourCurrentStep;\n\n // In case the tour should be paused between steps (cases of element toggle or animations)\n if (transitionTimeout) {\n setIsTourRunning(false);\n\n if (callback) callback();\n setTimeout(() => {\n setIsTourRunning(true);\n\n if (!isLastStepOfTour) setTourCurrentStep(tourStep => tourStep + 1);\n }, transitionTimeout);\n } else {\n if (!isLastStepOfTour) setTourCurrentStep(tourStep => tourStep + 1);\n\n // if there's some callback to be triggered, do it now\n if (callback) callback();\n }\n };\n\n const handlePrepStep = (stepName: string, callback: () => void, timeout: number) => {\n // If the current step doesnt match the callee step name, exit\n if (tourCurrentStepName !== stepName) return;\n\n setIsTourRunning(false);\n setTimeout(() => {\n callback();\n setIsTourRunning(true);\n }, timeout);\n };\n\n const handleTourPreviousStep = (transitionTimeout: number = 500, callback?: () => void) => {\n // Prevent any actions if there is no active tour or tour is paused\n if (!activeTour || !isTourRunning) return;\n\n if (transitionTimeout) {\n setIsTourRunning(false);\n\n if (callback) callback();\n setTimeout(() => {\n setIsTourRunning(true);\n\n setTourCurrentStep(tourStep => tourStep - 1);\n }, transitionTimeout);\n } else {\n setTourCurrentStep(tourStep => tourStep - 1);\n\n // if there's some callback to be triggered, do it now\n if (callback) callback();\n }\n };\n\n /** Clear out the currently active tour by reseting all related states */\n const handleClearActiveTour = () => {\n setTourCurrentStep(0);\n setIsTourRunning(false);\n setActiveTour(null);\n\n if (tourCurrentStep === 0) LocalStorageActions.removeItem(\"activeProductTour\");\n };\n\n /**\n * When the user navigates back in the browser history\n * by using the browser \"Back\" button, clear out (reset) the currently active tour\n **/\n useEffect(() => {\n if (!isTourRunning || navigationType !== \"POP\") return;\n handleClearActiveTour();\n }, [navigationType]);\n\n // Disable user induced page scrolling in case the tour is running\n // Note: This does not prevent the tour handling context/functionality from scrolling to steps\n useEffect(() => {\n if (isTourRunning) {\n document.body.classList.add(\"prevent-scroll\");\n } else {\n document.body.classList.remove(\"prevent-scroll\");\n }\n\n return () => document.body.classList.remove(\"prevent-scroll\");\n }, [isTourRunning]);\n\n /*=====================================\n CONTROL CURRENTLY ACTIVE TOUR\n\n This allows us to have more control in\n case we want to wait for animation or some user action\n before continuing the tour.\n ======================================*/\n const handleResumeTour = () => {\n if (!activeTour) return;\n setIsTourRunning(true);\n };\n\n const handlePauseTour = () => {\n if (!activeTour) return;\n setIsTourRunning(false);\n };\n\n return (\n \n {children}\n {showCancelTourModal ? (\n \n Are you sure you want to end the Tour? You can always start the tour from the{\" \"}\n Resources page in your account menu.\n \n }\n modifierClass=\"modal--md modal--stack-tours\"\n overlayModifierClass=\"modal--stack-tours\"\n handleCloseModal={() => setShowCancelTourModal(false)}\n >\n
\n setShowCancelTourModal(false)}\n >\n Cancel\n \n\n handleTourEnd()}\n >\n Yes, End Tour\n \n
\n \n ) : null}\n \n );\n};\n\nconst useTour = () => useContext(TourContext);\n\nexport { TourContext, TourContextWrapper, useTour };\n","import { RefObject, useEffect } from \"react\";\nimport { useTour } from \"../providers/tour-context\";\n\n/**\n * Hook for listening to and handling click events outside of the specified elements.\n * Used for example for closing dropdown menu if the user clicks somewhere outside of it.\n * @param ref A reference to the HTML Element that acts as the border where if clicked outside of it\n * the callback function will be triggered.\n * @param callback Callback function to be triggered if the user clicked somewhere outside of the\n * specified HTML Element.\n */\nexport default function useOnClickOutside(ref: RefObject, callback: Function): void {\n const { isTourRunning } = useTour();\n\n useEffect(() => {\n if (isTourRunning) return;\n\n const listener = (e: any) => {\n if (!ref.current || ref.current.contains(e.target)) return;\n\n callback();\n };\n\n document.addEventListener(\"mousedown\", listener);\n document.addEventListener(\"touchstart\", listener);\n\n // Clean up\n return () => {\n document.removeEventListener(\"mousedown\", listener);\n document.removeEventListener(\"touchstart\", listener);\n };\n });\n}\n","// Utilities & Hooks\nimport { useEffect, useRef } from \"react\";\n\n// Package\nimport SimpleBar from \"simplebar-react\";\nimport \"simplebar-react/dist/simplebar.min.css\";\n\n// Interfaces\nimport { CustomScrollbarsProps } from \"./interfaces\";\n\nconst CustomScrollbars: React.FC = ({\n children,\n maxHeight,\n autoHide = true,\n forceVisible = \"y\",\n autoScrollToBottom = null,\n hideAxis = null,\n}) => {\n const scrollbarsRef = useRef(null);\n const scrollbarsScrollToBottomElement = useRef(null);\n\n // Scroll down to the latest element in the list,\n // anytime the update is triggered (e.g. state has been updated)\n useEffect(() => {\n if (!autoScrollToBottom?.enabled || !scrollbarsScrollToBottomElement.current) return;\n scrollbarsScrollToBottomElement.current.scrollIntoView();\n }, [autoScrollToBottom?.updateTrigger]);\n\n return (\n \n {children}\n {autoScrollToBottom &&
}\n \n );\n};\n\nexport default CustomScrollbars;\n","import { Variants } from \"framer-motion\";\n\n/*============================\n FRAMER: HEADER\n==============================*/\nexport const FRAMER_HEADER_ANIMATION_COMMON: Variants = {\n initial: {\n opacity: 0,\n translateY: \"100px\",\n },\n animate: {\n opacity: 1,\n translateY: \"10px\",\n },\n exit: {\n opacity: 0,\n translateY: \"100px\",\n },\n};\n\nexport const FRAMER_HEADER_ANIMATION_WITH_OFFSET: Variants = {\n initial: {\n ...FRAMER_HEADER_ANIMATION_COMMON.initial,\n translateX: \"-50%\",\n right: \"unset\",\n left: \"50%\",\n },\n animate: {\n ...FRAMER_HEADER_ANIMATION_COMMON.animate,\n translateX: \"-50%\",\n right: \"unset\",\n left: \"50%\",\n },\n exit: {\n ...FRAMER_HEADER_ANIMATION_COMMON.exit,\n translateX: \"-50%\",\n right: \"unset\",\n left: \"50%\",\n },\n};\n\nexport const FRAMER_HEADER_TRANSITIONS = {\n duration: 0.5,\n type: \"spring\",\n};\n\n/*============================\n FRAMER: DROPDOWNS\n==============================*/\nexport const FRAMER_DROPDOWN_ANIMATION: Variants = {\n initial: props => {\n return {\n opacity: 0,\n translateY: \"50px\",\n translateX: props?.hasSideLabel ? 0 : \"-50%\",\n zIndex: 999,\n };\n },\n animate: props => {\n return {\n opacity: 1,\n translateY: 0,\n translateX: props?.hasSideLabel ? 0 : \"-50%\",\n zIndex: 999,\n };\n },\n exit: props => {\n return {\n opacity: 0,\n translateY: \"50px\",\n translateX: props?.hasSideLabel ? 0 : \"-50%\",\n zIndex: 999,\n };\n },\n};\n\nexport const FRAMER_DROPDOWN_TOP_ORIENTATION_ANIMATION: Variants = {\n initial: {\n opacity: 0,\n translateY: \"-50px\",\n translateX: \"-50%\",\n },\n animate: {\n opacity: 1,\n translateY: 0,\n translateX: \"-50%\",\n },\n exit: {\n opacity: 0,\n translateY: \"-50px\",\n translateX: \"-50%\",\n },\n};\n\nexport const FRAMER_DROPDOWN_ACTIONS_ANIMATION: Variants = {\n initial: { opacity: 0 },\n animate: { opacity: 1 },\n exit: { opacity: 0 },\n};\n\n/*============================\n FRAMER: SCHEDULE MEETING\n==============================*/\nexport const FRAMER_SCHEDULE_MEETING_ANIMATION: Variants = {\n enter: {\n opacity: 0,\n },\n center: {\n opacity: 1,\n },\n exit: {\n opacity: 0,\n },\n};\n\n/*============================\n FRAMER: APPLICATIONS \n SELECTION INDICATOR\n==============================*/\nexport const FRAMER_SELECTION_INDICATOR: Variants = {\n initial: { opacity: 0, height: 0, y: \"-80px\", position: \"relative\", zIndex: 0 },\n animate: {\n opacity: 1,\n height: \"50px\",\n y: 0,\n position: \"relative\",\n zIndex: 0,\n },\n exit: {\n height: 0,\n opacity: 0,\n y: \"-80px\",\n position: \"relative\",\n zIndex: 0,\n },\n};\n\n/*============================\n FRAMER: MARKETING BANNER\n==============================*/\nexport const FRAMER_MARKETING_BANNER_ANIMATION: Variants = {\n enter: () => {\n return {\n x: 0,\n y: \"calc(-100% - 30px)\",\n opacity: 1,\n transition: {\n duration: 0.5,\n ease: \"circInOut\",\n },\n };\n },\n center: () => {\n return {\n y: \"0%\",\n x: 0,\n opacity: 1,\n transition: {\n duration: 0.5,\n ease: \"circInOut\",\n },\n };\n },\n exit: () => {\n return {\n x: 0,\n y: \"calc(100% + 50px)\",\n opacity: 1,\n transition: {\n duration: 0.5,\n ease: \"circInOut\",\n },\n };\n },\n};\n","import { useEffect, useRef, useState } from \"react\";\n\n// Hooks & Utils\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Interfaces\nimport { DropdownItem, DropdownProps } from \"./interfaces\";\n\n// Assets\nimport { FaChevronDown as ChevronIcon } from \"react-icons/fa\";\n\n// Components\nimport Loader from \"../Loader/Loader\";\nimport CustomScrollbars from \"../CustomScrollbars/CustomScrollbars\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n// Constants\nimport {\n FRAMER_DROPDOWN_ANIMATION,\n FRAMER_DROPDOWN_TOP_ORIENTATION_ANIMATION,\n} from \"../../constants/framer\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\n\nconst Dropdown: React.FC = ({\n title,\n items,\n handleItemSelected,\n\n preselectedItemValue = \"\",\n isLoading = false,\n label = \"\",\n size = \"md\",\n disabled = false,\n modifierClass = \"\",\n orientation = \"bottom\",\n maxScrollableHeight = \"300px\",\n framerAnimationCustomProps = null,\n}) => {\n const [selectedItemText, setSelectedItemText] = useState(\"\");\n\n // Update the selected item based on the pre-selected title value\n useEffect(() => {\n setSelectedItemText(title);\n }, [title]);\n\n /*============================\n PRE-SELECT ITEM IF THERE'S\n A RECEIVED TITLE PROP THAT MATCHES IT\n ============================*/\n useEffect(() => {\n // Do not preselect anything if there's no such prop received\n if (!preselectedItemValue) return;\n\n // Find the item whose value matches the preselected item value received as a prop\n const preselectedItem: DropdownItem | undefined = items.find((item: DropdownItem) => {\n return item.value === preselectedItemValue;\n });\n\n // Exit if there's no match\n if (!preselectedItem) return;\n\n // Update the selected item's text state, which\n // will also mark the dropdown item as selected\n setSelectedItemText(preselectedItem.text);\n }, [preselectedItemValue, items]);\n\n /*===========================\n DROPDOWN MENU STATE\n ============================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n const handleToggleDropdownMenu = () => {\n // If the component is marked as 'disabled', prevent it from being opened\n if (disabled || isLoading) return;\n\n setIsDropdownOpen(!isDropdownOpen);\n };\n\n /*================================\n HANDLE DROPDOWN ITEM SELECTION\n ================================*/\n const handleDropdownItem = (item: DropdownItem) => {\n // Prevent functionality if the item is marked as \"disabled\"\n if (item.disabled) return;\n\n // Call the callback passed as prop\n handleItemSelected(item);\n\n // Display the selected item's text in the dropdown body\n setSelectedItemText(item.text);\n\n // Close the menu\n setIsDropdownOpen(false);\n };\n\n /*============================\n CLOSE WHEN CLICKED OUTSIDE\n ============================*/\n const dropdownRef = useRef(null);\n\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*============================\n DROPDOWN CLASS NAMES\n ============================*/\n let DROPDOWN_CLASSNAME = `dropdown dropdown--${size}`;\n if (disabled) DROPDOWN_CLASSNAME += \" dropdown--disabled\";\n if (modifierClass) DROPDOWN_CLASSNAME += ` ${modifierClass}`;\n if (isDropdownOpen) DROPDOWN_CLASSNAME += \" dropdown--active\";\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n
\n {label && (\n \n )}\n\n
\n
\n \n \n {selectedItemText || title}\n \n\n
\n {isLoading ? (\n \n ) : (\n \n )}\n
\n
\n\n \n {isDropdownOpen && (\n \n \n {items.map(item => {\n let DROPDOWN_ITEM_CLASSNAME: string = \"dropdown__item\";\n\n if (item.disabled) DROPDOWN_ITEM_CLASSNAME += \" dropdown__item--disabled\";\n if (item.text.toLowerCase() === selectedItemText.toLowerCase()) {\n DROPDOWN_ITEM_CLASSNAME += \" dropdown__item--selected\";\n }\n\n return (\n handleDropdownItem(item)}\n title={item.text}\n >\n
\n {item.icon &&
{item.icon}
}\n {item.text}\n\n {item.description && (\n

{item.description}

\n )}\n
\n \n );\n })}\n
\n \n )}\n
\n
\n
\n \n );\n};\nexport default Dropdown;\n","// Utilities\nimport { getIn } from \"formik\";\nimport { useTranslation } from \"react-i18next\";\n\n// Components\nimport Dropdown from \"../Dropdown/Dropdown\";\n\n// Interfaces\nimport { FormDropdownProps } from \"./interfaces\";\nimport { DropdownItem } from \"../Dropdown/interfaces\";\nimport { useEffect } from \"react\";\n\nconst FormDropdown = ({ field, form, relatedField = \"\", ...props }: FormDropdownProps) => {\n // Anytime the value of the related field is updated,\n // trigger a reset of the value of this field\n useEffect(() => {\n // Exit function if there's no related field\n if (!relatedField) return;\n\n // Exit function if the related field does not have any value (yet)\n if (!form.values[relatedField]) return;\n\n // If the value of the co-dependent field was updated,\n // trigger a reset of the value of this field\n form.setFieldValue(field.name, \"\");\n }, [form.values[relatedField]]);\n\n /*===========================\n HANDLE ERRORS\n ============================*/\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===========================\n APPEND MODIFIER CLASSES\n ============================*/\n const MODIFIER_CLASS: string = `${props.modifierClass} ${\n errors && touched ? \"dropdown--error\" : \"\"\n }`;\n\n /*===========================\n UPDATE FORMIK STATE\n \n Updates the targeted field with the selected value\n ============================*/\n const handleSelection = (item: DropdownItem) => form.setFieldValue(field.name, item.value);\n\n /*================================\n INTERNATIONALIZATION\n =================================*/\n const { t } = useTranslation();\n\n return (\n <>\n \n\n {errors && touched ?

{t(errors)}

: null}\n \n );\n};\n\nexport default FormDropdown;\n","import * as React from \"react\";\nconst SvgAppointmentDeleteIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 21, height: 21, viewBox: \"0 0 21 21\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { id: \"DeleteApptIcon_Neutral\", transform: \"translate(0.5 0.5)\" }, /* @__PURE__ */ React.createElement(\"g\", { id: \"Group_749\", \"data-name\": \"Group 749\", transform: \"translate(-916.864 -502.56)\" }, /* @__PURE__ */ React.createElement(\"circle\", { id: \"Ellipse_416\", \"data-name\": \"Ellipse 416\", cx: 10, cy: 10, r: 10, transform: \"translate(916.864 502.56)\", fill: \"none\", stroke: \"#999\", strokeMiterlimit: 10, strokeWidth: 1 })), /* @__PURE__ */ React.createElement(\"g\", { id: \"Group_750\", \"data-name\": \"Group 750\", transform: \"translate(-916.864 -502.56)\" }, /* @__PURE__ */ React.createElement(\"line\", { id: \"Line_303\", \"data-name\": \"Line 303\", x2: 8.994, y2: 8.994, transform: \"translate(922.367 508.063)\", fill: \"none\", stroke: \"#999\", strokeMiterlimit: 10, strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { id: \"Line_304\", \"data-name\": \"Line 304\", x1: 8.994, y2: 8.994, transform: \"translate(922.367 508.063)\", fill: \"none\", stroke: \"#999\", strokeMiterlimit: 10, strokeWidth: 1 }))));\nexport default SvgAppointmentDeleteIcon;\n","// Hooks & Utilities\nimport { getIn } from \"formik\";\nimport { useEffect, useState } from \"react\";\n\n// Assets\nimport DeleteIcon from \"../../assets/images/icons/appointment-delete-icon.svg?react\";\n\n// Interfaces\nimport { FormInputBubblesModel, FormInputBubblesProps } from \"./interfaces\";\n\nconst FormInputBubbles = ({\n form,\n field,\n label,\n isRequired = false,\n modifierClass = \"\",\n description = \"\",\n size = \"full\",\n bubbledValues,\n handleBubbledValues,\n textInput,\n handleTextInput,\n delimiters = [\";\", \"Enter\"],\n internalDelimiter = \";\",\n ...props\n}: FormInputBubblesProps) => {\n /*===============================\n HANDLE FORMIK ERRORS\n ================================*/\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===============================\n HANDLE DELETE BUBBLE BUTTON\n ================================*/\n const handleRemoveBubble = (selectedValueId: number) => {\n const copyBubbledValues = [...bubbledValues];\n\n const bubbleToRemove = copyBubbledValues.findIndex(entity => {\n return entity.id === selectedValueId;\n });\n\n copyBubbledValues.splice(bubbleToRemove, 1);\n handleBubbledValues([...copyBubbledValues]);\n form.setFieldValue(\n field.name,\n copyBubbledValues.map(bubbledValue => bubbledValue.value).join(internalDelimiter),\n );\n };\n\n /*===============================\n HANDLE INPUT CHANGE\n ================================*/\n const handleInputChange = (event: React.ChangeEvent) => {\n // Manually update the touched state of the field upon change\n if (!touched) form.setFieldTouched(field.name, true);\n\n // Ignore provided delimiters as values if they are inserted as single value\n if (delimiters.some(delimiter => event.target.value === delimiter)) return handleTextInput(\"\");\n\n // Replace all provided delimiters from the input with a semicolon for unification\n let value = event.target.value;\n delimiters.forEach((delimiter: string) => {\n value = value.replaceAll(delimiter, internalDelimiter);\n });\n\n // Split the unified delimiters value by the set semicolon\n if (value.split(internalDelimiter).length > 1) {\n const copyFinalValues = value.split(internalDelimiter).map((value, index) => {\n return { id: index + 1, value: value };\n });\n\n // Filter out the empty values after the delimiter unification split\n const filteredDelimiterValues = copyFinalValues.filter(\n bubble => bubble.value !== \"\" || delimiters.includes(bubble.value),\n );\n\n // Update the bubbledValues state\n handleBubbledValues([...bubbledValues, ...filteredDelimiterValues]);\n\n // Update the form value\n form.setFieldValue(\n field.name,\n filteredDelimiterValues.map(finalValue => finalValue.value).join(internalDelimiter),\n );\n } else {\n handleTextInput(value);\n }\n };\n\n /*===============================\n HANDLE BUBBLING\n ================================*/\n const handleBubbleSet = (event: React.KeyboardEvent) => {\n // If the input is empty and 'Backspace' has been pressed, remove the last bubbled element\n if (!textInput && event.key === \"Backspace\" && bubbledValues && bubbledValues.length) {\n const copyBubbledValues = [...bubbledValues];\n copyBubbledValues.pop();\n form.setFieldValue(\n field.name,\n copyBubbledValues.map(bubbledValue => bubbledValue.value).join(internalDelimiter),\n );\n\n handleBubbledValues(copyBubbledValues);\n }\n\n // Key triggers are the provided delimiters\n if (delimiters.includes(event.key) && textInput) {\n //disable form submission on enter press\n event.preventDefault();\n\n // Push current textInput as bubble\n const copyBubbledValues = [...bubbledValues];\n copyBubbledValues.push({ id: bubbledValues.length, value: textInput });\n handleBubbledValues(copyBubbledValues);\n\n // Push current textInput into form field\n form.setFieldValue(\n field.name,\n copyBubbledValues.map(value => value.value).join(internalDelimiter),\n );\n handleTextInput(\"\");\n }\n };\n\n // Convert any existing value in the input field that wasn't already converted to a bubble\n // when the user clicks anywhere outside of the input field\n const handleIncompleteValueBubbling = () => {\n // Do not trigger any action if there's no value in the input field\n if (!textInput) return;\n\n // Push current textInput as bubble\n const copyBubbledValues = [...bubbledValues];\n copyBubbledValues.push({ id: bubbledValues.length, value: textInput });\n handleBubbledValues(copyBubbledValues);\n\n // Push current textInput into form field\n form.setFieldValue(\n field.name,\n copyBubbledValues.map(value => value.value).join(internalDelimiter),\n );\n handleTextInput(\"\");\n };\n\n /*===============================\n FAULTY VALUES STATE\n\n Used to color bubbles that\n fail validation schema.\n ================================*/\n const [faultyValues, setFaultyValues] = useState([]);\n\n useEffect(() => {\n // Empty the erroneous values state if there are none\n if (!errors) return setFaultyValues([]);\n\n // Split the errors into individual strings\n const errorsArray = errors.split(\",\") ?? [];\n\n // Extract only the value from the error message & put into state\n // NOTE:Always put the erroneous value as first entity in validation schema error message\n setFaultyValues(errorsArray.map((value: string) => value.split(\" \")[0]));\n }, [errors]);\n\n return (\n <>\n
\n {label && (\n \n {label}\n \n )}\n\n
\n
\n \n
\n\n {/* DISPLAY ERROR MESSAGES */}\n {errors &&\n touched &&\n errors.split(\",\").map((error: string, index: number) => (\n

\n {error}\n

\n ))}\n\n {description &&

{description}

}\n
\n
\n \n );\n};\n\nexport default FormInputBubbles;\n","// Dashboard View mode types\nexport type DashboardViewMode = \"grid\" | \"classic\";\n\n// Make specific field(s) optional from a defined interface.\nexport type Optional = Pick, K> & Omit;\n\n// Make a generic object, consisted of fields and values defined based on the received type\nexport type PartialGenericObject = Partial>;\nexport type GenericObject = Record;\n\n/* \n The expected type for an image uploading field value.\n \n The possible values - \"Blob\" | \"string\" | \"null\" - are applied\n because of the following reasons:\n 1. The value that we get back from the API will always be either \n a \"string\" representing the URL to the image where it's uploaded,\n or \"null\" value if there's no uploaded image yet.\n 2. The remaining option of \"Blob\" type is added to satisfy the\n field when it's being included in FormData to be sent in the API request \n*/\nexport type ImageUploadType = Blob | string | null;\n\n// Type used for typecasting the 'location.state' field, to\n// be able to read a custom 'redirectTo' state\nexport type RouterLocationStateRedirectTo = Record<\"redirectTo\", string> | null;\n\n// Supported countries\nexport type CountryOption = \"US\" | \"CA\";\nexport interface CountryAddressLabelOptions {\n city: string;\n state: string;\n code: string;\n}\n\n// These are pre-defined system-wide user roles that are not changing\nexport enum UserRoleIDsEnum {\n SUPER_ADMIN = 1,\n ADMIN = 2, // Corresponds to Account Manager\n}\n\n// These are pre-defined system-wide user role names\nexport enum UserRoleNames {\n SUPER_ADMIN = \"Super Admin\",\n ACCOUNT_MANAGER = \"Account Manager\", // Also referred to as \"Admin\"\n COMPANY_USER = \"Company User\",\n}\n\n// Locales to be used for i18n troughout the application\nexport type InternationalizationLocales = \"en\" | \"es\";\n","import * as Yup from \"yup\";\nimport { UserRoleIDsEnum } from \"../interfaces/global\";\nimport { handleCheckStringForHTML } from \"../utilities/strings/handleCheckStringForHTML\";\nimport { SCHEMAS_NO_HTML_MESSAGE_TEXT, SCHEMAS_PASSWORD_MESSAGE } from \"./constants\";\n\n// SCHEMA REGEX\nimport { EMAIL_REGEX_PATTERN, PASSWORD_REGEX_PATTERN } from \"./regexes\";\n\n/*========================\n ACCOUNT - USERS (ADMIN)\n=========================*/\nexport const ACCOUNT_ADMIN_USERS_NEW = Yup.object().shape({\n first_name: Yup.string()\n .required(\"Please enter the first name of the user\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"admin-user-fname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n last_name: Yup.string()\n .required(\"Please enter the last name of the user\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"admin-user-lname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n title: Yup.string()\n .notRequired()\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"admin-user-title\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n email: Yup.string()\n .email(\"Please enter a valid email address\")\n .required(\"Please enter the email address of the user\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .test(\"admin-user-email\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n password: Yup.string()\n .matches(PASSWORD_REGEX_PATTERN, {\n message: SCHEMAS_PASSWORD_MESSAGE,\n })\n .min(8, \"The password needs to be at least 8 characters long.\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .required(\"Please enter a password\"),\n password_confirmation: Yup.string()\n .required(\"Passwords must be matching\")\n .when(\"password\", {\n is: (password: string) => (password && password.length > 0 ? true : false),\n then: schema =>\n schema\n .oneOf([Yup.ref(\"password\")], \"Passwords do not match.\")\n .required(\"Please confirm the password\"),\n }),\n role_id: Yup.number().nullable().required(\"Please select the user's role.\"),\n company_ids: Yup.array()\n .nullable()\n .test(\"companies-selection\", \"Please select at least 1 client.\", (value, context) => {\n const { role_id } = context.parent;\n const { SUPER_ADMIN, ADMIN } = UserRoleIDsEnum;\n\n // If the selected role is 'super admin' (ID 1) or 'account manager' (ID 2)\n // then the field is not required. Otherwise if any other role is selected, and there are no\n // selected companies, throw a validation error\n if ([SUPER_ADMIN, ADMIN].includes(role_id)) {\n return true;\n } else if (role_id !== SUPER_ADMIN && !value?.length) {\n return false;\n }\n\n return true;\n }),\n group_id: Yup.number()\n .nullable()\n .test(\"group-selection\", \"Please select the user's group.\", (value, context) => {\n const { role_id } = context.parent;\n const { SUPER_ADMIN } = UserRoleIDsEnum;\n\n // Only make the field required if the selected role is not \"super admin\"\n // and there is no selected user group at the moment\n if (role_id === SUPER_ADMIN) {\n return true;\n } else if (role_id !== SUPER_ADMIN && !value) {\n return false;\n }\n\n return true;\n }),\n});\n\nexport const ACCOUNT_ADMIN_USERS_EDIT_ACCOUNT_DETAILS = Yup.object().shape({\n first_name: Yup.string()\n .required(\"User's first name must be included\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"admin-user-fname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n last_name: Yup.string()\n .required(\"User's last name must be included\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"admin-user-lname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n title: Yup.string()\n .notRequired()\n .nullable()\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"admin-user-title\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n email: Yup.string()\n .email(\"Please enter a valid email address\")\n .required(\"User's email address must be included\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .test(\"admin-user-email\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n role_id: Yup.number().nullable().required(\"Please select the user's role\"),\n company_ids: Yup.array().nullable(),\n password: Yup.string()\n .matches(PASSWORD_REGEX_PATTERN, {\n message: SCHEMAS_PASSWORD_MESSAGE,\n })\n .min(8, \"The password needs to be at least 8 characters long.\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .notRequired(),\n password_confirmation: Yup.string()\n .notRequired()\n .when(\"password\", {\n is: (password: string) => {\n return password && password.length > 0 ? true : false;\n },\n then: schema =>\n schema\n .oneOf([Yup.ref(\"password\")], \"Passwords do not match.\")\n .required(\"Please confirm the password\"),\n }),\n});\n\n/*========================\n ACCOUNT - USERS\n=========================*/\nexport const ACCOUNT_USERS_NEW = Yup.object().shape({\n email: Yup.array()\n .required(\"Please enter at least one email address\")\n // Transform the received string into an array & run validation tests for each entity\n .transform(function (value, originalValue) {\n if (this.isType(value) && value !== null) {\n return value;\n }\n return originalValue ? originalValue.split(/[\\s;]+/) : [];\n })\n .test(\"email-valid-check\", function (value) {\n if (value) {\n // Make array of strings that fail the email regex test\n const invalidMails = value.filter(stringValue => {\n return !stringValue.match(EMAIL_REGEX_PATTERN);\n });\n\n // If there are invalid mails, map the invalid emails inside the error message\n return invalidMails.length\n ? this.createError({\n message: `${invalidMails.map(email => `${email} is not a valid email address`)}`,\n })\n : true;\n } else {\n return false;\n }\n })\n .test(\"unique-email\", function (value) {\n if (value) {\n // Make array of duplicate emails\n const findDuplicates = (arr: string[]) =>\n arr.filter((item, index) => arr.indexOf(item) !== index);\n const duplicateElements = findDuplicates(value);\n\n // If there are duplicate emails, map the duplicates inside the error message\n return duplicateElements.length\n ? this.createError({\n message: `${duplicateElements.map(email => `${email} is a duplicate email`)}`,\n })\n : true;\n } else {\n return false;\n }\n })\n .test(\"check-already-existing-user\", function (value, context) {\n if (value && context.parent.company_users) {\n // Cross-check each input mail with the already existing users\n const filterUsers = value.filter(inputMail =>\n context.parent.company_users.find(\n (companyUser: { email: string; status: string }) =>\n companyUser.email === inputMail && companyUser.status === \"active\",\n ),\n );\n\n // If there are already users in the input, map the emails inside the error message\n if (filterUsers.length) {\n return this.createError({\n message: `${filterUsers.map(email => `${email} is already a user in the company`)}`,\n });\n } else {\n return true;\n }\n } else {\n return false;\n }\n })\n .test(\"check-pending-invite\", function (value, context) {\n if (value && context.parent.company_users) {\n // Cross-check each input mail with the already pending invites\n const filterPending = value.filter(inputMail =>\n context.parent.company_users.find(\n (companyUser: { email: string; status: string }) =>\n companyUser.email === inputMail && companyUser.status === \"pending\",\n ),\n );\n\n // If there are pending invites in the input, map the emails inside the error message\n if (filterPending.length) {\n return this.createError({\n message: `${filterPending.map(email => `${email} is already invited to the company`)}`,\n });\n } else {\n return true;\n }\n } else {\n return false;\n }\n }),\n group_id: Yup.number().nullable().required(\"Please select the group this user will belong to\"),\n});\n\nexport const ACCOUNT_USERS_EDIT = Yup.object().shape({\n first_name: Yup.string()\n .required(\"Please enter the first name of the user\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"user-fname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n last_name: Yup.string()\n .required(\"Please enter the last name of the user\")\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"user-lname\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n title: Yup.string()\n .nullable()\n .notRequired()\n .max(30, \"Maximum of 30 characters allowed!\")\n .test(\"user-title\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n email: Yup.string()\n .email(\"Please enter a valid email address\")\n .required(\"Please enter the email address of the user\")\n .max(50, \"Maximum of 50 characters allowed!\")\n .test(\"user-email\", SCHEMAS_NO_HTML_MESSAGE_TEXT, value => {\n return !handleCheckStringForHTML(value as string);\n }),\n group_id: Yup.number().nullable().required(\"Please select the group this user will belong to\"),\n});\n\n/*========================\n ACCOUNT - MANAGE ALERTS\n=========================*/\nexport const ACCOUNT_MANAGE_ALERTS = Yup.object().shape({\n inhouse_alerts: Yup.array()\n .notRequired()\n // Transform the received string into an array & run validation tests for each entity\n .transform(function (value, originalValue) {\n if (this.isType(value) && value !== null) {\n return value;\n }\n return originalValue ? originalValue.split(/[\\s;]+/) : [];\n })\n .test(\"email-valid-check\", function (value) {\n if (!value) return true;\n\n // Make array of strings that fail the email regex test\n // or will fail the test that checks if any HTML tags are present\n const invalidMails = value.filter(stringValue => {\n return !stringValue.match(EMAIL_REGEX_PATTERN) || handleCheckStringForHTML(stringValue);\n });\n\n // If there are invalid mails, map the invalid emails inside the error message\n return invalidMails.length\n ? this.createError({\n message: `${invalidMails.map(email => `${email} is not a valid email address`)}`,\n })\n : true;\n }),\n ads: Yup.array().of(\n Yup.object().shape({\n emails: Yup.array()\n .notRequired()\n .nullable()\n // Transform the received string into an array & run validation tests for each entity\n .transform(function (value, originalValue) {\n if (this.isType(value) && value !== null) {\n return value;\n }\n return originalValue ? originalValue.split(/[\\s;]+/) : [];\n })\n .test(\"ads-email\", function (value) {\n if (!value) return true;\n\n // Make array of strings that fail the email regex test\n // or will fail the test that checks if any HTML tags are present\n const invalidMails = value.filter(stringValue => {\n return !stringValue.match(EMAIL_REGEX_PATTERN) || handleCheckStringForHTML(stringValue);\n });\n\n // If there are invalid mails, map the invalid emails inside the error message\n return invalidMails.length\n ? this.createError({\n message: `${invalidMails.map(email => `${email} is not a valid email address`)}`,\n })\n : true;\n }),\n }),\n ),\n});\n","import { useQuery } from \"@tanstack/react-query\";\nimport { GroupsResponseFieldsListNonAdmin } from \"./interfaces\";\nimport fetchHandler from \"../fetchHandler\";\n\n/**\n *\n * Get the list of existing groups that\n * the non-admin users have access to\n *\n */\nexport function useGroupsGetAll() {\n return useQuery({\n queryKey: [\"groups\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"groups\")) as GroupsResponseFieldsListNonAdmin;\n },\n });\n}\n","import { Link } from \"react-router\";\n\n// Assets\nimport { FaChevronLeft as BackIcon } from \"react-icons/fa\";\n\n// Components\nimport { Field, Form, Formik, FormikHelpers } from \"formik\";\nimport ContentHeader from \"../../../components/Content/ContentHeader\";\nimport FormDropdown from \"../../../components/Form/FormDropdown\";\nimport Button from \"../../../components/Button/Button\";\nimport FormInputBubbles from \"../../../components/Form/FormInputBubbles\";\n\n// Schemas\nimport { ACCOUNT_USERS_NEW } from \"../../../schemas/AccountSchemas\";\n\n// Utilities & Hooks\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useUsersAddToCompany, useUsersGetList } from \"../../../api/Users/Users\";\nimport { useGroupsGetAll } from \"../../../api/Groups/Groups\";\nimport useErrorReporting from \"../../../hooks/useErrorReporting\";\n\n// Interfaces\nimport { UsersAddFormikFields } from \"../../../api/Users/interfaces\";\nimport { DropdownItem } from \"../../../components/Dropdown/interfaces\";\nimport { GroupFields } from \"../../../api/Groups/interfaces\";\nimport { FormInputBubblesModel } from \"../../../components/Form/interfaces\";\n\nconst UsersNew = () => {\n const errorReporting = useErrorReporting();\n\n /*==============================\n OBTAIN THE LIST OF GROUPS\n ===============================*/\n const { data: groups, isPending: groupsIsPending } = useGroupsGetAll();\n\n const groupsDropdownItems: DropdownItem[] = useMemo(() => {\n if (!groups || !groups.length || groupsIsPending) return [];\n\n const mappedGroups: DropdownItem[] = groups.map((group: GroupFields) => {\n return { text: group.name, value: group.id, description: group.description };\n });\n\n return mappedGroups;\n }, [groups]);\n\n /*=====================================\n GET ALL USERS FROM CURRENT COMPANY\n\n Used to cross-reference with the \n current mailing list & display\n custom validation messages\n if an email is already in the company\n or the invatation is pending.\n ======================================*/\n const [companyUsers, setCompanyUsers] = useState<{ email: string; status: string }[]>([]);\n const { data: usersData, isPending: usersDataPending } = useUsersGetList();\n\n useEffect(() => {\n if (usersDataPending || !usersData) return;\n\n setCompanyUsers(\n usersData.map(user => {\n return { email: user.email, status: user.status };\n }),\n );\n }, [usersData]);\n\n /*==============================\n ADD A NEW USER TO THE COMPANY\n ===============================*/\n const addUser = useUsersAddToCompany();\n\n const handleAddUserToCompany = async (\n userDetails: UsersAddFormikFields,\n helpers: FormikHelpers,\n ) => {\n try {\n await addUser.mutateAsync({ email: userDetails.email, group_id: userDetails.group_id });\n\n // Reset the form to its initial values after successful submit\n setFinalEmails([]);\n helpers.resetForm();\n } catch (error) {\n errorReporting(\"Failed adding new user to current company\", error, { ...userDetails });\n }\n };\n\n /*==============================\n EMAILS INPUT FIELD\n ===============================*/\n const [emailValue, setEmailValue] = useState(\"\");\n const [finalEmails, setFinalEmails] = useState([]);\n\n return (\n
\n \n\n \n \n Back\n \n\n handleAddUserToCompany(values, helpers)}\n validateOnChange={true}\n validateOnBlur={true}\n >\n {({ values }) => (\n \n \n\n \n
\n \n Add User\n \n
\n \n )}\n \n
\n );\n};\n\nexport default UsersNew;\n","import { getIn } from \"formik\";\nimport { FormInputSideLabelProps } from \"./interfaces\";\n\nconst FormInputSideLabel = ({\n form,\n field,\n label,\n isRequired = false,\n modifierClass = \"\",\n description = \"\",\n size = \"full\",\n nestedChildrenElements = null,\n ...props\n}: FormInputSideLabelProps) => {\n /*===============================\n HANDLE FORMIK ERRORS\n ================================*/\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===============================\n TRIM FORM FIELD VALUE\n ================================*/\n const handleOnBlur = (event: React.FocusEvent) => {\n const trimmedValue: string = event.target.value.trim();\n\n // Update the form value to which this field corresponds to\n form.setFieldValue(field.name, trimmedValue);\n\n // Trigger internal Formik 'onBlur' events for the field\n field.onBlur(event);\n };\n\n return (\n
\n {label && (\n \n {label}\n \n )}\n\n
\n \n\n {/* ADDITIONAL NESTED ELEMENTS IF NEEDED (e.g. ICONS) */}\n {nestedChildrenElements}\n\n {/* DISPLAY ERROR MESSAGES */}\n {errors && touched &&

{errors}

}\n\n {description &&

{description}

}\n
\n
\n );\n};\n\nexport default FormInputSideLabel;\n","import { Link, useNavigate, useParams } from \"react-router\";\n\n// Assets\nimport { FaChevronLeft as BackIcon } from \"react-icons/fa\";\n\n// Components\nimport { Field, Form, Formik } from \"formik\";\nimport ContentHeader from \"../../../components/Content/ContentHeader\";\nimport Button from \"../../../components/Button/Button\";\nimport Skeleton from \"react-loading-skeleton\";\nimport Loader from \"../../../components/Loader/Loader\";\nimport FormInputSideLabel from \"../../../components/Form/FormInputSideLabel\";\nimport FormDropdown from \"../../../components/Form/FormDropdown\";\n\n// Interfaces\nimport { UsersEditFormikFields } from \"../../../api/Users/interfaces\";\nimport { DropdownItem } from \"../../../components/Dropdown/interfaces\";\nimport { GroupFields } from \"../../../api/Groups/interfaces\";\n\n// Utilities & Hooks\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useUsersEditSpecific, useUsersGetSpecific } from \"../../../api/Users/Users\";\nimport { useAuth } from \"../../../providers/auth-context\";\nimport { useGroupsGetAll } from \"../../../api/Groups/Groups\";\nimport useErrorReporting from \"../../../hooks/useErrorReporting\";\nimport handleFullnameCombination from \"../../../utilities/strings/handleFullnameCombination\";\n\n// Schemas\nimport { ACCOUNT_USERS_EDIT } from \"../../../schemas/AccountSchemas\";\n\nconst UsersEdit = () => {\n const { user } = useAuth();\n const { id } = useParams();\n const errorReporting = useErrorReporting();\n const navigate = useNavigate();\n\n /*==============================\n FORM FIELDS VALUES\n ===============================*/\n const [userFullname, setUserFullname] = useState(\"\");\n const [userFormDetails, setUserFormDetails] = useState({\n first_name: \"\",\n last_name: \"\",\n title: \"\",\n email: \"\",\n group_id: null,\n });\n\n /*==============================\n OBTAIN THE LIST OF ROLE GROUPS\n ===============================*/\n const { data: groups, isPending: groupsIsPending } = useGroupsGetAll();\n\n const ROLE_GROUPS: DropdownItem[] = useMemo(() => {\n if (!groups || !groups.length || groupsIsPending) return [];\n\n const mappedGroups: DropdownItem[] = groups.map((group: GroupFields) => {\n return { text: group.name, value: group.id, description: group.description };\n });\n\n return mappedGroups;\n }, [groups]);\n\n /*==============================\n GET TARGETED USER'S DETAILS\n ===============================*/\n const { data: userData, isPending: userIsPending } = useUsersGetSpecific(\n user.active_company.slug,\n id,\n );\n\n useEffect(() => {\n if (!userData || !Object.entries(userData).length || userIsPending) return;\n\n // Redirect if user doesnt belong to any group\n if (!userData.group_id) navigate(\"/403/\");\n\n // Construct the user's fullname\n const fullname: string = handleFullnameCombination(userData);\n\n setUserFullname(fullname);\n setUserFormDetails({\n first_name: userData.first_name,\n title: userData.title ?? \"\",\n last_name: userData.last_name,\n email: userData.email,\n group_id: userData.group_id,\n });\n }, [userData]);\n\n /*==============================\n EDIT SPECIFIED USER'S DETAILS\n ===============================*/\n const editUser = useUsersEditSpecific();\n\n const handleUserEdit = async (userDetails: UsersEditFormikFields) => {\n // Exit function if there's no valid \"active_company\" or user \"id\" value\n if (!user.active_company.slug || !id) return;\n\n try {\n // Construct the payload that will be sent in the request\n const editRequestPayload: UsersEditFormikFields = {\n first_name: userDetails.first_name,\n last_name: userDetails.last_name,\n title: userDetails.title,\n email: userDetails.email,\n group_id: userDetails.group_id,\n };\n\n await editUser.mutateAsync({\n userDetails: editRequestPayload,\n userId: id,\n });\n } catch (error) {\n errorReporting(\"Failed editing user details\", error, { user_id: id, ...userDetails });\n }\n };\n\n return (\n
\n \n Edit User -{\" \"}\n {userIsPending ? : userFullname || \"N/A\"}\n
\n }\n >\n\n \n \n Back\n \n\n {userIsPending ? (\n \n ) : (\n \n {({ values }) => (\n \n \n\n \n\n \n\n \n\n \n\n \n Edit User\n \n \n )}\n \n )}\n \n );\n};\n\nexport default UsersEdit;\n","import { ResourcesItem, ResourcesGroups } from \"./interfaces\";\n\nconst RESOURCES_FAQ: ResourcesItem[] = [\n { link: \"Quick_Contact_Card.pdf\", text: \"Quick Contact Card\" },\n { link: \"FAQS_General_Questions.pdf\", text: \"General\" },\n { link: \"FAQS_Personality_v_Behavior.pdf\", text: \"Personality Versus Behavior\" },\n { link: \"FAQS_InterpretingSBCA.pdf\", text: \"Interpreting the SBCA\" },\n { link: \"FAQS_SBCAProfileDescriptions.pdf\", text: \"SBCA Profile Descriptions\" },\n { link: \"FAQS_SBCAInnateDrive.pdf\", text: \"SBCA Measuring Innate Drive\" },\n { link: \"Resources_Dashboard_Tutorial.pdf\", text: \"Dashboard Tutorial\" },\n];\n\nconst RESOURCES_BEHAVIOUR_PROFILE: ResourcesItem[] = [\n { link: \"InterviewQuestions_CAREGIVER.pdf\", text: \"Interview Questions - CAREGIVER\" },\n { link: \"InterviewQuestions_PROCESSOR.pdf\", text: \"Interview Questions - PROCESSOR\" },\n { link: \"InterviewQuestions_THINKER.pdf\", text: \"Interview Questions - THINKER\" },\n { link: \"InterviewQuestions_INNATEDRIVE.pdf\", text: \"Interview Questions – INNATE DRIVE\" },\n];\n\nconst RESOURCES_POSITION_TYPE: ResourcesItem[] = [\n {\n link: \"InterviewQuestions_AdminClerical.pdf\",\n text: \"Interview Questions – Administrative / Clerical\",\n },\n {\n link: \"InterviewQuestions_CustomerService.pdf\",\n text: \"Interview Questions – Customer Service\",\n },\n { link: \"InterviewQuestions_Management.pdf\", text: \"Interview Questions - Management\" },\n { link: \"InterviewQuestions_Sales.pdf\", text: \"Interview Questions - Sales\" },\n { link: \"InterviewQuestions_Technical.pdf\", text: \"Interview Questions - Technical\" },\n];\n\nconst RESOURCES_TEMPLATE_DOCUMENTS: ResourcesItem[] = [\n { link: \"Simple_Job_Offer_Letter+V2.docx\", text: \"Simple Job Offer Letter\" },\n { link: \"Expanded_Job_Offer_Letter+V2.docx\", text: \"Expanded Job Offer Letter\" },\n {\n link: \"Employment_Reference_Check_Authorization_Form+V2.docx\",\n text: \"Employment Reference Check Authorization Form\",\n },\n];\n\nexport const RESOURCES_GROUPS: ResourcesGroups[] = [\n { title: \"Frequently Asked Questions\", pdfs: RESOURCES_FAQ },\n { title: \"Interview Questions - Behavior Profile\", pdfs: RESOURCES_BEHAVIOUR_PROFILE },\n { title: \"Interview Questions - Position Type\", pdfs: RESOURCES_POSITION_TYPE },\n { title: \"Template Documents\", pdfs: RESOURCES_TEMPLATE_DOCUMENTS },\n];\n","// Hooks & Utilities\nimport { useGetAvailableTours } from \"../../api/Tours/Tours\";\nimport { useNavigate } from \"react-router\";\nimport { LocalStorageActions } from \"../../utilities/handleLocalStorage\";\n\n// Components\nimport Skeleton from \"react-loading-skeleton\";\nimport ContentHeader from \"../../components/Content/ContentHeader\";\n\n// Interfaces\nimport { ResourcesGroups, ResourcesItem } from \"./interfaces\";\n\n// PDF assets\nimport { RESOURCES_GROUPS } from \"./statics\";\n\nconst Resources = () => {\n const { data: toursData, isPending: toursPending } = useGetAvailableTours();\n\n const navigate = useNavigate();\n\n const handleStartProductTour = (tourID: string) => {\n // Save the ID of the product tour to local LocalStorageActions\n LocalStorageActions.saveItem(\"activeProductTour\", tourID);\n\n // Redirect the user to the applications page\n // Note: We can change this to accept a dynamic URL parameter from the tour\n // if in the future we need tours on other pages of the application.\n navigate(\"/applications/\");\n };\n\n return (\n
\n \n
\n

New Feature Guides

\n
\n {toursPending ? (\n \n ) : toursData && toursData.length ? (\n toursData.map(tour => (\n handleStartProductTour(tour.name)}\n className=\"btn btn--text btn--text--secondary txt--left btn--font-md mb--10 px--0i\"\n >\n {tour.label}\n \n ))\n ) : null}\n
\n
\n\n {RESOURCES_GROUPS.map((resourceGroup: ResourcesGroups, groupIndex: number) => (\n
\n

{resourceGroup.title}

\n\n
\n {resourceGroup.pdfs.map((resource: ResourcesItem, resourceIndex: number) => (\n \n {resource.text}\n \n ))}\n
\n
\n ))}\n
\n );\n};\nexport default Resources;\n","import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"react-toastify\";\nimport fetchHandler from \"../fetchHandler\";\nimport { AlertsResponseFields, AlertsUpdateRequestFields } from \"./interfaces\";\n\n/*\n Get the details for managing alerts\n*/\nexport const useAlertsGetDetails = () => {\n return useQuery({\n queryKey: [\"alerts\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"alerts\")) as AlertsResponseFields;\n },\n });\n};\n\n/*\n Update the Alerts details\n*/\nexport const useAlertsUpdateDetails = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: (updatedDetails: AlertsUpdateRequestFields) => {\n return fetchHandler(\"POST\", \"alerts\", updatedDetails);\n },\n onSuccess: () => toast.success(\"Alert details updated successfully!\"),\n onError: (error: unknown) => error,\n onSettled: () => queryClient.invalidateQueries({ queryKey: [\"alerts\"] }),\n });\n};\n","/**\n * A utility function that converts the received timestamp value\n * into a Date object, in the correct local time, by multiplying the\n * initial timestamp by 1000 to get the correct amount of seconds.\n * @param timestamp Date in a timestamp format, as received from the server\n * @returns A Date object constructed by multiplying the received timestamp by 1000 (ms)\n * to always get the correct date value.\n */\nexport function handleDateAsTimestamp(timestamp: number): Date {\n const convertedTimestamp: Date = new Date(timestamp * 1000);\n return convertedTimestamp;\n}\n","// Hooks & Utilities\nimport { useEffect, useState } from \"react\";\nimport { Field, FieldArray, Form, Formik } from \"formik\";\nimport { useAlertsGetDetails, useAlertsUpdateDetails } from \"../../api/Alerts/Alerts\";\nimport { format } from \"date-fns\";\nimport { handleDateAsTimestamp } from \"../../utilities/dates/handleDateAsTimestamp\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport Button from \"../../components/Button/Button\";\nimport ContentHeader from \"../../components/Content/ContentHeader\";\nimport Skeleton from \"react-loading-skeleton\";\nimport FormInputBubbles from \"../../components/Form/FormInputBubbles\";\n\n// Schemas\nimport { ACCOUNT_MANAGE_ALERTS } from \"../../schemas/AccountSchemas\";\n\n// Interfaces\nimport { AlertsFormFields } from \"./interfaces\";\nimport { AlertsUpdateRequestFields } from \"../../api/Alerts/interfaces\";\nimport { FormInputBubblesModel } from \"../../components/Form/interfaces\";\n\nconst ManageAlerts = () => {\n const errorReporting = useErrorReporting();\n\n /*======================================\n ALERTS MANAGEMENT DETAILS\n =======================================*/\n const [formDetails, setFormDetails] = useState({\n inhouse_alerts: \"\",\n ads: [],\n });\n\n /*==============================\n BUBBLED EMAIL ALERTS\n ===============================*/\n const [inHouseEmailValue, setInHouseEmailValue] = useState(\"\");\n const [inHouseFinalEmails, setInHouseFinalEmails] = useState([]);\n\n const [onlineAdsAlertsEmailValues, setOnlineAdsAlertsEmailValues] = useState([]);\n const [onlineAdsAlertsFinalEmails, setOnlineAdsAlertsFinalEmails] = useState<\n FormInputBubblesModel[][]\n >([]);\n\n // Fetch the alert details from the API\n const { data, isPending } = useAlertsGetDetails();\n\n // Update the form details state with the fetched details from the API\n useEffect(() => {\n // Exit function if data is not available yet\n if (!data || !Object.entries(data).length || isPending) return;\n\n setFormDetails({\n inhouse_alerts: data.inhouse_alerts,\n ads: data.ads,\n });\n\n /*=======================================\n BUBBLED EMAILS - IN-HOUSE & ONLINE ADS\n =========================================*/\n const inhouseAlertsEmailBubbles: FormInputBubblesModel[] = data.inhouse_alerts\n ? data.inhouse_alerts.split(\";\").map((email, index) => {\n return { id: index, value: email };\n })\n : [];\n\n // We have an array of arrays here as we can target the individual input fields\n // corresponding to each indivdiual online job ad\n const onlineAdsAlertsEmailBubbles: FormInputBubblesModel[][] = data.ads.map(ad => {\n return ad.emails\n ? ad.emails.split(\";\").map((email, index) => {\n return { id: index, value: email };\n })\n : [];\n });\n\n setInHouseFinalEmails(inhouseAlertsEmailBubbles);\n setOnlineAdsAlertsFinalEmails(onlineAdsAlertsEmailBubbles);\n }, [data, isPending]);\n\n /*======================================\n ALERTS DETAILS UPDATES\n =======================================*/\n const updateAlertDetails = useAlertsUpdateDetails();\n\n const handleManagingAlerts = async (details: AlertsFormFields) => {\n try {\n const REQUEST_PAYLOAD: AlertsUpdateRequestFields = {\n inhouse_alerts: details.inhouse_alerts,\n ads: details.ads.map(ad => {\n return { id: ad.id, emails: ad.emails };\n }),\n };\n\n await updateAlertDetails.mutateAsync(REQUEST_PAYLOAD);\n } catch (error) {\n errorReporting(\"Failed updating alert details\", error, { ...details });\n }\n };\n\n /*======================================\n PREVIEW PAGE URL\n =======================================*/\n const [previewPageURL, setPreviewPageURL] = useState(\"\");\n\n useEffect(() => {\n const { protocol, host } = window.location;\n const url: string = `${protocol}//${host}/preview/job-ads`;\n setPreviewPageURL(url);\n }, []);\n\n return (\n
\n \n\n {isPending ? (\n
\n \n \n\n
\n
\n \n
\n
\n \n
\n
\n\n \n \n\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n ) : (\n \n {({ values, errors, setFieldValue }) => (\n
\n {/* IN-HOUSE ALERTS */}\n
\n

In-House Alerts

\n

\n Add emails to which in-house alerts will be sent.\n

\n
\n\n
\n
\n \n Email Addresses:\n \n
\n\n
\n {\n setInHouseFinalEmails(emails);\n setFieldValue(\"inhouse_alerts\", emails.map(email => email.value).join(\";\"));\n }}\n value={inHouseEmailValue}\n modifierClass=\"input-side-label mb--15\"\n textInput={inHouseEmailValue}\n handleTextInput={setInHouseEmailValue}\n delimiters={[\";\", \",\", \" \", \"Space\", \"Enter\"]}\n internalDelimiter=\";\"\n />\n
\n
\n\n {/* ONLINE ALERTS */}\n {values.ads.length > 0 ? (\n
\n

Online Alerts

\n

\n Separate multiple email addresses with a semicolon. Example:\n email@email.com;email2@email.com\n

\n
\n ) : null}\n\n (\n <>\n {values.ads.map((ad, index) => (\n
\n
\n

{ad.name}

\n \n (Click to preview job ad)\n \n
\n\n
\n {\n // Update the form value for the Ad at the specific index\n setFieldValue(\n `ads.${index}.emails`,\n emails.map(email => email.value).join(\";\"),\n );\n\n // Update the value for the bubbled emails in the input corresponding to the current index\n const updatedState = [...onlineAdsAlertsFinalEmails];\n updatedState.splice(index, 1, emails);\n setOnlineAdsAlertsFinalEmails(updatedState);\n }}\n modifierClass=\"input-side-label mb--15\"\n value={onlineAdsAlertsEmailValues[index]}\n textInput={onlineAdsAlertsEmailValues[index]}\n handleTextInput={(text: string) => {\n // Update the value for the input corresponding to the current index\n const updatedState = [...onlineAdsAlertsEmailValues];\n updatedState.splice(index, 1, text);\n setOnlineAdsAlertsEmailValues(updatedState);\n }}\n delimiters={[\";\", \",\", \" \", \"Space\", \"Enter\"]}\n internalDelimiter=\";\"\n />\n\n

\n Running {format(handleDateAsTimestamp(ad.date_from), \"MM/dd/yyyy\")} -{\" \"}\n {format(handleDateAsTimestamp(ad.date_to), \"MM/dd/yyyy\")}\n

\n
\n
\n ))}\n \n )}\n />\n\n
\n
\n \n Submit\n \n
\n
\n \n )}\n \n )}\n
\n );\n};\n\nexport default ManageAlerts;\n","import { useQuery } from \"@tanstack/react-query\";\nimport fetchHandler from \"../fetchHandler\";\nimport { VideoConferenceGetLinkFields, VideoConferenceFields } from \"./interfaces\";\n\n/**\n *\n * Join a video conference meeting\n *\n * @param videoConferenceHash The hashed link for joining the\n * video conference extracted from the URL parameter\n *\n */\nexport function useVideoConferencingJoin(videoConferenceHash: string | undefined) {\n return useQuery({\n queryKey: [\"video-conferencing-public\", videoConferenceHash],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `video-conference/join/${videoConferenceHash}`,\n )) as VideoConferenceFields;\n },\n enabled: !!videoConferenceHash,\n });\n}\n\n/**\n *\n * Get the details for a video conference\n *\n */\nexport function useVideoConferencingGetLink() {\n return useQuery({\n queryKey: [\"video-conferencing-get-link\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"video-conference\")) as VideoConferenceGetLinkFields;\n },\n });\n}\n","// Utilities & HOoks\nimport { AnimatePresence } from \"framer-motion\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-toastify\";\nimport { useVideoConferencingGetLink } from \"../../api/VideoConferencing/VideoConferencing\";\n\n// Components\nimport Button from \"../../components/Button/Button\";\nimport Loader from \"../../components/Loader/Loader\";\nimport Modal from \"../../components/Modal/Modal\";\n\nconst VideoConferencing = () => {\n /*===========================\n HELP MODAL\n ============================*/\n const [showHelpModal, setShowHelpModal] = useState(false);\n\n /*===========================\n MEETING LINK\n ============================*/\n const [meetingLink, setMeetingLink] = useState(\"\");\n const { data, isPending, error } = useVideoConferencingGetLink();\n\n // If details are fetched from the API, construct the meeting link\n // that will be displayed in the UI\n useEffect(() => {\n if (!data || !Object.entries(data).length || isPending) return;\n\n const { host, protocol } = window.location;\n const constructedMeetingLink: string = `${protocol}//${host}/vidconf/join/${data.video_link}`;\n setMeetingLink(constructedMeetingLink);\n }, [data, isPending]);\n\n const handleCopyMeetingLink = async () => {\n try {\n await navigator.clipboard.writeText(meetingLink);\n toast.info(\"Meeting link copied!\");\n } catch (error) {\n toast.error(\"Failed copying meeting link!\");\n }\n };\n\n return (\n
\n {isPending ? (\n \n ) : (\n <>\n {!data || !Object.entries(data).length || error ? (\n
\n

\n There was an issue obtaining the video conferencing details. Please reload the page\n and try again.\n

\n window.location.reload()}\n >\n Reload Page\n \n
\n ) : (\n <>\n
\n setShowHelpModal(true)}\n >\n ?\n
\n\n

\n Meeting link:\n {meetingLink}\n

\n
\n\n
\n
\n \n
\n
\n\n \n {showHelpModal ? (\n setShowHelpModal(false)}\n >\n
\n

\n Getting started with FirstChoice Hiring Video Conferencing is easy and just\n involves a few steps.\n

\n\n
    \n
  • \n Copy the meeting link (Control + C) next to the help icon and email it to\n your applicant and any other attendees.\n
  • \n
  • \n Don't forget to include a meeting time as your conference room is always\n available.\n
  • \n
  • \n Be sure to allow for enough time between meetings for people leaving the\n previous meeting.\n
  • \n
  • Click the button labeled \"Click Here to Start Video Chat\".
  • \n
  • Limit participants to 3-4 maximum for optimal performance.
  • \n
  • \n Mute microphone after speaking to avoid feedback and background noise\n using the \"Toggle Microphone\" button at the bottom of the conference\n window.\n
  • \n
\n\n

\n If you encounter any issues, don't hesistate to reach out to our support\n team at 1-877-449-7595.\n

\n
\n\n
\n setShowHelpModal(false)}\n >\n Close\n \n
\n \n ) : null}\n
\n \n )}\n \n )}\n \n );\n};\n\nexport default VideoConferencing;\n","// Utilities & Hooks\nimport { useAuth } from \"../../providers/auth-context\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport fetchHandler from \"../fetchHandler\";\n\n// Interfaces\nimport {\n AdminCompaniesAllFormsResponseFields,\n AdminCompaniesDropdownFields,\n AdminCompaniesListResponseFields,\n} from \"./interfaces\";\n\n/**\n *\n * Get the list of ALL existing companies\n *\n */\nexport const useAdminGetAllCompanies = () => {\n return useQuery({\n queryKey: [\"companies-admin\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"admin/company\")) as AdminCompaniesListResponseFields[];\n },\n meta: {\n errorMessage: \"Failed getting list of all existing companies\",\n },\n });\n};\n\n/**\n *\n * Get a list of all forms for the currently active company.\n *\n */\nexport const useAdminGetAllCompanyForms = () => {\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useQuery({\n queryKey: [\"companies-admin-forms\", companySlug],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `admin/company/${companySlug}/forms`,\n )) as AdminCompaniesAllFormsResponseFields[];\n },\n });\n};\n\n/**\n *\n * Get a list of all companies for the internal report company dropdown\n *\n */\nexport const useAdminGetAllCompaniesDropdown = () => {\n return useQuery({\n queryKey: [\"companies-admin-dropdown\"],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `admin/company/dropdown`,\n )) as AdminCompaniesDropdownFields[];\n },\n });\n};\n","import { useEffect, useRef, useState } from \"react\";\nimport { TooltipProps } from \"./interfaces\";\nimport useWindowResize from \"../../hooks/useWindowResize\";\n\nconst Tooltip = ({\n text,\n children,\n toggle = \"hover\",\n positioning = \"top\",\n size = \"sm\",\n wrapperModifierClass = \"\",\n modifierClass = \"\",\n}: TooltipProps) => {\n const [tootlipVisible, setTooltipVisible] = useState(false);\n\n // Toggle the tooltip ONLY on click event\n const handleTooltipOnClick = () => {\n if (toggle !== \"click\" || !text) return;\n setTooltipVisible(!tootlipVisible);\n };\n\n // Toggle the tooltip state ONLY when hovering over the item\n const handleTooltipOnHover = (action: \"enter\" | \"leave\") => {\n if (toggle !== \"hover\" || !text) return;\n\n if (action === \"enter\") {\n setTooltipVisible(true);\n } else {\n setTooltipVisible(false);\n }\n };\n\n // Responsive Positioning in case tooltip overflows viewport\n const tooltipRef = useRef(null);\n const [position, setPosition] = useState<\"left\" | \"right\" | \"top\" | \"bottom\" | null>(null);\n\n // Get the window width\n const [windowWidth] = useWindowResize(200);\n\n // Repositioning handler\n const handleResponsivePositioning = () => {\n if (!tooltipRef || !tooltipRef.current) return;\n\n const tooltipSpecs = tooltipRef.current.getBoundingClientRect();\n\n const isOverflowingRight = tooltipSpecs.left + tooltipSpecs.width + 30 >= windowWidth;\n\n const isOverflowingLeft = tooltipSpecs.left < 0;\n\n if (isOverflowingRight) {\n return setPosition(\"left\");\n }\n if (isOverflowingLeft) {\n return setPosition(\"right\");\n }\n };\n\n useEffect(() => {\n // If the viewport is at least 1200, reset to the received prop positioning\n if (windowWidth > 1200 && position !== positioning) {\n return setPosition(null);\n }\n\n // If the above condition fails, call the reposition handler\n handleResponsivePositioning();\n }, [tooltipRef, windowWidth]);\n\n return (\n handleTooltipOnHover(\"enter\")}\n onMouseLeave={() => handleTooltipOnHover(\"leave\")}\n >\n {text && (\n \n {text}\n \n )}\n {children}\n \n );\n};\n\nexport default Tooltip;\n","// Utilities & Hooks\nimport { useState, useRef, useMemo, useEffect } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport useDebounce from \"../../hooks/useDebounce\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Interfaces\nimport { DropdownItem, DropdownMultiselectProps } from \"./interfaces\";\n\n// Assets\nimport { FaChevronDown as ChevronIcon } from \"react-icons/fa\";\nimport { MdClear as ClearIcon } from \"react-icons/md\";\n\n// Components\nimport Tooltip from \"../Tooltip/Tooltip\";\nimport CustomScrollbars from \"../CustomScrollbars/CustomScrollbars\";\nimport Loader from \"../Loader/Loader\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n// Constants\nimport {\n FRAMER_DROPDOWN_ANIMATION,\n FRAMER_DROPDOWN_TOP_ORIENTATION_ANIMATION,\n} from \"../../constants/framer\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\n\nconst DropdownMultiselect = ({\n items,\n placeholder,\n handleSelectedItems,\n\n preselectedItems = [],\n searchable = false,\n searchDebounce = 500,\n clearable = false,\n label = \"\",\n disabled = false,\n isLoading = false,\n size = \"md\",\n modifierClass = \"\",\n orientation = \"bottom\",\n maxScrollableHeight = \"300px\",\n framerAnimationCustomProps = null,\n isRequired = false,\n}: DropdownMultiselectProps) => {\n /*===========================\n SAVE & MARK SELECTED ITEMS\n ============================*/\n const [selectedItemsValues, setSelectedItemsValues] = useState(preselectedItems);\n const [selectedItemsText, setSelectedItemsText] = useState(\"\");\n\n // Anytime the list of pre-selected items is updated, or\n // the list of items fetched from an API, trigger an update\n // to the local state too\n useEffect(() => {\n if (!preselectedItems.length || !items.length) return;\n\n // Preselect the items\n setSelectedItemsValues(preselectedItems);\n }, [preselectedItems, items]);\n\n // If there are no pre-selected items at all, clear out the inner state\n // for currently selected items and displayed text.\n // Example use case for this is when we clear out the selection to be passed down to this component\n useEffect(() => {\n if (!preselectedItems.length) {\n setSelectedItemsText(\"\");\n setSelectedItemsValues([]);\n }\n }, [preselectedItems.length]);\n\n /*===========================\n DROPDOWN MENU STATE\n ============================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n const handleToggleDropdownMenu = () => {\n // If the component is marked as 'disabled', prevent it from being opened\n if (disabled || isLoading) return;\n\n setIsDropdownOpen(!isDropdownOpen);\n };\n\n /*===========================\n DROPDOWN SEARCHING STATE\n ============================*/\n const [dropdownSearchedValue, setDropdownSearchedValue] = useState(\"\");\n const debouncedDropdownSearchValue: string = useDebounce(dropdownSearchedValue, searchDebounce);\n const dropdownSearchRef = useRef(null);\n\n const handleDropdownSearching = (event: React.ChangeEvent) => {\n setDropdownSearchedValue(event.target.value);\n };\n\n const handleDropdownSearchingKeypress = (event: React.KeyboardEvent) => {\n const { key } = event;\n\n if (key === \"Enter\") {\n const trimmedValue: string = event.currentTarget.value.trim();\n\n // Find the dropdown item with the matching text value\n const matchingDropdownItem: DropdownItem | undefined = items.find(item => {\n return item.text.toLowerCase() === trimmedValue.toLowerCase();\n });\n\n // Handle the matching item's selection\n if (matchingDropdownItem) {\n handleDropdownItem(matchingDropdownItem);\n\n // Clear out the searched value\n setDropdownSearchedValue(\"\");\n }\n }\n };\n\n // Focus on the input field when the dropdown menu is opened\n useEffect(() => {\n // If the dropdown is not searchable, or the reference does not exist, exit function\n if (!searchable || !dropdownSearchRef.current) return;\n\n if (isDropdownOpen) {\n dropdownSearchRef.current.focus();\n } else {\n dropdownSearchRef.current.blur();\n }\n }, [isDropdownOpen]);\n\n /*===========================\n DROPDOWN ITEMS STATE\n ============================*/\n const dropdownItems: DropdownItem[] = useMemo(() => {\n // Return a default empty array if there's no items available\n if (!items.length) return [];\n\n // Search trough the list of dropdown items and return\n // only those matching the searched value\n let filteredDropdownItems: DropdownItem[] = [...items];\n\n if (debouncedDropdownSearchValue) {\n filteredDropdownItems = matchSorter(filteredDropdownItems, debouncedDropdownSearchValue, {\n keys: [\"text\"],\n threshold: matchSorter.rankings.CONTAINS,\n });\n }\n\n // Sort the selected items to always be first\n if (selectedItemsValues.length) {\n filteredDropdownItems.sort(a => {\n // If the item's value is already part of the selected items values,\n // then we put this item to the top of the list, and this part is then sorted on\n // the selected values array on its own\n return selectedItemsValues.some(item => item === a.value) ? -1 : 1;\n });\n }\n return filteredDropdownItems;\n }, [items, selectedItemsValues, debouncedDropdownSearchValue]);\n\n /*================================\n HANDLE DROPDOWN ITEM SELECTION\n ================================*/\n const handleDropdownItem = (item: DropdownItem) => {\n // Prevent functionality if the item is marked as \"disabled\"\n if (item.disabled) return;\n\n // Focus on the input field\n if (dropdownSearchRef.current) dropdownSearchRef.current.focus();\n\n // If item does not exist - Add it to the list of selected items\n // If item already exists - Remove it from the list of selected items\n const selectedItemsValuesCopy: any[] = [...selectedItemsValues];\n\n // Check if the item is already selected\n const selectedItemIndex: number = selectedItemsValuesCopy.findIndex(selectedItem => {\n return selectedItem === item.value;\n });\n\n if (selectedItemIndex >= 0) {\n // Item already exists, remove it from array\n selectedItemsValuesCopy.splice(selectedItemIndex, 1);\n } else {\n // Item does not exist, add it to the array\n selectedItemsValuesCopy.push(item.value);\n }\n\n // Update the state of the selected items values,\n // which will also trigger updates to the displayed text values\n setSelectedItemsValues(selectedItemsValuesCopy);\n\n // Call the callback passed as prop\n handleSelectedItems(selectedItemsValuesCopy);\n };\n\n useEffect(() => {\n // Display the preselected items text in the dropdown's body\n const stringifiedSelectedItems: string = items\n .filter(item => selectedItemsValues.some(value => value == item.value))\n .map(item => item.text)\n .join(\", \");\n\n setSelectedItemsText(stringifiedSelectedItems);\n }, [selectedItemsValues]);\n\n /*============================\n CLEAR SELECTION OF ITEMS\n ============================*/\n const handleClearSelection = (event: React.MouseEvent) => {\n event.stopPropagation();\n\n // Prevent clearing selection if the dropdown is marked as disabled\n if (disabled) return;\n\n setSelectedItemsText(\"\");\n setSelectedItemsValues([]);\n handleSelectedItems([]);\n };\n\n /*============================\n CLOSE WHEN CLICKED OUTSIDE\n ============================*/\n const dropdownRef = useRef(null);\n\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*============================\n DROPDOWN CLASS NAMES\n ============================*/\n let DROPDOWN_CLASSNAME = `dropdown dropdown--${size}`;\n if (disabled) DROPDOWN_CLASSNAME += \" dropdown--disabled\";\n if (modifierClass) DROPDOWN_CLASSNAME += ` ${modifierClass}`;\n if (isDropdownOpen) DROPDOWN_CLASSNAME += \" dropdown--active\";\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n
\n {label && (\n \n {label}\n \n )}\n\n
\n
\n \n {selectedItemsText ? (\n {selectedItemsText}\n ) : (\n placeholder\n )}\n\n
\n {isLoading ? (\n \n ) : (\n <>\n {selectedItemsText.length > 0 && clearable && (\n \n \n \n \n
\n )}\n \n \n )}\n
\n
\n\n \n {isDropdownOpen && (\n \n {searchable && (\n
\n \n
\n )}\n\n \n
    \n {dropdownItems.length > 0 ? (\n dropdownItems.map(item => {\n let DROPDOWN_ITEM_CLASSNAME: string = \"dropdown__item\";\n const selectedItemTextIndex: number = selectedItemsText\n .split(\", \")\n .findIndex(selectedItem => {\n return selectedItem.toLowerCase() === item.text.toLowerCase();\n });\n\n if (item.disabled) DROPDOWN_ITEM_CLASSNAME += \" dropdown__item--disabled\";\n\n if (selectedItemTextIndex >= 0) {\n DROPDOWN_ITEM_CLASSNAME += \" dropdown__item--selected\";\n }\n\n return (\n handleDropdownItem(item)}\n title={item.text}\n >\n {item.icon &&
    {item.icon}
    }\n {item.text}\n \n );\n })\n ) : (\n
  • No item(s) found.
  • \n )}\n
\n
\n \n )}\n
\n
\n \n \n );\n};\n\nexport default DropdownMultiselect;\n","// Utilities & Hooks\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n AdminUsersEditRequestFields,\n AdminUsersCreateRequestFields,\n AdminUsersResponseFields,\n} from \"./interfaces\";\nimport { toast } from \"react-toastify\";\nimport fetchHandler from \"../fetchHandler\";\n\n/**\n *\n * Get the list of ALL existing users in the application\n *\n */\nexport function useAdminUsersGet() {\n return useQuery({\n queryKey: [\"users-admin\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"admin/users\")) as AdminUsersResponseFields[];\n },\n meta: {\n errorMessage: \"Failed getting list of all existing users\",\n },\n });\n}\n\n/**\n *\n * Get a specific user based on the user ID\n *\n */\nexport function useAdminUsersGetSpecific(userID: string | undefined) {\n return useQuery({\n queryKey: [\"user-admin\", userID],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `admin/users/${userID}`)) as AdminUsersResponseFields;\n },\n enabled: userID == undefined ? false : true,\n meta: {\n errorMessage: `Failed getting specific user with ID ${userID}`,\n },\n });\n}\n\n/**\n *\n * Delete the targeted user\n *\n * The mutation hook takes a \"userID\" parameter which can be either a number or null\n *\n */\nexport function useAdminUsersDelete() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: (userID: number) => {\n return fetchHandler(\"DELETE\", `admin/users/${userID}`);\n },\n onMutate: userID => {\n // Get admin users cached data\n const cachedUsersData = queryClient.getQueryData([\n \"users-admin\",\n ]) as AdminUsersResponseFields[];\n\n // Update query data with the provided user filtered out\n queryClient.setQueryData([\"users-admin\"], () => {\n if (!cachedUsersData) return;\n\n return cachedUsersData.filter(user => user.id !== userID);\n });\n\n // Show success notification\n toast.success(\"User deleted successfully.\", {\n toastId: \"users-admin-delete-user\",\n });\n\n // Return old data in case of error\n return { cachedUsersData };\n },\n onError: (error, _deleteDetails, context) => {\n // Dismiss the success notification from the UI first\n toast.dismiss(\"users-admin-delete-user\");\n\n // Insert old cache data upon error scenario\n queryClient.setQueryData([\"users-admin\"], context?.cachedUsersData);\n return error;\n },\n });\n}\n\n/**\n *\n * Creates a new user\n *\n * The mutation hook takes the form's details as a parameter\n *\n */\nexport function useAdminUsersCreate() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: (userDetails: AdminUsersCreateRequestFields) => {\n return fetchHandler(\"POST\", \"admin/users\", userDetails);\n },\n onMutate: () => {\n const existingUsers = queryClient.getQueryData([\"users-admin\"]);\n\n // Show success notification\n toast.success(\"User created successfully!\", {\n toastId: \"users-admin-create-user\",\n });\n\n return existingUsers;\n },\n onError: error => {\n // Dismiss the success notification from the UI first\n toast.dismiss(\"users-admin-create-user\");\n\n return error;\n },\n onSettled: () => queryClient.invalidateQueries({ queryKey: [\"users-admin\"] }),\n });\n}\n\n/**\n *\n * Updates the account details for a specific user\n *\n * @param userID The targeted user that will be updated\n *\n * The mutation hook takes the form's user details as a parameter\n */\nexport function useAdminUserUpdateAccountDetails() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: ({ userID, details }: { userID: string; details: AdminUsersEditRequestFields }) => {\n return fetchHandler(\"PUT\", `admin/users/${userID}`, details);\n },\n onMutate: ({ userID, details }) => {\n const cachedAdminUserData = queryClient.getQueryData([\n \"user-admin\",\n userID,\n ]) as AdminUsersResponseFields;\n\n queryClient.setQueryData([\"user-admin\", userID], {\n ...cachedAdminUserData,\n first_name: details.first_name,\n last_name: details.last_name,\n title: details.title,\n email: details.email,\n });\n\n // Show success notification\n toast.success(\"User account details were successfully updated\", {\n toastId: \"users-admin-update-user\",\n });\n\n return { cachedAdminUserData };\n },\n onError: (error, variables, context) => {\n // Dismiss the success notification from the UI first\n toast.dismiss(\"users-admin-update-user\");\n\n queryClient.setQueryData([\"user-admin\", variables.userID], context?.cachedAdminUserData);\n return error;\n },\n onSettled: (_data, _error, variables) => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n queryClient.invalidateQueries({ queryKey: [\"user-admin\", variables.userID] });\n },\n });\n}\n","// Utilities & Hooks\nimport { createColumnHelper } from \"@tanstack/react-table\";\nimport { format, parseJSON } from \"date-fns\";\nimport { matchSorter } from \"match-sorter\";\nimport { useMemo, useState } from \"react\";\nimport { Link } from \"react-router\";\nimport { useAdminUsersDelete, useAdminUsersGet } from \"../../../../api/Users/AdminUsers\";\nimport { toast } from \"react-toastify\";\nimport { useAuth } from \"../../../../providers/auth-context\";\nimport useErrorReporting from \"../../../../hooks/useErrorReporting\";\nimport handleFullnameCombination from \"../../../../utilities/strings/handleFullnameCombination\";\n\n// Interfaces\nimport { CompaniesResponseFields } from \"../../../../api/Company/interfaces\";\nimport { AdminUsersResponseFields } from \"../../../../api/Users/interfaces\";\nimport { AdminUsersListMappedFields, AdminUsersTableProps } from \"../interfaces\";\nimport { UserRoleNames } from \"../../../../interfaces/global\";\n\n// Components\nimport { AnimatePresence } from \"framer-motion\";\nimport Button from \"../../../../components/Button/Button\";\nimport Modal from \"../../../../components/Modal/Modal\";\nimport TableSkeletonPlaceholder from \"../../../../components/SkeletonPlaceholders/TableSkeletonPlaceholder\";\nimport Table from \"../../../../components/Table/Table\";\nimport PermissionCheckComponentWrapper from \"../../../../components/Wrappers/PermissionCheckComponentWrapper\";\nimport Tooltip from \"../../../../components/Tooltip/Tooltip\";\n\n// Assets\nimport { BsPatchQuestion as AutologinInfo } from \"react-icons/bs\";\n\nconst AdminUsersTable = ({ searchedUsers, companiesFilter }: AdminUsersTableProps) => {\n const errorReporting = useErrorReporting();\n const { user } = useAuth();\n\n /*=========================\n GENERATE TABLE COLUMNS\n ==========================*/\n const COLUMN_HELPER = createColumnHelper();\n const USERS_ADMIN_TABLE_COLUMNS = [\n COLUMN_HELPER.accessor(\"name\", {\n header: () => Name,\n size: 150,\n cell: data => {data.getValue() || \"N/A\"},\n }),\n COLUMN_HELPER.accessor(\"email\", {\n header: () => E-mail,\n size: 200,\n cell: data => {data.getValue() || \"N/A\"},\n }),\n COLUMN_HELPER.accessor(\"title\", {\n header: () => Title,\n size: 150,\n cell: data => {data.getValue() || \"N/A\"},\n }),\n COLUMN_HELPER.accessor(\"role\", {\n header: () => Role,\n cell: data => {data.getValue() || \"N/A\"},\n size: 130,\n }),\n COLUMN_HELPER.accessor(\"companies\", {\n header: () => Companies,\n size: 150,\n cell: data => {\n // Join the list of received companies into a single string\n const companies: string =\n data.getValue().length > 0\n ? data\n .getValue()\n .map((company: CompaniesResponseFields) => company.name)\n .join(\", \")\n : \"N/A\";\n\n return (\n \n {companies}\n \n );\n },\n }),\n COLUMN_HELPER.accessor(\"last_login_ip\", {\n header: () => IP Address,\n size: 130,\n enableSorting: false,\n cell: data => {data.getValue()},\n }),\n COLUMN_HELPER.accessor(\"last_login_time\", {\n header: () => Last Login,\n size: 130,\n cell: data => (\n \n {data.getValue() === \"N/A\"\n ? \"N/A\"\n : format(parseJSON(data.getValue()), \"dd MMM yyyy HH:mm\")}\n \n ),\n }),\n ...(user.role === UserRoleNames.SUPER_ADMIN\n ? [\n COLUMN_HELPER.accessor(\"autologin_token\", {\n header: () => (\n
\n Autologin Link\n \n \n \n
\n ),\n enableSorting: false,\n enableHiding: true,\n size: 140,\n cell: data => (\n
\n {data.getValue() ? (\n \n handleAutologinLink(data.cell.getValue(), data.row.original.email)\n }\n >\n Copy Link\n \n ) : (\n N/A\n )}\n
\n ),\n }),\n ]\n : []),\n COLUMN_HELPER.accessor(\"id\", {\n header: () => Action,\n enableSorting: false,\n size: 170,\n meta: {\n headerModifierClass: \"justify-content-end\",\n },\n cell: data => (\n
\n {user.role === UserRoleNames.ACCOUNT_MANAGER &&\n data.row.original.role === UserRoleNames.SUPER_ADMIN ? (\n No actions for super admins\n ) : (\n <>\n \n \n Edit\n \n \n \n {\n setIsDeleteUserModalOpen(true);\n setDeletedUserID(data.getValue());\n }}\n >\n Delete\n \n \n \n )}\n
\n ),\n }),\n ];\n\n /*=========================\n USERS TABLE DATA\n ==========================*/\n const { data, isPending, isFetching } = useAdminUsersGet();\n\n const adminUsers = useMemo(() => {\n // Exit & return a default value if there's no data\n if (!data || !data.length || isPending) return [];\n\n // Map the received fields from the API to what we need for the table\n let mappedUsers: AdminUsersListMappedFields[] = data.map((user: AdminUsersResponseFields) => {\n return {\n id: user.id,\n name: handleFullnameCombination(user),\n email: user.email,\n title: user.title || \"\",\n companies: user.companies.length > 0 ? user.companies : [],\n last_login_ip: user.user_data?.last_login_ip ?? \"N/A\",\n last_login_time: user.user_data?.last_login_time ?? \"N/A\",\n role: user.role,\n autologin_token: ![\"account manager\", \"super admin\"].includes(user.role.toLowerCase())\n ? user.autologin_token\n : null,\n };\n });\n\n // Filter out the users based on the selected companies\n if (companiesFilter.length) {\n mappedUsers = mappedUsers.filter(user => {\n return user.companies.some(company => {\n return companiesFilter.some(filteredCompany => {\n return company.name.toLowerCase() === filteredCompany.toLowerCase();\n });\n });\n });\n }\n\n // Filter out the users based on the searched value\n if (searchedUsers) {\n mappedUsers = matchSorter(mappedUsers, searchedUsers, {\n keys: [\"name\", \"email\"],\n threshold: matchSorter.rankings.CONTAINS,\n });\n }\n\n return mappedUsers;\n }, [data, searchedUsers, companiesFilter]);\n\n /*=========================\n DELETE A USER\n ==========================*/\n const [isDeleteUserModalOpen, setIsDeleteUserModalOpen] = useState(false);\n const [deletedUserID, setDeletedUserID] = useState(null);\n const userDelete = useAdminUsersDelete();\n\n // Trigger an API call to delete the targeted user\n const handleDeleteUser = async () => {\n try {\n if (!deletedUserID) throw new Error(\"No user ID found\");\n\n await userDelete.mutateAsync(deletedUserID);\n\n handleCloseDeleteUserModal();\n } catch (error) {\n errorReporting(\"Failed deleting user's account from 'Admin' page\", error, {\n user_id: deletedUserID,\n });\n }\n };\n\n // Close the modal and reset to default values\n const handleCloseDeleteUserModal = () => {\n setIsDeleteUserModalOpen(false);\n setDeletedUserID(null);\n };\n\n /*=========================\n AUTOLOGIN LINK\n\n Generate autologin link URL, constructed from\n the clicked user's `email` and `autologin_token` values\n ==========================*/\n const handleAutologinLink = async (token: string | null, email: string) => {\n // Exit function if there's no token for autologin to work with\n if (!token) return;\n\n const { host, protocol } = window.location;\n const autologinLink: string = `${protocol}//${host}/autologin?email=${email}&token=${token}`;\n\n try {\n await navigator.clipboard.writeText(autologinLink);\n toast.info(\"Autologin link copied!\");\n } catch (error) {\n toast.error(\"Failed copying autologin link!\");\n }\n };\n\n return (\n <>\n {isPending ? (\n \n ) : (\n \n )}\n\n \n {isDeleteUserModalOpen && (\n \n Are you sure you want to delete{\" \"}\n \n {adminUsers.find(user => user.id === deletedUserID)?.name ?? \"this user\"}?\n {\" \"}\n This action is irreversible.\n \n }\n modifierClass=\"modal--md modal--fixated\"\n handleCloseModal={handleCloseDeleteUserModal}\n >\n
\n \n Cancel\n \n\n \n Yes, Delete\n \n
\n \n )}\n
\n \n );\n};\n\nexport default AdminUsersTable;\n","// Utilities & Hooks\nimport { useState, useMemo, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { useAdminGetAllCompanies } from \"../../../../api/Company/AdminCompany\";\nimport { useExtractSearchParameters } from \"../../../../hooks/useExtractSearchParameters\";\n\n// Components\nimport ContentHeader from \"../../../../components/Content/ContentHeader\";\nimport DropdownMultiselect from \"../../../../components/Dropdown/DropdownMultiselect\";\nimport InputFieldSearch from \"../../../../components/Inputs/InputFieldSearch\";\nimport AdminUsersTable from \"./AdminUsersTable\";\nimport PermissionCheckComponentWrapper from \"../../../../components/Wrappers/PermissionCheckComponentWrapper\";\n\n// Interfaces\nimport { DropdownItem } from \"../../../../components/Dropdown/interfaces\";\n\nconst UsersAdmin = () => {\n const [searchParams, setSearchParams] = useExtractSearchParameters();\n\n /*==========================\n SEARCH TROUGH THE LIST\n OF USERS IN THE TABLE\n ===========================*/\n const [searchedUsers, setSearchedUsers] = useState(\"\");\n\n /*==========================\n FILTER USERS IN THE TABLE\n BASED ON SELECTED COMPANIES\n ===========================*/\n const [companiesFilter, setCompaniesFilter] = useState([]);\n const { data: companies, isPending: companiesIsPending } = useAdminGetAllCompanies();\n\n const companiesDropdownItems: DropdownItem[] = useMemo(() => {\n if (!companies || !companies.length || companiesIsPending) return [];\n\n // Map the companies to dropdown items\n return companies\n .map(company => {\n return { text: company.name, value: company.name };\n })\n .sort((companyA, companyB) => {\n return companyA.text.toLowerCase() > companyB.text.toLowerCase() ? 1 : -1;\n });\n }, [companies]);\n\n const handleCompaniesSelection = (companies: string[]) => {\n setCompaniesFilter(companies);\n\n // Join the selected companies in a string\n const companiesAsString: string = companies.join(\";\");\n\n // If there's a valid companies string value, add it as a search parameter.\n // Otherwise remove it from the search parameters\n if (companiesAsString) {\n setSearchParams(\n { ...searchParams, companies: companiesAsString, page: 1 },\n { replace: true },\n );\n } else {\n delete searchParams.companies;\n setSearchParams({ ...searchParams, page: 1 }, { replace: true });\n }\n };\n\n // Pre-select the companies filters if they are present in the search parameters\n useEffect(() => {\n if (searchParams.companies) {\n setCompaniesFilter(searchParams.companies.split(\";\"));\n }\n }, [searchParams.companies]);\n\n return (\n
\n \n
\n Users\n \n Add User\n \n
\n \n }\n modifierClass=\"content__header--no-underline\"\n >\n \n\n setSearchedUsers(search)}\n />\n \n\n \n
\n );\n};\n\nexport default UsersAdmin;\n","// Utilities\nimport { getIn } from \"formik\";\n\n// Components\nimport DropdownMultiselect from \"../Dropdown/DropdownMultiselect\";\n\n// Interfaces\nimport { FormDropdownMultiselectProps } from \"./interfaces\";\n\nconst FormDropdownMultiselect = ({\n field,\n form,\n handleFieldUpdate,\n ...props\n}: FormDropdownMultiselectProps) => {\n /*===========================\n HANDLE ERRORS\n ============================*/\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===========================\n APPEND MODIFIER CLASSES\n ============================*/\n const MODIFIER_CLASS: string = `${props.modifierClass} ${\n errors && touched ? \"dropdown--error\" : \"\"\n }`;\n\n /*===========================\n UPDATE FORMIK STATE\n \n Updates the targeted field with the selected value.\n If the latest clicked item's value was the same\n with the already selected item value, then clear out the selection\n ============================*/\n const handleSelection = (item: any[]) => {\n // Trigger field revalidation\n form.setFieldTouched(field.name);\n form.validateField(field.name);\n\n handleFieldUpdate(item);\n };\n\n return (\n <>\n \n {errors && touched ?

{errors}

: null}\n \n );\n};\n\nexport default FormDropdownMultiselect;\n","import { RefObject, useEffect } from \"react\";\n\ntype ScrollSettings = {\n block: \"start\" | \"end\" | \"center\" | \"nearest\";\n behavior?: \"auto\" | \"smooth\";\n inline?: \"start\" | \"end\" | \"center\" | \"nearest\";\n};\n\n/**\n *\n * Utility hook used for scrolling the user's viewport to a specifically targeted element.\n *\n * @param `stateTrigger` The inner state at the place where the hook is being used, that will trigger the scroll functionality\n * @param `ref` Reference to a specifically targeted element from the UI\n * @param `targetedArea` The area that will be targeted within the referenced element. This is the scrollable area.\n * @param `targetedElement` The element that is to be targeted and scrolled to within the scrollable area.\n * @param `scrollSettings` `optional` Configuration settings for `scrollIntoView` method.\n *\n */\nexport default function useScrollToActiveElement(\n stateTrigger: unknown,\n ref: RefObject,\n targetedArea: string,\n targetedElement: string,\n scrollSettings: ScrollSettings = { block: \"center\" },\n) {\n useEffect(() => {\n // Exit function if the referenced element is not available\n if (!ref.current) return;\n\n // Target the area that where the scrollable content exists\n const scrollableArea: HTMLElement | null = ref.current.querySelector(targetedArea);\n\n // Exit if area cannot be found\n if (!scrollableArea) return;\n\n // Target the specific element that exists within the scrollable area\n const elementToFocus: HTMLElement | null = scrollableArea.querySelector(targetedElement);\n\n if (!elementToFocus) return;\n\n elementToFocus.scrollIntoView(scrollSettings);\n }, [stateTrigger]);\n}\n","// Hooks & Utils\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport useScrollToActiveElement from \"../../hooks/useScrollToActiveElement\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Interfaces\nimport { DropdownItem, DropdownSearchableProps } from \"./interfaces\";\n\n// Assets\nimport { FaChevronDown as ChevronIcon } from \"react-icons/fa\";\n\n// Components\nimport Loader from \"../Loader/Loader\";\nimport CustomScrollbars from \"../CustomScrollbars/CustomScrollbars\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n// Constants\nimport {\n FRAMER_DROPDOWN_ANIMATION,\n FRAMER_DROPDOWN_TOP_ORIENTATION_ANIMATION,\n} from \"../../constants/framer\";\n\nconst DropdownSearchable: React.FC = ({\n items,\n handleItemSelected,\n\n preselectedItemValue = \"\",\n placeholder = \"\",\n label = \"\",\n size = \"md\",\n isLoading = false,\n disabled = false,\n modifierClass = \"\",\n allowDeselection = false,\n orientation = \"bottom\",\n maxScrollableHeight = \"300px\",\n deselectionHandler,\n isRequired = false,\n framerAnimationCustomProps = null,\n sortOnSearch = { enabled: false, direction: \"desc\" },\n}) => {\n const dropdownRef = useRef(null);\n const dropdownContentRef = useRef(null);\n const inputRef = useRef(null);\n\n // The value of the item that was selected from the dropdown menu\n const [selectedItemValue, setSelectedItemValue] = useState(\"\");\n const [searchValue, setSearchValue] = useState(\"\");\n const [placeholderValue, setPlaceholderValue] = useState(placeholder);\n\n /*===========================\n DROPDOWN MENU STATE\n ============================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n const handleToggleDropdownMenu = () => {\n // If the component is marked as 'disabled', prevent it from being opened\n if (disabled || isLoading) return;\n\n // Focus on the input field\n if (inputRef.current) inputRef.current.focus();\n\n setIsDropdownOpen(!isDropdownOpen);\n };\n\n /*===========================\n PRE-SELECT ITEM IF THERE'S\n A RECEIVED TITLE PROP THAT MATCHES IT\n ============================*/\n useEffect(() => {\n // Do not preselect anything if there's no such prop received\n if (!preselectedItemValue) return;\n\n // Find the item whose value matches the preselected item value received as a prop\n const preselectedItem: DropdownItem | undefined = items.find((item: DropdownItem) => {\n return item.value === preselectedItemValue;\n });\n\n // Exit if there's no match\n if (!preselectedItem) return;\n\n // Populate the input's placeholder text\n // and the state representing the currently selected item,\n // if there was a matching between the list of dropdown items\n // and the item value that was passed as preselected trough the prop\n setPlaceholderValue(preselectedItem.text);\n setSelectedItemValue(preselectedItem.value);\n }, [preselectedItemValue, items]);\n\n /*===========================\n HANDLE SEARCHING TROUGH\n THE RECEIVED DROPDOWN ITEMS\n ============================*/\n\n const handleOnSearch = (value: string) => {\n // Open the menu if it was closed\n if (!isDropdownOpen) setIsDropdownOpen(true);\n\n setSearchValue(value);\n };\n\n /*===========================\n FILTERED ITEMS\n ============================*/\n const FILTERED_DROPDOWN_ITEMS: DropdownItem[] = useMemo(() => {\n // If there's no value in the input field, return originally received list\n if (!searchValue) return items;\n\n // Filter the list of dropdown items based on user's input\n let FILTERED_ITEMS: DropdownItem[] = matchSorter(items, searchValue.trim(), {\n keys: [\"text\"],\n threshold: matchSorter.rankings.CONTAINS,\n });\n\n // Sort the dropdown items after searching (disabled by default)\n if (sortOnSearch.enabled) {\n FILTERED_ITEMS = FILTERED_ITEMS.sort((a, b) => {\n if (sortOnSearch.direction === \"asc\") {\n // If the received items contain a 'sortValue' property\n // use that value for sorting after searching\n // else just use the text field value for sorting\n if (a.sortValue && b.sortValue) {\n return a.sortValue > b.sortValue ? 1 : -1;\n } else {\n return a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1;\n }\n } else {\n if (a.sortValue && b.sortValue) {\n return a.sortValue > b.sortValue ? -1 : 1;\n } else {\n return a.text.toLowerCase() > b.text.toLowerCase() ? -1 : 1;\n }\n }\n });\n }\n\n return FILTERED_ITEMS;\n }, [items, searchValue]);\n\n /*===========================\n HANDLE DROPDOWN SELECTION\n ============================*/\n const handleDropdownItem = (item: DropdownItem) => {\n // Prevent functionality if the item is marked as \"disabled\"\n if (item.disabled) return;\n\n // If the clicked item is the same as the already selected one,\n // then just de-select the item and do not do anything extra\n if (allowDeselection && deselectionHandler && item.value === selectedItemValue) {\n setSelectedItemValue(\"\");\n setPlaceholderValue(placeholder);\n setSearchValue(\"\");\n\n // Callback function to be triggered on deselection\n deselectionHandler();\n } else {\n // Highlights (marks) the dropdown item that was selected\n setSelectedItemValue(item.value);\n\n // Update the state for the search and placeholder values\n // that are displayed in the input, based on the item that was selected\n setSearchValue(\"\");\n setPlaceholderValue(item.text);\n\n // Call the callback passed as prop\n handleItemSelected(item);\n }\n\n // Close the dropdown menu\n setIsDropdownOpen(false);\n };\n\n /*===========================\n CLOSE WHEN CLICKED OUTSIDE\n ============================*/\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n // Close when \"Escape\" key is pressed\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n /*============================\n DROPDOWN CLASS NAMES\n ============================*/\n let DROPDOWN_CLASSNAME = `dropdown dropdown--searchable dropdown--${size}`;\n if (disabled) DROPDOWN_CLASSNAME += \" dropdown--disabled\";\n if (modifierClass) DROPDOWN_CLASSNAME += ` ${modifierClass}`;\n if (isDropdownOpen) DROPDOWN_CLASSNAME += \" dropdown--active\";\n\n /*===============================\n SCROLL TO THE SELECTED ELEMENT\n ================================*/\n useScrollToActiveElement(\n isDropdownOpen,\n dropdownContentRef,\n \".simplebar-content\",\n \".dropdown__item--selected\",\n );\n\n return (\n
\n {label && (\n \n {label}\n \n )}\n\n
\n
\n \n ) => handleOnSearch(e.target.value)}\n title={searchValue || placeholderValue}\n />\n
\n {isLoading ? (\n \n ) : (\n \n )}\n
\n
\n\n \n {isDropdownOpen && (\n \n \n {FILTERED_DROPDOWN_ITEMS.length > 0 ? (\n <>\n {FILTERED_DROPDOWN_ITEMS.map((item: DropdownItem) => {\n let DROPDOWN_ITEM_CLASSNAME: string = \"dropdown__item\";\n\n if (item.disabled) DROPDOWN_ITEM_CLASSNAME += \" dropdown__item--disabled\";\n if (item.value === selectedItemValue) {\n DROPDOWN_ITEM_CLASSNAME += \" dropdown__item--selected\";\n }\n\n return (\n handleDropdownItem(item)}\n title={item.text}\n >\n
\n {item.icon &&
{item.icon}
}\n {item.text}\n\n {item.description && (\n

{item.description}

\n )}\n
\n \n );\n })}\n \n ) : (\n
  • No Data Found
  • \n )}\n
    \n \n )}\n
    \n
    \n
    \n \n );\n};\nexport default DropdownSearchable;\n","// Utilities\nimport { getIn } from \"formik\";\n\n// Components\nimport DropdownSearchable from \"../Dropdown/DropdownSearchable\";\n\n// Interfaces\nimport { FormDropdownSearchableProps } from \"./interfaces\";\nimport { DropdownItem } from \"../Dropdown/interfaces\";\n\nconst FormDropdownSearchable = ({\n field,\n form,\n handleFieldUpdate,\n ...props\n}: FormDropdownSearchableProps) => {\n /*===========================\n HANDLE ERRORS\n ============================*/\n const errors = getIn(form.errors, field.name);\n const touched = getIn(form.touched, field.name);\n\n /*===========================\n APPEND MODIFIER CLASSES\n ============================*/\n const MODIFIER_CLASS: string = `${props.modifierClass} ${\n errors && touched ? \"dropdown--error\" : \"\"\n }`;\n\n /*===========================\n UPDATE FORMIK STATE\n \n Updates the targeted field with the selected value.\n If the latest clicked item's value was the same\n with the already selected item value, then clear out the selection\n ============================*/\n const handleSelection = (item: DropdownItem) => handleFieldUpdate(item);\n return (\n <>\n \n {errors && touched ?

    {errors}

    : null}\n \n );\n};\n\nexport default FormDropdownSearchable;\n","// Utilities & Hooks\nimport { useQuery } from \"@tanstack/react-query\";\nimport fetchHandler from \"../fetchHandler\";\n\n// Interfaces\nimport { RolesListResponseFields } from \"./interfaces\";\n\n/**\n *\n * Get a list of all existing roles\n *\n */\nexport function useAdminRolesGetAll() {\n return useQuery({\n queryKey: [\"roles-admin\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"admin/roles\")) as RolesListResponseFields;\n },\n meta: {\n errorMessage: \"Failed getting list of all existing roles\",\n },\n });\n}\n","import { useMemo } from \"react\";\nimport { useAdminGetAllCompanies } from \"../../../../../api/Company/AdminCompany\";\nimport { AdminCompaniesListResponseFields } from \"../../../../../api/Company/interfaces\";\nimport { DropdownItem } from \"../../../../../components/Dropdown/interfaces\";\nimport { handleStringCapitalization } from \"../../../../../utilities/strings/handleStringCapitalization\";\n\ntype AdminUsersCompaniesHookReturn = [DropdownItem[], boolean];\n\n/**\n * Custom hook for fetching the companies list and then\n * mapping the received list of companies into a list of dropdown items\n * @returns A list of companies mapped as dropdown items, and a loading indicator\n */\nexport default function useAdminUsersCompanies(): AdminUsersCompaniesHookReturn {\n const { data, isPending: companiesLoading } = useAdminGetAllCompanies();\n\n // Map the receivied companies data into dropdown items\n const companies = useMemo(() => {\n // Exit & return default value if there's no data\n if (!data || !data.length || companiesLoading) return [];\n\n // Map the companies to dropdown items\n const companyDropdownItems: DropdownItem[] = data\n .map((company: AdminCompaniesListResponseFields) => {\n return { text: handleStringCapitalization(company.name, [\" \"]), value: company.id };\n })\n .sort((companyA, companyB) => {\n return companyA.text.toLowerCase() > companyB.text.toLowerCase() ? 1 : -1;\n });\n\n return companyDropdownItems;\n }, [data]);\n\n return [companies, companiesLoading];\n}\n","// Hooks & Utilities\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"react-toastify\";\nimport fetchHandler from \"../fetchHandler\";\n\n// Interfaces\nimport {\n PrivilegesCrudFormFields,\n PrivilegesGroupFields,\n PrivilegesResponseFields,\n PrivilegesUserCompanyPermissionsAndGroups,\n PrivilegesUserResponseFields,\n PrivilegesUserUpdateFields,\n} from \"./interfaces\";\n\n/**\n *\n * Get the list of all existing privileges that\n * are to be listed in the UI.\n *\n */\nexport const usePrivilegesGetAll = () => {\n return useQuery({\n queryKey: [\"privileges-all\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"admin/permissions\")) as PrivilegesResponseFields;\n },\n });\n};\n\n/**\n *\n * Get a list of all the privileges related groups\n *\n */\nexport const usePrivilegesGroupsGetAll = () => {\n return useQuery({\n queryKey: [\"privileges-groups\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"admin/groups\")) as PrivilegesGroupFields[];\n },\n });\n};\n\n/**\n *\n * Get the details for the targeted privileges group\n *\n * @param groupID ID of the targeted privileges group\n *\n */\nexport const usePrivilegesGroupsGetSpecific = (groupID: string | undefined) => {\n return useQuery({\n queryKey: [\"privileges-groups\", groupID],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `admin/groups/${groupID}`)) as PrivilegesCrudFormFields;\n },\n enabled: !!groupID,\n });\n};\n\n/**\n *\n * Create new privileges groups\n *\n */\nexport const usePrivilegesGroupsCreate = () => {\n return useMutation({\n mutationFn: async (groupDetails: PrivilegesCrudFormFields) => {\n return await fetchHandler(\"POST\", `admin/groups`, groupDetails);\n },\n onSuccess: () => toast.success(\"Successfully created privileges group!\"),\n onError: (error: unknown) => error,\n });\n};\n\n/**\n *\n * Update the details of the targeted privileges group\n *\n * @param groupID ID of the targeted privileges group\n *\n */\nexport const usePrivilegesGroupEdit = (groupID: string | undefined) => {\n return useMutation({\n mutationFn: async (updateDetails: PrivilegesCrudFormFields) => {\n return await fetchHandler(\"PUT\", `admin/groups/${groupID}`, updateDetails);\n },\n onSuccess: () => toast.success(\"Successfully edited privileges group!\"),\n onError: (error: unknown) => error,\n });\n};\n\n/**\n *\n * Delete the targeted group of privileges\n *\n */\nexport const usePrivilegesGroupDelete = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (groupID: number | null) => {\n return await fetchHandler(\"DELETE\", `admin/groups/${groupID}`);\n },\n onMutate: (groupID: number | null) => {\n // Get currently cached privileges group\n const cachedPrivilegesGroups = queryClient.getQueryData([\n \"privileges-groups\",\n ]) as PrivilegesGroupFields[];\n\n // Remove the targeted group from the list\n queryClient.setQueryData(\n [\"privileges-groups\"],\n (oldPrivilegesGroupsData: PrivilegesGroupFields[] | undefined) => {\n if (!oldPrivilegesGroupsData) return;\n\n return oldPrivilegesGroupsData.filter(group => group.id !== groupID);\n },\n );\n\n // Show success notification\n toast.success(\"Privilege Group deleted successfully!\", {\n toastId: \"privileges-groups-delete\",\n });\n\n return { cachedPrivilegesGroups };\n },\n onError: (error, _groupID, context) => {\n toast.dismiss(\"privileges-groups-delete\");\n\n queryClient.setQueryData([\"privileges-groups\"], context?.cachedPrivilegesGroups);\n return error;\n },\n });\n};\n\n/**\n *\n * Fetch all the details for the user's permissions. This contains:\n * - `groups` - a list of all the existing groups that can be selected for the user\n * - `companies` - the list of companies to which the user belongs to\n * - `permissions` - the list of all existing `public` and `admin` permissions\n *\n * @param userID ID of the targeted user for who we want to obtain the permission details\n *\n */\nexport const usePrivilegesGetUser = (userID: string | undefined) => {\n return useQuery({\n queryKey: [\"privileges-user-permissions\", userID],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `admin/users/${userID}/permissions`,\n )) as PrivilegesUserResponseFields;\n },\n enabled: !!userID,\n });\n};\n\n/**\n *\n * Update the targeted user's permissions.\n *\n * The mutation hook takes in an object parameter representing\n * the update details that consist of:\n *\n * - `companies` - an array of IDs representing the companies\n * for which the permissions will be updated\n * - `groups` - an array of IDs representing the privilege groups to be applied to the user\n * - `permissions` - an array of IDs representing the individual permissions\n *\n * @param userID The ID of the targeted user\n *\n */\nexport const usePrivilegesUserUpdate = (userID: string | undefined) => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (updateDetails: PrivilegesUserUpdateFields) => {\n return await fetchHandler(\"POST\", `admin/users/${userID}/permissions`, updateDetails);\n },\n onSuccess: () => toast.success(\"Successfully updated user's permissions!\"),\n onError: (error: unknown) => error,\n onSettled: () => {\n // Invalidate the logged in user's \"profile\" cached data\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n },\n });\n};\n\n/**\n *\n * Fetch the list of `groups` and `permissions` that\n * the user has in the specific company that is selected at the moment\n *\n * @param userID The ID of the targeted user\n * @param companyID The ID of the specifically selected company\n *\n */\nexport const usePrivilegesGetCompanyPermissionsAndGroups = (\n userID: string | undefined,\n companyID: number | null,\n) => {\n return useQuery({\n queryKey: [\"privileges-user-company-permissions\", userID, companyID],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `admin/users/${userID}/company/${companyID}/permissions`,\n )) as PrivilegesUserCompanyPermissionsAndGroups;\n },\n enabled: !!companyID,\n });\n};\n","// Assets\nimport { FaChevronLeft as BackIcon } from \"react-icons/fa\";\nimport { BiShow as ShowPasswordIcon, BiHide as HidePasswordIcon } from \"react-icons/bi\";\n\n// Components\nimport { Field, Form, Formik, FormikProps } from \"formik\";\nimport ContentHeader from \"../../../../components/Content/ContentHeader\";\nimport Button from \"../../../../components/Button/Button\";\nimport FormDropdownMultiselect from \"../../../../components/Form/FormDropdownMultiselect\";\nimport FormDropdownSearchable from \"../../../../components/Form/FormDropdownSearchable\";\nimport FormInputSideLabel from \"../../../../components/Form/FormInputSideLabel\";\n\n// Interfaces\nimport { DropdownItem } from \"../../../../components/Dropdown/interfaces\";\nimport {\n AdminUsersCreateRequestFields,\n AdminUsersPreRequestFields,\n} from \"../../../../api/Users/interfaces\";\nimport { RolesResponseFields } from \"../../../../api/Roles/interfaces\";\nimport { UserRoleIDsEnum } from \"../../../../interfaces/global\";\n\n// Utilities & Hooks\nimport { Link, useNavigate } from \"react-router\";\nimport { useAdminUsersCreate } from \"../../../../api/Users/AdminUsers\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useAdminRolesGetAll } from \"../../../../api/Roles/AdminRoles\";\nimport useErrorReporting from \"../../../../hooks/useErrorReporting\";\nimport useAdminUsersCompanies from \"./hooks/useAdminUsersCompanies\";\n\n// Schemas\nimport { ACCOUNT_ADMIN_USERS_NEW } from \"../../../../schemas/AccountSchemas\";\nimport { usePrivilegesGroupsGetAll } from \"../../../../api/Privileges/Privileges\";\n\nconst AdminUsersNew = () => {\n const navigate = useNavigate();\n const formRef = useRef>(null);\n const errorReporting = useErrorReporting();\n\n /*===============================\n GET THE LIST OF COMPANIES\n ================================*/\n const [companies, companiesLoading] = useAdminUsersCompanies();\n\n /*===============================\n OBTAIN THE ROLES FROM THE API\n AND MAP THEM INTO DROPDOWN ITEMS\n ================================*/\n const { data: rolesData, isPending: rolesIsPending } = useAdminRolesGetAll();\n const [roles, setRoles] = useState([]);\n const [selectedRoleID, setSelectedRoleID] = useState(null);\n\n useEffect(() => {\n // Return a default empty array if there's no \"roles\" data available yet\n if (!rolesData || !rolesData.length || rolesIsPending) return;\n\n const mappedRoles: DropdownItem[] = rolesData.map((role: RolesResponseFields) => {\n return { text: role.label, value: role.id, description: role.description };\n });\n\n setRoles(mappedRoles);\n }, [rolesData]);\n\n /*===============================\n GET THE LIST OF GROUPS\n ================================*/\n const { data: groupsData, isPending: groupsIsPending } = usePrivilegesGroupsGetAll();\n const [groups, setGroups] = useState([]);\n const [preselectedGroupID, setPreselectedGroupID] = useState(null);\n\n useEffect(() => {\n if (!groupsData || !groupsData.length || groupsIsPending) return;\n\n const mappedGroups: DropdownItem[] = [...groupsData]\n .filter(group => {\n return selectedRoleID === UserRoleIDsEnum.ADMIN\n ? group.is_admin_only\n : !group.is_admin_only;\n })\n .map(group => {\n return { text: group.name, value: group.id, description: group.description };\n });\n\n setGroups(mappedGroups);\n\n // Prevent any group pre-selection if there's no selected role or if selected role is \"super admin\"\n if (!selectedRoleID || selectedRoleID === UserRoleIDsEnum.SUPER_ADMIN) return;\n\n // Handle Formik inner referece to be able to directly update field values\n if (!formRef || !formRef.current) return;\n const { setFieldValue } = formRef.current;\n\n // Handle pre-selecting the correct group based on the already selected role\n // For 'admin' (Account Manager) role, we pre-select the first available group\n // For any other selected role, we pre-select the \"Standard User\" group\n if (selectedRoleID === UserRoleIDsEnum.ADMIN) {\n setPreselectedGroupID(mappedGroups[0].value as number);\n setFieldValue(\"group_id\", mappedGroups[0].value);\n } else {\n // Find the \"Standard User\" group and pre-select it\n const groupToBePreselected = groupsData.find(group => {\n return group.name.toLowerCase() === \"standard user\";\n });\n\n // Exit and do not try to pre-select anything if group is not found\n if (!groupToBePreselected) return;\n\n setPreselectedGroupID(groupToBePreselected.id);\n setFieldValue(\"group_id\", groupToBePreselected.id);\n }\n }, [groupsData, selectedRoleID]);\n\n /*===============================\n CREATE A NEW USER\n ================================*/\n const createUser = useAdminUsersCreate();\n\n const handleCreateNewUser = async (details: AdminUsersPreRequestFields) => {\n try {\n // Construct the final request payload processing the fields before sending the API request\n const REQUEST_PAYLOAD: AdminUsersCreateRequestFields = {\n first_name: details.first_name,\n last_name: details.last_name,\n title: details.title,\n email: details.email,\n password: details.password,\n password_confirmation: details.password_confirmation,\n company_ids: details.company_ids,\n role_id: details.role_id,\n group_id: details.group_id,\n };\n\n await createUser.mutateAsync(REQUEST_PAYLOAD);\n\n // Redirect back to the users listing\n navigate(\"/account/admin/users/\");\n } catch (error) {\n errorReporting(\"Failed creating new user account from 'Admin' page\", error, { ...details });\n }\n };\n\n /*===============================\n HANDLE PASSWORD \n FIELDS VISIBILITY\n ================================*/\n const [showPassword, setShowPassword] = useState(false);\n const [showConfirmPassword, setShowConfirmPassword] = useState(false);\n\n return (\n
    \n \n\n \n \n Back\n \n\n handleCreateNewUser(details)}\n >\n {({ values, validateForm, setFieldValue }) => (\n \n \n \n\n \n \n \n {showPassword ? (\n setShowPassword(!showPassword)} />\n ) : (\n setShowPassword(!showPassword)} />\n )}\n
    \n }\n />\n \n {showConfirmPassword ? (\n setShowConfirmPassword(!showConfirmPassword)}\n />\n ) : (\n setShowConfirmPassword(!showConfirmPassword)}\n />\n )}\n \n }\n modifierClass=\"mb--15\"\n size=\"full\"\n autoComplete=\"new-password\"\n />\n {\n setFieldValue(\"role_id\", role.value);\n setSelectedRoleID(role.value as number);\n\n // Clear out the selection of the fields when a super admin role is selected\n const { SUPER_ADMIN, ADMIN } = UserRoleIDsEnum;\n if (role.value === SUPER_ADMIN) setFieldValue(\"group_id\", null);\n\n // Clear out the selection of companies anytime a super admin or admin role is selected\n if ([SUPER_ADMIN, ADMIN].includes(role.value as number)) {\n setFieldValue(\"company_ids\", []);\n }\n\n // Re-validate the form anytime the role changes, as this can trigger\n // clearing the selection of group and company selections\n validateForm();\n }}\n framerAnimationCustomProps={{ hasSideLabel: true }}\n />\n setFieldValue(\"group_id\", group.value)}\n preselectedItemValue={\n values.role_id !== UserRoleIDsEnum.SUPER_ADMIN ? preselectedGroupID : null\n }\n framerAnimationCustomProps={{ hasSideLabel: true }}\n />\n setFieldValue(\"company_ids\", companies)}\n framerAnimationCustomProps={{ hasSideLabel: true }}\n />\n \n Create User\n \n \n )}\n \n \n );\n};\n\nexport default AdminUsersNew;\n","// Assets\nimport { FaChevronLeft as BackIcon } from \"react-icons/fa\";\nimport { BiShow as ShowPasswordIcon, BiHide as HidePasswordIcon } from \"react-icons/bi\";\n\n// Utilities & Hooks\nimport { Link, useParams } from \"react-router\";\nimport {\n useAdminUsersGetSpecific,\n useAdminUserUpdateAccountDetails,\n} from \"../../../../api/Users/AdminUsers\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useAdminRolesGetAll } from \"../../../../api/Roles/AdminRoles\";\nimport useErrorReporting from \"../../../../hooks/useErrorReporting\";\nimport handleFullnameCombination from \"../../../../utilities/strings/handleFullnameCombination\";\nimport useAdminUsersCompanies from \"./hooks/useAdminUsersCompanies\";\n\n// Components\nimport { Field, Form, Formik } from \"formik\";\nimport ContentHeader from \"../../../../components/Content/ContentHeader\";\nimport Skeleton from \"react-loading-skeleton\";\nimport Button from \"../../../../components/Button/Button\";\nimport FormDropdownMultiselect from \"../../../../components/Form/FormDropdownMultiselect\";\nimport FormInputSideLabel from \"../../../../components/Form/FormInputSideLabel\";\nimport Loader from \"../../../../components/Loader/Loader\";\nimport PermissionCheckComponentWrapper from \"../../../../components/Wrappers/PermissionCheckComponentWrapper\";\nimport FormDropdownSearchable from \"../../../../components/Form/FormDropdownSearchable\";\n\n// Interfaces\nimport { AdminUsersEditFormDetailsState } from \"../interfaces\";\nimport { DropdownItem } from \"../../../../components/Dropdown/interfaces\";\nimport { RolesResponseFields } from \"../../../../api/Roles/interfaces\";\nimport { UserRoleIDsEnum } from \"../../../../interfaces/global\";\n\n// Schemas\nimport { ACCOUNT_ADMIN_USERS_EDIT_ACCOUNT_DETAILS } from \"../../../../schemas/AccountSchemas\";\n\nconst AdminUsersEdit = () => {\n const { id } = useParams();\n const errorReporting = useErrorReporting();\n\n /*=============================\n GET USER'S DETAILS\n ==============================*/\n const { data: userData, isPending: userIsPending } = useAdminUsersGetSpecific(id);\n\n const [userFullname, setUserFullname] = useState(\"\");\n const [userFormDetails, setUserFormDetails] = useState({\n first_name: \"\",\n last_name: \"\",\n title: \"\",\n email: \"\",\n company_ids: [],\n password: \"\",\n password_confirmation: \"\",\n role_id: null,\n });\n\n /*===============================\n GET THE LIST OF COMPANIES\n ================================*/\n const [companies, companiesLoading] = useAdminUsersCompanies();\n\n /*===============================\n OBTAIN THE ROLES FROM THE API\n AND MAP THEM INTO DROPDOWN ITEMS\n ================================*/\n const { data: roles, isPending: rolesIsPending } = useAdminRolesGetAll();\n\n // Map the roles into dropdown items\n const rolesDropdownItems: DropdownItem[] = useMemo(() => {\n // Return a default empty array if there's no \"roles\" data available yet\n if (!roles || !roles.length || rolesIsPending) return [];\n\n const mappedRoles: DropdownItem[] = roles.map((role: RolesResponseFields) => {\n return { text: role.label, value: role.id, description: role.description };\n });\n\n return mappedRoles;\n }, [roles]);\n\n /*===============================\n PREPOPULATE THE USER'S \n FORM FIELDS\n ================================*/\n useEffect(() => {\n // Exit function if there's no \"user\" or \"roles\" data available\n if (!userData || !roles || !roles.length) return;\n\n // Construct the user's fullname\n const fullname: string = handleFullnameCombination(userData);\n\n // Extract only the IDs of the companies to which the user belongs to\n // which will be used to pre-select the companies in the dropdown menu\n const companiesIDs: number[] = userData.companies.map(company => company.id);\n\n // Find the user's role from the list of existing roles fetched from the API\n const matchedRole: RolesResponseFields | undefined = roles.find(role => {\n return role.label.toLowerCase() === userData.role.toLowerCase();\n });\n\n // Update the user's related states\n setUserFullname(fullname);\n setUserFormDetails({\n first_name: userData.first_name,\n last_name: userData.last_name,\n title: userData.title || \"\",\n email: userData.email,\n company_ids: companiesIDs,\n role_id: matchedRole?.id ?? null,\n password: \"\",\n password_confirmation: \"\",\n });\n }, [roles, userData]);\n\n /*===============================\n UPDATE USER'S ACCOUNT DETAILS\n ================================*/\n const accountDetailsUpdate = useAdminUserUpdateAccountDetails();\n\n const handleUpdateAccountDetails = async (details: AdminUsersEditFormDetailsState) => {\n try {\n if (!id) throw new Error(\"No user ID found\");\n\n // Destructure the form details\n const {\n first_name,\n last_name,\n title,\n email,\n company_ids,\n password,\n password_confirmation,\n role_id,\n } = details;\n\n // Construct the request object by omitting the 'email' field if\n // the latest form value for this field is exactly the same as the prepoulated one\n const accountDetailsRequest = {\n first_name,\n last_name,\n title,\n company_ids,\n email,\n role_id,\n ...(password && { password }),\n ...(password_confirmation && { password_confirmation }),\n };\n\n await accountDetailsUpdate.mutateAsync({ userID: id, details: accountDetailsRequest });\n } catch (error) {\n errorReporting(\"Failed updating user's account details\", error, { user_id: id, ...details });\n }\n };\n\n /*===============================\n HANDLE PASSWORD \n FIELDS VISIBILITY\n ================================*/\n const [showPassword, setShowPassword] = useState(false);\n const [showConfirmPassword, setShowConfirmPassword] = useState(false);\n\n return (\n
    \n \n Edit User -{\" \"}\n {userIsPending ? : userFullname || \"N/A\"}\n \n }\n />\n\n \n \n Back\n \n\n {userIsPending ? (\n \n ) : (\n <>\n \n {({ values, validateForm, setFieldValue }) => (\n \n \n\n \n\n \n\n \n\n {\n setFieldValue(\"role_id\", role.value);\n\n // Clear out the selected companies if the selected role is \"super admin\" or \"admin\"\n const { SUPER_ADMIN, ADMIN } = UserRoleIDsEnum;\n if ([SUPER_ADMIN, ADMIN].includes(role.value as number)) {\n setFieldValue(\"company_ids\", []);\n }\n\n // Re-validate the form anytime the role changes, as this can trigger\n // clearing the selection of group and company selections\n validateForm();\n }}\n preselectedItemValue={values.role_id}\n framerAnimationCustomProps={{ hasSideLabel: true }}\n />\n\n {\n setFieldValue(\"company_ids\", companies);\n }}\n framerAnimationCustomProps={{ hasSideLabel: true }}\n />\n\n \n {showPassword ? (\n setShowPassword(!showPassword)} />\n ) : (\n setShowPassword(!showPassword)} />\n )}\n
    \n }\n />\n\n \n {showConfirmPassword ? (\n setShowConfirmPassword(!showConfirmPassword)}\n />\n ) : (\n setShowConfirmPassword(!showConfirmPassword)}\n />\n )}\n \n }\n />\n\n \n Save Changes\n \n \n )}\n \n\n {userData?.companies && userData.companies.length > 0 ? (\n \n
    \n\n
    \n
    \n {userIsPending ? (\n \n ) : (\n `${userFullname}'s Privileges`\n )}\n
    \n\n \n Edit User Privileges\n \n
    \n
    \n ) : null}\n \n )}\n \n );\n};\n\nexport default AdminUsersEdit;\n","export default \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAABCCAYAAAAMlmvWAAAACXBIWXMAAAsSAAALEgHS3X78AAAIW0lEQVR4nGL8//+/AQMDwwSGkQ4YGAoAAAAA//9iYWBgEGBgYLAf8WHBwCAAAAAA//9ioanxz24yMDy7wcDw/hmmnJIpA4OyCU2tJwkwMDAAAAAA//+ifmB8/8zAcGQJA8OZjQwM75/jUTgDQmk7MjDYxAx8wDAwMAAAAAD//wKVGQ4MDAz7qWLa7ukMDIeXMDD8+EK6XiUTBga/MgYGKXWqOIVkwMDgCAAAAP//ok5ggFLDjGQGhuc3KXdSWBMDg4k/5eaQChgYHAEAAAD//2Ki2AhQudDuQZ2AAIFVdQwMm7qoYxYpgIGBAQAAAP//oiwwQCliYT552QIfOLIUUubQEzAwMAAAAAD//6IsMBYWECgkKQCgFHL3DP2Cg4GBAQAAAP//Ij8wQDF3j8aOXVVDW/ORAQMDAwAAAP//Ii8wQNmDHvkalOrolV0YGBgAAAAA//8iLzCu7qN+OYELgKpregAGBgYAAAAA//8iLzDObKKbA8GpA1Rj0RowMDAAAAAA//8iLzBoXVagA1BKpDVgYGAAAAAA//8iPTDoXMLTzU4GBgYAAAAA//8iPTDeP6WJQwbcTgYGBgAAAAD//yIjMLD0QGkNaNWWQQYMDAwAAAAA//+ivDk+BMH3b/8xXc3AwAAAAAD//yI9MDh46e99QUmqGPP+9T+GGc2fGJ49+IMpycDAAAAAAP//In08Q0qDCs4iEQhKU6QdlBKObP/BsGvtdwa3YE4GZS1WTEUMDAwAAAAA//8iIzAGYLyBAjuvnPnFsGnRN4b3b/4xSMkzM7gGc2JXyMDAAAAAAP//Ij0wOHkZGCTVqddlJwaQMQoGyhIrZ3xhuHsdkiU4uBgZEorwZHEGBgYAAAAA//8irwA18SNLG1mAg4eBQduJaK2gLLF77XeGtvwP8IAAAf84LgZBUTzeZWBgAAAAAP//Im8MFDQSBeoz0KN/YhtDtFLkLIEMtI3ZGEzs2PFrZmBgAAAAAP//Ii8wQFkF5Mjd0EFdWgFQqgANFhMA6FkCGQiKMDGEZ3ITdiEDAwMAAAD//yJ/dNw1k4Hhyn7alh3hzZCAxwGQawmcRmRwM3ByMRK2i4GBAQAAAP//oqzRBXIsKPZoAYz98JYVoCzRX/ERb0Dgq0YxAAMDAwAAAP//omzeBFTlZcxjYJiRRN3yAxQQoIDGAvBlCRSnEahGMQADAwMAAAD//6K8OQ4LECq1EhlsorEGBK5aAhsgphrFAAwMDAAAAAD//6LOjBooQApWQ4YCz5I58AMKTNAkEpasgauWwAWIqUYxAAMDAwAAAP//ot70IqigA8UoqNoFjVsSGyigQAAVxlgmjojNEsiA2GoUAzAwMAAAAAD//8I5o3b1x3+Gx7/+M3jwkZmTQIPG905DhuzQB2cEpSCpSdkUa1ObmFoCGwBVo4Ud/ETXHiiAgcERAAAA///CGRif/jIwuN75zfDx338GD14mcKCQHTAkAFKzBDLIqOElqfZAAQwMjgAAAAD//8I717rj0z+G5EeIJMrLzECzgCEnSyADUDVKau2BAhgYHAEAAAD//8JbZoA8bMnNyHD8K2Qw5PNfBobVH/6BMbUChtwsgQzIqUYxAAMDAwAAAP//IjgLDyo3LG79xmsIuQFDSZaAAVA1WtTOT1btgQIYGBwBAAAA//8iWJvIsjEyFIkxM/S9+otTDakphtIsgQzIrUYxAAMDAwAAAP//Imp9BqwwffIb+9ghLoAeMNTIEsgAVI0mFFOpO8DA4AgAAAD//yJ6sQp6YUoqEPj0jUH0zkcGkeusDNJP2RnY/pBV/cEBhdUoJmBgcAQAAAD//yK60YVemBIL/v/8w8D14BXD03ffGcCzH0oQrP+fk0H0PhvZAUNKb5QowMDAAAAAAP//IqkF2i/NQrAwhYH/f/4xcL76yPD24TuGb1jkLzJ+Z2BQAmHSA4bU3ihRgIGBAQAAAP//IikwiClMQYD1w1eG/w/fMrz9RlzAkRIw1KpGMQADAwMAAAD//yK5b5IqzMyw6v0/rIUpLEu8eUd+AYkvYMjtjRIFGBgYAAAAAP//Ijkw+JgZGBolmVEKU0JZglyAHjCNauJUq0YxAAMDAwAAAP//IstkWGEKAqAswXr1CTggaAmkFVgYHBxokz3AgIGBAQAAAP//IjuYQYUp781nDB+uv2D4SGTZQC6Q5WFlmO5ApcEjXICBgQEAAAD//yI7MECFaYICcaPOlILpDlIM/GzMtLWEgYEBAAAA//+iKANm6gqBY42WoMJIhMFGkoumdoABAwMDAAAA//+iKDBAsdVhKU4916ABHWEOhgpjUZqZjwIYGBgAAAAA//+iuGj2VuBlsKZBzPGxMTEsc5Whurk4AQMDAwAAAP//oko9Nd1eiuoO67CUYJDjpW0WRAEMDAwAAAAA//+iSmCAHA3K29QCXvK8DFFq/FQzjyjAwMAAAAAA//+iWguGWoUpvapRDMDAwAAAAAD//6JaYFCrMKVXNYoBGBgYAAAAAP//omrbltLClJ7VKAZgYGAAAAAA//+iekOf3MKU3tUoBmBgYAAAAAD//6J6YJBTmA5ENYoBGBgYAAAAAP//okkXkNTCdCCqUQzAwMAAAAAA//+iSWCQUpgOVDWKARgYGAAAAAD//6LZ4AAxhelAVqMYgIGBAQAAAP//ounkKaHCdCCrUQzAwMAAAAAA//+iaWDgK0wHuhrFAAwMDAAAAAD//6L5tDq2wnQwVKMYgIGBAQAAAP//onlgoBemg6UaxQAMDAwAAAAA//+iyxYL5MJ0sFSjGICBgQEAAAD//wKNjn9gYGA4SGuL+mwkOIqPvFCOUuO/Smu7yAIMDB8AAAAA//8DACWKO3Ya3JOdAAAAAElFTkSuQmCC\"","import * as React from \"react\";\nconst SvgApplicationsIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 34.059, height: 19.368, viewBox: \"0 0 34.059 19.368\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(-4188.461 -2390.152)\" }, /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4213.434,2409.52h-15.84a6.524,6.524,0,0,1,3.568-7.066,10.306,10.306,0,0,1,8.511-.062A6.585,6.585,0,0,1,4213.434,2409.52Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4209.9,2394.823a4.428,4.428,0,1,1-4.42-4.671A4.527,4.527,0,0,1,4209.9,2394.823Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4222.5,2407.351h-6.764a8.645,8.645,0,0,0-2.6-5.235,7.169,7.169,0,0,1,7.822.5C4222.323,2403.847,4222.607,2405.49,4222.5,2407.351Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4197.9,2402.076a8.664,8.664,0,0,0-2.634,5.262h-6.729a4.917,4.917,0,0,1,3.345-5.75A7.687,7.687,0,0,1,4197.9,2402.076Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4219.871,2396.278a3.674,3.674,0,0,1-3.55,3.734,3.726,3.726,0,0,1-3.584-3.725,3.68,3.68,0,0,1,3.608-3.751A3.639,3.639,0,0,1,4219.871,2396.278Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4191.136,2396.295a3.552,3.552,0,1,1,7.094,0,3.551,3.551,0,1,1-7.094,0Z\" })));\nexport default SvgApplicationsIcon;\n","import * as React from \"react\";\nconst SvgAppointmentsIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 22.5, height: 25, viewBox: \"0 0 22.5 25\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(-4249.376 -2388.469)\" }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4266.248,2390.9c.31.029.565.033.81.08a3.124,3.124,0,0,1,2.593,3.207q.007,4.522-.006,9.046a.822.822,0,0,0,.328.7,5.389,5.389,0,0,1,.208,8.035,5.452,5.452,0,0,1-7.944-.465.8.8,0,0,0-.7-.334c-2.907.012-5.813,0-8.72.01a3.3,3.3,0,0,1-2.634-1.066,3.022,3.022,0,0,1-.8-2.05q-.012-7.011,0-14.024a3.133,3.133,0,0,1,3.024-3.11c.107,0,.215,0,.376,0,0-.3,0-.571,0-.844a1.58,1.58,0,1,1,3.16-.011c.005.265,0,.53,0,.823h7.12c0-.259,0-.519,0-.78a1.591,1.591,0,0,1,3.179-.127C4266.268,2390.289,4266.248,2390.59,4266.248,2390.9Zm-4.874,19.214a5.478,5.478,0,0,1,7.212-7.111v-6.089h-18.144c-.008.119-.019.214-.019.309q0,5.286,0,10.57a3.022,3.022,0,0,0,.075.719,2.145,2.145,0,0,0,2.3,1.6q4.106,0,8.209,0Zm9.451-2.058a4.374,4.374,0,0,0-4.335-4.4,4.4,4.4,0,1,0,4.335,4.4Zm-5.617-16.621c0-.423,0-.847,0-1.27s-.2-.672-.539-.674-.548.265-.55.671q0,1.289,0,2.577c0,.383.192.6.517.611a.551.551,0,0,0,.57-.609C4265.214,2392.3,4265.208,2391.869,4265.208,2391.433Zm-11.371,0c0,.436,0,.872,0,1.307.005.4.184.6.518.605a.542.542,0,0,0,.566-.611q.012-1.326,0-2.652a.536.536,0,0,0-.543-.6.557.557,0,0,0-.541.6C4253.831,2390.54,4253.837,2390.989,4253.837,2391.437Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4266.043,2408.029V2406.9c0-.774-.007-.548.007-1.322a.5.5,0,0,1,.51-.546.511.511,0,0,1,.529.531c.011,1.33.013,1.66,0,2.991a.491.491,0,0,1-.541.513c-.98,0-.96.006-1.939,0a.518.518,0,0,1-.568-.524.533.533,0,0,1,.585-.512c.654-.007.307,0,.96,0Z\" }))));\nexport default SvgAppointmentsIcon;\n","import * as React from \"react\";\nconst SvgCommunicationIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 15.681, height: 15.685, viewBox: \"0 0 15.681 15.685\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(-0.063)\" }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M121.212,0V14.505a1.176,1.176,0,1,1-2.352,0V2.842a.49.49,0,0,0-.49-.49h-9.507V0Z\", transform: \"translate(-105.468)\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M12.73,121.1a2.16,2.16,0,0,1-.183-.375c-.194-.525-.135.182-.135-11.924H.063v11.173a1.178,1.178,0,0,0,1.176,1.176c12.418,0,11.537.025,11.491-.05Zm-6.836-1.4a.49.49,0,0,1-.49.49H1.517a.49.49,0,0,1-.49-.49v-3.888a.49.49,0,0,1,.49-.49H5.4a.49.49,0,0,1,.49.49Zm5.064.49H7.348a.49.49,0,1,1,0-.98h3.61a.49.49,0,1,1,0,.98Zm0-1.944H7.348a.49.49,0,1,1,0-.98h3.61a.49.49,0,1,1,0,.98Zm0-1.944H7.348a.49.49,0,0,1,0-.98h3.61a.49.49,0,0,1,0,.98Zm.49-2.434a.49.49,0,0,1-.49.49H1.517a.49.49,0,0,1-.49-.49v-3.61a.49.49,0,0,1,.49-.49h9.441a.49.49,0,0,1,.49.49Z\", transform: \"translate(0 -105.468)\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M63.533,172.27h8.461v2.63H63.533Z\", transform: \"translate(-61.526 -166.994)\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M63.533,353.6H66.44v2.907H63.533Z\", transform: \"translate(-61.526 -342.77)\" })));\nexport default SvgCommunicationIcon;\n","import * as React from \"react\";\nconst SvgClientsIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 16.219, height: 14.408, viewBox: \"0 0 16.219 14.408\", ...props }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M584.891,6124.579a.574.574,0,0,1-.157.361c-.657.65-1.324,1.288-1.995,1.923a.484.484,0,0,1-.3.14c-1.335.009-2.1.006-3.43.006a1.044,1.044,0,0,1-.164,0v-.975h-4.012v.975c-.1.007-.185,0-.265,0-1.285,0-1.994,0-3.279-.006a.56.56,0,0,1-.347-.14c-.685-.643-1.362-1.3-2.036-1.952a.376.376,0,0,1-.119-.229c-.006-1.193,0-1.233,0-2.425,0-.015.012-.03.026-.06H584.9Z\", transform: \"translate(-568.729 -6118.714)\", fill: \"#888\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M584.948,6132.577a.541.541,0,0,1-.548.545H569.283a.544.544,0,0,1-.548-.494c0-2.024,0-3.472,0-5.5a.335.335,0,0,1,0-.12c.591.568,1.182,1.1,1.732,1.651a.944.944,0,0,0,.745.306c1.294-.021,2.011-.009,3.3-.009h.3v1.261h4.03v-1.261h.294c1.344,0,2.112,0,3.455.006a.686.686,0,0,0,.528-.217c.557-.548,1.122-1.086,1.685-1.627.029-.028.061-.054.118-.1Z\", transform: \"translate(-568.729 -6118.714)\", fill: \"#888\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M573.372,6118.714h6.938l.854,2.217c-.343,0-.651.008-.958-.008-.054,0-.125-.094-.153-.16q-.18-.429-.336-.867a.243.243,0,0,0-.267-.194q-2.607.009-5.215,0a.241.241,0,0,0-.269.191c-.1.3-.222.6-.346.89a.253.253,0,0,1-.166.142c-.3.014-.6.007-.937.007Z\", transform: \"translate(-568.729 -6118.714)\", fill: \"#888\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M576.205,6127.389h1.272v1.485h-1.272Z\", transform: \"translate(-568.729 -6118.714)\", fill: \"#888\" }));\nexport default SvgClientsIcon;\n","import * as React from \"react\";\nconst SvgMessagesIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 15.988, height: 12.433, viewBox: \"0 0 15.988 12.433\", ...props }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M636.181,6124.7c0-2.451-2.585-4.441-5.773-4.441s-5.774,1.987-5.774,4.441a3.762,3.762,0,0,0,1.055,2.554,6.253,6.253,0,0,1-.994,1.513.221.221,0,0,0-.041.242.218.218,0,0,0,.2.133,4.89,4.89,0,0,0,2.462-.695,7.073,7.073,0,0,0,3.089.694C633.6,6129.144,636.181,6127.157,636.181,6124.7Zm3.386,6.107a3.757,3.757,0,0,0,1.055-2.556c0-1.857-1.485-3.447-3.589-4.111a4.013,4.013,0,0,1,.036.558c0,2.942-2.991,5.33-6.661,5.33a8.262,8.262,0,0,1-.88-.053,6,6,0,0,0,5.321,2.717,7,7,0,0,0,3.088-.694,4.883,4.883,0,0,0,2.463.694.217.217,0,0,0,.2-.133.221.221,0,0,0-.042-.242,6.181,6.181,0,0,1-.993-1.51Z\", transform: \"translate(-624.634 -6120.262)\", fill: \"#888\" }));\nexport default SvgMessagesIcon;\n","import * as React from \"react\";\nconst SvgAdManagerIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 17.004, height: 14.133, viewBox: \"0 0 17.004 14.133\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(0.5)\" }, /* @__PURE__ */ React.createElement(\"rect\", { width: 16.004, height: 11.459, transform: \"translate(0 2.174)\", strokeWidth: 1, stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", fill: \"none\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M550.011,6119.249\", transform: \"translate(-542.108 -6119.249)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { x2: 15.806, transform: \"translate(0.198 4.94)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { x2: 15.806, transform: \"translate(0.198 6.916)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { x2: 3.952, transform: \"translate(2.371 11.262)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 })));\nexport default SvgAdManagerIcon;\n","import * as React from \"react\";\nconst SvgPrivilegesIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 14.316, height: 14.896, viewBox: \"0 0 14.316 14.896\", className: \"header-dropdown__link__icon--privileges\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(0.5 0.737)\" }, /* @__PURE__ */ React.createElement(\"circle\", { cx: 1.166, cy: 1.166, r: 1.166, transform: \"translate(2.489 2.498)\", strokeWidth: 1, stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", fill: \"none\" }), /* @__PURE__ */ React.createElement(\"line\", { x2: 3.416, y2: 3.416, transform: \"translate(8.075 7.133)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M728.943,6129.3l-3.116-3.1-1.031-1.031a4.663,4.663,0,1,0-3.115,3.246l1.2,1.2,1.414.3.353,1.462,1.3.187.333,1.395h2.666Z\", transform: \"translate(-715.627 -6119.31)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 })));\nexport default SvgPrivilegesIcon;\n","import * as React from \"react\";\nconst SvgReportsIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { id: \"ReportsIcon_neutral\", xmlns: \"http://www.w3.org/2000/svg\", width: 14.005, height: 16.5, viewBox: \"0 0 14.005 16.5\", ...props }, /* @__PURE__ */ React.createElement(\"path\", { id: \"Path_1063\", \"data-name\": \"Path 1063\", d: \"M671.489,6120.285H674.2c0,.291,0,1.205,0,1.481,0,.359.13.508.483.509q2.829.009,5.656,0c.36,0,.478-.138.484-.5,0-.277,0-1.193,0-1.486h2.716v3.916a1.326,1.326,0,0,0-.938-.23q-2.517,0-5.034,0c-.545,0-.641.1-.641.653v10.249h-5.435Z\", transform: \"translate(-671.489 -6118.372)\", fill: \"#888\" }), /* @__PURE__ */ React.createElement(\"path\", { id: \"Path_1064\", \"data-name\": \"Path 1064\", d: \"M677.135,6133.959V6124.5h4.022c0,.094,0,.188,0,.283,0,.768.011,1.535.015,2.3,0,.4.112.512.52.514.758,0,1.516,0,2.274,0h.305v6.364Zm3.23-4.267h1.812c.315,0,.631,0,.947,0a.385.385,0,0,0,.021-.77,2.121,2.121,0,0,0-.215-.012c-1.7,0-2.769,0-4.474,0a1.137,1.137,0,0,0-.294.028.371.371,0,0,0-.249.512.426.426,0,0,0,.442.245C679.236,6129.69,679.482,6129.693,680.365,6129.693Zm0,3.113c.893,0,1.786,0,2.679,0,.29,0,.465-.145.472-.375s-.184-.4-.477-.4H678.37c-.307,0-.487.144-.493.38s.171.395.5.395Zm.021-2.327c-.333,0-.667,0-1,0-.578,0-.516,0-1.093,0a.373.373,0,0,0-.41.372.4.4,0,0,0,.4.4,1.131,1.131,0,0,0,.135,0c1.74,0,2.841,0,4.581,0,.327,0,.513-.138.521-.383s-.18-.392-.506-.392C682.132,6130.478,681.258,6130.479,680.383,6130.479Zm-1.3-2.349c.468,0,.936,0,1.4,0,.3,0,.487-.151.489-.383s-.185-.391-.481-.392c-.927,0-1.215,0-2.143,0-.292,0-.465.142-.472.369-.007.247.164.4.464.405C678.8,6128.134,678.625,6128.13,679.084,6128.13Z\", transform: \"translate(-670.266 -6117.46)\", fill: \"#888\" }), /* @__PURE__ */ React.createElement(\"path\", { id: \"Path_1065\", \"data-name\": \"Path 1065\", d: \"M674.888,6120.626v-.954c.262,0-.127,0,.123,0,.354,0,.493-.142.5-.488,0-.152,0-.3,0-.471h2.537c0,.161,0,.32,0,.478.007.338.144.475.477.481.243,0-.151,0,.092.008.007,0,.015.01.038.027v.919Z\", transform: \"translate(-670.753 -6118.713)\", fill: \"#888\" }), /* @__PURE__ */ React.createElement(\"path\", { id: \"Path_1066\", \"data-name\": \"Path 1066\", d: \"M681.1,6124.7l2.046,2.059H681.1Z\", transform: \"translate(-669.406 -6117.417)\", fill: \"#888\" }));\nexport default SvgReportsIcon;\n","import * as React from \"react\";\nconst SvgJobBoardIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 9.085, height: 14.782, viewBox: \"0 0 9.085 14.782\", className: \"header-dropdown__link__icon--job-positions\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(0.5 0.5)\" }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M603.2,6122.2l-1.47-1.838h6.615l-1.47,1.838\", transform: \"translate(-600.997 -6120.365)\", fill: \"#888\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M607.058,6125.623l2.205,2.389h-8.085l2.205-2.389\", transform: \"translate(-601.178 -6118.64)\", fill: \"#888\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { y1: 5.145, transform: \"translate(2.205 1.838)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { y2: 5.145, transform: \"translate(5.88 1.838)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 }), /* @__PURE__ */ React.createElement(\"line\", { y2: 4.41, transform: \"translate(4.043 9.372)\", fill: \"none\", stroke: \"#888\", strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 1 })));\nexport default SvgJobBoardIcon;\n","import * as React from \"react\";\nconst SvgSendSbcaRequestIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { id: \"Layer_2\", \"data-name\": \"Layer 2\", xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 20.18 17.41\", ...props }, /* @__PURE__ */ React.createElement(\"defs\", null, /* @__PURE__ */ React.createElement(\"style\", null, \"\\n .icon-sbca-request {\\n fill: none;\\n stroke: #999;\\n stroke-miterlimit: 10;\\n }\\n \")), /* @__PURE__ */ React.createElement(\"g\", { id: \"Layer_1-2\", \"data-name\": \"Layer 1\" }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"polygon\", { className: \"icon-sbca-request\", points: \"19.42 .81 1.22 7.4 6.55 10.28 6.55 15.56 8.77 11.75 15.03 15.87 19.42 .81\" }), /* @__PURE__ */ React.createElement(\"polyline\", { className: \"icon-sbca-request\", points: \"8.77 11.75 17.54 2.69 8.09 9.32\" }), /* @__PURE__ */ React.createElement(\"line\", { className: \"icon-sbca-request\", x1: 6.55, y1: 15.56, x2: 10.75, y2: 13.05 }))));\nexport default SvgSendSbcaRequestIcon;\n","import * as React from \"react\";\nconst SvgEmailTemplatesIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 15.06 15.06\", className: \"header-dropdown__link__icon--email-templates\", ...props }, /* @__PURE__ */ React.createElement(\"defs\", null, /* @__PURE__ */ React.createElement(\"style\", null, \"\\n .email-template-icon {\\n fill: none;\\n stroke: #999;\\n stroke-miterlimit: 10;\\n }\\n \")), /* @__PURE__ */ React.createElement(\"g\", { id: \"Layer_1-2\", \"data-name\": \"Layer 1\" }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"path\", { className: \"email-template-icon\", d: \"M2.44,8.53V.74c0-.13.11-.24.24-.24h9.7c.13,0,.24.11.24.24v7.79\" }), /* @__PURE__ */ React.createElement(\"path\", { className: \"email-template-icon\", d: \"M12.62,4.71l1.76,1.46c.11.09.18.23.18.37v7.29c0,.4-.33.73-.73.73H1.23c-.4,0-.73-.33-.73-.73v-7.29c0-.14.06-.28.18-.37l1.76-1.46\" }), /* @__PURE__ */ React.createElement(\"path\", { className: \"email-template-icon\", d: \"M1.95,13.35l5.28-4.13c.18-.14.42-.14.6,0l5.28,4.13\" }), /* @__PURE__ */ React.createElement(\"line\", { className: \"email-template-icon\", x1: 9.21, y1: 10.3, x2: 14.56, y2: 7.53 }), /* @__PURE__ */ React.createElement(\"line\", { className: \"email-template-icon\", x1: 0.5, y1: 7.53, x2: 5.85, y2: 10.3 }), /* @__PURE__ */ React.createElement(\"path\", { className: \"email-template-icon\", d: \"M11.17.5v2.85s-.06.08-.1.05l-1.12-.82-1.12.82s-.1,0-.1-.05V.5\" }), /* @__PURE__ */ React.createElement(\"line\", { className: \"email-template-icon\", x1: 3.89, y1: 2.2, x2: 5.35, y2: 2.2 }), /* @__PURE__ */ React.createElement(\"line\", { className: \"email-template-icon\", x1: 3.89, y1: 4.86, x2: 11.17, y2: 4.86 }), /* @__PURE__ */ React.createElement(\"line\", { className: \"email-template-icon\", x1: 3.89, y1: 6.8, x2: 11.17, y2: 6.8 }))));\nexport default SvgEmailTemplatesIcon;\n","import * as React from \"react\";\nconst SvgVideoConferenceIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 17.63 10.8\", strokeWidth: 3, ...props }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"path\", { d: \"M17.63,10.8l-5.5-3.14V3.15l5.5-3.15v10.8ZM12.86,7.24l4.05,2.31V1.26l-4.05,2.31v3.67Z\" }), /* @__PURE__ */ React.createElement(\"path\", { d: \"M10.3,10.54H1.36c-.75,0-1.36-.7-1.36-1.56V1.82C0,.96.61.26,1.36.26h8.95c.75,0,1.36.7,1.36,1.56v7.16c0,.86-.61,1.56-1.36,1.56ZM1.36.99c-.35,0-.63.37-.63.83v7.16c0,.46.28.83.63.83h8.95c.35,0,.63-.37.63-.83V1.82c0-.45-.29-.83-.63-.83H1.36Z\" }))));\nexport default SvgVideoConferenceIcon;\n","import * as React from \"react\";\nconst SvgResourcesIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 12.31 16.98\", className: \"header-dropdown__link__icon--resources\", ...props }, /* @__PURE__ */ React.createElement(\"defs\", null, /* @__PURE__ */ React.createElement(\"style\", null, \"\\n .resources-icon {\\n fill: none;\\n stroke: #999;\\n stroke-miterlimit: 10;\\n }\\n \")), /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"rect\", { className: \"resources-icon\", x: 0.5, y: 1.36, width: 11.31, height: 15.12 }), /* @__PURE__ */ React.createElement(\"path\", { className: \"resources-icon\", d: \"M7.81.5h-3.22c-.45,0-.82.37-.82.82v.03c0,.47.38.86.86.86h3.15c.47,0,.86-.38.86-.86v-.03c0-.45-.37-.82-.82-.82h0Z\" })), /* @__PURE__ */ React.createElement(\"line\", { className: \"resources-icon\", x1: 10, y1: 4.51, x2: 5.97, y2: 4.51 }), /* @__PURE__ */ React.createElement(\"polyline\", { className: \"resources-icon\", points: \"4.55 3.75 3.03 5.27 2.22 4.46\" }), /* @__PURE__ */ React.createElement(\"line\", { className: \"resources-icon\", x1: 10, y1: 7.68, x2: 5.97, y2: 7.68 }), /* @__PURE__ */ React.createElement(\"polyline\", { className: \"resources-icon\", points: \"4.55 6.92 3.03 8.44 2.22 7.63\" }), /* @__PURE__ */ React.createElement(\"line\", { className: \"resources-icon\", x1: 10, y1: 10.85, x2: 5.97, y2: 10.85 }), /* @__PURE__ */ React.createElement(\"polyline\", { className: \"resources-icon\", points: \"4.55 10.09 3.03 11.61 2.22 10.81\" }), /* @__PURE__ */ React.createElement(\"line\", { className: \"resources-icon\", x1: 10, y1: 14.02, x2: 5.97, y2: 14.02 }), /* @__PURE__ */ React.createElement(\"polyline\", { className: \"resources-icon\", points: \"4.55 13.26 3.03 14.78 2.22 13.98\" }))));\nexport default SvgResourcesIcon;\n","import AppointmentsIcon from \"../../assets/images/icons/appointments-icon.svg?react\";\nimport ApplicationsIcon from \"../../assets/images/icons/applications-icon.svg?react\";\nimport ArticlesIcon from \"../../assets/images/icons/communication-icon.svg?react\";\nimport ClientsIcon from \"../../assets/images/icons/clients-icon.svg?react\";\nimport MessagesIcon from \"../../assets/images/icons/messages-icon.svg?react\";\nimport AdManagerIcon from \"../../assets/images/icons/ad-manager-icon.svg?react\";\nimport PrivilegesIcon from \"../../assets/images/icons/privileges-icon.svg?react\";\nimport ReportsIcon from \"../../assets/images/icons/reports-icon.svg?react\";\nimport JobPositionsIcon from \"../../assets/images/icons/job-board-icon.svg?react\";\nimport SendSBCARequestIcon from \"../../assets/images/icons/send-sbca-request-icon.svg?react\";\nimport EmailTemplatesIcon from \"../../assets/images/icons/email-templates-icon.svg?react\";\nimport VideoConferenceIcon from \"../../assets/images/icons/video-conference-icon.svg?react\";\nimport ResourcesIcon from \"../../assets/images/icons/resources-icon.svg?react\";\nimport { AiOutlineNotification as MarketingAnnouncementsIcon } from \"react-icons/ai\";\n\nimport { HeaderGroupRouteProps, HeaderAdminDropdownRouteProps } from \"./interfaces\";\n\nexport const ACCOUNT_DROPDOWN_MY_ACCOUNT: HeaderGroupRouteProps = {\n title: \"My Account\",\n items: [\n {\n path: \"/account/profile/\",\n text: \"Profile Settings\",\n permissions: [\"*\"],\n },\n {\n path: \"/account/users/\",\n text: \"Users\",\n permissions: [\"applicant_dashboard_users_edit\"],\n },\n {\n path: \"/account/admin/users/\",\n text: \"Users Admin\",\n permissions: [\"users_admin_view\"],\n },\n ],\n};\n\nexport const ACCOUNT_DROPDOWN_SETTINGS: HeaderGroupRouteProps = {\n title: \"Settings\",\n items: [\n {\n path: \"/account/manage-alerts/\",\n text: \"Manage Alerts\",\n permissions: [\"ad_alerts_manage\"],\n },\n ],\n};\n\nexport const HEADER_DROPDOWN_HELP: HeaderGroupRouteProps = {\n title: \"Need Help?\",\n items: [\n {\n externalPath: true,\n path: \"https://firstchoicehiring.com/helpdesk\",\n text: \"Contact Support\",\n permissions: [\"*\"],\n },\n {\n externalPath: true,\n path: \"tel:+18889906451\",\n text: \"Sales 1-888-990-6451\",\n permissions: [\"*\"],\n },\n {\n externalPath: true,\n path: \"tel:+18774497595\",\n text: \"Support 1-877-449-7595\",\n permissions: [\"*\"],\n },\n ],\n};\n\nexport const ADMIN_MENU_DROPDOWN: HeaderAdminDropdownRouteProps[] = [\n {\n path: \"/job-ads/overview/\",\n text: \"Ad Manager\",\n icon: ,\n permissions: [\"ad_manager_view\"],\n },\n {\n path: \"/appointments/\",\n text: \"Appointments\",\n icon: ,\n permissions: [\"applicant_appointments_view\"],\n },\n {\n path: \"/applications/\",\n text: \"Applications\",\n icon: ,\n permissions: [\"applicant_dashboard_view\"],\n },\n {\n path: \"/articles/\",\n text: \"Articles\",\n icon: ,\n permissions: [\"articles_user_view\"],\n },\n {\n path: \"/articles/overview/\",\n text: \"Articles Overview\",\n icon: ,\n permissions: [\"articles_view\"],\n },\n {\n path: \"/job-positions/\",\n text: \"Job Positions\",\n icon: ,\n permissions: [\"company_job_position_view\"],\n },\n {\n path: \"/admin/clients/\",\n text: \"Clients\",\n icon: ,\n permissions: [\"client_admin_view\"],\n },\n {\n path: \"/notification-logs/\",\n text: \"Notification Logs\",\n icon: ,\n permissions: [\"messages_view\"],\n },\n {\n path: \"/reports/\",\n text: \"Reports\",\n icon: ,\n permissions: [\"reports_view\", \"view_company_reports\"],\n },\n {\n path: \"/account/email-templates/\",\n text: \"Email Templates\",\n icon: ,\n permissions: [\"*\"],\n },\n {\n path: \"/account/vidconf/\",\n text: \"Video Conferencing\",\n icon: ,\n permissions: [\"*\"],\n },\n {\n path: \"/account/resources/\",\n text: \"Resources\",\n icon: ,\n permissions: [\"*\"],\n },\n {\n path: \"/privileges/\",\n text: \"Privileges\",\n icon: ,\n permissions: [\"privileges_view\"],\n },\n {\n path: \"/assessment/request/\",\n text: \"Send SBCA Request\",\n icon: ,\n permissions: [\"send_sbca_link_request\"],\n },\n {\n path: \"/marketing/banners\",\n text: \"Announcement Banners\",\n icon: ,\n permissions: [\"marketing_banners_view\"],\n },\n];\n","import React, { Fragment } from \"react\";\n\n// Interfaces\nimport { HeaderDropdownGroupProps } from \"./interfaces\";\n\n// Components\nimport { Link } from \"react-router\";\nimport PermissionCheckComponentWrapper from \"../Wrappers/PermissionCheckComponentWrapper\";\n\nconst HeaderDropdownGroup: React.FC = ({ data }) => {\n return (\n
  • \n
    {data.title}
    \n
    \n {data.items.map((link, index) => (\n \n \n {link.externalPath ? (\n \n {link.text}\n \n ) : (\n \n {link.text}\n \n )}\n \n \n ))}\n
    \n
  • \n );\n};\nexport default HeaderDropdownGroup;\n","import { useEffect, useRef, useState } from \"react\";\n\n// Providers\nimport { useAuth } from \"../../providers/auth-context\";\n\n// Hooks & Utils\nimport { LocalStorageActions } from \"../../utilities/handleLocalStorage\";\nimport { useAuthenticationLogout } from \"../../api/Authentication/Authentication\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Interfaces\nimport { HeaderDropdownGeneralProps } from \"./interfaces\";\n\n// Statics\nimport { ACCOUNT_DROPDOWN_MY_ACCOUNT, ACCOUNT_DROPDOWN_SETTINGS } from \"./statics\";\n\n// Components\nimport { useLocation, useNavigate } from \"react-router\";\nimport Button from \"../Button/Button\";\nimport Loader from \"../Loader/Loader\";\nimport HeaderDropdownGroup from \"./HeaderDropdownGroup\";\n\n// Assets\nimport { FaChevronDown as ChevronIcon } from \"react-icons/fa\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport {\n FRAMER_HEADER_TRANSITIONS,\n FRAMER_HEADER_ANIMATION_WITH_OFFSET,\n} from \"../../constants/framer\";\n\nconst HeaderDropdownAccount: React.FC = () => {\n const navigate = useNavigate();\n const { user } = useAuth();\n const errorReporting = useErrorReporting();\n\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n /*======================\n Close dropdown on click outside\n =======================*/\n const dropdownRef = useRef(null);\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*================================\n LOGOUT THE USER\n\n Logs out the user out of the application,\n removes related saved fields in local storage and\n updates the authentication context\n =================================*/\n const authenticationLogout = useAuthenticationLogout();\n\n const handleLogout = async () => {\n try {\n await authenticationLogout.mutateAsync();\n\n // Remove the chats that were stored in local storage\n LocalStorageActions.removeItem(\"fch-chats\");\n\n // Once successfully redirected, clear the location's state\n navigate(\"/login/\", { state: undefined });\n } catch (error) {\n errorReporting(\"Failed logging out the user\", error, { user_id: user.id });\n }\n };\n\n /*============================\n CLOSE MENU ON ROUTE CHANGE\n =============================*/\n const location = useLocation();\n\n useEffect(() => {\n setIsDropdownOpen(false);\n }, [location.pathname]);\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n
    \n setIsDropdownOpen(!isDropdownOpen)}\n className={`header-dropdown__body ${isDropdownOpen ? \"header-dropdown__body--active\" : \"\"}`}\n data-testid=\"components:dropdown-header-account\"\n >\n {user.first_name ? (\n {user.first_name}\n ) : (\n \n Loading...\n \n \n )}\n\n \n
    \n\n \n {isDropdownOpen && (\n \n \n\n \n\n {/*LOG OUT*/}\n
  • \n \n Logout\n \n
  • \n \n )}\n
    \n \n );\n};\nexport default HeaderDropdownAccount;\n","import React, { useRef, useState } from \"react\";\n\n// Hooks & Utils\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\nimport PermissionCheckComponentWrapper from \"../Wrappers/PermissionCheckComponentWrapper\";\nimport { useAuth } from \"../../providers/auth-context\";\n\n// Interfaces\nimport { HeaderDropdownGeneralProps } from \"./interfaces\";\n\n// Statics\nimport { ADMIN_MENU_DROPDOWN } from \"./statics\";\nimport { FRAMER_HEADER_TRANSITIONS, FRAMER_HEADER_ANIMATION_COMMON } from \"../../constants/framer\";\n\n// Assets\nimport { FaEllipsisH, FaChevronDown as ChevronIcon } from \"react-icons/fa\";\n\n// Components\nimport { Link } from \"react-router\";\nimport useWindowResize from \"../../hooks/useWindowResize\";\n\nconst HeaderDropdownAdminMenu: React.FC = ({ modifierClass = \"\" }) => {\n const { user } = useAuth();\n\n /*======================\n Close dropdown on click outside\n =======================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n const dropdownRef = useRef(null);\n\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n // Use the window width to control the text shown for the menu\n const [windowWidth] = useWindowResize(100, window.innerWidth);\n\n return (\n \n setIsDropdownOpen(!isDropdownOpen)}\n >\n {windowWidth > 767 ? (\n \n ) : (\n <>\n Application Links\n \n \n )}\n \n\n \n {isDropdownOpen && (\n \n {ADMIN_MENU_DROPDOWN.map((link, index) => {\n if (link.path === \"/assessment/request/\" && user.active_company.disable_sbca) return;\n\n return (\n \n setIsDropdownOpen(false)}\n >\n {link.icon}\n {link.text}\n \n \n );\n })}\n \n )}\n \n \n );\n};\nexport default HeaderDropdownAdminMenu;\n","import * as React from \"react\";\nconst SvgBriefcase = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 25, height: 23.069, viewBox: \"0 0 25 23.069\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { transform: \"translate(-4300.993 -2388.121)\" }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4325.992,2402.264a6.249,6.249,0,1,0-8.766,8.876,2.741,2.741,0,0,1-.294.032q-7.2,0-14.39,0a1.445,1.445,0,0,1-1.548-1.55q0-7.731,0-15.462a1.429,1.429,0,0,1,1.535-1.557c1.652-.01,3.305,0,4.957,0h.463c0-.285,0-1.55,0-1.815a2.617,2.617,0,0,1,2.672-2.663q2.848,0,5.7,0a2.62,2.62,0,0,1,2.657,2.643c0,.259,0,1.517,0,1.834h5.327a1.486,1.486,0,0,1,1.69,1.674q0,3.792,0,7.584Zm-8.659-9.685c0-.289,0-1.535,0-1.781a1.379,1.379,0,0,0-.038-.255.957.957,0,0,0-.94-.79q-2.9-.009-5.807,0a.948.948,0,0,0-.958.854c-.031.314,0,1.634,0,1.972Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#fff\", d: \"M4324.348,2408.61c.424.42.851.839,1.274,1.261a.715.715,0,0,1,.086,1.1.734.734,0,0,1-1.107-.07c-.365-.354-.733-.707-1.075-1.083a.43.43,0,0,0-.6-.111,3.483,3.483,0,1,1,1.717-1.661C4324.559,2408.227,4324.458,2408.4,4324.348,2408.61Zm-2.864-4.3a2.217,2.217,0,1,0,2.194,2.281A2.237,2.237,0,0,0,4321.484,2404.309Z\" }))));\nexport default SvgBriefcase;\n","import React, { useEffect, useRef, useState } from \"react\";\nimport { Link, useLocation } from \"react-router\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n// Hooks & Utils\nimport { useAuth } from \"../../providers/auth-context\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Interfaces\nimport { HeaderDropdownGeneralProps } from \"./interfaces\";\n\n// Assets\nimport BriefcaseIcon from \"../../assets/images/icons/briefcase.svg?react\";\n\n// Components\nimport Tooltip from \"../Tooltip/Tooltip\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport { FRAMER_HEADER_ANIMATION_COMMON, FRAMER_HEADER_TRANSITIONS } from \"../../constants/framer\";\nimport { UserRoleNames } from \"../../interfaces/global\";\n\nconst HeaderDropdownWebsite: React.FC = ({ modifierClass = \"\" }) => {\n const location = useLocation();\n\n /*======================\n IN-HOUSE APPLICATION\n FORM LINK\n =======================*/\n const { user } = useAuth();\n\n /*======================\n HANDLE DROPDOWN MENU\n =======================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n const dropdownRef = useRef(null);\n\n useEffect(() => {\n setIsDropdownOpen(false);\n }, [location.pathname]);\n\n // Close menu if clicked outside\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n \n \n setIsDropdownOpen(!isDropdownOpen)}\n className={`header-dropdown__body ${\n isDropdownOpen ? \"header-dropdown__body--active\" : \"\"\n }`}\n data-testid=\"component:dropdown-header-website\"\n >\n \n \n\n \n {isDropdownOpen && (\n \n
  • \n
    \n \n Standard Application\n \n\n {user.role === UserRoleNames.SUPER_ADMIN ? (\n \n Custom Application\n \n ) : null}\n
    \n
  • \n \n )}\n
    \n \n \n );\n};\nexport default HeaderDropdownWebsite;\n","import * as React from \"react\";\nconst SvgBell = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 21.13, height: 23.972, viewBox: \"0 0 21.13 23.972\", ...props }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M1075.409,1895.958a.868.868,0,0,0-.2-.5c-.731-.807-1.487-1.588-2.217-2.4a.9.9,0,0,1-.206-.538c-.016-1.555,0-3.111-.011-4.666a8.4,8.4,0,0,0-5.575-8.02.444.444,0,0,1-.374-.529c.024-.884.008-1.768.008-2.657h-3.961c0,.875-.018,1.724.009,2.571a.534.534,0,0,1-.435.639,8.352,8.352,0,0,0-5.5,7.714c-.037,1.659-.007,3.319-.026,4.979a.894.894,0,0,1-.209.537c-.73.809-1.481,1.595-2.22,2.394a.6.6,0,0,0-.192.312c-.017.66-.008,1.322-.008,2.022h7.9a2.787,2.787,0,0,0,2.6,2.8c1.441.031,2.456-1.005,2.733-2.823h7.889C1075.419,1897.154,1075.433,1896.556,1075.409,1895.958Zm-10.553-13.358c-3.007.576-4.7,2.571-5.066,6.037h-1.529a7.576,7.576,0,0,1,6.6-7.776Z\", transform: \"translate(-1054.294 -1876.648)\", fill: \"#fff\" }));\nexport default SvgBell;\n","// Utilities\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"react-toastify\";\n\nimport fetchHandler from \"../fetchHandler\";\n\n// Interfaces\nimport {\n ArticleDetailsResponseFields,\n ArticlesNotificationsResponseFields,\n ArticlesResponseFields,\n} from \"./interfaces\";\nimport { useNavigate } from \"react-router\";\n\n/**\n *\n * Get the list of existing articles from the API\n *\n */\nexport function useArticlesGet() {\n return useQuery({\n queryKey: [\"articles\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"articles\")) as ArticlesResponseFields[];\n },\n meta: {\n errorMessage: \"Failed getting list of existing articles\",\n },\n });\n}\n\n/**\n *\n * Get the details of a specific article using it's ID\n * @param id The ID of the targeted article\n *\n */\nexport function useArticlesGetSpecific(id: string) {\n return useQuery({\n queryKey: [\"article\", id],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `articles/${id}`)) as ArticleDetailsResponseFields;\n },\n meta: {\n errorMessage: `Failed getting the article with ID ${id}`,\n },\n enabled: !!id,\n });\n}\n\n/**\n *\n * Get the existing notifications for all of the unread Articles\n *\n */\nexport function useArticlesNotificationsGet() {\n return useQuery({\n queryKey: [\"articles-notifications\"],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n \"articles/notifications\",\n )) as ArticlesNotificationsResponseFields;\n },\n meta: {\n errorMessage: \"Failed getting existing notifications for unread articles\",\n },\n });\n}\n\n/**\n *\n * Mark a specific article notification as read, and\n * redirect the user to that articles' details page\n *\n */\nexport function useArticlesMarkNotificationAsRead() {\n const queryClient = useQueryClient();\n const navigate = useNavigate();\n\n return useMutation({\n mutationFn: async (id: number) => {\n const cachedNotifications = queryClient.getQueryData([\n \"articles-notifications\",\n ]) as ArticlesNotificationsResponseFields;\n\n // Redirect the user to the article page corresponding to the clicked notification\n navigate(`/articles/${id}/`);\n\n // Safeguard to prevent decrementing unread notifications below 0\n if (cachedNotifications.unread_notifications === 0) return;\n\n // Check if the clicked notification is \"unread\" prior to the action\n const clickedNotification = cachedNotifications.notifications.find(notification => {\n return notification.id === id;\n });\n\n // If no matching notification was found in the cached data with the clicked one,\n // or if it's already marked as \"read\", exit the function preventing any updates\n if (!clickedNotification || clickedNotification.read) return;\n\n queryClient.setQueryData([\"articles-notifications\"], {\n unread_notifications: --cachedNotifications.unread_notifications,\n notifications: cachedNotifications.notifications.map(notification => {\n if (notification.id === id) {\n return { ...notification, read: true };\n } else {\n return notification;\n }\n }),\n });\n },\n });\n}\n\n/**\n *\n * Marks all article notifications as read\n *\n */\nexport function useArticlesNotificationsMarkAllAsRead() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async () => {\n return await fetchHandler(\"POST\", \"articles/notifications/mark-all-as-read\");\n },\n onMutate: () => {\n const cachedArticlesNotifications = queryClient.getQueryData([\n \"articles-notifications\",\n ]) as ArticlesNotificationsResponseFields;\n\n // Update the state of all notifications to be marked as 'read'\n queryClient.setQueryData([\"articles-notifications\"], {\n unread_notifications: 0,\n notifications: cachedArticlesNotifications.notifications.map(notification => ({\n ...notification,\n read: true,\n })),\n });\n\n // Display toast notification in the ui\n toast.success(\"All notifications marked as read!\", {\n toastId: \"articles-mark-all-as-read\",\n });\n\n return { cachedArticlesNotifications };\n },\n onError: (error, _variables, context) => {\n // Dismiss success notification from the UI first\n toast.dismiss(\"articles-mark-all-as-read\");\n\n queryClient.setQueryData([\"articles-notifications\"], context?.cachedArticlesNotifications);\n return error;\n },\n });\n}\n","import { formatDistanceToNow } from \"date-fns\";\n\n/**\n * A utility function that gives how long ago in the past or in the future\n * a given date is from the current date.\n * @param date A string or Date object representation of a date.\n * @returns The distance (in the past or the future) between the current date and the\n * date passed as an argument to the function.\n */\nexport function handleDistanceToNow(date: number | string | Date): string {\n const formattedDate: Date = new Date(date);\n const distanceToNow: string = formatDistanceToNow(formattedDate, { addSuffix: true });\n return distanceToNow;\n}\n","// Utilities & Hooks\nimport { handleDateAsTimestamp } from \"../../utilities/dates/handleDateAsTimestamp\";\nimport { handleDistanceToNow } from \"../../utilities/dates/handleDistanceToNow\";\nimport { useArticlesMarkNotificationAsRead } from \"../../api/Articles/Articles\";\n\n// Interfaces\nimport { ArticleNotificationProps } from \"./interfaces\";\n\nconst ArticleNotification = ({\n id,\n create_date,\n title,\n summary,\n feature_image,\n read = false,\n handleNotificationsMenu,\n}: ArticleNotificationProps) => {\n const markAsRead = useArticlesMarkNotificationAsRead();\n\n const handleNotification = async (notificationId: number) => {\n // Mark the notification as read and redirect the user to the specific article page\n await markAsRead.mutateAsync(notificationId);\n\n // Close the notifications menu after redirecting the user\n handleNotificationsMenu();\n };\n\n return (\n handleNotification(id)}\n data-testid={`component:articles-notification--${read ? \"read\" : \"unread\"}`}\n >\n
    \n \n
    \n\n
    \n
    \n {title}\n
    \n\n

    \n {summary}\n

    \n
    \n\n \n {handleDistanceToNow(handleDateAsTimestamp(create_date))}\n \n \n );\n};\n\nexport default ArticleNotification;\n","import Skeleton from \"react-loading-skeleton\";\n\nconst ArticlesNotificationsSkeleton = () => {\n return (\n \n
    \n \n\n \n
    \n\n {[...new Array(3)].map((_article, index: number) => (\n
    \n
    \n \n
    \n\n
    \n \n\n \n \n \n \n
    \n\n \n \n \n
    \n ))}\n\n
    \n \n
    \n \n );\n};\n\nexport default ArticlesNotificationsSkeleton;\n","// Assets\nimport NotificationsIcon from \"../../assets/images/icons/bell.svg?react\";\n\n// Utilities & Hooks\nimport { useEffect, useRef, useState } from \"react\";\nimport { Link } from \"react-router\";\nimport {\n useArticlesNotificationsMarkAllAsRead,\n useArticlesNotificationsGet,\n} from \"../../api/Articles/Articles\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Components\nimport ArticleNotification from \"./ArticleNotification\";\nimport ArticlesNotificationsSkeleton from \"./ArticlesNotificationsSkeleton\";\nimport Loader from \"../Loader/Loader\";\n\n// Interfaces\nimport { ArticlesNotificationsState } from \"../../api/Articles/interfaces\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport { FRAMER_HEADER_TRANSITIONS, FRAMER_HEADER_ANIMATION_COMMON } from \"../../constants/framer\";\n\nconst ArticleNotificationsMenu = () => {\n const errorReporting = useErrorReporting();\n\n /*=================================\n ARTICLES NOTIFICATIONS MENU STATE\n ==================================*/\n const [isNotificationsMenuOpen, setIsNotificationsMenuOpen] = useState(false);\n const notificationsMenuRef = useRef(null);\n\n // Close the menu when clicked outside of it\n useOnClickOutside(notificationsMenuRef, () => setIsNotificationsMenuOpen(false));\n\n /*==============================\n GET ARTICLES NOTIFICATIONS\n ===============================*/\n // prettier-ignore\n const [articlesNotifications, setArticlesNotifications] = useState([]);\n const [articlesNotificationsCounter, setArticlesNotificationsCounter] = useState(0);\n const {\n data,\n isPending,\n isFetching,\n refetch: refetchArticlesNotifications,\n } = useArticlesNotificationsGet();\n\n // Update the articles notifications state based\n // on the response from the API\n useEffect(() => {\n if (!data || !data.notifications || !data.notifications.length || isPending) return;\n\n // Update the list of the notifications\n setArticlesNotifications(data.notifications);\n\n // Update the counter of unread notifications\n setArticlesNotificationsCounter(data.unread_notifications);\n }, [data]);\n\n // Trigger a refetch of the Articles Notifications\n // query anytime the menu is reopened\n useEffect(() => {\n if (!isNotificationsMenuOpen) return;\n\n refetchArticlesNotifications();\n }, [isNotificationsMenuOpen]);\n\n /*==============================\n MARK ALL NOTIFICATIONS AS READ\n ===============================*/\n const markAllAsRead = useArticlesNotificationsMarkAllAsRead();\n const handleMarkAllAsRead = async () => {\n try {\n await markAllAsRead.mutateAsync();\n } catch (error) {\n errorReporting(\"Failed marking all notifications as read.\", error);\n }\n };\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(notificationsMenuRef, () => setIsNotificationsMenuOpen(false));\n\n return (\n \n setIsNotificationsMenuOpen(!isNotificationsMenuOpen)}\n >\n \n\n {articlesNotificationsCounter > 0 && (\n \n {articlesNotificationsCounter}\n \n )}\n \n\n \n {isNotificationsMenuOpen && (\n <>\n {isPending ? (\n \n ) : (\n \n {articlesNotifications.length > 0 ? (\n <>\n
    \n
    \n Notifications\n {isFetching && }\n
    \n\n \n Mark All as Read\n \n
    \n\n {articlesNotifications.map((notification: ArticlesNotificationsState) => (\n setIsNotificationsMenuOpen(false)}\n />\n ))}\n\n
    \n setIsNotificationsMenuOpen(false)}\n >\n See All\n \n
    \n \n ) : (\n
    \n
    \n No new notifications\n {isFetching && }\n
    \n
    \n )}\n \n )}\n \n )}\n
    \n \n );\n};\n\nexport default ArticleNotificationsMenu;\n","// Utilities & Hooks\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport fetchHandler from \"../fetchHandler\";\n\n// Interfaces\nimport { CompaniesResponseFields } from \"./interfaces\";\n\n/**\n *\n * Get list of all companies to which the logged in user has access to\n *\n */\nexport function useCompaniesGetAvailable() {\n return useQuery({\n queryKey: [\"companies\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"company\")) as CompaniesResponseFields[];\n },\n meta: {\n errorMessage: \"Failed getting list of available companies\",\n },\n });\n}\n\n/**\n *\n * Get the list of positions that are available\n * within the company that we're currently viewing\n *\n */\nexport function useCompaniesGetAvailablePositions() {\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useQuery({\n queryKey: [\"companies-positions\", companySlug],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `company/${companySlug}/positions`)) as string[];\n },\n enabled: !!companySlug,\n });\n}\n\n/**\n *\n * Get the list of all positions for specific company\n *\n */\nexport function useCompaniesGetSpecificPositions(companySlug?: string) {\n return useQuery({\n queryKey: [\"companies-positions\", companySlug],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `company/${companySlug}/positions`)) as string[];\n },\n enabled: !!companySlug,\n });\n}\n\n/**\n *\n * Get only the `currently` existing job positions for the selected company\n *\n */\nexport function useCompaniesGetJobPositionsForCurrentCompany() {\n const { user } = useAuth();\n const companySlug: string = user.active_company.slug;\n\n return useQuery({\n queryKey: [\"job-positions-current-company\", companySlug],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", `company/${companySlug}/job-positions`)) as string[];\n },\n enabled: !!companySlug,\n });\n}\n","import { useLocation } from \"react-router\";\n\nexport const DETAILS_SPECIFIC_PAGES: string[] = [\n \"/applications/\", // Represents the Application by ID page\n];\n\n/**\n *\n * Utility function for handling page reload, and possible redirect,\n * based on the page from which the user triggered a changed in the\n * currently active company.\n *\n * If the user was on a \"details-specific\" page,\n * e.g. page for specific application (/applications/:id),\n * then on successful active company change, the user will be redirected\n * to the default route defined in the router (in this case: the home Applications page).\n *\n * Otherwise only a page reload will be triggered, if the active company change\n * was not triggered from some \"details-specific\" page, e.g. Appointments page.\n *\n * @returns Function to be triggered on successful Active Company change API request\n *\n */\nexport default function handleRedirectOnActiveCompanyChange() {\n const { pathname } = useLocation();\n\n // Check if the page on which the user triggered a change\n // in the currently active company, is a \"details-specific\" page.\n const detailsSpecificPageCheck: boolean = DETAILS_SPECIFIC_PAGES.some(pageURL => {\n const pathnameLowercased: string = pathname.toLowerCase();\n const pageURLLowercased: string = pageURL.toLowerCase();\n\n return pathnameLowercased.includes(pageURLLowercased);\n });\n\n function handleLocationReplacement() {\n if (detailsSpecificPageCheck) {\n window.location.replace(\"/\");\n } else {\n window.location.reload();\n }\n }\n\n return { handleLocationReplacement };\n}\n","// Utilities & Hooks\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useCompaniesGetAvailable } from \"../../api/Company/Company\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport { useUserSetActiveCompany } from \"../../api/User/User\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\nimport handleRedirectOnActiveCompanyChange from \"./utils/handleRedirectOnActiveCompanyChange\";\n\n// Components\nimport DropdownSearchable from \"../Dropdown/DropdownSearchable\";\nimport Loader from \"../Loader/Loader\";\n\n// Interfaces\nimport { DropdownItem } from \"../Dropdown/interfaces\";\nimport { createPortal } from \"react-dom\";\nimport { useNavigate } from \"react-router\";\n\nconst HeaderCompaniesMenu = () => {\n const errorReporting = useErrorReporting();\n const navigate = useNavigate();\n\n /*================================\n FETCH THE LIST OF COMPANIES THAT\n THE LOGGED IN USER HAS ACCESS TO\n =================================*/\n const { data, isPending } = useCompaniesGetAvailable();\n\n // Map the received companies into dropdown items\n const availableCompanies: DropdownItem[] = useMemo(() => {\n // Exit function and return default dataset if there's no available data\n if (!data || !data.length || isPending) return [];\n\n // Redirect to \"Account Profile\" page if the\n // logged in user does not have access to any company\n if (data && !data.length) {\n navigate(\"/account/profile/\");\n return [];\n }\n\n // Sort the list of received companies in alphabetical order, and map them into dropdown items\n return data\n .map(company => {\n return { text: company.name, value: company.id };\n })\n .sort((companyA, companyB) => {\n return companyA.text.toLowerCase() > companyB.text.toLowerCase() ? 1 : -1;\n });\n }, [data]);\n\n /*================================\n PRE-SELECT THE USER'S \n ACTIVE COMPANY IN THE LIST\n =================================*/\n const [activeCompanyID, setActiveCompanyID] = useState(0);\n const { user } = useAuth();\n\n useEffect(() => {\n if (!user || !user.active_company || !availableCompanies.length) return;\n\n // Find the company with the matching name\n // and pre-select it's \"ID\" as a value\n const matchingCompany = availableCompanies.find(company => {\n return company.text.toLowerCase() === user.active_company.name.toLowerCase();\n });\n\n // If the company exists, extract it's ID value\n // and use it as the state for the preselected item's value\n if (matchingCompany) {\n const matchingCompanyID: number = matchingCompany.value as number;\n setActiveCompanyID(matchingCompanyID);\n }\n }, [user, availableCompanies]);\n\n /*================================\n UPDATE THE CURRENT USER'S\n ACTIVE COMPANY \n =================================*/\n const [selectedCompanyName, setSelectedCompanyName] = useState(\"\");\n const [showOverlay, setShowOverlay] = useState(false);\n const setActiveCompany = useUserSetActiveCompany();\n const { handleLocationReplacement } = handleRedirectOnActiveCompanyChange();\n\n const handleUsersActiveCompany = async (company: DropdownItem) => {\n // Display the page overlay and the company name\n setShowOverlay(true);\n setSelectedCompanyName(company.text);\n\n try {\n const companyID = company.value as number;\n await setActiveCompany.mutateAsync(companyID);\n\n // Reload the page (and possibly redirect the user) after a successful request to the API\n handleLocationReplacement();\n } catch (error) {\n setShowOverlay(false);\n setSelectedCompanyName(\"\");\n errorReporting(\"Failed to update user's active company.\", error, {\n user_id: user.id,\n company_id: company.value,\n });\n }\n };\n\n return (\n <>\n \n\n {showOverlay\n ? createPortal(\n
    \n
    \n \n
    \n

    Loading data for {selectedCompanyName}...

    \n

    Page will reload shortly

    \n
    \n
    \n
    ,\n document.body,\n )\n : null}\n \n );\n};\n\nexport default HeaderCompaniesMenu;\n","// Hooks\nimport { useRef, useState } from \"react\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Interfaces\nimport { DropdownActionsProps, DropdownItem } from \"./interfaces\";\n\n// Assets\nimport { IoMdArrowDropdown as DropdownToggleIcon } from \"react-icons/io\";\n\n// Components\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n// Constants\nimport { FRAMER_DROPDOWN_ACTIONS_ANIMATION } from \"../../constants/framer\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\n\nconst DropdownActions = ({\n title,\n items,\n selectedItem = null,\n handleDropdownItem,\n\n customToggleElement = null,\n size = \"md\",\n isDisabled = false,\n orientation = \"bottom\",\n modifierClass = \"\",\n}: DropdownActionsProps) => {\n /*======================================\n DROPDOWN VISIBILITY\n =======================================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n const dropdownRef = useRef(null);\n\n const handleDropdownVisibility = (event: React.MouseEvent) => {\n // Prevent event propagation to parent elements\n event.stopPropagation();\n\n if (isDisabled) return;\n\n setIsDropdownOpen(!isDropdownOpen);\n };\n\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*======================================\n DROPDOWN ITEM FUNCTIONALITY\n =======================================*/\n const handleSelectedDropdownItem = (dropdownItem: DropdownItem) => {\n // Prevent functions from being called if the item is disabled\n if (dropdownItem.disabled) return;\n\n handleDropdownItem(dropdownItem);\n };\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n \n
    \n {customToggleElement ? (\n customToggleElement\n ) : (\n <>\n {title}\n \n \n )}\n
    \n\n \n {isDropdownOpen && (\n \n {items.map((dropdownItem: DropdownItem) => (\n handleSelectedDropdownItem(dropdownItem)}\n >\n {dropdownItem.icon && (\n
    {dropdownItem.icon}
    \n )}\n {dropdownItem.text}\n {dropdownItem.description && (\n

    {dropdownItem.description}

    \n )}\n \n ))}\n \n )}\n
    \n \n );\n};\n\nexport default DropdownActions;\n","// Components\nimport DropdownActions from \"../Dropdown/DropdownActions\";\n\n// Interfaces\nimport { DropdownItem } from \"../Dropdown/interfaces\";\nimport { ChatsSortBy, ChatsSortProps } from \"./interfaces\";\n\nconst ChatMessagesSort = ({ sortBy, handleChatMessagesSorting }: ChatsSortProps) => {\n return (\n
    \n
    Sort by:
    \n {\n handleChatMessagesSorting(sortOption.value as ChatsSortBy);\n }}\n />\n
    \n );\n};\n\nexport default ChatMessagesSort;\n","import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"react-toastify\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport handlePermissionCheck from \"../../utilities/handlePermissionCheck\";\nimport fetchHandler from \"../fetchHandler\";\nimport {\n ChatBlockActionsPayloadFields,\n ChatMessagesResponseFields,\n ChatNewConverationRequestFields,\n ChatsForSpecificUserResponseFields,\n} from \"./interfaces\";\n\n/**\n *\n * Get all the chats that exist for a specific user\n *\n * @param selectedUserID Optional parameter - The ID of the targeted user for who we want to\n * get all existing chats.\n *\n * If no valid ID is provided, defaults to the ID of the currently logged in user.\n *\n */\nexport function useChatGetAllUserChats(selectedUserID?: number) {\n const { user: currentUser } = useAuth();\n\n // If a specific user was selected from the list, use their ID to fetch the chats.\n // Otherwise default to the ID of the currently logged in user\n const id: number = selectedUserID || currentUser.id;\n\n // Check if the user has a permission to read the chats\n const hasPermissionToReadChats: boolean = handlePermissionCheck([\"sms_read\"]);\n\n return useQuery({\n queryKey: [\"sms-chats\", id],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `sms/chats?user_id=${id}`,\n )) as ChatsForSpecificUserResponseFields[];\n },\n enabled: !!id && hasPermissionToReadChats === true,\n });\n}\n\n/**\n *\n * \"Block\" or \"Unblock\" a targeted chat\n *\n */\nexport function useChatBlockActions() {\n const { user } = useAuth();\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (chatBlockActions: ChatBlockActionsPayloadFields) => {\n return await fetchHandler(\"POST\", `sms/chats/${chatBlockActions.chatID}/block`, {\n action: chatBlockActions.action,\n });\n },\n onMutate: (chatActions: ChatBlockActionsPayloadFields) => {\n // Dismiss notification for chat actions (if it exists in the UI)\n toast.dismiss(\"chat-block\");\n\n // Make a copy of the currently existing, cached, chats data\n const previousChats = queryClient.getQueryData([\n \"sms-chats\",\n user.id,\n ]) as ChatsForSpecificUserResponseFields[];\n\n // Find the targeted chat from the cached data\n const targetedChatIndex: number = previousChats.findIndex(previousChat => {\n return previousChat.id === chatActions.chatID;\n });\n\n // If targeted chat cannot be found, exit function\n if (targetedChatIndex < 0) return { previousChats };\n\n // Make a copy of the chats list that we'll use for mutations\n const updatedChats: ChatsForSpecificUserResponseFields[] = [...previousChats];\n\n // Update the properties of the targeted chat, by making a new object,\n // so we can safely update the state\n const updatedTargetedChat: ChatsForSpecificUserResponseFields = {\n ...updatedChats[targetedChatIndex],\n is_blocked: chatActions.action === \"block\" ? 1 : 0,\n };\n\n // Update the list of chats with the newly created (updated) chat\n updatedChats.splice(targetedChatIndex, 1, updatedTargetedChat);\n\n // Update the query data (this will also trigger UI state update)\n queryClient.setQueryData([\"sms-chats\", user.id], updatedChats);\n\n // Show success notification\n const actionSuccessMessage: string = `${\n chatActions.action === \"block\" ? \"blocked\" : \"unblocked\"\n }`;\n toast.success(`Chat ${actionSuccessMessage} successfully!`, {\n toastId: \"chat-block\",\n });\n\n // Return the previously cached chats so they can be used in an error scenario\n return { previousChats };\n },\n onError: (error, _variables, context) => {\n // Dismiss success notification from the UI first\n toast.dismiss(\"chat-block\");\n\n // In case of an error, fallback to the previously stored chats\n queryClient.setQueryData([\"sms-chats\", user.id], context?.previousChats);\n\n // Return the error message\n return error;\n },\n });\n}\n\n/**\n *\n * Get all the messages for a specific chat\n *\n */\nexport function useChatGetMessages(chatID: number | null) {\n return useQuery({\n queryKey: [\"sms-chat-messages\", chatID],\n queryFn: async () => {\n return (await fetchHandler(\n \"GET\",\n `sms/chats/${chatID}/messages`,\n )) as ChatMessagesResponseFields;\n },\n enabled: !!chatID,\n });\n}\n\n/**\n *\n * Initialize a new chat conversation with\n * the targeted applicant for the first time\n *\n */\nexport function useChatInitializeNewConversation() {\n const queryClient = useQueryClient();\n const { handleUpdateUserDetails } = useAuth();\n\n return useMutation({\n mutationFn: async (details: ChatNewConverationRequestFields) => {\n return await fetchHandler(\"POST\", \"sms/messages\", details);\n },\n onMutate: () => {\n handleUpdateUserDetails({ bandwidth_number_status: \"pending\" });\n },\n onError: error => error,\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n },\n });\n}\n","// Utilities & Hooks\nimport { useChatBlockActions } from \"../../api/Chat/Chat\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\n\n// Components\nimport DropdownActions from \"../Dropdown/DropdownActions\";\n\n// Assets\nimport { BiBlock as BlockIcon, BiDotsHorizontalRounded as ChatActionsIcon } from \"react-icons/bi\";\n\n// Interfaces\nimport { ChatActionsProps } from \"./interfaces\";\nimport { DropdownItem } from \"../Dropdown/interfaces\";\n\nconst ChatActions = ({ id, is_blocked = 0, orientation = \"bottom\" }: ChatActionsProps) => {\n const errorReporting = useErrorReporting();\n const chatBlockActions = useChatBlockActions();\n\n const handleChatBlockActions = async (chatAction: DropdownItem) => {\n // If there's no valid chat ID available, exit function, preventing any mutations\n if (!id) return;\n\n // Type of action to be taken for the targeted chat\n const action: \"block\" | \"unblock\" = chatAction.value === 1 ? \"block\" : \"unblock\";\n\n try {\n await chatBlockActions.mutateAsync({\n chatID: id,\n action,\n });\n } catch (error) {\n errorReporting(`Failed chat ${action} action.`, error, { chat_id: id, chat_action: action });\n }\n };\n\n return (\n
    \n ,\n },\n ]}\n orientation={orientation}\n size=\"sm\"\n title=\"Actions\"\n customToggleElement={}\n handleDropdownItem={handleChatBlockActions}\n />\n
    \n );\n};\n\nexport default ChatActions;\n","/**\n *\n * Utility function for extracting the initials of the user's\n * fullname, taking only the initials of the first and last name.\n *\n * @param fullname A string represntation of the concatenation of the users name (first, middle, and last).\n * @param initialsJoin A string representation of how to join the extracted name initials. Defaults to `\" \"`.\n *\n * @returns The user initials from the first and last name.\n */\nexport function handleUserInitials(fullname: string, initialsJoin: string = \" \"): string {\n // If there's no valid value received, exit function with default value\n if (!fullname) return \"N/A\";\n\n // Split the fullname into the parts that make it up\n const partsOfName: string[] = fullname.split(\" \");\n\n // Get the initial of the first name, in case the\n // user only provided their first name\n const initials: string[] = [partsOfName[0].substring(0, 1).toUpperCase()];\n\n // If there are multiple parts of the received fullname,\n // such as middle name(s) and last name, then take into\n // consideration only the first letter of the user's last name\n if (partsOfName.length > 1) {\n initials.push(partsOfName[partsOfName.length - 1].substring(0, 1).toUpperCase());\n }\n\n // Join the extracted name initials and join them\n // using the provided join value\n return initials.join(initialsJoin);\n}\n","import { handleUserInitials } from \"../../utilities/strings/handleUserInitials\";\nimport { ChatAvatarProps } from \"./interfaces\";\n\n// Assets\nimport { BiBlock as ChatAvatarBlockedIcon } from \"react-icons/bi\";\n\nconst ChatAvatar = ({\n name,\n image_url = null,\n size = \"md\",\n status = \"active\",\n modifierClass = \"\",\n}: ChatAvatarProps) => {\n return (\n
    \n {image_url ? (\n \"Applicant's\n ) : (\n

    {handleUserInitials(name)}

    \n )}\n\n {status === \"blocked\" && (\n
    \n \n
    \n )}\n
    \n );\n};\n\nexport default ChatAvatar;\n","// Utilities\nimport { format } from \"date-fns\";\n\n// Components\nimport ChatActions from \"./ChatActions\";\nimport ChatAvatar from \"./ChatAvatar\";\n\n// Interfaces\nimport { ChatMessageListItemProps } from \"./interfaces\";\n\nconst ChatMessageListItem = ({\n id,\n photo,\n full_name,\n last_message,\n timestamp,\n modifierClass = \"\",\n is_read,\n is_blocked,\n hasActionsMenu,\n handleChatMessageListItem,\n}: ChatMessageListItemProps) => {\n return (\n handleChatMessageListItem(id)}\n >\n \n\n
    \n
    \n
    \n {full_name}\n
    \n\n {hasActionsMenu && }\n
    \n

    {last_message}

    \n

    \n {format(new Date(timestamp), \"HH:mmaaa | MMM dd yyyy\")}\n

    \n
    \n \n );\n};\n\nexport default ChatMessageListItem;\n","import Skeleton from \"react-loading-skeleton\";\n\nconst SkeletonChatMessageListItem = () => {\n return (\n
    \n \n\n
    \n
    \n \n \n
    \n\n

    \n \n

    \n

    \n \n

    \n
    \n
    \n );\n};\n\nexport default SkeletonChatMessageListItem;\n","import * as React from \"react\";\nconst SvgSmsIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", width: 27, height: 24, viewBox: \"0 0 22 19\", ...props }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M3865.11,3091.832a11.42,11.42,0,0,0-8.224-3.355l-.475.022a11.251,11.251,0,0,0-7.7,3.3,8.5,8.5,0,0,0-.349,11.825.375.375,0,0,1,.066.474,4.448,4.448,0,0,1-1.494,1.85c-.116.083-.247.29-.216.392a.516.516,0,0,0,.383.242,6.271,6.271,0,0,0,3.745-.655.49.49,0,0,1,.4,0,11.826,11.826,0,0,0,6.987,1.31,10.918,10.918,0,0,0,7.162-3.59A8.532,8.532,0,0,0,3865.11,3091.832Zm-13.991,7.812a1.73,1.73,0,1,1,1.716-1.744v.012a1.722,1.722,0,0,1-1.711,1.733h-.005Zm5.733,0a1.73,1.73,0,1,1,1.767-1.718,1.7,1.7,0,0,1-1.68,1.72c-.029,0-.058,0-.087,0Zm5.84,0a1.671,1.671,0,1,1,.036,0h-.036Z\", transform: \"translate(-3846.138 -3088.476)\", fill: \"#fff\" }));\nexport default SvgSmsIcon;\n","import { ChatsSortBy, ChatsSortingValues } from \"./interfaces\";\n\nexport const CHATS_SORTING_OPTIONS: Record = {\n most_recent: {\n fieldname: \"last_message.timestamp\",\n direction: \"desc\",\n },\n blocked: {\n fieldname: \"is_blocked\",\n direction: \"desc\",\n },\n unread: {\n fieldname: \"last_message.is_read\",\n direction: \"asc\",\n },\n};\n\n// A set of skeleton placeholder messages to be displayed in the UI\n// while the messages for the openede chat are being fetched\nexport const CHATS_MESSAGE_SKELETON_PLACEHOLDERS: Array<\"in\" | \"out\"> = [\n \"in\",\n \"out\",\n \"out\",\n \"in\",\n \"out\",\n \"in\",\n];\n\n// Hard limit of how many characters can the message have\nexport const CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT: number = 180;\n\n// Circular progress bar properties\nexport const CHAT_CIRCLE_PROGRES_RADIUS: number = 6;\nexport const CHAT_CIRCLE_PROGRES_DASH_ARRAY: number = 2 * Math.PI * CHAT_CIRCLE_PROGRES_RADIUS;\n","/**\n *\n * Utility function for moving an array item from it's starting position\n * to the desired position in the same array\n *\n * @param array The array to be rearranged\n * @param startingPosition The starting position of the targeted element\n * @param desiredPosition The desired position of the targeted element\n * @returns A rearranged array, with the targeted element moved from\n * its starting to its desired position\n *\n */\nexport default function handleMoveArrayItem(\n array: T[],\n startingPosition: number,\n desiredPosition: number,\n): T[] {\n // Early exit if no valid 'array' value was received\n if (!array || !array.length) return [];\n\n const arrayCopy: T[] = [...array];\n const targetedElement: T = arrayCopy[startingPosition];\n\n // Remove the element from the starting position\n arrayCopy.splice(startingPosition, 1);\n\n // Place the element to the desired index position\n arrayCopy.splice(desiredPosition, 0, targetedElement);\n\n return arrayCopy;\n}\n","import { debounce } from \"lodash-es\";\nimport { useEffect, useState } from \"react\";\n\n/**\n *\n * Custom hook used for extracting the device's inner height after any\n * changes have happend to the device's viewport.\n *\n * This is useful for situations where the available height of the device\n * is changed due to viewport resizing, such as opening / closing the virtual keyboard\n * that shows up on mobile devices when clicked on an input / textarea fields.\n *\n * @param delay Debouncing delay for the viewport's resize event handler. Defaults to `100ms`.\n *\n * @returns The current device's `visualViewport height`.\n *\n */\nexport function useVisualViewportResize(delay: number = 100): number {\n const [viewportHeight, setViewportHeight] = useState(\n visualViewport?.height || window.innerHeight,\n );\n\n useEffect(() => {\n // If visual viewport is not supported, exit function\n if (!visualViewport) return;\n\n const handleViewportResize = (event: any) => setViewportHeight(event.target?.height);\n const debouncedViewportResize = debounce(handleViewportResize, delay);\n\n // Listen to any changes in the device's viewport\n visualViewport.addEventListener(\"resize\", debouncedViewportResize);\n\n // Remove event listeners when unmounted\n return () => {\n visualViewport?.removeEventListener(\"resize\", debouncedViewportResize);\n };\n }, [delay]);\n\n return viewportHeight;\n}\n","import * as React from \"react\";\nconst SvgChatPanelMinimizeIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 512 512\", ...props }, /* @__PURE__ */ React.createElement(\"path\", { d: \"M32 416c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H32z\" }));\nexport default SvgChatPanelMinimizeIcon;\n","// Utilities\nimport { motion } from \"framer-motion\";\nimport { format } from \"date-fns\";\n\n// Components\nimport ChatAvatar from \"../ChatAvatar\";\nimport Loader from \"../../Loader/Loader\";\n\n// Interfaces\nimport { ChatConversationMessageProps } from \"./interfaces\";\n\n// Assets\nimport {\n MdDoneAll as ChatMessageDeliveredIcon,\n MdWarning as ChatMessageFailedIcon,\n} from \"react-icons/md\";\n\nconst ChatConversationMessage = ({\n name,\n content,\n status,\n image_url = null,\n is_blocked = 0,\n direction = \"out\",\n time,\n}: ChatConversationMessageProps) => {\n return (\n \n {direction === \"in\" && (\n \n )}\n\n
    \n
    {name}
    \n\n \n

    {content}

    \n

    {format(new Date(time * 1000), \"hh:mmaaa\")}

    \n
    \n\n {status === \"message-sending\" ? (\n
    \n \n
    Sending message
    \n
    \n ) : null}\n\n {status === \"message-delivered\" ? (\n
    \n \n
    Message delivered.
    \n
    \n ) : null}\n\n {status === \"message-failed\" ? (\n
    \n \n
    Message not sent.
    \n
    \n ) : null}\n \n \n );\n};\n\nexport default ChatConversationMessage;\n","// Components\nimport ChatConversationMessage from \"./ChatConversationMessage\";\n\n// Interfaces\nimport { ChatResponseFields } from \"../../../api/Chat/interfaces\";\nimport { ChatConversationMessageGroupProps } from \"./interfaces\";\n\nconst ChatConversationMessageGroup = ({\n groupDate,\n groupMessages,\n chatConversationDetails,\n}: ChatConversationMessageGroupProps) => {\n return (\n
    \n
    \n
    {groupDate}
    \n
    \n\n {groupMessages.map((message: ChatResponseFields) => (\n \n ))}\n
    \n );\n};\n\nexport default ChatConversationMessageGroup;\n","/**\n *\n * Utility function that calculates what percent of\n * the `total value` is the given `current value`.\n *\n * @returns Calculated percentage number.\n *\n */\nexport function handleCalculatePercentage(current_value: number, total_value: number): number {\n return (current_value / total_value) * 100;\n}\n","import { handleCalculatePercentage } from \"../../../utilities/numbers/handleCalculatePercentage\";\nimport { CHAT_CIRCLE_PROGRES_DASH_ARRAY } from \"../constants\";\n\n/**\n *\n * Calculate the progress of the typed message,\n * when the progress is displayed trough a circular progressbar.\n *\n * Calculate the progress towards the message character limit,\n * based on the currently typed message\n *\n * @param messageLength The length of the currently typed message\n * @param maxMessageLength The maximum allowed length of the message to be typed\n * @param progressIndicator The type of progress indicator that is being used.\n *\n * @returns The progress percentage of the typed message\n *\n */\nexport function handleCalculateChatMessageProgress(\n messageLength: number,\n maxMessageLength: number,\n progressIndicator: \"circular\" | \"line\",\n): number {\n // Calculate the percentage representing the progress\n // of the typed message (how much characters we have left until reaching the limit)\n const progressPercentage: number = handleCalculatePercentage(messageLength, maxMessageLength);\n\n // Calculate the offset of the stroke's dash array\n // that will result in filling the circular progress\n if (progressIndicator === \"circular\") {\n return CHAT_CIRCLE_PROGRES_DASH_ARRAY * ((100 - progressPercentage) / 100);\n } else {\n return progressPercentage;\n }\n}\n","import * as React from \"react\";\nconst SvgChatSendIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { id: \"send\", xmlns: \"http://www.w3.org/2000/svg\", width: 16.455, height: 14.198, viewBox: \"0 0 16.455 14.198\", ...props }, /* @__PURE__ */ React.createElement(\"g\", { id: \"Group_1074\", \"data-name\": \"Group 1074\", transform: \"translate(0 0)\" }, /* @__PURE__ */ React.createElement(\"path\", { id: \"Path_1082\", \"data-name\": \"Path 1082\", d: \"M15.476,40.742,2.2,35.233a1.589,1.589,0,0,0-2.149,1.86l1.182,4.634H7.018a.482.482,0,0,1,0,.964H1.232L.051,47.326A1.589,1.589,0,0,0,2.2,49.186l13.276-5.509a1.589,1.589,0,0,0,0-2.935Z\", transform: \"translate(-0.001 -35.111)\", fill: \"#fff\" })));\nexport default SvgChatSendIcon;\n","// Utilities & Hooks\nimport { useEffect, useRef, useState } from \"react\";\nimport { handleCalculateChatMessageProgress } from \"../utils/handleChatMessageProgress\";\nimport handlePermissionCheck from \"../../../utilities/handlePermissionCheck\";\n\n// Assets\nimport ChatSendMessageIcon from \"../../../assets/images/icons/chat-send-icon.svg?react\";\nimport { BsInfoCircle as InfoIcon } from \"react-icons/bs\";\n\n// Constants\nimport {\n CHAT_CIRCLE_PROGRES_DASH_ARRAY,\n CHAT_CIRCLE_PROGRES_RADIUS,\n CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT,\n} from \"../constants\";\n\n// Interfaces\nimport { ChatConversationFormProps } from \"./interfaces\";\n\n// Contexts\nimport { useChatsContext } from \"../ChatWrapper/ChatContextWrapper\";\nimport { useAuth } from \"../../../providers/auth-context\";\n\nconst TEMPORARY_DISABLED = true;\n\nconst ChatConversationForm = ({\n id,\n chatLocation = \"page\",\n selectedUserID = 0,\n bandwidthNumberStatus = \"unavailable\",\n handleSendChatMessage,\n}: ChatConversationFormProps) => {\n const { user } = useAuth();\n\n /*============================\n TRACK TYPED MESSAGE\n =============================*/\n const [typedMessageCharactersCount, setTypedMessageCharactersCount] = useState(0);\n const [typedMessage, setTypedMessage] = useState(\"\");\n const [typedMessageProgressPercentage, setTypedMessageProgressPercentage] = useState(0);\n\n const handleMessageTyping = (event: React.ChangeEvent) => {\n // Note: Restriction is temporary; will be removed\n if (TEMPORARY_DISABLED) return;\n\n // Dynamic textarea height as the user types\n if (textareaRef && textareaRef.current) {\n if (event.target.value) {\n textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;\n } else {\n textareaRef.current.style.height = \"24px\";\n }\n }\n\n // Prevent typing more than the maximum allowed message characters count\n // This is an extra layer of protection used for mobile devices where\n // the \"maxLength\" property applied to the textarea field is not working correctly\n if (event.target.value.length > CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT) return;\n\n setTypedMessage(event.target.value);\n };\n\n const handleMessageTypingKeydown = (event: React.KeyboardEvent) => {\n // If user presses only on the \"Enter\" key, then trigger sending the message\n // Otherwise, if user presses \"Enter\" and \"Shift\" key in combination, insert new line\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleTriggerSendChatMessage();\n }\n };\n\n // Count the message characters anytime the typed message is updated\n useEffect(() => {\n setTypedMessageCharactersCount(typedMessage.length);\n\n // Calculate the progress percentage based on how\n // many characters were typed within the message\n const progressPercentage: number = handleCalculateChatMessageProgress(\n typedMessage.length,\n CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT,\n chatLocation === \"page\" ? \"circular\" : \"line\",\n );\n\n setTypedMessageProgressPercentage(progressPercentage);\n }, [typedMessage]);\n\n /*=============================\n TRIGGER SENDING THE MESSAGE\n ================================*/\n const handleTriggerSendChatMessage = () => {\n // Note: Restriction is temporary; will be removed\n if (TEMPORARY_DISABLED) return;\n\n // Prevent form submission when the form is in \"readonly\" mode\n if (isReadOnly) return;\n\n // Prevent trying to send the message if there's an empty message,\n // or if user entered more than 180 characters\n if (\n typedMessageCharactersCount === 0 ||\n typedMessageCharactersCount > CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT\n )\n return;\n\n // Trigger function for sending the message\n handleSendChatMessage(typedMessage);\n\n // Clear out the message after sending it\n setTypedMessage(\"\");\n };\n\n /*=====================================\n CHECK IF FORM IS IN \"READ-ONLY\" MODE\n ======================================*/\n const [isReadOnly, setIsReadOnly] = useState(() => {\n // When we open the chat conversation in a panel, by default the form is not in \"readonly\" mode.\n // When we open the chat conversation in the dedicated page, by default the form is in \"readonly\" mode.\n return chatLocation === \"page\" ? true : false;\n });\n const permissionCheckFormWrite = handlePermissionCheck([\"sms_write\"]);\n\n useEffect(() => {\n // If no user data is available, exit the function\n if (!user.id) return;\n\n // If the logged in user does not have the \"write\" permission,\n // then the form is in \"readonly\" mode\n if (!permissionCheckFormWrite) {\n setIsReadOnly(true);\n return;\n }\n\n // If there's no valid \"selectedUserID\" value received as a prop,\n // then the form is not in \"read only\" mode, meaning that we're not trying\n // to see the messages of someone else, but instead we're viewing our own messages\n if (!selectedUserID) {\n setIsReadOnly(false);\n return;\n }\n\n // If the logged in user's ID matches the ID of the selected user then\n // form is in \"readonly\" mode to prevent sending messages impersonating another user.\n // Otherwise, logged in users are viewing their own chats & messages and can send a message.\n if (user.id !== selectedUserID) {\n setIsReadOnly(true);\n } else {\n setIsReadOnly(false);\n }\n }, [user.id, selectedUserID, permissionCheckFormWrite]);\n\n /*===========================\n FOCUS ON THE TEXTAREA\n ============================*/\n const chatsContext = useChatsContext();\n const textareaRef = useRef(null);\n\n useEffect(() => {\n // If its mobile device do not auto-focus\n if (window.innerWidth <= 575) return;\n\n // Exit function if the ref for the textarea cannot be found\n if (!textareaRef.current) return;\n\n // If user is viewing the dedicated Chats page, automatically focus on the textarea\n // Otherwise, if the user is using Chat Panels, check if the ID of the panel\n // is matching the one of the currently active (focused) chat panel, before focusing to the textarea\n if (chatLocation === \"page\") {\n textareaRef.current.focus();\n } else {\n if (id && id === chatsContext.activeChatID) textareaRef.current.focus();\n }\n }, [id, chatsContext.activeChatID]);\n\n return (\n <>\n \n \n\n \n {/* iOS: Has to be wrapped in some element otherwise wont render the SVG */}\n \n \n \n \n\n {chatLocation === \"panel\" ? (\n = CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT\n ? \"chat-conversation__form__progress__indicator--limit\"\n : \"\"\n }`}\n >\n ) : null}\n \n\n
    \n {chatLocation === \"page\" ? (\n = CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT\n ? \"chat-conversation__form__progress__indicator--limit\"\n : \"\"\n }`}\n height=\"16\"\n width=\"16\"\n >\n \n \n \n ) : null}\n \n = CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT\n ? \"chat-conversation__form__progress__typed--limit\"\n : \"\"\n }`}\n >\n {typedMessageCharactersCount}\n \n / {CHAT_MESSAGE_CHARACTERS_COUNT_LIMIT}\n \n
    \n\n {isReadOnly && chatLocation === \"page\" ? (\n
    \n

    \n \n You can only view these messages. You can not engage in conversation from this account.\n

    \n
    \n ) : null}\n \n );\n};\n\nexport default ChatConversationForm;\n","import Skeleton from \"react-loading-skeleton\";\nimport { ChatConversationLocation } from \"./interfaces\";\n\nconst ChatConversationMessageSkeleton = ({\n chatLocation = \"page\",\n direction,\n}: {\n chatLocation?: ChatConversationLocation;\n direction: \"in\" | \"out\";\n}) => {\n return (\n \n {direction === \"in\" && }\n\n
    \n
    \n \n
    \n\n
    \n \n

    \n \n

    \n
    \n
    \n \n );\n};\n\nexport default ChatConversationMessageSkeleton;\n","import { io } from \"socket.io-client\";\nimport { SOCKET_URL } from \"./config\";\n\n/**\n *\n * Initializes SocketIO client so chat connections can be made.\n *\n * The `autoConnect` flag is set to `false` to prevent establishing connections\n * to the socket on the unauthenticated parts of the application.\n *\n * Only the authenticated pages should be able to initialize the socket client.\n *\n **/\nconst socket = io(SOCKET_URL, {\n autoConnect: false,\n});\n\nexport default socket;\n","// Utilities & Hooks\nimport { useEffect, useState } from \"react\";\nimport { useChatGetMessages, useChatInitializeNewConversation } from \"../../../api/Chat/Chat\";\nimport { groupBy } from \"lodash-es\";\nimport { format } from \"date-fns\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useAuth } from \"../../../providers/auth-context\";\nimport { ChatMessagesResponseFields, ChatResponseFields } from \"../../../api/Chat/interfaces\";\nimport { useChatsContext } from \"../ChatWrapper/ChatContextWrapper\";\nimport useErrorReporting from \"../../../hooks/useErrorReporting\";\n\n// Components\nimport CustomScrollbars from \"../../CustomScrollbars/CustomScrollbars\";\nimport ChatConversationMessageGroup from \"../Conversation/ChatConversationMessageGroup\";\nimport ChatConversationForm from \"../Conversation/ChatConversationForm\";\nimport ChatConversationMessageSkeleton from \"../Conversation/ChatConversationMessageSkeleton\";\nimport ChatPanelFirstMessageInfo from \"./ChatPanelFirstMessageInfo\";\n\n// Interfaces\nimport {\n ChatConversationMessageGroups,\n ChatPanelConversationProps,\n} from \"../Conversation/interfaces\";\n\n// Constants\nimport { CHATS_MESSAGE_SKELETON_PLACEHOLDERS } from \"../constants\";\n\n// Socket\nimport socket from \"../../../config/chat-socket\";\n\nconst TEMPORARY_DISABLED = true;\n\nconst ChatPanelConversation = ({\n details,\n chatConversationDetails,\n modifierClass = \"\",\n}: ChatPanelConversationProps) => {\n const queryClient = useQueryClient();\n const chatsContext = useChatsContext();\n const { user, handleUpdateUserDetails } = useAuth();\n const errorReporting = useErrorReporting();\n\n /*=======================\n ID OF CURRENT CHAT\n ========================*/\n const [chatID, setChatID] = useState(details.chat_id);\n\n /*================================\n FETCH ALL THE MESSAGES FOR\n THE TARGETED CHAT\n =================================*/\n const { data, isFetching, isPending, isRefetching } = useChatGetMessages(chatID);\n\n /*================================\n MAP RECEIVED CHAT MESSAGES DATA\n =================================*/\n const [chatMessages, setChatMessages] = useState([]);\n const [chatMessagesUI, setChatMessagesUI] = useState([]);\n\n // When the component mounts, read the existing chat data for it,\n // and if there are any messages for it use them to populate the\n // messages that are displayed in the UI\n useEffect(() => {\n const existingChatData = queryClient.getQueryData([\n \"sms-chat-messages\",\n chatID,\n ]) as ChatMessagesResponseFields;\n\n // If there's no data for the chat, exit function preventing trying to map over non-existing data\n if (\n !existingChatData ||\n !Object.entries(existingChatData).length ||\n !existingChatData.messages.length ||\n isRefetching\n ) {\n return;\n }\n\n // Group the messages by date\n // TODO: Maybe extract this in a separate function\n const groupedMessages = groupBy(existingChatData.messages, message => {\n const formattedMessageDate: string = format(\n new Date(message.timestamp * 1000),\n \"MMM dd yyyy\",\n );\n return formattedMessageDate;\n });\n\n // If there are no groups of messages, exit function\n if (!Object.entries(groupedMessages).length) return;\n\n // Convert the grouped messages object to an ittreable array\n // in reverse order, meaning the latest messages will be\n // shown at the bottom of the conversation\n const arrayOfGroupedMessages = Object.entries(groupedMessages).map(message => {\n const [key, value] = message;\n return { groupDate: key, groupMessages: value };\n });\n\n setChatMessagesUI(arrayOfGroupedMessages);\n }, [chatID]);\n\n // When the data from the API for the messages that belong to a\n // specific chat are fetched, update the messages that are displayed in the UI\n useEffect(() => {\n if (!data || !data.messages || !data.messages.length) return;\n setChatMessages(data.messages);\n }, [data]);\n\n // When the list of chat messages is updated after a message was sent\n // when working with the socket communication, update the messages that are\n // displayed in the UI\n useEffect(() => {\n if (!chatMessages.length) return;\n\n // Group the messages by date\n const groupedMessages = groupBy(chatMessages, message => {\n const formattedMessageDate: string = format(\n new Date(message.timestamp * 1000),\n \"MMM dd yyyy\",\n );\n return formattedMessageDate;\n });\n\n // If there are no groups of messages, exit function\n if (!Object.entries(groupedMessages).length) return;\n\n // Convert the grouped messages object to an ittreable array\n // in reverse order, meaning the latest messages will be\n // shown at the bottom of the conversation\n const arrayOfGroupedMessages = Object.entries(groupedMessages).map(message => {\n const [key, value] = message;\n return { groupDate: key, groupMessages: value };\n });\n\n setChatMessagesUI(arrayOfGroupedMessages);\n }, [chatMessages]);\n\n /*=====================================\n SEND MESSAGE AND INITIALIZE NEW CHAT\n ======================================*/\n const chatInitializeNew = useChatInitializeNewConversation();\n const handleChatInitializeNewConversation = async (message: string) => {\n // Note: Restriction is temporary; will be removed\n if (TEMPORARY_DISABLED) return;\n\n if (!message) return;\n\n const TEMPORARY_MESSAGE_FOR_UI: ChatResponseFields = {\n id: null,\n chat_id: null,\n direction: \"out\",\n content: message,\n is_read: 1,\n timestamp: new Date().getTime() / 1000,\n status: \"message-sending\",\n };\n\n try {\n // Update the UI directly\n const updatedMessages = [...chatMessages];\n updatedMessages.push(TEMPORARY_MESSAGE_FOR_UI);\n setChatMessages(updatedMessages);\n\n await chatInitializeNew.mutateAsync({\n applicant_id: details.applicant.id,\n application_id: details.application_id as number,\n message,\n });\n } catch (error) {\n setChatMessages([{ ...TEMPORARY_MESSAGE_FOR_UI, status: \"message-failed\" }]);\n errorReporting(\"Failed initializing new chat conversation\", error, {\n applicant_id: details.applicant.id,\n application_id: details.application_id as number,\n });\n }\n };\n\n // Update the displayed chat messages data\n useEffect(() => {\n if (!chatInitializeNew.data) return;\n\n // Update the chat messages data for the chat that we're viewing at the moment\n const chatData = chatInitializeNew.data;\n queryClient.setQueryData([\"sms-chat-messages\", chatData.id], chatData);\n setChatID(chatData.id);\n\n // The formatted dataset that will be appended to the list\n // of existing user chats, directly in the cached query data\n const CHAT_DATA_CACHE = {\n id: chatData.id,\n is_blocked: chatData.is_blocked,\n user_id: chatData.user_id,\n applicant: chatData.applicant,\n last_message: chatData.messages[0],\n };\n\n // Update the list of existing user chats that are\n // saved in the cached react query data\n const updatedList = chatsContext.existingUserChats ? [...chatsContext.existingUserChats] : [];\n updatedList.unshift(CHAT_DATA_CACHE);\n queryClient.setQueryData([\"sms-chats\", user.id], updatedList);\n }, [chatInitializeNew.data]);\n\n /*=====================================\n SEND A MESSAGE TO THE SOCKET\n ======================================*/\n const handleChatSendNewMessage = (message: string) => {\n // Note: Restriction is temporary; will be removed\n if (TEMPORARY_DISABLED) return;\n\n if (!message) return;\n\n // Update the UI with the message that we just sent\n const SENT_MESSAGE_TIMESTAMP = new Date().getTime() / 1000;\n\n const SENT_MESSAGE: ChatResponseFields = {\n id: `${chatID}_${chatMessages.length + 1}`,\n chat_id: chatID,\n direction: \"out\",\n content: message,\n is_read: 1,\n timestamp: SENT_MESSAGE_TIMESTAMP,\n status: user.bandwidth_number_status === \"unavailable\" ? \"message-sending\" : null,\n };\n\n // Update the UI state for the displayed chat messages\n const outboundMessages = [...chatMessages];\n outboundMessages.push(SENT_MESSAGE);\n setChatMessages(outboundMessages);\n\n // Emit event to the socket with the newly sent message\n socket.emit(\"outbound message\", {\n chat_id: chatID,\n content: message,\n temporary_id: SENT_MESSAGE.id,\n });\n\n // Block all open chat panels temporarily from being able to send a new message\n // until a new number from Bandwidth has been issued\n if (user.bandwidth_number_status === \"unavailable\") {\n handleUpdateUserDetails({ bandwidth_number_status: \"pending\" });\n }\n\n // After successfully sending a message to an existing chat,\n // update that chat's last message properties that are displayed\n // in the chat messages dropdown menu\n if (!chatsContext.existingUserChats) return;\n\n const updatedExistingUserChats = [...chatsContext.existingUserChats];\n\n // Find the matching chat, move it to the start of the list and update it's last message value\n const matchingChatIndex: number = updatedExistingUserChats.findIndex(existingChat => {\n return existingChat.id === chatID;\n });\n\n // Exit if chat cannot be found\n if (matchingChatIndex < 0) return;\n\n // Update the last message value\n updatedExistingUserChats.splice(matchingChatIndex, 1, {\n ...updatedExistingUserChats[matchingChatIndex],\n last_message: {\n ...updatedExistingUserChats[matchingChatIndex].last_message,\n content: message,\n timestamp: SENT_MESSAGE_TIMESTAMP,\n },\n });\n\n // Update the cached query data\n queryClient.setQueryData([\"sms-chats\", user.id], updatedExistingUserChats);\n };\n\n socket.on(\"inbound message\", (incomingMessage: ChatResponseFields) => {\n // Prevent state updates if the chat is still marked as new\n if (details.is_new_chat) return;\n\n // Prevent state updates if the ID of the chat to which the message belongs to\n // does not corresponds to the currently opened chat\n if (incomingMessage.chat_id !== chatID) return;\n\n // Update the state with all the incoming messages\n const incomingMessages = [...chatMessages];\n incomingMessages.push(incomingMessage);\n setChatMessages(incomingMessages);\n });\n\n /*=====================================\n MESSAGE RECEIVED & STATUS EVENTS\n ======================================*/\n socket.on(\"message_received\", messageDetails => {\n // If the message does not belong to this chat, exit\n if (messageDetails.chat_id !== chatID) return;\n\n // Find the message from the list of already\n // present messages that should be updated in the UI\n const updatedMessages = [...chatMessages];\n const updatedMessageIndex: number = updatedMessages.findIndex(message => {\n return message.id === messageDetails.temporary_id;\n });\n\n // If the message cannot be found, exit\n if (updatedMessageIndex < 0) return;\n\n // Update the \"id\" property of the targeted message,\n // from its \"temporary\" value to the \"real\" value received from the server\n updatedMessages.splice(updatedMessageIndex, 1, {\n ...updatedMessages[updatedMessageIndex],\n id: messageDetails.id,\n });\n\n setChatMessages(updatedMessages);\n });\n\n socket.on(\"message_status\", messageDetails => {\n // If the message does not belong to this chat, exit\n if (messageDetails.chat_id !== chatID) return;\n\n // Find the message from the list of already\n // present messages that should be updated in the UI\n const updatedMessages = [...chatMessages];\n const updatedMessageIndex: number = updatedMessages.findIndex(message => {\n return message.id === messageDetails.message_id;\n });\n\n // If the message cannot be found, exit\n if (updatedMessageIndex < 0) return;\n\n // Update the \"status\" property of the targeted message\n updatedMessages.splice(updatedMessageIndex, 1, {\n ...updatedMessages[updatedMessageIndex],\n status: messageDetails.status,\n });\n\n setChatMessages(updatedMessages);\n\n // Re-enable all open chats to be able to send a new message once\n // new number from Bandwidth has been issued successfully\n if (user.bandwidth_number_status === \"pending\") {\n handleUpdateUserDetails({ bandwidth_number_status: \"available\" });\n }\n });\n\n return (\n \n
    \n \n {isFetching && isPending ? (\n CHATS_MESSAGE_SKELETON_PLACEHOLDERS.map(placeholder => (\n \n ))\n ) : !TEMPORARY_DISABLED &&\n chatMessages.length === 0 &&\n user.bandwidth_number_status === \"unavailable\" ? (\n \n ) : (\n chatMessagesUI.map(group => (\n \n ))\n )}\n \n
    \n\n {\n if (details.is_new_chat) {\n handleChatInitializeNewConversation(message);\n } else {\n handleChatSendNewMessage(message);\n }\n }}\n bandwidthNumberStatus={user.bandwidth_number_status}\n />\n \n );\n};\n\nexport default ChatPanelConversation;\n","// Hooks\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { useChatsContext } from \"../ChatWrapper/ChatContextWrapper\";\nimport { useAuth } from \"../../../providers/auth-context\";\nimport { useVisualViewportResize } from \"../../../hooks/useVisualViewportResize\";\nimport useOnClickOutside from \"../../../hooks/useOnClickOutside\";\n\n// Assets\nimport { MdClose as ChatPanelCloseIcon } from \"react-icons/md\";\nimport ChatPanelMinimizeIcon from \"../../../assets/images/icons/chat-panel-minimize-icon.svg?react\";\n\n// Components\nimport ChatActions from \"../ChatActions\";\nimport ChatAvatar from \"../ChatAvatar\";\nimport ChatPanelConversation from \"./ChatPanelConversation\";\n\n// Interfaces\nimport { ChatPanelProps } from \"../interfaces\";\n\nconst ChatPanel = ({ details, modifierClass = \"\" }: ChatPanelProps) => {\n const chatsContext = useChatsContext();\n const { user } = useAuth();\n\n /*============================\n MINIMIZED\n =============================*/\n const [isChatPanelMinimized, setIsChatPanelMinimized] = useState(details.is_minimized);\n\n // Update the internal state anytime the received prop value changes\n useEffect(() => {\n setIsChatPanelMinimized(details.is_minimized);\n }, [details.is_minimized]);\n\n const handleChatPanelMinimization = (event: React.MouseEvent) => {\n event.stopPropagation();\n chatsContext.handleMinimizeChat(details.id);\n };\n\n // Remove active (focused) state of the chat if the panel is to be minimized,\n // and also update the minimized state value for the chat that is saved in local storage\n useEffect(() => {\n if (isChatPanelMinimized) chatsContext.handleChatSetActive(null);\n }, [isChatPanelMinimized]);\n\n /*============================\n TOGGLE THE CHAT\n\n - Mark the opened chat panel as active\n - If minimized, expand it\n =============================*/\n const handleChatToggle = () => {\n chatsContext.handleChatSetActive(details.id);\n\n // If the chat is minimized, we want to expand it\n if (isChatPanelMinimized) chatsContext.handleMinimizeChat(details.id);\n };\n\n /*==========================\n CLOSE THE CHAT PANEL\n ===========================*/\n const handleCloseChat = (event: React.MouseEvent) => {\n event.stopPropagation();\n chatsContext.handleCloseChat(details.id);\n };\n\n /*==========================\n NOTIFY FOR NEW MESSAGE\n\n Adds a blinking animation to the chat panel\n if there's a new unread message in it \n ===========================*/\n const [hasNewUnreadMessage, setHasNewUnreadMessage] = useState(false);\n\n useEffect(() => {\n // If there are no existing user's chats, exit the function\n if (!chatsContext.existingUserChats || !chatsContext.existingUserChats.length) return;\n\n // Find the matching opened chat panel, from the list of existing user chats\n const chatIndex: number = chatsContext.existingUserChats.findIndex(chat => {\n return chat.id === details.chat_id;\n });\n\n // If such chat does not exist in the list, exit function\n if (chatIndex < 0) return;\n\n // If the last message for this chat is read, exit the function\n if (chatsContext.existingUserChats[chatIndex].last_message.is_read) {\n setHasNewUnreadMessage(false);\n } else {\n // If the last message for this chat is unread, trigger blinking animation\n setHasNewUnreadMessage(true);\n }\n }, [chatsContext.existingUserChats]);\n\n /*================================\n DEFOCUS CHATS\n =================================*/\n const chatPanelRef = useRef(null);\n useOnClickOutside(chatPanelRef, () => {\n // Prevent removing the active chat ID value when\n // clicked within the currently active chat panel\n if (details.id !== chatsContext.activeChatID) return;\n\n chatsContext.handleChatSetActive(null);\n });\n\n /*================================\n ACTIVE PANEL\n =================================*/\n const [chatPanelIsActive, setChatPanelIsActive] = useState(false);\n\n useEffect(() => {\n if (details.id === chatsContext.activeChatID) {\n setChatPanelIsActive(true);\n } else {\n setChatPanelIsActive(false);\n }\n }, [details, chatsContext.activeChatID]);\n\n /*================================\n SHRINK CHAT PANEL BASED\n ON VIEWPORT HEIGHT\n =================================*/\n const [shouldShrinkChatPanel, setShouldShrinkChatPanel] = useState(false);\n const viewportHeight = useVisualViewportResize();\n\n useEffect(() => {\n // Do not do anything if there's no viewport height to work with\n if (viewportHeight === 0) return;\n\n // Control whether the chat panel should be shrinked or not\n // based on if the device's viewport has changed due to virtual keyboard being opened\n // Constraint is set to 385px height here as that's the maximum height of the chat panel itself\n if (viewportHeight <= 385) {\n setShouldShrinkChatPanel(true);\n } else {\n setShouldShrinkChatPanel(false);\n }\n }, [viewportHeight]);\n\n // Do not render anything in the UI\n // until we have the neccessary data\n if (!user.id || !chatsContext.existingUserChats) return null;\n\n return (\n \n
    \n
    \n \n
    \n {details.applicant.name}\n
    \n
    \n\n
    \n {!details.is_new_chat && details.chat_id !== null ? (\n \n ) : null}\n\n handleChatPanelMinimization(event)}\n />\n handleCloseChat(event)}\n />\n
    \n
    \n\n
    \n \n
    \n \n );\n};\n\nexport default ChatPanel;\n","// Hooks\nimport { useEffect, useState } from \"react\";\n\n// Components\nimport ChatAvatar from \"./ChatAvatar\";\n\n// Context\nimport socket from \"../../config/chat-socket\";\nimport { useChatsContext } from \"./ChatWrapper/ChatContextWrapper\";\nimport { useAuth } from \"../../providers/auth-context\";\n\n// Interfaces\nimport { ChatBubbleProps } from \"./interfaces\";\nimport { ChatMessagesResponseFields, ChatResponseFields } from \"../../api/Chat/interfaces\";\n\n// Assets\nimport { IoCloseSharp as ChatBubbleCloseIcon } from \"react-icons/io5\";\nimport { useQueryClient } from \"@tanstack/react-query\";\n\nconst ChatBubble = ({ details }: ChatBubbleProps) => {\n const queryClient = useQueryClient();\n const chatsContext = useChatsContext();\n const { user } = useAuth();\n\n /*============================\n CLOSE SPECIFIC CHAT BUBBLE\n =============================*/\n const handleCloseChatBubble = (event: React.MouseEvent) => {\n event.stopPropagation();\n chatsContext.handleCloseChat(details.id);\n };\n\n /*============================\n COUNT THE UNREAD MESSAGES\n =============================*/\n const [chatMessages, setChatMessages] = useState([]);\n const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);\n const [latestReceivedMessage, setLatestReceivedMessage] = useState(\"\");\n\n useEffect(() => {\n if (!chatMessages.length) return;\n\n // Count the unread messages\n const unreadMessages: number = chatMessages.filter(message => !message.is_read).length;\n setUnreadMessagesCount(unreadMessages);\n }, [chatMessages]);\n\n // Read all the messages for this specific chat from the cached data\n // and update the state, which will trigger re-count of the unread messages\n const messagesForChat = queryClient.getQueryData([\n \"sms-chat-messages\",\n details.chat_id,\n ]) as ChatMessagesResponseFields;\n\n useEffect(() => {\n if (!messagesForChat || !messagesForChat.messages.length) return;\n\n setChatMessages(messagesForChat.messages);\n }, [messagesForChat]);\n\n /*============================\n INCOMING MESSAGES\n =============================*/\n socket.on(\"inbound message\", (incomingMessage: ChatResponseFields) => {\n // Do not show any messages and notifications if the\n // message that was received is not meant for the chat bubble\n if (incomingMessage.chat_id !== details.chat_id) return;\n\n const updatedChatMessages = [...chatMessages];\n updatedChatMessages.push(incomingMessage);\n setChatMessages(updatedChatMessages);\n\n // Update the text to be displayed for the latest received message\n setLatestReceivedMessage(incomingMessage.content);\n\n // Update the cached chat messages data for the chat for\n // which a new message was received trough the socket\n const cachedChatMessages = queryClient.getQueryData([\n \"sms-chat-messages\",\n incomingMessage.chat_id,\n ]) as ChatMessagesResponseFields;\n\n queryClient.setQueryData([\"sms-chat-messages\", incomingMessage.chat_id], {\n ...cachedChatMessages,\n messages: updatedChatMessages,\n });\n });\n\n // Remove the latest received message from state\n // shortly after its animation ends\n useEffect(() => {\n if (!latestReceivedMessage) return;\n\n // Reset the state shortly after the animation ends\n setTimeout(() => setLatestReceivedMessage(\"\"), 3000);\n }, [latestReceivedMessage]);\n\n // Do not render anything in the UI\n // until we have the neccessary data\n if (!user.id || !chatsContext.existingUserChats) return null;\n\n return (\n
    chatsContext.handleOpenBubbleChat(details)}>\n \n\n {latestReceivedMessage && (\n
    \n
    {details.applicant.name}
    \n

    {latestReceivedMessage}

    \n
    \n )}\n\n handleCloseChatBubble(event)}\n >\n \n
    \n\n {unreadMessagesCount > 0 ? (\n \n {unreadMessagesCount}\n \n ) : null}\n \n );\n};\n\nexport default ChatBubble;\n","// Hooks\nimport { useEffect, useState } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\n\n// Socket & Context\nimport socket from \"../../config/chat-socket\";\nimport { useChatsContext } from \"./ChatWrapper/ChatContextWrapper\";\n\n// Assets\nimport { IoCloseSharp as ChatBubbleCloseIcon } from \"react-icons/io5\";\n\n// Interfaces\nimport { ChatDetails } from \"./ChatWrapper/interfaces\";\nimport { ChatMessagesResponseFields, ChatResponseFields } from \"../../api/Chat/interfaces\";\nimport { ChatBubbleExtrasItemProps } from \"./interfaces\";\n\nconst ChatBubbleExtrasItem = ({\n chatDetails,\n handleCloseBubbleExtrasMenu,\n}: ChatBubbleExtrasItemProps) => {\n const queryClient = useQueryClient();\n const chatsContext = useChatsContext();\n\n /*============================\n COUNT THE UNREAD MESSAGES\n =============================*/\n const [chatMessages, setChatMessages] = useState([]);\n const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);\n useEffect(() => {\n if (!chatMessages.length) return;\n\n // Count the unread messages\n const unreadMessages: number = chatMessages.filter(message => !message.is_read).length;\n setUnreadMessagesCount(unreadMessages);\n }, [chatMessages]);\n\n // Read all the messages for this specific chat from the cached data\n // and update the state, which will trigger re-count of the unread messages\n const messagesForChat = queryClient.getQueryData([\n \"sms-chat-messages\",\n chatDetails.chat_id,\n ]) as ChatMessagesResponseFields;\n\n useEffect(() => {\n if (!messagesForChat || !messagesForChat.messages.length) return;\n setChatMessages(messagesForChat.messages);\n }, [messagesForChat]);\n\n /*============================\n INCOMING MESSAGES\n =============================*/\n socket.on(\"inbound message\", (incomingMessage: ChatResponseFields) => {\n // Do not show any messages and notifications if the\n // message that was received is not meant for the chat bubble\n if (incomingMessage.chat_id !== chatDetails.chat_id) return;\n\n const updatedChatMessages = [...chatMessages];\n updatedChatMessages.push(incomingMessage);\n setChatMessages(updatedChatMessages);\n });\n\n /*==========================\n HANDLE BUBBLE CHATS\n ===========================*/\n const handleOpenBubbleChat = (event: React.MouseEvent, chatDetails: ChatDetails) => {\n event.stopPropagation();\n chatsContext.handleOpenBubbleChat(chatDetails);\n handleCloseBubbleExtrasMenu();\n };\n\n const handleCloseBubbleChat = (event: React.MouseEvent, id: number) => {\n event.stopPropagation();\n chatsContext.handleCloseChat(id);\n };\n\n return (\n handleOpenBubbleChat(event, chatDetails)}\n >\n {chatDetails.applicant.name}\n\n handleCloseBubbleChat(event, chatDetails.id)}\n >\n \n \n\n {unreadMessagesCount > 0 ? (\n \n {unreadMessagesCount}\n \n ) : null}\n \n );\n};\n\nexport default ChatBubbleExtrasItem;\n","// Hooks\nimport { useRef, useState } from \"react\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\n\n// Context\nimport { useChatsContext } from \"./ChatWrapper/ChatContextWrapper\";\n\n// Components\nimport ChatBubbleExtrasItem from \"./ChatBubbleExtrasItem\";\nimport CustomScrollbars from \"../CustomScrollbars/CustomScrollbars\";\n\n// Interfaces\nimport { ChatBubbleExtrasProps } from \"./interfaces\";\n\nconst ChatBubbleExtras = ({ chats }: ChatBubbleExtrasProps) => {\n const { user } = useAuth();\n const chatsContext = useChatsContext();\n\n /*==========================\n MENU STATE\n ===========================*/\n const [isExtrasMenuOpen, setIsExtrasMenuOpen] = useState(false);\n const bubblesExtrasMenuRef = useRef(null);\n\n useOnClickOutside(bubblesExtrasMenuRef, () => setIsExtrasMenuOpen(false));\n\n // Do not render anything in the UI\n // until we have the neccessary data\n if (!user.id || !chatsContext.existingUserChats) return null;\n\n return (\n setIsExtrasMenuOpen(!isExtrasMenuOpen)}\n >\n
    + {chats.length}
    \n\n {isExtrasMenuOpen ? (\n
      \n \n {chats.map(chat => (\n setIsExtrasMenuOpen(false)}\n />\n ))}\n \n
    \n ) : null}\n \n );\n};\n\nexport default ChatBubbleExtras;\n","// Hooks & Utilities\nimport { createContext, PropsWithChildren, useContext, useEffect, useState } from \"react\";\nimport { useChatGetAllUserChats } from \"../../../api/Chat/Chat\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useAuth } from \"../../../providers/auth-context\";\nimport { useLocation } from \"react-router\";\nimport { cloneDeep } from \"lodash-es\";\nimport { LocalStorageActions } from \"../../../utilities/handleLocalStorage\";\nimport handleMoveArrayItem from \"../../../utilities/data/handleMoveArrayItem\";\nimport fetchHandler from \"../../../api/fetchHandler\";\nimport handlePermissionCheck from \"../../../utilities/handlePermissionCheck\";\nimport useWindowResize from \"../../../hooks/useWindowResize\";\n\n// Components\nimport ChatPanel from \"../Panel/ChatPanel\";\nimport ChatBubble from \"../ChatBubble\";\nimport ChatBubbleExtras from \"../ChatBubbleExtras\";\n\n// Interfaces\nimport { ChatDetails, ChatsContextProps } from \"./interfaces\";\nimport {\n ChatIsLastMessageRead,\n ChatResponseFields,\n ChatsForSpecificUserResponseFields,\n} from \"../../../api/Chat/interfaces\";\n\n// Socket Client\nimport socket from \"../../../config/chat-socket\";\n\nexport const ChatContext = createContext({\n existingUserChats: undefined,\n existingUserChatsLoading: false,\n unreadChatsCount: 0,\n activeChatID: null,\n handleOpenChat: () => undefined,\n handleCloseChat: () => undefined,\n handleMinimizeChat: () => undefined,\n handleChatSetActive: () => undefined,\n handleOpenBubbleChat: () => undefined,\n});\n\nconst ChatContextWrapper: React.FC = ({ children }) => {\n const queryClient = useQueryClient();\n const { user } = useAuth();\n\n useEffect(() => {\n // Manually connect to the socket once the ChatContextWrapper\n // component is mounted which happens only on Authenticated Layout pages.\n socket.connect();\n\n // Manually disconnect from the socket once the ChatContextWrapper\n // component has been unmounted (e.g. user moved to page that's not part of the Authenticated Layout)\n return () => {\n socket.disconnect();\n };\n }, []);\n\n /*============================\n USER'S CHATS\n =============================*/\n const { data: existingUserChats, isPending: existingUserChatsPending } = useChatGetAllUserChats();\n const [unreadChatsCount, setUnreadChatsCount] = useState(0);\n\n // Count all the chats whose last message is marked as unread\n useEffect(() => {\n if (!existingUserChats) return;\n\n const unreadChats = existingUserChats.filter(chat => !chat.last_message.is_read);\n setUnreadChatsCount(unreadChats.length);\n }, [existingUserChats]);\n\n /*============================\n ACTIVE (SELECTED) CHAT\n =============================*/\n const [activeChatID, setActiveChatID] = useState(null);\n\n const handleChatSetActive = (chatID: number | null) => setActiveChatID(chatID);\n\n /*============================\n OPENED CHATS\n =============================*/\n const [openedChats, setOpenedChats] = useState(() => {\n const chatsInStorage = LocalStorageActions.getItem(\"fch-chats\") || [];\n return chatsInStorage as ChatDetails[];\n });\n\n const handleOpenChat = (chat: ChatDetails) => {\n let openedChatsCopy = [...openedChats];\n\n // Find the index of the chat that we opened\n const chatIndex: number = openedChatsCopy.findIndex(openedChat => {\n return openedChat.id === chat.id;\n });\n\n // If the chat doesn't already exist, add it to the start of the list\n // Otherwise expand it, and if it was displayed as a bubble, rearrange it to first position\n if (chatIndex < 0) {\n openedChatsCopy.unshift(chat);\n } else {\n openedChatsCopy[chatIndex].is_minimized = false;\n\n // Only rearrange the chats if the chat that we tried reopening\n // was being displayed as a chat bubble / bubble extras item\n if (chatIndex > 1) {\n openedChatsCopy = handleMoveArrayItem(openedChatsCopy, chatIndex, 0);\n }\n }\n\n // Update the state of currently opened chats\n setOpenedChats(openedChatsCopy);\n\n // Mark the latest opened chat as active\n setActiveChatID(chat.id);\n };\n\n /*========================\n CLOSE CHAT\n =========================*/\n const handleCloseChat = (id: number) => {\n const filteredChats: ChatDetails[] = [...openedChats].filter(chat => {\n return chat.id !== id;\n });\n\n setOpenedChats(filteredChats);\n\n // If we closed the chat that was marked as active (focused)\n // then reset the corresponding state value to its defaults\n if (id === activeChatID) handleChatSetActive(null);\n };\n\n /*========================\n MINIMIZE CHAT\n =========================*/\n const handleMinimizeChat = (id: number) => {\n const openedChatsCopy = cloneDeep(openedChats);\n\n // Find the targeted chat from the list of opened chats\n const matchingChatIndex: number = openedChatsCopy.findIndex(chat => {\n return chat.id === id;\n });\n\n // If chat cannot be found, exit function\n if (matchingChatIndex < 0) return;\n\n // Update the minimized state of the chat\n openedChatsCopy.splice(matchingChatIndex, 1, {\n ...openedChatsCopy[matchingChatIndex],\n is_minimized: !openedChatsCopy[matchingChatIndex].is_minimized,\n });\n\n setOpenedChats(openedChatsCopy);\n };\n\n // Find the matching opened chats, with the chats returned from the API,\n // and update the state of the opened chats with the latest received changes from the API\n useEffect(() => {\n // If there are no opened chats, or no user chats fetched from the API, exit function\n if (!openedChats.length || !existingUserChats || !existingUserChats.length) return;\n\n const updatedChats: ChatDetails[] = [...openedChats];\n\n // From the list of fetched chats extract the chats whose IDs match\n // the IDs of the currently opened chats, so their values can be updated\n const matchingChats: ChatsForSpecificUserResponseFields[] = existingUserChats.filter(\n existingChat => {\n return openedChats.some(openedChat => {\n return existingChat.applicant.id === openedChat.applicant.id;\n });\n },\n );\n\n // Exit function if none of the chats received from the API\n // are part of the currently opened chats, as there'll be nothing to update\n if (!matchingChats.length) return;\n\n matchingChats.forEach(matchingChat => {\n // Find the index of the individual opened and matching chats\n // Using the \"applicant.id\" value as matching condition because this\n // will allow us to also target the chats that were brand new before sending a message\n // and update their ID and \"is_new_chat\" properties\n const openedChatIndex = updatedChats.findIndex(chat => {\n return chat.applicant.id === matchingChat.applicant.id;\n });\n\n // Exit function if chat cannot be found\n if (openedChatIndex < 0) return;\n\n // Update the targeted opened chat with the new details\n // received from the API response\n updatedChats.splice(openedChatIndex, 1, {\n ...updatedChats[openedChatIndex],\n id: matchingChat.id,\n is_blocked: matchingChat.is_blocked,\n is_new_chat: false,\n chat_id: matchingChat.id,\n });\n });\n\n // Update the state of the opened chats\n setOpenedChats(updatedChats);\n }, [existingUserChats]);\n\n /*============================\n OPEN CHAT BUBBLES\n\n Moves the clicked chat displayed as a bubble\n to the front of the list of opened chats, so it\n can be opened as a chat panel instead\n =============================*/\n const handleOpenBubbleChat = (chat: ChatDetails) => {\n // Find the chat that was previously displayed as a bubble\n const bubbleChatIndex: number = openedChats.findIndex(bubbleChat => {\n return bubbleChat.id === chat.id;\n });\n\n // Exit function if bubble cannot be found\n if (bubbleChatIndex < 0) return;\n\n // Move the chat from it's bubble position, to the front of the list\n const rearrangedChats = handleMoveArrayItem(openedChats, bubbleChatIndex, 0);\n\n // Always set the first item in the newly rearranged array to not be minimized\n const rearrangedChatsDeepCopy = cloneDeep(rearrangedChats);\n rearrangedChatsDeepCopy[0].is_minimized = false;\n\n setOpenedChats(rearrangedChatsDeepCopy);\n\n // Mark the bubble chat that was opened as active\n setActiveChatID(chat.id);\n };\n\n /*=============================\n LISTEN TO INCOMING MESSAGES\n\n If there's an incoming message, update the list\n of existing user chats that are displayed in the chats dropdown\n ==============================*/\n socket.on(\"inbound message\", (incomingMessage: ChatResponseFields) => {\n if (!existingUserChats || !existingUserChats.length) return;\n\n // Find the matching chat ID to which the incoming message is belonging\n const matchingChatIndex: number = existingUserChats.findIndex(existingChat => {\n return existingChat.id === incomingMessage.chat_id;\n });\n\n // Exit function if chat does not exist\n if (matchingChatIndex < 0) return;\n\n // Received message should be marked as \"read\" only if the chat\n // that received the message is `opened` and `focused`\n const isIncomingMessageRead: ChatIsLastMessageRead =\n activeChatID != null ? (activeChatID === incomingMessage.chat_id ? 1 : 0) : 0;\n\n // Update the last message value\n const updatedExistingUserChats = [...existingUserChats];\n updatedExistingUserChats.splice(matchingChatIndex, 1, {\n ...updatedExistingUserChats[matchingChatIndex],\n last_message: {\n ...updatedExistingUserChats[matchingChatIndex].last_message,\n content: incomingMessage.content,\n timestamp: incomingMessage.timestamp,\n is_read: isIncomingMessageRead,\n },\n });\n\n // Update the cached query data\n queryClient.setQueryData([\"sms-chats\", user.id], updatedExistingUserChats);\n });\n\n /*=============================\n HIDE CHAT PANELS & BUBBLES \n IF VISTING CHATS PAGE\n ===============================*/\n const [shouldDisplayChats, setShouldDisplayChats] = useState(true);\n const location = useLocation();\n const hasChatsReadPermission = handlePermissionCheck([\"sms_read\"]);\n\n useEffect(() => {\n // If the user values are received, and the user\n // does not have the needed permission to see the chats,\n // then remove them from local storage (if there were any)\n if (user.id && !hasChatsReadPermission) {\n LocalStorageActions.removeItem(\"fch-chats\");\n setShouldDisplayChats(false);\n\n return;\n }\n\n if (location.pathname.startsWith(\"/chats/messages\")) {\n setShouldDisplayChats(false);\n } else {\n setShouldDisplayChats(true);\n }\n }, [user.id, hasChatsReadPermission, location.pathname]);\n\n /*=============================\n RESAVE THE OPENED CHATS IN \n LOCAL STORAGE ON ANY CHANGE\n ===============================*/\n useEffect(() => {\n // Prevent re-saving the chats to local storage if the user\n // does not have the necessary \"sms_read\" permission\n if (user.id && !hasChatsReadPermission) return;\n\n const filteredOpenChats: ChatDetails[] = [...openedChats].filter(chat => !chat.is_new_chat);\n LocalStorageActions.saveItem(\"fch-chats\", filteredOpenChats);\n }, [openedChats, user.id, hasChatsReadPermission]);\n\n /*=================================\n MARK MESSAGES AS READ ON FOCUS\n ==================================*/\n\n useEffect(() => {\n // If there are no existing user chats yet, exit function\n if (!existingUserChats || !existingUserChats.length) return;\n\n // If there's no valid active chat value selected, exit function\n if (!activeChatID) return;\n\n // Find the focused chat from the list of existing user chats\n const matchingChatIndex: number = existingUserChats.findIndex(chat => {\n return chat.id === activeChatID;\n });\n\n // If the chat that is selected as active cannot be\n // found within the list of existing chats, exit function\n if (matchingChatIndex < 0) return;\n\n // If all the messages for the targeted chat\n // are already marked as read do not send emit the event\n if (existingUserChats[matchingChatIndex].last_message.is_read) return;\n\n socket.emit(\"mark_chat_as_read\", { chat_id: activeChatID });\n }, [activeChatID]);\n\n // Listen to receiving events from the socket to\n // trigger UI updates marking the messages for the matching chat as read\n socket.on(\"chat_marked_as_read\", chatData => {\n // If there are no existing user chats yet, exit function\n if (!existingUserChats || !existingUserChats.length) return;\n\n // Find the chat for who we want to mark all messages as read\n const matchingChatIndex: number = existingUserChats.findIndex(chat => {\n return chat.id === chatData.chat_id;\n });\n\n // If the chat for which we received the event cannot be\n // found within the list of existing chats, exit function\n if (matchingChatIndex < 0) return;\n\n // If all of the messages for the targeted chat are already\n // marked as \"read\", exit the function, to prevent unnecessary state updates\n if (existingUserChats[matchingChatIndex].last_message.is_read) return;\n\n // Update all the messages for this chat as \"read\"\n const updatedChats = [...existingUserChats];\n updatedChats[matchingChatIndex] = {\n ...updatedChats[matchingChatIndex],\n last_message: {\n ...updatedChats[matchingChatIndex].last_message,\n is_read: 1,\n },\n };\n\n // Update the query data for the existing user chats\n queryClient.setQueryData([\"sms-chats\", user.id], updatedChats);\n });\n\n /*=================================\n FETCH MESSAGES FOR OPENED CHATS\n ON PAGE LOAD\n ==================================*/\n useEffect(() => {\n handleFetchMessagesForOpenedChats();\n }, []);\n\n const handleFetchMessagesForOpenedChats = async () => {\n const savedChats = LocalStorageActions.getItem(\"fch-chats\") || [];\n\n // Exit function if there are no chats to work with\n if (!savedChats || !savedChats.length) return;\n\n savedChats.forEach(async (savedChat: ChatResponseFields) => {\n const data = await fetchHandler(\"GET\", `sms/chats/${savedChat.chat_id}/messages`);\n\n queryClient.setQueryData([\"sms-chat-messages\", savedChat.chat_id], data);\n });\n };\n\n /*=====================================\n CONTROL HOW THE CHATS WRAPPER\n CONTENT IS BEING RENDERED\n\n - For >=768px default behavior with\n up to 2 chat panels, 6 bubbles, and rest\n is considered as \"extras\"\n - For <768px only 1 chat panel and all\n other chats that were opened are handled\n trough the \"extras\" menu\n ======================================= */\n const [windowWidth] = useWindowResize(300);\n\n return (\n \n {children}\n\n {shouldDisplayChats ? (\n windowWidth >= 768 ? (\n
    \n
    \n {openedChats.slice(0, 2).map(chat => (\n \n ))}\n
    \n\n {openedChats.length > 2 ? (\n
    \n {openedChats.slice(2, 8).map(chat => (\n \n ))}\n\n {openedChats.length > 8 ? : null}\n
    \n ) : null}\n
    \n ) : (\n
    \n
    \n {openedChats.slice(0, 1).map(chat => (\n \n ))}\n {openedChats.length > 1 ? (\n
    \n {openedChats.length > 1 ? (\n \n ) : null}\n
    \n ) : null}\n
    \n
    \n )\n ) : null}\n \n );\n};\n\n// Wrapper hook around the context provider\n// ensuring that the provider is available to be consumed\nconst useChatsContext = () => {\n const chatsContext = useContext(ChatContext);\n\n // Handle potential scenario in which the provider is not available yet to be used\n if (!chatsContext) throw new Error(\"Chats Context Provider does not exist.\");\n\n return chatsContext;\n};\n\nexport { ChatContextWrapper, useChatsContext };\n","// Utilities & Hooks\nimport { useMemo, useRef, useState } from \"react\";\nimport { orderBy } from \"lodash\";\nimport { useAuth } from \"../../providers/auth-context\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\nimport useWindowResize from \"../../hooks/useWindowResize\";\n\n// Components\nimport { Link, useLocation } from \"react-router\";\nimport Tooltip from \"../Tooltip/Tooltip\";\nimport ChatMessagesSort from \"./ChatMessagesSort\";\nimport ChatMessageListItem from \"./ChatMessageListItem\";\nimport Loader from \"../Loader/Loader\";\nimport SkeletonChatMessageListItem from \"./Skeleton/SkeletonChatMesageListItem\";\n\n// Assets\nimport SmsIcon from \"../../assets/images/icons/sms-icon.svg?react\";\n\n// Interfaces\nimport { ChatsForSpecificUserResponseFields } from \"../../api/Chat/interfaces\";\nimport { ChatsSortBy } from \"./interfaces\";\n\n// Constants\nimport { CHATS_SORTING_OPTIONS } from \"./constants\";\n\n// Context\nimport { useChatsContext } from \"./ChatWrapper/ChatContextWrapper\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport {\n FRAMER_HEADER_ANIMATION_WITH_OFFSET,\n FRAMER_HEADER_TRANSITIONS,\n} from \"../../constants/framer\";\n\nconst ChatMessagesDropdown = () => {\n const chatsContext = useChatsContext();\n\n const location = useLocation();\n const { user } = useAuth();\n\n // If the page that we're currently viewing is the \"Chat Messages\" dedicated page\n // then we hide the chat messages dropdown icon and we render nothing for this component\n if (location.pathname.startsWith(\"/chats/messages\")) return null;\n\n /*============================\n DROPDOWN VISIBILITY\n =============================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n const dropdownRef = useRef(null);\n\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n\n /*============================\n USER'S CHATS\n\n - Fetch from the API\n - Map over the received list\n - Order (sort) the chats based on selected sorting option\n - Extract up to 5 chats to be shown\n =============================*/\n const [sortBy, setSortBy] = useState(\"most_recent\");\n const [windowWidth] = useWindowResize();\n\n const CHATS = useMemo(() => {\n // If there's no chats data available, exit function\n if (\n !chatsContext.existingUserChats ||\n !chatsContext.existingUserChats.length ||\n chatsContext.existingUserChatsLoading\n ) {\n return [];\n }\n\n // Make a copy of the original data before manipulating it\n let chatsData = [...chatsContext.existingUserChats];\n\n // Filter out the data that will be presented in the dropdown menu\n // so that we dont take into consideration messages that are not\n // matching the condition used for sorting the list of displayed messages\n if ([\"blocked\", \"unread\"].includes(sortBy)) {\n chatsData = chatsData.filter((chat: any) => {\n return chat[CHATS_SORTING_OPTIONS[sortBy].fieldname];\n });\n }\n\n // Order the chat messages data based on selected option\n chatsData = orderBy(\n chatsData,\n [CHATS_SORTING_OPTIONS[sortBy].fieldname],\n [CHATS_SORTING_OPTIONS[sortBy].direction],\n );\n\n // Display the latest chats in the dropdown menu\n // If mobile device display the latest 3 chats,\n // and if tablet or desktop then display up to 5 of the latest chats\n return chatsData.slice(0, windowWidth > 575 ? 5 : 3);\n }, [chatsContext.existingUserChats, sortBy, windowWidth]);\n\n /*===============================\n MARK CHAT AS READ\n ================================*/\n const handleChatSelection = (chatID: number) => {\n // If there are no existing user chats, prevent any selection\n if (!chatsContext.existingUserChats || !chatsContext.existingUserChats.length) return;\n\n // Mark the specific chat as read in the UI\n const targetedChatIndex: number = chatsContext.existingUserChats.findIndex(chat => {\n return chat.id === chatID;\n });\n\n if (targetedChatIndex < 0) return;\n\n // Open a chat panel for the selected chat\n chatsContext.handleOpenChat({\n id: chatsContext.existingUserChats[targetedChatIndex].id,\n chat_id: chatsContext.existingUserChats[targetedChatIndex].id,\n application_id: null,\n is_blocked: chatsContext.existingUserChats[targetedChatIndex].is_blocked,\n is_new_chat: false,\n applicant: {\n id: chatsContext.existingUserChats[targetedChatIndex].applicant.id,\n name: chatsContext.existingUserChats[targetedChatIndex].applicant.full_name,\n photo: chatsContext.existingUserChats[targetedChatIndex].applicant.photo,\n },\n is_minimized: false,\n });\n\n // Close the dropdown menu after a chat was opened\n setIsDropdownOpen(false);\n };\n\n /*=======================\n CLOSE ON \"ESCAPE\" KEY\n ========================*/\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n \n \n
    setIsDropdownOpen(!isDropdownOpen)}>\n \n\n {chatsContext.unreadChatsCount > 0 ? (\n {chatsContext.unreadChatsCount}\n ) : null}\n
    \n \n\n \n {isDropdownOpen ? (\n \n {/* HEADER */}\n
    \n
    \n
    \n \n Hi, {user.first_name ?? }!\n \n
    \n\n {chatsContext.unreadChatsCount > 0 ? (\n
    \n {chatsContext.unreadChatsCount} unread{\" \"}\n {chatsContext.unreadChatsCount === 1 ? \"message\" : \"messages\"}\n
    \n ) : null}\n
    \n setSortBy(sort)}\n />\n
    \n\n
    \n {chatsContext.existingUserChatsLoading ? (\n [...Array(windowWidth > 575 ? 5 : 3).keys()].map((_, skeletonIndex: number) => (\n \n ))\n ) : CHATS.length > 0 ? (\n CHATS.map((chat: ChatsForSpecificUserResponseFields) => (\n \n ))\n ) : (\n
    \n
    No conversations found.
    \n
    \n )}\n
    \n\n {/* FOOTER */}\n
    \n \n See All\n \n
    \n \n ) : null}\n
    \n \n );\n};\n\nexport default ChatMessagesDropdown;\n","import * as React from \"react\";\nconst SvgHeaderNeedHelpIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { xmlns: \"http://www.w3.org/2000/svg\", viewBox: \"0 0 30.9 30.9\", width: 25, height: 25, ...props }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"path\", { fill: \"#FFFFFF\", d: \"M15.45,29.12c-7.55,0-13.67-6.12-13.67-13.67S7.9,1.78,15.45,1.78s13.67,6.12,13.67,13.67-6.12,13.67-13.67,13.67h0ZM15.45,0C6.92,0,0,6.92,0,15.45s6.92,15.45,15.45,15.45,15.45-6.92,15.45-15.45S23.98,0,15.45,0h0Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"#FFFFFF\", d: \"M15.15,24.91c-.93,0-1.72-.76-1.72-1.74s.78-1.72,1.72-1.72c.98,0,1.72.78,1.72,1.72,0,.98-.73,1.74-1.72,1.74h0ZM20.82,11.54c-.08,2.27-1.99,4.16-3.61,5.65l-.15.13c-.15.13-.23.3-.23.53v1.24c0,.68-.5,1.16-1.14,1.16h-1.11c-.68,0-1.16-.48-1.16-1.16v-2.4c0-.35.13-.66.35-.88.28-.28.53-.45.81-.73,1.29-1.21,2.75-2.62,2.83-3.56v-.08c0-1.19-.96-2.09-2.09-2.09-.68,0-1.34.3-1.72.88-.28.43-.25.86-.25,1.19.03.28.03.48-.05.63l-.03.03c-.18.28-.43.4-.76.4h-1.21c-.68.03-1.09-.43-1.19-.91v-.03c-.1-1.19-.03-1.51.08-1.89l.05-.13c.78-2.32,2.55-3.53,5.07-3.53,2.98,0,5.43,2.45,5.5,5.4v.15h0Z\" }))));\nexport default SvgHeaderNeedHelpIcon;\n","// Hooks & Utilities\nimport { useEffect, useRef, useState } from \"react\";\nimport useOnClickOutside from \"../../hooks/useOnClickOutside\";\nimport { useOnEscapeKey } from \"../../hooks/useOnEscapeKey\";\nimport { useLocation } from \"react-router\";\n\n// Components\nimport Tooltip from \"../Tooltip/Tooltip\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n// Assets & statics\nimport HelpIcon from \"../../assets/images/icons/header-need-help-icon.svg?react\";\nimport {\n FRAMER_HEADER_ANIMATION_WITH_OFFSET,\n FRAMER_HEADER_TRANSITIONS,\n} from \"../../constants/framer\";\nimport { HEADER_DROPDOWN_HELP } from \"./statics\";\n\nconst HeaderDropdownNeedHelp = () => {\n /*======================\n HANDLE DROPDOWN MENU\n =======================*/\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n const location = useLocation();\n\n useEffect(() => {\n setIsDropdownOpen(false);\n }, [location.pathname]);\n\n /*=======================================\n CLOSE ON \"ESCAPE\" KEY & OUTSIDE CLICK\n ======================================*/\n const dropdownRef = useRef(null);\n useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false));\n useOnEscapeKey(dropdownRef, () => setIsDropdownOpen(false));\n\n return (\n \n \n setIsDropdownOpen(!isDropdownOpen)}\n className={`header-dropdown__body ${\n isDropdownOpen ? \"header-dropdown__body--active\" : \"\"\n }`}\n data-testid=\"component:dropdown-header-need-help\"\n >\n \n \n\n \n {isDropdownOpen ? (\n <>\n \n
    \n
      \n {HEADER_DROPDOWN_HELP.items.map(dropdownItem => {\n return (\n \n {dropdownItem.text}\n \n );\n })}\n
    \n
    \n \n \n ) : null}\n
    \n \n \n );\n};\n\nexport default HeaderDropdownNeedHelp;\n","import * as React from \"react\";\nconst SvgCareerPagesIcon = (props) => /* @__PURE__ */ React.createElement(\"svg\", { id: \"Layer_2\", \"data-name\": \"Layer 2\", xmlns: \"http://www.w3.org/2000/svg\", width: 29, height: 22, viewBox: \"0 0 29.45 22.83\", ...props }, /* @__PURE__ */ React.createElement(\"defs\", null, /* @__PURE__ */ React.createElement(\"style\", null, \"\\n .cls-1 {\\n fill: #fff;\\n }\\n \")), /* @__PURE__ */ React.createElement(\"g\", { id: \"Layer_4\", \"data-name\": \"Layer 4\" }, /* @__PURE__ */ React.createElement(\"g\", null, /* @__PURE__ */ React.createElement(\"path\", { fill: \"currentColor\", d: \"M27.38,0H2.08C.93,0,0,.93,0,2.08v18.68c0,1.15.93,2.08,2.08,2.08h25.3c1.15,0,2.08-.93,2.08-2.08V2.08c0-1.15-.93-2.08-2.08-2.08ZM13.25,2.95c.81,0,1.47.66,1.47,1.47s-.66,1.47-1.47,1.47-1.47-.66-1.47-1.47.66-1.47,1.47-1.47ZM8.84,2.95c.81,0,1.47.66,1.47,1.47s-.66,1.47-1.47,1.47-1.47-.66-1.47-1.47.66-1.47,1.47-1.47ZM4.42,2.95c.81,0,1.47.66,1.47,1.47s-.66,1.47-1.47,1.47-1.47-.66-1.47-1.47.66-1.47,1.47-1.47ZM26.14,18.06c0,1.01-.82,1.82-1.82,1.82H5.13c-1.01,0-1.82-.82-1.82-1.82v-8.06c0-1.05.85-1.9,1.9-1.9h19.03c1.05,0,1.9.85,1.9,1.9v8.06ZM25.4,5.15h-5.15c-.61,0-1.1-.49-1.1-1.1s.49-1.1,1.1-1.1h5.15c.61,0,1.1.49,1.1,1.1s-.49,1.1-1.1,1.1Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"currentColor\", d: \"M7.86,11.05h10.07c.61,0,1.1.49,1.1,1.1h0c0,.62-.49,1.11-1.1,1.11H7.86c-.34,0-.62-.28-.62-.62v-.98c0-.34.28-.62.62-.62Z\" }), /* @__PURE__ */ React.createElement(\"path\", { fill: \"currentColor\", d: \"M7.86,15.46h6.38c.61,0,1.1.49,1.1,1.1h0c0,.62-.49,1.11-1.1,1.11h-6.38c-.34,0-.62-.28-.62-.62v-.98c0-.34.28-.62.62-.62Z\" }))));\nexport default SvgCareerPagesIcon;\n","import { Link } from \"react-router\";\nimport handlePermissionCheck from \"../../utilities/handlePermissionCheck\";\nimport Tooltip from \"../Tooltip/Tooltip\";\nimport CareerPagesIcon from \"../../assets/images/icons/career-pages-icon.svg?react\";\nimport { useAuth } from \"../../providers/auth-context\";\n\nconst HeaderCareerPages = () => {\n const { user } = useAuth();\n\n const hasCareerPageEditingPermission = handlePermissionCheck([\"career_page_edit\"]);\n\n return (\n \n {hasCareerPageEditingPermission ? (\n \n \n \n ) : user.active_company.is_career_page_enabled ? (\n \n \n \n ) : (\n \n \n \n )}\n \n );\n};\n\nexport default HeaderCareerPages;\n","// Hooks & Utilities\nimport { useAuth } from \"../../providers/auth-context\";\nimport { useEffect, useState } from \"react\";\n\n// Assets\nimport Logo from \"../../assets/images/fch-logo.png\";\nimport ApplicationsIcon from \"../../assets/images/icons/applications-icon.svg?react\";\nimport AppointmentIcon from \"../../assets/images/icons/appointments-icon.svg?react\";\n\n// Interfaces\nimport { HeaderProps } from \"./interfaces\";\n\n// Components\nimport { Link, useLocation } from \"react-router\";\nimport HeaderDropdownAccount from \"./HeaderDropdownAccount\";\nimport HeaderDropdownAdminMenu from \"./HeaderDropdownAdminMenu\";\nimport HeaderDropdownWebsite from \"./HeaderDropdownWebsite\";\nimport ArticlesNotificationsMenu from \"../Articles/ArticlesNotificationsMenu\";\nimport HeaderCompaniesMenu from \"./HeaderCompaniesMenu\";\nimport Tooltip from \"../Tooltip/Tooltip\";\nimport PermissionCheckComponentWrapper from \"../Wrappers/PermissionCheckComponentWrapper\";\nimport ChatMessagesDropdown from \"../Chat/ChatMessagesDropdown\";\nimport HeaderDropdownNeedHelp from \"./HeaderDropdownHelp\";\nimport HeaderCareerPages from \"./HeaderCareerPages\";\n\nconst Header = ({ isFullHeader = true }: HeaderProps) => {\n const { user } = useAuth();\n const { pathname } = useLocation();\n\n /*================================\n CONTROL COMPANIES MENU DISPLAY\n =================================*/\n const [shouldShowCompaniesMenu, setShouldShowCompaniesMenu] = useState(false);\n\n useEffect(() => {\n // If user doesn't have access to any company, do not display the menu\n // Super Admins and Account Manager (admins) have access to all companies\n if (![\"super admin\", \"admin\"].includes(user?.role?.toLowerCase()) && !user.companies.length) {\n setShouldShowCompaniesMenu(false);\n return;\n }\n\n // Do not display the menu if the page is not supposed to do that\n if (!isFullHeader) {\n setShouldShowCompaniesMenu(false);\n return;\n }\n\n setShouldShowCompaniesMenu(true);\n }, [user, pathname]);\n\n return (\n
    \n {/*LOGO*/}\n {![\"super admin\", \"admin\"].includes(user.role?.toLowerCase()) && !user.active_company.slug ? (\n
    \n \"FirstChoice\n
    \n ) : (\n \n \"FirstChoice\n \n )}\n\n {/* COMPANIES */}\n {shouldShowCompaniesMenu ? : null}\n\n \n\n \n
    \n );\n};\nexport default Header;\n","export default \"__VITE_ASSET__NKLB0cUl__\"","import FCHLogoFull from \"../../assets/images/fch-full-logo-white.png\";\n\nconst Footer = ({ noLogo = false }) => {\n // Get the current year\n const copyrightYear = new Date().getFullYear();\n\n return (\n \n {!noLogo && (\n \n )}\n

    \n ® FirstChoice Hiring 2014-{copyrightYear}. All Rights Reserved.\n

    \n \n );\n};\nexport default Footer;\n","import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { cloneDeep } from \"lodash-es\";\nimport { toast } from \"react-toastify\";\nimport { BannerCreatePayload, BannerEditPayload, BannersListResponse } from \"./interfaces\";\nimport fetchHandler from \"../fetchHandler\";\n\n/** Get the list of all existing banners */\nexport const useMarketingBannersGetAll = () => {\n return useQuery({\n queryKey: [\"marketing-banners-all\"],\n queryFn: async () => {\n return (await fetchHandler(\"GET\", \"admin/banners\")) as BannersListResponse[];\n },\n });\n};\n\n/** Create a new marketing announcement banner */\nexport const useMarketingBannersCreate = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (createPayload: BannerCreatePayload) => {\n return await fetchHandler(\"POST\", \"admin/banners\", createPayload);\n },\n onMutate: (createPayload: BannerCreatePayload) => {\n // Get currently cached banners\n const cachedBanners = queryClient.getQueryData([\n \"marketing-banners-all\",\n ]) as BannersListResponse[];\n\n // Add the new banner to the list of existing banners\n const updatedBannersList = cloneDeep(cachedBanners);\n updatedBannersList.push({\n id: 0,\n text: createPayload.text,\n hyperlink: createPayload.hyperlink,\n is_active: false,\n order: 0,\n });\n\n // Update the cached data\n queryClient.setQueryData([\"marketing-banners-all\"], updatedBannersList);\n\n // Show a success notification in the UI\n toast.success(\"Successfully created new banner!\", {\n toastId: \"marketing-banners-new\",\n });\n\n return { cachedBanners };\n },\n onError: (error, _banner, context) => {\n toast.dismiss(\"marketing-banners-new\");\n queryClient.setQueryData([\"marketing-banners-all\"], context?.cachedBanners);\n return error;\n },\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n queryClient.invalidateQueries({ queryKey: [\"marketing-banners-all\"] });\n },\n });\n};\n\n/** Update the targeted marketing announcement banner */\nexport const useMarketingBannersEdit = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (editPayload: BannerEditPayload) => {\n return await fetchHandler(\"PUT\", `admin/banners/${editPayload.id}`, editPayload);\n },\n onMutate: editPayload => {\n // Get currently cached banners\n const cachedBanners = queryClient.getQueryData([\n \"marketing-banners-all\",\n ]) as BannersListResponse[];\n\n // Early exit in case there are no banners to be found in the cache\n if (!cachedBanners || !cachedBanners.length) return { cachedBanners };\n\n // Find the targeted banner\n const targetedBannerIndex = cachedBanners.findIndex(banner => {\n return banner.id === editPayload.id;\n });\n\n // Early exit in case the targeted banner cannot be found\n if (targetedBannerIndex < 0) return { cachedBanners };\n\n // Update the targeted banner\n const updatedBannersList = cloneDeep(cachedBanners);\n updatedBannersList[targetedBannerIndex] = {\n ...updatedBannersList[targetedBannerIndex],\n text: editPayload.text,\n hyperlink: editPayload.hyperlink,\n };\n\n // Update the cached data\n queryClient.setQueryData([\"marketing-banners-all\"], updatedBannersList);\n\n // Show a success notification in the UI\n toast.success(\"Successfully edited banner!\", {\n toastId: \"marketing-banners-edit\",\n });\n\n return { cachedBanners };\n },\n onError: (error, _banner, context) => {\n toast.dismiss(\"marketing-banners-edit\");\n queryClient.setQueryData([\"marketing-banners-all\"], context?.cachedBanners);\n return error;\n },\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n queryClient.invalidateQueries({ queryKey: [\"marketing-banners-all\"] });\n },\n });\n};\n\n/** Delete the targeted marketing announcement banner */\nexport const useMarketingBannersDelete = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (bannerID: number) => {\n return await fetchHandler(\"DELETE\", `admin/banners/${bannerID}`);\n },\n onMutate: (bannerID: number) => {\n // Get currently cached banners\n const cachedBanners = queryClient.getQueryData([\n \"marketing-banners-all\",\n ]) as BannersListResponse[];\n\n // Remove the targeted banner from the list\n let updatedBannersList = cloneDeep(cachedBanners);\n updatedBannersList = updatedBannersList.filter(banner => banner.id !== bannerID);\n\n // Update the cached data\n queryClient.setQueryData([\"marketing-banners-all\"], updatedBannersList);\n\n // Show a success notification in the UI\n toast.success(\"Successfully deleted banner!\", {\n toastId: \"marketing-banners-delete\",\n });\n\n return { cachedBanners };\n },\n onError: (error, _banner, context) => {\n toast.dismiss(\"marketing-banners-delete\");\n queryClient.setQueryData([\"marketing-banners-all\"], context?.cachedBanners);\n return error;\n },\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n queryClient.invalidateQueries({ queryKey: [\"marketing-banners-all\"] });\n },\n });\n};\n\n/** Update the status of the targeted marketing announcement banner */\nexport const useMarketingBannersToggleStatus = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (bannerID: number) => {\n return await fetchHandler(\"POST\", `admin/banners/${bannerID}/change-status`);\n },\n onMutate: (bannerID: number) => {\n // Get currently cached banners\n const cachedBanners = queryClient.getQueryData([\n \"marketing-banners-all\",\n ]) as BannersListResponse[];\n\n // Early exit in case there are no banners to be found in the cache\n if (!cachedBanners || !cachedBanners.length) return { cachedBanners };\n\n // Find the targeted banner\n const targetedBannerIndex = cachedBanners.findIndex(banner => {\n return banner.id === bannerID;\n });\n\n // Early exit in case the targeted banner cannot be found\n if (targetedBannerIndex < 0) return { cachedBanners };\n\n // Update the targeted banner\n const updatedBannersList = cloneDeep(cachedBanners);\n const targetedBannerStatus = updatedBannersList[targetedBannerIndex].is_active;\n updatedBannersList[targetedBannerIndex].is_active = !targetedBannerStatus;\n\n // Update the cached data\n queryClient.setQueryData([\"marketing-banners-all\"], updatedBannersList);\n\n // Show a success notification in the UI\n toast.success(\n `Successfully updated banner status to ${!targetedBannerStatus ? \"active\" : \"inactive\"}!`,\n { toastId: \"marketing-banners-status-toggle\" },\n );\n\n return { cachedBanners };\n },\n onError: (error, _banner, context) => {\n toast.dismiss(\"marketing-banners-status-toggle\");\n queryClient.setQueryData([\"marketing-banners-all\"], context?.cachedBanners);\n return error;\n },\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n queryClient.invalidateQueries({ queryKey: [\"marketing-banners-all\"] });\n },\n });\n};\n\n/** Marks the marketing announcement banners as closed for the logged in user */\nexport const useMarketingBannersClose = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async () => {\n return fetchHandler(\"POST\", \"banners/close\");\n },\n onError: error => error,\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n },\n });\n};\n\n/** Update the order in which the marketing announcement banners are shown to the user */\nexport const useMarketingBannersReorder = () => {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (banner_ids: number[]) => {\n return fetchHandler(\"POST\", `admin/banners/order`, { banner_ids });\n },\n onError: error => error,\n onSettled: () => {\n queryClient.invalidateQueries({ queryKey: [\"marketing-banners-all\"] });\n queryClient.invalidateQueries({ queryKey: [\"user-profile\"] });\n },\n });\n};\n","import { useEffect, useRef } from \"react\";\n\ntype IntervalCallbackFunction = () => unknown | void;\n\n/**\n *\n * Hook for ease of usage of `setInterval` functionality (e.g. for timers)\n *\n * @param callback The passed callback function that will be triggered where the hook is being used.\n * @param delay The amonut of delay between each iterration of the called `setInterval`. Defaults to `500ms`\n *\n */\nfunction useInterval(callback: IntervalCallbackFunction, delay: number | null = 500) {\n const savedCallback = useRef(callback);\n\n useEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n useEffect(() => {\n if (typeof delay !== \"number\") return;\n\n const tick = () => savedCallback.current();\n const interval = setInterval(tick, delay);\n return () => clearInterval(interval);\n }, [delay]);\n}\n\nexport default useInterval;\n","// Hooks\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useEffect, useState } from \"react\";\nimport { useMarketingBannersClose } from \"../../api/Marketing/Banners\";\nimport { useTour } from \"../../providers/tour-context\";\nimport useInterval from \"../../hooks/useInterval\";\nimport useErrorReporting from \"../../hooks/useErrorReporting\";\nimport parse from \"html-react-parser\";\n\n// Assets\nimport { IoClose as MarketingBannerCloseIcon } from \"react-icons/io5\";\nimport { FaChevronUp as BannerPrev, FaChevronDown as BannerNext } from \"react-icons/fa\";\n\n// Framer\nimport { FRAMER_MARKETING_BANNER_ANIMATION } from \"../../constants/framer\";\n\n// Components\nimport AnimateHeight from \"react-animate-height\";\n\n// Interfaces\nimport { BannersListResponse } from \"../../api/Marketing/interfaces\";\n\nfunction MarketingBanner({\n banners,\n showBannerOnPageLoad,\n}: {\n banners: BannersListResponse[];\n showBannerOnPageLoad: boolean;\n}) {\n const [showBanner, setShowBanner] = useState(false);\n const { isTourRunning } = useTour();\n const errorReporting = useErrorReporting();\n\n useEffect(() => {\n setShowBanner(showBannerOnPageLoad);\n }, [showBannerOnPageLoad]);\n\n // Reset the currently shown banner to the first one in the list\n // if the list of banners has been updated in the meantime\n useEffect(() => {\n setCurrentShownBannerIndex(0);\n }, [banners]);\n\n /*==============================\n CYCLE TROUGH AVAILABLE BANNERS\n ===============================*/\n const [pauseInterval, setPauseInterval] = useState(false);\n const [currentShownBannerIndex, setCurrentShownBannerIndex] = useState(0);\n\n useInterval(\n () => {\n let prevIndex = currentShownBannerIndex;\n\n // Only start cycling trough the banners if there's more than 1 item available\n if (banners.length > 1) prevIndex++;\n\n // Once the current cycle iterration exceeds the number of received banners,\n // reset back to the first one in the received list\n if (prevIndex > banners.length - 1) prevIndex = 0;\n\n setCurrentShownBannerIndex(prevIndex);\n },\n pauseInterval ? null : 10_000,\n );\n\n /*==============================\n CLOSE TARGETED BANNER\n ===============================*/\n const closeBanners = useMarketingBannersClose();\n const handleCloseBanners = async () => {\n try {\n setShowBanner(false);\n await closeBanners.mutateAsync();\n } catch (error) {\n errorReporting(\"Failed closing all banners!\", error);\n }\n };\n\n /*==============================\n BANNER MANUAL MOVEMENT\n ===============================*/\n const [disableBannerCycleButtons, setDisableBannerCycleButtons] = useState(false);\n\n const handleGoToPreviousBanner = () => {\n // Prevent any banner cycle actions (both manual and auto-cycling)\n if (disableBannerCycleButtons) return;\n setPauseInterval(true);\n setDisableBannerCycleButtons(true);\n\n // Go to the previous banner in the list\n if (currentShownBannerIndex === 0) {\n setCurrentShownBannerIndex(banners.length - 1);\n } else {\n setCurrentShownBannerIndex(currentShownBannerIndex - 1);\n }\n };\n\n const handleGoToNextBanner = () => {\n // Prevent any banner cycle actions (both manual and auto-cycling)\n if (disableBannerCycleButtons) return;\n setPauseInterval(true);\n setDisableBannerCycleButtons(true);\n\n // Go to the next banner in the list\n if (currentShownBannerIndex === banners.length - 1) {\n setCurrentShownBannerIndex(0);\n } else {\n setCurrentShownBannerIndex(currentShownBannerIndex + 1);\n }\n };\n\n // Re-enable the manual movement buttons and the\n // auto-cycling of the banners after 1s to avoid users spamming the actions\n useEffect(() => {\n if (!disableBannerCycleButtons) return;\n\n const disableBtnsTimeout = setTimeout(() => {\n setDisableBannerCycleButtons(false);\n setPauseInterval(false);\n }, 1000);\n\n return () => clearTimeout(disableBtnsTimeout);\n }, [disableBannerCycleButtons]);\n\n // If there are no banners to be shown, do not render anything\n if (isTourRunning || !banners.length || !banners[currentShownBannerIndex]) {\n return null;\n }\n\n return (\n \n
    \n
    \n \n \n {banners[currentShownBannerIndex].hyperlink ? (\n \n {parse(banners[currentShownBannerIndex].text)}\n \n ) : (\n <>{parse(banners[currentShownBannerIndex].text)}\n )}\n \n \n\n {banners.length > 1 ? (\n
    \n \n {currentShownBannerIndex + 1} / {banners.length}\n \n
    \n \n \n
    \n
    \n ) : null}\n\n \n \n
    \n
    \n \n \n );\n}\n\nexport default MarketingBanner;\n","// Components\nimport Header from \"../components/Header/Header\";\nimport Footer from \"../components/Footer/Footer\";\nimport MarketingBanner from \"../components/Banner/MarketingBanner\";\n\n// Context Provider\nimport { ChatContextWrapper } from \"../components/Chat/ChatWrapper/ChatContextWrapper\";\n\n// Hooks\nimport { useAuth } from \"../providers/auth-context\";\nimport { useEffect, useState } from \"react\";\n\ninterface LayoutAuthenticatedProps {\n children: React.ReactNode;\n isFullHeader?: boolean;\n}\n\nconst LayoutAuthenticated = ({ isFullHeader = true, children }: LayoutAuthenticatedProps) => {\n const { user } = useAuth();\n const [showBannerOnPageLoad, setShowBannerOnPageLoad] = useState(false);\n\n useEffect(() => {\n if (!user || !user.marketing_banners.length) {\n setShowBannerOnPageLoad(false);\n } else {\n setShowBannerOnPageLoad(true);\n }\n }, [user]);\n\n return (\n \n {showBannerOnPageLoad ? (\n \n ) : null}\n
    \n
    {children}
    \n