@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.
Files changed (263) hide show
  1. package/package.json +139 -141
  2. package/src/GlobalErrorBoundary.jsx +31 -0
  3. package/src/SilentErrorFallback.jsx +68 -0
  4. package/src/TrackingProviderWrapper.jsx +40 -0
  5. package/src/_tests_/__mocks__/MockTranslationProvider.jsx +21 -0
  6. package/src/_tests_/__mocks__/ckeditor.js +45 -0
  7. package/src/_tests_/__mocks__/fileMock.js +1 -0
  8. package/src/_tests_/__mocks__/useranalytics.js +5 -0
  9. package/src/_tests_/views/components/Dashboard/DashboardUI.test.jsx +137 -0
  10. package/src/_tests_/views/components/Dashboard/DashboardUIMock.js +877 -0
  11. package/src/_tests_/views/components/ForgotPassword/ForgotPassword.test.jsx +314 -0
  12. package/src/_tests_/views/components/LocationDirective/LocationDirective.test.jsx.disable +229 -0
  13. package/src/_tests_/views/components/LocationDirective/mockLocationDirective.js +810 -0
  14. package/src/_tests_/views/components/LocationType/MockLocationType.js +42259 -0
  15. package/src/_tests_/views/components/LocationType/addlocationtype.test.jsx.disable +276 -0
  16. package/src/_tests_/views/components/LocationType/editlocationtype.test.jsx.disable +262 -0
  17. package/src/_tests_/views/components/LocationType/locationtype.test.jsx.disable +148 -0
  18. package/src/_tests_/views/components/Profile/UpdateProfileModalData.js +4396 -0
  19. package/src/_tests_/views/components/Profile/updateprofilemodal.test.jsx +282 -0
  20. package/src/_tests_/views/components/SideBar/MockSideBar.js +1379 -0
  21. package/src/_tests_/views/components/SideBar/SideBar.test.jsx +98 -0
  22. package/src/_tests_/views/components/SystemConfig/ManageSystemConfig/AddManageSystemConfig.test.jsx.disable +164 -0
  23. package/src/_tests_/views/components/SystemConfig/ManageSystemConfig/UpdateManageSystemConfig.test.jsx.disable +157 -0
  24. package/src/_tests_/views/components/SystemConfig/MockSystemConfig.js +1280 -0
  25. package/src/_tests_/views/components/SystemConfig/SystemConfig.test.jsx.disable +165 -0
  26. package/src/_tests_/views/components/login/Login.test.jsx +276 -0
  27. package/src/_tests_/views/components/login/MockAuthorise.js +2414 -0
  28. package/src/_tests_/views/components/login/ServiceResponse.js +595 -0
  29. package/src/_tests_/views/components/user/MockManageUser.js +7965 -0
  30. package/src/_tests_/views/components/user/manageuser.test.jsx.disable +989 -0
  31. package/src/_tests_/views/components/user/mockUsersData.js +582 -0
  32. package/src/assets/img/OASISLogin.png +0 -0
  33. package/src/assets/img/bahaarNew.png +0 -0
  34. package/src/assets/img/dnhdd4K.png +0 -0
  35. package/src/assets/img/govtofup.png +0 -0
  36. package/src/assets/img/sewarural4K.png +0 -0
  37. package/src/assets/img/techo4K.png +0 -0
  38. package/src/assets/img/up4K.png +0 -0
  39. package/src/common/HolidayList.jsx +573 -0
  40. package/src/common/MalaciaousInputUtil.js +23 -0
  41. package/src/common/SafeHtml.jsx +17 -0
  42. package/src/common/VersionManager.jsx +109 -0
  43. package/src/common/constants/PerformanceDashboard.js +514 -0
  44. package/src/common/constants/app.constant.js +781 -0
  45. package/src/common/constants/cccVerificationConstants.js +18 -0
  46. package/src/common/constants/fhsrConstant.js +33 -0
  47. package/src/common/constants/gvk-verification.constant.js +76 -0
  48. package/src/common/constants/search.constant.js +23 -0
  49. package/src/common/constants/teleconsulatationConstant.jsx +1339 -0
  50. package/src/common/directives/SearchTemplate.jsx +784 -0
  51. package/src/common/directives/SearchTemplate.scss +14 -0
  52. package/src/common/dynamicView/DynamicView.jsx +353 -0
  53. package/src/common/dynamicView/InputFieldComponent.jsx +1501 -0
  54. package/src/common/dynamicView/InputViewComponent.jsx +298 -0
  55. package/src/common/dynamicView/InputViewComponent.scss +15 -0
  56. package/src/common/env.js +5 -0
  57. package/src/common/filters/locationNameFilter.js +26 -0
  58. package/src/common/fontAwesomeIcons/FontAwesomeIcons.jsx +27 -0
  59. package/src/common/fontAwesomeIcons/FontAwesomeIconsNames.js +1968 -0
  60. package/src/common/fontPreferences/fontSizeProvider.jsx +34 -0
  61. package/src/common/fontPreferences/fontSizeSelector.jsx +116 -0
  62. package/src/common/getAssignedFeature/getAssignedFeature.js +32 -0
  63. package/src/common/interceptors/AxiosInterceptor.js +216 -0
  64. package/src/common/languageTranslator/TranslationContext.js +5 -0
  65. package/src/common/languageTranslator/TranslationProvider.jsx +24 -0
  66. package/src/common/languageTranslator/i18n.js +49 -0
  67. package/src/common/services/AuthenticateService.js +116 -0
  68. package/src/common/services/DownloadFile.js +35 -0
  69. package/src/common/services/ForgotPassword.js +18 -0
  70. package/src/common/services/FormConfiguratorService.js +195 -0
  71. package/src/common/services/GlobalApis.js +84 -0
  72. package/src/common/services/InterceptorNavigationService.js +17 -0
  73. package/src/common/services/LocationService.js +65 -0
  74. package/src/common/services/LocationType.js +11 -0
  75. package/src/common/services/QueryBuilder.js +36 -0
  76. package/src/common/services/Roles.js +28 -0
  77. package/src/common/services/SyncWithServer.js +15 -0
  78. package/src/common/services/SystemConfig.js +15 -0
  79. package/src/common/services/TranslationService.js +70 -0
  80. package/src/common/services/TwoFactorService.js +7 -0
  81. package/src/common/services/Users.js +91 -0
  82. package/src/common/services/Webtasks.js +27 -0
  83. package/src/common/services/util/Convert-pad-data-to-API-format.jsx +167 -0
  84. package/src/common/services/util/Convert-to-UI-format.jsx +82 -0
  85. package/src/common/services/util/EmptyPrescriptionPadData.jsx +11 -0
  86. package/src/common/services/util/GeneralUtil.js +456 -0
  87. package/src/common/services/util/Prescription-pad-util.js +339 -0
  88. package/src/common/services/util/PrescriptionPadData.js +67 -0
  89. package/src/common/services/util/PrescriptionpadCommonUtil.js +96 -0
  90. package/src/common/services/util/ReportFieldUtil.jsx +398 -0
  91. package/src/common/services/util/WebSocketContext.jsx +261 -0
  92. package/src/common/syncWithServer/SyncWithServerDialog.jsx +170 -0
  93. package/src/common/syncWithServer/SyncWithServerDialogSkeleton.jsx +67 -0
  94. package/src/common/tests/CustomWrapper.jsx +49 -0
  95. package/src/common/tests/TranslationWrapper.jsx +38 -0
  96. package/src/common/themeProvider/ColorInputs.jsx +97 -0
  97. package/src/common/themeProvider/EditableColorInput.jsx +128 -0
  98. package/src/common/themeProvider/ThemeEditor.jsx +319 -0
  99. package/src/common/themeProvider/ThemeProvider.jsx +210 -0
  100. package/src/common/themeProvider/themeConfig.js +558 -0
  101. package/src/common/toaster/toaster.jsx +30 -0
  102. package/src/firebaseConfig.js +24 -0
  103. package/src/global.scss +221 -0
  104. package/src/hooks/.gitkeep +0 -0
  105. package/src/hooks/useAESEncryption.js +56 -0
  106. package/src/hooks/useCaching.js +43 -0
  107. package/src/hooks/useDebounce.js +34 -0
  108. package/src/hooks/useDebounceFn.js +50 -0
  109. package/src/hooks/useDownloadPdf.js +358 -0
  110. package/src/hooks/useDownloadXlsx.js +55 -0
  111. package/src/hooks/useListValueFieldValues.js +30 -0
  112. package/src/hooks/useLocationHierarchies.js +63 -0
  113. package/src/hooks/useLocationHierarchyTranslate.js +16 -0
  114. package/src/hooks/useOnline.js +27 -0
  115. package/src/hooks/usePagination.js +63 -0
  116. package/src/hooks/useRefreshToken.js +87 -0
  117. package/src/hooks/useScript.js +25 -0
  118. package/src/hooks/useStopwatch.js +75 -0
  119. package/src/hooks/useTrackEvent.js +22 -0
  120. package/src/hooks/useWebAudioRecorder.js +115 -0
  121. package/src/layout/LoaderComponet.jsx +22 -0
  122. package/src/layout/LoaderContext.jsx +29 -0
  123. package/src/layout/mainLayout/AdaptiveZoom.jsx +27 -0
  124. package/src/layout/mainLayout/Chatbot.jsx +243 -0
  125. package/src/layout/mainLayout/Layout.jsx +445 -0
  126. package/src/layout/mainLayout/Profile/UpdateProfileModal.jsx +684 -0
  127. package/src/layout/mainLayout/header/LogoutModal.jsx +131 -0
  128. package/src/layout/mainLayout/header/Navbar.jsx +1677 -0
  129. package/src/layout/mainLayout/header/Navbar.scss +4 -0
  130. package/src/layout/mainLayout/header/index.js +0 -0
  131. package/src/layout/mainLayout/sidebar/SideBar.jsx +1402 -0
  132. package/src/layout/mainLayout/sidebar/Sidebar.css +159 -0
  133. package/src/layout/mainLayout/sidebar/index.js +0 -0
  134. package/src/logo.svg +1 -0
  135. package/src/reportWebVitals.js +13 -0
  136. package/src/setupFirebaseMessaging.js +28 -0
  137. package/src/setupTests.js +8 -0
  138. package/src/store/actions/AuthenticationActions.js +0 -0
  139. package/src/store/actions/ReportsActions.js +0 -0
  140. package/src/store/actions/TranslationAction.js +0 -0
  141. package/src/store/index.js +8 -0
  142. package/src/store/reducer.js +46 -0
  143. package/src/store/reducers/AuthenticationReducer.js +50 -0
  144. package/src/store/reducers/CalendarEventReducer.js +41 -0
  145. package/src/store/reducers/ConditionClipboardReducer.js +45 -0
  146. package/src/store/reducers/FeatureReducer.js +27 -0
  147. package/src/store/reducers/FormConfiguratorReducer.js +38 -0
  148. package/src/store/reducers/LoadingReducer.js +20 -0
  149. package/src/store/reducers/MembersAuthenticationReducer.js +28 -0
  150. package/src/store/reducers/PrescriptionPadReducer.js +329 -0
  151. package/src/store/reducers/QuestionaireReducer.js +29 -0
  152. package/src/store/reducers/ReportsReducer.js +24 -0
  153. package/src/store/reducers/SkeletonReducer.js +20 -0
  154. package/src/store/reducers/ThemeReducer.js +106 -0
  155. package/src/store/reducers/TranslationReducer.js +126 -0
  156. package/src/store/reducers/dashboardEditorSlice.js +77 -0
  157. package/src/store/reducers/districtHealthDashboardSlice.js +58 -0
  158. package/src/store/reducers/immunizationSlice.js +234 -0
  159. package/src/store/slices/dashboardPagesSlice.js +51 -0
  160. package/src/utils/.gitkeep +0 -0
  161. package/src/utils/FormConstants.js +2629 -0
  162. package/src/utils/GujaratTopoChart.jsx +483 -0
  163. package/src/utils/UUIDgenerator.js +8 -0
  164. package/src/utils/appointment-utils/appointment-utils.js +123 -0
  165. package/src/utils/feature.js +42 -0
  166. package/src/utils/getThemeColor.js +12 -0
  167. package/src/utils/localStorageHelper.js +11 -0
  168. package/src/utils/notifications/enable-push-notifications.js +27 -0
  169. package/src/utils/resolveAppliedStyle.js +11 -0
  170. package/src/utils/themeConfigs.js +1483 -0
  171. package/src/views/custom-components/.gitkeep +0 -0
  172. package/src/views/custom-components/AgIconButton/RIf.jsx +14 -0
  173. package/src/views/custom-components/AgIconButton/button.jsx +108 -0
  174. package/src/views/custom-components/AgIconButton/waterDrop.jsx +95 -0
  175. package/src/views/custom-components/AgIconButton/waterDrop.scss +37 -0
  176. package/src/views/custom-components/AlertPlaceholder.jsx +32 -0
  177. package/src/views/custom-components/AllFaIconsSelector.jsx +56 -0
  178. package/src/views/custom-components/CkEditor/CkEditor.js +102 -0
  179. package/src/views/custom-components/CustomAccordion.jsx +72 -0
  180. package/src/views/custom-components/CustomActionIcons.jsx +118 -0
  181. package/src/views/custom-components/CustomAutoComplete.jsx +188 -0
  182. package/src/views/custom-components/CustomCheckBox.jsx +60 -0
  183. package/src/views/custom-components/CustomConfirmationModal.jsx +118 -0
  184. package/src/views/custom-components/CustomCountrySelect.jsx +129 -0
  185. package/src/views/custom-components/CustomDatePicker.jsx +122 -0
  186. package/src/views/custom-components/CustomDropdown.jsx +191 -0
  187. package/src/views/custom-components/CustomFileUpload.jsx +387 -0
  188. package/src/views/custom-components/CustomFullCalendar.jsx +514 -0
  189. package/src/views/custom-components/CustomInfiniteScroll.jsx +126 -0
  190. package/src/views/custom-components/CustomRadioComponent.jsx +65 -0
  191. package/src/views/custom-components/CustomStatsComponent.jsx +114 -0
  192. package/src/views/custom-components/CustomSvgUpload.jsx +170 -0
  193. package/src/views/custom-components/CustomSwitch.jsx +37 -0
  194. package/src/views/custom-components/CustomTabPanel.jsx +19 -0
  195. package/src/views/custom-components/CustomTextArea.jsx +62 -0
  196. package/src/views/custom-components/CustomTextArea.scss +27 -0
  197. package/src/views/custom-components/CustomTextField.jsx +116 -0
  198. package/src/views/custom-components/CustomToggleSwitch.jsx +138 -0
  199. package/src/views/custom-components/CustomTooltip.jsx +51 -0
  200. package/src/views/custom-components/CustomZoomImage.jsx +134 -0
  201. package/src/views/custom-components/CustomizedTable/CustomizedTableV2.jsx +1407 -0
  202. package/src/views/custom-components/CustomizedTable/VirtualizeTableBody.jsx +295 -0
  203. package/src/views/custom-components/CustomizedTable/helper.jsx +159 -0
  204. package/src/views/custom-components/CustomizedTable.jsx +532 -0
  205. package/src/views/custom-components/EditInputField.jsx +174 -0
  206. package/src/views/custom-components/FieldDescription.jsx +22 -0
  207. package/src/views/custom-components/FileDisplayComponent.jsx +138 -0
  208. package/src/views/custom-components/FormItem.jsx +53 -0
  209. package/src/views/custom-components/GenericChart.jsx +80 -0
  210. package/src/views/custom-components/InfoBadge.jsx +60 -0
  211. package/src/views/custom-components/PostgresEditor.jsx +801 -0
  212. package/src/views/custom-components/ResizableEditAutocompleteField.jsx +249 -0
  213. package/src/views/custom-components/ResizableEditInputField.jsx +215 -0
  214. package/src/views/custom-components/ResizeableEditSelectField.jsx +197 -0
  215. package/src/views/custom-components/SideOverlay.jsx +113 -0
  216. package/src/views/custom-components/SideOverlay.scss +42 -0
  217. package/src/views/custom-components/calendar.scss +571 -0
  218. package/src/views/feature-components/.gitkeep +0 -0
  219. package/src/views/feature-components/Dashboard/DashboardUI.jsx +1043 -0
  220. package/src/views/feature-components/Dashboard/DhnddModal/AshaDataQualityVerificationModal.jsx +278 -0
  221. package/src/views/feature-components/Dashboard/PinFeatureModal.jsx +143 -0
  222. package/src/views/feature-components/Dashboard/QuickLinks.jsx +163 -0
  223. package/src/views/feature-components/Dashboard/Taskbar.jsx +56 -0
  224. package/src/views/feature-components/Dashboard/WebtasksFilterForm.jsx +109 -0
  225. package/src/views/feature-components/Dashboard/WidgetCard.jsx +161 -0
  226. package/src/views/feature-components/Dashboard/actionModal.jsx +263 -0
  227. package/src/views/feature-components/Dashboard/ekavachModal/HealthWorkerIncorrectDetailsModal.jsx +332 -0
  228. package/src/views/feature-components/Dashboard/ekavachModal/MoMaternalDeathVerifcationModal.jsx +275 -0
  229. package/src/views/feature-components/Dashboard/ekavachModal/MoVerficationForChildScreeningMoadal.jsx +566 -0
  230. package/src/views/feature-components/FeatureUsageAnalytics/FeatureUsageAnalytics.jsx +989 -0
  231. package/src/views/feature-components/Features/NewServerManagement.jsx +217 -0
  232. package/src/views/feature-components/Features/ServerManagement.scss +120 -0
  233. package/src/views/feature-components/ForgotPassword/ForgotPassword.jsx +226 -0
  234. package/src/views/feature-components/LocationDirective/LocationDirective.jsx +992 -0
  235. package/src/views/feature-components/LocationDirective/LocationDirectiveV2.jsx +909 -0
  236. package/src/views/feature-components/NotFound.jsx +66 -0
  237. package/src/views/feature-components/Onboarding/Onboarding.jsx +1400 -0
  238. package/src/views/feature-components/Skeletons.js +115 -0
  239. package/src/views/feature-components/Unauthorized.jsx +48 -0
  240. package/src/views/feature-components/VerifyRoute.jsx +88 -0
  241. package/src/views/feature-components/YearlyRecap/YearlyRecap.jsx +357 -0
  242. package/src/views/feature-components/YearlyRecap/components/RecapSlide.jsx +183 -0
  243. package/src/views/feature-components/YearlyRecap/languageTranslator/TranslationContext.js +5 -0
  244. package/src/views/feature-components/YearlyRecap/languageTranslator/TranslationProvider.jsx +26 -0
  245. package/src/views/feature-components/YearlyRecap/languageTranslator/i18n.js +46 -0
  246. package/src/views/feature-components/YearlyRecap/languageTranslator/translations.json +167 -0
  247. package/src/views/feature-components/YearlyRecap/slides/IntroSlide.jsx +233 -0
  248. package/src/views/feature-components/YearlyRecap/slides/MaternalHealthSlide.jsx +146 -0
  249. package/src/views/feature-components/YearlyRecap/slides/MetricSlide.jsx +227 -0
  250. package/src/views/feature-components/YearlyRecap/slides/OutroSlide.jsx +701 -0
  251. package/src/views/feature-components/YearlyRecap/slides/ReachSlide.jsx +273 -0
  252. package/src/views/feature-components/login/Login.jsx +840 -0
  253. package/src/views/feature-components/login/Login.scss +154 -0
  254. package/src/views/feature-components/login/LoginConfigurator.jsx +1149 -0
  255. package/src/views/feature-components/login/TwoFactorSetupModal.jsx +411 -0
  256. package/src/views/feature-components/login/simplifyMenu.js +45 -0
  257. package/src/views/feature-components/system-config/ManageSystemConfigs.jsx +284 -0
  258. package/src/views/feature-components/system-config/SystemConfig.jsx +299 -0
  259. package/src/views/feature-components/users/ChangePasswordModal.jsx +243 -0
  260. package/src/views/feature-components/users/PasswordField.jsx +56 -0
  261. package/dist/index.css +0 -1
  262. package/dist/index.js +0 -32001
  263. package/dist/index.js.map +0 -1
@@ -0,0 +1,1149 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useForm, useFieldArray } from 'react-hook-form';
3
+ import {
4
+ Box,
5
+ Button,
6
+ Card,
7
+ TextField,
8
+ Typography,
9
+ Switch,
10
+ FormControlLabel,
11
+ Select,
12
+ MenuItem,
13
+ InputLabel,
14
+ FormControl,
15
+ IconButton,
16
+ Divider,
17
+ Tabs,
18
+ Tab,
19
+ Paper,
20
+ Grid,
21
+ Menu
22
+ } from '@mui/material';
23
+ import { api } from '@/common/interceptors/AxiosInterceptor';
24
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
25
+ import {
26
+ faPlus,
27
+ faTrash,
28
+ faSave,
29
+ faUndo,
30
+ faCopy,
31
+ faEye,
32
+ faMobileAlt,
33
+ faDesktop,
34
+ faDownload,
35
+ faPerson,
36
+ faBookMedical,
37
+ faPassport,
38
+ faPalette,
39
+ } from '@fortawesome/free-solid-svg-icons';
40
+ import { showToast } from '@/common/toaster/toaster';
41
+ import { imageMap } from './Login';
42
+ import { bahaarJSON, upJSON, phmpJSON, sewaruralJSON, oasisJSON, techoJSON, defaultJSON } from '@/common/constants/app.constant';
43
+
44
+ const PRESETS = {
45
+ default: defaultJSON,
46
+ bahaar: bahaarJSON,
47
+ ekavach: upJSON,
48
+ dnhdd: phmpJSON,
49
+ sewarural: sewaruralJSON,
50
+ oasis: oasisJSON,
51
+ gujarat: techoJSON
52
+ };
53
+
54
+ const PRESET_LABELS = {
55
+ custom: 'Custom Config',
56
+ default: 'Default (Medplat)',
57
+ bahaar: 'Bahaar (Sath)',
58
+ ekavach: 'UP (eKavach)',
59
+ dnhdd: 'PHMP (DNH & DD)',
60
+ sewarural: 'Sewa Rural',
61
+ oasis: 'OASIS (PHFI/LSHTM)',
62
+ gujarat: 'TeCHO+ (Gujarat)'
63
+ };
64
+
65
+ const iconMap = {
66
+ PERSON: faPerson,
67
+ MEDICAL: faBookMedical,
68
+ PASSPORT: faPassport,
69
+ };
70
+
71
+ export default function LoginConfigurator() {
72
+ const [selectedPresetKey, setSelectedPresetKey] = useState('gujarat');
73
+ const [activeTab, setActiveTab] = useState(0); // 0 = Form Editor, 1 = Raw JSON
74
+ const [previewDevice, setPreviewDevice] = useState('desktop'); // desktop or mobile
75
+ const [presetMenuAnchor, setPresetMenuAnchor] = useState(null);
76
+
77
+ const containerRef = React.useRef(null);
78
+ const [scale, setScale] = useState(1);
79
+ const isResettingRef = React.useRef(false);
80
+
81
+ const targetWidth = previewDevice === 'mobile' ? 360 : 800;
82
+ const targetHeight = previewDevice === 'mobile' ? 640 : 500;
83
+
84
+ useEffect(() => {
85
+ if (!containerRef.current) return;
86
+ const updateScale = () => {
87
+ const parent = containerRef.current.parentElement;
88
+ if (!parent) return;
89
+ // Get padding-adjusted parent height and width
90
+ const parentWidth = parent.clientWidth - 32;
91
+ const parentHeight = parent.clientHeight - 32;
92
+
93
+ const widthScale = parentWidth / targetWidth;
94
+ const heightScale = parentHeight / targetHeight;
95
+ const scaleFactor = Math.max(0.2, Math.min(widthScale, heightScale, 1)); // max scale = 1, min scale = 0.2
96
+ setScale(scaleFactor);
97
+ };
98
+
99
+ updateScale();
100
+ window.addEventListener('resize', updateScale);
101
+
102
+ // ResizeObserver for highly responsive updates when layout layout shifts
103
+ const observer = new ResizeObserver(() => {
104
+ updateScale();
105
+ });
106
+ if (containerRef.current.parentElement) {
107
+ observer.observe(containerRef.current.parentElement);
108
+ }
109
+
110
+ return () => {
111
+ window.removeEventListener('resize', updateScale);
112
+ observer.disconnect();
113
+ };
114
+ }, [previewDevice, targetWidth, targetHeight]);
115
+
116
+
117
+ const { register, control, handleSubmit, watch, reset, getValues, setValue } = useForm({
118
+ defaultValues: PRESETS.gujarat
119
+ });
120
+
121
+ const { fields: sectionFields, append: appendSection, remove: removeSection } = useFieldArray({
122
+ control,
123
+ name: "footerSections"
124
+ });
125
+
126
+ const { fields: bottomLogoFields, append: appendBottomLogo, remove: removeBottomLogo } = useFieldArray({
127
+ control,
128
+ name: "bottomLogos"
129
+ });
130
+
131
+ const currentValues = watch();
132
+
133
+ useEffect(() => {
134
+ if (isResettingRef.current) {
135
+ isResettingRef.current = false;
136
+ return;
137
+ }
138
+ if (selectedPresetKey !== 'custom' && PRESETS[selectedPresetKey]) {
139
+ const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(PRESETS[selectedPresetKey]);
140
+ if (isDifferent) {
141
+ setSelectedPresetKey('custom');
142
+ }
143
+ }
144
+ }, [currentValues, selectedPresetKey]);
145
+
146
+ // Load preset helper
147
+ const handleLoadPreset = (key) => {
148
+ setSelectedPresetKey(key);
149
+ if (key !== 'custom') {
150
+ isResettingRef.current = true;
151
+ reset(PRESETS[key] || {});
152
+ showToast({ message: `Loaded ${PRESET_LABELS[key]} preset`, type: 'info' });
153
+ } else {
154
+ showToast({ message: `Switched to Custom preset mode`, type: 'info' });
155
+ }
156
+ };
157
+
158
+ const handleOpenPresetMenu = (event) => {
159
+ setPresetMenuAnchor(event.currentTarget);
160
+ };
161
+
162
+ const handleClosePresetMenu = () => {
163
+ setPresetMenuAnchor(null);
164
+ };
165
+
166
+ const handleSelectPresetToLoad = (presetKey) => {
167
+ isResettingRef.current = true;
168
+ reset(PRESETS[presetKey] || {});
169
+ showToast({ message: `Loaded values from ${PRESET_LABELS[presetKey]} into Custom Config`, type: 'success' });
170
+ handleClosePresetMenu();
171
+ };
172
+
173
+ // Load config from API on mount
174
+ useEffect(() => {
175
+ const fetchConfig = async () => {
176
+ try {
177
+ const response = await api.get('login/preference');
178
+ if (response?.data) {
179
+ let data = response.data;
180
+ // Handle cases where response might be wrapped in SystemConfiguration or custom DTO
181
+ if (data && typeof data === 'object') {
182
+ if ('keyValue' in data) {
183
+ data = data.keyValue;
184
+ } else if ('value' in data) {
185
+ data = data.value;
186
+ } else if ('preference' in data) {
187
+ data = data.preference;
188
+ }
189
+ }
190
+ if (typeof data === 'string') {
191
+ try {
192
+ data = JSON.parse(data);
193
+ } catch (err) {
194
+ console.error('Error parsing config string from API response', err);
195
+ }
196
+ }
197
+ if (data && typeof data === 'object') {
198
+ isResettingRef.current = true;
199
+ reset(data);
200
+ showToast({ message: 'Loaded configuration from server', type: 'success' });
201
+
202
+ // Check if the loaded data matches any of the available presets
203
+ const matchedKey = Object.keys(PRESETS).find(key => {
204
+ return JSON.stringify(PRESETS[key]) === JSON.stringify(data);
205
+ });
206
+ setSelectedPresetKey(matchedKey || 'custom');
207
+ return;
208
+ }
209
+ }
210
+ throw new Error('Invalid or empty config from server');
211
+ } catch (error) {
212
+ console.error('Failed to fetch login preference from API', error);
213
+
214
+ // Fallback to local storage if API fails or returns empty
215
+ const env = import.meta.env.VITE_APP_IMPLEMENTATION || 'gujarat';
216
+ const localKey = env === 'gujaratStaging' ? 'gujarat' : env;
217
+ const savedCustom = localStorage.getItem('medplat_login_config_custom');
218
+ const savedEnv = localStorage.getItem(`medplat_login_config_${localKey}`);
219
+ const saved = savedCustom || savedEnv;
220
+ if (saved) {
221
+ try {
222
+ const parsed = JSON.parse(saved);
223
+ isResettingRef.current = true;
224
+ reset(parsed);
225
+
226
+ const matchedKey = Object.keys(PRESETS).find(key => {
227
+ return JSON.stringify(PRESETS[key]) === JSON.stringify(parsed);
228
+ });
229
+ setSelectedPresetKey(matchedKey || 'custom');
230
+ showToast({ message: 'Loaded local configuration backup', type: 'info' });
231
+ } catch (e) {
232
+ console.error("Error reading initial local storage config", e);
233
+ }
234
+ } else if (PRESETS[localKey]) {
235
+ isResettingRef.current = true;
236
+ reset(PRESETS[localKey]);
237
+ setSelectedPresetKey(localKey);
238
+ }
239
+ }
240
+ };
241
+
242
+ fetchConfig();
243
+ }, [reset]);
244
+
245
+ // Save changes helper
246
+ const onSave = async (data) => {
247
+ try {
248
+ await api.post('login/set-preference', JSON.stringify(data));
249
+ localStorage.setItem(`medplat_login_config_${selectedPresetKey}`, JSON.stringify(data));
250
+ showToast({ message: 'Configuration saved successfully to server!', type: 'success' });
251
+ } catch (e) {
252
+ console.error('Failed to save to server', e);
253
+ localStorage.setItem(`medplat_login_config_${selectedPresetKey}`, JSON.stringify(data));
254
+ showToast({ message: 'Failed to save to server. Saved locally as backup.', type: 'warning' });
255
+ }
256
+ };
257
+
258
+ // Copy to clipboard helper
259
+ const handleCopyJSON = () => {
260
+ navigator.clipboard.writeText(JSON.stringify(currentValues, null, 2));
261
+ showToast({ message: 'JSON copied to clipboard', type: 'success' });
262
+ };
263
+
264
+ // Reset to static constant default
265
+ const handleResetToDefault = async () => {
266
+ const defaultData = PRESETS[selectedPresetKey] || PRESETS.gujarat;
267
+ isResettingRef.current = true;
268
+ reset(defaultData);
269
+ localStorage.removeItem(`medplat_login_config_${selectedPresetKey}`);
270
+ try {
271
+ await api.post('login/set-preference', JSON.stringify(defaultData));
272
+ showToast({ message: `Reset ${PRESET_LABELS[selectedPresetKey] || 'Custom'} to initial defaults on server`, type: 'info' });
273
+ } catch (e) {
274
+ console.error(e);
275
+ showToast({ message: `Reset locally. Failed to update server defaults.`, type: 'warning' });
276
+ }
277
+ };
278
+
279
+ // Helper to add sub-items
280
+ const addSubItem = (sectionIndex, fieldType) => {
281
+ const sections = getValues('footerSections');
282
+ const targetSection = sections[sectionIndex];
283
+ if (fieldType === 'buttons') {
284
+ const current = targetSection.buttons || [];
285
+ setValue(`footerSections.${sectionIndex}.buttons`, [...current, { icon: null, text: 'New Button', href: '#' }]);
286
+ } else if (fieldType === 'logos') {
287
+ const current = targetSection.logos || [];
288
+ setValue(`footerSections.${sectionIndex}.logos`, [...current, { image: 'medplat-highres.png', href: '#', alt: 'Logo' }]);
289
+ } else if (fieldType === 'logosWithText') {
290
+ const current = targetSection.items || [];
291
+ setValue(`footerSections.${sectionIndex}.items`, [...current, { image: 'medplat-highres.png', href: '#', alt: 'Logo', text: 'Supported text' }]);
292
+ }
293
+ };
294
+
295
+ // Helper to remove sub-items
296
+ const removeSubItem = (sectionIndex, fieldType, itemIndex) => {
297
+ const sections = getValues('footerSections');
298
+ const targetSection = sections[sectionIndex];
299
+ if (fieldType === 'buttons') {
300
+ const current = targetSection.buttons || [];
301
+ setValue(`footerSections.${sectionIndex}.buttons`, current.filter((_, i) => i !== itemIndex));
302
+ } else if (fieldType === 'logos') {
303
+ const current = targetSection.logos || [];
304
+ setValue(`footerSections.${sectionIndex}.logos`, current.filter((_, i) => i !== itemIndex));
305
+ } else if (fieldType === 'logosWithText') {
306
+ const current = targetSection.items || [];
307
+ setValue(`footerSections.${sectionIndex}.items`, current.filter((_, i) => i !== itemIndex));
308
+ }
309
+ };
310
+
311
+ return (
312
+ <Box className="min-h-screen bg-slate-50 p-6 font-sans">
313
+ {/* Header section */}
314
+ <Paper elevation={0} className="mb-6 p-6 rounded-2xl bg-gradient-to-r from-slate-900 to-indigo-950 text-white flex flex-col md:flex-row md:items-center justify-between gap-4 shadow-sm border border-slate-800">
315
+ <Box>
316
+ <Typography variant="h4" className="font-extrabold tracking-tight flex items-center gap-3">
317
+ <FontAwesomeIcon icon={faPalette} className="text-indigo-400" />
318
+ Login Configurator
319
+ </Typography>
320
+ <Typography variant="body2" className="text-slate-300 mt-1">
321
+ Customize the look, buttons, logos, and footer links of your portal login screen.
322
+ </Typography>
323
+ </Box>
324
+ <Box className="flex flex-wrap items-center gap-3">
325
+ <FormControl size="small" className="min-w-[180px] rounded-lg">
326
+ <InputLabel id="preset-select-label" style={{ color: 'white' }}>Preset Template</InputLabel>
327
+ <Select
328
+ labelId="preset-select-label"
329
+ value={selectedPresetKey}
330
+ label="Preset Template"
331
+ onChange={(e) => handleLoadPreset(e.target.value)}
332
+ style={{ color: 'white' }}
333
+ >
334
+ {Object.entries(PRESET_LABELS).map(([key, label]) => (
335
+ <MenuItem key={key} value={key}>{label}</MenuItem>
336
+ ))}
337
+ </Select>
338
+ </FormControl>
339
+ {selectedPresetKey === 'custom' && (
340
+ <>
341
+ <Button
342
+ variant="outlined"
343
+ onClick={handleOpenPresetMenu}
344
+ className="border-indigo-400 hover:border-indigo-300 text-indigo-200 normal-case rounded-xl font-medium"
345
+ startIcon={<FontAwesomeIcon icon={faDownload} />}
346
+ >
347
+ Load Preset
348
+ </Button>
349
+ <Menu
350
+ anchorEl={presetMenuAnchor}
351
+ open={Boolean(presetMenuAnchor)}
352
+ onClose={handleClosePresetMenu}
353
+ >
354
+ {Object.entries(PRESET_LABELS)
355
+ .filter(([key]) => key !== 'custom')
356
+ .map(([key, label]) => (
357
+ <MenuItem key={key} onClick={() => handleSelectPresetToLoad(key)}>
358
+ {label}
359
+ </MenuItem>
360
+ ))}
361
+ </Menu>
362
+ </>
363
+ )}
364
+ <Button
365
+ variant="outlined"
366
+ onClick={handleResetToDefault}
367
+ className="border-slate-500 hover:border-slate-300 text-slate-200 normal-case rounded-xl font-medium"
368
+ startIcon={<FontAwesomeIcon icon={faUndo} />}
369
+ >
370
+ Reset Default
371
+ </Button>
372
+ <Button
373
+ variant="contained"
374
+ onClick={handleSubmit(onSave)}
375
+ className="bg-indigo-600 hover:bg-indigo-500 text-white normal-case rounded-xl font-semibold shadow-md"
376
+ startIcon={<FontAwesomeIcon icon={faSave} />}
377
+ >
378
+ Save Changes
379
+ </Button>
380
+ </Box>
381
+ </Paper>
382
+
383
+ {/* Main Grid split editor & preview */}
384
+ <Grid container spacing={4}>
385
+ {/* Left column editor */}
386
+ <Grid item xs={12} lg={7}>
387
+ <Card className="rounded-2xl shadow-sm border border-slate-200 h-[calc(100vh-190px)] flex flex-col overflow-hidden">
388
+ <Box className="border-b border-slate-200 bg-white flex justify-between items-center px-4">
389
+ <Tabs value={activeTab} onChange={(e, val) => setActiveTab(val)}>
390
+ <Tab label="Form Editor" className="font-semibold text-slate-700 normal-case" />
391
+ <Tab label="Raw JSON View" className="font-semibold text-slate-700 normal-case" />
392
+ </Tabs>
393
+ {activeTab === 1 && (
394
+ <IconButton onClick={handleCopyJSON} color="primary" size="small" title="Copy JSON">
395
+ <FontAwesomeIcon icon={faCopy} />
396
+ </IconButton>
397
+ )}
398
+ </Box>
399
+
400
+ <Box className="flex-1 overflow-y-auto p-6 bg-slate-50/50">
401
+ {activeTab === 0 ? (
402
+ <form className="space-y-6">
403
+ {/* General Config Section */}
404
+ <Paper className="p-5 rounded-xl border border-slate-200/80 shadow-none space-y-4">
405
+ <Typography className="text-slate-800 font-bold text-lg border-b pb-2 flex items-center gap-2">
406
+ General Settings
407
+ </Typography>
408
+ <Grid container spacing={2}>
409
+ <Grid item xs={12} sm={6}>
410
+ <FormControlLabel
411
+ control={
412
+ <Switch
413
+ checked={!!currentValues.centerLogin}
414
+ onChange={(e) => setValue('centerLogin', e.target.checked)}
415
+ color="primary"
416
+ />
417
+ }
418
+ label="Center Login Card"
419
+ />
420
+ <Typography variant="caption" className="block text-slate-400 -mt-1">
421
+ If enabled, centers the login box. Otherwise, uses a split layout.
422
+ </Typography>
423
+ </Grid>
424
+ <Grid item xs={12} sm={6}>
425
+ <FormControlLabel
426
+ control={
427
+ <Switch
428
+ checked={!!currentValues.isDownloadLink}
429
+ onChange={(e) => setValue('isDownloadLink', e.target.checked)}
430
+ color="primary"
431
+ />
432
+ }
433
+ label="Show APK Download"
434
+ />
435
+ <Typography variant="caption" className="block text-slate-400 -mt-1">
436
+ Show the link to download the mobile application.
437
+ </Typography>
438
+ </Grid>
439
+ <Grid item xs={12} sm={6}>
440
+ <FormControlLabel
441
+ control={
442
+ <Switch
443
+ checked={!!currentValues.showLogoDivider}
444
+ onChange={(e) => setValue('showLogoDivider', e.target.checked)}
445
+ color="primary"
446
+ />
447
+ }
448
+ label="Show Logo Vertical Divider"
449
+ />
450
+ </Grid>
451
+ <Grid item xs={12} sm={6}>
452
+ <TextField
453
+ {...register('logoHeightClass')}
454
+ label="Logo Height Class"
455
+ placeholder="h-12 w-auto"
456
+ fullWidth
457
+ size="small"
458
+ InputLabelProps={{ shrink: true }}
459
+ />
460
+ </Grid>
461
+ <Grid item xs={12} sm={6}>
462
+ <TextField
463
+ {...register('inputFieldClass')}
464
+ label="Input Focus Tailwind Class"
465
+ placeholder="e.g. [&_.MuiOutlinedInput-root.Mui-focused_fieldset]:border-[#0A204A]"
466
+ fullWidth
467
+ size="small"
468
+ InputLabelProps={{ shrink: true }}
469
+ />
470
+ </Grid>
471
+ <Grid item xs={12} sm={6}>
472
+ <TextField
473
+ {...register('submitButtonClass')}
474
+ label="Button Bg Tailwind Class"
475
+ placeholder="e.g. bg-[#0A204A]"
476
+ fullWidth
477
+ size="small"
478
+ InputLabelProps={{ shrink: true }}
479
+ />
480
+ </Grid>
481
+ </Grid>
482
+ </Paper>
483
+
484
+ {/* Brand & Background Images */}
485
+ <Paper className="p-5 rounded-xl border border-slate-200/80 shadow-none space-y-4">
486
+ <Typography className="text-slate-800 font-bold text-lg border-b pb-2">
487
+ Images Configuration
488
+ </Typography>
489
+ <Grid container spacing={3}>
490
+ <Grid item xs={12} sm={6}>
491
+ <FormControl fullWidth size="small">
492
+ <InputLabel id="brand-logo-label">Brand Logo Image</InputLabel>
493
+ <Select
494
+ labelId="brand-logo-label"
495
+ label="Brand Logo Image"
496
+ value={currentValues.brandLogo || ''}
497
+ onChange={(e) => setValue('brandLogo', e.target.value)}
498
+ renderValue={(value) => (
499
+ <Box className="flex items-center gap-2">
500
+ <img src={imageMap[value]} alt="" className="h-6 w-auto object-contain bg-slate-100 p-0.5 rounded" />
501
+ <span>{value}</span>
502
+ </Box>
503
+ )}
504
+ >
505
+ {Object.keys(imageMap).map((key) => (
506
+ <MenuItem key={key} value={key} className="flex items-center gap-3">
507
+ <img src={imageMap[key]} alt="" className="h-6 w-12 object-contain bg-slate-100 p-0.5 rounded" />
508
+ <span>{key}</span>
509
+ </MenuItem>
510
+ ))}
511
+ </Select>
512
+ </FormControl>
513
+ </Grid>
514
+
515
+ <Grid item xs={12} sm={6}>
516
+ <FormControl fullWidth size="small">
517
+ <InputLabel id="bg-img-label">Split Background Image</InputLabel>
518
+ <Select
519
+ labelId="bg-img-label"
520
+ label="Split Background Image"
521
+ value={currentValues.implementationImage || ''}
522
+ onChange={(e) => setValue('implementationImage', e.target.value)}
523
+ renderValue={(value) => (
524
+ <Box className="flex items-center gap-2">
525
+ <img src={imageMap[value]} alt="" className="h-6 w-auto object-contain bg-slate-100 p-0.5 rounded" />
526
+ <span>{value}</span>
527
+ </Box>
528
+ )}
529
+ >
530
+ {Object.keys(imageMap).map((key) => (
531
+ <MenuItem key={key} value={key} className="flex items-center gap-3">
532
+ <img src={imageMap[key]} alt="" className="h-6 w-12 object-contain bg-slate-100 p-0.5 rounded" />
533
+ <span>{key}</span>
534
+ </MenuItem>
535
+ ))}
536
+ </Select>
537
+ </FormControl>
538
+ </Grid>
539
+ </Grid>
540
+ </Paper>
541
+
542
+ {/* Footer Sections */}
543
+ <Paper className="p-5 rounded-xl border border-slate-200/80 shadow-none space-y-4">
544
+ <Box className="flex justify-between items-center border-b pb-2">
545
+ <Typography className="text-slate-800 font-bold text-lg">
546
+ Footer Sections
547
+ </Typography>
548
+ <Button
549
+ size="small"
550
+ variant="outlined"
551
+ onClick={() => appendSection({ type: 'logos', logos: [] })}
552
+ startIcon={<FontAwesomeIcon icon={faPlus} />}
553
+ className="normal-case rounded-lg"
554
+ >
555
+ Add Section
556
+ </Button>
557
+ </Box>
558
+
559
+ {sectionFields.length === 0 ? (
560
+ <Box className="text-center py-6 text-slate-400">
561
+ No footer sections configured. Click "Add Section" to create one.
562
+ </Box>
563
+ ) : (
564
+ <div className="space-y-4">
565
+ {sectionFields.map((field, idx) => {
566
+ const sectionType = watch(`footerSections.${idx}.type`);
567
+ return (
568
+ <Card key={field.id} className="p-4 border border-slate-200 shadow-none bg-white rounded-xl space-y-4 relative">
569
+ <Box className="flex justify-between items-center">
570
+ <FormControl size="small" className="w-[180px]">
571
+ <InputLabel>Section Type</InputLabel>
572
+ <Select
573
+ label="Section Type"
574
+ value={sectionType || 'logos'}
575
+ onChange={(e) => {
576
+ const type = e.target.value;
577
+ setValue(`footerSections.${idx}.type`, type);
578
+ if (type === 'buttons') {
579
+ setValue(`footerSections.${idx}.buttons`, []);
580
+ } else if (type === 'logos') {
581
+ setValue(`footerSections.${idx}.logos`, []);
582
+ } else if (type === 'textWithLogo') {
583
+ setValue(`footerSections.${idx}.text`, 'Funded By');
584
+ setValue(`footerSections.${idx}.image', 'medplat-highres.png`);
585
+ setValue(`footerSections.${idx}.href`, '#');
586
+ setValue(`footerSections.${idx}.alt`, '');
587
+ } else if (type === 'logosWithText') {
588
+ setValue(`footerSections.${idx}.items`, []);
589
+ }
590
+ }}
591
+ >
592
+ <MenuItem value="buttons">Quick Link Buttons</MenuItem>
593
+ <MenuItem value="logos">Partners Logos</MenuItem>
594
+ <MenuItem value="textWithLogo">Single Text & Logo</MenuItem>
595
+ <MenuItem value="logosWithText">Logos side-by-side with Text</MenuItem>
596
+ </Select>
597
+ </FormControl>
598
+ <IconButton
599
+ size="small"
600
+ color="error"
601
+ onClick={() => removeSection(idx)}
602
+ >
603
+ <FontAwesomeIcon icon={faTrash} />
604
+ </IconButton>
605
+ </Box>
606
+
607
+ <Divider />
608
+
609
+ {/* Rendering section items depending on type */}
610
+ {sectionType === 'buttons' && (
611
+ <div className="space-y-3">
612
+ <Box className="flex justify-between items-center">
613
+ <Typography variant="subtitle2" className="text-slate-600 font-bold">Buttons list</Typography>
614
+ <Button
615
+ size="small"
616
+ variant="outlined"
617
+ color="primary"
618
+ onClick={() => addSubItem(idx, 'buttons')}
619
+ startIcon={<FontAwesomeIcon icon={faPlus} />}
620
+ className="normal-case rounded-lg"
621
+ >
622
+ Add Button
623
+ </Button>
624
+ </Box>
625
+ {(watch(`footerSections.${idx}.buttons`) || []).map((btn, btnIdx) => (
626
+ <Box key={btnIdx} className="flex flex-wrap items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
627
+ <FormControl size="small" className="w-[120px]">
628
+ <InputLabel>Icon</InputLabel>
629
+ <Select
630
+ label="Icon"
631
+ value={btn.icon || ''}
632
+ onChange={(e) => setValue(`footerSections.${idx}.buttons.${btnIdx}.icon`, e.target.value || null)}
633
+ >
634
+ <MenuItem value="">None</MenuItem>
635
+ <MenuItem value="PERSON">Person</MenuItem>
636
+ <MenuItem value="MEDICAL">Medical</MenuItem>
637
+ <MenuItem value="PASSPORT">Passport</MenuItem>
638
+ </Select>
639
+ </FormControl>
640
+ <TextField
641
+ {...register(`footerSections.${idx}.buttons.${btnIdx}.text`)}
642
+ label="Button Text"
643
+ size="small"
644
+ className="flex-1 min-w-[140px]"
645
+ InputLabelProps={{ shrink: true }}
646
+ />
647
+ <TextField
648
+ {...register(`footerSections.${idx}.buttons.${btnIdx}.href`)}
649
+ label="Link (Href)"
650
+ size="small"
651
+ className="flex-1 min-w-[180px]"
652
+ InputLabelProps={{ shrink: true }}
653
+ />
654
+ <IconButton size="small" color="error" onClick={() => removeSubItem(idx, 'buttons', btnIdx)}>
655
+ <FontAwesomeIcon icon={faTrash} />
656
+ </IconButton>
657
+ </Box>
658
+ ))}
659
+ </div>
660
+ )}
661
+
662
+ {sectionType === 'logos' && (
663
+ <div className="space-y-3">
664
+ <Box className="flex justify-between items-center">
665
+ <Typography variant="subtitle2" className="text-slate-600 font-bold">Logos list</Typography>
666
+ <Button
667
+ size="small"
668
+ variant="outlined"
669
+ color="primary"
670
+ onClick={() => addSubItem(idx, 'logos')}
671
+ startIcon={<FontAwesomeIcon icon={faPlus} />}
672
+ className="normal-case rounded-lg"
673
+ >
674
+ Add Logo
675
+ </Button>
676
+ </Box>
677
+ {(watch(`footerSections.${idx}.logos`) || []).map((logo, logoIdx) => (
678
+ <Box key={logoIdx} className="flex flex-wrap items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
679
+ <FormControl size="small" className="w-[180px]">
680
+ <InputLabel>Logo Image</InputLabel>
681
+ <Select
682
+ label="Logo Image"
683
+ value={logo.image || ''}
684
+ onChange={(e) => setValue(`footerSections.${idx}.logos.${logoIdx}.image`, e.target.value)}
685
+ >
686
+ {Object.keys(imageMap).map((key) => (
687
+ <MenuItem key={key} value={key}>{key}</MenuItem>
688
+ ))}
689
+ </Select>
690
+ </FormControl>
691
+ <TextField
692
+ {...register(`footerSections.${idx}.logos.${logoIdx}.href`)}
693
+ label="Link (Href)"
694
+ size="small"
695
+ className="flex-1 min-w-[140px]"
696
+ InputLabelProps={{ shrink: true }}
697
+ />
698
+ <TextField
699
+ {...register(`footerSections.${idx}.logos.${logoIdx}.alt`)}
700
+ label="Alt Tag"
701
+ size="small"
702
+ className="flex-1 min-w-[120px]"
703
+ InputLabelProps={{ shrink: true }}
704
+ />
705
+ <IconButton size="small" color="error" onClick={() => removeSubItem(idx, 'logos', logoIdx)}>
706
+ <FontAwesomeIcon icon={faTrash} />
707
+ </IconButton>
708
+ </Box>
709
+ ))}
710
+ </div>
711
+ )}
712
+
713
+ {sectionType === 'textWithLogo' && (
714
+ <Grid container spacing={2}>
715
+ <Grid item xs={12} sm={6}>
716
+ <TextField
717
+ {...register(`footerSections.${idx}.text`)}
718
+ label="Leading Text"
719
+ size="small"
720
+ fullWidth
721
+ InputLabelProps={{ shrink: true }}
722
+ />
723
+ </Grid>
724
+ <Grid item xs={12} sm={6}>
725
+ <FormControl fullWidth size="small">
726
+ <InputLabel>Logo Image</InputLabel>
727
+ <Select
728
+ label="Logo Image"
729
+ value={watch(`footerSections.${idx}.image`) || ''}
730
+ onChange={(e) => setValue(`footerSections.${idx}.image`, e.target.value)}
731
+ >
732
+ {Object.keys(imageMap).map((key) => (
733
+ <MenuItem key={key} value={key}>{key}</MenuItem>
734
+ ))}
735
+ </Select>
736
+ </FormControl>
737
+ </Grid>
738
+ <Grid item xs={12} sm={6}>
739
+ <TextField
740
+ {...register(`footerSections.${idx}.href`)}
741
+ label="Link (Href)"
742
+ size="small"
743
+ fullWidth
744
+ InputLabelProps={{ shrink: true }}
745
+ />
746
+ </Grid>
747
+ <Grid item xs={12} sm={6}>
748
+ <TextField
749
+ label="Alt Tag"
750
+ size="small"
751
+ fullWidth
752
+ value={watch(`footerSections.${idx}.alt`) || ''}
753
+ onChange={(e) => setValue(`footerSections.${idx}.alt`, e.target.value)}
754
+ />
755
+ </Grid>
756
+ </Grid>
757
+ )}
758
+
759
+ {sectionType === 'logosWithText' && (
760
+ <div className="space-y-3">
761
+ <Box className="flex justify-between items-center">
762
+ <Typography variant="subtitle2" className="text-slate-600 font-bold">Items list</Typography>
763
+ <Button
764
+ size="small"
765
+ variant="outlined"
766
+ color="primary"
767
+ onClick={() => addSubItem(idx, 'logosWithText')}
768
+ startIcon={<FontAwesomeIcon icon={faPlus} />}
769
+ className="normal-case rounded-lg"
770
+ >
771
+ Add Item
772
+ </Button>
773
+ </Box>
774
+ {(watch(`footerSections.${idx}.items`) || []).map((item, itemIdx) => (
775
+ <Box key={itemIdx} className="flex flex-wrap items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
776
+ <FormControl size="small" className="w-[180px]">
777
+ <InputLabel>Image</InputLabel>
778
+ <Select
779
+ label="Image"
780
+ value={item.image || ''}
781
+ onChange={(e) => setValue(`footerSections.${idx}.items.${itemIdx}.image`, e.target.value)}
782
+ >
783
+ {Object.keys(imageMap).map((key) => (
784
+ <MenuItem key={key} value={key}>{key}</MenuItem>
785
+ ))}
786
+ </Select>
787
+ </FormControl>
788
+ <TextField
789
+ label="Label Text"
790
+ size="small"
791
+ className="flex-1 min-w-[120px]"
792
+ value={item.text || ''}
793
+ onChange={(e) => setValue(`footerSections.${idx}.items.${itemIdx}.text`, e.target.value)}
794
+ />
795
+ <TextField
796
+ label="Link (Href)"
797
+ size="small"
798
+ className="flex-1 min-w-[120px]"
799
+ value={item.href || ''}
800
+ onChange={(e) => setValue(`footerSections.${idx}.items.${itemIdx}.href`, e.target.value)}
801
+ />
802
+ <TextField
803
+ label="Alt Tag"
804
+ size="small"
805
+ className="flex-1 min-w-[100px]"
806
+ value={item.alt || ''}
807
+ onChange={(e) => setValue(`footerSections.${idx}.items.${itemIdx}.alt`, e.target.value)}
808
+ />
809
+ <IconButton size="small" color="error" onClick={() => removeSubItem(idx, 'logosWithText', itemIdx)}>
810
+ <FontAwesomeIcon icon={faTrash} />
811
+ </IconButton>
812
+ </Box>
813
+ ))}
814
+ </div>
815
+ )}
816
+ </Card>
817
+ );
818
+ })}
819
+ </div>
820
+ )}
821
+ </Paper>
822
+
823
+ {/* Bottom Logos */}
824
+ <Paper className="p-5 rounded-xl border border-slate-200/80 shadow-none space-y-4">
825
+ <Box className="flex justify-between items-center border-b pb-2">
826
+ <Typography className="text-slate-800 font-bold text-lg">
827
+ Bottom Logos (Below divider)
828
+ </Typography>
829
+ <Button
830
+ size="small"
831
+ variant="outlined"
832
+ onClick={() => appendBottomLogo({ image: 'medplat-highres.png', href: '#', alt: 'Medplat Logo' })}
833
+ startIcon={<FontAwesomeIcon icon={faPlus} />}
834
+ className="normal-case rounded-lg"
835
+ >
836
+ Add Bottom Logo
837
+ </Button>
838
+ </Box>
839
+
840
+ {bottomLogoFields.length === 0 ? (
841
+ <Box className="text-center py-6 text-slate-400">
842
+ No bottom logos. Click "Add Bottom Logo" to create one.
843
+ </Box>
844
+ ) : (
845
+ <div className="space-y-3">
846
+ {bottomLogoFields.map((field, idx) => (
847
+ <Box key={field.id} className="flex flex-wrap items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
848
+ <FormControl size="small" className="w-[180px]">
849
+ <InputLabel>Image</InputLabel>
850
+ <Select
851
+ label="Image"
852
+ value={watch(`bottomLogos.${idx}.image`) || ''}
853
+ onChange={(e) => setValue(`bottomLogos.${idx}.image`, e.target.value)}
854
+ >
855
+ {Object.keys(imageMap).map((key) => (
856
+ <MenuItem key={key} value={key}>{key}</MenuItem>
857
+ ))}
858
+ </Select>
859
+ </FormControl>
860
+ <TextField
861
+ label="Link (Href)"
862
+ size="small"
863
+ className="flex-1 min-w-[140px]"
864
+ value={watch(`bottomLogos.${idx}.href`) || ''}
865
+ onChange={(e) => setValue(`bottomLogos.${idx}.href`, e.target.value)}
866
+ />
867
+ <TextField
868
+ label="Alt Tag"
869
+ size="small"
870
+ className="flex-1 min-w-[120px]"
871
+ value={watch(`bottomLogos.${idx}.alt`) || ''}
872
+ onChange={(e) => setValue(`bottomLogos.${idx}.alt`, e.target.value)}
873
+ />
874
+ <IconButton size="small" color="error" onClick={() => removeBottomLogo(idx)}>
875
+ <FontAwesomeIcon icon={faTrash} />
876
+ </IconButton>
877
+ </Box>
878
+ ))}
879
+ </div>
880
+ )}
881
+ </Paper>
882
+ </form>
883
+ ) : (
884
+ /* Raw JSON View */
885
+ <Box className="h-full flex flex-col">
886
+ <Box className="flex-1 bg-slate-900 text-slate-200 p-4 rounded-xl font-mono text-sm overflow-auto max-h-[500px]">
887
+ <pre>{JSON.stringify(currentValues, null, 2)}</pre>
888
+ </Box>
889
+ <Typography variant="caption" className="text-slate-400 mt-2 block">
890
+ Copy this JSON structure to paste inside `app.constant.js` for hardcoding deployment values.
891
+ </Typography>
892
+ </Box>
893
+ )}
894
+ </Box>
895
+ </Card>
896
+ </Grid>
897
+
898
+ {/* Right column live preview */}
899
+ <Grid item xs={12} lg={5}>
900
+ <Card className="rounded-2xl shadow-sm border border-slate-200 h-[calc(100vh-190px)] flex flex-col overflow-hidden bg-slate-100">
901
+ <Box className="border-b border-slate-200 bg-white flex justify-between items-center px-4 py-2">
902
+ <Typography className="text-slate-700 font-extrabold text-sm tracking-wider flex items-center gap-2">
903
+ <FontAwesomeIcon icon={faEye} className="text-indigo-600" />
904
+ Live Preview
905
+ </Typography>
906
+ <Box className="flex gap-2">
907
+ <Button
908
+ size="small"
909
+ variant={previewDevice === 'desktop' ? 'contained' : 'outlined'}
910
+ onClick={() => setPreviewDevice('desktop')}
911
+ className={`rounded-lg py-1 px-3 normal-case ${previewDevice === 'desktop' ? 'bg-indigo-600 text-white' : 'text-indigo-600 border-indigo-200'}`}
912
+ startIcon={<FontAwesomeIcon icon={faDesktop} />}
913
+ >
914
+ Desktop
915
+ </Button>
916
+ <Button
917
+ size="small"
918
+ variant={previewDevice === 'mobile' ? 'contained' : 'outlined'}
919
+ onClick={() => setPreviewDevice('mobile')}
920
+ className={`rounded-lg py-1 px-3 normal-case ${previewDevice === 'mobile' ? 'bg-indigo-600 text-white' : 'text-indigo-600 border-indigo-200'}`}
921
+ startIcon={<FontAwesomeIcon icon={faMobileAlt} />}
922
+ >
923
+ Mobile
924
+ </Button>
925
+ </Box>
926
+ </Box>
927
+
928
+ {/* Container for simulation mockup */}
929
+ <Box className="flex-1 flex items-center justify-center p-4 overflow-hidden bg-slate-100/50">
930
+ <Box
931
+ ref={containerRef}
932
+ className="transition-all duration-300 bg-slate-200 rounded-2xl overflow-hidden border-4 border-slate-300 shadow-lg"
933
+ style={{
934
+ width: targetWidth * scale,
935
+ height: targetHeight * scale,
936
+ position: 'relative',
937
+ }}
938
+ >
939
+ {/* Simulated Screen */}
940
+ <Box
941
+ className="bg-slate-100 select-none"
942
+ style={{
943
+ width: targetWidth,
944
+ height: targetHeight,
945
+ transform: `scale(${scale})`,
946
+ transformOrigin: 'top left',
947
+ position: 'absolute',
948
+ top: 0,
949
+ left: 0,
950
+ fontSize: '0.8rem',
951
+ }}
952
+ >
953
+ {currentValues.centerLogin || previewDevice === 'mobile' ? (
954
+ /* Centered Card Layout */
955
+ <Box className="w-full h-full flex items-center justify-center bg-slate-50 p-6 overflow-y-auto">
956
+ <Paper className="rounded-xl p-5 w-full max-w-sm shadow-md border border-slate-200">
957
+ {renderPreviewForm(currentValues)}
958
+ </Paper>
959
+ </Box>
960
+ ) : (
961
+ /* Split Layout */
962
+ <Box className="w-full h-full flex overflow-hidden">
963
+ {/* Left image */}
964
+ <Box className="flex-1 bg-slate-300 relative overflow-hidden">
965
+ <img
966
+ src={imageMap[currentValues.implementationImage]}
967
+ alt=""
968
+ className="w-full h-full object-cover"
969
+ />
970
+ </Box>
971
+ {/* Right form panel */}
972
+ <Box className="w-1/2 bg-slate-50 overflow-y-auto p-6 flex flex-col justify-center">
973
+ {renderPreviewForm(currentValues)}
974
+ </Box>
975
+ </Box>
976
+ )}
977
+ </Box>
978
+ </Box>
979
+ </Box>
980
+ </Card>
981
+ </Grid>
982
+ </Grid>
983
+ </Box>
984
+ );
985
+ }
986
+
987
+ // Function to render mockup preview forms
988
+ function renderPreviewForm(config) {
989
+ return (
990
+ <Box className="flex flex-col items-center w-full">
991
+ {/* Brand logo */}
992
+ {config.brandLogo && (
993
+ <img
994
+ src={imageMap[config.brandLogo]}
995
+ alt="Brand Logo"
996
+ className="h-10 w-auto mb-3 object-contain"
997
+ />
998
+ )}
999
+
1000
+ {/* Divider */}
1001
+ <Box className="flex items-center w-full my-2">
1002
+ <Divider className="flex-1" />
1003
+ <Typography className="px-2 text-xs font-semibold text-slate-500">
1004
+ LOGIN
1005
+ </Typography>
1006
+ <Divider className="flex-1" />
1007
+ </Box>
1008
+
1009
+ {/* Fields */}
1010
+ <Box className="w-full space-y-2 mt-2">
1011
+ <TextField
1012
+ disabled
1013
+ value="admin"
1014
+ size="small"
1015
+ fullWidth
1016
+ className={config.inputFieldClass}
1017
+ variant="outlined"
1018
+ label="Username"
1019
+ InputProps={{ style: { fontSize: '0.8rem' } }}
1020
+ />
1021
+ <TextField
1022
+ disabled
1023
+ type="password"
1024
+ value="••••••••"
1025
+ size="small"
1026
+ fullWidth
1027
+ className={config.inputFieldClass}
1028
+ variant="outlined"
1029
+ label="Password"
1030
+ InputProps={{ style: { fontSize: '0.8rem' } }}
1031
+ />
1032
+ </Box>
1033
+
1034
+ {/* Login Button */}
1035
+ <Button
1036
+ disabled
1037
+ variant="contained"
1038
+ fullWidth
1039
+ className={`mt-4 py-1.5 text-xs font-semibold rounded-full text-white shadow-none ${config.submitButtonClass || 'bg-blue-600'}`}
1040
+ style={{ pointerEvents: 'none' }}
1041
+ >
1042
+ LOGIN
1043
+ </Button>
1044
+
1045
+ {/* Forgot password */}
1046
+ <Typography variant="caption" className="text-blue-500 mt-2 hover:underline cursor-pointer">
1047
+ Forgot Password?
1048
+ </Typography>
1049
+
1050
+ {/* Download app link */}
1051
+ {config.isDownloadLink && (
1052
+ <Typography variant="caption" className="text-blue-500 mt-1 hover:underline cursor-pointer flex items-center gap-1 font-medium">
1053
+ <span className="text-red-500 font-bold">(New)</span>
1054
+ <FontAwesomeIcon icon={faDownload} size="xs" />
1055
+ Download Mobile App
1056
+ </Typography>
1057
+ )}
1058
+
1059
+ {/* Footer sections */}
1060
+ {config.footerSections && config.footerSections.length > 0 && (
1061
+ <Box className="w-full mt-4 space-y-3">
1062
+ {config.footerSections.map((section, idx) => {
1063
+ const isButtons = section.type === 'buttons';
1064
+ const showDivider = idx === 0 || (idx > 0 && config.footerSections[idx - 1].type !== 'buttons');
1065
+
1066
+ return (
1067
+ <Box key={idx} className="w-full flex flex-col items-center">
1068
+ {showDivider && <Divider className="w-full my-1 opacity-60" />}
1069
+
1070
+ {section.type === 'buttons' && (
1071
+ <Box className="flex justify-between gap-2 mt-2 w-full">
1072
+ {(section.buttons || []).map((btn, btnIdx) => {
1073
+ const icon = iconMap[btn.icon];
1074
+ return (
1075
+ <Box
1076
+ key={btnIdx}
1077
+ className="flex-1 flex flex-col items-center justify-center py-2 bg-slate-100 rounded-xl border border-slate-200 cursor-pointer"
1078
+ >
1079
+ {icon && <FontAwesomeIcon icon={icon} className="text-[#00685b] mb-1" />}
1080
+ <Typography style={{ fontSize: '0.65rem' }} className="font-extrabold text-[#00685b] uppercase text-center tracking-tight">
1081
+ {btn.text}
1082
+ </Typography>
1083
+ </Box>
1084
+ );
1085
+ })}
1086
+ </Box>
1087
+ )}
1088
+
1089
+ {section.type === 'logos' && (
1090
+ <Box className="w-full mt-2 flex items-center justify-center gap-3">
1091
+ {(section.logos || []).map((logo, logoIdx) => {
1092
+ const isMedplat = logo.image === 'medplat-highres.png';
1093
+ const showDividerFlag = isMedplat && logoIdx > 0 && section.logos[logoIdx - 1].image !== 'medplat-highres.png' && config.showLogoDivider;
1094
+ return (
1095
+ <React.Fragment key={logoIdx}>
1096
+ {showDividerFlag && <Divider orientation="vertical" flexItem className="h-4 mx-0.5" />}
1097
+ <img
1098
+ src={imageMap[logo.image]}
1099
+ alt={logo.alt}
1100
+ className={config.logoHeightClass || 'h-6 w-auto object-contain'}
1101
+ />
1102
+ </React.Fragment>
1103
+ );
1104
+ })}
1105
+ </Box>
1106
+ )}
1107
+
1108
+ {section.type === 'textWithLogo' && (
1109
+ <Box className="w-full mt-2 flex items-center justify-center gap-1">
1110
+ <span className="text-[10px] text-slate-500 font-medium">{section.text}</span>
1111
+ <img src={imageMap[section.image]} alt={section.alt} className="h-6 w-auto object-contain" />
1112
+ </Box>
1113
+ )}
1114
+
1115
+ {section.type === 'logosWithText' && (
1116
+ <Box className="w-full mt-2 flex items-center justify-center gap-2">
1117
+ {(section.items || []).map((item, itemIdx) => (
1118
+ <Box key={itemIdx} className="flex items-center gap-1">
1119
+ <img src={imageMap[item.image]} alt={item.alt} className="h-5 w-auto object-contain" />
1120
+ {item.text && <span style={{ fontSize: '7px' }} className="text-slate-500">{item.text}</span>}
1121
+ </Box>
1122
+ ))}
1123
+ </Box>
1124
+ )}
1125
+ </Box>
1126
+ );
1127
+ })}
1128
+ </Box>
1129
+ )}
1130
+
1131
+ {/* Bottom Logos */}
1132
+ {config.bottomLogos && config.bottomLogos.length > 0 && (
1133
+ <Box className="w-full mt-3 flex flex-col items-center">
1134
+ <Divider className="w-full my-1" />
1135
+ <Box className="flex items-center gap-2 mt-1">
1136
+ {config.bottomLogos.map((logo, idx) => (
1137
+ <img
1138
+ key={idx}
1139
+ src={imageMap[logo.image]}
1140
+ alt={logo.alt}
1141
+ className="h-6 w-auto object-contain"
1142
+ />
1143
+ ))}
1144
+ </Box>
1145
+ </Box>
1146
+ )}
1147
+ </Box>
1148
+ );
1149
+ }