@argusoft/medplat-app-shell 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +139 -141
- package/src/GlobalErrorBoundary.jsx +31 -0
- package/src/SilentErrorFallback.jsx +68 -0
- package/src/TrackingProviderWrapper.jsx +40 -0
- package/src/_tests_/__mocks__/MockTranslationProvider.jsx +21 -0
- package/src/_tests_/__mocks__/ckeditor.js +45 -0
- package/src/_tests_/__mocks__/fileMock.js +1 -0
- package/src/_tests_/__mocks__/useranalytics.js +5 -0
- package/src/_tests_/views/components/Dashboard/DashboardUI.test.jsx +137 -0
- package/src/_tests_/views/components/Dashboard/DashboardUIMock.js +877 -0
- package/src/_tests_/views/components/ForgotPassword/ForgotPassword.test.jsx +314 -0
- package/src/_tests_/views/components/LocationDirective/LocationDirective.test.jsx.disable +229 -0
- package/src/_tests_/views/components/LocationDirective/mockLocationDirective.js +810 -0
- package/src/_tests_/views/components/LocationType/MockLocationType.js +42259 -0
- package/src/_tests_/views/components/LocationType/addlocationtype.test.jsx.disable +276 -0
- package/src/_tests_/views/components/LocationType/editlocationtype.test.jsx.disable +262 -0
- package/src/_tests_/views/components/LocationType/locationtype.test.jsx.disable +148 -0
- package/src/_tests_/views/components/Profile/UpdateProfileModalData.js +4396 -0
- package/src/_tests_/views/components/Profile/updateprofilemodal.test.jsx +282 -0
- package/src/_tests_/views/components/SideBar/MockSideBar.js +1379 -0
- package/src/_tests_/views/components/SideBar/SideBar.test.jsx +98 -0
- package/src/_tests_/views/components/SystemConfig/ManageSystemConfig/AddManageSystemConfig.test.jsx.disable +164 -0
- package/src/_tests_/views/components/SystemConfig/ManageSystemConfig/UpdateManageSystemConfig.test.jsx.disable +157 -0
- package/src/_tests_/views/components/SystemConfig/MockSystemConfig.js +1280 -0
- package/src/_tests_/views/components/SystemConfig/SystemConfig.test.jsx.disable +165 -0
- package/src/_tests_/views/components/login/Login.test.jsx +276 -0
- package/src/_tests_/views/components/login/MockAuthorise.js +2414 -0
- package/src/_tests_/views/components/login/ServiceResponse.js +595 -0
- package/src/_tests_/views/components/user/MockManageUser.js +7965 -0
- package/src/_tests_/views/components/user/manageuser.test.jsx.disable +989 -0
- package/src/_tests_/views/components/user/mockUsersData.js +582 -0
- package/src/assets/img/OASISLogin.png +0 -0
- package/src/assets/img/bahaarNew.png +0 -0
- package/src/assets/img/dnhdd4K.png +0 -0
- package/src/assets/img/govtofup.png +0 -0
- package/src/assets/img/sewarural4K.png +0 -0
- package/src/assets/img/techo4K.png +0 -0
- package/src/assets/img/up4K.png +0 -0
- package/src/common/HolidayList.jsx +573 -0
- package/src/common/MalaciaousInputUtil.js +23 -0
- package/src/common/SafeHtml.jsx +17 -0
- package/src/common/VersionManager.jsx +109 -0
- package/src/common/constants/PerformanceDashboard.js +514 -0
- package/src/common/constants/app.constant.js +781 -0
- package/src/common/constants/cccVerificationConstants.js +18 -0
- package/src/common/constants/fhsrConstant.js +33 -0
- package/src/common/constants/gvk-verification.constant.js +76 -0
- package/src/common/constants/search.constant.js +23 -0
- package/src/common/constants/teleconsulatationConstant.jsx +1339 -0
- package/src/common/directives/SearchTemplate.jsx +784 -0
- package/src/common/directives/SearchTemplate.scss +14 -0
- package/src/common/dynamicView/DynamicView.jsx +353 -0
- package/src/common/dynamicView/InputFieldComponent.jsx +1501 -0
- package/src/common/dynamicView/InputViewComponent.jsx +298 -0
- package/src/common/dynamicView/InputViewComponent.scss +15 -0
- package/src/common/env.js +5 -0
- package/src/common/filters/locationNameFilter.js +26 -0
- package/src/common/fontAwesomeIcons/FontAwesomeIcons.jsx +27 -0
- package/src/common/fontAwesomeIcons/FontAwesomeIconsNames.js +1968 -0
- package/src/common/fontPreferences/fontSizeProvider.jsx +34 -0
- package/src/common/fontPreferences/fontSizeSelector.jsx +116 -0
- package/src/common/getAssignedFeature/getAssignedFeature.js +32 -0
- package/src/common/interceptors/AxiosInterceptor.js +216 -0
- package/src/common/languageTranslator/TranslationContext.js +5 -0
- package/src/common/languageTranslator/TranslationProvider.jsx +24 -0
- package/src/common/languageTranslator/i18n.js +49 -0
- package/src/common/services/AuthenticateService.js +116 -0
- package/src/common/services/DownloadFile.js +35 -0
- package/src/common/services/ForgotPassword.js +18 -0
- package/src/common/services/FormConfiguratorService.js +195 -0
- package/src/common/services/GlobalApis.js +84 -0
- package/src/common/services/InterceptorNavigationService.js +17 -0
- package/src/common/services/LocationService.js +65 -0
- package/src/common/services/LocationType.js +11 -0
- package/src/common/services/QueryBuilder.js +36 -0
- package/src/common/services/Roles.js +28 -0
- package/src/common/services/SyncWithServer.js +15 -0
- package/src/common/services/SystemConfig.js +15 -0
- package/src/common/services/TranslationService.js +70 -0
- package/src/common/services/TwoFactorService.js +7 -0
- package/src/common/services/Users.js +91 -0
- package/src/common/services/Webtasks.js +27 -0
- package/src/common/services/util/Convert-pad-data-to-API-format.jsx +167 -0
- package/src/common/services/util/Convert-to-UI-format.jsx +82 -0
- package/src/common/services/util/EmptyPrescriptionPadData.jsx +11 -0
- package/src/common/services/util/GeneralUtil.js +456 -0
- package/src/common/services/util/Prescription-pad-util.js +339 -0
- package/src/common/services/util/PrescriptionPadData.js +67 -0
- package/src/common/services/util/PrescriptionpadCommonUtil.js +96 -0
- package/src/common/services/util/ReportFieldUtil.jsx +398 -0
- package/src/common/services/util/WebSocketContext.jsx +261 -0
- package/src/common/syncWithServer/SyncWithServerDialog.jsx +170 -0
- package/src/common/syncWithServer/SyncWithServerDialogSkeleton.jsx +67 -0
- package/src/common/tests/CustomWrapper.jsx +49 -0
- package/src/common/tests/TranslationWrapper.jsx +38 -0
- package/src/common/themeProvider/ColorInputs.jsx +97 -0
- package/src/common/themeProvider/EditableColorInput.jsx +128 -0
- package/src/common/themeProvider/ThemeEditor.jsx +319 -0
- package/src/common/themeProvider/ThemeProvider.jsx +210 -0
- package/src/common/themeProvider/themeConfig.js +558 -0
- package/src/common/toaster/toaster.jsx +30 -0
- package/src/firebaseConfig.js +24 -0
- package/src/global.scss +221 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/useAESEncryption.js +56 -0
- package/src/hooks/useCaching.js +43 -0
- package/src/hooks/useDebounce.js +34 -0
- package/src/hooks/useDebounceFn.js +50 -0
- package/src/hooks/useDownloadPdf.js +358 -0
- package/src/hooks/useDownloadXlsx.js +55 -0
- package/src/hooks/useListValueFieldValues.js +30 -0
- package/src/hooks/useLocationHierarchies.js +63 -0
- package/src/hooks/useLocationHierarchyTranslate.js +16 -0
- package/src/hooks/useOnline.js +27 -0
- package/src/hooks/usePagination.js +63 -0
- package/src/hooks/useRefreshToken.js +87 -0
- package/src/hooks/useScript.js +25 -0
- package/src/hooks/useStopwatch.js +75 -0
- package/src/hooks/useTrackEvent.js +22 -0
- package/src/hooks/useWebAudioRecorder.js +115 -0
- package/src/layout/LoaderComponet.jsx +22 -0
- package/src/layout/LoaderContext.jsx +29 -0
- package/src/layout/mainLayout/AdaptiveZoom.jsx +27 -0
- package/src/layout/mainLayout/Chatbot.jsx +243 -0
- package/src/layout/mainLayout/Layout.jsx +445 -0
- package/src/layout/mainLayout/Profile/UpdateProfileModal.jsx +684 -0
- package/src/layout/mainLayout/header/LogoutModal.jsx +131 -0
- package/src/layout/mainLayout/header/Navbar.jsx +1677 -0
- package/src/layout/mainLayout/header/Navbar.scss +4 -0
- package/src/layout/mainLayout/header/index.js +0 -0
- package/src/layout/mainLayout/sidebar/SideBar.jsx +1402 -0
- package/src/layout/mainLayout/sidebar/Sidebar.css +159 -0
- package/src/layout/mainLayout/sidebar/index.js +0 -0
- package/src/logo.svg +1 -0
- package/src/reportWebVitals.js +13 -0
- package/src/setupFirebaseMessaging.js +28 -0
- package/src/setupTests.js +8 -0
- package/src/store/actions/AuthenticationActions.js +0 -0
- package/src/store/actions/ReportsActions.js +0 -0
- package/src/store/actions/TranslationAction.js +0 -0
- package/src/store/index.js +8 -0
- package/src/store/reducer.js +46 -0
- package/src/store/reducers/AuthenticationReducer.js +50 -0
- package/src/store/reducers/CalendarEventReducer.js +41 -0
- package/src/store/reducers/ConditionClipboardReducer.js +45 -0
- package/src/store/reducers/FeatureReducer.js +27 -0
- package/src/store/reducers/FormConfiguratorReducer.js +38 -0
- package/src/store/reducers/LoadingReducer.js +20 -0
- package/src/store/reducers/MembersAuthenticationReducer.js +28 -0
- package/src/store/reducers/PrescriptionPadReducer.js +329 -0
- package/src/store/reducers/QuestionaireReducer.js +29 -0
- package/src/store/reducers/ReportsReducer.js +24 -0
- package/src/store/reducers/SkeletonReducer.js +20 -0
- package/src/store/reducers/ThemeReducer.js +106 -0
- package/src/store/reducers/TranslationReducer.js +126 -0
- package/src/store/reducers/dashboardEditorSlice.js +77 -0
- package/src/store/reducers/districtHealthDashboardSlice.js +58 -0
- package/src/store/reducers/immunizationSlice.js +234 -0
- package/src/store/slices/dashboardPagesSlice.js +51 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/FormConstants.js +2629 -0
- package/src/utils/GujaratTopoChart.jsx +483 -0
- package/src/utils/UUIDgenerator.js +8 -0
- package/src/utils/appointment-utils/appointment-utils.js +123 -0
- package/src/utils/feature.js +42 -0
- package/src/utils/getThemeColor.js +12 -0
- package/src/utils/localStorageHelper.js +11 -0
- package/src/utils/notifications/enable-push-notifications.js +27 -0
- package/src/utils/resolveAppliedStyle.js +11 -0
- package/src/utils/themeConfigs.js +1483 -0
- package/src/views/custom-components/.gitkeep +0 -0
- package/src/views/custom-components/AgIconButton/RIf.jsx +14 -0
- package/src/views/custom-components/AgIconButton/button.jsx +108 -0
- package/src/views/custom-components/AgIconButton/waterDrop.jsx +95 -0
- package/src/views/custom-components/AgIconButton/waterDrop.scss +37 -0
- package/src/views/custom-components/AlertPlaceholder.jsx +32 -0
- package/src/views/custom-components/AllFaIconsSelector.jsx +56 -0
- package/src/views/custom-components/CkEditor/CkEditor.js +102 -0
- package/src/views/custom-components/CustomAccordion.jsx +72 -0
- package/src/views/custom-components/CustomActionIcons.jsx +118 -0
- package/src/views/custom-components/CustomAutoComplete.jsx +188 -0
- package/src/views/custom-components/CustomCheckBox.jsx +60 -0
- package/src/views/custom-components/CustomConfirmationModal.jsx +118 -0
- package/src/views/custom-components/CustomCountrySelect.jsx +129 -0
- package/src/views/custom-components/CustomDatePicker.jsx +122 -0
- package/src/views/custom-components/CustomDropdown.jsx +191 -0
- package/src/views/custom-components/CustomFileUpload.jsx +387 -0
- package/src/views/custom-components/CustomFullCalendar.jsx +514 -0
- package/src/views/custom-components/CustomInfiniteScroll.jsx +126 -0
- package/src/views/custom-components/CustomRadioComponent.jsx +65 -0
- package/src/views/custom-components/CustomStatsComponent.jsx +114 -0
- package/src/views/custom-components/CustomSvgUpload.jsx +170 -0
- package/src/views/custom-components/CustomSwitch.jsx +37 -0
- package/src/views/custom-components/CustomTabPanel.jsx +19 -0
- package/src/views/custom-components/CustomTextArea.jsx +62 -0
- package/src/views/custom-components/CustomTextArea.scss +27 -0
- package/src/views/custom-components/CustomTextField.jsx +116 -0
- package/src/views/custom-components/CustomToggleSwitch.jsx +138 -0
- package/src/views/custom-components/CustomTooltip.jsx +51 -0
- package/src/views/custom-components/CustomZoomImage.jsx +134 -0
- package/src/views/custom-components/CustomizedTable/CustomizedTableV2.jsx +1407 -0
- package/src/views/custom-components/CustomizedTable/VirtualizeTableBody.jsx +295 -0
- package/src/views/custom-components/CustomizedTable/helper.jsx +159 -0
- package/src/views/custom-components/CustomizedTable.jsx +532 -0
- package/src/views/custom-components/EditInputField.jsx +174 -0
- package/src/views/custom-components/FieldDescription.jsx +22 -0
- package/src/views/custom-components/FileDisplayComponent.jsx +138 -0
- package/src/views/custom-components/FormItem.jsx +53 -0
- package/src/views/custom-components/GenericChart.jsx +80 -0
- package/src/views/custom-components/InfoBadge.jsx +60 -0
- package/src/views/custom-components/PostgresEditor.jsx +801 -0
- package/src/views/custom-components/ResizableEditAutocompleteField.jsx +249 -0
- package/src/views/custom-components/ResizableEditInputField.jsx +215 -0
- package/src/views/custom-components/ResizeableEditSelectField.jsx +197 -0
- package/src/views/custom-components/SideOverlay.jsx +113 -0
- package/src/views/custom-components/SideOverlay.scss +42 -0
- package/src/views/custom-components/calendar.scss +571 -0
- package/src/views/feature-components/.gitkeep +0 -0
- package/src/views/feature-components/Dashboard/DashboardUI.jsx +1043 -0
- package/src/views/feature-components/Dashboard/DhnddModal/AshaDataQualityVerificationModal.jsx +278 -0
- package/src/views/feature-components/Dashboard/PinFeatureModal.jsx +143 -0
- package/src/views/feature-components/Dashboard/QuickLinks.jsx +163 -0
- package/src/views/feature-components/Dashboard/Taskbar.jsx +56 -0
- package/src/views/feature-components/Dashboard/WebtasksFilterForm.jsx +109 -0
- package/src/views/feature-components/Dashboard/WidgetCard.jsx +161 -0
- package/src/views/feature-components/Dashboard/actionModal.jsx +263 -0
- package/src/views/feature-components/Dashboard/ekavachModal/HealthWorkerIncorrectDetailsModal.jsx +332 -0
- package/src/views/feature-components/Dashboard/ekavachModal/MoMaternalDeathVerifcationModal.jsx +275 -0
- package/src/views/feature-components/Dashboard/ekavachModal/MoVerficationForChildScreeningMoadal.jsx +566 -0
- package/src/views/feature-components/FeatureUsageAnalytics/FeatureUsageAnalytics.jsx +989 -0
- package/src/views/feature-components/Features/NewServerManagement.jsx +217 -0
- package/src/views/feature-components/Features/ServerManagement.scss +120 -0
- package/src/views/feature-components/ForgotPassword/ForgotPassword.jsx +226 -0
- package/src/views/feature-components/LocationDirective/LocationDirective.jsx +992 -0
- package/src/views/feature-components/LocationDirective/LocationDirectiveV2.jsx +909 -0
- package/src/views/feature-components/NotFound.jsx +66 -0
- package/src/views/feature-components/Onboarding/Onboarding.jsx +1400 -0
- package/src/views/feature-components/Skeletons.js +115 -0
- package/src/views/feature-components/Unauthorized.jsx +48 -0
- package/src/views/feature-components/VerifyRoute.jsx +88 -0
- package/src/views/feature-components/YearlyRecap/YearlyRecap.jsx +357 -0
- package/src/views/feature-components/YearlyRecap/components/RecapSlide.jsx +183 -0
- package/src/views/feature-components/YearlyRecap/languageTranslator/TranslationContext.js +5 -0
- package/src/views/feature-components/YearlyRecap/languageTranslator/TranslationProvider.jsx +26 -0
- package/src/views/feature-components/YearlyRecap/languageTranslator/i18n.js +46 -0
- package/src/views/feature-components/YearlyRecap/languageTranslator/translations.json +167 -0
- package/src/views/feature-components/YearlyRecap/slides/IntroSlide.jsx +233 -0
- package/src/views/feature-components/YearlyRecap/slides/MaternalHealthSlide.jsx +146 -0
- package/src/views/feature-components/YearlyRecap/slides/MetricSlide.jsx +227 -0
- package/src/views/feature-components/YearlyRecap/slides/OutroSlide.jsx +701 -0
- package/src/views/feature-components/YearlyRecap/slides/ReachSlide.jsx +273 -0
- package/src/views/feature-components/login/Login.jsx +840 -0
- package/src/views/feature-components/login/Login.scss +154 -0
- package/src/views/feature-components/login/LoginConfigurator.jsx +1149 -0
- package/src/views/feature-components/login/TwoFactorSetupModal.jsx +411 -0
- package/src/views/feature-components/login/simplifyMenu.js +45 -0
- package/src/views/feature-components/system-config/ManageSystemConfigs.jsx +284 -0
- package/src/views/feature-components/system-config/SystemConfig.jsx +299 -0
- package/src/views/feature-components/users/ChangePasswordModal.jsx +243 -0
- package/src/views/feature-components/users/PasswordField.jsx +56 -0
- package/dist/index.css +0 -1
- package/dist/index.js +0 -32001
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getApkInfo,
|
|
3
|
+
getFeaturesV2,
|
|
4
|
+
getLoggedInUser,
|
|
5
|
+
getSystemNotice,
|
|
6
|
+
loginService,
|
|
7
|
+
} from '@/common/services/AuthenticateService';
|
|
8
|
+
import medplatLogo from '@/assets/img/medplat-highres.png';
|
|
9
|
+
import GeneralUtil from '@/common/services/util/GeneralUtil';
|
|
10
|
+
import { api } from '@/common/interceptors/AxiosInterceptor';
|
|
11
|
+
import { showToast } from '@/common/toaster/toaster';
|
|
12
|
+
import { setLoggedIn, setMenuItem, setToken, setUserDetails } from '@/store/reducers/AuthenticationReducer';
|
|
13
|
+
import CustomTextField from '@/views/custom-components/CustomTextField';
|
|
14
|
+
import '@/views/feature-components/login/Login.scss';
|
|
15
|
+
import { bahaarJSON, upJSON, phmpJSON, sewaruralJSON, oasisJSON, techoJSON, defaultJSON } from '@/common/constants/app.constant';
|
|
16
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
17
|
+
import { Box, Button, Card, CircularProgress, Divider, Link, Typography } from '@mui/material';
|
|
18
|
+
import Grid from '@mui/material/Grid2';
|
|
19
|
+
import { default as React, startTransition, useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { useForm } from 'react-hook-form';
|
|
21
|
+
import { useDispatch } from 'react-redux';
|
|
22
|
+
import { useNavigate } from 'react-router-dom';
|
|
23
|
+
import * as Yup from 'yup';
|
|
24
|
+
import { simplifyMenu } from '@/views/feature-components/login/simplifyMenu';
|
|
25
|
+
import TwoFactorSetupModal from '@/views/feature-components/login/TwoFactorSetupModal';
|
|
26
|
+
import { PasswordField } from '@/views/feature-components/users/PasswordField';
|
|
27
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
28
|
+
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
|
|
29
|
+
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';
|
|
30
|
+
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
|
|
31
|
+
import LockClockOutlinedIcon from '@mui/icons-material/LockClockOutlined';
|
|
32
|
+
import bankverification from '@/assets/img/nhm-logo.png';
|
|
33
|
+
import GovOfGujarat from '@/assets/img/govtofgujarat.png';
|
|
34
|
+
import SewaRural from '@/assets/img/sewa-rural.png';
|
|
35
|
+
import lshtmLogo from '@/assets/img/lshtm-logo.jpg';
|
|
36
|
+
import phfiLogo from '@/assets/img/phfi-logo.jpg';
|
|
37
|
+
import dsprudLogo from '@/assets/img/dsprud-logo.png';
|
|
38
|
+
import wbUniLogo from '@/assets/img/wbu-logo.png';
|
|
39
|
+
import oxUniLogo from '@/assets/img/ou-logo.png';
|
|
40
|
+
import ficciLogo from '@/assets/img/ficci.png';
|
|
41
|
+
import wbGovLogo from '@/assets/img/wb-gov-logo.png';
|
|
42
|
+
import ttcLogo from '@/assets/img/ttc.png';
|
|
43
|
+
import { faBookMedical, faDownload, faPassport, faPerson } from '@fortawesome/free-solid-svg-icons';
|
|
44
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
45
|
+
import bahaarLogo from '@/assets/img/bahaarNew.png';
|
|
46
|
+
import techoLogo from '@/assets/img/techo4K.png';
|
|
47
|
+
import sewaLogo from '@/assets/img/sewarural4K.png';
|
|
48
|
+
import ekavachLogo from '@/assets/img/up4K.png';
|
|
49
|
+
import dnhddLogo from '@/assets/img/dnhdd4K.png';
|
|
50
|
+
import oasisLogo from '@/assets/img/OASISLogin.png';
|
|
51
|
+
import sathLogo from '@/assets/img/sath.png';
|
|
52
|
+
import techoBrandLogo from '@/assets/img/techo_plus.png';
|
|
53
|
+
import sewaruralBrandLogo from '@/assets/img/sewarural.png';
|
|
54
|
+
import dnhddBrandLogo from '@/assets/img/dnhdd.png';
|
|
55
|
+
import ekavachBrandLogo from '@/assets/img/ekavach.png';
|
|
56
|
+
import oasisBrandLogo from '@/assets/img/oasis-logo.png';
|
|
57
|
+
import GovOfUP from '@/assets/img/govtofup.png';
|
|
58
|
+
|
|
59
|
+
export const imageMap = {
|
|
60
|
+
'nhm-logo.png': bankverification,
|
|
61
|
+
'govtofgujarat.png': GovOfGujarat,
|
|
62
|
+
'govtofup.png': GovOfUP,
|
|
63
|
+
'sewa-rural.png': SewaRural,
|
|
64
|
+
'lshtm-logo.jpg': lshtmLogo,
|
|
65
|
+
'phfi-logo.jpg': phfiLogo,
|
|
66
|
+
'dsprud-logo.png': dsprudLogo,
|
|
67
|
+
'wbu-logo.png': wbUniLogo,
|
|
68
|
+
'ou-logo.png': oxUniLogo,
|
|
69
|
+
'ficci.png': ficciLogo,
|
|
70
|
+
'wb-gov-logo.png': wbGovLogo,
|
|
71
|
+
'ttc.png': ttcLogo,
|
|
72
|
+
'medplat-highres.png': medplatLogo,
|
|
73
|
+
'bahaarNew.png': bahaarLogo,
|
|
74
|
+
'techo4K.png': techoLogo,
|
|
75
|
+
'sewarural4K.png': sewaLogo,
|
|
76
|
+
'up4K.png': ekavachLogo,
|
|
77
|
+
'dnhdd4K.png': dnhddLogo,
|
|
78
|
+
'OASISLogin.png': oasisLogo,
|
|
79
|
+
'sath.png': sathLogo,
|
|
80
|
+
'techo_plus.png': techoBrandLogo,
|
|
81
|
+
'sewarural.png': sewaruralBrandLogo,
|
|
82
|
+
'dnhdd.png': dnhddBrandLogo,
|
|
83
|
+
'ekavach.png': ekavachBrandLogo,
|
|
84
|
+
'oasis-logo.png': oasisBrandLogo,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const configMap = {
|
|
89
|
+
bahaar: bahaarJSON,
|
|
90
|
+
gujarat: techoJSON,
|
|
91
|
+
gujaratStaging: techoJSON,
|
|
92
|
+
sewarural: sewaruralJSON,
|
|
93
|
+
dnhdd: phmpJSON,
|
|
94
|
+
ekavach: upJSON,
|
|
95
|
+
oasis: oasisJSON,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getActiveJSON = () => {
|
|
99
|
+
const env = import.meta.env.VITE_APP_IMPLEMENTATION || 'gujarat';
|
|
100
|
+
const localKey = env === 'gujaratStaging' ? 'gujarat' : env;
|
|
101
|
+
const savedCustom = localStorage.getItem('medplat_login_config_custom');
|
|
102
|
+
const savedEnv = localStorage.getItem(`medplat_login_config_${localKey}`);
|
|
103
|
+
const saved = savedCustom || savedEnv;
|
|
104
|
+
if (saved) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(saved);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error('Failed to parse saved config from localStorage', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return configMap[env] || defaultJSON;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
const iconMap = {
|
|
116
|
+
PERSON: faPerson,
|
|
117
|
+
MEDICAL: faBookMedical,
|
|
118
|
+
PASSPORT: faPassport,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @module Login
|
|
123
|
+
* @returns {JSX.Element}
|
|
124
|
+
*/
|
|
125
|
+
const Login = () => {
|
|
126
|
+
const navigate = useNavigate();
|
|
127
|
+
const dispatch = useDispatch();
|
|
128
|
+
const [submitting, setSubmitting] = useState(false);
|
|
129
|
+
|
|
130
|
+
// Use state to handle login object
|
|
131
|
+
const [login, setLogin] = useState({});
|
|
132
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
133
|
+
// True once the backend has signalled that this user must supply a 2FA code.
|
|
134
|
+
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
|
135
|
+
// Toggles the "lost access to your code" helper message on the 2FA challenge step.
|
|
136
|
+
const [noCodeHelp, setNoCodeHelp] = useState(false);
|
|
137
|
+
// Holds details for the (skippable) first-time Google Authenticator setup shown after login.
|
|
138
|
+
const [twoFactorSetup, setTwoFactorSetup] = useState(null);
|
|
139
|
+
// Holds details for the "login successful" celebration shown after a 2FA code challenge.
|
|
140
|
+
const [loginSuccess, setLoginSuccess] = useState(null);
|
|
141
|
+
// Attempts left before lockout (shown after a wrong password/code); null when not applicable.
|
|
142
|
+
const [attemptsRemaining, setAttemptsRemaining] = useState(null);
|
|
143
|
+
// Remaining lockout time in seconds (drives the countdown); 0 when not locked.
|
|
144
|
+
const [lockoutSeconds, setLockoutSeconds] = useState(0);
|
|
145
|
+
|
|
146
|
+
// Create refs for navigate, dispatch, and manualRefresh
|
|
147
|
+
const navigateRef = useRef(navigate);
|
|
148
|
+
const dispatchRef = useRef(dispatch);
|
|
149
|
+
const [activeJSON, setActiveJSON] = useState(() => getActiveJSON());
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const fetchConfig = async () => {
|
|
153
|
+
try {
|
|
154
|
+
const response = await api.get('login/preference');
|
|
155
|
+
if (response?.data) {
|
|
156
|
+
let data = response.data;
|
|
157
|
+
// Extract the preference string from the backend wrapper structure (e.g. keyValue)
|
|
158
|
+
if (data && typeof data === 'object') {
|
|
159
|
+
if ('keyValue' in data) {
|
|
160
|
+
data = data.keyValue;
|
|
161
|
+
} else if ('value' in data) {
|
|
162
|
+
data = data.value;
|
|
163
|
+
} else if ('preference' in data) {
|
|
164
|
+
data = data.preference;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (typeof data === 'string') {
|
|
168
|
+
try {
|
|
169
|
+
data = JSON.parse(data);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error('Error parsing config string from API in Login.jsx', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (data && typeof data === 'object') {
|
|
175
|
+
setActiveJSON(data);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error('Failed to load login config from server', err);
|
|
180
|
+
const env = import.meta.env.VITE_APP_IMPLEMENTATION || 'gujarat';
|
|
181
|
+
const localKey = env === 'gujaratStaging' ? 'gujarat' : env;
|
|
182
|
+
const savedCustom = localStorage.getItem('medplat_login_config_custom');
|
|
183
|
+
const savedEnv = localStorage.getItem(`medplat_login_config_${localKey}`);
|
|
184
|
+
const saved = savedCustom || savedEnv;
|
|
185
|
+
if (saved) {
|
|
186
|
+
try {
|
|
187
|
+
setActiveJSON(JSON.parse(saved));
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('Failed to parse saved config from localStorage in catch block', e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
fetchConfig();
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handles the local login process for a user. This function performs authentication, updates
|
|
199
|
+
* application state, stores login information in local storage, and redirects the user to the
|
|
200
|
+
* home page upon successful login.
|
|
201
|
+
* @async
|
|
202
|
+
* @function doLoginLocal
|
|
203
|
+
* @param {Object} user - The user object containing login credentials.
|
|
204
|
+
* @param {string} user.username - The username of the user trying to log in.
|
|
205
|
+
* @param {string} user.password - The password of the user trying to log in.
|
|
206
|
+
* @throws {Error} Throws an error if any part of the login process fails.
|
|
207
|
+
* @returns {Promise<void>} A promise that resolves when the login process is complete.
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
const doLoginLocal = async (user) => {
|
|
211
|
+
try {
|
|
212
|
+
setSubmitting(true);
|
|
213
|
+
setAttemptsRemaining(null);
|
|
214
|
+
const loginResponse = await loginService(user.username, user.password, undefined, user.totpCode);
|
|
215
|
+
dispatchRef.current(setToken(loginResponse.data));
|
|
216
|
+
|
|
217
|
+
const loginTime = new Date();
|
|
218
|
+
localStorage.setItem('loginTime', JSON.stringify(loginTime));
|
|
219
|
+
const loggedInUserResponse = await getLoggedInUser();
|
|
220
|
+
if (loggedInUserResponse.data.roleId) {
|
|
221
|
+
const features = await getFeaturesV2();
|
|
222
|
+
loggedInUserResponse.data.features = features?.data;
|
|
223
|
+
let linearMenuItems = {};
|
|
224
|
+
Object.entries(features?.data || {}).forEach((element) => {
|
|
225
|
+
simplifyMenu(element, linearMenuItems);
|
|
226
|
+
});
|
|
227
|
+
dispatch(setMenuItem(linearMenuItems));
|
|
228
|
+
// Call simplifyMenu with the menuItem and linearMenuItems
|
|
229
|
+
} else {
|
|
230
|
+
loggedInUserResponse.roles = [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dispatchRef.current(setUserDetails(loggedInUserResponse.data));
|
|
234
|
+
dispatchRef.current(setLoggedIn());
|
|
235
|
+
|
|
236
|
+
const webTask = loggedInUserResponse?.data?.features?.manage
|
|
237
|
+
? loggedInUserResponse?.data?.features?.manage.find((item) => item.name === 'Web Tasks')
|
|
238
|
+
: false;
|
|
239
|
+
sessionStorage.setItem('isTecho', true);
|
|
240
|
+
setSubmitting(false);
|
|
241
|
+
const redirectTo = webTask ? `/ui/medplat/dashboard/webtasks` : '/ui';
|
|
242
|
+
// Role requires 2FA but enrolment isn't done yet: show the skippable setup step before continuing.
|
|
243
|
+
if (loggedInUserResponse?.data?.twoFactorSetupRequired) {
|
|
244
|
+
setTwoFactorSetup({
|
|
245
|
+
userId: loggedInUserResponse.data.id,
|
|
246
|
+
userName: loggedInUserResponse.data.userName,
|
|
247
|
+
redirectTo,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Celebrate every successful login (simple or after a 2FA code challenge) before continuing.
|
|
252
|
+
setLoginSuccess({ redirectTo, name: loggedInUserResponse?.data?.name });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error(error);
|
|
255
|
+
setSubmitting(false);
|
|
256
|
+
const data = error.response?.data;
|
|
257
|
+
const errorCode = data?.error;
|
|
258
|
+
const errorMessage = data?.error_description;
|
|
259
|
+
const remaining = data?.attempts_remaining != null ? parseInt(data.attempts_remaining, 10) : null;
|
|
260
|
+
const lockedMinutes = data?.locked_for_minutes != null ? parseInt(data.locked_for_minutes, 10) : null;
|
|
261
|
+
|
|
262
|
+
if (errorCode === '2fa_required') {
|
|
263
|
+
// First step succeeded (valid password) but the user needs to supply an authenticator code.
|
|
264
|
+
setTwoFactorRequired(true);
|
|
265
|
+
showToast({ message: 'Enter the 6 digit code from your authenticator app', type: 'info' });
|
|
266
|
+
} else if (lockedMinutes != null) {
|
|
267
|
+
// Too many failed attempts: show the lockout countdown inline.
|
|
268
|
+
setAttemptsRemaining(null);
|
|
269
|
+
setLockoutSeconds(lockedMinutes * 60);
|
|
270
|
+
} else if (remaining != null) {
|
|
271
|
+
// Wrong password or code: show how many attempts are left inline. Stay on the code step if 2FA.
|
|
272
|
+
if (errorCode === 'invalid_2fa_code') setTwoFactorRequired(true);
|
|
273
|
+
setAttemptsRemaining(remaining);
|
|
274
|
+
} else if (errorMessage === 'Bad credentials') {
|
|
275
|
+
showToast({ message: 'Invalid username or password', type: 'error' });
|
|
276
|
+
} else {
|
|
277
|
+
showToast({ message: errorMessage ? errorMessage : 'Error in login', type: 'error' });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const LoginSchema = Yup.object().shape({
|
|
283
|
+
username: Yup.string().required('Username is required'),
|
|
284
|
+
password: Yup.string().required('Password is required'),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const {
|
|
288
|
+
control,
|
|
289
|
+
handleSubmit,
|
|
290
|
+
setValue,
|
|
291
|
+
formState: { errors },
|
|
292
|
+
} = useForm({
|
|
293
|
+
defaultValues: {
|
|
294
|
+
username: '',
|
|
295
|
+
password: '',
|
|
296
|
+
totpCode: '',
|
|
297
|
+
},
|
|
298
|
+
resolver: yupResolver(LoginSchema),
|
|
299
|
+
mode: 'onChange', // Trigger validation on change
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Submits the login form by invoking the `doLoginLocal` function with the provided user credentials.
|
|
304
|
+
* This function is typically used as a callback for form submission to handle user authentication.
|
|
305
|
+
* @function submit
|
|
306
|
+
* @param {Object} values - The form values containing user credentials.
|
|
307
|
+
* @param {string} values.username - The username of the user trying to log in.
|
|
308
|
+
* @param {string} values.password - The password of the user trying to log in.
|
|
309
|
+
*
|
|
310
|
+
* @returns {void}
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
const submit = async (values) => {
|
|
314
|
+
try {
|
|
315
|
+
await doLoginLocal({
|
|
316
|
+
username: values.username,
|
|
317
|
+
password: values.password,
|
|
318
|
+
totpCode: twoFactorRequired ? values.totpCode : undefined,
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error(error);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* React effect hook that performs initialization tasks when the component mounts.
|
|
327
|
+
* This hook checks for an existing token in local storage, retrieves encryption keys,
|
|
328
|
+
* and updates the login state with system notices and APK information.
|
|
329
|
+
* @function useEffect
|
|
330
|
+
* @returns {void}
|
|
331
|
+
*/
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
const tokenString = localStorage.getItem('token');
|
|
335
|
+
if (tokenString) {
|
|
336
|
+
const isTecho = sessionStorage.getItem('isTecho');
|
|
337
|
+
if (!isTecho) {
|
|
338
|
+
navigate('/ui/unauthorized');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
navigateRef.current('/ui/medplat/dashboard/webtasks');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Only clear if we are genuinely on the login page without a token
|
|
346
|
+
localStorage.removeItem('theme_config');
|
|
347
|
+
localStorage.removeItem('theme_existence');
|
|
348
|
+
// getKeyAndIV();
|
|
349
|
+
|
|
350
|
+
const initializeLogin = async () => {
|
|
351
|
+
const updatedLogin = { ...login };
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const systemNoticeResponse = await getSystemNotice();
|
|
355
|
+
if (systemNoticeResponse.data?.keyValue) {
|
|
356
|
+
updatedLogin.showSystemNotice = true;
|
|
357
|
+
updatedLogin.systemNotice = systemNoticeResponse.data.keyValue;
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
GeneralUtil.showMessageOnApiCallFailure(error);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const apkInfoResponse = await getApkInfo();
|
|
365
|
+
if (apkInfoResponse.data.length === 0) {
|
|
366
|
+
updatedLogin.hideAppLink = true;
|
|
367
|
+
} else {
|
|
368
|
+
updatedLogin.link = apkInfoResponse.data[0];
|
|
369
|
+
updatedLogin.version = apkInfoResponse.data[1];
|
|
370
|
+
updatedLogin.release_date = apkInfoResponse.data[2];
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
GeneralUtil.showMessageOnApiCallFailure(error);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
setLogin(updatedLogin); // Update login state
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
initializeLogin();
|
|
380
|
+
}, []);
|
|
381
|
+
|
|
382
|
+
// After the "login successful" celebration, continue into the app.
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (!loginSuccess) return undefined;
|
|
385
|
+
const timer = setTimeout(() => navigateRef.current(loginSuccess.redirectTo), 1900);
|
|
386
|
+
return () => clearTimeout(timer);
|
|
387
|
+
}, [loginSuccess]);
|
|
388
|
+
|
|
389
|
+
// Silently re-enable the form once the lockout window has passed (no visible countdown).
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (lockoutSeconds <= 0) return undefined;
|
|
392
|
+
const id = setTimeout(() => setLockoutSeconds(0), lockoutSeconds * 1000);
|
|
393
|
+
return () => clearTimeout(id);
|
|
394
|
+
}, [lockoutSeconds > 0]);
|
|
395
|
+
|
|
396
|
+
const jsonRenderer = () => {
|
|
397
|
+
return <>
|
|
398
|
+
<Grid size={{ xs: 12 }} className="flex flex-col items-center">
|
|
399
|
+
<img src={imageMap[activeJSON.brandLogo]} className="h-[70px] mb-4" alt="logo" />
|
|
400
|
+
<Box className="flex items-center w-[80%] my-2">
|
|
401
|
+
<Divider className="flex-1" />
|
|
402
|
+
<Typography className="px-3 text-xl font-semibold text-gray-700">
|
|
403
|
+
{twoFactorRequired ? 'VERIFY' : 'LOGIN'}
|
|
404
|
+
</Typography>
|
|
405
|
+
<Divider className="flex-1" />
|
|
406
|
+
</Box>
|
|
407
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
408
|
+
{!twoFactorRequired ? (
|
|
409
|
+
<motion.div
|
|
410
|
+
key="credentials"
|
|
411
|
+
className="w-full flex flex-col items-center"
|
|
412
|
+
initial={{ opacity: 0, x: -24 }}
|
|
413
|
+
animate={{ opacity: 1, x: 0 }}
|
|
414
|
+
exit={{ opacity: 0, x: -24 }}
|
|
415
|
+
transition={{ duration: 0.25 }}
|
|
416
|
+
>
|
|
417
|
+
<Box className="w-[85%] mt-3">
|
|
418
|
+
<CustomTextField
|
|
419
|
+
className={activeJSON.inputFieldClass}
|
|
420
|
+
control={control}
|
|
421
|
+
name="username"
|
|
422
|
+
placeholder="Username"
|
|
423
|
+
errors={errors}
|
|
424
|
+
size="medium"
|
|
425
|
+
fullWidth
|
|
426
|
+
autoComplete="off"
|
|
427
|
+
autoFocus
|
|
428
|
+
/>
|
|
429
|
+
</Box>
|
|
430
|
+
<Box className="w-[85%] mt-3">
|
|
431
|
+
<PasswordField
|
|
432
|
+
className={activeJSON.inputFieldClass}
|
|
433
|
+
control={control}
|
|
434
|
+
showPassword={showPassword}
|
|
435
|
+
togglePasswordVisibility={() => setShowPassword((cur) => !cur)}
|
|
436
|
+
name="password"
|
|
437
|
+
placeholder="Password"
|
|
438
|
+
errors={errors}
|
|
439
|
+
autoComplete="new-password"
|
|
440
|
+
/>
|
|
441
|
+
</Box>
|
|
442
|
+
</motion.div>
|
|
443
|
+
) : (
|
|
444
|
+
<motion.div
|
|
445
|
+
key="twofactor"
|
|
446
|
+
className="w-[85%] mt-2 flex flex-col items-center"
|
|
447
|
+
initial={{ opacity: 0, x: 24 }}
|
|
448
|
+
animate={{ opacity: 1, x: 0 }}
|
|
449
|
+
exit={{ opacity: 0, x: 24 }}
|
|
450
|
+
transition={{ duration: 0.25 }}
|
|
451
|
+
>
|
|
452
|
+
<motion.div
|
|
453
|
+
initial={{ scale: 0.5, opacity: 0 }}
|
|
454
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
455
|
+
transition={{ type: 'spring', stiffness: 260, damping: 18 }}
|
|
456
|
+
className="flex items-center justify-center w-14 h-14 rounded-full bg-blue-50 text-[#007ACC] mb-2"
|
|
457
|
+
>
|
|
458
|
+
<ShieldOutlinedIcon fontSize="medium" />
|
|
459
|
+
</motion.div >
|
|
460
|
+
<Typography className="text-base font-semibold text-gray-800">
|
|
461
|
+
Two-Factor Authentication
|
|
462
|
+
</Typography>
|
|
463
|
+
<Typography className="text-sm text-gray-500 text-center mt-1 mb-3">
|
|
464
|
+
Enter the 6-digit code from your authenticator app to continue.
|
|
465
|
+
</Typography>
|
|
466
|
+
<Box className="w-full">
|
|
467
|
+
<CustomTextField
|
|
468
|
+
control={control}
|
|
469
|
+
name="totpCode"
|
|
470
|
+
placeholder="••••••"
|
|
471
|
+
errors={errors}
|
|
472
|
+
size="medium"
|
|
473
|
+
fullWidth
|
|
474
|
+
autoComplete="off"
|
|
475
|
+
autoFocus
|
|
476
|
+
inputProps={{
|
|
477
|
+
maxLength: 6,
|
|
478
|
+
inputMode: 'numeric',
|
|
479
|
+
pattern: '[0-9]*',
|
|
480
|
+
style: {
|
|
481
|
+
textAlign: 'center',
|
|
482
|
+
letterSpacing: '0.5em',
|
|
483
|
+
fontSize: '1.2rem',
|
|
484
|
+
fontWeight: 600,
|
|
485
|
+
},
|
|
486
|
+
onInput: (e) => {
|
|
487
|
+
e.target.value = e.target.value.replace(/\D/g, '');
|
|
488
|
+
},
|
|
489
|
+
}}
|
|
490
|
+
/>
|
|
491
|
+
</Box>
|
|
492
|
+
<Box className="w-full flex flex-col items-center mt-3">
|
|
493
|
+
<Link
|
|
494
|
+
component="button"
|
|
495
|
+
type="button"
|
|
496
|
+
onClick={() => setNoCodeHelp((cur) => !cur)}
|
|
497
|
+
className="text-[#007ACC] no-underline text-sm"
|
|
498
|
+
>
|
|
499
|
+
Don't have access to your code?
|
|
500
|
+
</Link>
|
|
501
|
+
<AnimatePresence>
|
|
502
|
+
{noCodeHelp && (
|
|
503
|
+
<motion.div
|
|
504
|
+
initial={{ opacity: 0, height: 0 }}
|
|
505
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
506
|
+
exit={{ opacity: 0, height: 0 }}
|
|
507
|
+
transition={{ duration: 0.2 }}
|
|
508
|
+
className="overflow-hidden w-full"
|
|
509
|
+
>
|
|
510
|
+
<Box className="mt-2 rounded-xl bg-amber-50 border border-amber-200 px-4 py-3">
|
|
511
|
+
<Typography className="text-xs text-amber-800 text-center leading-relaxed">
|
|
512
|
+
Please contact your administrator to reset your authenticator. You'll be able to
|
|
513
|
+
scan a new QR code and set it up again on your next login.
|
|
514
|
+
</Typography>
|
|
515
|
+
</Box>
|
|
516
|
+
</motion.div>
|
|
517
|
+
)}
|
|
518
|
+
</AnimatePresence>
|
|
519
|
+
<Link
|
|
520
|
+
component="button"
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() => {
|
|
523
|
+
setTwoFactorRequired(false);
|
|
524
|
+
setNoCodeHelp(false);
|
|
525
|
+
setValue('totpCode', '');
|
|
526
|
+
}}
|
|
527
|
+
className="text-gray-400 no-underline text-xs mt-3"
|
|
528
|
+
>
|
|
529
|
+
← Back to login
|
|
530
|
+
</Link>
|
|
531
|
+
</Box>
|
|
532
|
+
</motion.div >
|
|
533
|
+
)}
|
|
534
|
+
</AnimatePresence >
|
|
535
|
+
|
|
536
|
+
{/* Attempts-remaining / lockout feedback */}
|
|
537
|
+
< AnimatePresence mode="wait" >
|
|
538
|
+
{lockoutSeconds > 0 ? (
|
|
539
|
+
<motion.div
|
|
540
|
+
key="locked"
|
|
541
|
+
initial={{ opacity: 0, y: -8 }}
|
|
542
|
+
animate={{ opacity: 1, y: 0 }}
|
|
543
|
+
exit={{ opacity: 0, y: -8 }}
|
|
544
|
+
transition={{ duration: 0.2 }}
|
|
545
|
+
className="w-[85%] mt-3"
|
|
546
|
+
>
|
|
547
|
+
<Box className="flex items-center gap-3 rounded-xl bg-red-50 border border-red-200 px-4 py-3">
|
|
548
|
+
<LockClockOutlinedIcon className="text-red-500" />
|
|
549
|
+
<div className="text-left">
|
|
550
|
+
<Typography className="text-sm font-semibold text-red-700">
|
|
551
|
+
Account locked
|
|
552
|
+
</Typography>
|
|
553
|
+
<Typography className="text-xs text-red-600">
|
|
554
|
+
Too many failed attempts. Please try again later.
|
|
555
|
+
</Typography>
|
|
556
|
+
</div>
|
|
557
|
+
</Box>
|
|
558
|
+
</motion.div>
|
|
559
|
+
) : attemptsRemaining != null ? (
|
|
560
|
+
<motion.div
|
|
561
|
+
key="attempts"
|
|
562
|
+
initial={{ opacity: 0, y: -8 }}
|
|
563
|
+
animate={{ opacity: 1, y: 0 }}
|
|
564
|
+
exit={{ opacity: 0, y: -8 }}
|
|
565
|
+
transition={{ duration: 0.2 }}
|
|
566
|
+
className="w-[85%] mt-3"
|
|
567
|
+
>
|
|
568
|
+
<Box className="flex items-center gap-3 rounded-xl bg-amber-50 border border-amber-200 px-4 py-3">
|
|
569
|
+
<WarningAmberRoundedIcon className="text-amber-500" />
|
|
570
|
+
<Typography className="text-sm text-amber-800 text-left">
|
|
571
|
+
{twoFactorRequired ? 'Incorrect code.' : 'Incorrect username or password.'}{' '}
|
|
572
|
+
<strong>{attemptsRemaining}</strong>{' '}
|
|
573
|
+
{attemptsRemaining === 1 ? 'attempt' : 'attempts'} remaining before your account is locked.
|
|
574
|
+
</Typography>
|
|
575
|
+
</Box>
|
|
576
|
+
</motion.div>
|
|
577
|
+
) : null}
|
|
578
|
+
</AnimatePresence >
|
|
579
|
+
|
|
580
|
+
<Button
|
|
581
|
+
type="submit"
|
|
582
|
+
variant="contained"
|
|
583
|
+
className={`mt-5 w-[85%] h-11 rounded-full shadow-md normal-case text-base font-medium ${!submitting ? activeJSON.submitButtonClass : ''}`}
|
|
584
|
+
data-testid="login-submit-button"
|
|
585
|
+
disabled={submitting || lockoutSeconds > 0}
|
|
586
|
+
>
|
|
587
|
+
{submitting ? (
|
|
588
|
+
<CircularProgress size={24} color="inherit" />
|
|
589
|
+
) : lockoutSeconds > 0 ? (
|
|
590
|
+
'Locked'
|
|
591
|
+
) : twoFactorRequired ? (
|
|
592
|
+
'VERIFY'
|
|
593
|
+
) : (
|
|
594
|
+
'LOGIN'
|
|
595
|
+
)}
|
|
596
|
+
</Button>
|
|
597
|
+
|
|
598
|
+
<Link
|
|
599
|
+
href="/ui/forgotpassword"
|
|
600
|
+
className="mt-3 text-[#007ACC] no-underline"
|
|
601
|
+
onClick={(e) => {
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
startTransition(() => {
|
|
604
|
+
navigate('/ui/forgotpassword');
|
|
605
|
+
});
|
|
606
|
+
}}
|
|
607
|
+
>
|
|
608
|
+
Forgot Password?
|
|
609
|
+
</Link>
|
|
610
|
+
|
|
611
|
+
{
|
|
612
|
+
activeJSON.isDownloadLink &&
|
|
613
|
+
(!login.hideAppLink || login.version != null) && (
|
|
614
|
+
<Link href={login.link} download className="mt-1 text-[#007ACC] no-underline hover:underline" >
|
|
615
|
+
<span className="text-red-600 font-bold" > (New) </span>
|
|
616
|
+
<FontAwesomeIcon icon={faDownload} />
|
|
617
|
+
Download Mobile App {login.version ? `(${login.version})` : ''} {' '}
|
|
618
|
+
{login.release_date ? `(${login.release_date})` : ''}
|
|
619
|
+
</Link>
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
{
|
|
624
|
+
activeJSON.footerSections && activeJSON.footerSections.length > 0 && (
|
|
625
|
+
<Box className="w-full flex flex-col items-center" >
|
|
626
|
+
{
|
|
627
|
+
activeJSON.footerSections.map((section, idx) => {
|
|
628
|
+
const isButtons = section.type === 'buttons';
|
|
629
|
+
const showDivider = idx === 0 || (idx > 0 && activeJSON.footerSections[idx - 1].type !== 'buttons');
|
|
630
|
+
|
|
631
|
+
return (
|
|
632
|
+
<React.Fragment key={idx} >
|
|
633
|
+
{showDivider && (
|
|
634
|
+
<Divider className={isButtons ? 'w-[85%] opacity-40' : 'w-full mt-2 mb-2'} />
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
{
|
|
639
|
+
section.type === 'buttons' && (
|
|
640
|
+
<Box className="flex justify-between gap-4 mt-6 w-full px-2" >
|
|
641
|
+
{
|
|
642
|
+
section.buttons.map((btn, btnIdx) => {
|
|
643
|
+
const icon = iconMap[btn.icon];
|
|
644
|
+
const displayText = btn.text.replace('{{CURRENT_YEAR}}', new Date().getFullYear());
|
|
645
|
+
return (
|
|
646
|
+
<Box
|
|
647
|
+
key={btnIdx}
|
|
648
|
+
className="flex-1 flex flex-col items-center justify-center py-4 bg-[#eef4fa] rounded-[2rem] border border-[#e2efe2] cursor-pointer hover:bg-[#e5f3ff] transition-colors"
|
|
649
|
+
onClick={() => window.open(btn.href, '_blank')
|
|
650
|
+
}
|
|
651
|
+
>
|
|
652
|
+
{icon && <FontAwesomeIcon icon={icon} size="2x" className="text-[#00685b]" />}
|
|
653
|
+
< Typography className={`${icon ? 'text-[14px]' : 'text-[16px]'} font-extrabold text-[#00685b] ${icon ? 'mt-3' : ''} tracking-tighter uppercase whitespace-nowrap`
|
|
654
|
+
}>
|
|
655
|
+
{displayText}
|
|
656
|
+
</Typography>
|
|
657
|
+
</Box>
|
|
658
|
+
);
|
|
659
|
+
})}
|
|
660
|
+
</Box>
|
|
661
|
+
)}
|
|
662
|
+
|
|
663
|
+
{
|
|
664
|
+
section.type === 'logos' && (
|
|
665
|
+
<Box className="w-full mt-6 mb-2 flex items-center justify-center gap-6 px-4" >
|
|
666
|
+
{
|
|
667
|
+
section.logos.map((logo, logoIdx) => {
|
|
668
|
+
const isMedplatLogo = logo.image === 'medplat-highres.png';
|
|
669
|
+
const showVerticalDivider = isMedplatLogo && logoIdx > 0 && section.logos[logoIdx - 1].image !== 'medplat-highres.png' && activeJSON.showLogoDivider;
|
|
670
|
+
const heightClass = activeJSON.logoHeightClass || (isMedplatLogo ? 'h-12 w-auto' : 'h-10 w-auto');
|
|
671
|
+
return (
|
|
672
|
+
<React.Fragment key={logoIdx} >
|
|
673
|
+
{showVerticalDivider && <Divider orientation="vertical" flexItem className="h-8 mx-1 opacity-50" />}
|
|
674
|
+
<a href={logo.href} target="_blank" rel="noreferrer" >
|
|
675
|
+
<img
|
|
676
|
+
src={imageMap[logo.image]}
|
|
677
|
+
alt={logo.alt}
|
|
678
|
+
className={heightClass}
|
|
679
|
+
/>
|
|
680
|
+
</a>
|
|
681
|
+
</React.Fragment>
|
|
682
|
+
);
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
</Box>
|
|
686
|
+
)}
|
|
687
|
+
|
|
688
|
+
{
|
|
689
|
+
section.type === 'textWithLogo' && (
|
|
690
|
+
<Box className="w-full mt-2 flex items-center justify-center gap-6 px-4" >
|
|
691
|
+
<a href={section.href} target="_blank" rel="noreferrer" >
|
|
692
|
+
<span className="flex items-center gap-2" >
|
|
693
|
+
{section.text} < img src={imageMap[section.image]} alt={section.alt} className="h-14 w-auto" />
|
|
694
|
+
</span>
|
|
695
|
+
</a>
|
|
696
|
+
</Box>
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
{
|
|
701
|
+
section.type === 'logosWithText' && (
|
|
702
|
+
<Box className="w-full mt-2 flex items-center justify-center gap-6 px-4" >
|
|
703
|
+
{
|
|
704
|
+
section.items.map((item, itemIdx) => {
|
|
705
|
+
const hasText = !!item.text;
|
|
706
|
+
return (
|
|
707
|
+
<React.Fragment key={itemIdx} >
|
|
708
|
+
{hasText && <Divider orientation="vertical" flexItem />}
|
|
709
|
+
<a href={item.href} target="_blank" rel="noreferrer" >
|
|
710
|
+
{
|
|
711
|
+
hasText ? (
|
|
712
|
+
<span className="flex items-center gap-2" >
|
|
713
|
+
<img src={imageMap[item.image]} alt={item.alt} className="h-16 w-auto" /> {' '}
|
|
714
|
+
{item.text
|
|
715
|
+
}
|
|
716
|
+
</span>
|
|
717
|
+
) : (
|
|
718
|
+
<img src={imageMap[item.image]} alt={item.alt} className="h-8 w-auto" />
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
</a>
|
|
722
|
+
</React.Fragment>
|
|
723
|
+
);
|
|
724
|
+
})}
|
|
725
|
+
</Box>
|
|
726
|
+
)}
|
|
727
|
+
</React.Fragment>
|
|
728
|
+
);
|
|
729
|
+
})}
|
|
730
|
+
</Box>
|
|
731
|
+
)}
|
|
732
|
+
|
|
733
|
+
{
|
|
734
|
+
activeJSON.bottomLogos && activeJSON.bottomLogos.map((logo, idx) => (
|
|
735
|
+
<React.Fragment key={idx} >
|
|
736
|
+
<Divider className="w-full mt-2 mb-2" />
|
|
737
|
+
<div className="flex items-center" >
|
|
738
|
+
<a href={logo.href} target="_blank" rel="noreferrer" border="0" >
|
|
739
|
+
<img src={imageMap[logo.image]} alt={logo.alt} height="50" className="h-12 w-auto" />
|
|
740
|
+
</a>
|
|
741
|
+
</div>
|
|
742
|
+
</React.Fragment>
|
|
743
|
+
))
|
|
744
|
+
}
|
|
745
|
+
</Grid>
|
|
746
|
+
</>
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return (
|
|
750
|
+
<>
|
|
751
|
+
<AnimatePresence>
|
|
752
|
+
{loginSuccess && (
|
|
753
|
+
<motion.div
|
|
754
|
+
key="login-success"
|
|
755
|
+
className="fixed inset-0 z-[200] flex items-center justify-center bg-gradient-to-br from-slate-100 to-slate-300 p-4"
|
|
756
|
+
initial={{ opacity: 0 }}
|
|
757
|
+
animate={{ opacity: 1 }}
|
|
758
|
+
exit={{ opacity: 0 }}
|
|
759
|
+
>
|
|
760
|
+
<motion.div
|
|
761
|
+
initial={{ scale: 0.95, opacity: 0, y: 16 }}
|
|
762
|
+
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
763
|
+
transition={{ type: 'spring', stiffness: 220, damping: 22 }}
|
|
764
|
+
className="bg-white rounded-2xl shadow-2xl px-10 py-12 flex flex-col items-center text-center max-w-sm w-full"
|
|
765
|
+
>
|
|
766
|
+
<div className="relative flex items-center justify-center mb-5">
|
|
767
|
+
<motion.span
|
|
768
|
+
className="absolute inline-flex h-20 w-20 rounded-full bg-green-200"
|
|
769
|
+
initial={{ scale: 0.6, opacity: 0.7 }}
|
|
770
|
+
animate={{ scale: 1.6, opacity: 0 }}
|
|
771
|
+
transition={{ duration: 1.1, repeat: Infinity }}
|
|
772
|
+
/>
|
|
773
|
+
<motion.div
|
|
774
|
+
initial={{ scale: 0, rotate: -20 }}
|
|
775
|
+
animate={{ scale: 1, rotate: 0 }}
|
|
776
|
+
transition={{ type: 'spring', stiffness: 260, damping: 14 }}
|
|
777
|
+
className="relative text-green-500"
|
|
778
|
+
>
|
|
779
|
+
<CheckCircleRoundedIcon sx={{ fontSize: 80 }} />
|
|
780
|
+
</motion.div>
|
|
781
|
+
</div>
|
|
782
|
+
<Typography className="text-xl font-bold text-gray-800">Login successful</Typography>
|
|
783
|
+
<Typography className="text-sm text-gray-500 mt-2">
|
|
784
|
+
Welcome back{loginSuccess.name ? `, ${loginSuccess.name}` : ''}! Taking you to your dashboard…
|
|
785
|
+
</Typography>
|
|
786
|
+
<div className="flex items-center gap-2 mt-5 text-gray-400">
|
|
787
|
+
<CircularProgress size={14} color="inherit" />
|
|
788
|
+
</div>
|
|
789
|
+
</motion.div>
|
|
790
|
+
</motion.div>
|
|
791
|
+
)}
|
|
792
|
+
</AnimatePresence>
|
|
793
|
+
{twoFactorSetup && (
|
|
794
|
+
<TwoFactorSetupModal
|
|
795
|
+
open={!!twoFactorSetup}
|
|
796
|
+
userId={twoFactorSetup.userId}
|
|
797
|
+
userName={twoFactorSetup.userName}
|
|
798
|
+
onSkip={() => {
|
|
799
|
+
const target = twoFactorSetup.redirectTo;
|
|
800
|
+
setTwoFactorSetup(null);
|
|
801
|
+
navigateRef.current(target);
|
|
802
|
+
}}
|
|
803
|
+
onVerified={() => {
|
|
804
|
+
const target = twoFactorSetup.redirectTo;
|
|
805
|
+
setTwoFactorSetup(null);
|
|
806
|
+
navigateRef.current(target);
|
|
807
|
+
}}
|
|
808
|
+
/>
|
|
809
|
+
)}
|
|
810
|
+
{activeJSON.centerLogin ? (
|
|
811
|
+
/* ── Centered Card layout (centerLogin: true) ── */
|
|
812
|
+
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-100 to-slate-300 pt-2 pb-2">
|
|
813
|
+
<Card elevation={6} className="rounded-2xl px-0 py-5 w-full max-w-lg">
|
|
814
|
+
<Grid component="form" onSubmit={handleSubmit(submit)} container justifyContent="center" className="w-full">
|
|
815
|
+
{jsonRenderer()}
|
|
816
|
+
</Grid>
|
|
817
|
+
</Card>
|
|
818
|
+
</div>
|
|
819
|
+
) : (
|
|
820
|
+
/* ── Split-panel layout (centerLogin: false) ── */
|
|
821
|
+
<div className="login-split-container">
|
|
822
|
+
{/* Left Panel - Image Area */}
|
|
823
|
+
<div className="login-left-panel">
|
|
824
|
+
<img src={imageMap[activeJSON.implementationImage]} className="login-image" />
|
|
825
|
+
</div>
|
|
826
|
+
{/* Right Panel - Form Area */}
|
|
827
|
+
<div className="login-right-panel">
|
|
828
|
+
<div className="login-form-container">
|
|
829
|
+
<form onSubmit={handleSubmit(submit)} className="w-full">
|
|
830
|
+
{jsonRenderer()}
|
|
831
|
+
</form>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
)}
|
|
836
|
+
</>
|
|
837
|
+
);
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
export default Login;
|