rez_core 7.1.3 → 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 (455) 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/listmaster/controller/list-master.controller.d.ts +6 -21
  25. package/dist/module/listmaster/controller/list-master.controller.js +6 -17
  26. package/dist/module/listmaster/controller/list-master.controller.js.map +1 -1
  27. package/dist/module/listmaster/service/list-master.service.d.ts +4 -9
  28. package/dist/module/listmaster/service/list-master.service.js +45 -54
  29. package/dist/module/listmaster/service/list-master.service.js.map +1 -1
  30. package/dist/module/meta/controller/media.controller.d.ts +3 -0
  31. package/dist/module/meta/controller/media.controller.js +27 -0
  32. package/dist/module/meta/controller/media.controller.js.map +1 -1
  33. package/dist/module/meta/entity/media-data.entity.d.ts +1 -0
  34. package/dist/module/meta/entity/media-data.entity.js +4 -0
  35. package/dist/module/meta/entity/media-data.entity.js.map +1 -1
  36. package/dist/module/meta/repository/attribute-master.repository.js +14 -14
  37. package/dist/module/meta/service/entity-dynamic.service.js +16 -16
  38. package/dist/module/meta/service/entity-dynamic.service.js.map +1 -1
  39. package/dist/module/meta/service/entity-master.service.js +20 -20
  40. package/dist/module/meta/service/media-data.service.d.ts +2 -0
  41. package/dist/module/meta/service/media-data.service.js +8 -0
  42. package/dist/module/meta/service/media-data.service.js.map +1 -1
  43. package/dist/module/meta/service/resolver.service.js +23 -13
  44. package/dist/module/meta/service/resolver.service.js.map +1 -1
  45. package/dist/module/notification/notification.module.js +2 -0
  46. package/dist/module/notification/notification.module.js.map +1 -1
  47. package/dist/module/notification/repository/notification.repository.d.ts +7 -0
  48. package/dist/module/notification/repository/notification.repository.js +43 -0
  49. package/dist/module/notification/repository/notification.repository.js.map +1 -0
  50. package/dist/module/notification/service/notification.service.d.ts +3 -1
  51. package/dist/module/notification/service/notification.service.js +27 -40
  52. package/dist/module/notification/service/notification.service.js.map +1 -1
  53. package/dist/module/workflow/controller/workflow.controller.js +1 -1
  54. package/dist/module/workflow/controller/workflow.controller.js.map +1 -1
  55. package/dist/module/workflow/repository/action-data.repository.js +10 -3
  56. package/dist/module/workflow/repository/action-data.repository.js.map +1 -1
  57. package/dist/module/workflow/repository/action.repository.js +2 -2
  58. package/dist/module/workflow/repository/activity-log.repository.js +4 -4
  59. package/dist/module/workflow/repository/activity-log.repository.js.map +1 -1
  60. package/dist/module/workflow/repository/comm-template.repository.js +4 -4
  61. package/dist/module/workflow/repository/comm-template.repository.js.map +1 -1
  62. package/dist/module/workflow/repository/stage.repository.js +8 -8
  63. package/dist/module/workflow/repository/task.repository.js +4 -4
  64. package/dist/module/workflow/repository/task.repository.js.map +1 -1
  65. package/dist/module/workflow/service/action-template-mapping.service.js +2 -2
  66. package/dist/module/workflow/service/action.service.js +5 -5
  67. package/dist/module/workflow/service/comm-template.service.js +1 -1
  68. package/dist/module/workflow/service/comm-template.service.js.map +1 -1
  69. package/dist/module/workflow/service/entity-modification.service.d.ts +4 -1
  70. package/dist/module/workflow/service/entity-modification.service.js +9 -5
  71. package/dist/module/workflow/service/entity-modification.service.js.map +1 -1
  72. package/dist/module/workflow/service/populate-workflow.service.d.ts +1 -1
  73. package/dist/module/workflow/service/populate-workflow.service.js +24 -24
  74. package/dist/module/workflow/service/populate-workflow.service.js.map +1 -1
  75. package/dist/module/workflow/service/task.service.js +10 -11
  76. package/dist/module/workflow/service/task.service.js.map +1 -1
  77. package/dist/module/workflow/service/workflow-list-master.service.js +2 -2
  78. package/dist/module/workflow/service/workflow-list-master.service.js.map +1 -1
  79. package/dist/module/workflow-automation/service/schedule-handler.service.js +9 -9
  80. package/dist/module/workflow-automation/service/workflow-automation.service.js +8 -6
  81. package/dist/module/workflow-automation/service/workflow-automation.service.js.map +1 -1
  82. package/dist/tsconfig.build.tsbuildinfo +1 -1
  83. package/dist/utils/service/reflection-helper.service.js +2 -2
  84. package/docs/modules/event-driven-integration-design.md +91 -91
  85. package/docs/modules/integration.md +250 -250
  86. package/eslint.config.mjs +34 -34
  87. package/nest-cli.json +14 -14
  88. package/package.json +128 -128
  89. package/src/app.controller.ts +12 -12
  90. package/src/app.module.ts +62 -62
  91. package/src/app.service.ts +8 -8
  92. package/src/config/bull.config.ts +72 -72
  93. package/src/config/config.module.ts +17 -17
  94. package/src/config/database.config.ts +48 -48
  95. package/src/config/redis.config.ts +55 -55
  96. package/src/constant/attribute.constant.ts +8 -8
  97. package/src/constant/db-data-type.constant.ts +160 -160
  98. package/src/constant/entity.constant.ts +3 -3
  99. package/src/constant/global.constant.ts +67 -67
  100. package/src/constant/status.constant.ts +3 -3
  101. package/src/core.module.ts +96 -96
  102. package/src/decorators/roles.decorator.ts +7 -7
  103. package/src/dtos/response.dto.ts +6 -6
  104. package/src/dtos/response.ts +5 -5
  105. package/src/index.ts +1 -1
  106. package/src/module/auth/auth.module.ts +65 -65
  107. package/src/module/auth/controller/auth.controller.ts +28 -28
  108. package/src/module/auth/dto/user.dto.ts +56 -56
  109. package/src/module/auth/guards/google-auth.guard.ts +9 -9
  110. package/src/module/auth/guards/jwt.guard.ts +22 -22
  111. package/src/module/auth/services/auth.service.ts +56 -56
  112. package/src/module/auth/services/jwt.service.ts +11 -11
  113. package/src/module/auth/strategies/google.strategy.ts +54 -54
  114. package/src/module/auth/strategies/jwt.strategy.ts +65 -65
  115. package/src/module/auth/strategies/local.strategy.ts +13 -13
  116. package/src/module/dashboard/controller/dashboard.controller.ts +38 -38
  117. package/src/module/dashboard/dashboard.module.ts +19 -19
  118. package/src/module/dashboard/entity/dashboard_page_data.entity.ts +23 -23
  119. package/src/module/dashboard/entity/widget_master.entity.ts +15 -15
  120. package/src/module/dashboard/repository/dashboard.repository.ts +49 -49
  121. package/src/module/dashboard/service/dashboard.service.ts +69 -69
  122. package/src/module/eav/EAV_USAGE_GUIDE.md +351 -351
  123. package/src/module/eav/controller/eav.controller.ts +119 -119
  124. package/src/module/eav/dto/eav-operation.dto.ts +62 -62
  125. package/src/module/eav/eav.module.ts +79 -79
  126. package/src/module/eav/entity/eav-boolean.entity.ts +25 -25
  127. package/src/module/eav/entity/eav-date.entity.ts +24 -24
  128. package/src/module/eav/entity/eav-decimal.entity.ts +24 -24
  129. package/src/module/eav/entity/eav-int.entity.ts +24 -24
  130. package/src/module/eav/entity/eav-json.entity.ts +24 -24
  131. package/src/module/eav/entity/eav-text.entity.ts +24 -24
  132. package/src/module/eav/entity/eav-time.entity.ts +24 -24
  133. package/src/module/eav/entity/eav-timestamp.entity.ts +24 -24
  134. package/src/module/eav/entity/eav-varchar.entity.ts +24 -24
  135. package/src/module/eav/interface/eav-strategy.interface.ts +32 -32
  136. package/src/module/eav/repository/eav-boolean.repository.ts +67 -67
  137. package/src/module/eav/repository/eav-date.repository.ts +67 -67
  138. package/src/module/eav/repository/eav-decimal.repository.ts +67 -67
  139. package/src/module/eav/repository/eav-int.repository.ts +67 -67
  140. package/src/module/eav/repository/eav-json.repository.ts +67 -67
  141. package/src/module/eav/repository/eav-text.repository.ts +67 -67
  142. package/src/module/eav/repository/eav-time.repository.ts +67 -67
  143. package/src/module/eav/repository/eav-timestamp.repository.ts +67 -67
  144. package/src/module/eav/repository/eav-varchar.repository.ts +67 -67
  145. package/src/module/eav/service/eav-boolean.service.ts +64 -64
  146. package/src/module/eav/service/eav-date.service.ts +64 -64
  147. package/src/module/eav/service/eav-decimal.service.ts +64 -64
  148. package/src/module/eav/service/eav-factory.service.ts +93 -93
  149. package/src/module/eav/service/eav-int.service.ts +64 -64
  150. package/src/module/eav/service/eav-json.service.ts +64 -64
  151. package/src/module/eav/service/eav-text.service.ts +64 -64
  152. package/src/module/eav/service/eav-time.service.ts +64 -64
  153. package/src/module/eav/service/eav-timestamp.service.ts +64 -64
  154. package/src/module/eav/service/eav-varchar.service.ts +65 -65
  155. package/src/module/eav/service/eav.service.ts +116 -116
  156. package/src/module/entity_json/controller/entity_json.controller.ts +75 -75
  157. package/src/module/entity_json/docs/FlatJson_Filterin_System.md +2803 -2803
  158. package/src/module/entity_json/entity/entityJson.entity.ts +42 -42
  159. package/src/module/entity_json/entity_json.module.ts +22 -22
  160. package/src/module/entity_json/service/entityJson.repository.ts +37 -37
  161. package/src/module/entity_json/service/entity_json.service.ts +492 -492
  162. package/src/module/export/controller/export.controller.ts +83 -83
  163. package/src/module/export/export.module.ts +14 -14
  164. package/src/module/export/service/export.service.ts +107 -107
  165. package/src/module/filter/controller/filter.controller.ts +214 -214
  166. package/src/module/filter/dto/filter-request.dto.ts +41 -41
  167. package/src/module/filter/entity/saved-filter-detail.entity.ts +37 -37
  168. package/src/module/filter/entity/saved-filter-master.entity.ts +30 -30
  169. package/src/module/filter/filter.module.ts +33 -33
  170. package/src/module/filter/repository/saved-filter.repository.ts +249 -247
  171. package/src/module/filter/repository/saved.filter-detail.repository.ts +19 -19
  172. package/src/module/filter/service/filter-evaluator.service.ts +82 -82
  173. package/src/module/filter/service/filter.service.ts +1752 -1722
  174. package/src/module/filter/service/saved-filter.service.ts +164 -164
  175. package/src/module/ics/controller/ics.controller.ts +21 -21
  176. package/src/module/ics/dto/ics.dto.ts +55 -55
  177. package/src/module/ics/ics.module.ts +13 -13
  178. package/src/module/ics/service/ics.service.ts +57 -57
  179. package/src/module/integration/controller/calender-event.controller.ts +31 -31
  180. package/src/module/integration/controller/integration.controller.ts +662 -662
  181. package/src/module/integration/controller/wrapper.controller.ts +37 -37
  182. package/src/module/integration/dto/create-config.dto.ts +526 -526
  183. package/src/module/integration/entity/integration-config.entity.ts +112 -112
  184. package/src/module/integration/entity/integration-entity-mapper.entity.ts +14 -14
  185. package/src/module/integration/entity/integration-source.entity.ts +17 -17
  186. package/src/module/integration/entity/user-integration.entity.ts +71 -71
  187. package/src/module/integration/examples/usage.example.ts +338 -338
  188. package/src/module/integration/factories/base.factory.ts +7 -7
  189. package/src/module/integration/factories/email.factory.ts +49 -49
  190. package/src/module/integration/factories/integration.factory.ts +121 -121
  191. package/src/module/integration/factories/sms.factory.ts +51 -51
  192. package/src/module/integration/factories/telephone.factory.ts +41 -41
  193. package/src/module/integration/factories/whatsapp.factory.ts +56 -56
  194. package/src/module/integration/integration.module.ts +110 -110
  195. package/src/module/integration/service/calendar-event.service.ts +118 -118
  196. package/src/module/integration/service/integration-entity-mapper.service.ts +17 -17
  197. package/src/module/integration/service/integration-queue.service.ts +229 -229
  198. package/src/module/integration/service/integration.service.ts +2651 -2632
  199. package/src/module/integration/service/oauth.service.ts +226 -224
  200. package/src/module/integration/service/wrapper.service.ts +754 -753
  201. package/src/module/integration/strategies/email/gmail-api.strategy.ts +307 -281
  202. package/src/module/integration/strategies/email/outlook-api.strategy.ts +44 -44
  203. package/src/module/integration/strategies/email/outlook.strategy.ts +64 -64
  204. package/src/module/integration/strategies/email/sendgrid-api.strategy.ts +263 -260
  205. package/src/module/integration/strategies/integration.strategy.ts +97 -97
  206. package/src/module/integration/strategies/sms/gupshup-sms.strategy.ts +146 -146
  207. package/src/module/integration/strategies/sms/msg91-sms.strategy.ts +164 -164
  208. package/src/module/integration/strategies/sms/tubelight-sms.strategy.ts +163 -163
  209. package/src/module/integration/strategies/telephone/ozonetel-voice.strategy.ts +238 -238
  210. package/src/module/integration/strategies/telephone/tubelight-voice.strategy.ts +210 -210
  211. package/src/module/integration/strategies/whatsapp/gupshup-whatsapp.strategy.ts +359 -359
  212. package/src/module/integration/strategies/whatsapp/tubelight-whatsapp.strategy.ts +372 -372
  213. package/src/module/integration/strategies/whatsapp/whatsapp-cloud.strategy.ts +403 -403
  214. package/src/module/integration/strategies/whatsapp/whatsapp.strategy.ts +57 -57
  215. package/src/module/layout/controller/layout.controller.ts +38 -38
  216. package/src/module/layout/entity/header-items.entity.ts +28 -28
  217. package/src/module/layout/entity/header-section.entity.ts +13 -13
  218. package/src/module/layout/layout.module.ts +20 -20
  219. package/src/module/layout/repository/header-items.repository.ts +18 -18
  220. package/src/module/layout/repository/header-section.repository.ts +16 -16
  221. package/src/module/layout/service/header-section.service.ts +25 -25
  222. package/src/module/layout_preference/controller/layout_preference.controller.ts +76 -76
  223. package/src/module/layout_preference/entity/layout_preference.entity.ts +28 -28
  224. package/src/module/layout_preference/layout_preference.module.ts +22 -22
  225. package/src/module/layout_preference/repository/layout_preference.repository.ts +65 -65
  226. package/src/module/layout_preference/service/layout_preference.service.ts +191 -191
  227. package/src/module/linked_attributes/controller/linked_attributes.controller.ts +137 -137
  228. package/src/module/linked_attributes/dto/create-linked-attribute-smart.dto.ts +54 -54
  229. package/src/module/linked_attributes/entity/linked_attribute.entity.ts +51 -51
  230. package/src/module/linked_attributes/linked_attributes.module.ts +23 -23
  231. package/src/module/linked_attributes/repository/linked_attribute.repository.ts +12 -12
  232. package/src/module/linked_attributes/service/linked_attributes.service.ts +650 -650
  233. package/src/module/linked_attributes/test/linked-attributes.service.spec.ts +244 -244
  234. package/src/module/listmaster/controller/list-master.controller.ts +215 -226
  235. package/src/module/listmaster/entity/list-master-items.entity.ts +30 -30
  236. package/src/module/listmaster/entity/list-master.entity.ts +25 -25
  237. package/src/module/listmaster/listmaster.module.ts +46 -46
  238. package/src/module/listmaster/repository/list-master-items.repository.ts +262 -262
  239. package/src/module/listmaster/repository/list-master.repository.ts +60 -60
  240. package/src/module/listmaster/service/list-master-engine.ts +19 -19
  241. package/src/module/listmaster/service/list-master-extension.interface.ts +4 -4
  242. package/src/module/listmaster/service/list-master-item.service.ts +382 -382
  243. package/src/module/listmaster/service/list-master-registry.ts +15 -15
  244. package/src/module/listmaster/service/list-master.service.ts +774 -760
  245. package/src/module/mapper/controller/field-mapper.controller.ts +76 -76
  246. package/src/module/mapper/controller/mapper.controller.ts +20 -20
  247. package/src/module/mapper/dto/field-mapper.dto.ts +14 -14
  248. package/src/module/mapper/entity/field-lovs.entity.ts +15 -15
  249. package/src/module/mapper/entity/field-mapper.entity.ts +49 -49
  250. package/src/module/mapper/entity/mapper.entity.ts +9 -9
  251. package/src/module/mapper/mapper.module.ts +35 -35
  252. package/src/module/mapper/repository/field-lovs.repository.ts +35 -35
  253. package/src/module/mapper/repository/field-mapper.repository.ts +42 -42
  254. package/src/module/mapper/repository/mapper.repository.ts +32 -32
  255. package/src/module/mapper/service/field-mapper.service.ts +268 -268
  256. package/src/module/mapper/service/mapper.service.ts +80 -80
  257. package/src/module/master/controller/master.controller.ts +71 -71
  258. package/src/module/master/service/master.service.ts +460 -460
  259. package/src/module/master/service/poupulate-meta.service.ts +210 -210
  260. package/src/module/meta/controller/attribute-master.controller.ts +83 -83
  261. package/src/module/meta/controller/entity-dynamic.controller.ts +123 -123
  262. package/src/module/meta/controller/entity-master.controller.ts +41 -41
  263. package/src/module/meta/controller/entity-relation.controller.ts +36 -36
  264. package/src/module/meta/controller/entity.controller.ts +301 -301
  265. package/src/module/meta/controller/entity.public.controller.ts +76 -76
  266. package/src/module/meta/controller/media.controller.ts +162 -135
  267. package/src/module/meta/controller/meta.controller.ts +80 -80
  268. package/src/module/meta/controller/view-master.controller.ts +79 -79
  269. package/src/module/meta/dto/entity-list-data.dto.ts +6 -6
  270. package/src/module/meta/dto/entity-tab.dto.ts +4 -4
  271. package/src/module/meta/dto/entity-table.dto.ts +12 -12
  272. package/src/module/meta/entity/attribute-master.entity.ts +62 -62
  273. package/src/module/meta/entity/base-entity.entity.ts +52 -52
  274. package/src/module/meta/entity/dynamic.entity.ts +5 -5
  275. package/src/module/meta/entity/entity-master.entity.ts +53 -53
  276. package/src/module/meta/entity/entity-relation-data.entity.ts +24 -24
  277. package/src/module/meta/entity/entity-relation.entity.ts +18 -18
  278. package/src/module/meta/entity/entity-table-column.entity.ts +56 -56
  279. package/src/module/meta/entity/entity-table.entity.ts +45 -45
  280. package/src/module/meta/entity/media-data.entity.ts +35 -32
  281. package/src/module/meta/entity/preference.entity.ts +57 -57
  282. package/src/module/meta/entity/view-master.entity.ts +36 -36
  283. package/src/module/meta/entity.module.ts +153 -153
  284. package/src/module/meta/repository/attribute-master.repository.ts +206 -206
  285. package/src/module/meta/repository/entity-attribute-update.repository.ts +48 -48
  286. package/src/module/meta/repository/entity-master.repository.ts +120 -120
  287. package/src/module/meta/repository/entity-relation.repository.ts +36 -36
  288. package/src/module/meta/repository/entity-table-column.repository.ts +39 -39
  289. package/src/module/meta/repository/entity-table.repository.ts +53 -53
  290. package/src/module/meta/repository/media-data.repository.ts +50 -50
  291. package/src/module/meta/repository/preference.repository.ts +20 -20
  292. package/src/module/meta/repository/user-app-mapping.repository.ts +28 -28
  293. package/src/module/meta/repository/view-master.repository.ts +42 -42
  294. package/src/module/meta/service/attribute-master.service.ts +329 -329
  295. package/src/module/meta/service/common.service.ts +9 -9
  296. package/src/module/meta/service/entity-attribute-update.service.ts +26 -26
  297. package/src/module/meta/service/entity-dynamic.service.ts +1038 -1037
  298. package/src/module/meta/service/entity-master.service.ts +288 -288
  299. package/src/module/meta/service/entity-realation-data.service.ts +9 -9
  300. package/src/module/meta/service/entity-relation.service.ts +85 -85
  301. package/src/module/meta/service/entity-service-impl.service.ts +390 -390
  302. package/src/module/meta/service/entity-table-column.service.ts +26 -26
  303. package/src/module/meta/service/entity-table.service.ts +144 -144
  304. package/src/module/meta/service/entity-validation.service.ts +187 -187
  305. package/src/module/meta/service/entity.service.ts +48 -48
  306. package/src/module/meta/service/field-group.service.ts +103 -103
  307. package/src/module/meta/service/media-data.service.ts +397 -385
  308. package/src/module/meta/service/preference.service.ts +16 -16
  309. package/src/module/meta/service/resolver.service.ts +293 -260
  310. package/src/module/meta/service/section-master.service.ts +104 -104
  311. package/src/module/meta/service/update-form-json.service.ts +22 -22
  312. package/src/module/meta/service/user-app-mapping.service.ts +17 -17
  313. package/src/module/meta/service/view-master.service.ts +127 -127
  314. package/src/module/microservice-client/microservice-clients.module.ts +13 -13
  315. package/src/module/microservice-client/service/microservice-client-factory.ts +37 -37
  316. package/src/module/microservice-client/service/microservice-clients.ts +4 -4
  317. package/src/module/notification/controller/notification.controller.ts +58 -58
  318. package/src/module/notification/entity/notification.entity.ts +76 -76
  319. package/src/module/notification/entity/otp.entity.ts +28 -28
  320. package/src/module/notification/firebase-admin.config.ts +22 -22
  321. package/src/module/notification/notification.module.ts +65 -63
  322. package/src/module/notification/repository/notification.repository.ts +33 -0
  323. package/src/module/notification/repository/otp.repository.ts +27 -27
  324. package/src/module/notification/service/email.service.ts +142 -142
  325. package/src/module/notification/service/notification.service.ts +145 -163
  326. package/src/module/preference_master/entity/preference.entity.ts +25 -25
  327. package/src/module/preference_master/preference.service.ts +27 -27
  328. package/src/module/preference_master/repo/preference.repository.ts +36 -36
  329. package/src/module/third-party-module/entity/third-party-api-registry.entity.ts +52 -52
  330. package/src/module/third-party-module/repository/third-party-api-registry.repository.ts +20 -20
  331. package/src/module/third-party-module/service/api-registry.service.ts +13 -13
  332. package/src/module/third-party-module/third-party.module.ts +12 -12
  333. package/src/module/workflow/controller/action-category.controller.ts +54 -54
  334. package/src/module/workflow/controller/action-resource-mapping.controller.ts +23 -23
  335. package/src/module/workflow/controller/action-template-mapping.controller.ts +35 -35
  336. package/src/module/workflow/controller/action.controller.ts +111 -111
  337. package/src/module/workflow/controller/activity-log.controller.ts +55 -55
  338. package/src/module/workflow/controller/comm-template.controller.ts +43 -43
  339. package/src/module/workflow/controller/entity-modification.controller.ts +35 -35
  340. package/src/module/workflow/controller/form-master.controller.ts +43 -43
  341. package/src/module/workflow/controller/stage-group.controller.ts +49 -49
  342. package/src/module/workflow/controller/stage.controller.ts +51 -51
  343. package/src/module/workflow/controller/task.controller.ts +77 -77
  344. package/src/module/workflow/controller/workflow-list-master.controller.ts +44 -44
  345. package/src/module/workflow/controller/workflow-meta.controller.ts +80 -80
  346. package/src/module/workflow/controller/workflow.controller.ts +66 -66
  347. package/src/module/workflow/entity/action-category.entity.ts +33 -33
  348. package/src/module/workflow/entity/action-data.entity.ts +51 -51
  349. package/src/module/workflow/entity/action-resources-mapping.entity.ts +21 -21
  350. package/src/module/workflow/entity/action-template-mapping.entity.ts +12 -12
  351. package/src/module/workflow/entity/action.entity.ts +48 -48
  352. package/src/module/workflow/entity/activity-log.entity.ts +39 -39
  353. package/src/module/workflow/entity/comm-template.entity.ts +38 -38
  354. package/src/module/workflow/entity/entity-modification.entity.ts +33 -33
  355. package/src/module/workflow/entity/form.entity.ts +21 -21
  356. package/src/module/workflow/entity/stage-action-mapping.entity.ts +12 -12
  357. package/src/module/workflow/entity/stage-group.entity.ts +18 -18
  358. package/src/module/workflow/entity/stage-movement-data.entity.ts +33 -33
  359. package/src/module/workflow/entity/stage.entity.ts +15 -15
  360. package/src/module/workflow/entity/task-data.entity.ts +84 -84
  361. package/src/module/workflow/entity/template-attach-mapper.entity.ts +30 -30
  362. package/src/module/workflow/entity/workflow-data.entity.ts +6 -6
  363. package/src/module/workflow/entity/workflow-level-mapping.entity.ts +18 -18
  364. package/src/module/workflow/entity/workflow.entity.ts +15 -15
  365. package/src/module/workflow/repository/action-category.repository.ts +78 -78
  366. package/src/module/workflow/repository/action-data.repository.ts +353 -345
  367. package/src/module/workflow/repository/action.repository.ts +339 -339
  368. package/src/module/workflow/repository/activity-log.repository.ts +148 -148
  369. package/src/module/workflow/repository/comm-template.repository.ts +157 -157
  370. package/src/module/workflow/repository/form-master.repository.ts +50 -50
  371. package/src/module/workflow/repository/stage-group.repository.ts +186 -186
  372. package/src/module/workflow/repository/stage-movement.repository.ts +217 -217
  373. package/src/module/workflow/repository/stage.repository.ts +160 -160
  374. package/src/module/workflow/repository/task.repository.ts +156 -154
  375. package/src/module/workflow/repository/workflow.repository.ts +42 -42
  376. package/src/module/workflow/service/action-category.service.ts +32 -32
  377. package/src/module/workflow/service/action-data.service.ts +62 -62
  378. package/src/module/workflow/service/action-resources-mapping.service.ts +10 -10
  379. package/src/module/workflow/service/action-template-mapping.service.ts +137 -137
  380. package/src/module/workflow/service/action.service.ts +300 -300
  381. package/src/module/workflow/service/activity-log.service.ts +106 -106
  382. package/src/module/workflow/service/comm-template.service.ts +179 -179
  383. package/src/module/workflow/service/entity-modification.service.ts +63 -55
  384. package/src/module/workflow/service/form-master.service.ts +35 -35
  385. package/src/module/workflow/service/populate-workflow.service.ts +313 -313
  386. package/src/module/workflow/service/stage-action-mapping.service.ts +5 -5
  387. package/src/module/workflow/service/stage-group.service.ts +325 -325
  388. package/src/module/workflow/service/stage.service.ts +196 -196
  389. package/src/module/workflow/service/task.service.ts +546 -547
  390. package/src/module/workflow/service/workflow-list-master.service.ts +68 -68
  391. package/src/module/workflow/service/workflow-meta.service.ts +638 -638
  392. package/src/module/workflow/service/workflow.service.ts +212 -212
  393. package/src/module/workflow/workflow.module.ts +180 -180
  394. package/src/module/workflow-automation/SCHEDULING_GUIDE.md +145 -145
  395. package/src/module/workflow-automation/controller/workflow-automation.controller.ts +43 -43
  396. package/src/module/workflow-automation/entity/workflow-automation-action.entity.ts +26 -26
  397. package/src/module/workflow-automation/entity/workflow-automation.entity.ts +35 -35
  398. package/src/module/workflow-automation/interface/action.decorator.ts +7 -7
  399. package/src/module/workflow-automation/interface/action.interface.ts +5 -5
  400. package/src/module/workflow-automation/service/action-registery.service.ts +35 -35
  401. package/src/module/workflow-automation/service/schedule-handler.service.ts +167 -167
  402. package/src/module/workflow-automation/service/workflow-automation-engine.service.ts +219 -219
  403. package/src/module/workflow-automation/service/workflow-automation.service.ts +486 -486
  404. package/src/module/workflow-automation/workflow-automation.module.ts +55 -55
  405. package/src/module/workflow-schedule/INSTALLATION.md +244 -244
  406. package/src/module/workflow-schedule/MULTI_PROJECT_GUIDE.md +196 -196
  407. package/src/module/workflow-schedule/README.md +422 -422
  408. package/src/module/workflow-schedule/constants/schedule.constants.ts +48 -48
  409. package/src/module/workflow-schedule/controller/workflow-schedule.controller.ts +253 -253
  410. package/src/module/workflow-schedule/docs/CLAUDE_CODE_GUIDE.md +510 -510
  411. package/src/module/workflow-schedule/docs/CLAUDE_CODE_PROMPT.md +362 -362
  412. package/src/module/workflow-schedule/docs/RUN_CLAUDE_CODE.sh +68 -68
  413. package/src/module/workflow-schedule/dto/create-schedule.dto.ts +147 -147
  414. package/src/module/workflow-schedule/dto/get-execution-logs.dto.ts +119 -119
  415. package/src/module/workflow-schedule/dto/update-schedule.dto.ts +96 -96
  416. package/src/module/workflow-schedule/entities/scheduled-workflow.entity.ts +148 -148
  417. package/src/module/workflow-schedule/entities/workflow-execution-log.entity.ts +154 -154
  418. package/src/module/workflow-schedule/interfaces/schedule-job-data.interface.ts +51 -51
  419. package/src/module/workflow-schedule/interfaces/workflow-schedule-options.interface.ts +12 -12
  420. package/src/module/workflow-schedule/processors/schedule.processor.ts +616 -616
  421. package/src/module/workflow-schedule/service/workflow-schedule.service.ts +588 -588
  422. package/src/module/workflow-schedule/workflow-schedule.module.ts +67 -67
  423. package/src/resources/dev.properties.yaml +33 -33
  424. package/src/resources/local.properties.yaml +27 -27
  425. package/src/resources/properties.module.ts +12 -12
  426. package/src/resources/properties.yaml.ts +11 -11
  427. package/src/resources/uat.properties.yaml +31 -31
  428. package/src/table.config.ts +123 -123
  429. package/src/utils/dto/excel-data.dto.ts +14 -14
  430. package/src/utils/dto/excelsheet-data.dto.ts +5 -5
  431. package/src/utils/service/base64util.service.ts +18 -18
  432. package/src/utils/service/clockIDGenUtil.service.ts +21 -21
  433. package/src/utils/service/codeGenerator.service.ts +22 -22
  434. package/src/utils/service/dateUtil.service.ts +17 -17
  435. package/src/utils/service/encryptUtil.service.ts +97 -97
  436. package/src/utils/service/excel-helper.service.ts +72 -72
  437. package/src/utils/service/excelUtil.service.ts +15 -15
  438. package/src/utils/service/file-util.service.ts +11 -11
  439. package/src/utils/service/json-util.service.ts +23 -23
  440. package/src/utils/service/loggingUtil.service.ts +88 -88
  441. package/src/utils/service/reflection-helper.service.ts +62 -62
  442. package/src/utils/service/wbsCodeGen.service.ts +8 -8
  443. package/src/utils/utils.module.ts +27 -27
  444. package/tsconfig.build.json +4 -4
  445. package/tsconfig.json +24 -24
  446. package/.claude/settings.local.json +0 -26
  447. package/.idea/250218_nodejs_core.iml +0 -9
  448. package/.idea/copilot.data.migration.agent.xml +0 -6
  449. package/.idea/copilot.data.migration.ask.xml +0 -6
  450. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  451. package/.idea/copilot.data.migration.edit.xml +0 -6
  452. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  453. package/.idea/misc.xml +0 -6
  454. package/.idea/prettier.xml +0 -6
  455. 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
+ }