@argusoft/medplat-app-shell 1.0.6 → 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,801 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+ import * as monaco from 'monaco-editor';
3
+ import { execute } from '@/common/services/QueryBuilder';
4
+ import useCaching from '@/hooks/useCaching';
5
+ import PropTypes from 'prop-types';
6
+
7
+ // ─── Alias generation ────────────────────────────────────────────────────────
8
+
9
+ const PG_KEYWORDS = new Set([
10
+ 'all',
11
+ 'analyse',
12
+ 'analyze',
13
+ 'and',
14
+ 'any',
15
+ 'array',
16
+ 'as',
17
+ 'asc',
18
+ 'asymmetric',
19
+ 'authorization',
20
+ 'between',
21
+ 'binary',
22
+ 'both',
23
+ 'case',
24
+ 'cast',
25
+ 'check',
26
+ 'collate',
27
+ 'column',
28
+ 'constraint',
29
+ 'create',
30
+ 'cross',
31
+ 'current_date',
32
+ 'current_role',
33
+ 'current_time',
34
+ 'current_timestamp',
35
+ 'current_user',
36
+ 'default',
37
+ 'deferrable',
38
+ 'desc',
39
+ 'distinct',
40
+ 'do',
41
+ 'else',
42
+ 'end',
43
+ 'except',
44
+ 'false',
45
+ 'fetch',
46
+ 'for',
47
+ 'foreign',
48
+ 'from',
49
+ 'grant',
50
+ 'group',
51
+ 'having',
52
+ 'in',
53
+ 'initially',
54
+ 'intersect',
55
+ 'into',
56
+ 'join',
57
+ 'leading',
58
+ 'limit',
59
+ 'localtime',
60
+ 'localtimestamp',
61
+ 'natural',
62
+ 'not',
63
+ 'null',
64
+ 'offset',
65
+ 'on',
66
+ 'only',
67
+ 'or',
68
+ 'order',
69
+ 'placing',
70
+ 'primary',
71
+ 'references',
72
+ 'returning',
73
+ 'select',
74
+ 'session_user',
75
+ 'some',
76
+ 'symmetric',
77
+ 'table',
78
+ 'then',
79
+ 'to',
80
+ 'trailing',
81
+ 'true',
82
+ 'union',
83
+ 'unique',
84
+ 'user',
85
+ 'using',
86
+ 'variadic',
87
+ 'verbose',
88
+ 'when',
89
+ 'where',
90
+ 'window',
91
+ 'with',
92
+ 'if',
93
+ 'abs',
94
+ 'avg',
95
+ 'ceiling',
96
+ 'char_length',
97
+ 'coalesce',
98
+ 'concat',
99
+ 'count',
100
+ 'date_part',
101
+ 'date_trunc',
102
+ 'decode',
103
+ 'encode',
104
+ 'exp',
105
+ 'floor',
106
+ 'initcap',
107
+ 'length',
108
+ 'lower',
109
+ 'lpad',
110
+ 'ltrim',
111
+ 'max',
112
+ 'min',
113
+ 'mod',
114
+ 'now',
115
+ 'nullif',
116
+ 'position',
117
+ 'power',
118
+ 'random',
119
+ 'repeat',
120
+ 'replace',
121
+ 'reverse',
122
+ 'round',
123
+ 'row_number',
124
+ 'sign',
125
+ 'sin',
126
+ 'split_part',
127
+ 'sqrt',
128
+ 'strpos',
129
+ 'substring',
130
+ 'sum',
131
+ 'to_char',
132
+ 'to_date',
133
+ 'to_timestamp',
134
+ 'translate',
135
+ 'trim',
136
+ 'upper',
137
+ ]);
138
+
139
+ const ALIAS_STRIP_SUFFIXES = new Set(['rel', 'ref', 'map']);
140
+
141
+ function getAlias(str) {
142
+ if (typeof str !== 'string') return '';
143
+ const parts = str.split('_');
144
+ if (ALIAS_STRIP_SUFFIXES.has(parts[parts.length - 1].toLowerCase())) parts.pop();
145
+ let alias = parts
146
+ .map((p) => p[0])
147
+ .join('')
148
+ .toLowerCase();
149
+ if (PG_KEYWORDS.has(alias)) alias += '2';
150
+ return alias;
151
+ }
152
+
153
+ // ─── Static completion lists ──────────────────────────────────────────────────
154
+
155
+ const KEYWORDS = () =>
156
+ [
157
+ { label: 'SELECT', insertText: 'SELECT ', documentation: 'Retrieve rows from a table or view.' },
158
+ { label: 'FROM', insertText: 'FROM ', documentation: 'Specify the source table.' },
159
+ { label: 'WHERE', insertText: 'WHERE ', documentation: 'Filter rows by condition.' },
160
+ { label: 'AND', insertText: 'AND ', documentation: 'Logical AND.' },
161
+ { label: 'OR', insertText: 'OR ', documentation: 'Logical OR.' },
162
+ { label: 'NOT', insertText: 'NOT ', documentation: 'Logical NOT.' },
163
+ { label: 'IN', insertText: 'IN ()', documentation: 'Match any value in a list.' },
164
+ { label: 'NOT IN', insertText: 'NOT IN ()', documentation: 'Exclude values in a list.' },
165
+ { label: 'IS NULL', insertText: 'IS NULL', documentation: 'Test for NULL.' },
166
+ { label: 'IS NOT NULL', insertText: 'IS NOT NULL', documentation: 'Test for non-NULL.' },
167
+ { label: 'LIKE', insertText: "LIKE '%$1%'", documentation: 'Pattern match with wildcards.' },
168
+ { label: 'ILIKE', insertText: "ILIKE '%$1%'", documentation: 'Case-insensitive pattern match.' },
169
+ { label: 'BETWEEN', insertText: 'BETWEEN $1 AND $2', documentation: 'Range test.' },
170
+ { label: 'EXISTS', insertText: 'EXISTS (SELECT 1 FROM $1)', documentation: 'Subquery existence test.' },
171
+ { label: 'GROUP BY', insertText: 'GROUP BY ', documentation: 'Group rows sharing a property.' },
172
+ { label: 'ORDER BY', insertText: 'ORDER BY ', documentation: 'Sort result rows.' },
173
+ { label: 'ASC', insertText: 'ASC', documentation: 'Ascending sort order.' },
174
+ { label: 'DESC', insertText: 'DESC', documentation: 'Descending sort order.' },
175
+ { label: 'LIMIT', insertText: 'LIMIT ', documentation: 'Maximum number of rows to return.' },
176
+ { label: 'OFFSET', insertText: 'OFFSET ', documentation: 'Skip this many rows.' },
177
+ { label: 'HAVING', insertText: 'HAVING ', documentation: 'Filter grouped data.' },
178
+ { label: 'DISTINCT', insertText: 'DISTINCT ', documentation: 'Return only unique rows.' },
179
+ { label: 'JOIN', insertText: 'JOIN $1 ON $2', documentation: 'Inner join.' },
180
+ { label: 'LEFT JOIN', insertText: 'LEFT JOIN $1 ON $2', documentation: 'Left outer join.' },
181
+ { label: 'RIGHT JOIN', insertText: 'RIGHT JOIN $1 ON $2', documentation: 'Right outer join.' },
182
+ { label: 'FULL JOIN', insertText: 'FULL JOIN $1 ON $2', documentation: 'Full outer join.' },
183
+ { label: 'CROSS JOIN', insertText: 'CROSS JOIN $1', documentation: 'Cartesian product.' },
184
+ { label: 'UNION', insertText: 'UNION\n', documentation: 'Combine result sets, remove duplicates.' },
185
+ { label: 'UNION ALL', insertText: 'UNION ALL\n', documentation: 'Combine result sets, keep duplicates.' },
186
+ { label: 'EXCEPT', insertText: 'EXCEPT\n', documentation: 'Rows in first but not second set.' },
187
+ { label: 'INTERSECT', insertText: 'INTERSECT\n', documentation: 'Rows in both sets.' },
188
+ { label: 'RETURNING', insertText: 'RETURNING ', documentation: 'Return modified rows.' },
189
+ { label: 'UPDATE', insertText: 'UPDATE $1 SET $2 WHERE $3;', documentation: 'Modify existing rows.' },
190
+ { label: 'DELETE', insertText: 'DELETE FROM $1 WHERE $2;', documentation: 'Remove rows from a table.' },
191
+ { label: 'INSERT INTO', insertText: 'INSERT INTO $1 ($2)\nVALUES ($3);', documentation: 'Add new rows.' },
192
+ { label: 'SERIAL', insertText: 'SERIAL', documentation: 'Auto-incrementing integer.' },
193
+ { label: 'BIGSERIAL', insertText: 'BIGSERIAL', documentation: 'Auto-incrementing big integer.' },
194
+ { label: 'TEXT', insertText: 'TEXT', documentation: 'Variable-length text.' },
195
+ { label: 'INTEGER', insertText: 'INTEGER', documentation: '4-byte integer.' },
196
+ { label: 'BIGINT', insertText: 'BIGINT', documentation: '8-byte integer.' },
197
+ { label: 'BOOLEAN', insertText: 'BOOLEAN', documentation: 'True/false value.' },
198
+ { label: 'JSONB', insertText: 'JSONB', documentation: 'Binary JSON.' },
199
+ { label: 'ARRAY', insertText: 'ARRAY', documentation: 'PostgreSQL array type.' },
200
+ { label: 'TIMESTAMP', insertText: 'TIMESTAMP', documentation: 'Date and time.' },
201
+ { label: 'TIMESTAMPTZ', insertText: 'TIMESTAMPTZ', documentation: 'Date and time with timezone.' },
202
+ { label: 'DATE', insertText: 'DATE', documentation: 'Calendar date.' },
203
+ { label: 'UUID', insertText: 'UUID', documentation: 'Universally unique identifier.' },
204
+ { label: 'NUMERIC', insertText: 'NUMERIC', documentation: 'Exact numeric value.' },
205
+ { label: 'VARCHAR', insertText: 'VARCHAR()', documentation: 'Variable-length character string.' },
206
+ ].map((kw) => ({
207
+ ...kw,
208
+ kind: monaco.languages.CompletionItemKind.Keyword,
209
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
210
+ }));
211
+
212
+ const FUNCTIONS = () =>
213
+ [
214
+ // Aggregate
215
+ { label: 'COUNT()', insertText: 'COUNT($1)', documentation: 'Count rows.' },
216
+ { label: 'SUM()', insertText: 'SUM($1)', documentation: 'Sum values.' },
217
+ { label: 'AVG()', insertText: 'AVG($1)', documentation: 'Average of values.' },
218
+ { label: 'MIN()', insertText: 'MIN($1)', documentation: 'Minimum value.' },
219
+ { label: 'MAX()', insertText: 'MAX($1)', documentation: 'Maximum value.' },
220
+ { label: 'ARRAY_AGG()', insertText: 'ARRAY_AGG($1)', documentation: 'Aggregate into array.' },
221
+ { label: 'STRING_AGG()', insertText: "STRING_AGG($1, '$2')", documentation: 'Concatenate with delimiter.' },
222
+ { label: 'jsonb_agg()', insertText: 'jsonb_agg($1)', documentation: 'Aggregate into JSONB array.' },
223
+ // Window
224
+ { label: 'ROW_NUMBER()', insertText: 'ROW_NUMBER() OVER ($1)', documentation: 'Row number within partition.' },
225
+ { label: 'RANK()', insertText: 'RANK() OVER ($1)', documentation: 'Rank with gaps.' },
226
+ { label: 'DENSE_RANK()', insertText: 'DENSE_RANK() OVER ($1)', documentation: 'Rank without gaps.' },
227
+ { label: 'LEAD()', insertText: 'LEAD($1, 1) OVER ($2)', documentation: 'Value from next row.' },
228
+ { label: 'LAG()', insertText: 'LAG($1, 1) OVER ($2)', documentation: 'Value from previous row.' },
229
+ { label: 'FIRST_VALUE()', insertText: 'FIRST_VALUE($1) OVER ($2)', documentation: 'First value in window.' },
230
+ { label: 'LAST_VALUE()', insertText: 'LAST_VALUE($1) OVER ($2)', documentation: 'Last value in window.' },
231
+ // Date/time
232
+ { label: 'NOW()', insertText: 'NOW()', documentation: 'Current timestamp.' },
233
+ { label: 'CURRENT_DATE', insertText: 'CURRENT_DATE', documentation: 'Current date.' },
234
+ { label: 'CURRENT_TIME', insertText: 'CURRENT_TIME', documentation: 'Current time.' },
235
+ { label: 'CURRENT_TIMESTAMP', insertText: 'CURRENT_TIMESTAMP', documentation: 'Current date and time.' },
236
+ { label: 'DATE_PART()', insertText: "DATE_PART('$1', $2)", documentation: 'Extract date field.' },
237
+ { label: 'DATE_TRUNC()', insertText: "DATE_TRUNC('$1', $2)", documentation: 'Truncate to date precision.' },
238
+ { label: 'EXTRACT()', insertText: 'EXTRACT($1 FROM $2)', documentation: 'Extract date/time field.' },
239
+ { label: 'AGE()', insertText: 'AGE($1)', documentation: 'Interval from timestamp to now.' },
240
+ { label: 'TO_CHAR()', insertText: "TO_CHAR($1, '$2')", documentation: 'Format value as string.' },
241
+ { label: 'TO_DATE()', insertText: "TO_DATE('$1', '$2')", documentation: 'Parse string to date.' },
242
+ { label: 'TO_TIMESTAMP()', insertText: "TO_TIMESTAMP($1, '$2')", documentation: 'Parse string to timestamp.' },
243
+ // String
244
+ { label: 'LENGTH()', insertText: 'LENGTH($1)', documentation: 'String length.' },
245
+ { label: 'UPPER()', insertText: 'UPPER($1)', documentation: 'Uppercase.' },
246
+ { label: 'LOWER()', insertText: 'LOWER($1)', documentation: 'Lowercase.' },
247
+ { label: 'TRIM()', insertText: 'TRIM($1)', documentation: 'Remove leading/trailing spaces.' },
248
+ { label: 'LTRIM()', insertText: 'LTRIM($1)', documentation: 'Remove leading spaces.' },
249
+ { label: 'RTRIM()', insertText: 'RTRIM($1)', documentation: 'Remove trailing spaces.' },
250
+ { label: 'LPAD()', insertText: "LPAD($1, $2, '$3')", documentation: 'Pad string on the left.' },
251
+ { label: 'RPAD()', insertText: "RPAD($1, $2, '$3')", documentation: 'Pad string on the right.' },
252
+ { label: 'CONCAT()', insertText: 'CONCAT($1, $2)', documentation: 'Concatenate strings.' },
253
+ { label: 'CONCAT_WS()', insertText: "CONCAT_WS('$1', $2)", documentation: 'Concatenate with separator.' },
254
+ { label: 'REPLACE()', insertText: "REPLACE($1, '$2', '$3')", documentation: 'Replace occurrences.' },
255
+ { label: 'SUBSTRING()', insertText: 'SUBSTRING($1, $2, $3)', documentation: 'Extract substring.' },
256
+ { label: 'SPLIT_PART()', insertText: "SPLIT_PART($1, '$2', $3)", documentation: 'Split string by delimiter.' },
257
+ { label: 'INITCAP()', insertText: 'INITCAP($1)', documentation: 'Capitalize first letter of each word.' },
258
+ { label: 'REVERSE()', insertText: 'REVERSE($1)', documentation: 'Reverse a string.' },
259
+ { label: 'REPEAT()', insertText: 'REPEAT($1, $2)', documentation: 'Repeat string N times.' },
260
+ { label: 'STRPOS()', insertText: "STRPOS($1, '$2')", documentation: 'Find position of substring.' },
261
+ { label: 'POSITION()', insertText: "POSITION('$1' IN $2)", documentation: 'Position of substring.' },
262
+ { label: 'TRANSLATE()', insertText: 'TRANSLATE($1, $2, $3)', documentation: 'Character-level translation.' },
263
+ { label: 'FORMAT()', insertText: "FORMAT('$1', $2)", documentation: 'printf-style format.' },
264
+ // Math
265
+ { label: 'ROUND()', insertText: 'ROUND($1, $2)', documentation: 'Round to N decimal places.' },
266
+ { label: 'FLOOR()', insertText: 'FLOOR($1)', documentation: 'Round down.' },
267
+ { label: 'CEILING()', insertText: 'CEILING($1)', documentation: 'Round up.' },
268
+ { label: 'ABS()', insertText: 'ABS($1)', documentation: 'Absolute value.' },
269
+ { label: 'MOD()', insertText: 'MOD($1, $2)', documentation: 'Modulo.' },
270
+ { label: 'POWER()', insertText: 'POWER($1, $2)', documentation: 'Raise to power.' },
271
+ { label: 'SQRT()', insertText: 'SQRT($1)', documentation: 'Square root.' },
272
+ { label: 'RANDOM()', insertText: 'RANDOM()', documentation: 'Random float [0,1).' },
273
+ // Type conversion
274
+ { label: 'CAST()', insertText: 'CAST($1 AS $2)', documentation: 'Convert to type.' },
275
+ { label: 'COALESCE()', insertText: 'COALESCE($1, $2)', documentation: 'First non-null value.' },
276
+ { label: 'NULLIF()', insertText: 'NULLIF($1, $2)', documentation: 'Return NULL if equal.' },
277
+ // UUID
278
+ { label: 'GEN_RANDOM_UUID()', insertText: 'GEN_RANDOM_UUID()', documentation: 'Generate a random UUID (pg 13+).' },
279
+ { label: 'uuid_generate_v4()', insertText: 'uuid_generate_v4()', documentation: 'Generate UUID v4 (uuid-ossp).' },
280
+ // Array
281
+ { label: 'UNNEST()', insertText: 'UNNEST($1)', documentation: 'Expand array to rows.' },
282
+ { label: 'STRING_TO_ARRAY()', insertText: "STRING_TO_ARRAY($1, '$2')", documentation: 'Split string to array.' },
283
+ { label: 'ARRAY_LENGTH()', insertText: 'ARRAY_LENGTH($1, 1)', documentation: 'Length of array.' },
284
+ { label: 'ARRAY_APPEND()', insertText: 'ARRAY_APPEND($1, $2)', documentation: 'Append element to array.' },
285
+ { label: 'ARRAY_REMOVE()', insertText: 'ARRAY_REMOVE($1, $2)', documentation: 'Remove element from array.' },
286
+ // JSONB
287
+ { label: 'jsonb_pretty()', insertText: 'jsonb_pretty($1)', documentation: 'Pretty-print JSONB.' },
288
+ {
289
+ label: 'jsonb_array_elements()',
290
+ insertText: 'jsonb_array_elements($1)',
291
+ documentation: 'Expand JSONB array to rows.',
292
+ },
293
+ {
294
+ label: 'jsonb_array_elements_text()',
295
+ insertText: 'jsonb_array_elements_text($1)',
296
+ documentation: 'Expand JSONB array to text rows.',
297
+ },
298
+ { label: 'jsonb_object_keys()', insertText: 'jsonb_object_keys($1)', documentation: 'Get keys of JSONB object.' },
299
+ { label: 'jsonb_each()', insertText: 'jsonb_each($1)', documentation: 'Expand JSONB to key/value rows.' },
300
+ { label: 'jsonb_each_text()', insertText: 'jsonb_each_text($1)', documentation: 'Expand JSONB to key/text rows.' },
301
+ {
302
+ label: 'jsonb_path_query()',
303
+ insertText: 'jsonb_path_query($1, $2)',
304
+ documentation: 'Query JSONB with JSONPath.',
305
+ },
306
+ { label: 'jsonb_path_exists()', insertText: 'jsonb_path_exists($1, $2)', documentation: 'Test JSONPath match.' },
307
+ { label: 'jsonb_set()', insertText: 'jsonb_set($1, $2, $3)', documentation: 'Set value at JSONB path.' },
308
+ { label: 'jsonb_insert()', insertText: 'jsonb_insert($1, $2, $3)', documentation: 'Insert at JSONB path.' },
309
+ { label: 'jsonb_build_object()', insertText: 'jsonb_build_object($1)', documentation: 'Build JSONB object.' },
310
+ { label: 'jsonb_build_array()', insertText: 'jsonb_build_array($1)', documentation: 'Build JSONB array.' },
311
+ // Custom platform functions
312
+ { label: 'get_asha_by_area()', insertText: 'get_asha_by_area($1)', documentation: 'get_asha_by_area(int8)' },
313
+ {
314
+ label: 'get_fhw_by_location()',
315
+ insertText: 'get_fhw_by_location($1)',
316
+ documentation: 'get_fhw_by_location(int8)',
317
+ },
318
+ { label: 'get_financial_year()', insertText: 'get_financial_year($1)', documentation: 'get_financial_year(date)' },
319
+ {
320
+ label: 'get_vaccination_string()',
321
+ insertText: 'get_vaccination_string($1)',
322
+ documentation: 'get_vaccination_string(int8)',
323
+ },
324
+ {
325
+ label: 'get_location_hierarchy()',
326
+ insertText: 'get_location_hierarchy($1)',
327
+ documentation: 'get_location_hierarchy(int8)',
328
+ },
329
+ {
330
+ label: 'get_location_hierarchy_by_type()',
331
+ insertText: 'get_location_hierarchy_by_type($1, $2)',
332
+ documentation: 'get_location_hierarchy_by_type(int8, varchar)',
333
+ },
334
+ {
335
+ label: 'generate_immunisations_for_form_configurator()',
336
+ insertText: 'generate_immunisations_for_form_configurator($1)',
337
+ documentation: 'generate_immunisations_for_form_configurator(text)',
338
+ },
339
+ {
340
+ label: 'get_milliseconds_from_epoch()',
341
+ insertText: 'get_milliseconds_from_epoch($1)',
342
+ documentation: 'get_milliseconds_from_epoch(TIMESTAMPTZ)',
343
+ },
344
+ {
345
+ label: 'get_date_from_milliseconds()',
346
+ insertText: 'get_date_from_milliseconds($1)',
347
+ documentation: 'get_date_from_milliseconds(bigint)',
348
+ },
349
+ {
350
+ label: 'get_timestamp_from_milliseconds()',
351
+ insertText: 'get_timestamp_from_milliseconds($1)',
352
+ documentation: 'get_timestamp_from_milliseconds(bigint)',
353
+ },
354
+ {
355
+ label: 'get_location_hierarchy_language_wise()',
356
+ insertText: 'get_location_hierarchy_language_wise()',
357
+ documentation: 'get_location_hierarchy_language_wise()',
358
+ },
359
+ {
360
+ label: 'get_image_annotation_data_by_key()',
361
+ insertText: 'get_image_annotation_data_by_key($1)',
362
+ documentation: 'get_image_annotation_data_by_key()',
363
+ },
364
+ {
365
+ label: 'get_immunisation_as_list()',
366
+ insertText: 'get_immunisation_as_list($1)',
367
+ documentation: 'get_immunisation_as_list()',
368
+ },
369
+ {
370
+ label: 'get_immunisation_as_list_v2(immunisation_data text)',
371
+ kind: monaco.languages.CompletionItemKind.Function,
372
+ insertText: 'get_immunisation_as_list_v2($1)',
373
+ documentation: 'get_immunisation_as_list_v2(immunisation_data text)',
374
+ },
375
+ {
376
+ label: 'get_immunisation_as_string()',
377
+ insertText: 'get_immunisation_as_string($1)',
378
+ documentation: 'get_immunisation_as_string()',
379
+ },
380
+ {
381
+ label: 'get_immunisation_as_string_v2(existing_immunisation_data text, immunisation_data text)',
382
+ kind: monaco.languages.CompletionItemKind.Function,
383
+ insertText: 'get_immunisation_as_string_v2($1, $2)',
384
+ documentation: 'get_immunisation_as_string_v2(existing_immunisation_data text, immunisation_data text)',
385
+ },
386
+ { label: 'mark_files_permanent()', insertText: 'mark_files_permanent()', documentation: 'mark_files_permanent()' },
387
+ {
388
+ label: 'get_listvalue_id_by_field_key_and_code()',
389
+ insertText: 'get_listvalue_id_by_field_key_and_code($1, $2)',
390
+ documentation: 'get_listvalue_id_by_field_key_and_code(text, text)',
391
+ },
392
+ ].map((fn) => ({
393
+ ...fn,
394
+ kind: monaco.languages.CompletionItemKind.Function,
395
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
396
+ }));
397
+
398
+ const SNIPPETS = () =>
399
+ [
400
+ {
401
+ label: 'SELECT * FROM',
402
+ detail: 'snippet',
403
+ insertText: 'SELECT *\nFROM $1;',
404
+ documentation: 'Basic SELECT query.',
405
+ },
406
+ {
407
+ label: 'WITH CTE',
408
+ detail: 'snippet',
409
+ insertText: 'WITH ${1:cte_name} AS (\n\t$2\n)\nSELECT *\nFROM ${1:cte_name};',
410
+ documentation: 'Common Table Expression.',
411
+ },
412
+ {
413
+ label: 'CASE WHEN',
414
+ detail: 'snippet',
415
+ insertText: 'CASE\n\tWHEN $1 THEN $2\n\tELSE $3\nEND',
416
+ documentation: 'Conditional expression.',
417
+ },
418
+ {
419
+ label: 'PARTITION BY',
420
+ detail: 'snippet',
421
+ insertText: 'PARTITION BY $1 ORDER BY $2',
422
+ documentation: 'Window function partition.',
423
+ },
424
+ {
425
+ label: 'INSERT INTO ... SELECT',
426
+ detail: 'snippet',
427
+ insertText: 'INSERT INTO ${1:table} ($2)\nSELECT $3\nFROM ${4:source};',
428
+ documentation: 'Insert rows from a SELECT.',
429
+ },
430
+ {
431
+ label: 'LATERAL JOIN',
432
+ detail: 'snippet',
433
+ insertText: 'CROSS JOIN LATERAL (\n\t$1\n) ${2:alias}',
434
+ documentation: 'Lateral subquery join.',
435
+ },
436
+ {
437
+ label: 'ON CONFLICT DO NOTHING',
438
+ detail: 'snippet',
439
+ insertText: 'ON CONFLICT DO NOTHING',
440
+ documentation: 'Upsert: ignore duplicates.',
441
+ },
442
+ {
443
+ label: 'ON CONFLICT DO UPDATE',
444
+ detail: 'snippet',
445
+ insertText: 'ON CONFLICT ($1) DO UPDATE SET $2 = EXCLUDED.$2',
446
+ documentation: 'Upsert: update on conflict.',
447
+ },
448
+ ].map((sn) => ({
449
+ ...sn,
450
+ kind: monaco.languages.CompletionItemKind.Snippet,
451
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
452
+ }));
453
+
454
+ // ─── Query parsing helpers ────────────────────────────────────────────────────
455
+
456
+ /**
457
+ * Extract all table references in the query, returning { alias -> tableName }.
458
+ * Handles:
459
+ * - FROM t1, t2
460
+ * - FROM t1 a, t2 b
461
+ * - FROM t1 AS a LEFT JOIN t2 AS b ON ...
462
+ * - WITH cte AS (...) SELECT ... FROM cte
463
+ */
464
+ function buildAliasMap(sql, schemaKeys) {
465
+ const aliasMap = {}; // alias/tableName -> canonical tableName
466
+
467
+ // 1. Named CTEs → their name acts as a virtual table
468
+ const cteRegex = /\b(\w+)\s+AS\s*\(/gi;
469
+ let m;
470
+ while ((m = cteRegex.exec(sql)) !== null) {
471
+ const cteName = m[1].toLowerCase();
472
+ if (!['select', 'insert', 'update', 'delete', 'with'].includes(cteName)) {
473
+ aliasMap[cteName] = '__CTE__'; // marks it as virtual – no column hints
474
+ }
475
+ }
476
+
477
+ // 2. Token-by-token pass — handles all JOIN variants, AS keyword, and alias guards:
478
+ const tokenRegex =
479
+ /\b(?:from|join|inner\s+join|left\s+(?:outer\s+)?join|right\s+(?:outer\s+)?join|full\s+(?:outer\s+)?join|cross\s+join|update|into)\s+(\w+)(?:\s+(?:as\s+)?(\w+))?/gi;
480
+ while ((m = tokenRegex.exec(sql)) !== null) {
481
+ const table = m[1].toLowerCase();
482
+ const alias = m[2] ? m[2].toLowerCase() : null;
483
+ if (schemaKeys.includes(table)) {
484
+ aliasMap[table] = table; // table referenced by its own name
485
+ if (alias && alias !== 'set' && alias !== 'where' && alias !== 'on') {
486
+ aliasMap[alias] = table;
487
+ }
488
+ }
489
+ }
490
+
491
+ return aliasMap;
492
+ }
493
+
494
+ /**
495
+ * Return all table names referenced at or before `offset`.
496
+ */
497
+ function getReferencedTables(sql, offset, schemaKeys) {
498
+ const slice = sql.slice(0, offset);
499
+ const aliasMap = buildAliasMap(slice, schemaKeys);
500
+ const tables = new Set();
501
+ for (const tbl of Object.values(aliasMap)) {
502
+ if (tbl !== '__CTE__') tables.add(tbl);
503
+ }
504
+ return tables;
505
+ }
506
+
507
+ /**
508
+ * Detect what context the cursor is in:
509
+ * 'TABLE' – right after FROM / JOIN / UPDATE / INTO
510
+ * 'COLUMN' – inside SELECT columns, WHERE, SET, HAVING, ORDER BY, etc.
511
+ * 'ALIAS' – after "alias." prefix
512
+ * 'GENERAL' – anything else
513
+ */
514
+ function detectContext(sql, offset) {
515
+ const before = sql.slice(0, offset).replace(/\s+/g, ' ');
516
+
517
+ // After alias dot: "t1."
518
+ const aliasDotMatch = before.match(/\b(\w+)\.(\w*)$/);
519
+ if (aliasDotMatch) {
520
+ return { type: 'ALIAS', alias: aliasDotMatch[1].toLowerCase(), partial: aliasDotMatch[2].toLowerCase() };
521
+ }
522
+
523
+ // Immediately after table-expecting keywords
524
+ if (/(?:from|join|update|into)\s+\w*$/i.test(before)) {
525
+ return { type: 'TABLE' };
526
+ }
527
+
528
+ // Inside UPDATE SET / INSERT INTO cols / DELETE FROM
529
+ if (/update\s+\w+\s+set\s+[\s\S]*$/i.test(before)) return { type: 'COLUMN_SET' };
530
+ if (/insert\s+into\s+\w+\s*\(/i.test(before)) return { type: 'COLUMN_INSERT' };
531
+ if (/delete\s+from\s+\w+\s+where\s+[\s\S]*$/i.test(before)) return { type: 'COLUMN_WHERE' };
532
+
533
+ // After SELECT, WHERE, HAVING, ON, SET — suggest columns
534
+ if (/(?:select|where|having|on|and|or|not|,)\s+\w*$/i.test(before)) return { type: 'COLUMN' };
535
+ if (/order\s+by\s+[\w,\s]*$/i.test(before)) return { type: 'COLUMN' };
536
+ if (/group\s+by\s+[\w,\s]*$/i.test(before)) return { type: 'COLUMN' };
537
+
538
+ return { type: 'GENERAL' };
539
+ }
540
+
541
+ // ─── Provider registration ────────────────────────────────────────────────────
542
+
543
+ let _providerDisposable = null;
544
+
545
+ function registerProvider(monacoInstance, schema) {
546
+ // Dispose previous instance if any
547
+ if (_providerDisposable) {
548
+ _providerDisposable.dispose();
549
+ _providerDisposable = null;
550
+ }
551
+
552
+ _providerDisposable = monacoInstance.languages.registerCompletionItemProvider('sql', {
553
+ // Only auto-trigger on dot (alias completion) and structural chars.
554
+ // For everything else the user presses Ctrl+Space / Cmd+Space.
555
+ triggerCharacters: ['.', ',', '('],
556
+
557
+ provideCompletionItems(model, position) {
558
+ const sql = model.getValue();
559
+ const offset = model.getOffsetAt(position);
560
+
561
+ const word = model.getWordUntilPosition(position);
562
+ const range = {
563
+ startLineNumber: position.lineNumber,
564
+ endLineNumber: position.lineNumber,
565
+ startColumn: word.startColumn,
566
+ endColumn: word.endColumn,
567
+ };
568
+
569
+ const schemaKeys = Object.keys(schema);
570
+ const ctx = detectContext(sql, offset);
571
+ const aliasMap = buildAliasMap(sql, schemaKeys);
572
+
573
+ const suggestions = [];
574
+
575
+ // ── ALIAS.column context ──────────────────────────────────────────────
576
+ if (ctx.type === 'ALIAS') {
577
+ const resolvedTable = aliasMap[ctx.alias];
578
+ if (resolvedTable && schema[resolvedTable]) {
579
+ for (const col of schema[resolvedTable]) {
580
+ if (!ctx.partial || col.name.toLowerCase().startsWith(ctx.partial)) {
581
+ suggestions.push({
582
+ label: col.name,
583
+ kind: monacoInstance.languages.CompletionItemKind.Field,
584
+ detail: `${col.type} · ${resolvedTable}`,
585
+ insertText: col.name,
586
+ sortText: '0' + col.name, // float to top
587
+ range,
588
+ });
589
+ }
590
+ }
591
+ }
592
+ // Return early — nothing else relevant when typing alias.xxx
593
+ return { suggestions };
594
+ }
595
+
596
+ // ── TABLE context ──────────────────────────────────────────────────────
597
+ if (ctx.type === 'TABLE') {
598
+ for (const table of schemaKeys) {
599
+ suggestions.push({
600
+ label: table,
601
+ kind: monacoInstance.languages.CompletionItemKind.Struct,
602
+ detail: 'table',
603
+ insertText: `${table} ${getAlias(table)}`,
604
+ insertTextRules: monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet,
605
+ sortText: '0' + table,
606
+ range,
607
+ });
608
+ }
609
+ // Also allow functions after FROM (e.g. FROM unnest(...))
610
+ suggestions.push(...FUNCTIONS(monacoInstance).map((f) => ({ ...f, range, sortText: '1' + f.label })));
611
+ return { suggestions };
612
+ }
613
+
614
+ // ── COLUMN context (SET / INSERT cols / WHERE / SELECT) ───────────────
615
+ if (['COLUMN', 'COLUMN_SET', 'COLUMN_INSERT', 'COLUMN_WHERE'].includes(ctx.type)) {
616
+ const referencedTables = getReferencedTables(sql, offset, schemaKeys);
617
+
618
+ for (const table of referencedTables) {
619
+ const cols = schema[table] || [];
620
+ for (const col of cols) {
621
+ const colAlias = Object.entries(aliasMap).find(([, v]) => v === table)?.[0] ?? table;
622
+ suggestions.push({
623
+ label: col.name,
624
+ kind: monacoInstance.languages.CompletionItemKind.Field,
625
+ detail: `${col.type} · ${table}`,
626
+ // Prefix with "alias." when multiple tables are in scope
627
+ insertText: referencedTables.size > 1 ? `${colAlias}.${col.name}` : col.name,
628
+ sortText: '0' + col.name,
629
+ range,
630
+ });
631
+ }
632
+ }
633
+
634
+ // Fall through and also offer keywords + functions
635
+ }
636
+
637
+ // ── GENERAL / fallthrough ─────────────────────────────────────────────
638
+ suggestions.push(
639
+ ...KEYWORDS(monacoInstance).map((k) => ({ ...k, range, sortText: '2' + k.label })),
640
+ ...FUNCTIONS(monacoInstance).map((f) => ({ ...f, range, sortText: '3' + f.label })),
641
+ ...SNIPPETS(monacoInstance).map((s) => ({ ...s, range, sortText: '4' + s.label }))
642
+ );
643
+
644
+ // Always add table names as a fallback (lower priority)
645
+ for (const table of schemaKeys) {
646
+ suggestions.push({
647
+ label: table,
648
+ kind: monacoInstance.languages.CompletionItemKind.Struct,
649
+ detail: 'table',
650
+ insertText: table,
651
+ sortText: '9' + table,
652
+ range,
653
+ });
654
+ }
655
+
656
+ return { suggestions };
657
+ },
658
+ });
659
+ }
660
+
661
+ // ─── Component ────────────────────────────────────────────────────────────────
662
+
663
+ const PostgresEditor = ({ height = '40vh', defaultValue, onChange, mode = 'EDIT_MODE' }) => {
664
+ const { getCachedData } = useCaching();
665
+ const schema = useRef({});
666
+ const containerRef = useRef(null);
667
+ const editorRef = useRef(null);
668
+
669
+ const loadSchema = async () => {
670
+ const data = await getCachedData({
671
+ cacheKey: [],
672
+ fetchFn: async () =>
673
+ execute({
674
+ code: 'MEDPLAT_FORM_GET_DB_SCHEMA',
675
+ parameters: {},
676
+ }),
677
+ staleTime: Infinity,
678
+ cacheTime: 1000 * 60 * 30,
679
+ });
680
+
681
+ const tableSchema = {};
682
+ for (const row of data.data.result) {
683
+ const table = row.table_name;
684
+ if (!tableSchema[table]) tableSchema[table] = [];
685
+ tableSchema[table].push({ name: row.column_name, type: row.data_type });
686
+ }
687
+ schema.current = tableSchema;
688
+
689
+ // Re-register provider now that schema is available
690
+ registerProvider(monaco, schema.current);
691
+ };
692
+
693
+ useEffect(() => {
694
+ if (!containerRef.current) return;
695
+
696
+ const editor = monaco.editor.create(containerRef.current, {
697
+ value: defaultValue || '',
698
+ language: 'sql',
699
+ theme: 'vs-dark',
700
+ fontSize: 14,
701
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
702
+ fontLigatures: true,
703
+ minimap: { enabled: true },
704
+ automaticLayout: true,
705
+ scrollBeyondLastLine: false,
706
+ wordWrap: 'on',
707
+ occurrencesHighlight: false,
708
+ selectionHighlight: false,
709
+ readOnly: mode === 'READ_ONLY',
710
+ domReadOnly: mode === 'READ_ONLY',
711
+ // Autocomplete — manual trigger only (Ctrl+Space / Cmd+Space)
712
+ // quickSuggestions fires on every keystroke which is distracting in SQL.
713
+ // The dot trigger above still auto-completes alias.column naturally.
714
+ suggest: {
715
+ showKeywords: true,
716
+ showSnippets: true,
717
+ showFunctions: true,
718
+ showFields: true,
719
+ showStructs: true,
720
+ insertMode: 'replace',
721
+ filterGraceful: true,
722
+ snippetsPreventQuickSuggestions: false,
723
+ },
724
+ quickSuggestions: false,
725
+ quickSuggestionsDelay: 0,
726
+ parameterHints: { enabled: true },
727
+ // Editor quality-of-life
728
+ formatOnPaste: false,
729
+ tabSize: 2,
730
+ renderWhitespace: 'selection',
731
+ smoothScrolling: true,
732
+ cursorBlinking: 'smooth',
733
+ cursorSmoothCaretAnimation: 'on',
734
+ bracketPairColorization: { enabled: true },
735
+ matchBrackets: 'always',
736
+ });
737
+
738
+ editorRef.current = editor;
739
+ requestAnimationFrame(() => {
740
+ editor.focus();
741
+ });
742
+ editor.onDidChangeModelContent(() => {
743
+ onChange?.(editor.getValue());
744
+ });
745
+
746
+ // Register with whatever schema we have at mount time (may be empty)
747
+ registerProvider(monaco, schema.current);
748
+
749
+ // Then load the real schema
750
+ loadSchema();
751
+
752
+ return () => {
753
+ editor.dispose();
754
+ if (_providerDisposable) {
755
+ _providerDisposable.dispose();
756
+ _providerDisposable = null;
757
+ }
758
+ };
759
+ }, []);
760
+
761
+ // Keep editor value in sync if defaultValue prop changes externally
762
+ useEffect(() => {
763
+ const editor = editorRef.current;
764
+ if (!editor) return;
765
+ const current = editor.getValue();
766
+ if (defaultValue !== undefined && defaultValue !== current) {
767
+ editor.setValue(defaultValue || '');
768
+ }
769
+ }, [defaultValue]);
770
+
771
+ return (
772
+ <div style={{ position: 'relative', width: '100%' }}>
773
+ <div ref={containerRef} style={{ height, width: '100%' }} />
774
+ {mode !== 'READ_ONLY' && (
775
+ <div
776
+ style={{
777
+ position: 'absolute',
778
+ bottom: 6,
779
+ right: 10,
780
+ fontSize: 11,
781
+ color: 'rgba(255,255,255,0.3)',
782
+ pointerEvents: 'none',
783
+ fontFamily: 'monospace',
784
+ userSelect: 'none',
785
+ }}
786
+ >
787
+ Ctrl+Space · suggestions &nbsp;·&nbsp; Tab · expand snippet
788
+ </div>
789
+ )}
790
+ </div>
791
+ );
792
+ };
793
+
794
+ PostgresEditor.propTypes = {
795
+ height: PropTypes.any,
796
+ defaultValue: PropTypes.any,
797
+ onChange: PropTypes.func,
798
+ mode: PropTypes.oneOf(['EDIT_MODE', 'READ_ONLY']),
799
+ };
800
+
801
+ export default PostgresEditor;