rez_core 7.1.2 → 7.1.4

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 (474) hide show
  1. package/.idea/250218_ether_core.iml +12 -0
  2. package/.idea/codeStyles/Project.xml +58 -58
  3. package/.idea/codeStyles/codeStyleConfig.xml +4 -4
  4. package/.idea/modules.xml +7 -7
  5. package/.idea/vcs.xml +5 -5
  6. package/.prettierrc +3 -3
  7. package/README.md +99 -99
  8. package/dist/module/filter/repository/saved-filter.repository.js +2 -0
  9. package/dist/module/filter/repository/saved-filter.repository.js.map +1 -1
  10. package/dist/module/filter/service/filter.service.js +33 -28
  11. package/dist/module/filter/service/filter.service.js.map +1 -1
  12. package/dist/module/integration/examples/usage.example.js +9 -9
  13. package/dist/module/integration/service/integration.service.d.ts +6 -1
  14. package/dist/module/integration/service/integration.service.js +14 -2
  15. package/dist/module/integration/service/integration.service.js.map +1 -1
  16. package/dist/module/integration/service/oauth.service.js +2 -0
  17. package/dist/module/integration/service/oauth.service.js.map +1 -1
  18. package/dist/module/integration/service/wrapper.service.js +1 -0
  19. package/dist/module/integration/service/wrapper.service.js.map +1 -1
  20. package/dist/module/integration/strategies/email/gmail-api.strategy.js +23 -7
  21. package/dist/module/integration/strategies/email/gmail-api.strategy.js.map +1 -1
  22. package/dist/module/integration/strategies/email/sendgrid-api.strategy.js +8 -5
  23. package/dist/module/integration/strategies/email/sendgrid-api.strategy.js.map +1 -1
  24. package/dist/module/layout_preference/controller/layout_preference.controller.d.ts +0 -6
  25. package/dist/module/layout_preference/controller/layout_preference.controller.js +0 -16
  26. package/dist/module/layout_preference/controller/layout_preference.controller.js.map +1 -1
  27. package/dist/module/layout_preference/layout_preference.module.js +1 -1
  28. package/dist/module/layout_preference/layout_preference.module.js.map +1 -1
  29. package/dist/module/layout_preference/service/layout_preference.service.d.ts +0 -4
  30. package/dist/module/layout_preference/service/layout_preference.service.js +0 -67
  31. package/dist/module/layout_preference/service/layout_preference.service.js.map +1 -1
  32. package/dist/module/listmaster/controller/list-master.controller.d.ts +1 -1
  33. package/dist/module/listmaster/controller/list-master.controller.js +9 -6
  34. package/dist/module/listmaster/controller/list-master.controller.js.map +1 -1
  35. package/dist/module/listmaster/repository/list-master-items.repository.js +1 -1
  36. package/dist/module/listmaster/repository/list-master-items.repository.js.map +1 -1
  37. package/dist/module/listmaster/service/list-master.service.d.ts +1 -1
  38. package/dist/module/listmaster/service/list-master.service.js +7 -7
  39. package/dist/module/listmaster/service/list-master.service.js.map +1 -1
  40. package/dist/module/meta/controller/media.controller.d.ts +3 -0
  41. package/dist/module/meta/controller/media.controller.js +27 -0
  42. package/dist/module/meta/controller/media.controller.js.map +1 -1
  43. package/dist/module/meta/entity/media-data.entity.d.ts +1 -0
  44. package/dist/module/meta/entity/media-data.entity.js +4 -0
  45. package/dist/module/meta/entity/media-data.entity.js.map +1 -1
  46. package/dist/module/meta/entity.module.js +2 -0
  47. package/dist/module/meta/entity.module.js.map +1 -1
  48. package/dist/module/meta/repository/attribute-master.repository.js +14 -14
  49. package/dist/module/meta/service/entity-dynamic.service.js +16 -16
  50. package/dist/module/meta/service/entity-dynamic.service.js.map +1 -1
  51. package/dist/module/meta/service/entity-master.service.js +20 -20
  52. package/dist/module/meta/service/entity-service-impl.service.d.ts +0 -2
  53. package/dist/module/meta/service/entity-service-impl.service.js +0 -5
  54. package/dist/module/meta/service/entity-service-impl.service.js.map +1 -1
  55. package/dist/module/meta/service/entity-table.service.d.ts +3 -8
  56. package/dist/module/meta/service/entity-table.service.js +54 -52
  57. package/dist/module/meta/service/entity-table.service.js.map +1 -1
  58. package/dist/module/meta/service/media-data.service.d.ts +2 -0
  59. package/dist/module/meta/service/media-data.service.js +8 -0
  60. package/dist/module/meta/service/media-data.service.js.map +1 -1
  61. package/dist/module/meta/service/resolver.service.js +23 -13
  62. package/dist/module/meta/service/resolver.service.js.map +1 -1
  63. package/dist/module/notification/notification.module.js +2 -0
  64. package/dist/module/notification/notification.module.js.map +1 -1
  65. package/dist/module/notification/repository/notification.repository.d.ts +7 -0
  66. package/dist/module/notification/repository/notification.repository.js +43 -0
  67. package/dist/module/notification/repository/notification.repository.js.map +1 -0
  68. package/dist/module/notification/service/notification.service.d.ts +3 -1
  69. package/dist/module/notification/service/notification.service.js +27 -40
  70. package/dist/module/notification/service/notification.service.js.map +1 -1
  71. package/dist/module/workflow/controller/workflow.controller.js +1 -1
  72. package/dist/module/workflow/controller/workflow.controller.js.map +1 -1
  73. package/dist/module/workflow/repository/action-data.repository.js +10 -3
  74. package/dist/module/workflow/repository/action-data.repository.js.map +1 -1
  75. package/dist/module/workflow/repository/action.repository.js +2 -2
  76. package/dist/module/workflow/repository/activity-log.repository.js +4 -4
  77. package/dist/module/workflow/repository/activity-log.repository.js.map +1 -1
  78. package/dist/module/workflow/repository/comm-template.repository.js +4 -4
  79. package/dist/module/workflow/repository/comm-template.repository.js.map +1 -1
  80. package/dist/module/workflow/repository/stage.repository.js +8 -8
  81. package/dist/module/workflow/repository/task.repository.js +4 -4
  82. package/dist/module/workflow/repository/task.repository.js.map +1 -1
  83. package/dist/module/workflow/service/action-template-mapping.service.js +2 -2
  84. package/dist/module/workflow/service/action.service.js +5 -5
  85. package/dist/module/workflow/service/comm-template.service.js +1 -1
  86. package/dist/module/workflow/service/comm-template.service.js.map +1 -1
  87. package/dist/module/workflow/service/entity-modification.service.d.ts +4 -1
  88. package/dist/module/workflow/service/entity-modification.service.js +9 -5
  89. package/dist/module/workflow/service/entity-modification.service.js.map +1 -1
  90. package/dist/module/workflow/service/populate-workflow.service.d.ts +1 -1
  91. package/dist/module/workflow/service/populate-workflow.service.js +24 -24
  92. package/dist/module/workflow/service/populate-workflow.service.js.map +1 -1
  93. package/dist/module/workflow/service/task.service.js +10 -11
  94. package/dist/module/workflow/service/task.service.js.map +1 -1
  95. package/dist/module/workflow/service/workflow-list-master.service.js +2 -2
  96. package/dist/module/workflow/service/workflow-list-master.service.js.map +1 -1
  97. package/dist/module/workflow-automation/service/schedule-handler.service.js +9 -9
  98. package/dist/module/workflow-automation/service/workflow-automation.service.js +8 -6
  99. package/dist/module/workflow-automation/service/workflow-automation.service.js.map +1 -1
  100. package/dist/table.config.d.ts +1 -1
  101. package/dist/tsconfig.build.tsbuildinfo +1 -1
  102. package/dist/utils/service/reflection-helper.service.js +2 -2
  103. package/docs/modules/event-driven-integration-design.md +91 -91
  104. package/docs/modules/integration.md +250 -250
  105. package/eslint.config.mjs +34 -34
  106. package/nest-cli.json +14 -14
  107. package/package.json +128 -128
  108. package/src/app.controller.ts +12 -12
  109. package/src/app.module.ts +62 -62
  110. package/src/app.service.ts +8 -8
  111. package/src/config/bull.config.ts +72 -72
  112. package/src/config/config.module.ts +17 -17
  113. package/src/config/database.config.ts +48 -48
  114. package/src/config/redis.config.ts +55 -55
  115. package/src/constant/attribute.constant.ts +8 -8
  116. package/src/constant/db-data-type.constant.ts +160 -160
  117. package/src/constant/entity.constant.ts +3 -3
  118. package/src/constant/global.constant.ts +67 -67
  119. package/src/constant/status.constant.ts +3 -3
  120. package/src/core.module.ts +96 -96
  121. package/src/decorators/roles.decorator.ts +7 -7
  122. package/src/dtos/response.dto.ts +6 -6
  123. package/src/dtos/response.ts +5 -5
  124. package/src/index.ts +1 -1
  125. package/src/module/auth/auth.module.ts +65 -65
  126. package/src/module/auth/controller/auth.controller.ts +28 -28
  127. package/src/module/auth/dto/user.dto.ts +56 -56
  128. package/src/module/auth/guards/google-auth.guard.ts +9 -9
  129. package/src/module/auth/guards/jwt.guard.ts +22 -22
  130. package/src/module/auth/services/auth.service.ts +56 -56
  131. package/src/module/auth/services/jwt.service.ts +11 -11
  132. package/src/module/auth/strategies/google.strategy.ts +54 -54
  133. package/src/module/auth/strategies/jwt.strategy.ts +65 -65
  134. package/src/module/auth/strategies/local.strategy.ts +13 -13
  135. package/src/module/dashboard/controller/dashboard.controller.ts +38 -38
  136. package/src/module/dashboard/dashboard.module.ts +19 -19
  137. package/src/module/dashboard/entity/dashboard_page_data.entity.ts +23 -23
  138. package/src/module/dashboard/entity/widget_master.entity.ts +15 -15
  139. package/src/module/dashboard/repository/dashboard.repository.ts +49 -49
  140. package/src/module/dashboard/service/dashboard.service.ts +69 -69
  141. package/src/module/eav/EAV_USAGE_GUIDE.md +351 -351
  142. package/src/module/eav/controller/eav.controller.ts +119 -119
  143. package/src/module/eav/dto/eav-operation.dto.ts +62 -62
  144. package/src/module/eav/eav.module.ts +79 -79
  145. package/src/module/eav/entity/eav-boolean.entity.ts +25 -25
  146. package/src/module/eav/entity/eav-date.entity.ts +24 -24
  147. package/src/module/eav/entity/eav-decimal.entity.ts +24 -24
  148. package/src/module/eav/entity/eav-int.entity.ts +24 -24
  149. package/src/module/eav/entity/eav-json.entity.ts +24 -24
  150. package/src/module/eav/entity/eav-text.entity.ts +24 -24
  151. package/src/module/eav/entity/eav-time.entity.ts +24 -24
  152. package/src/module/eav/entity/eav-timestamp.entity.ts +24 -24
  153. package/src/module/eav/entity/eav-varchar.entity.ts +24 -24
  154. package/src/module/eav/interface/eav-strategy.interface.ts +32 -32
  155. package/src/module/eav/repository/eav-boolean.repository.ts +67 -67
  156. package/src/module/eav/repository/eav-date.repository.ts +67 -67
  157. package/src/module/eav/repository/eav-decimal.repository.ts +67 -67
  158. package/src/module/eav/repository/eav-int.repository.ts +67 -67
  159. package/src/module/eav/repository/eav-json.repository.ts +67 -67
  160. package/src/module/eav/repository/eav-text.repository.ts +67 -67
  161. package/src/module/eav/repository/eav-time.repository.ts +67 -67
  162. package/src/module/eav/repository/eav-timestamp.repository.ts +67 -67
  163. package/src/module/eav/repository/eav-varchar.repository.ts +67 -67
  164. package/src/module/eav/service/eav-boolean.service.ts +64 -64
  165. package/src/module/eav/service/eav-date.service.ts +64 -64
  166. package/src/module/eav/service/eav-decimal.service.ts +64 -64
  167. package/src/module/eav/service/eav-factory.service.ts +93 -93
  168. package/src/module/eav/service/eav-int.service.ts +64 -64
  169. package/src/module/eav/service/eav-json.service.ts +64 -64
  170. package/src/module/eav/service/eav-text.service.ts +64 -64
  171. package/src/module/eav/service/eav-time.service.ts +64 -64
  172. package/src/module/eav/service/eav-timestamp.service.ts +64 -64
  173. package/src/module/eav/service/eav-varchar.service.ts +65 -65
  174. package/src/module/eav/service/eav.service.ts +116 -116
  175. package/src/module/entity_json/controller/entity_json.controller.ts +75 -75
  176. package/src/module/entity_json/docs/FlatJson_Filterin_System.md +2803 -2803
  177. package/src/module/entity_json/entity/entityJson.entity.ts +42 -42
  178. package/src/module/entity_json/entity_json.module.ts +22 -22
  179. package/src/module/entity_json/service/entityJson.repository.ts +37 -37
  180. package/src/module/entity_json/service/entity_json.service.ts +492 -492
  181. package/src/module/export/controller/export.controller.ts +83 -83
  182. package/src/module/export/export.module.ts +14 -14
  183. package/src/module/export/service/export.service.ts +107 -107
  184. package/src/module/filter/controller/filter.controller.ts +214 -214
  185. package/src/module/filter/dto/filter-request.dto.ts +41 -41
  186. package/src/module/filter/entity/saved-filter-detail.entity.ts +37 -37
  187. package/src/module/filter/entity/saved-filter-master.entity.ts +30 -30
  188. package/src/module/filter/filter.module.ts +33 -33
  189. package/src/module/filter/repository/saved-filter.repository.ts +249 -247
  190. package/src/module/filter/repository/saved.filter-detail.repository.ts +19 -19
  191. package/src/module/filter/service/filter-evaluator.service.ts +82 -82
  192. package/src/module/filter/service/filter.service.ts +1752 -1722
  193. package/src/module/filter/service/saved-filter.service.ts +164 -164
  194. package/src/module/ics/controller/ics.controller.ts +21 -21
  195. package/src/module/ics/dto/ics.dto.ts +55 -55
  196. package/src/module/ics/ics.module.ts +13 -13
  197. package/src/module/ics/service/ics.service.ts +57 -57
  198. package/src/module/integration/controller/calender-event.controller.ts +31 -31
  199. package/src/module/integration/controller/integration.controller.ts +662 -662
  200. package/src/module/integration/controller/wrapper.controller.ts +37 -37
  201. package/src/module/integration/dto/create-config.dto.ts +526 -526
  202. package/src/module/integration/entity/integration-config.entity.ts +112 -112
  203. package/src/module/integration/entity/integration-entity-mapper.entity.ts +14 -14
  204. package/src/module/integration/entity/integration-source.entity.ts +17 -17
  205. package/src/module/integration/entity/user-integration.entity.ts +71 -71
  206. package/src/module/integration/examples/usage.example.ts +338 -338
  207. package/src/module/integration/factories/base.factory.ts +7 -7
  208. package/src/module/integration/factories/email.factory.ts +49 -49
  209. package/src/module/integration/factories/integration.factory.ts +121 -121
  210. package/src/module/integration/factories/sms.factory.ts +51 -51
  211. package/src/module/integration/factories/telephone.factory.ts +41 -41
  212. package/src/module/integration/factories/whatsapp.factory.ts +56 -56
  213. package/src/module/integration/integration.module.ts +110 -110
  214. package/src/module/integration/service/calendar-event.service.ts +118 -118
  215. package/src/module/integration/service/integration-entity-mapper.service.ts +17 -17
  216. package/src/module/integration/service/integration-queue.service.ts +229 -229
  217. package/src/module/integration/service/integration.service.ts +2651 -2632
  218. package/src/module/integration/service/oauth.service.ts +226 -224
  219. package/src/module/integration/service/wrapper.service.ts +754 -753
  220. package/src/module/integration/strategies/email/gmail-api.strategy.ts +307 -281
  221. package/src/module/integration/strategies/email/outlook-api.strategy.ts +44 -44
  222. package/src/module/integration/strategies/email/outlook.strategy.ts +64 -64
  223. package/src/module/integration/strategies/email/sendgrid-api.strategy.ts +263 -260
  224. package/src/module/integration/strategies/integration.strategy.ts +97 -97
  225. package/src/module/integration/strategies/sms/gupshup-sms.strategy.ts +146 -146
  226. package/src/module/integration/strategies/sms/msg91-sms.strategy.ts +164 -164
  227. package/src/module/integration/strategies/sms/tubelight-sms.strategy.ts +163 -163
  228. package/src/module/integration/strategies/telephone/ozonetel-voice.strategy.ts +238 -238
  229. package/src/module/integration/strategies/telephone/tubelight-voice.strategy.ts +210 -210
  230. package/src/module/integration/strategies/whatsapp/gupshup-whatsapp.strategy.ts +359 -359
  231. package/src/module/integration/strategies/whatsapp/tubelight-whatsapp.strategy.ts +372 -372
  232. package/src/module/integration/strategies/whatsapp/whatsapp-cloud.strategy.ts +403 -403
  233. package/src/module/integration/strategies/whatsapp/whatsapp.strategy.ts +57 -57
  234. package/src/module/layout/controller/layout.controller.ts +38 -38
  235. package/src/module/layout/entity/header-items.entity.ts +28 -28
  236. package/src/module/layout/entity/header-section.entity.ts +13 -13
  237. package/src/module/layout/layout.module.ts +20 -20
  238. package/src/module/layout/repository/header-items.repository.ts +18 -18
  239. package/src/module/layout/repository/header-section.repository.ts +16 -16
  240. package/src/module/layout/service/header-section.service.ts +25 -25
  241. package/src/module/layout_preference/controller/layout_preference.controller.ts +76 -76
  242. package/src/module/layout_preference/entity/layout_preference.entity.ts +28 -28
  243. package/src/module/layout_preference/layout_preference.module.ts +22 -22
  244. package/src/module/layout_preference/repository/layout_preference.repository.ts +65 -65
  245. package/src/module/layout_preference/service/layout_preference.service.ts +191 -191
  246. package/src/module/linked_attributes/controller/linked_attributes.controller.ts +137 -137
  247. package/src/module/linked_attributes/dto/create-linked-attribute-smart.dto.ts +54 -54
  248. package/src/module/linked_attributes/entity/linked_attribute.entity.ts +51 -51
  249. package/src/module/linked_attributes/linked_attributes.module.ts +23 -23
  250. package/src/module/linked_attributes/repository/linked_attribute.repository.ts +12 -12
  251. package/src/module/linked_attributes/service/linked_attributes.service.ts +650 -650
  252. package/src/module/linked_attributes/test/linked-attributes.service.spec.ts +244 -244
  253. package/src/module/listmaster/controller/list-master.controller.ts +215 -208
  254. package/src/module/listmaster/entity/list-master-items.entity.ts +30 -30
  255. package/src/module/listmaster/entity/list-master.entity.ts +25 -25
  256. package/src/module/listmaster/listmaster.module.ts +46 -46
  257. package/src/module/listmaster/repository/list-master-items.repository.ts +262 -261
  258. package/src/module/listmaster/repository/list-master.repository.ts +60 -60
  259. package/src/module/listmaster/service/list-master-engine.ts +19 -19
  260. package/src/module/listmaster/service/list-master-extension.interface.ts +4 -4
  261. package/src/module/listmaster/service/list-master-item.service.ts +382 -382
  262. package/src/module/listmaster/service/list-master-registry.ts +15 -15
  263. package/src/module/listmaster/service/list-master.service.ts +774 -768
  264. package/src/module/mapper/controller/field-mapper.controller.ts +76 -76
  265. package/src/module/mapper/controller/mapper.controller.ts +20 -20
  266. package/src/module/mapper/dto/field-mapper.dto.ts +14 -14
  267. package/src/module/mapper/entity/field-lovs.entity.ts +15 -15
  268. package/src/module/mapper/entity/field-mapper.entity.ts +49 -49
  269. package/src/module/mapper/entity/mapper.entity.ts +9 -9
  270. package/src/module/mapper/mapper.module.ts +35 -35
  271. package/src/module/mapper/repository/field-lovs.repository.ts +35 -35
  272. package/src/module/mapper/repository/field-mapper.repository.ts +42 -42
  273. package/src/module/mapper/repository/mapper.repository.ts +32 -32
  274. package/src/module/mapper/service/field-mapper.service.ts +268 -268
  275. package/src/module/mapper/service/mapper.service.ts +80 -80
  276. package/src/module/master/controller/master.controller.ts +71 -71
  277. package/src/module/master/service/master.service.ts +460 -460
  278. package/src/module/master/service/poupulate-meta.service.ts +210 -210
  279. package/src/module/meta/controller/attribute-master.controller.ts +83 -83
  280. package/src/module/meta/controller/entity-dynamic.controller.ts +123 -123
  281. package/src/module/meta/controller/entity-master.controller.ts +41 -41
  282. package/src/module/meta/controller/entity-relation.controller.ts +36 -36
  283. package/src/module/meta/controller/entity.controller.ts +301 -301
  284. package/src/module/meta/controller/entity.public.controller.ts +76 -76
  285. package/src/module/meta/controller/media.controller.ts +162 -135
  286. package/src/module/meta/controller/meta.controller.ts +80 -80
  287. package/src/module/meta/controller/view-master.controller.ts +79 -79
  288. package/src/module/meta/dto/entity-list-data.dto.ts +6 -6
  289. package/src/module/meta/dto/entity-tab.dto.ts +4 -4
  290. package/src/module/meta/dto/entity-table.dto.ts +12 -12
  291. package/src/module/meta/entity/attribute-master.entity.ts +62 -62
  292. package/src/module/meta/entity/base-entity.entity.ts +52 -52
  293. package/src/module/meta/entity/dynamic.entity.ts +5 -5
  294. package/src/module/meta/entity/entity-master.entity.ts +53 -53
  295. package/src/module/meta/entity/entity-relation-data.entity.ts +24 -24
  296. package/src/module/meta/entity/entity-relation.entity.ts +18 -18
  297. package/src/module/meta/entity/entity-table-column.entity.ts +56 -56
  298. package/src/module/meta/entity/entity-table.entity.ts +45 -45
  299. package/src/module/meta/entity/media-data.entity.ts +35 -32
  300. package/src/module/meta/entity/preference.entity.ts +57 -57
  301. package/src/module/meta/entity/view-master.entity.ts +36 -36
  302. package/src/module/meta/entity.module.ts +153 -151
  303. package/src/module/meta/repository/attribute-master.repository.ts +206 -206
  304. package/src/module/meta/repository/entity-attribute-update.repository.ts +48 -48
  305. package/src/module/meta/repository/entity-master.repository.ts +120 -120
  306. package/src/module/meta/repository/entity-relation.repository.ts +36 -36
  307. package/src/module/meta/repository/entity-table-column.repository.ts +39 -39
  308. package/src/module/meta/repository/entity-table.repository.ts +53 -53
  309. package/src/module/meta/repository/media-data.repository.ts +50 -50
  310. package/src/module/meta/repository/preference.repository.ts +20 -20
  311. package/src/module/meta/repository/user-app-mapping.repository.ts +28 -28
  312. package/src/module/meta/repository/view-master.repository.ts +42 -42
  313. package/src/module/meta/service/attribute-master.service.ts +329 -329
  314. package/src/module/meta/service/common.service.ts +9 -9
  315. package/src/module/meta/service/entity-attribute-update.service.ts +26 -26
  316. package/src/module/meta/service/entity-dynamic.service.ts +1038 -1037
  317. package/src/module/meta/service/entity-master.service.ts +288 -288
  318. package/src/module/meta/service/entity-realation-data.service.ts +9 -9
  319. package/src/module/meta/service/entity-relation.service.ts +85 -85
  320. package/src/module/meta/service/entity-service-impl.service.ts +390 -394
  321. package/src/module/meta/service/entity-table-column.service.ts +26 -26
  322. package/src/module/meta/service/entity-table.service.ts +144 -157
  323. package/src/module/meta/service/entity-validation.service.ts +187 -187
  324. package/src/module/meta/service/entity.service.ts +48 -48
  325. package/src/module/meta/service/field-group.service.ts +103 -103
  326. package/src/module/meta/service/media-data.service.ts +397 -385
  327. package/src/module/meta/service/preference.service.ts +16 -16
  328. package/src/module/meta/service/resolver.service.ts +293 -260
  329. package/src/module/meta/service/section-master.service.ts +104 -104
  330. package/src/module/meta/service/update-form-json.service.ts +22 -22
  331. package/src/module/meta/service/user-app-mapping.service.ts +17 -17
  332. package/src/module/meta/service/view-master.service.ts +127 -127
  333. package/src/module/microservice-client/microservice-clients.module.ts +13 -13
  334. package/src/module/microservice-client/service/microservice-client-factory.ts +37 -37
  335. package/src/module/microservice-client/service/microservice-clients.ts +4 -4
  336. package/src/module/notification/controller/notification.controller.ts +58 -58
  337. package/src/module/notification/entity/notification.entity.ts +76 -76
  338. package/src/module/notification/entity/otp.entity.ts +28 -28
  339. package/src/module/notification/firebase-admin.config.ts +22 -22
  340. package/src/module/notification/notification.module.ts +65 -63
  341. package/src/module/notification/repository/notification.repository.ts +33 -0
  342. package/src/module/notification/repository/otp.repository.ts +27 -27
  343. package/src/module/notification/service/email.service.ts +142 -142
  344. package/src/module/notification/service/notification.service.ts +145 -163
  345. package/src/module/preference_master/entity/preference.entity.ts +25 -25
  346. package/src/module/preference_master/preference.service.ts +27 -27
  347. package/src/module/preference_master/repo/preference.repository.ts +36 -36
  348. package/src/module/third-party-module/entity/third-party-api-registry.entity.ts +52 -52
  349. package/src/module/third-party-module/repository/third-party-api-registry.repository.ts +20 -20
  350. package/src/module/third-party-module/service/api-registry.service.ts +13 -13
  351. package/src/module/third-party-module/third-party.module.ts +12 -12
  352. package/src/module/workflow/controller/action-category.controller.ts +54 -54
  353. package/src/module/workflow/controller/action-resource-mapping.controller.ts +23 -23
  354. package/src/module/workflow/controller/action-template-mapping.controller.ts +35 -35
  355. package/src/module/workflow/controller/action.controller.ts +111 -111
  356. package/src/module/workflow/controller/activity-log.controller.ts +55 -55
  357. package/src/module/workflow/controller/comm-template.controller.ts +43 -43
  358. package/src/module/workflow/controller/entity-modification.controller.ts +35 -35
  359. package/src/module/workflow/controller/form-master.controller.ts +43 -43
  360. package/src/module/workflow/controller/stage-group.controller.ts +49 -49
  361. package/src/module/workflow/controller/stage.controller.ts +51 -51
  362. package/src/module/workflow/controller/task.controller.ts +77 -77
  363. package/src/module/workflow/controller/workflow-list-master.controller.ts +44 -44
  364. package/src/module/workflow/controller/workflow-meta.controller.ts +80 -80
  365. package/src/module/workflow/controller/workflow.controller.ts +66 -66
  366. package/src/module/workflow/entity/action-category.entity.ts +33 -33
  367. package/src/module/workflow/entity/action-data.entity.ts +51 -51
  368. package/src/module/workflow/entity/action-resources-mapping.entity.ts +21 -21
  369. package/src/module/workflow/entity/action-template-mapping.entity.ts +12 -12
  370. package/src/module/workflow/entity/action.entity.ts +48 -48
  371. package/src/module/workflow/entity/activity-log.entity.ts +39 -39
  372. package/src/module/workflow/entity/comm-template.entity.ts +38 -38
  373. package/src/module/workflow/entity/entity-modification.entity.ts +33 -33
  374. package/src/module/workflow/entity/form.entity.ts +21 -21
  375. package/src/module/workflow/entity/stage-action-mapping.entity.ts +12 -12
  376. package/src/module/workflow/entity/stage-group.entity.ts +18 -18
  377. package/src/module/workflow/entity/stage-movement-data.entity.ts +33 -33
  378. package/src/module/workflow/entity/stage.entity.ts +15 -15
  379. package/src/module/workflow/entity/task-data.entity.ts +84 -84
  380. package/src/module/workflow/entity/template-attach-mapper.entity.ts +30 -30
  381. package/src/module/workflow/entity/workflow-data.entity.ts +6 -6
  382. package/src/module/workflow/entity/workflow-level-mapping.entity.ts +18 -18
  383. package/src/module/workflow/entity/workflow.entity.ts +15 -15
  384. package/src/module/workflow/repository/action-category.repository.ts +78 -78
  385. package/src/module/workflow/repository/action-data.repository.ts +353 -345
  386. package/src/module/workflow/repository/action.repository.ts +339 -339
  387. package/src/module/workflow/repository/activity-log.repository.ts +148 -148
  388. package/src/module/workflow/repository/comm-template.repository.ts +157 -157
  389. package/src/module/workflow/repository/form-master.repository.ts +50 -50
  390. package/src/module/workflow/repository/stage-group.repository.ts +186 -186
  391. package/src/module/workflow/repository/stage-movement.repository.ts +217 -217
  392. package/src/module/workflow/repository/stage.repository.ts +160 -160
  393. package/src/module/workflow/repository/task.repository.ts +156 -154
  394. package/src/module/workflow/repository/workflow.repository.ts +42 -42
  395. package/src/module/workflow/service/action-category.service.ts +32 -32
  396. package/src/module/workflow/service/action-data.service.ts +62 -62
  397. package/src/module/workflow/service/action-resources-mapping.service.ts +10 -10
  398. package/src/module/workflow/service/action-template-mapping.service.ts +137 -137
  399. package/src/module/workflow/service/action.service.ts +300 -300
  400. package/src/module/workflow/service/activity-log.service.ts +106 -106
  401. package/src/module/workflow/service/comm-template.service.ts +179 -179
  402. package/src/module/workflow/service/entity-modification.service.ts +63 -55
  403. package/src/module/workflow/service/form-master.service.ts +35 -35
  404. package/src/module/workflow/service/populate-workflow.service.ts +313 -313
  405. package/src/module/workflow/service/stage-action-mapping.service.ts +5 -5
  406. package/src/module/workflow/service/stage-group.service.ts +325 -325
  407. package/src/module/workflow/service/stage.service.ts +196 -196
  408. package/src/module/workflow/service/task.service.ts +546 -547
  409. package/src/module/workflow/service/workflow-list-master.service.ts +68 -68
  410. package/src/module/workflow/service/workflow-meta.service.ts +638 -638
  411. package/src/module/workflow/service/workflow.service.ts +212 -212
  412. package/src/module/workflow/workflow.module.ts +180 -180
  413. package/src/module/workflow-automation/SCHEDULING_GUIDE.md +145 -145
  414. package/src/module/workflow-automation/controller/workflow-automation.controller.ts +43 -43
  415. package/src/module/workflow-automation/entity/workflow-automation-action.entity.ts +26 -26
  416. package/src/module/workflow-automation/entity/workflow-automation.entity.ts +35 -35
  417. package/src/module/workflow-automation/interface/action.decorator.ts +7 -7
  418. package/src/module/workflow-automation/interface/action.interface.ts +5 -5
  419. package/src/module/workflow-automation/service/action-registery.service.ts +35 -35
  420. package/src/module/workflow-automation/service/schedule-handler.service.ts +167 -167
  421. package/src/module/workflow-automation/service/workflow-automation-engine.service.ts +219 -219
  422. package/src/module/workflow-automation/service/workflow-automation.service.ts +486 -486
  423. package/src/module/workflow-automation/workflow-automation.module.ts +55 -55
  424. package/src/module/workflow-schedule/INSTALLATION.md +244 -244
  425. package/src/module/workflow-schedule/MULTI_PROJECT_GUIDE.md +196 -196
  426. package/src/module/workflow-schedule/README.md +422 -422
  427. package/src/module/workflow-schedule/constants/schedule.constants.ts +48 -48
  428. package/src/module/workflow-schedule/controller/workflow-schedule.controller.ts +253 -253
  429. package/src/module/workflow-schedule/docs/CLAUDE_CODE_GUIDE.md +510 -510
  430. package/src/module/workflow-schedule/docs/CLAUDE_CODE_PROMPT.md +362 -362
  431. package/src/module/workflow-schedule/docs/RUN_CLAUDE_CODE.sh +68 -68
  432. package/src/module/workflow-schedule/dto/create-schedule.dto.ts +147 -147
  433. package/src/module/workflow-schedule/dto/get-execution-logs.dto.ts +119 -119
  434. package/src/module/workflow-schedule/dto/update-schedule.dto.ts +96 -96
  435. package/src/module/workflow-schedule/entities/scheduled-workflow.entity.ts +148 -148
  436. package/src/module/workflow-schedule/entities/workflow-execution-log.entity.ts +154 -154
  437. package/src/module/workflow-schedule/interfaces/schedule-job-data.interface.ts +51 -51
  438. package/src/module/workflow-schedule/interfaces/workflow-schedule-options.interface.ts +12 -12
  439. package/src/module/workflow-schedule/processors/schedule.processor.ts +616 -616
  440. package/src/module/workflow-schedule/service/workflow-schedule.service.ts +588 -588
  441. package/src/module/workflow-schedule/workflow-schedule.module.ts +67 -67
  442. package/src/resources/dev.properties.yaml +33 -33
  443. package/src/resources/local.properties.yaml +27 -27
  444. package/src/resources/properties.module.ts +12 -12
  445. package/src/resources/properties.yaml.ts +11 -11
  446. package/src/resources/uat.properties.yaml +31 -31
  447. package/src/table.config.ts +123 -123
  448. package/src/utils/dto/excel-data.dto.ts +14 -14
  449. package/src/utils/dto/excelsheet-data.dto.ts +5 -5
  450. package/src/utils/service/base64util.service.ts +18 -18
  451. package/src/utils/service/clockIDGenUtil.service.ts +21 -21
  452. package/src/utils/service/codeGenerator.service.ts +22 -22
  453. package/src/utils/service/dateUtil.service.ts +17 -17
  454. package/src/utils/service/encryptUtil.service.ts +97 -97
  455. package/src/utils/service/excel-helper.service.ts +72 -72
  456. package/src/utils/service/excelUtil.service.ts +15 -15
  457. package/src/utils/service/file-util.service.ts +11 -11
  458. package/src/utils/service/json-util.service.ts +23 -23
  459. package/src/utils/service/loggingUtil.service.ts +88 -88
  460. package/src/utils/service/reflection-helper.service.ts +62 -62
  461. package/src/utils/service/wbsCodeGen.service.ts +8 -8
  462. package/src/utils/utils.module.ts +27 -27
  463. package/tsconfig.build.json +4 -4
  464. package/tsconfig.json +24 -24
  465. package/.claude/settings.local.json +0 -26
  466. package/.idea/250218_nodejs_core.iml +0 -9
  467. package/.idea/copilot.data.migration.agent.xml +0 -6
  468. package/.idea/copilot.data.migration.ask.xml +0 -6
  469. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  470. package/.idea/copilot.data.migration.edit.xml +0 -6
  471. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  472. package/.idea/misc.xml +0 -6
  473. package/.idea/prettier.xml +0 -6
  474. package/server.log +0 -850
@@ -1,1722 +1,1752 @@
1
- import { BadRequestException, Inject, Injectable } from '@nestjs/common';
2
- import { AttributeMasterService } from 'src/module/meta/service/attribute-master.service';
3
- import { EntityMasterService } from 'src/module/meta/service/entity-master.service';
4
- import { FilterCondition, FilterRequestDto } from '../dto/filter-request.dto';
5
- import { SavedFilterService } from './saved-filter.service';
6
-
7
- import * as moment from 'moment';
8
- import { EntityRelationService } from 'src/module/meta/service/entity-relation.service';
9
- import { ResolverService } from 'src/module/meta/service/resolver.service';
10
- import { LoggingService } from 'src/utils/service/loggingUtil.service';
11
- import { ConfigService } from '@nestjs/config';
12
- import { ReflectionHelper } from '../../../utils/service/reflection-helper.service';
13
- import { EntityManager } from 'typeorm';
14
-
15
- @Injectable()
16
- export class FilterService {
17
- constructor(
18
- private entityManager: EntityManager,
19
- @Inject('AttributeMasterService')
20
- private readonly attributeMasterService: AttributeMasterService,
21
- @Inject('EntityMasterService')
22
- private readonly entityMasterService: EntityMasterService,
23
- @Inject('SavedFilterService')
24
- private readonly savedFilterService: SavedFilterService,
25
- @Inject('EntityRelationService')
26
- private readonly entityRelationService: EntityRelationService,
27
- private readonly resolverService: ResolverService,
28
- @Inject() protected readonly loggingService: LoggingService,
29
- private readonly configService: ConfigService,
30
- private readonly reflectionHelper: ReflectionHelper,
31
- ) {
32
- }
33
-
34
- schema = this.configService.get('DB_SCHEMA');
35
-
36
- private async gettab_value_counts(
37
- tableName: string,
38
- column: string | undefined,
39
- whereClauses: { query: string; params: Record<string, any> }[],
40
- ) {
41
- if (!column) return [];
42
-
43
- let whereSQL = '';
44
- const values: any[] = [];
45
- let paramIndex = 1; // PostgreSQL placeholders start at $1
46
-
47
- if (whereClauses.length > 0) {
48
- const clauseParts = whereClauses.map((clause) => {
49
- let parsedQuery = clause.query.replace(/\be\./g, ''); // remove e.
50
-
51
- Object.entries(clause.params).forEach(([key, val]) => {
52
- if (Array.isArray(val)) {
53
- // Create ($1,$2,$3)
54
- const placeholders = val.map(() => `$${paramIndex++}`).join(', ');
55
-
56
- parsedQuery = parsedQuery.replace(
57
- new RegExp(`:${key}`, 'g'),
58
- `(${placeholders})`,
59
- );
60
-
61
- values.push(...val.map((v) => String(v)));
62
- } else {
63
- // Create $1
64
- const placeholder = `$${paramIndex++}`;
65
-
66
- parsedQuery = parsedQuery.replace(
67
- new RegExp(`:${key}`, 'g'),
68
- placeholder,
69
- );
70
-
71
- values.push(String(val));
72
- }
73
- });
74
-
75
- return parsedQuery;
76
- });
77
-
78
- whereSQL = `WHERE ${clauseParts.join(' AND ')}`;
79
- }
80
-
81
- const rawSQL = `
82
- SELECT ${column} AS tab_value, COUNT(*) AS tab_value_count
83
- FROM ${this.schema}.${tableName} ${whereSQL}
84
- GROUP BY ${column}
85
- `;
86
-
87
-
88
- const rows = await this.entityManager.query(rawSQL, values);
89
-
90
- const total = rows.reduce(
91
- (sum, r) => sum + parseInt(r.tab_value_count, 10),
92
- 0,
93
- );
94
-
95
- return [
96
- { tab_value: 'All', tab_value_count: total },
97
- ...rows.map((r) => ({
98
- tab_value: r.tab_value ?? 'BLANK',
99
- tab_value_count: parseInt(r.tab_value_count, 10),
100
- })),
101
- ];
102
- }
103
-
104
- async applyFilterWrapper(dto: FilterRequestDto) {
105
- const {
106
- entity_type,
107
- quickFilter = [],
108
- savedFilterCode,
109
- attributeFilter = [],
110
- loggedInUser,
111
- } = dto;
112
-
113
- // 🔹 Step 1: Collect all filters (from body + savedFilter)
114
- const savedFilters = await this.getSavedFilters(
115
- savedFilterCode ?? undefined,
116
- );
117
- const allFilters = [
118
- ...quickFilter,
119
- ...attributeFilter,
120
- ...savedFilters,
121
- ].filter((f) => f.filter_value !== '');
122
-
123
- // 🔹 Step 2: Group filters by filter_entity_type
124
- const grouped = allFilters.reduce(
125
- (acc, f) => {
126
- if (!acc[f.filter_entity_type]) acc[f.filter_entity_type] = [];
127
- acc[f.filter_entity_type].push(f);
128
- return acc;
129
- },
130
- {} as Record<string, any[]>,
131
- );
132
-
133
- console.log('🟠 [FilterService] Grouped filters by entity type:', grouped);
134
-
135
- // 🔹 Step 3: Handle sub-entities first
136
- let intersectionIds: number[] | null = null;
137
- for (const [subEntityType, filters] of Object.entries(grouped)) {
138
- if (subEntityType === entity_type) continue; // skip main entity for now
139
-
140
- let { queryParams, tabs, ...newDto } = dto;
141
-
142
- const subDto: FilterRequestDto = {
143
- ...newDto,
144
- entity_type: subEntityType,
145
- quickFilter: filters as FilterCondition[],
146
- savedFilterCode: null, // already merged
147
- attributeFilter: [],
148
- };
149
-
150
- const subResult = await this.applyFilter(subDto);
151
- const subEntityIds = subResult.data.entity_list.map((row) => row.id);
152
-
153
- console.log(
154
- `🧩 [FilterService] Sub-entity ${subEntityType} returned IDs:`,
155
- subEntityIds,
156
- );
157
-
158
- if (!subEntityIds.length) {
159
- console.log(
160
- `ℹ️ [FilterService] No records for sub-entity ${subEntityType}, returning empty result`,
161
- );
162
- return {
163
- success: true,
164
- data: { entity_tabs: [], entity_list: [], pagination: {} },
165
- };
166
- }
167
-
168
- const relatedIds = await this.entityRelationService.getRelatedEntityIds(
169
- entity_type,
170
- subEntityType,
171
- subEntityIds,
172
- dto.loggedInUser.enterprise_id,
173
- );
174
-
175
- intersectionIds =
176
- intersectionIds === null
177
- ? relatedIds
178
- : intersectionIds.filter((id) => relatedIds.includes(id));
179
-
180
- if (intersectionIds.length === 0) {
181
- console.log(
182
- '🚫 [FilterService] No intersection IDs left, returning empty result',
183
- );
184
- return {
185
- success: true,
186
- data: { entity_tabs: [], entity_list: [], pagination: {} },
187
- };
188
- }
189
- }
190
-
191
- // 🔹 Step 4: Call applyFilter for main entity
192
- const mainFilters = grouped[entity_type] || [];
193
- const mainDto: FilterRequestDto = {
194
- ...dto,
195
- quickFilter: [...mainFilters],
196
- savedFilterCode: null,
197
- attributeFilter: [],
198
- };
199
-
200
- if (intersectionIds && intersectionIds.length > 0) {
201
- (mainDto.quickFilter ??= []).push({
202
- filter_attribute: 'id',
203
- filter_operator: 'equal',
204
- filter_value: intersectionIds,
205
- filter_entity_type: entity_type,
206
- });
207
- }
208
-
209
- return await this.applyFilter(mainDto);
210
- }
211
-
212
- async applyFilter(dto: FilterRequestDto) {
213
- const {
214
- entity_type,
215
- quickFilter,
216
- savedFilterCode,
217
- attributeFilter,
218
- tabs,
219
- sortby,
220
- loggedInUser,
221
- queryParams,
222
- customLevelType,
223
- customLevelId,
224
- customAppCode,
225
- defaultfilter,
226
- } = dto;
227
-
228
- // abstract user details
229
- const {
230
- level_type,
231
- level_id,
232
- id: user_id,
233
- enterprise_id,
234
- } = loggedInUser || {};
235
-
236
- // Fetch meta from entity table service
237
- const entityMeta = await this.entityMasterService.getEntityData(
238
- entity_type,
239
- loggedInUser,
240
- );
241
- const tableName = entityMeta?.data_source; // data_source is the table name
242
-
243
- if (!tableName) {
244
- console.error(`❌ [FilterService] Invalid entity_type: ${entity_type}`);
245
- throw new BadRequestException(`Invalid entity_type: ${entity_type}`);
246
- }
247
-
248
- const getAttributeColumnMeta =
249
- await this.attributeMasterService.findAttributesByMappedEntityType(
250
- entity_type,
251
- loggedInUser,
252
- );
253
-
254
- const attributeMetaMap = getAttributeColumnMeta.reduce(
255
- (acc, attr) => {
256
- acc[attr.attribute_key] = attr;
257
- return acc;
258
- },
259
- {} as Record<string, any>,
260
- );
261
-
262
- // Get and parse saved filters
263
- const savedFilters = await this.getSavedFilters(
264
- savedFilterCode ?? undefined,
265
- );
266
- const savedFiltersNormalized = savedFilters.map((f) => ({
267
- filter_attribute: f.filter_attribute,
268
- filter_operator: f.filter_operator,
269
- filter_value: f.filter_value,
270
- }));
271
-
272
- const baseFilters = [
273
- ...(quickFilter || []).filter(
274
- (f) => f.filter_value != null && f.filter_value !== '',
275
- ),
276
- ...savedFiltersNormalized.filter(
277
- (f) => f.filter_value != null && f.filter_value !== '',
278
- ),
279
- ...(attributeFilter || []).filter(
280
- (f) => f.filter_value != null && f.filter_value !== '',
281
- ),
282
- ];
283
-
284
- // 🧱 Build where clausesx
285
- const baseWhere = this.buildWhereClauses(baseFilters, attributeMetaMap);
286
-
287
- // Handle TEMPLATE entity special condition
288
- if (entity_type === 'TEMP' || entity_type === 'TEM') {
289
-
290
- if (level_type === 'ORG') {
291
- baseWhere.push({
292
- query: ` ((e.level_type = 'ORG' AND e.level_id = '${loggedInUser.enterprise_id}'))`,
293
- params: {},
294
- });
295
- } else if (level_type === 'SCH') {
296
- baseWhere.push({
297
- query: `
298
- (
299
- (e.level_type = 'SCH' AND e.level_id = '${loggedInUser.level_id}')
300
- OR
301
- (
302
- e.level_type = 'ORG'
303
- AND e.level_id = '${loggedInUser.enterprise_id}'
304
- AND e.enterprise_id = '${loggedInUser.enterprise_id}'
305
- AND e.code NOT IN (
306
- SELECT sub.code
307
- FROM frm_wf_comm_template AS sub
308
- WHERE sub.level_type = 'SCH' AND sub.level_id = '${loggedInUser.level_id}'
309
- )
310
- )
311
- )
312
- `,
313
- params: {},
314
- });
315
- }
316
- }
317
-
318
- // Default org/level clause — skip TEMPLATE entity
319
- if (
320
- entity_type !== 'ORGP' &&
321
- entity_type !== 'TEMP' &&
322
- entity_type !== 'TEM' && // skip TEMPLATE
323
- entity_type !== 'SCH' &&
324
- level_type &&
325
- level_id &&
326
- enterprise_id &&
327
- !customLevelType &&
328
- !customLevelId &&
329
- defaultfilter !== 'false' // ✅ only run if defaultfilter is not false
330
- ) {
331
- baseWhere.push({
332
- query:
333
- 'e.level_type = :level_type AND e.level_id = :level_id',
334
- params: {
335
- level_type,
336
- level_id,
337
- },
338
- });
339
- }
340
-
341
- if (entity_type == 'USR' || entity_type == 'UPR') {
342
- baseWhere.push({
343
- query: 'e.is_customer is NULL',
344
- params: {},
345
- });
346
- }
347
-
348
- if (customLevelType && customLevelId && customAppCode) {
349
- baseWhere.push({
350
- query:
351
- 'e.level_type = :customLevelType AND e.level_id = :customLevelId AND e.appcode = :customAppCode',
352
- params: {
353
- customLevelType,
354
- customLevelId: String(customLevelId),
355
- customAppCode,
356
- },
357
- });
358
- }
359
-
360
- // Handle queryParams filters
361
- if (queryParams) {
362
- Object.entries(queryParams).forEach(([key, value]) => {
363
- if (!value) return;
364
-
365
- // Always convert to string
366
- const strValue = String(value);
367
-
368
- if (key === 'attributeName' && queryParams['attributeValue']) {
369
- const attrName = strValue;
370
- const attrValue = String(queryParams['attributeValue']);
371
-
372
- baseWhere.push({
373
- query: `e.${attrName} = :${attrName}`,
374
- params: { [attrName]: attrValue }, // <-- string
375
- });
376
- } else if (key !== 'attributeValue') {
377
- baseWhere.push({
378
- query: `e.${key} = :${key}`,
379
- params: { [key]: strValue }, // <-- string
380
- });
381
- }
382
- });
383
- }
384
-
385
- console.log('🟠 [FilterService] Constructed baseWhere clauses:', baseWhere);
386
-
387
- let layoutPreferenceRepo =
388
- this.reflectionHelper.getRepoService('LayoutPreference');
389
-
390
- const layoutPreference = await layoutPreferenceRepo.findOne({
391
- where: {
392
- user_id: user_id,
393
- mapped_entity_type: entity_type,
394
- mapped_level_id: level_id,
395
- mapped_level_type: level_type,
396
- type: 'layout',
397
- },
398
- });
399
-
400
- // Extract layout preference
401
- const layout = layoutPreference?.mapped_json?.quick_tab || {};
402
- const showList =
403
- layout?.show_list?.map((val) => {
404
- if (typeof val === 'string') {
405
- // legacy case — old format like ['Active', 'Inactive']
406
- return val.toLowerCase();
407
- }
408
-
409
- if (val && typeof val === 'object' && val.value !== undefined) {
410
- // new format — [{ label: 'Active', value: '13023' }]
411
- return String(val.value).toLowerCase();
412
- }
413
-
414
- return ''; // fallback safety
415
- }) || [];
416
-
417
- let filteredTabs = await this.getFilteredTabs(
418
- layout,
419
- showList,
420
- entityMeta,
421
- baseWhere,
422
- tabs,
423
- loggedInUser,
424
- );
425
-
426
- const dataWhere = [...baseWhere];
427
-
428
- if (tabs?.columnName && tabs?.value) {
429
- const tabAttrMeta = attributeMetaMap[tabs.columnName];
430
- const tabValue = tabs.value.toLowerCase();
431
-
432
- if (tabValue === 'others') {
433
- // Extract 'value' (IDs) from showList, ignore 'all'
434
- const valuesToExclude = showList
435
- .filter((v) => v.label.toLowerCase() !== 'all')
436
- .map((v) => v.value);
437
-
438
- if (valuesToExclude.length > 0) {
439
- for (const value of valuesToExclude) {
440
- const resolvedId = await this.resolverService.getResolvedId(
441
- loggedInUser,
442
- tabs.columnName,
443
- value,
444
- entity_type,
445
- );
446
- if (resolvedId) {
447
- const tabCondition = this.buildCondition(
448
- {
449
- filter_attribute: tabs.columnName,
450
- filter_operator: 'not_equal',
451
- filter_value: [resolvedId],
452
- skip_id: true,
453
- },
454
- tabAttrMeta,
455
- );
456
- if (tabCondition) dataWhere.push(tabCondition);
457
- }
458
- }
459
- }
460
- } else if (tabValue !== 'all') {
461
- const resolvedId = await this.resolverService.getResolvedId(
462
- loggedInUser,
463
- tabs.columnName,
464
- tabs.value,
465
- entity_type,
466
- );
467
-
468
- if (resolvedId) {
469
- const tabCondition = this.buildCondition(
470
- {
471
- filter_attribute: tabs.columnName,
472
- filter_operator: 'equal',
473
- filter_value: [resolvedId],
474
- skip_id: true,
475
- },
476
- tabAttrMeta,
477
- );
478
- if (tabCondition) dataWhere.push(tabCondition);
479
- }
480
- }
481
- }
482
-
483
- let qb = this.entityManager
484
- .createQueryBuilder()
485
- .select('e.*')
486
- .from(`${this.schema}.${tableName}`, 'e');
487
- dataWhere.forEach((clause) => qb.andWhere(clause.query, clause.params));
488
-
489
- // Apply sorting
490
- qb = await this.sortTabsByShowList(qb, sortby, layoutPreference, tabs);
491
-
492
- // Check if hierarchical mode is enabled
493
- const isHierarchical = entityMeta?.is_hierarchical === true;
494
-
495
- let page = dto.page && dto.page > 0 ? dto.page : 1;
496
- let size = dto.size && dto.size > 0 ? dto.size : 10;
497
-
498
- // Skip pagination if hierarchical mode is enabled
499
- if (!isHierarchical && dto.page && dto.size) {
500
- qb.skip((page - 1) * size).take(size);
501
- }
502
-
503
- let query = await qb.getQuery();
504
-
505
- console.log('------------------------------------------------------\n');
506
- console.log(query);
507
- console.log('\n------------------------------------------------------');
508
- const entity_list = await qb.getRawMany();
509
-
510
- console.log(`📦 [FilterService] Fetched ${entity_list.length} records`);
511
-
512
- const dateAttributes = Object.entries(attributeMetaMap)
513
- .filter(([_, attr]) => attr.element_type === 'date')
514
- .map(([key]) => key);
515
-
516
- const formatDate = (dateStr: string | null): string | null =>
517
- dateStr ? moment(dateStr).format('DD-MM-YYYY') : '';
518
-
519
- const formatDatesInRow = (row: any): any => {
520
- const formattedRow = { ...row };
521
- for (const key of dateAttributes) {
522
- if (formattedRow[key])
523
- formattedRow[key] = formatDate(formattedRow[key]);
524
- }
525
- return formattedRow;
526
- };
527
-
528
- const formattedEntityList = entity_list.map(formatDatesInRow);
529
-
530
- // Build hierarchy if hierarchical mode is enabled
531
- let finalEntityList = formattedEntityList;
532
- if (isHierarchical) {
533
- finalEntityList = this.buildHierarchyList(formattedEntityList);
534
- console.log(
535
- `🌳 [FilterService] Built hierarchical structure with ${finalEntityList.length} root nodes`,
536
- );
537
- }
538
-
539
- const resolvedEntityList = await Promise.all(
540
- formattedEntityList.map((row) =>
541
- this.resolverService.getResolvedData(loggedInUser, row, entity_type),
542
- ),
543
- );
544
-
545
- let resolvedTabs = await Promise.all(
546
- filteredTabs.map(async (tab) => {
547
- const tabAttrKey = layout?.attribute || tabs?.columnName;
548
-
549
- if (
550
- !tabAttrKey ||
551
- tab.tab_value?.toLowerCase() === 'all' ||
552
- tab.tab_value?.toLowerCase() === 'others'
553
- ) {
554
- return tab;
555
- }
556
-
557
- const resolvedVal = await this.resolverService.getResolvedValue(
558
- loggedInUser,
559
- tabAttrKey,
560
- tab.tab_value,
561
- entity_type,
562
- );
563
-
564
- return { ...tab, tab_value: resolvedVal ?? tab.tab_value };
565
- }),
566
- );
567
- resolvedTabs = Object.values(
568
- resolvedTabs.reduce((acc, item) => {
569
- if (!acc[item.tab_value]) {
570
- acc[item.tab_value] = { ...item };
571
- } else {
572
- acc[item.tab_value].tab_value_count += item.tab_value_count;
573
- }
574
- return acc;
575
- }, {} as Record<string, { tab_value: string; tab_value_count: number }>)
576
- );
577
-
578
- const countQb = this.entityManager
579
- .createQueryBuilder()
580
- .select('COUNT(*)', 'count')
581
- .from(`${this.schema}.${tableName}`, 'e');
582
- dataWhere.forEach((clause) =>
583
- countQb.andWhere(clause.query, clause.params),
584
- );
585
- const countResult = await countQb.getRawOne();
586
- const total = parseInt(countResult.count, 10);
587
-
588
- console.log('📊 [FilterService] Returning final result with total:', total);
589
-
590
- return {
591
- success: true,
592
- data: {
593
- entity_tabs: resolvedTabs,
594
- entity_list: resolvedEntityList,
595
- is_hierarchical: isHierarchical,
596
- pagination: {
597
- total,
598
- page,
599
- size,
600
- totalPages: Math.ceil(total / size),
601
- hasNextPage: page * size < total,
602
- hasPreviousPage: page > 1,
603
- },
604
- },
605
- };
606
- }
607
-
608
- /**
609
- * Build hierarchical structure from flat entity list using wbs_code
610
- * @param entityList - Flat list of entities with wbs_code field
611
- * @returns Hierarchical tree structure with children property
612
- */
613
- private buildHierarchyList(entityList: any[]): any[] {
614
- const entityMap = new Map<string, any>();
615
- const rootEntities: any[] = [];
616
-
617
- // Initialize all entities in the map with children array
618
- entityList.forEach((entity) => {
619
- if (entity.wbs_code) {
620
- entityMap.set(entity.wbs_code, {
621
- ...entity,
622
- children: [],
623
- });
624
- }
625
- });
626
-
627
- // Build the hierarchy based on WBS code
628
- entityList.forEach((entity) => {
629
- if (!entity.wbs_code) {
630
- // Entities without wbs_code are treated as root
631
- rootEntities.push({ ...entity, children: [] });
632
- return;
633
- }
634
-
635
- // Get parent WBS code by removing the last segment
636
- const parentWbs = entity.wbs_code.split('.').slice(0, -1).join('.');
637
-
638
- if (parentWbs && entityMap.has(parentWbs)) {
639
- // Add to parent's children
640
- entityMap.get(parentWbs).children.push(entityMap.get(entity.wbs_code));
641
- } else {
642
- // No parent found, this is a root entity
643
- rootEntities.push(entityMap.get(entity.wbs_code));
644
- }
645
- });
646
-
647
- return rootEntities;
648
- }
649
-
650
- // GET FILTERED TABS LOGIC
651
- private async getFilteredTabs(
652
- layout: any,
653
- showList: any,
654
- entityMeta: any,
655
- baseWhere: any,
656
- tabs: any,
657
- loggedInUser: any,
658
- ) {
659
- let allTabs;
660
- if (layout.attribute) {
661
- allTabs = await this.gettab_value_counts(
662
- entityMeta?.data_source,
663
- layout.attribute,
664
- baseWhere,
665
- );
666
- } else {
667
- allTabs = await this.gettab_value_counts(
668
- entityMeta?.data_source,
669
- tabs?.columnName,
670
- baseWhere,
671
- );
672
- }
673
-
674
- let filteredTabs: any[] = [];
675
-
676
- if (showList?.length > 0) {
677
- // Extract tab IDs (values)
678
- const showValues = (showList || []).map((item) => {
679
- const val = typeof item === 'object' ? item.value : item;
680
- return val ? String(val).toLowerCase() : '';
681
- });
682
-
683
- // Handle "all" logic
684
- const isAllNeeded = layout?.isAllSelected && !showValues.includes('all');
685
- if (isAllNeeded) {
686
- showList.push({ label: 'All', value: 'all' });
687
- showValues.push('all');
688
- }
689
-
690
- // Filter by value IDs (not labels)
691
- filteredTabs = allTabs.filter((tab) => {
692
- const tabValueStr = tab.tab_value
693
- ? String(tab.tab_value).toLowerCase()
694
- : '';
695
- return showValues.includes(tabValueStr);
696
- });
697
-
698
- // Handle "all" tab
699
- const allTab = filteredTabs.find(
700
- (tab) => tab.tab_value?.toString().toLowerCase() === 'all',
701
- );
702
- filteredTabs = filteredTabs.filter(
703
- (tab) => tab.tab_value?.toString().toLowerCase() !== 'all',
704
- );
705
-
706
- // SORTING LOGIC
707
- if (layout.sorting === 'asc') {
708
- filteredTabs.sort((a, b) =>
709
- a.tab_value.toString().localeCompare(b.tab_value.toString()),
710
- );
711
- } else if (layout.sorting === 'dsc') {
712
- filteredTabs.sort((a, b) =>
713
- b.tab_value.toString().localeCompare(a.tab_value.toString()),
714
- );
715
- } else if (layout.sorting === 'count_asc') {
716
- filteredTabs.sort((a, b) => a.tab_value_count - b.tab_value_count);
717
- } else if (layout.sorting === 'count_dsc') {
718
- filteredTabs.sort((a, b) => b.tab_value_count - a.tab_value_count);
719
- } else if (layout.sorting === 'custom') {
720
- // Keep order same as showList
721
- const orderMap = new Map<string, number>(
722
- showList.map((item, index) => [
723
- item.value?.toString().toLowerCase(),
724
- index,
725
- ]),
726
- );
727
- filteredTabs.sort((a, b) => {
728
- const aIndex =
729
- orderMap.get(a.tab_value?.toString().toLowerCase()) ?? Infinity;
730
- const bIndex =
731
- orderMap.get(b.tab_value?.toString().toLowerCase()) ?? Infinity;
732
- return aIndex - bIndex;
733
- });
734
- }
735
-
736
- // Re-add "all" tab at beginning if needed
737
- if (allTab) filteredTabs.unshift(allTab);
738
-
739
- // Combine others if enabled
740
- if (layout?.isCombineOther) {
741
- const originalAllTab = allTabs.find(
742
- (tab) => tab.tab_value?.toString().toLowerCase() === 'all',
743
- );
744
- const allCount = originalAllTab?.tab_value_count ?? 0;
745
- const knownTabCountSum = filteredTabs
746
- .filter((tab) => tab.tab_value?.toString().toLowerCase() !== 'all')
747
- .reduce((acc, tab) => acc + tab.tab_value_count, 0);
748
-
749
- const othersCount = allCount - knownTabCountSum;
750
- filteredTabs.push({
751
- tab_value: 'OTHERS',
752
- tab_value_count: othersCount < 0 ? 0 : othersCount,
753
- });
754
- }
755
- } else {
756
- filteredTabs = allTabs;
757
- }
758
-
759
- return filteredTabs;
760
- }
761
-
762
- // SORTING LOGIC FOR TABS
763
- private async sortTabsByShowList(
764
- qb: any,
765
- sortby: any,
766
- layoutPreference: any,
767
- tabs: any,
768
- ) {
769
- if (layoutPreference && layoutPreference[0]?.mapped_json?.sorting) {
770
- if (Array.isArray(layoutPreference[0]?.mapped_json?.sorting?.tabs)) {
771
- const preferenceTabArray =
772
- layoutPreference[0]?.mapped_json?.sorting?.tabs;
773
- const tabFilter = preferenceTabArray.find(
774
- (tabData) => tabData.tab_name === tabs?.value,
775
- );
776
- tabFilter?.sortby.forEach(({ column, order }) => {
777
- qb.addOrderBy(
778
- `e.${column}`,
779
- order?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
780
- );
781
- });
782
- } else if (
783
- Array.isArray(layoutPreference[0]?.mapped_json?.sorting?.sortby)
784
- ) {
785
- layoutPreference[0]?.mapped_json?.sorting?.sortby?.forEach(
786
- ({ column, order }) => {
787
- qb.addOrderBy(
788
- `e.${column}`,
789
- order?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
790
- );
791
- },
792
- );
793
- }
794
- }
795
-
796
- if (Array.isArray(sortby) && sortby.length > 0) {
797
- sortby.forEach(({ sortColum, sortType }) => {
798
- if (sortColum) {
799
- qb.addOrderBy(
800
- `e.${sortColum}`,
801
- sortType?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
802
- );
803
- }
804
- });
805
- } else {
806
- // fallback if no explicit sortby sent
807
- qb.addOrderBy('e.created_date', 'DESC');
808
- }
809
-
810
- return qb;
811
- }
812
-
813
- private parseFilters(raw: any, isSingle = false): any[] {
814
- if (!raw) return [];
815
-
816
- if (typeof raw === 'string') {
817
- try {
818
- const parsed = JSON.parse(raw);
819
- return isSingle ? [parsed] : parsed;
820
- } catch {
821
- throw new BadRequestException('Invalid JSON in filter input');
822
- }
823
- }
824
-
825
- return isSingle ? [raw] : Array.isArray(raw) ? raw : [];
826
- }
827
-
828
- private async getSavedFilters(code?: string): Promise<any[]> {
829
- if (!code) return [];
830
-
831
- const savedFilter = await this.savedFilterService.getDetailsByCode(code);
832
- if (!savedFilter) {
833
- throw new BadRequestException(`Saved filter not found for code: ${code}`);
834
- }
835
- return savedFilter;
836
- }
837
-
838
- private buildWhereClauses(
839
- filters: any[],
840
- attributeMeta: Record<string, any>,
841
- ) {
842
- return filters
843
- .map((f) => {
844
- const clause = this.buildCondition(
845
- f,
846
- attributeMeta[f.filter_attribute],
847
- );
848
-
849
- if (!clause) return null;
850
-
851
- // Force every param to be a string
852
- if (clause.params) {
853
- Object.keys(clause.params).forEach((k) => {
854
- const val = clause.params[k];
855
- if (!Array.isArray(val)) {
856
- clause.params[k] = String(val); // only convert scalar values
857
- }
858
- });
859
- }
860
-
861
- return clause;
862
- })
863
- .filter(
864
- (clause): clause is { query: string; params: Record<string, string> } =>
865
- clause !== null,
866
- );
867
- }
868
-
869
- private buildCondition(
870
- filter: any,
871
- meta: any,
872
- ): { query: string; params: any } | null {
873
- if (!meta) return null;
874
-
875
- let attr = filter.filter_attribute;
876
- const val = filter.filter_value;
877
- const op = filter.filter_operator;
878
- const key = `param_${attr}_${Math.random().toString(36).substring(2, 8)}`;
879
-
880
- // if (
881
- // (meta.data_source_type === 'entity' ||
882
- // meta.data_source_type === 'master') &&
883
- // !filter.skip_id
884
- // ) {
885
- // attr = `${attr}_id`;
886
- // }
887
-
888
- switch (meta.element_type) {
889
- case 'text':
890
- return this.buildTextCondition(attr, op, val, key);
891
- case 'number':
892
- return this.buildNumberCondition(attr, op, val, key);
893
- case 'date':
894
- return this.buildDateCondition(attr, op, val, key);
895
- case 'select':
896
- return this.buildMultiSelectCondition(attr, op, val, key);
897
- case 'multiselect':
898
- return this.buildMultiSelectCondition(attr, op, val, key);
899
- case 'checkbox':
900
- return this.buildMultiSelectCondition(attr, op, val, key);
901
- case 'radio':
902
- return this.buildMultiSelectCondition(attr, op, val, key);
903
- case 'year':
904
- return this.buildYearCondition(attr, op, val, key);
905
- default:
906
- return null;
907
- }
908
- }
909
-
910
- private buildTextCondition(attr: string, op: string, val: any, key: string) {
911
- switch (op) {
912
- case 'contains':
913
- return {
914
- query: `LOWER(e.${attr}) LIKE :${key}`,
915
- params: { [key]: `%${val ? val.toLowerCase() : ''}%` },
916
- };
917
- case 'equal':
918
- return {
919
- query: `e.${attr} = :${key}`,
920
- params: { [key]: val },
921
- };
922
- case 'not_equal':
923
- return {
924
- query: `e.${attr} != :${key}`,
925
- params: { [key]: val },
926
- };
927
- case 'empty':
928
- return {
929
- query: `e.${attr} IS NULL`,
930
- params: {},
931
- };
932
- case 'not_empty':
933
- return {
934
- query: `e.${attr} IS NOT NULL`,
935
- params: {},
936
- };
937
- default:
938
- throw new BadRequestException(`Unsupported operator for text: ${op}`);
939
- }
940
- }
941
-
942
- private buildNumberCondition(
943
- attr: string,
944
- op: string,
945
- val: any,
946
- key: string,
947
- ) {
948
- switch (op) {
949
- case 'equal':
950
- return { query: `e.${attr} = :${key}`, params: { [key]: val } };
951
- case 'not_equal':
952
- return { query: `e.${attr} != :${key}`, params: { [key]: val } };
953
- case 'greater_than':
954
- return { query: `e.${attr} > :${key}`, params: { [key]: val } };
955
- case 'less_than':
956
- return { query: `e.${attr} < :${key}`, params: { [key]: val } };
957
- case 'less_than_qual_to':
958
- return { query: `e.${attr} <= :${key}`, params: { [key]: val } };
959
- case 'greater_than_qual_to':
960
- return { query: `e.${attr} >= :${key}`, params: { [key]: val } };
961
- case 'empty':
962
- return { query: `e.${attr} IS NULL`, params: {} };
963
- case 'not_empty':
964
- return { query: `e.${attr} IS NOT NULL`, params: {} };
965
-
966
- default:
967
- throw new BadRequestException(`Unsupported operator for number: ${op}`);
968
- }
969
- }
970
-
971
- private buildDateCondition(attr: string, op: string, val: any, key: string) {
972
- const dateColumn = `DATE(e.${attr})`;
973
- const monthColumn = `DATE_TRUNC('month', e.${attr})`;
974
-
975
- // convert to number when needed
976
- const numVal = Number(val);
977
-
978
- // INSIDE buildDateCondition
979
- const subtractBusinessDays = (days: number): string => {
980
- let d = new Date();
981
- let count = 0;
982
-
983
- while (count < days) {
984
- d.setDate(d.getDate() - 1);
985
-
986
- const day = d.getDay(); // 0 = Sunday, 6 = Saturday
987
-
988
- if (day !== 0 && day !== 6) {
989
- count++;
990
- }
991
- }
992
-
993
- return d.toISOString().split('T')[0];
994
- };
995
-
996
- switch (op) {
997
- // ============================================
998
- // BASIC COMPARISONS
999
- // ============================================
1000
- case 'equal':
1001
- case 'is':
1002
- return {
1003
- query: `${dateColumn} = :${key}`,
1004
- params: { [key]: val },
1005
- };
1006
-
1007
- case 'before':
1008
- case 'is_before':
1009
- return {
1010
- query: `${dateColumn} < :${key}`,
1011
- params: { [key]: val },
1012
- };
1013
-
1014
- case 'after':
1015
- case 'is_after':
1016
- return {
1017
- query: `${dateColumn} > :${key}`,
1018
- params: { [key]: val },
1019
- };
1020
-
1021
- case 'is_on_or_before':
1022
- return {
1023
- query: `${dateColumn} <= :${key}`,
1024
- params: { [key]: val },
1025
- };
1026
-
1027
- case 'is_on_or_after':
1028
- return {
1029
- query: `${dateColumn} >= :${key}`,
1030
- params: { [key]: val },
1031
- };
1032
-
1033
- // ============================================
1034
- // EMPTY / NOT EMPTY
1035
- // ============================================
1036
- case 'empty':
1037
- return {
1038
- query: `e.${attr} IS NULL`,
1039
- params: {},
1040
- };
1041
-
1042
- case 'not_empty':
1043
- return {
1044
- query: `e.${attr} IS NOT NULL`,
1045
- params: {},
1046
- };
1047
-
1048
- // ============================================
1049
- // DAY OFFSET LOGIC (ALWAYS ADJUST -1 DAY)
1050
- // ============================================
1051
- case 'is_day_before':
1052
- if (isNaN(numVal)) {
1053
- throw new BadRequestException(
1054
- 'Value must be a number for is_day_before',
1055
- );
1056
- }
1057
-
1058
- const dayBefore = (() => {
1059
- const d = new Date();
1060
- d.setDate(d.getDate() - numVal);
1061
-
1062
- // Format as YYYY-MM-DD in IST
1063
- return d.toLocaleDateString('en-CA', { timeZone: 'Asia/Kolkata' });
1064
- })();
1065
-
1066
- return {
1067
- query: `${dateColumn} <= :${key}`,
1068
- params: { [key]: dayBefore },
1069
- };
1070
-
1071
- case 'is_day_after':
1072
- if (isNaN(numVal)) {
1073
- throw new BadRequestException(
1074
- 'Value must be a number for is_day_after',
1075
- );
1076
- }
1077
-
1078
- const dayAfter = (() => {
1079
- const d = new Date();
1080
- d.setDate(d.getDate() - 1 + numVal); // -1 because DB stores previous day
1081
- return d.toISOString().split('T')[0];
1082
- })();
1083
-
1084
- return {
1085
- query: `${dateColumn} > :${key}`,
1086
- params: { [key]: dayAfter },
1087
- };
1088
-
1089
- // ============================================
1090
- // MONTH OFFSET LOGIC
1091
- // ============================================
1092
- case 'is_month_before':
1093
- if (isNaN(numVal)) {
1094
- throw new BadRequestException(
1095
- 'Value must be a number for is_month_before',
1096
- );
1097
- }
1098
-
1099
- const monthBefore = (() => {
1100
- const d = new Date();
1101
- d.setMonth(d.getMonth() - numVal);
1102
- d.setDate(1);
1103
- d.setDate(d.getDate() - 1);
1104
- return d.toISOString().split('T')[0];
1105
- })();
1106
-
1107
- return {
1108
- query: `${monthColumn} < DATE_TRUNC('month', (:${key})::date)`,
1109
- params: { [key]: monthBefore },
1110
- };
1111
-
1112
- case 'is_month_after':
1113
- if (isNaN(numVal)) {
1114
- throw new BadRequestException(
1115
- 'Value must be a number for is_month_after',
1116
- );
1117
- }
1118
-
1119
- const monthAfter = (() => {
1120
- const d = new Date();
1121
- d.setMonth(d.getMonth() + numVal);
1122
- d.setDate(1);
1123
- d.setDate(d.getDate() - 1);
1124
- return d.toISOString().split('T')[0];
1125
- })();
1126
-
1127
- return {
1128
- query: `${monthColumn} > DATE_TRUNC('month', (:${key})::date)`,
1129
- params: { [key]: monthAfter },
1130
- };
1131
-
1132
- // ===== BETWEEN =====
1133
- case 'between': {
1134
- if (typeof val === 'string') {
1135
- val = val.split(',').map((v) => v.trim());
1136
- }
1137
-
1138
- if (
1139
- !Array.isArray(val) ||
1140
- val.length !== 2 ||
1141
- val[0] === '' ||
1142
- val[1] === ''
1143
- ) {
1144
- console.log(`Invalid value for between: ${val}`);
1145
- return null;
1146
- }
1147
-
1148
- return {
1149
- query: `${dateColumn} BETWEEN :${key}_start AND :${key}_end`,
1150
- params: {
1151
- [`${key}_start`]: val[0],
1152
- [`${key}_end`]: val[1],
1153
- },
1154
- };
1155
- }
1156
-
1157
- // ===== TODAY =====
1158
- case 'today': {
1159
- const today = moment().format('YYYY-MM-DD');
1160
- return {
1161
- query: `${dateColumn} = :today`,
1162
- params: { today },
1163
- };
1164
- }
1165
-
1166
- // ============================================
1167
- // BUSINESS DAY OFFSET LOGIC (SKIPS WEEKENDS)
1168
- // ALWAYS ADJUST -1 DAY FOR DB SHIFT
1169
- // ============================================
1170
-
1171
- case 'is_before_business_days': {
1172
- if (isNaN(numVal)) {
1173
- throw new BadRequestException('Value must be a number');
1174
- }
1175
-
1176
- const targetDate = subtractBusinessDays(numVal);
1177
-
1178
- return {
1179
- query: `${dateColumn} <= :${key} AND EXTRACT(DOW FROM ${dateColumn}) NOT IN (0, 6)`,
1180
- params: { [key]: targetDate },
1181
- };
1182
- }
1183
-
1184
- case 'is_after_business_days': {
1185
- if (isNaN(numVal)) {
1186
- throw new BadRequestException(
1187
- 'Value must be a number for is_after_business_days',
1188
- );
1189
- }
1190
-
1191
- const businessAfter = (() => {
1192
- let d = new Date();
1193
- let count = 0;
1194
-
1195
- while (count < numVal) {
1196
- d.setDate(d.getDate() + 1);
1197
- const day = d.getDay(); // 0=Sun, 6=Sat
1198
- if (day !== 0 && day !== 6) count++;
1199
- }
1200
-
1201
- // DB shift -1 day
1202
- d.setDate(d.getDate() - 1);
1203
-
1204
- return d.toISOString().split('T')[0];
1205
- })();
1206
-
1207
- return {
1208
- query: `${dateColumn} > :${key}`,
1209
- params: { [key]: businessAfter },
1210
- };
1211
- }
1212
-
1213
- // ============================================
1214
- // IN LAST DAY (range: today to N days before)
1215
- // ============================================
1216
- case 'in_last_day': {
1217
- if (isNaN(numVal)) {
1218
- throw new BadRequestException(
1219
- 'Value must be a number for in_last_day',
1220
- );
1221
- }
1222
-
1223
- const today = (() => {
1224
- const d = new Date();
1225
- return d.toISOString().split('T')[0];
1226
- })();
1227
-
1228
- const lastN = (() => {
1229
- const d = new Date();
1230
- d.setDate(d.getDate() - numVal);
1231
- return d.toISOString().split('T')[0];
1232
- })();
1233
-
1234
- return {
1235
- query: `${dateColumn} BETWEEN :${key}_start AND :${key}_end`,
1236
- params: {
1237
- [`${key}_start`]: lastN,
1238
- [`${key}_end`]: today,
1239
- },
1240
- };
1241
- }
1242
-
1243
- // ============================================
1244
- // IN NEXT DAY (range: today to N days after)
1245
- // ============================================
1246
- case 'in_next_day': {
1247
- if (isNaN(numVal)) {
1248
- throw new BadRequestException(
1249
- 'Value must be a number for in_next_day',
1250
- );
1251
- }
1252
-
1253
- const today = (() => {
1254
- const d = new Date();
1255
- return d.toISOString().split('T')[0];
1256
- })();
1257
-
1258
- const nextN = (() => {
1259
- const d = new Date();
1260
- d.setDate(d.getDate() + numVal);
1261
- return d.toISOString().split('T')[0];
1262
- })();
1263
-
1264
- return {
1265
- query: `${dateColumn} BETWEEN :${key}_start AND :${key}_end`,
1266
- params: {
1267
- [`${key}_start`]: today,
1268
- [`${key}_end`]: nextN,
1269
- },
1270
- };
1271
- }
1272
-
1273
- default:
1274
- throw new BadRequestException(`Unsupported operator for date: ${op}`);
1275
- }
1276
- }
1277
-
1278
- private buildSelectCondition(
1279
- attr: string,
1280
- op: string,
1281
- val: any,
1282
- key: string,
1283
- ) {
1284
- switch (op) {
1285
- case 'equal':
1286
- return { query: `e.${attr} = :${key}`, params: { [key]: val } };
1287
- case 'not_equal':
1288
- return { query: `e.${attr} != :${key}`, params: { [key]: val } };
1289
- case 'empty':
1290
- return { query: `e.${attr} IS NULL`, params: {} };
1291
- case 'not_empty':
1292
- return { query: `e.${attr} IS NOT NULL`, params: {} };
1293
- default:
1294
- throw new BadRequestException(`Unsupported operator for select: ${op}`);
1295
- }
1296
- }
1297
-
1298
- private buildMultiSelectCondition(
1299
- attr: string,
1300
- op: string,
1301
- val: any,
1302
- key: string,
1303
- ) {
1304
- let arr: string[] = [];
1305
-
1306
- // SAFETY: Convert all to array
1307
- if (Array.isArray(val)) {
1308
- arr = val.map((v) => String(v));
1309
- } else if (typeof val === 'string') {
1310
- arr = val.split(',').map((v) => v.trim());
1311
- } else {
1312
- throw new BadRequestException(`Invalid multiselect value`);
1313
- }
1314
-
1315
- if (arr.length === 0) {
1316
- return { query: '1=1', params: {} };
1317
- }
1318
- if (op === 'equal') {
1319
- return {
1320
- query: `e.${attr}::text = ANY(:${key})`,
1321
- params: { [key]: `{${arr.join(',')}}` },
1322
- };
1323
- }
1324
-
1325
- if (op === 'not_equal') {
1326
- return {
1327
- query: `NOT (e.${attr}::text = ANY(:${key}))`,
1328
- params: { [key]: `{${arr.join(',')}}` },
1329
- };
1330
- }
1331
-
1332
-
1333
-
1334
- if (op === 'contains') {
1335
- return {
1336
- query: `e.${attr}::text LIKE :${key}`,
1337
- params: { [key]: `%${val}%` },
1338
- };
1339
- }
1340
-
1341
- if (op === 'not_contains') {
1342
- return {
1343
- query: `e.${attr}::text NOT LIKE :${key}`,
1344
- params: { [key]: `%${val}%` },
1345
- };
1346
- }
1347
-
1348
- if (op === 'empty') return { query: `e.${attr} IS NULL`, params: {} };
1349
- if (op === 'not_empty')
1350
- return { query: `e.${attr} IS NOT NULL`, params: {} };
1351
-
1352
- throw new BadRequestException(`Unsupported operator: ${op}`);
1353
- }
1354
-
1355
- private buildYearCondition(attr: string, op: string, val: any, key: string) {
1356
- switch (op) {
1357
- case 'equal':
1358
- return {
1359
- query: `e.${attr} = :${key}`,
1360
- params: { [key]: parseInt(val, 10) },
1361
- };
1362
- case 'not_equal':
1363
- return {
1364
- query: `e.${attr} != :${key}`,
1365
- params: { [key]: parseInt(val, 10) },
1366
- };
1367
- case 'greater_than':
1368
- return {
1369
- query: `e.${attr} > :${key}`,
1370
- params: { [key]: parseInt(val, 10) },
1371
- };
1372
- case 'less_than':
1373
- return {
1374
- query: `e.${attr} < :${key}`,
1375
- params: { [key]: parseInt(val, 10) },
1376
- };
1377
- default:
1378
- throw new BadRequestException(`Unsupported operator for year: ${op}`);
1379
- }
1380
- }
1381
-
1382
- /**
1383
- * Core filter data retrieval method
1384
- * @param entityType - Entity type to query
1385
- * @param filterConditions - Array of filter conditions
1386
- * @param sortColumns - Array of sort columns with order
1387
- * @param columns - Array of attribute keys to select (if empty, returns id and name)
1388
- * @param flatJson - Whether to use flat JSON structure
1389
- * @param ids - Optional array of IDs to filter by
1390
- * @param pagination - Pagination parameters (page, size)
1391
- * @param userData - Logged in user data
1392
- * @returns Object with entity_list and pagination info
1393
- */
1394
- async GetFilterData(
1395
- entityType: string,
1396
- filterConditions: FilterCondition[] = [],
1397
- sortColumns: { column: string; order: 'ASC' | 'DESC' }[] = [],
1398
- columns: string[] = [],
1399
- flatJson: boolean = false,
1400
- ids: number[] = [],
1401
- pagination?: { page: number; size: number },
1402
- userData?: any,
1403
- ) {
1404
- console.log('🔹 [GetFilterData] Starting with params:', {
1405
- entityType,
1406
- filterConditions,
1407
- sortColumns,
1408
- columns,
1409
- flatJson,
1410
- ids,
1411
- pagination,
1412
- });
1413
-
1414
- // Get EntityMaster by EntityType
1415
- const entityMeta = await this.entityMasterService.getEntityData(
1416
- entityType,
1417
- userData,
1418
- );
1419
- const tableName = entityMeta?.data_source;
1420
-
1421
- if (!tableName) {
1422
- throw new BadRequestException(`Invalid entity_type: ${entityType}`);
1423
- }
1424
-
1425
- // Get AttributeMaster by EntityType
1426
- const getAttributeColumnMeta =
1427
- await this.attributeMasterService.findAttributesByMappedEntityType(
1428
- entityType,
1429
- userData,
1430
- );
1431
-
1432
- const attributeMetaMap = getAttributeColumnMeta.reduce(
1433
- (acc, attr) => {
1434
- acc[attr.attribute_key] = attr;
1435
- return acc;
1436
- },
1437
- {} as Record<string, any>,
1438
- );
1439
-
1440
- // Build WHERE clauses using existing function
1441
- const whereClauses = this.buildWhereClauses(
1442
- filterConditions,
1443
- attributeMetaMap,
1444
- );
1445
-
1446
- // Add IDs condition if provided
1447
- if (ids && ids.length > 0) {
1448
- const idsStr = ids.map((id) => String(id));
1449
- whereClauses.push({
1450
- query: 'e.id::text = ANY(:ids)',
1451
- params: { ids: `{${idsStr.join(',')}}` },
1452
- });
1453
- }
1454
-
1455
- // Build SELECT clause
1456
- let selectClause = 'e.*'; // Default to all columns
1457
- if (columns && columns.length > 0) {
1458
- // Select specific columns
1459
- const validColumns = columns
1460
- .filter((col) => attributeMetaMap[col])
1461
- .map((col) => `e.${col}`)
1462
- .join(', ');
1463
- selectClause = validColumns || 'e.id, e.name';
1464
- } else {
1465
- // If columns is empty, return id and name
1466
- selectClause = 'e.id, e.name';
1467
- }
1468
-
1469
- // Build query
1470
- let qb = this.entityManager
1471
- .createQueryBuilder()
1472
- .select(selectClause)
1473
- .from(`${this.schema}.${tableName}`, 'e');
1474
-
1475
- // Apply WHERE clauses
1476
- whereClauses.forEach((clause) => qb.andWhere(clause.query, clause.params));
1477
-
1478
- // Build SORT clauses
1479
- if (sortColumns && sortColumns.length > 0) {
1480
- sortColumns.forEach(({ column, order }) => {
1481
- if (attributeMetaMap[column]) {
1482
- qb.addOrderBy(`e.${column}`, order);
1483
- }
1484
- });
1485
- }
1486
-
1487
- // Apply pagination if provided
1488
- let page = 1;
1489
- let size = 10;
1490
- if (pagination && pagination.page > 0 && pagination.size > 0) {
1491
- page = pagination.page;
1492
- size = pagination.size;
1493
- qb.skip((page - 1) * size).take(size);
1494
- }
1495
-
1496
- // Execute query
1497
- const query = qb.getQuery();
1498
- console.log('🔹 [GetFilterData] Executing query:', query);
1499
-
1500
- const entity_list = await qb.getRawMany();
1501
-
1502
- // Get total count
1503
- const countQb = this.entityManager
1504
- .createQueryBuilder()
1505
- .select('COUNT(*)', 'count')
1506
- .from(`${this.schema}.${tableName}`, 'e');
1507
- whereClauses.forEach((clause) =>
1508
- countQb.andWhere(clause.query, clause.params),
1509
- );
1510
- const countResult = await countQb.getRawOne();
1511
- const total = parseInt(countResult.count, 10);
1512
-
1513
- console.log(
1514
- `🔹 [GetFilterData] Fetched ${entity_list.length} records, total: ${total}`,
1515
- );
1516
-
1517
- const result: any = {
1518
- entity_list,
1519
- };
1520
-
1521
- // Add pagination info if pagination was requested
1522
- if (pagination) {
1523
- result.pagination = {
1524
- total,
1525
- page,
1526
- size,
1527
- totalPages: Math.ceil(total / size),
1528
- hasNextPage: page * size < total,
1529
- hasPreviousPage: page > 1,
1530
- };
1531
- }
1532
-
1533
- return result;
1534
- }
1535
-
1536
- /**
1537
- * Internal filter data retrieval with filter code resolution
1538
- * @param entityType - Entity type to query
1539
- * @param filterCode - Saved filter code to resolve
1540
- * @param quickFilter - Quick filter conditions
1541
- * @param columns - Array of attribute keys to select
1542
- * @param flatJson - Whether to use flat JSON structure
1543
- * @param ids - Optional array of IDs to filter by
1544
- * @param userData - Logged in user data
1545
- * @returns Object with entity_list and pagination info
1546
- */
1547
- async GetFilterDataInternal(
1548
- entityType: string,
1549
- filterCode?: string,
1550
- quickFilter: FilterCondition[] = [],
1551
- columns: string[] = [],
1552
- flatJson: boolean = false,
1553
- ids: number[] = [],
1554
- userData?: any,
1555
- ) {
1556
- console.log('🔸 [GetFilterDataInternal] Starting with params:', {
1557
- entityType,
1558
- filterCode,
1559
- quickFilter,
1560
- columns,
1561
- flatJson,
1562
- ids,
1563
- });
1564
-
1565
- // Aggregate filterCode + quickfilter
1566
- const savedFilters = await this.getSavedFilters(filterCode);
1567
-
1568
- // Normalize saved filters to FilterCondition format
1569
- const savedFiltersNormalized = savedFilters.map((f) => ({
1570
- filter_attribute: f.filter_attribute,
1571
- filter_operator: f.filter_operator,
1572
- filter_value: f.filter_value,
1573
- filter_entity_type: entityType,
1574
- }));
1575
-
1576
- // Merge all filters
1577
- const allFilters = [...quickFilter, ...savedFiltersNormalized].filter(
1578
- (f) => f.filter_value !== '' && f.filter_value != null,
1579
- );
1580
-
1581
- console.log('🔸 [GetFilterDataInternal] Aggregated filters:', allFilters);
1582
-
1583
- // Call GetFilterData
1584
- return await this.GetFilterData(
1585
- entityType,
1586
- allFilters,
1587
- [], // No sort columns at this level
1588
- columns,
1589
- flatJson,
1590
- ids,
1591
- undefined, // No pagination at this level
1592
- userData,
1593
- );
1594
- }
1595
-
1596
- /**
1597
- * Filter data retrieval for listing with layout preferences
1598
- * @param entityType - Entity type to query
1599
- * @param quickFilter - Quick filter conditions
1600
- * @param filterCode - Saved filter code to resolve
1601
- * @param attributeFilter - Attribute filter conditions
1602
- * @param sortColumn - Sort column with order (will be overridden by layout preference)
1603
- * @param pagination - Pagination parameters
1604
- * @param userData - Logged in user data
1605
- * @returns Object with entity_list and pagination info
1606
- */
1607
- async GetFilterDataForListing(
1608
- entityType: string,
1609
- quickFilter: FilterCondition[] = [],
1610
- filterCode?: string,
1611
- attributeFilter: FilterCondition[] = [],
1612
- sortColumn?: { column: string; order: 'ASC' | 'DESC' },
1613
- pagination?: { page: number; size: number },
1614
- userData?: any,
1615
- ) {
1616
- console.log('🔷 [GetFilterDataForListing] Starting with params:', {
1617
- entityType,
1618
- quickFilter,
1619
- filterCode,
1620
- attributeFilter,
1621
- sortColumn,
1622
- pagination,
1623
- });
1624
-
1625
- const { level_type, level_id, id: user_id } = userData || {};
1626
-
1627
- // Check flat_json by EntityMaster
1628
- const entityMeta = await this.entityMasterService.getEntityData(
1629
- entityType,
1630
- userData,
1631
- );
1632
- const flatJson = (entityMeta as any)?.flat_json || false;
1633
-
1634
- // Aggregate quickfilter[], attributeFilter[] and filterCode
1635
- const savedFilters = await this.getSavedFilters(filterCode);
1636
- const savedFiltersNormalized = savedFilters.map((f) => ({
1637
- filter_attribute: f.filter_attribute,
1638
- filter_operator: f.filter_operator,
1639
- filter_value: f.filter_value,
1640
- filter_entity_type: entityType,
1641
- }));
1642
-
1643
- const allFilters = [
1644
- ...quickFilter,
1645
- ...attributeFilter,
1646
- ...savedFiltersNormalized,
1647
- ].filter((f) => f.filter_value !== '' && f.filter_value != null);
1648
-
1649
- console.log('🔷 [GetFilterDataForListing] Aggregated filters:', allFilters);
1650
-
1651
- // Get LayoutPreference by (EntityType, user_id, type:layout)
1652
- let layoutPreferenceRepo =
1653
- this.reflectionHelper.getRepoService('LayoutPreference');
1654
-
1655
- const layoutPreference = await layoutPreferenceRepo.findOne({
1656
- where: {
1657
- user_id: user_id,
1658
- mapped_entity_type: entityType,
1659
- mapped_level_id: level_id,
1660
- mapped_level_type: level_type,
1661
- type: 'layout',
1662
- },
1663
- });
1664
-
1665
- // Get col[] from LayoutPreference
1666
- let columns: string[] = [];
1667
- if (layoutPreference?.mapped_json?.columns) {
1668
- columns = layoutPreference.mapped_json.columns.map(
1669
- (col: any) => col.attribute_key || col,
1670
- );
1671
- } else {
1672
- // Default columns when no layout preference exists
1673
- columns = ['id', 'name', 'code', 'status', 'description'];
1674
- }
1675
-
1676
- console.log('🔷 [GetFilterDataForListing] Layout columns:', columns);
1677
-
1678
- // Get sortColumns[] from LayoutPreference (override sortColumn if exists)
1679
- let sortColumns: { column: string; order: 'ASC' | 'DESC' }[] = [];
1680
-
1681
- if (layoutPreference?.mapped_json?.sorting?.sortby) {
1682
- // Layout preference sorting exists - override sortColumn
1683
- sortColumns = layoutPreference.mapped_json.sorting.sortby.map(
1684
- (sort: any) => ({
1685
- column: sort.column,
1686
- order: sort.order?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
1687
- }),
1688
- );
1689
- console.log(
1690
- '🔷 [GetFilterDataForListing] Using layout preference sorting:',
1691
- sortColumns,
1692
- );
1693
- } else if (sortColumn) {
1694
- // No layout preference - use provided sortColumn
1695
- sortColumns = [sortColumn];
1696
- console.log(
1697
- '🔷 [GetFilterDataForListing] Using provided sortColumn:',
1698
- sortColumns,
1699
- );
1700
- }
1701
-
1702
- // Call GetFilterData
1703
- return await this.GetFilterData(
1704
- entityType,
1705
- allFilters,
1706
- sortColumns,
1707
- columns,
1708
- flatJson,
1709
- [],
1710
- pagination,
1711
- userData,
1712
- );
1713
- }
1714
-
1715
- async queryWithSchema(sql: string, params: any[] = []) {
1716
- await this.entityManager.query('BEGIN');
1717
- await this.entityManager.query(`SET LOCAL search_path TO ${this.schema}`);
1718
- const result = await this.entityManager.query(sql, params);
1719
- await this.entityManager.query('COMMIT');
1720
- return result;
1721
- }
1722
- }
1
+ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
2
+ import { AttributeMasterService } from 'src/module/meta/service/attribute-master.service';
3
+ import { EntityMasterService } from 'src/module/meta/service/entity-master.service';
4
+ import { FilterCondition, FilterRequestDto } from '../dto/filter-request.dto';
5
+ import { SavedFilterService } from './saved-filter.service';
6
+
7
+ import * as moment from 'moment';
8
+ import { EntityRelationService } from 'src/module/meta/service/entity-relation.service';
9
+ import { ResolverService } from 'src/module/meta/service/resolver.service';
10
+ import { LoggingService } from 'src/utils/service/loggingUtil.service';
11
+ import { ConfigService } from '@nestjs/config';
12
+ import { ReflectionHelper } from '../../../utils/service/reflection-helper.service';
13
+ import { EntityManager } from 'typeorm';
14
+
15
+ @Injectable()
16
+ export class FilterService {
17
+ constructor(
18
+ private entityManager: EntityManager,
19
+ @Inject('AttributeMasterService')
20
+ private readonly attributeMasterService: AttributeMasterService,
21
+ @Inject('EntityMasterService')
22
+ private readonly entityMasterService: EntityMasterService,
23
+ @Inject('SavedFilterService')
24
+ private readonly savedFilterService: SavedFilterService,
25
+ @Inject('EntityRelationService')
26
+ private readonly entityRelationService: EntityRelationService,
27
+ private readonly resolverService: ResolverService,
28
+ @Inject() protected readonly loggingService: LoggingService,
29
+ private readonly configService: ConfigService,
30
+ private readonly reflectionHelper: ReflectionHelper,
31
+ ) {}
32
+
33
+ schema = this.configService.get('DB_SCHEMA');
34
+
35
+ private async gettab_value_counts(
36
+ tableName: string,
37
+ column: string | undefined,
38
+ whereClauses: { query: string; params: Record<string, any> }[],
39
+ ) {
40
+ if (!column) return [];
41
+
42
+ // let whereSQL = '';
43
+ // const values: any[] = [];
44
+ // let paramIndex = 1; // PostgreSQL placeholders start at $1
45
+
46
+ // if (whereClauses.length > 0) {
47
+ // const clauseParts = whereClauses.map((clause) => {
48
+ // let parsedQuery = clause.query.replace(/\be\./g, ''); // remove e.
49
+
50
+ // Object.entries(clause.params).forEach(([key, val]) => {
51
+ // if (Array.isArray(val)) {
52
+ // // Create ($1,$2,$3)
53
+ // const placeholders = val.map(() => `$${paramIndex++}`).join(', ');
54
+
55
+ // parsedQuery = parsedQuery.replace(
56
+ // new RegExp(`:${key}`, 'g'),
57
+ // `(${placeholders})`,
58
+ // );
59
+
60
+ // values.push(...val.map((v) => String(v)));
61
+ // } else {
62
+ // // Create $1
63
+ // const placeholder = `$${paramIndex++}`;
64
+
65
+ // parsedQuery = parsedQuery.replace(
66
+ // new RegExp(`:${key}`, 'g'),
67
+ // placeholder,
68
+ // );
69
+
70
+ // values.push(String(val));
71
+ // }
72
+ // });
73
+
74
+ // return parsedQuery;
75
+ // });
76
+
77
+ // whereSQL = `WHERE ${clauseParts.join(' AND ')}`;
78
+ // }
79
+
80
+ let whereSQL = '';
81
+ const values: any[] = [];
82
+ let paramIndex = 1;
83
+
84
+ if (whereClauses.length > 0) {
85
+ const clauseParts = whereClauses.map((clause) => {
86
+ let parsedQuery = clause.query.replace(/\be\./g, '');
87
+
88
+ Object.entries(clause.params).forEach(([key, val]) => {
89
+ if (Array.isArray(val)) {
90
+ // Create ($1,$2,$3)
91
+ const placeholders = val.map(() => `$${paramIndex++}`).join(', ');
92
+
93
+ parsedQuery = parsedQuery
94
+ .replace(new RegExp(`:...${key}`, 'g'), `(${placeholders})`)
95
+ .replace(new RegExp(`:${key}`, 'g'), `(${placeholders})`);
96
+
97
+ values.push(...val.map((v) => String(v)));
98
+ } else {
99
+ // Create $1
100
+ const placeholder = `$${paramIndex++}`;
101
+
102
+ parsedQuery = parsedQuery
103
+ .replace(new RegExp(`:...${key}`, 'g'), placeholder)
104
+ .replace(new RegExp(`:${key}`, 'g'), placeholder);
105
+
106
+ values.push(String(val));
107
+ }
108
+ });
109
+
110
+ return parsedQuery;
111
+ });
112
+
113
+ whereSQL = `WHERE ${clauseParts.join(' AND ')}`;
114
+ }
115
+
116
+ const rawSQL = `
117
+ SELECT ${column} AS tab_value, COUNT(*) AS tab_value_count
118
+ FROM ${this.schema}.${tableName} ${whereSQL}
119
+ GROUP BY ${column}
120
+ `;
121
+
122
+ const rows = await this.entityManager.query(rawSQL, values);
123
+
124
+ const total = rows.reduce(
125
+ (sum, r) => sum + parseInt(r.tab_value_count, 10),
126
+ 0,
127
+ );
128
+
129
+ return [
130
+ { tab_value: 'All', tab_value_count: total },
131
+ ...rows.map((r) => ({
132
+ tab_value: r.tab_value ?? 'BLANK',
133
+ tab_value_count: parseInt(r.tab_value_count, 10),
134
+ })),
135
+ ];
136
+ }
137
+
138
+ async applyFilterWrapper(dto: FilterRequestDto) {
139
+ const {
140
+ entity_type,
141
+ quickFilter = [],
142
+ savedFilterCode,
143
+ attributeFilter = [],
144
+ loggedInUser,
145
+ } = dto;
146
+
147
+ // 🔹 Step 1: Collect all filters (from body + savedFilter)
148
+ const savedFilters = await this.getSavedFilters(
149
+ savedFilterCode ?? undefined,
150
+ );
151
+ const allFilters = [
152
+ ...quickFilter,
153
+ ...attributeFilter,
154
+ ...savedFilters,
155
+ ].filter((f) => f.filter_value !== '');
156
+
157
+ // 🔹 Step 2: Group filters by filter_entity_type
158
+ const grouped = allFilters.reduce(
159
+ (acc, f) => {
160
+ if (!acc[f.filter_entity_type]) acc[f.filter_entity_type] = [];
161
+ acc[f.filter_entity_type].push(f);
162
+ return acc;
163
+ },
164
+ {} as Record<string, any[]>,
165
+ );
166
+
167
+ console.log('🟠 [FilterService] Grouped filters by entity type:', grouped);
168
+
169
+ // 🔹 Step 3: Handle sub-entities first
170
+ let intersectionIds: number[] | null = null;
171
+ for (const [subEntityType, filters] of Object.entries(grouped)) {
172
+ if (subEntityType === entity_type) continue; // skip main entity for now
173
+
174
+ let { queryParams, tabs, ...newDto } = dto;
175
+
176
+ const subDto: FilterRequestDto = {
177
+ ...newDto,
178
+ entity_type: subEntityType,
179
+ quickFilter: filters as FilterCondition[],
180
+ savedFilterCode: null, // already merged
181
+ attributeFilter: [],
182
+ };
183
+
184
+ const subResult = await this.applyFilter(subDto);
185
+ const subEntityIds = subResult.data.entity_list.map((row) => row.id);
186
+
187
+ console.log(
188
+ `🧩 [FilterService] Sub-entity ${subEntityType} returned IDs:`,
189
+ subEntityIds,
190
+ );
191
+
192
+ if (!subEntityIds.length) {
193
+ console.log(
194
+ `ℹ️ [FilterService] No records for sub-entity ${subEntityType}, returning empty result`,
195
+ );
196
+ return {
197
+ success: true,
198
+ data: { entity_tabs: [], entity_list: [], pagination: {} },
199
+ };
200
+ }
201
+
202
+ const relatedIds = await this.entityRelationService.getRelatedEntityIds(
203
+ entity_type,
204
+ subEntityType,
205
+ subEntityIds,
206
+ dto.loggedInUser.enterprise_id,
207
+ );
208
+
209
+ intersectionIds =
210
+ intersectionIds === null
211
+ ? relatedIds
212
+ : intersectionIds.filter((id) => relatedIds.includes(id));
213
+
214
+ if (intersectionIds.length === 0) {
215
+ console.log(
216
+ '🚫 [FilterService] No intersection IDs left, returning empty result',
217
+ );
218
+ return {
219
+ success: true,
220
+ data: { entity_tabs: [], entity_list: [], pagination: {} },
221
+ };
222
+ }
223
+ }
224
+
225
+ // 🔹 Step 4: Call applyFilter for main entity
226
+ const mainFilters = grouped[entity_type] || [];
227
+ const mainDto: FilterRequestDto = {
228
+ ...dto,
229
+ quickFilter: [...mainFilters],
230
+ savedFilterCode: null,
231
+ attributeFilter: [],
232
+ };
233
+
234
+ if (intersectionIds && intersectionIds.length > 0) {
235
+ (mainDto.quickFilter ??= []).push({
236
+ filter_attribute: 'id',
237
+ filter_operator: 'equal',
238
+ filter_value: intersectionIds,
239
+ filter_entity_type: entity_type,
240
+ });
241
+ }
242
+
243
+ return await this.applyFilter(mainDto);
244
+ }
245
+
246
+ async applyFilter(dto: FilterRequestDto) {
247
+ const {
248
+ entity_type,
249
+ quickFilter,
250
+ savedFilterCode,
251
+ attributeFilter,
252
+ tabs,
253
+ sortby,
254
+ loggedInUser,
255
+ queryParams,
256
+ customLevelType,
257
+ customLevelId,
258
+ customAppCode,
259
+ defaultfilter,
260
+ } = dto;
261
+
262
+ // abstract user details
263
+ const {
264
+ level_type,
265
+ level_id,
266
+ id: user_id,
267
+ enterprise_id,
268
+ } = loggedInUser || {};
269
+
270
+ // Fetch meta from entity table service
271
+ const entityMeta = await this.entityMasterService.getEntityData(
272
+ entity_type,
273
+ loggedInUser,
274
+ );
275
+ const tableName = entityMeta?.data_source; // data_source is the table name
276
+
277
+ if (!tableName) {
278
+ console.error(`❌ [FilterService] Invalid entity_type: ${entity_type}`);
279
+ throw new BadRequestException(`Invalid entity_type: ${entity_type}`);
280
+ }
281
+
282
+ const getAttributeColumnMeta =
283
+ await this.attributeMasterService.findAttributesByMappedEntityType(
284
+ entity_type,
285
+ loggedInUser,
286
+ );
287
+
288
+ const attributeMetaMap = getAttributeColumnMeta.reduce(
289
+ (acc, attr) => {
290
+ acc[attr.attribute_key] = attr;
291
+ return acc;
292
+ },
293
+ {} as Record<string, any>,
294
+ );
295
+
296
+ // Get and parse saved filters
297
+ const savedFilters = await this.getSavedFilters(
298
+ savedFilterCode ?? undefined,
299
+ );
300
+ const savedFiltersNormalized = savedFilters.map((f) => ({
301
+ filter_attribute: f.filter_attribute,
302
+ filter_operator: f.filter_operator,
303
+ filter_value: f.filter_value,
304
+ }));
305
+
306
+ const baseFilters = [
307
+ ...(quickFilter || []).filter(
308
+ (f) => f.filter_value != null && f.filter_value !== '',
309
+ ),
310
+ ...savedFiltersNormalized.filter(
311
+ (f) => f.filter_value != null && f.filter_value !== '',
312
+ ),
313
+ ...(attributeFilter || []).filter(
314
+ (f) => f.filter_value != null && f.filter_value !== '',
315
+ ),
316
+ ];
317
+
318
+ // 🧱 Build where clausesx
319
+ const baseWhere = this.buildWhereClauses(baseFilters, attributeMetaMap);
320
+
321
+ // Handle TEMPLATE entity special condition
322
+ if (entity_type === 'TEMP' || entity_type === 'TEM') {
323
+ if (level_type === 'ORG') {
324
+ baseWhere.push({
325
+ query: ` ((e.level_type = 'ORG' AND e.level_id = '${loggedInUser.enterprise_id}'))`,
326
+ params: {},
327
+ });
328
+ } else if (level_type === 'SCH') {
329
+ baseWhere.push({
330
+ query: `
331
+ (
332
+ (e.level_type = 'SCH' AND e.level_id = '${loggedInUser.level_id}')
333
+ OR
334
+ (
335
+ e.level_type = 'ORG'
336
+ AND e.level_id = '${loggedInUser.enterprise_id}'
337
+ AND e.enterprise_id = '${loggedInUser.enterprise_id}'
338
+ AND e.code NOT IN (
339
+ SELECT sub.code
340
+ FROM ${this.schema}.frm_wf_comm_template AS sub
341
+ WHERE sub.level_type = 'SCH' AND sub.level_id = '${loggedInUser.level_id}'
342
+ )
343
+ )
344
+ )
345
+ `,
346
+ params: {},
347
+ });
348
+ }
349
+ }
350
+
351
+ // Default org/level clause skip Tsaved-filEMPLATE entity
352
+ if (
353
+ entity_type !== 'ORGP' &&
354
+ entity_type !== 'TEMP' &&
355
+ entity_type !== 'TEM' && // ✅ skip TEMPLATE
356
+ entity_type !== 'SCH' &&
357
+ level_type &&
358
+ level_id &&
359
+ enterprise_id &&
360
+ !customLevelType &&
361
+ !customLevelId &&
362
+ defaultfilter !== 'false' // ✅ only run if defaultfilter is not false
363
+ ) {
364
+ baseWhere.push({
365
+ query: 'e.level_type = :level_type AND e.level_id = :level_id',
366
+ params: {
367
+ level_type,
368
+ level_id,
369
+ },
370
+ });
371
+ }
372
+
373
+ if (entity_type == 'USR' || entity_type == 'UPR') {
374
+ baseWhere.push({
375
+ query: 'e.is_customer is NULL',
376
+ params: {},
377
+ });
378
+ }
379
+
380
+ if (customLevelType && customLevelId && customAppCode) {
381
+ baseWhere.push({
382
+ query:
383
+ 'e.level_type = :customLevelType AND e.level_id = :customLevelId AND e.appcode = :customAppCode',
384
+ params: {
385
+ customLevelType,
386
+ customLevelId: String(customLevelId),
387
+ customAppCode,
388
+ },
389
+ });
390
+ }
391
+
392
+ // Handle queryParams filters
393
+ if (queryParams) {
394
+ Object.entries(queryParams).forEach(([key, value]) => {
395
+ if (!value) return;
396
+
397
+ // Always convert to string
398
+ const strValue = String(value);
399
+
400
+ if (key === 'attributeName' && queryParams['attributeValue']) {
401
+ const attrName = strValue;
402
+ const attrValue = String(queryParams['attributeValue']);
403
+
404
+ baseWhere.push({
405
+ query: `e.${attrName} = :${attrName}`,
406
+ params: { [attrName]: attrValue }, // <-- string
407
+ });
408
+ } else if (key !== 'attributeValue') {
409
+ baseWhere.push({
410
+ query: `e.${key} = :${key}`,
411
+ params: { [key]: strValue }, // <-- string
412
+ });
413
+ }
414
+ });
415
+ }
416
+
417
+ console.log('🟠 [FilterService] Constructed baseWhere clauses:', baseWhere);
418
+
419
+ let layoutPreferenceRepo =
420
+ this.reflectionHelper.getRepoService('LayoutPreference');
421
+
422
+ const layoutPreference = await layoutPreferenceRepo.findOne({
423
+ where: {
424
+ user_id: user_id,
425
+ mapped_entity_type: entity_type,
426
+ mapped_level_id: level_id,
427
+ mapped_level_type: level_type,
428
+ type: 'layout',
429
+ },
430
+ });
431
+
432
+ // Extract layout preference
433
+ const layout = layoutPreference?.mapped_json?.quick_tab || {};
434
+ const showList =
435
+ layout?.show_list?.map((val) => {
436
+ if (typeof val === 'string') {
437
+ // legacy case — old format like ['Active', 'Inactive']
438
+ return val.toLowerCase();
439
+ }
440
+
441
+ if (val && typeof val === 'object' && val.value !== undefined) {
442
+ // new format — [{ label: 'Active', value: '13023' }]
443
+ return String(val.value).toLowerCase();
444
+ }
445
+
446
+ return ''; // fallback safety
447
+ }) || [];
448
+
449
+ let filteredTabs = await this.getFilteredTabs(
450
+ layout,
451
+ showList,
452
+ entityMeta,
453
+ baseWhere,
454
+ tabs,
455
+ loggedInUser,
456
+ );
457
+
458
+ const dataWhere = [...baseWhere];
459
+
460
+ if (tabs?.columnName && tabs?.value) {
461
+ const tabAttrMeta = attributeMetaMap[tabs.columnName];
462
+ const tabValue = tabs.value.toLowerCase();
463
+
464
+ if (tabValue === 'others') {
465
+ // Extract 'value' (IDs) from showList, ignore 'all'
466
+ const valuesToExclude = showList
467
+ .filter((v) => v.label.toLowerCase() !== 'all')
468
+ .map((v) => v.value);
469
+
470
+ if (valuesToExclude.length > 0) {
471
+ for (const value of valuesToExclude) {
472
+ const resolvedId = await this.resolverService.getResolvedId(
473
+ loggedInUser,
474
+ tabs.columnName,
475
+ value,
476
+ entity_type,
477
+ );
478
+ if (resolvedId) {
479
+ const tabCondition = this.buildCondition(
480
+ {
481
+ filter_attribute: tabs.columnName,
482
+ filter_operator: 'not_equal',
483
+ filter_value: [resolvedId],
484
+ skip_id: true,
485
+ },
486
+ tabAttrMeta,
487
+ );
488
+ if (tabCondition) dataWhere.push(tabCondition);
489
+ }
490
+ }
491
+ }
492
+ } else if (tabValue !== 'all') {
493
+ const resolvedId = await this.resolverService.getResolvedId(
494
+ loggedInUser,
495
+ tabs.columnName,
496
+ tabs.value,
497
+ entity_type,
498
+ );
499
+
500
+ if (resolvedId) {
501
+ const tabCondition = this.buildCondition(
502
+ {
503
+ filter_attribute: tabs.columnName,
504
+ filter_operator: 'equal',
505
+ filter_value: [resolvedId],
506
+ skip_id: true,
507
+ },
508
+ tabAttrMeta,
509
+ );
510
+ if (tabCondition) dataWhere.push(tabCondition);
511
+ }
512
+ }
513
+ }
514
+
515
+ let qb = this.entityManager
516
+ .createQueryBuilder()
517
+ .select('e.*')
518
+ .from(`${this.schema}.${tableName}`, 'e');
519
+ dataWhere.forEach((clause) => qb.andWhere(clause.query, clause.params));
520
+
521
+ // Apply sorting
522
+ qb = await this.sortTabsByShowList(qb, sortby, layoutPreference, tabs);
523
+
524
+ // Check if hierarchical mode is enabled
525
+ const isHierarchical = entityMeta?.is_hierarchical === true;
526
+
527
+ let page = dto.page && dto.page > 0 ? dto.page : 1;
528
+ let size = dto.size && dto.size > 0 ? dto.size : 10;
529
+
530
+ // Skip pagination if hierarchical mode is enabled
531
+ if (!isHierarchical && dto.page && dto.size) {
532
+ qb.skip((page - 1) * size).take(size);
533
+ }
534
+
535
+ let query = await qb.getQuery();
536
+
537
+ console.log('------------------------------------------------------\n');
538
+ console.log(query);
539
+ console.log('\n------------------------------------------------------');
540
+ const entity_list = await qb.getRawMany();
541
+
542
+ console.log(`📦 [FilterService] Fetched ${entity_list.length} records`);
543
+
544
+ const dateAttributes = Object.entries(attributeMetaMap)
545
+ .filter(([_, attr]) => attr.element_type === 'date')
546
+ .map(([key]) => key);
547
+
548
+ const formatDate = (dateStr: string | null): string | null =>
549
+ dateStr ? moment(dateStr).format('DD-MM-YYYY') : '';
550
+
551
+ const formatDatesInRow = (row: any): any => {
552
+ const formattedRow = { ...row };
553
+ for (const key of dateAttributes) {
554
+ if (formattedRow[key])
555
+ formattedRow[key] = formatDate(formattedRow[key]);
556
+ }
557
+ return formattedRow;
558
+ };
559
+
560
+ const formattedEntityList = entity_list.map(formatDatesInRow);
561
+
562
+ // Build hierarchy if hierarchical mode is enabled
563
+ let finalEntityList = formattedEntityList;
564
+ if (isHierarchical) {
565
+ finalEntityList = this.buildHierarchyList(formattedEntityList);
566
+ console.log(
567
+ `🌳 [FilterService] Built hierarchical structure with ${finalEntityList.length} root nodes`,
568
+ );
569
+ }
570
+
571
+ const resolvedEntityList = await Promise.all(
572
+ formattedEntityList.map((row) =>
573
+ this.resolverService.getResolvedData(loggedInUser, row, entity_type),
574
+ ),
575
+ );
576
+
577
+ let resolvedTabs = await Promise.all(
578
+ filteredTabs.map(async (tab) => {
579
+ const tabAttrKey = layout?.attribute || tabs?.columnName;
580
+
581
+ if (
582
+ !tabAttrKey ||
583
+ tab.tab_value?.toLowerCase() === 'all' ||
584
+ tab.tab_value?.toLowerCase() === 'others'
585
+ ) {
586
+ return tab;
587
+ }
588
+
589
+ const resolvedVal = await this.resolverService.getResolvedValue(
590
+ loggedInUser,
591
+ tabAttrKey,
592
+ tab.tab_value,
593
+ entity_type,
594
+ );
595
+
596
+ return { ...tab, tab_value: resolvedVal ?? tab.tab_value };
597
+ }),
598
+ );
599
+ resolvedTabs = Object.values(
600
+ resolvedTabs.reduce((acc, item) => {
601
+ if (!acc[item.tab_value]) {
602
+ acc[item.tab_value] = { ...item };
603
+ } else {
604
+ acc[item.tab_value].tab_value_count += item.tab_value_count;
605
+ }
606
+ return acc;
607
+ }, {} as Record<string, { tab_value: string; tab_value_count: number }>)
608
+ );
609
+
610
+ const countQb = this.entityManager
611
+ .createQueryBuilder()
612
+ .select('COUNT(*)', 'count')
613
+ .from(`${this.schema}.${tableName}`, 'e');
614
+ dataWhere.forEach((clause) =>
615
+ countQb.andWhere(clause.query, clause.params),
616
+ );
617
+ const countResult = await countQb.getRawOne();
618
+ const total = parseInt(countResult.count, 10);
619
+
620
+ console.log('📊 [FilterService] Returning final result with total:', total);
621
+
622
+ return {
623
+ success: true,
624
+ data: {
625
+ entity_tabs: resolvedTabs,
626
+ entity_list: resolvedEntityList,
627
+ is_hierarchical: isHierarchical,
628
+ pagination: {
629
+ total,
630
+ page,
631
+ size,
632
+ totalPages: Math.ceil(total / size),
633
+ hasNextPage: page * size < total,
634
+ hasPreviousPage: page > 1,
635
+ },
636
+ },
637
+ };
638
+ }
639
+
640
+ /**
641
+ * Build hierarchical structure from flat entity list using wbs_code
642
+ * @param entityList - Flat list of entities with wbs_code field
643
+ * @returns Hierarchical tree structure with children property
644
+ */
645
+ private buildHierarchyList(entityList: any[]): any[] {
646
+ const entityMap = new Map<string, any>();
647
+ const rootEntities: any[] = [];
648
+
649
+ // Initialize all entities in the map with children array
650
+ entityList.forEach((entity) => {
651
+ if (entity.wbs_code) {
652
+ entityMap.set(entity.wbs_code, {
653
+ ...entity,
654
+ children: [],
655
+ });
656
+ }
657
+ });
658
+
659
+ // Build the hierarchy based on WBS code
660
+ entityList.forEach((entity) => {
661
+ if (!entity.wbs_code) {
662
+ // Entities without wbs_code are treated as root
663
+ rootEntities.push({ ...entity, children: [] });
664
+ return;
665
+ }
666
+
667
+ // Get parent WBS code by removing the last segment
668
+ const parentWbs = entity.wbs_code.split('.').slice(0, -1).join('.');
669
+
670
+ if (parentWbs && entityMap.has(parentWbs)) {
671
+ // Add to parent's children
672
+ entityMap.get(parentWbs).children.push(entityMap.get(entity.wbs_code));
673
+ } else {
674
+ // No parent found, this is a root entity
675
+ rootEntities.push(entityMap.get(entity.wbs_code));
676
+ }
677
+ });
678
+
679
+ return rootEntities;
680
+ }
681
+
682
+ // GET FILTERED TABS LOGIC
683
+ private async getFilteredTabs(
684
+ layout: any,
685
+ showList: any,
686
+ entityMeta: any,
687
+ baseWhere: any,
688
+ tabs: any,
689
+ loggedInUser: any,
690
+ ) {
691
+ let allTabs;
692
+ if (layout.attribute) {
693
+ allTabs = await this.gettab_value_counts(
694
+ entityMeta?.data_source,
695
+ layout.attribute,
696
+ baseWhere,
697
+ );
698
+ } else {
699
+ allTabs = await this.gettab_value_counts(
700
+ entityMeta?.data_source,
701
+ tabs?.columnName,
702
+ baseWhere,
703
+ );
704
+ }
705
+
706
+ let filteredTabs: any[] = [];
707
+
708
+ if (showList?.length > 0) {
709
+ // Extract tab IDs (values)
710
+ const showValues = (showList || []).map((item) => {
711
+ const val = typeof item === 'object' ? item.value : item;
712
+ return val ? String(val).toLowerCase() : '';
713
+ });
714
+
715
+ // Handle "all" logic
716
+ const isAllNeeded = layout?.isAllSelected && !showValues.includes('all');
717
+ if (isAllNeeded) {
718
+ showList.push({ label: 'All', value: 'all' });
719
+ showValues.push('all');
720
+ }
721
+
722
+ // Filter by value IDs (not labels)
723
+ filteredTabs = allTabs.filter((tab) => {
724
+ const tabValueStr = tab.tab_value
725
+ ? String(tab.tab_value).toLowerCase()
726
+ : '';
727
+ return showValues.includes(tabValueStr);
728
+ });
729
+
730
+ // Handle "all" tab
731
+ const allTab = filteredTabs.find(
732
+ (tab) => tab.tab_value?.toString().toLowerCase() === 'all',
733
+ );
734
+ filteredTabs = filteredTabs.filter(
735
+ (tab) => tab.tab_value?.toString().toLowerCase() !== 'all',
736
+ );
737
+
738
+ // SORTING LOGIC
739
+ if (layout.sorting === 'asc') {
740
+ filteredTabs.sort((a, b) =>
741
+ a.tab_value.toString().localeCompare(b.tab_value.toString()),
742
+ );
743
+ } else if (layout.sorting === 'dsc') {
744
+ filteredTabs.sort((a, b) =>
745
+ b.tab_value.toString().localeCompare(a.tab_value.toString()),
746
+ );
747
+ } else if (layout.sorting === 'count_asc') {
748
+ filteredTabs.sort((a, b) => a.tab_value_count - b.tab_value_count);
749
+ } else if (layout.sorting === 'count_dsc') {
750
+ filteredTabs.sort((a, b) => b.tab_value_count - a.tab_value_count);
751
+ } else if (layout.sorting === 'custom') {
752
+ // Keep order same as showList
753
+ const orderMap = new Map<string, number>(
754
+ showList.map((item, index) => [
755
+ item.value?.toString().toLowerCase(),
756
+ index,
757
+ ]),
758
+ );
759
+ filteredTabs.sort((a, b) => {
760
+ const aIndex =
761
+ orderMap.get(a.tab_value?.toString().toLowerCase()) ?? Infinity;
762
+ const bIndex =
763
+ orderMap.get(b.tab_value?.toString().toLowerCase()) ?? Infinity;
764
+ return aIndex - bIndex;
765
+ });
766
+ }
767
+
768
+ // Re-add "all" tab at beginning if needed
769
+ if (allTab) filteredTabs.unshift(allTab);
770
+
771
+ // Combine others if enabled
772
+ if (layout?.isCombineOther) {
773
+ const originalAllTab = allTabs.find(
774
+ (tab) => tab.tab_value?.toString().toLowerCase() === 'all',
775
+ );
776
+ const allCount = originalAllTab?.tab_value_count ?? 0;
777
+ const knownTabCountSum = filteredTabs
778
+ .filter((tab) => tab.tab_value?.toString().toLowerCase() !== 'all')
779
+ .reduce((acc, tab) => acc + tab.tab_value_count, 0);
780
+
781
+ const othersCount = allCount - knownTabCountSum;
782
+ filteredTabs.push({
783
+ tab_value: 'OTHERS',
784
+ tab_value_count: othersCount < 0 ? 0 : othersCount,
785
+ });
786
+ }
787
+ } else {
788
+ filteredTabs = allTabs;
789
+ }
790
+
791
+ return filteredTabs;
792
+ }
793
+
794
+ // SORTING LOGIC FOR TABS
795
+ private async sortTabsByShowList(
796
+ qb: any,
797
+ sortby: any,
798
+ layoutPreference: any,
799
+ tabs: any,
800
+ ) {
801
+ const sorting = layoutPreference?.mapped_json?.sorting;
802
+
803
+ if (sorting) {
804
+ if (Array.isArray(sorting?.tabs)) {
805
+ const preferenceTabArray = sorting?.tabs;
806
+ const tabFilter = preferenceTabArray.find(
807
+ (tabData) => tabData.tab_name?.value === tabs?.value,
808
+ );
809
+
810
+ tabFilter?.sortby?.forEach(({ column, order }) => {
811
+ qb.addOrderBy(
812
+ `e.${column}`,
813
+ order?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
814
+ );
815
+ });
816
+ } else if (Array.isArray(sorting.sortby)) {
817
+ sorting?.sortby?.forEach(({ column, order }) => {
818
+ qb.addOrderBy(
819
+ `e.${column}`,
820
+ order?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
821
+ );
822
+ });
823
+ }
824
+ }
825
+
826
+ if (Array.isArray(sortby) && sortby.length > 0) {
827
+ sortby.forEach(({ sortColum, sortType }) => {
828
+ if (sortColum) {
829
+ qb.addOrderBy(
830
+ `e.${sortColum}`,
831
+ sortType?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
832
+ );
833
+ }
834
+ });
835
+ } else {
836
+ // fallback if no explicit sortby sent
837
+ qb.addOrderBy('e.created_date', 'DESC');
838
+ }
839
+
840
+ return qb;
841
+ }
842
+
843
+ private parseFilters(raw: any, isSingle = false): any[] {
844
+ if (!raw) return [];
845
+
846
+ if (typeof raw === 'string') {
847
+ try {
848
+ const parsed = JSON.parse(raw);
849
+ return isSingle ? [parsed] : parsed;
850
+ } catch {
851
+ throw new BadRequestException('Invalid JSON in filter input');
852
+ }
853
+ }
854
+
855
+ return isSingle ? [raw] : Array.isArray(raw) ? raw : [];
856
+ }
857
+
858
+ private async getSavedFilters(code?: string): Promise<any[]> {
859
+ if (!code) return [];
860
+
861
+ const savedFilter = await this.savedFilterService.getDetailsByCode(code);
862
+ if (!savedFilter) {
863
+ throw new BadRequestException(`Saved filter not found for code: ${code}`);
864
+ }
865
+ return savedFilter;
866
+ }
867
+
868
+ private buildWhereClauses(
869
+ filters: any[],
870
+ attributeMeta: Record<string, any>,
871
+ ) {
872
+ return filters
873
+ .map((f) => {
874
+ const clause = this.buildCondition(
875
+ f,
876
+ attributeMeta[f.filter_attribute],
877
+ );
878
+
879
+ if (!clause) return null;
880
+
881
+ // Force every param to be a string
882
+ if (clause.params) {
883
+ Object.keys(clause.params).forEach((k) => {
884
+ const val = clause.params[k];
885
+ if (!Array.isArray(val)) {
886
+ clause.params[k] = String(val); // only convert scalar values
887
+ }
888
+ });
889
+ }
890
+
891
+ return clause;
892
+ })
893
+ .filter(
894
+ (clause): clause is { query: string; params: Record<string, string> } =>
895
+ clause !== null,
896
+ );
897
+ }
898
+
899
+ private buildCondition(
900
+ filter: any,
901
+ meta: any,
902
+ ): { query: string; params: any } | null {
903
+ if (!meta) return null;
904
+
905
+ let attr = filter.filter_attribute;
906
+ const val = filter.filter_value;
907
+ const op = filter.filter_operator;
908
+ const key = `param_${attr}_${Math.random().toString(36).substring(2, 8)}`;
909
+
910
+ // if (
911
+ // (meta.data_source_type === 'entity' ||
912
+ // meta.data_source_type === 'master') &&
913
+ // !filter.skip_id
914
+ // ) {
915
+ // attr = `${attr}_id`;
916
+ // }
917
+
918
+ switch (meta.element_type) {
919
+ case 'text':
920
+ return this.buildTextCondition(attr, op, val, key);
921
+ case 'number':
922
+ return this.buildNumberCondition(attr, op, val, key);
923
+ case 'date':
924
+ return this.buildDateCondition(attr, op, val, key);
925
+ case 'select':
926
+ return this.buildMultiSelectCondition(attr, op, val, key);
927
+ case 'multiselect':
928
+ return this.buildMultiSelectCondition(attr, op, val, key);
929
+ case 'checkbox':
930
+ return this.buildMultiSelectCondition(attr, op, val, key);
931
+ case 'radio':
932
+ return this.buildMultiSelectCondition(attr, op, val, key);
933
+ case 'year':
934
+ return this.buildYearCondition(attr, op, val, key);
935
+ default:
936
+ return null;
937
+ }
938
+ }
939
+
940
+ private buildTextCondition(attr: string, op: string, val: any, key: string) {
941
+ switch (op) {
942
+ case 'contains':
943
+ return {
944
+ query: `LOWER(e.${attr}) LIKE :${key}`,
945
+ params: { [key]: `%${val ? val.toLowerCase() : ''}%` },
946
+ };
947
+ case 'equal':
948
+ return {
949
+ query: `e.${attr} = :${key}`,
950
+ params: { [key]: val },
951
+ };
952
+ case 'not_equal':
953
+ return {
954
+ query: `e.${attr} != :${key}`,
955
+ params: { [key]: val },
956
+ };
957
+ case 'empty':
958
+ return {
959
+ query: `e.${attr} IS NULL`,
960
+ params: {},
961
+ };
962
+ case 'not_empty':
963
+ return {
964
+ query: `e.${attr} IS NOT NULL`,
965
+ params: {},
966
+ };
967
+ default:
968
+ throw new BadRequestException(`Unsupported operator for text: ${op}`);
969
+ }
970
+ }
971
+
972
+ private buildNumberCondition(
973
+ attr: string,
974
+ op: string,
975
+ val: any,
976
+ key: string,
977
+ ) {
978
+ switch (op) {
979
+ case 'equal':
980
+ return { query: `e.${attr} = :${key}`, params: { [key]: val } };
981
+ case 'not_equal':
982
+ return { query: `e.${attr} != :${key}`, params: { [key]: val } };
983
+ case 'greater_than':
984
+ return { query: `e.${attr} > :${key}`, params: { [key]: val } };
985
+ case 'less_than':
986
+ return { query: `e.${attr} < :${key}`, params: { [key]: val } };
987
+ case 'less_than_qual_to':
988
+ return { query: `e.${attr} <= :${key}`, params: { [key]: val } };
989
+ case 'greater_than_qual_to':
990
+ return { query: `e.${attr} >= :${key}`, params: { [key]: val } };
991
+ case 'empty':
992
+ return { query: `e.${attr} IS NULL`, params: {} };
993
+ case 'not_empty':
994
+ return { query: `e.${attr} IS NOT NULL`, params: {} };
995
+
996
+ default:
997
+ throw new BadRequestException(`Unsupported operator for number: ${op}`);
998
+ }
999
+ }
1000
+
1001
+ private buildDateCondition(attr: string, op: string, val: any, key: string) {
1002
+ const dateColumn = `DATE(e.${attr})`;
1003
+ const monthColumn = `DATE_TRUNC('month', e.${attr})`;
1004
+
1005
+ // convert to number when needed
1006
+ const numVal = Number(val);
1007
+
1008
+ // INSIDE buildDateCondition
1009
+ const subtractBusinessDays = (days: number): string => {
1010
+ let d = new Date();
1011
+ let count = 0;
1012
+
1013
+ while (count < days) {
1014
+ d.setDate(d.getDate() - 1);
1015
+
1016
+ const day = d.getDay(); // 0 = Sunday, 6 = Saturday
1017
+
1018
+ if (day !== 0 && day !== 6) {
1019
+ count++;
1020
+ }
1021
+ }
1022
+
1023
+ return d.toISOString().split('T')[0];
1024
+ };
1025
+
1026
+ switch (op) {
1027
+ // ============================================
1028
+ // BASIC COMPARISONS
1029
+ // ============================================
1030
+ case 'equal':
1031
+ case 'is':
1032
+ return {
1033
+ query: `${dateColumn} = :${key}`,
1034
+ params: { [key]: val },
1035
+ };
1036
+
1037
+ case 'before':
1038
+ case 'is_before':
1039
+ return {
1040
+ query: `${dateColumn} < :${key}`,
1041
+ params: { [key]: val },
1042
+ };
1043
+
1044
+ case 'after':
1045
+ case 'is_after':
1046
+ return {
1047
+ query: `${dateColumn} > :${key}`,
1048
+ params: { [key]: val },
1049
+ };
1050
+
1051
+ case 'is_on_or_before':
1052
+ return {
1053
+ query: `${dateColumn} <= :${key}`,
1054
+ params: { [key]: val },
1055
+ };
1056
+
1057
+ case 'is_on_or_after':
1058
+ return {
1059
+ query: `${dateColumn} >= :${key}`,
1060
+ params: { [key]: val },
1061
+ };
1062
+
1063
+ // ============================================
1064
+ // EMPTY / NOT EMPTY
1065
+ // ============================================
1066
+ case 'empty':
1067
+ return {
1068
+ query: `e.${attr} IS NULL`,
1069
+ params: {},
1070
+ };
1071
+
1072
+ case 'not_empty':
1073
+ return {
1074
+ query: `e.${attr} IS NOT NULL`,
1075
+ params: {},
1076
+ };
1077
+
1078
+ // ============================================
1079
+ // DAY OFFSET LOGIC (ALWAYS ADJUST -1 DAY)
1080
+ // ============================================
1081
+ case 'is_day_before':
1082
+ if (isNaN(numVal)) {
1083
+ throw new BadRequestException(
1084
+ 'Value must be a number for is_day_before',
1085
+ );
1086
+ }
1087
+
1088
+ const dayBefore = (() => {
1089
+ const d = new Date();
1090
+ d.setDate(d.getDate() - numVal);
1091
+
1092
+ // Format as YYYY-MM-DD in IST
1093
+ return d.toLocaleDateString('en-CA', { timeZone: 'Asia/Kolkata' });
1094
+ })();
1095
+
1096
+ return {
1097
+ query: `${dateColumn} <= :${key}`,
1098
+ params: { [key]: dayBefore },
1099
+ };
1100
+
1101
+ case 'is_day_after':
1102
+ if (isNaN(numVal)) {
1103
+ throw new BadRequestException(
1104
+ 'Value must be a number for is_day_after',
1105
+ );
1106
+ }
1107
+
1108
+ const dayAfter = (() => {
1109
+ const d = new Date();
1110
+ d.setDate(d.getDate() - 1 + numVal); // -1 because DB stores previous day
1111
+ return d.toISOString().split('T')[0];
1112
+ })();
1113
+
1114
+ return {
1115
+ query: `${dateColumn} > :${key}`,
1116
+ params: { [key]: dayAfter },
1117
+ };
1118
+
1119
+ // ============================================
1120
+ // MONTH OFFSET LOGIC
1121
+ // ============================================
1122
+ case 'is_month_before':
1123
+ if (isNaN(numVal)) {
1124
+ throw new BadRequestException(
1125
+ 'Value must be a number for is_month_before',
1126
+ );
1127
+ }
1128
+
1129
+ const monthBefore = (() => {
1130
+ const d = new Date();
1131
+ d.setMonth(d.getMonth() - numVal);
1132
+ d.setDate(1);
1133
+ d.setDate(d.getDate() - 1);
1134
+ return d.toISOString().split('T')[0];
1135
+ })();
1136
+
1137
+ return {
1138
+ query: `${monthColumn} < DATE_TRUNC('month', (:${key})::date)`,
1139
+ params: { [key]: monthBefore },
1140
+ };
1141
+
1142
+ case 'is_month_after':
1143
+ if (isNaN(numVal)) {
1144
+ throw new BadRequestException(
1145
+ 'Value must be a number for is_month_after',
1146
+ );
1147
+ }
1148
+
1149
+ const monthAfter = (() => {
1150
+ const d = new Date();
1151
+ d.setMonth(d.getMonth() + numVal);
1152
+ d.setDate(1);
1153
+ d.setDate(d.getDate() - 1);
1154
+ return d.toISOString().split('T')[0];
1155
+ })();
1156
+
1157
+ return {
1158
+ query: `${monthColumn} > DATE_TRUNC('month', (:${key})::date)`,
1159
+ params: { [key]: monthAfter },
1160
+ };
1161
+
1162
+ // ===== BETWEEN =====
1163
+ case 'between': {
1164
+ if (typeof val === 'string') {
1165
+ val = val.split(',').map((v) => v.trim());
1166
+ }
1167
+
1168
+ if (
1169
+ !Array.isArray(val) ||
1170
+ val.length !== 2 ||
1171
+ val[0] === '' ||
1172
+ val[1] === ''
1173
+ ) {
1174
+ console.log(`Invalid value for between: ${val}`);
1175
+ return null;
1176
+ }
1177
+
1178
+ return {
1179
+ query: `${dateColumn} BETWEEN :${key}_start AND :${key}_end`,
1180
+ params: {
1181
+ [`${key}_start`]: val[0],
1182
+ [`${key}_end`]: val[1],
1183
+ },
1184
+ };
1185
+ }
1186
+
1187
+ // ===== TODAY =====
1188
+ case 'today': {
1189
+ const today = moment().format('YYYY-MM-DD');
1190
+ return {
1191
+ query: `${dateColumn} = :today`,
1192
+ params: { today },
1193
+ };
1194
+ }
1195
+
1196
+ // ============================================
1197
+ // BUSINESS DAY OFFSET LOGIC (SKIPS WEEKENDS)
1198
+ // ALWAYS ADJUST -1 DAY FOR DB SHIFT
1199
+ // ============================================
1200
+
1201
+ case 'is_before_business_days': {
1202
+ if (isNaN(numVal)) {
1203
+ throw new BadRequestException('Value must be a number');
1204
+ }
1205
+
1206
+ const targetDate = subtractBusinessDays(numVal);
1207
+
1208
+ return {
1209
+ query: `${dateColumn} <= :${key} AND EXTRACT(DOW FROM ${dateColumn}) NOT IN (0, 6)`,
1210
+ params: { [key]: targetDate },
1211
+ };
1212
+ }
1213
+
1214
+ case 'is_after_business_days': {
1215
+ if (isNaN(numVal)) {
1216
+ throw new BadRequestException(
1217
+ 'Value must be a number for is_after_business_days',
1218
+ );
1219
+ }
1220
+
1221
+ const businessAfter = (() => {
1222
+ let d = new Date();
1223
+ let count = 0;
1224
+
1225
+ while (count < numVal) {
1226
+ d.setDate(d.getDate() + 1);
1227
+ const day = d.getDay(); // 0=Sun, 6=Sat
1228
+ if (day !== 0 && day !== 6) count++;
1229
+ }
1230
+
1231
+ // DB shift -1 day
1232
+ d.setDate(d.getDate() - 1);
1233
+
1234
+ return d.toISOString().split('T')[0];
1235
+ })();
1236
+
1237
+ return {
1238
+ query: `${dateColumn} > :${key}`,
1239
+ params: { [key]: businessAfter },
1240
+ };
1241
+ }
1242
+
1243
+ // ============================================
1244
+ // IN LAST DAY (range: today to N days before)
1245
+ // ============================================
1246
+ case 'in_last_day': {
1247
+ if (isNaN(numVal)) {
1248
+ throw new BadRequestException(
1249
+ 'Value must be a number for in_last_day',
1250
+ );
1251
+ }
1252
+
1253
+ const today = (() => {
1254
+ const d = new Date();
1255
+ return d.toISOString().split('T')[0];
1256
+ })();
1257
+
1258
+ const lastN = (() => {
1259
+ const d = new Date();
1260
+ d.setDate(d.getDate() - numVal);
1261
+ return d.toISOString().split('T')[0];
1262
+ })();
1263
+
1264
+ return {
1265
+ query: `${dateColumn} BETWEEN :${key}_start AND :${key}_end`,
1266
+ params: {
1267
+ [`${key}_start`]: lastN,
1268
+ [`${key}_end`]: today,
1269
+ },
1270
+ };
1271
+ }
1272
+
1273
+ // ============================================
1274
+ // IN NEXT DAY (range: today to N days after)
1275
+ // ============================================
1276
+ case 'in_next_day': {
1277
+ if (isNaN(numVal)) {
1278
+ throw new BadRequestException(
1279
+ 'Value must be a number for in_next_day',
1280
+ );
1281
+ }
1282
+
1283
+ const today = (() => {
1284
+ const d = new Date();
1285
+ return d.toISOString().split('T')[0];
1286
+ })();
1287
+
1288
+ const nextN = (() => {
1289
+ const d = new Date();
1290
+ d.setDate(d.getDate() + numVal);
1291
+ return d.toISOString().split('T')[0];
1292
+ })();
1293
+
1294
+ return {
1295
+ query: `${dateColumn} BETWEEN :${key}_start AND :${key}_end`,
1296
+ params: {
1297
+ [`${key}_start`]: today,
1298
+ [`${key}_end`]: nextN,
1299
+ },
1300
+ };
1301
+ }
1302
+
1303
+ default:
1304
+ throw new BadRequestException(`Unsupported operator for date: ${op}`);
1305
+ }
1306
+ }
1307
+
1308
+ private buildSelectCondition(
1309
+ attr: string,
1310
+ op: string,
1311
+ val: any,
1312
+ key: string,
1313
+ ) {
1314
+ switch (op) {
1315
+ case 'equal':
1316
+ return { query: `e.${attr} = :${key}`, params: { [key]: val } };
1317
+ case 'not_equal':
1318
+ return { query: `e.${attr} != :${key}`, params: { [key]: val } };
1319
+ case 'empty':
1320
+ return { query: `e.${attr} IS NULL`, params: {} };
1321
+ case 'not_empty':
1322
+ return { query: `e.${attr} IS NOT NULL`, params: {} };
1323
+ default:
1324
+ throw new BadRequestException(`Unsupported operator for select: ${op}`);
1325
+ }
1326
+ }
1327
+
1328
+ private buildMultiSelectCondition(
1329
+ attr: string,
1330
+ op: string,
1331
+ val: any,
1332
+ key: string,
1333
+ ) {
1334
+ let arr: string[] = [];
1335
+
1336
+ // SAFETY: Convert all to array
1337
+ if (Array.isArray(val)) {
1338
+ arr = val.map((v) => String(v));
1339
+ } else if (typeof val === 'string') {
1340
+ arr = val.split(',').map((v) => v.trim());
1341
+ } else {
1342
+ throw new BadRequestException(`Invalid multiselect value`);
1343
+ }
1344
+
1345
+ if (arr.length === 0) {
1346
+ return { query: '1=1', params: {} };
1347
+ }
1348
+ if (op === 'equal') {
1349
+ return {
1350
+ query: `e.${attr}::text = ANY(:${key})`,
1351
+ params: { [key]: `{${arr.join(',')}}` },
1352
+ };
1353
+ }
1354
+
1355
+ if (op === 'not_equal') {
1356
+ return {
1357
+ query: `NOT (e.${attr}::text = ANY(:${key}))`,
1358
+ params: { [key]: `{${arr.join(',')}}` },
1359
+ };
1360
+ }
1361
+
1362
+
1363
+
1364
+ if (op === 'contains') {
1365
+ return {
1366
+ query: `e.${attr}::text LIKE :${key}`,
1367
+ params: { [key]: `%${val}%` },
1368
+ };
1369
+ }
1370
+
1371
+ if (op === 'not_contains') {
1372
+ return {
1373
+ query: `e.${attr}::text NOT LIKE :${key}`,
1374
+ params: { [key]: `%${val}%` },
1375
+ };
1376
+ }
1377
+
1378
+ if (op === 'empty') return { query: `e.${attr} IS NULL`, params: {} };
1379
+ if (op === 'not_empty')
1380
+ return { query: `e.${attr} IS NOT NULL`, params: {} };
1381
+
1382
+ throw new BadRequestException(`Unsupported operator: ${op}`);
1383
+ }
1384
+
1385
+ private buildYearCondition(attr: string, op: string, val: any, key: string) {
1386
+ switch (op) {
1387
+ case 'equal':
1388
+ return {
1389
+ query: `e.${attr} = :${key}`,
1390
+ params: { [key]: parseInt(val, 10) },
1391
+ };
1392
+ case 'not_equal':
1393
+ return {
1394
+ query: `e.${attr} != :${key}`,
1395
+ params: { [key]: parseInt(val, 10) },
1396
+ };
1397
+ case 'greater_than':
1398
+ return {
1399
+ query: `e.${attr} > :${key}`,
1400
+ params: { [key]: parseInt(val, 10) },
1401
+ };
1402
+ case 'less_than':
1403
+ return {
1404
+ query: `e.${attr} < :${key}`,
1405
+ params: { [key]: parseInt(val, 10) },
1406
+ };
1407
+ default:
1408
+ throw new BadRequestException(`Unsupported operator for year: ${op}`);
1409
+ }
1410
+ }
1411
+
1412
+ /**
1413
+ * Core filter data retrieval method
1414
+ * @param entityType - Entity type to query
1415
+ * @param filterConditions - Array of filter conditions
1416
+ * @param sortColumns - Array of sort columns with order
1417
+ * @param columns - Array of attribute keys to select (if empty, returns id and name)
1418
+ * @param flatJson - Whether to use flat JSON structure
1419
+ * @param ids - Optional array of IDs to filter by
1420
+ * @param pagination - Pagination parameters (page, size)
1421
+ * @param userData - Logged in user data
1422
+ * @returns Object with entity_list and pagination info
1423
+ */
1424
+ async GetFilterData(
1425
+ entityType: string,
1426
+ filterConditions: FilterCondition[] = [],
1427
+ sortColumns: { column: string; order: 'ASC' | 'DESC' }[] = [],
1428
+ columns: string[] = [],
1429
+ flatJson: boolean = false,
1430
+ ids: number[] = [],
1431
+ pagination?: { page: number; size: number },
1432
+ userData?: any,
1433
+ ) {
1434
+ console.log('🔹 [GetFilterData] Starting with params:', {
1435
+ entityType,
1436
+ filterConditions,
1437
+ sortColumns,
1438
+ columns,
1439
+ flatJson,
1440
+ ids,
1441
+ pagination,
1442
+ });
1443
+
1444
+ // Get EntityMaster by EntityType
1445
+ const entityMeta = await this.entityMasterService.getEntityData(
1446
+ entityType,
1447
+ userData,
1448
+ );
1449
+ const tableName = entityMeta?.data_source;
1450
+
1451
+ if (!tableName) {
1452
+ throw new BadRequestException(`Invalid entity_type: ${entityType}`);
1453
+ }
1454
+
1455
+ // Get AttributeMaster by EntityType
1456
+ const getAttributeColumnMeta =
1457
+ await this.attributeMasterService.findAttributesByMappedEntityType(
1458
+ entityType,
1459
+ userData,
1460
+ );
1461
+
1462
+ const attributeMetaMap = getAttributeColumnMeta.reduce(
1463
+ (acc, attr) => {
1464
+ acc[attr.attribute_key] = attr;
1465
+ return acc;
1466
+ },
1467
+ {} as Record<string, any>,
1468
+ );
1469
+
1470
+ // Build WHERE clauses using existing function
1471
+ const whereClauses = this.buildWhereClauses(
1472
+ filterConditions,
1473
+ attributeMetaMap,
1474
+ );
1475
+
1476
+ // Add IDs condition if provided
1477
+ if (ids && ids.length > 0) {
1478
+ const idsStr = ids.map((id) => String(id));
1479
+ whereClauses.push({
1480
+ query: 'e.id::text = ANY(:ids)',
1481
+ params: { ids: `{${idsStr.join(',')}}` },
1482
+ });
1483
+ }
1484
+
1485
+ // Build SELECT clause
1486
+ let selectClause = 'e.*'; // Default to all columns
1487
+ if (columns && columns.length > 0) {
1488
+ // Select specific columns
1489
+ const validColumns = columns
1490
+ .filter((col) => attributeMetaMap[col])
1491
+ .map((col) => `e.${col}`)
1492
+ .join(', ');
1493
+ selectClause = validColumns || 'e.id, e.name';
1494
+ } else {
1495
+ // If columns is empty, return id and name
1496
+ selectClause = 'e.id, e.name';
1497
+ }
1498
+
1499
+ // Build query
1500
+ let qb = this.entityManager
1501
+ .createQueryBuilder()
1502
+ .select(selectClause)
1503
+ .from(`${this.schema}.${tableName}`, 'e');
1504
+
1505
+ // Apply WHERE clauses
1506
+ whereClauses.forEach((clause) => qb.andWhere(clause.query, clause.params));
1507
+
1508
+ // Build SORT clauses
1509
+ if (sortColumns && sortColumns.length > 0) {
1510
+ sortColumns.forEach(({ column, order }) => {
1511
+ if (attributeMetaMap[column]) {
1512
+ qb.addOrderBy(`e.${column}`, order);
1513
+ }
1514
+ });
1515
+ }
1516
+
1517
+ // Apply pagination if provided
1518
+ let page = 1;
1519
+ let size = 10;
1520
+ if (pagination && pagination.page > 0 && pagination.size > 0) {
1521
+ page = pagination.page;
1522
+ size = pagination.size;
1523
+ qb.skip((page - 1) * size).take(size);
1524
+ }
1525
+
1526
+ // Execute query
1527
+ const query = qb.getQuery();
1528
+ console.log('🔹 [GetFilterData] Executing query:', query);
1529
+
1530
+ const entity_list = await qb.getRawMany();
1531
+
1532
+ // Get total count
1533
+ const countQb = this.entityManager
1534
+ .createQueryBuilder()
1535
+ .select('COUNT(*)', 'count')
1536
+ .from(`${this.schema}.${tableName}`, 'e');
1537
+ whereClauses.forEach((clause) =>
1538
+ countQb.andWhere(clause.query, clause.params),
1539
+ );
1540
+ const countResult = await countQb.getRawOne();
1541
+ const total = parseInt(countResult.count, 10);
1542
+
1543
+ console.log(
1544
+ `🔹 [GetFilterData] Fetched ${entity_list.length} records, total: ${total}`,
1545
+ );
1546
+
1547
+ const result: any = {
1548
+ entity_list,
1549
+ };
1550
+
1551
+ // Add pagination info if pagination was requested
1552
+ if (pagination) {
1553
+ result.pagination = {
1554
+ total,
1555
+ page,
1556
+ size,
1557
+ totalPages: Math.ceil(total / size),
1558
+ hasNextPage: page * size < total,
1559
+ hasPreviousPage: page > 1,
1560
+ };
1561
+ }
1562
+
1563
+ return result;
1564
+ }
1565
+
1566
+ /**
1567
+ * Internal filter data retrieval with filter code resolution
1568
+ * @param entityType - Entity type to query
1569
+ * @param filterCode - Saved filter code to resolve
1570
+ * @param quickFilter - Quick filter conditions
1571
+ * @param columns - Array of attribute keys to select
1572
+ * @param flatJson - Whether to use flat JSON structure
1573
+ * @param ids - Optional array of IDs to filter by
1574
+ * @param userData - Logged in user data
1575
+ * @returns Object with entity_list and pagination info
1576
+ */
1577
+ async GetFilterDataInternal(
1578
+ entityType: string,
1579
+ filterCode?: string,
1580
+ quickFilter: FilterCondition[] = [],
1581
+ columns: string[] = [],
1582
+ flatJson: boolean = false,
1583
+ ids: number[] = [],
1584
+ userData?: any,
1585
+ ) {
1586
+ console.log('🔸 [GetFilterDataInternal] Starting with params:', {
1587
+ entityType,
1588
+ filterCode,
1589
+ quickFilter,
1590
+ columns,
1591
+ flatJson,
1592
+ ids,
1593
+ });
1594
+
1595
+ // Aggregate filterCode + quickfilter
1596
+ const savedFilters = await this.getSavedFilters(filterCode);
1597
+
1598
+ // Normalize saved filters to FilterCondition format
1599
+ const savedFiltersNormalized = savedFilters.map((f) => ({
1600
+ filter_attribute: f.filter_attribute,
1601
+ filter_operator: f.filter_operator,
1602
+ filter_value: f.filter_value,
1603
+ filter_entity_type: entityType,
1604
+ }));
1605
+
1606
+ // Merge all filters
1607
+ const allFilters = [...quickFilter, ...savedFiltersNormalized].filter(
1608
+ (f) => f.filter_value !== '' && f.filter_value != null,
1609
+ );
1610
+
1611
+ console.log('🔸 [GetFilterDataInternal] Aggregated filters:', allFilters);
1612
+
1613
+ // Call GetFilterData
1614
+ return await this.GetFilterData(
1615
+ entityType,
1616
+ allFilters,
1617
+ [], // No sort columns at this level
1618
+ columns,
1619
+ flatJson,
1620
+ ids,
1621
+ undefined, // No pagination at this level
1622
+ userData,
1623
+ );
1624
+ }
1625
+
1626
+ /**
1627
+ * Filter data retrieval for listing with layout preferences
1628
+ * @param entityType - Entity type to query
1629
+ * @param quickFilter - Quick filter conditions
1630
+ * @param filterCode - Saved filter code to resolve
1631
+ * @param attributeFilter - Attribute filter conditions
1632
+ * @param sortColumn - Sort column with order (will be overridden by layout preference)
1633
+ * @param pagination - Pagination parameters
1634
+ * @param userData - Logged in user data
1635
+ * @returns Object with entity_list and pagination info
1636
+ */
1637
+ async GetFilterDataForListing(
1638
+ entityType: string,
1639
+ quickFilter: FilterCondition[] = [],
1640
+ filterCode?: string,
1641
+ attributeFilter: FilterCondition[] = [],
1642
+ sortColumn?: { column: string; order: 'ASC' | 'DESC' },
1643
+ pagination?: { page: number; size: number },
1644
+ userData?: any,
1645
+ ) {
1646
+ console.log('🔷 [GetFilterDataForListing] Starting with params:', {
1647
+ entityType,
1648
+ quickFilter,
1649
+ filterCode,
1650
+ attributeFilter,
1651
+ sortColumn,
1652
+ pagination,
1653
+ });
1654
+
1655
+ const { level_type, level_id, id: user_id } = userData || {};
1656
+
1657
+ // Check flat_json by EntityMaster
1658
+ const entityMeta = await this.entityMasterService.getEntityData(
1659
+ entityType,
1660
+ userData,
1661
+ );
1662
+ const flatJson = (entityMeta as any)?.flat_json || false;
1663
+
1664
+ // Aggregate quickfilter[], attributeFilter[] and filterCode
1665
+ const savedFilters = await this.getSavedFilters(filterCode);
1666
+ const savedFiltersNormalized = savedFilters.map((f) => ({
1667
+ filter_attribute: f.filter_attribute,
1668
+ filter_operator: f.filter_operator,
1669
+ filter_value: f.filter_value,
1670
+ filter_entity_type: entityType,
1671
+ }));
1672
+
1673
+ const allFilters = [
1674
+ ...quickFilter,
1675
+ ...attributeFilter,
1676
+ ...savedFiltersNormalized,
1677
+ ].filter((f) => f.filter_value !== '' && f.filter_value != null);
1678
+
1679
+ console.log('🔷 [GetFilterDataForListing] Aggregated filters:', allFilters);
1680
+
1681
+ // Get LayoutPreference by (EntityType, user_id, type:layout)
1682
+ let layoutPreferenceRepo =
1683
+ this.reflectionHelper.getRepoService('LayoutPreference');
1684
+
1685
+ const layoutPreference = await layoutPreferenceRepo.findOne({
1686
+ where: {
1687
+ user_id: user_id,
1688
+ mapped_entity_type: entityType,
1689
+ mapped_level_id: level_id,
1690
+ mapped_level_type: level_type,
1691
+ type: 'layout',
1692
+ },
1693
+ });
1694
+
1695
+ // Get col[] from LayoutPreference
1696
+ let columns: string[] = [];
1697
+ if (layoutPreference?.mapped_json?.columns) {
1698
+ columns = layoutPreference.mapped_json.columns.map(
1699
+ (col: any) => col.attribute_key || col,
1700
+ );
1701
+ } else {
1702
+ // Default columns when no layout preference exists
1703
+ columns = ['id', 'name', 'code', 'status', 'description'];
1704
+ }
1705
+
1706
+ console.log('🔷 [GetFilterDataForListing] Layout columns:', columns);
1707
+
1708
+ // Get sortColumns[] from LayoutPreference (override sortColumn if exists)
1709
+ let sortColumns: { column: string; order: 'ASC' | 'DESC' }[] = [];
1710
+
1711
+ if (layoutPreference?.mapped_json?.sorting?.sortby) {
1712
+ // Layout preference sorting exists - override sortColumn
1713
+ sortColumns = layoutPreference.mapped_json.sorting.sortby.map(
1714
+ (sort: any) => ({
1715
+ column: sort.column,
1716
+ order: sort.order?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC',
1717
+ }),
1718
+ );
1719
+ console.log(
1720
+ '🔷 [GetFilterDataForListing] Using layout preference sorting:',
1721
+ sortColumns,
1722
+ );
1723
+ } else if (sortColumn) {
1724
+ // No layout preference - use provided sortColumn
1725
+ sortColumns = [sortColumn];
1726
+ console.log(
1727
+ '🔷 [GetFilterDataForListing] Using provided sortColumn:',
1728
+ sortColumns,
1729
+ );
1730
+ }
1731
+
1732
+ // Call GetFilterData
1733
+ return await this.GetFilterData(
1734
+ entityType,
1735
+ allFilters,
1736
+ sortColumns,
1737
+ columns,
1738
+ flatJson,
1739
+ [],
1740
+ pagination,
1741
+ userData,
1742
+ );
1743
+ }
1744
+
1745
+ async queryWithSchema(sql: string, params: any[] = []) {
1746
+ await this.entityManager.query('BEGIN');
1747
+ await this.entityManager.query(`SET LOCAL search_path TO ${this.schema}`);
1748
+ const result = await this.entityManager.query(sql, params);
1749
+ await this.entityManager.query('COMMIT');
1750
+ return result;
1751
+ }
1752
+ }