Преглед изворни кода

Merge branch 'development'

pearlgw пре 1 месец
родитељ
комит
7a1fe8367b
59 измењених фајлова са 2232 додато и 456 уклоњено
  1. 2 0
      index.js
  2. 658 0
      package-lock.json
  3. 1 0
      package.json
  4. 20 0
      prisma/migrations/20250703030428_add_table_status_history/migration.sql
  5. 35 0
      prisma/migrations/20250703030614_update_name_table/migration.sql
  6. 9 0
      prisma/migrations/20250703030902_update_name_field_table_status_histories/migration.sql
  7. 4 0
      prisma/migrations/20250703075659_add_field_in_table_hospital/migration.sql
  8. 2 0
      prisma/migrations/20250705091358_update_field_user_area/migration.sql
  9. 19 6
      prisma/schema.prisma
  10. 1 1
      src/controllers/admin/HospitalController.js
  11. 29 0
      src/controllers/admin/StatusHistoryController.js
  12. 4 5
      src/controllers/admin/VendorController.js
  13. 1 1
      src/controllers/sales/AreaController.js
  14. 3 3
      src/controllers/sales/HospitalController.js
  15. 29 0
      src/controllers/sales/StatusHistoryController.js
  16. 21 0
      src/middleware/CheckRoles.js
  17. 22 0
      src/middleware/ExtractToken.js
  18. 12 0
      src/middleware/Keycloak.js
  19. 2 12
      src/repository/admin/ExecutivesHistoryRepository.js
  20. 63 36
      src/repository/admin/HospitalRepository.js
  21. 64 0
      src/repository/admin/StatusHistoryRepository.js
  22. 58 78
      src/repository/admin/VendorHistoryRepository.js
  23. 2 18
      src/repository/admin/VendorRepository.js
  24. 1 6
      src/repository/sales/AreaRepository.js
  25. 8 12
      src/repository/sales/ExecutivesHistoryRepository.js
  26. 63 4
      src/repository/sales/HospitalRepository.js
  27. 70 0
      src/repository/sales/StatusHistoryRepository.js
  28. 58 78
      src/repository/sales/VendorHistoryRepository.js
  29. 20 15
      src/resources/admin/hospital/HospitalCollection.js
  30. 27 0
      src/resources/admin/status_history/StatusHistoryCollection.js
  31. 19 15
      src/resources/admin/vendor/VendorCollection.js
  32. 16 8
      src/resources/admin/vendor/VendorResource.js
  33. 18 9
      src/resources/sales/area/UserAreaCollection.js
  34. 20 15
      src/resources/sales/hospital/HospitalCollection.js
  35. 16 8
      src/resources/sales/hospital/HospitalResource.js
  36. 27 0
      src/resources/sales/status_history/StatusHistoryCollection.js
  37. 16 7
      src/routes/admin/CityRoute.js
  38. 46 17
      src/routes/admin/HospitalRoute.js
  39. 12 7
      src/routes/admin/ProvinceRoute.js
  40. 17 7
      src/routes/admin/SalesRoute.js
  41. 18 8
      src/routes/admin/VendorRoute.js
  42. 11 4
      src/routes/sales/AreaRoute.js
  43. 43 16
      src/routes/sales/HospitalRoute.js
  44. 7 3
      src/routes/sales/VendorRoute.js
  45. 79 2
      src/services/admin/HospitalService.js
  46. 76 0
      src/services/admin/StatusHistoryService.js
  47. 66 6
      src/services/admin/VendorHistoryService.js
  48. 7 4
      src/services/admin/VendorService.js
  49. 1 1
      src/services/sales/AreaService.js
  50. 85 6
      src/services/sales/HospitalService.js
  51. 99 0
      src/services/sales/StatusHistoryService.js
  52. 64 5
      src/services/sales/VendorHistoryService.js
  53. 1 1
      src/services/sales/VendorService.js
  54. 47 0
      src/utils/CheckUserKeycloak.js
  55. 66 29
      src/utils/LogActivity.js
  56. 10 3
      src/validators/admin/hospital/HospitalValidators.js
  57. 19 0
      src/validators/admin/status_history/StatusHistoryValidators.js
  58. 9 0
      src/validators/admin/vendor_history/VendorHistoriValidators.js
  59. 9 0
      src/validators/sales/vendor_history/VendorHistoriValidators.js

+ 2 - 0
index.js

@@ -17,8 +17,10 @@ const logRoutes = require('./src/routes/superadmin/LogRoute.js')
17 17
 const areaRoutes = require('./src/routes/sales/AreaRoute.js')
18 18
 const vendorSalesRoutes = require('./src/routes/sales/VendorRoute.js')
19 19
 const { port } = require('./config/config.js')
20
+const keycloak = require('./src/middleware/Keycloak.js');
20 21
 
21 22
 app.use(cors())
23
+app.use(keycloak.middleware());
22 24
 app.use(express.json())
23 25
 app.use('/storage/', express.static(path.join(__dirname, 'storage/')));
24 26
 

+ 658 - 0
package-lock.json

@@ -21,6 +21,7 @@
21 21
         "dotenv": "^16.5.0",
22 22
         "express": "^5.1.0",
23 23
         "jsonwebtoken": "^9.0.2",
24
+        "keycloak-connect": "^26.1.1",
24 25
         "multer": "^2.0.1",
25 26
         "pg": "^8.16.2",
26 27
         "qs": "^6.14.0"
@@ -127,6 +128,40 @@
127 128
         "@prisma/debug": "6.10.1"
128 129
       }
129 130
     },
131
+    "node_modules/@testim/chrome-version": {
132
+      "version": "1.1.4",
133
+      "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz",
134
+      "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==",
135
+      "license": "MIT",
136
+      "optional": true
137
+    },
138
+    "node_modules/@tootallnate/quickjs-emscripten": {
139
+      "version": "0.23.0",
140
+      "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
141
+      "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
142
+      "license": "MIT",
143
+      "optional": true
144
+    },
145
+    "node_modules/@types/node": {
146
+      "version": "24.0.10",
147
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
148
+      "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
149
+      "license": "MIT",
150
+      "optional": true,
151
+      "dependencies": {
152
+        "undici-types": "~7.8.0"
153
+      }
154
+    },
155
+    "node_modules/@types/yauzl": {
156
+      "version": "2.10.3",
157
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
158
+      "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
159
+      "license": "MIT",
160
+      "optional": true,
161
+      "dependencies": {
162
+        "@types/node": "*"
163
+      }
164
+    },
130 165
     "node_modules/accepts": {
131 166
       "version": "2.0.0",
132 167
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -140,12 +175,47 @@
140 175
         "node": ">= 0.6"
141 176
       }
142 177
     },
178
+    "node_modules/agent-base": {
179
+      "version": "7.1.3",
180
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
181
+      "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
182
+      "license": "MIT",
183
+      "optional": true,
184
+      "engines": {
185
+        "node": ">= 14"
186
+      }
187
+    },
143 188
     "node_modules/append-field": {
144 189
       "version": "1.0.0",
145 190
       "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
146 191
       "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
147 192
       "license": "MIT"
148 193
     },
194
+    "node_modules/asn1.js": {
195
+      "version": "5.4.1",
196
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
197
+      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
198
+      "license": "MIT",
199
+      "dependencies": {
200
+        "bn.js": "^4.0.0",
201
+        "inherits": "^2.0.1",
202
+        "minimalistic-assert": "^1.0.0",
203
+        "safer-buffer": "^2.1.0"
204
+      }
205
+    },
206
+    "node_modules/ast-types": {
207
+      "version": "0.13.4",
208
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
209
+      "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
210
+      "license": "MIT",
211
+      "optional": true,
212
+      "dependencies": {
213
+        "tslib": "^2.0.1"
214
+      },
215
+      "engines": {
216
+        "node": ">=4"
217
+      }
218
+    },
149 219
     "node_modules/asynckit": {
150 220
       "version": "0.4.0",
151 221
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -163,6 +233,16 @@
163 233
         "proxy-from-env": "^1.1.0"
164 234
       }
165 235
     },
236
+    "node_modules/basic-ftp": {
237
+      "version": "5.0.5",
238
+      "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
239
+      "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
240
+      "license": "MIT",
241
+      "optional": true,
242
+      "engines": {
243
+        "node": ">=10.0.0"
244
+      }
245
+    },
166 246
     "node_modules/bcrypt": {
167 247
       "version": "6.0.0",
168 248
       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -177,6 +257,12 @@
177 257
         "node": ">= 18"
178 258
       }
179 259
     },
260
+    "node_modules/bn.js": {
261
+      "version": "4.12.2",
262
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
263
+      "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
264
+      "license": "MIT"
265
+    },
180 266
     "node_modules/body-parser": {
181 267
       "version": "2.2.0",
182 268
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -197,6 +283,22 @@
197 283
         "node": ">=18"
198 284
       }
199 285
     },
286
+    "node_modules/brorand": {
287
+      "version": "1.1.0",
288
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
289
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
290
+      "license": "MIT"
291
+    },
292
+    "node_modules/buffer-crc32": {
293
+      "version": "0.2.13",
294
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
295
+      "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
296
+      "license": "MIT",
297
+      "optional": true,
298
+      "engines": {
299
+        "node": "*"
300
+      }
301
+    },
200 302
     "node_modules/buffer-equal-constant-time": {
201 303
       "version": "1.0.1",
202 304
       "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -258,6 +360,29 @@
258 360
         "url": "https://github.com/sponsors/ljharb"
259 361
       }
260 362
     },
363
+    "node_modules/chromedriver": {
364
+      "version": "138.0.1",
365
+      "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.1.tgz",
366
+      "integrity": "sha512-QS/Z1qB2OpKsPUlJjkfKmpf9lGw6ObB0dX5+dP3M0gdtbu80TUnS+EjXbtu6YkbMfr2/Qt8IfOONrawNW1GwhA==",
367
+      "hasInstallScript": true,
368
+      "license": "Apache-2.0",
369
+      "optional": true,
370
+      "dependencies": {
371
+        "@testim/chrome-version": "^1.1.4",
372
+        "axios": "^1.7.4",
373
+        "compare-versions": "^6.1.0",
374
+        "extract-zip": "^2.0.1",
375
+        "proxy-agent": "^6.4.0",
376
+        "proxy-from-env": "^1.1.0",
377
+        "tcp-port-used": "^1.0.2"
378
+      },
379
+      "bin": {
380
+        "chromedriver": "bin/chromedriver"
381
+      },
382
+      "engines": {
383
+        "node": ">=20"
384
+      }
385
+    },
261 386
     "node_modules/combined-stream": {
262 387
       "version": "1.0.8",
263 388
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -270,6 +395,13 @@
270 395
         "node": ">= 0.8"
271 396
       }
272 397
     },
398
+    "node_modules/compare-versions": {
399
+      "version": "6.1.1",
400
+      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
401
+      "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
402
+      "license": "MIT",
403
+      "optional": true
404
+    },
273 405
     "node_modules/concat-stream": {
274 406
       "version": "2.0.0",
275 407
       "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -337,6 +469,16 @@
337 469
         "node": ">= 0.10"
338 470
       }
339 471
     },
472
+    "node_modules/data-uri-to-buffer": {
473
+      "version": "6.0.2",
474
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
475
+      "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
476
+      "license": "MIT",
477
+      "optional": true,
478
+      "engines": {
479
+        "node": ">= 14"
480
+      }
481
+    },
340 482
     "node_modules/date-fns": {
341 483
       "version": "4.1.0",
342 484
       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
@@ -385,6 +527,28 @@
385 527
         }
386 528
       }
387 529
     },
530
+    "node_modules/deep-is": {
531
+      "version": "0.1.4",
532
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
533
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
534
+      "license": "MIT",
535
+      "optional": true
536
+    },
537
+    "node_modules/degenerator": {
538
+      "version": "5.0.1",
539
+      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
540
+      "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
541
+      "license": "MIT",
542
+      "optional": true,
543
+      "dependencies": {
544
+        "ast-types": "^0.13.4",
545
+        "escodegen": "^2.1.0",
546
+        "esprima": "^4.0.1"
547
+      },
548
+      "engines": {
549
+        "node": ">= 14"
550
+      }
551
+    },
388 552
     "node_modules/delayed-stream": {
389 553
       "version": "1.0.0",
390 554
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -444,6 +608,21 @@
444 608
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
445 609
       "license": "MIT"
446 610
     },
611
+    "node_modules/elliptic": {
612
+      "version": "6.6.1",
613
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
614
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
615
+      "license": "MIT",
616
+      "dependencies": {
617
+        "bn.js": "^4.11.9",
618
+        "brorand": "^1.1.0",
619
+        "hash.js": "^1.0.0",
620
+        "hmac-drbg": "^1.0.1",
621
+        "inherits": "^2.0.4",
622
+        "minimalistic-assert": "^1.0.1",
623
+        "minimalistic-crypto-utils": "^1.0.1"
624
+      }
625
+    },
447 626
     "node_modules/encodeurl": {
448 627
       "version": "2.0.0",
449 628
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -453,6 +632,16 @@
453 632
         "node": ">= 0.8"
454 633
       }
455 634
     },
635
+    "node_modules/end-of-stream": {
636
+      "version": "1.4.5",
637
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
638
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
639
+      "license": "MIT",
640
+      "optional": true,
641
+      "dependencies": {
642
+        "once": "^1.4.0"
643
+      }
644
+    },
456 645
     "node_modules/es-define-property": {
457 646
       "version": "1.0.1",
458 647
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -504,6 +693,62 @@
504 693
       "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
505 694
       "license": "MIT"
506 695
     },
696
+    "node_modules/escodegen": {
697
+      "version": "2.1.0",
698
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
699
+      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
700
+      "license": "BSD-2-Clause",
701
+      "optional": true,
702
+      "dependencies": {
703
+        "esprima": "^4.0.1",
704
+        "estraverse": "^5.2.0",
705
+        "esutils": "^2.0.2"
706
+      },
707
+      "bin": {
708
+        "escodegen": "bin/escodegen.js",
709
+        "esgenerate": "bin/esgenerate.js"
710
+      },
711
+      "engines": {
712
+        "node": ">=6.0"
713
+      },
714
+      "optionalDependencies": {
715
+        "source-map": "~0.6.1"
716
+      }
717
+    },
718
+    "node_modules/esprima": {
719
+      "version": "4.0.1",
720
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
721
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
722
+      "license": "BSD-2-Clause",
723
+      "optional": true,
724
+      "bin": {
725
+        "esparse": "bin/esparse.js",
726
+        "esvalidate": "bin/esvalidate.js"
727
+      },
728
+      "engines": {
729
+        "node": ">=4"
730
+      }
731
+    },
732
+    "node_modules/estraverse": {
733
+      "version": "5.3.0",
734
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
735
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
736
+      "license": "BSD-2-Clause",
737
+      "optional": true,
738
+      "engines": {
739
+        "node": ">=4.0"
740
+      }
741
+    },
742
+    "node_modules/esutils": {
743
+      "version": "2.0.3",
744
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
745
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
746
+      "license": "BSD-2-Clause",
747
+      "optional": true,
748
+      "engines": {
749
+        "node": ">=0.10.0"
750
+      }
751
+    },
507 752
     "node_modules/etag": {
508 753
       "version": "1.8.1",
509 754
       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -555,6 +800,37 @@
555 800
         "url": "https://opencollective.com/express"
556 801
       }
557 802
     },
803
+    "node_modules/extract-zip": {
804
+      "version": "2.0.1",
805
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
806
+      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
807
+      "license": "BSD-2-Clause",
808
+      "optional": true,
809
+      "dependencies": {
810
+        "debug": "^4.1.1",
811
+        "get-stream": "^5.1.0",
812
+        "yauzl": "^2.10.0"
813
+      },
814
+      "bin": {
815
+        "extract-zip": "cli.js"
816
+      },
817
+      "engines": {
818
+        "node": ">= 10.17.0"
819
+      },
820
+      "optionalDependencies": {
821
+        "@types/yauzl": "^2.9.1"
822
+      }
823
+    },
824
+    "node_modules/fd-slicer": {
825
+      "version": "1.1.0",
826
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
827
+      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
828
+      "license": "MIT",
829
+      "optional": true,
830
+      "dependencies": {
831
+        "pend": "~1.2.0"
832
+      }
833
+    },
558 834
     "node_modules/finalhandler": {
559 835
       "version": "2.1.0",
560 836
       "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@@ -693,6 +969,37 @@
693 969
         "node": ">= 0.4"
694 970
       }
695 971
     },
972
+    "node_modules/get-stream": {
973
+      "version": "5.2.0",
974
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
975
+      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
976
+      "license": "MIT",
977
+      "optional": true,
978
+      "dependencies": {
979
+        "pump": "^3.0.0"
980
+      },
981
+      "engines": {
982
+        "node": ">=8"
983
+      },
984
+      "funding": {
985
+        "url": "https://github.com/sponsors/sindresorhus"
986
+      }
987
+    },
988
+    "node_modules/get-uri": {
989
+      "version": "6.0.4",
990
+      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
991
+      "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
992
+      "license": "MIT",
993
+      "optional": true,
994
+      "dependencies": {
995
+        "basic-ftp": "^5.0.2",
996
+        "data-uri-to-buffer": "^6.0.2",
997
+        "debug": "^4.3.4"
998
+      },
999
+      "engines": {
1000
+        "node": ">= 14"
1001
+      }
1002
+    },
696 1003
     "node_modules/gopd": {
697 1004
       "version": "1.2.0",
698 1005
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -732,6 +1039,16 @@
732 1039
         "url": "https://github.com/sponsors/ljharb"
733 1040
       }
734 1041
     },
1042
+    "node_modules/hash.js": {
1043
+      "version": "1.1.7",
1044
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
1045
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
1046
+      "license": "MIT",
1047
+      "dependencies": {
1048
+        "inherits": "^2.0.3",
1049
+        "minimalistic-assert": "^1.0.1"
1050
+      }
1051
+    },
735 1052
     "node_modules/hasown": {
736 1053
       "version": "2.0.2",
737 1054
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -744,6 +1061,17 @@
744 1061
         "node": ">= 0.4"
745 1062
       }
746 1063
     },
1064
+    "node_modules/hmac-drbg": {
1065
+      "version": "1.0.1",
1066
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
1067
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
1068
+      "license": "MIT",
1069
+      "dependencies": {
1070
+        "hash.js": "^1.0.3",
1071
+        "minimalistic-assert": "^1.0.0",
1072
+        "minimalistic-crypto-utils": "^1.0.1"
1073
+      }
1074
+    },
747 1075
     "node_modules/http-errors": {
748 1076
       "version": "2.0.0",
749 1077
       "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -769,6 +1097,34 @@
769 1097
         "node": ">= 0.8"
770 1098
       }
771 1099
     },
1100
+    "node_modules/http-proxy-agent": {
1101
+      "version": "7.0.2",
1102
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
1103
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
1104
+      "license": "MIT",
1105
+      "optional": true,
1106
+      "dependencies": {
1107
+        "agent-base": "^7.1.0",
1108
+        "debug": "^4.3.4"
1109
+      },
1110
+      "engines": {
1111
+        "node": ">= 14"
1112
+      }
1113
+    },
1114
+    "node_modules/https-proxy-agent": {
1115
+      "version": "7.0.6",
1116
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
1117
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
1118
+      "license": "MIT",
1119
+      "optional": true,
1120
+      "dependencies": {
1121
+        "agent-base": "^7.1.2",
1122
+        "debug": "4"
1123
+      },
1124
+      "engines": {
1125
+        "node": ">= 14"
1126
+      }
1127
+    },
772 1128
     "node_modules/iconv-lite": {
773 1129
       "version": "0.6.3",
774 1130
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -787,6 +1143,30 @@
787 1143
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
788 1144
       "license": "ISC"
789 1145
     },
1146
+    "node_modules/ip-address": {
1147
+      "version": "9.0.5",
1148
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
1149
+      "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
1150
+      "license": "MIT",
1151
+      "optional": true,
1152
+      "dependencies": {
1153
+        "jsbn": "1.1.0",
1154
+        "sprintf-js": "^1.1.3"
1155
+      },
1156
+      "engines": {
1157
+        "node": ">= 12"
1158
+      }
1159
+    },
1160
+    "node_modules/ip-regex": {
1161
+      "version": "4.3.0",
1162
+      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
1163
+      "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
1164
+      "license": "MIT",
1165
+      "optional": true,
1166
+      "engines": {
1167
+        "node": ">=8"
1168
+      }
1169
+    },
790 1170
     "node_modules/ipaddr.js": {
791 1171
       "version": "1.9.1",
792 1172
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -802,6 +1182,28 @@
802 1182
       "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
803 1183
       "license": "MIT"
804 1184
     },
1185
+    "node_modules/is-url": {
1186
+      "version": "1.2.4",
1187
+      "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
1188
+      "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
1189
+      "license": "MIT",
1190
+      "optional": true
1191
+    },
1192
+    "node_modules/is2": {
1193
+      "version": "2.0.9",
1194
+      "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz",
1195
+      "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==",
1196
+      "license": "MIT",
1197
+      "optional": true,
1198
+      "dependencies": {
1199
+        "deep-is": "^0.1.3",
1200
+        "ip-regex": "^4.1.0",
1201
+        "is-url": "^1.2.4"
1202
+      },
1203
+      "engines": {
1204
+        "node": ">=v0.10.0"
1205
+      }
1206
+    },
805 1207
     "node_modules/jiti": {
806 1208
       "version": "2.4.2",
807 1209
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
@@ -812,6 +1214,13 @@
812 1214
         "jiti": "lib/jiti-cli.mjs"
813 1215
       }
814 1216
     },
1217
+    "node_modules/jsbn": {
1218
+      "version": "1.1.0",
1219
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
1220
+      "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
1221
+      "license": "MIT",
1222
+      "optional": true
1223
+    },
815 1224
     "node_modules/jsonwebtoken": {
816 1225
       "version": "9.0.2",
817 1226
       "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -845,6 +1254,17 @@
845 1254
         "safe-buffer": "^5.0.1"
846 1255
       }
847 1256
     },
1257
+    "node_modules/jwk-to-pem": {
1258
+      "version": "2.0.7",
1259
+      "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz",
1260
+      "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==",
1261
+      "license": "Apache-2.0",
1262
+      "dependencies": {
1263
+        "asn1.js": "^5.3.0",
1264
+        "elliptic": "^6.6.1",
1265
+        "safe-buffer": "^5.0.1"
1266
+      }
1267
+    },
848 1268
     "node_modules/jws": {
849 1269
       "version": "3.2.2",
850 1270
       "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@@ -855,6 +1275,21 @@
855 1275
         "safe-buffer": "^5.0.1"
856 1276
       }
857 1277
     },
1278
+    "node_modules/keycloak-connect": {
1279
+      "version": "26.1.1",
1280
+      "resolved": "https://registry.npmjs.org/keycloak-connect/-/keycloak-connect-26.1.1.tgz",
1281
+      "integrity": "sha512-2wvNJXldB9Em+mp6liJ+AnftcJovFEvNhUgv3hblNDmVihBoBqn4zFlwLIN41lo0H8CicB2T86xZ5U2MiQ9FFA==",
1282
+      "license": "Apache-2.0",
1283
+      "dependencies": {
1284
+        "jwk-to-pem": "^2.0.0"
1285
+      },
1286
+      "engines": {
1287
+        "node": ">=14"
1288
+      },
1289
+      "optionalDependencies": {
1290
+        "chromedriver": "latest"
1291
+      }
1292
+    },
858 1293
     "node_modules/lodash.includes": {
859 1294
       "version": "4.3.0",
860 1295
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -897,6 +1332,16 @@
897 1332
       "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
898 1333
       "license": "MIT"
899 1334
     },
1335
+    "node_modules/lru-cache": {
1336
+      "version": "7.18.3",
1337
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
1338
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
1339
+      "license": "ISC",
1340
+      "optional": true,
1341
+      "engines": {
1342
+        "node": ">=12"
1343
+      }
1344
+    },
900 1345
     "node_modules/math-intrinsics": {
901 1346
       "version": "1.1.0",
902 1347
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -948,6 +1393,18 @@
948 1393
         "node": ">= 0.6"
949 1394
       }
950 1395
     },
1396
+    "node_modules/minimalistic-assert": {
1397
+      "version": "1.0.1",
1398
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
1399
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
1400
+      "license": "ISC"
1401
+    },
1402
+    "node_modules/minimalistic-crypto-utils": {
1403
+      "version": "1.0.1",
1404
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
1405
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
1406
+      "license": "MIT"
1407
+    },
951 1408
     "node_modules/minimist": {
952 1409
       "version": "1.2.8",
953 1410
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
@@ -1045,6 +1502,16 @@
1045 1502
         "node": ">= 0.6"
1046 1503
       }
1047 1504
     },
1505
+    "node_modules/netmask": {
1506
+      "version": "2.0.2",
1507
+      "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
1508
+      "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
1509
+      "license": "MIT",
1510
+      "optional": true,
1511
+      "engines": {
1512
+        "node": ">= 0.4.0"
1513
+      }
1514
+    },
1048 1515
     "node_modules/node-addon-api": {
1049 1516
       "version": "8.4.0",
1050 1517
       "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz",
@@ -1107,6 +1574,40 @@
1107 1574
         "wrappy": "1"
1108 1575
       }
1109 1576
     },
1577
+    "node_modules/pac-proxy-agent": {
1578
+      "version": "7.2.0",
1579
+      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
1580
+      "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
1581
+      "license": "MIT",
1582
+      "optional": true,
1583
+      "dependencies": {
1584
+        "@tootallnate/quickjs-emscripten": "^0.23.0",
1585
+        "agent-base": "^7.1.2",
1586
+        "debug": "^4.3.4",
1587
+        "get-uri": "^6.0.1",
1588
+        "http-proxy-agent": "^7.0.0",
1589
+        "https-proxy-agent": "^7.0.6",
1590
+        "pac-resolver": "^7.0.1",
1591
+        "socks-proxy-agent": "^8.0.5"
1592
+      },
1593
+      "engines": {
1594
+        "node": ">= 14"
1595
+      }
1596
+    },
1597
+    "node_modules/pac-resolver": {
1598
+      "version": "7.0.1",
1599
+      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
1600
+      "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
1601
+      "license": "MIT",
1602
+      "optional": true,
1603
+      "dependencies": {
1604
+        "degenerator": "^5.0.0",
1605
+        "netmask": "^2.0.2"
1606
+      },
1607
+      "engines": {
1608
+        "node": ">= 14"
1609
+      }
1610
+    },
1110 1611
     "node_modules/parseurl": {
1111 1612
       "version": "1.3.3",
1112 1613
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1125,6 +1626,13 @@
1125 1626
         "node": ">=16"
1126 1627
       }
1127 1628
     },
1629
+    "node_modules/pend": {
1630
+      "version": "1.2.0",
1631
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
1632
+      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
1633
+      "license": "MIT",
1634
+      "optional": true
1635
+    },
1128 1636
     "node_modules/pg": {
1129 1637
       "version": "8.16.2",
1130 1638
       "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz",
@@ -1292,12 +1800,43 @@
1292 1800
         "node": ">= 0.10"
1293 1801
       }
1294 1802
     },
1803
+    "node_modules/proxy-agent": {
1804
+      "version": "6.5.0",
1805
+      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
1806
+      "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
1807
+      "license": "MIT",
1808
+      "optional": true,
1809
+      "dependencies": {
1810
+        "agent-base": "^7.1.2",
1811
+        "debug": "^4.3.4",
1812
+        "http-proxy-agent": "^7.0.1",
1813
+        "https-proxy-agent": "^7.0.6",
1814
+        "lru-cache": "^7.14.1",
1815
+        "pac-proxy-agent": "^7.1.0",
1816
+        "proxy-from-env": "^1.1.0",
1817
+        "socks-proxy-agent": "^8.0.5"
1818
+      },
1819
+      "engines": {
1820
+        "node": ">= 14"
1821
+      }
1822
+    },
1295 1823
     "node_modules/proxy-from-env": {
1296 1824
       "version": "1.1.0",
1297 1825
       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1298 1826
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1299 1827
       "license": "MIT"
1300 1828
     },
1829
+    "node_modules/pump": {
1830
+      "version": "3.0.3",
1831
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
1832
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
1833
+      "license": "MIT",
1834
+      "optional": true,
1835
+      "dependencies": {
1836
+        "end-of-stream": "^1.1.0",
1837
+        "once": "^1.3.1"
1838
+      }
1839
+    },
1301 1840
     "node_modules/qs": {
1302 1841
       "version": "6.14.0",
1303 1842
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -1520,6 +2059,57 @@
1520 2059
         "url": "https://github.com/sponsors/ljharb"
1521 2060
       }
1522 2061
     },
2062
+    "node_modules/smart-buffer": {
2063
+      "version": "4.2.0",
2064
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
2065
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
2066
+      "license": "MIT",
2067
+      "optional": true,
2068
+      "engines": {
2069
+        "node": ">= 6.0.0",
2070
+        "npm": ">= 3.0.0"
2071
+      }
2072
+    },
2073
+    "node_modules/socks": {
2074
+      "version": "2.8.5",
2075
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
2076
+      "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
2077
+      "license": "MIT",
2078
+      "optional": true,
2079
+      "dependencies": {
2080
+        "ip-address": "^9.0.5",
2081
+        "smart-buffer": "^4.2.0"
2082
+      },
2083
+      "engines": {
2084
+        "node": ">= 10.0.0",
2085
+        "npm": ">= 3.0.0"
2086
+      }
2087
+    },
2088
+    "node_modules/socks-proxy-agent": {
2089
+      "version": "8.0.5",
2090
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
2091
+      "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
2092
+      "license": "MIT",
2093
+      "optional": true,
2094
+      "dependencies": {
2095
+        "agent-base": "^7.1.2",
2096
+        "debug": "^4.3.4",
2097
+        "socks": "^2.8.3"
2098
+      },
2099
+      "engines": {
2100
+        "node": ">= 14"
2101
+      }
2102
+    },
2103
+    "node_modules/source-map": {
2104
+      "version": "0.6.1",
2105
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
2106
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
2107
+      "license": "BSD-3-Clause",
2108
+      "optional": true,
2109
+      "engines": {
2110
+        "node": ">=0.10.0"
2111
+      }
2112
+    },
1523 2113
     "node_modules/split2": {
1524 2114
       "version": "4.2.0",
1525 2115
       "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1529,6 +2119,13 @@
1529 2119
         "node": ">= 10.x"
1530 2120
       }
1531 2121
     },
2122
+    "node_modules/sprintf-js": {
2123
+      "version": "1.1.3",
2124
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
2125
+      "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
2126
+      "license": "BSD-3-Clause",
2127
+      "optional": true
2128
+    },
1532 2129
     "node_modules/statuses": {
1533 2130
       "version": "2.0.2",
1534 2131
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -1555,6 +2152,42 @@
1555 2152
         "safe-buffer": "~5.2.0"
1556 2153
       }
1557 2154
     },
2155
+    "node_modules/tcp-port-used": {
2156
+      "version": "1.0.2",
2157
+      "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
2158
+      "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==",
2159
+      "license": "MIT",
2160
+      "optional": true,
2161
+      "dependencies": {
2162
+        "debug": "4.3.1",
2163
+        "is2": "^2.0.6"
2164
+      }
2165
+    },
2166
+    "node_modules/tcp-port-used/node_modules/debug": {
2167
+      "version": "4.3.1",
2168
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
2169
+      "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
2170
+      "license": "MIT",
2171
+      "optional": true,
2172
+      "dependencies": {
2173
+        "ms": "2.1.2"
2174
+      },
2175
+      "engines": {
2176
+        "node": ">=6.0"
2177
+      },
2178
+      "peerDependenciesMeta": {
2179
+        "supports-color": {
2180
+          "optional": true
2181
+        }
2182
+      }
2183
+    },
2184
+    "node_modules/tcp-port-used/node_modules/ms": {
2185
+      "version": "2.1.2",
2186
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
2187
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
2188
+      "license": "MIT",
2189
+      "optional": true
2190
+    },
1558 2191
     "node_modules/toidentifier": {
1559 2192
       "version": "1.0.1",
1560 2193
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1564,6 +2197,13 @@
1564 2197
         "node": ">=0.6"
1565 2198
       }
1566 2199
     },
2200
+    "node_modules/tslib": {
2201
+      "version": "2.8.1",
2202
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2203
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2204
+      "license": "0BSD",
2205
+      "optional": true
2206
+    },
1567 2207
     "node_modules/type-is": {
1568 2208
       "version": "2.0.1",
1569 2209
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@@ -1584,6 +2224,13 @@
1584 2224
       "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1585 2225
       "license": "MIT"
1586 2226
     },
2227
+    "node_modules/undici-types": {
2228
+      "version": "7.8.0",
2229
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
2230
+      "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
2231
+      "license": "MIT",
2232
+      "optional": true
2233
+    },
1587 2234
     "node_modules/unpipe": {
1588 2235
       "version": "1.0.0",
1589 2236
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1622,6 +2269,17 @@
1622 2269
       "engines": {
1623 2270
         "node": ">=0.4"
1624 2271
       }
2272
+    },
2273
+    "node_modules/yauzl": {
2274
+      "version": "2.10.0",
2275
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
2276
+      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
2277
+      "license": "MIT",
2278
+      "optional": true,
2279
+      "dependencies": {
2280
+        "buffer-crc32": "~0.2.3",
2281
+        "fd-slicer": "~1.1.0"
2282
+      }
1625 2283
     }
1626 2284
   }
1627 2285
 }

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
25 25
     "dotenv": "^16.5.0",
26 26
     "express": "^5.1.0",
27 27
     "jsonwebtoken": "^9.0.2",
28
+    "keycloak-connect": "^26.1.1",
28 29
     "multer": "^2.0.1",
29 30
     "pg": "^8.16.2",
30 31
     "qs": "^6.14.0"

+ 20 - 0
prisma/migrations/20250703030428_add_table_status_history/migration.sql

@@ -0,0 +1,20 @@
1
+-- CreateTable
2
+CREATE TABLE "StatusHistory" (
3
+    "id" TEXT NOT NULL,
4
+    "hospital_id" TEXT NOT NULL,
5
+    "user_id" TEXT NOT NULL,
6
+    "old_status" "ProgressStatus" NOT NULL,
7
+    "new_status" "ProgressStatus" NOT NULL,
8
+    "notes" TEXT,
9
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+    "updatedAt" TIMESTAMP(3) NOT NULL,
11
+    "deletedAt" TIMESTAMP(3),
12
+
13
+    CONSTRAINT "StatusHistory_pkey" PRIMARY KEY ("id")
14
+);
15
+
16
+-- AddForeignKey
17
+ALTER TABLE "StatusHistory" ADD CONSTRAINT "StatusHistory_hospital_id_fkey" FOREIGN KEY ("hospital_id") REFERENCES "hospitals"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
18
+
19
+-- AddForeignKey
20
+ALTER TABLE "StatusHistory" ADD CONSTRAINT "StatusHistory_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 35 - 0
prisma/migrations/20250703030614_update_name_table/migration.sql

@@ -0,0 +1,35 @@
1
+/*
2
+  Warnings:
3
+
4
+  - You are about to drop the `StatusHistory` table. If the table is not empty, all the data it contains will be lost.
5
+
6
+*/
7
+-- DropForeignKey
8
+ALTER TABLE "StatusHistory" DROP CONSTRAINT "StatusHistory_hospital_id_fkey";
9
+
10
+-- DropForeignKey
11
+ALTER TABLE "StatusHistory" DROP CONSTRAINT "StatusHistory_user_id_fkey";
12
+
13
+-- DropTable
14
+DROP TABLE "StatusHistory";
15
+
16
+-- CreateTable
17
+CREATE TABLE "status_histories" (
18
+    "id" TEXT NOT NULL,
19
+    "hospital_id" TEXT NOT NULL,
20
+    "user_id" TEXT NOT NULL,
21
+    "old_status" "ProgressStatus" NOT NULL,
22
+    "new_status" "ProgressStatus" NOT NULL,
23
+    "notes" TEXT,
24
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+    "updatedAt" TIMESTAMP(3) NOT NULL,
26
+    "deletedAt" TIMESTAMP(3),
27
+
28
+    CONSTRAINT "status_histories_pkey" PRIMARY KEY ("id")
29
+);
30
+
31
+-- AddForeignKey
32
+ALTER TABLE "status_histories" ADD CONSTRAINT "status_histories_hospital_id_fkey" FOREIGN KEY ("hospital_id") REFERENCES "hospitals"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
33
+
34
+-- AddForeignKey
35
+ALTER TABLE "status_histories" ADD CONSTRAINT "status_histories_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 9 - 0
prisma/migrations/20250703030902_update_name_field_table_status_histories/migration.sql

@@ -0,0 +1,9 @@
1
+/*
2
+  Warnings:
3
+
4
+  - You are about to drop the column `notes` on the `status_histories` table. All the data in the column will be lost.
5
+
6
+*/
7
+-- AlterTable
8
+ALTER TABLE "status_histories" DROP COLUMN "notes",
9
+ADD COLUMN     "note" TEXT;

+ 4 - 0
prisma/migrations/20250703075659_add_field_in_table_hospital/migration.sql

@@ -0,0 +1,4 @@
1
+-- AlterTable
2
+ALTER TABLE "hospitals" ADD COLUMN     "gmaps_url" TEXT,
3
+ADD COLUMN     "latitude" DOUBLE PRECISION,
4
+ADD COLUMN     "longitude" DOUBLE PRECISION;

+ 2 - 0
prisma/migrations/20250705091358_update_field_user_area/migration.sql

@@ -0,0 +1,2 @@
1
+-- DropForeignKey
2
+ALTER TABLE "user_areas" DROP CONSTRAINT "user_areas_user_id_fkey";

+ 19 - 6
prisma/schema.prisma

@@ -54,9 +54,6 @@ model User {
54 54
   createdAt  DateTime   @default(now())
55 55
   updatedAt  DateTime   @updatedAt
56 56
   deletedAt  DateTime?
57
-  hospitals  Hospital[]
58
-  user_areas UserArea[]
59
-  vendors    Vendor[]
60 57
 
61 58
   @@map("users")
62 59
 }
@@ -103,14 +100,17 @@ model Hospital {
103 100
   progress_status      ProgressStatus
104 101
   note                 String?
105 102
   created_by           String
103
+  latitude             Float?
104
+  longitude            Float?
105
+  gmaps_url            String?             @db.Text
106 106
   createdAt            DateTime            @default(now())
107 107
   updatedAt            DateTime            @updatedAt
108 108
   deletedAt            DateTime?
109 109
   province             Province            @relation(fields: [province_id], references: [id])
110 110
   city                 City                @relation(fields: [city_id], references: [id])
111
-  user                 User                @relation(fields: [created_by], references: [id])
112 111
   vendor_histories     VendorHistory[]
113 112
   executives_histories ExecutivesHistory[]
113
+  status_histories     StatusHistory[]
114 114
 
115 115
   @@map("hospitals")
116 116
 }
@@ -126,7 +126,6 @@ model Vendor {
126 126
   createdAt        DateTime        @default(now())
127 127
   updatedAt        DateTime        @updatedAt
128 128
   deletedAt        DateTime?
129
-  user             User            @relation(fields: [created_by], references: [id])
130 129
   vendor_histories VendorHistory[]
131 130
 
132 131
   @@map("vendors")
@@ -136,7 +135,6 @@ model UserArea {
136 135
   id          String    @id @default(uuid())
137 136
   user_id     String
138 137
   province_id String
139
-  user        User      @relation(fields: [user_id], references: [id])
140 138
   province    Province  @relation(fields: [province_id], references: [id])
141 139
   createdAt   DateTime  @default(now())
142 140
   updatedAt   DateTime  @updatedAt
@@ -177,3 +175,18 @@ model ExecutivesHistory {
177 175
 
178 176
   @@map("executives_histories")
179 177
 }
178
+
179
+model StatusHistory {
180
+  id          String         @id @default(uuid())
181
+  hospital_id String
182
+  user_id     String
183
+  old_status  ProgressStatus
184
+  new_status  ProgressStatus
185
+  note        String?        @db.Text
186
+  hospital    Hospital       @relation(fields: [hospital_id], references: [id])
187
+  createdAt   DateTime       @default(now())
188
+  updatedAt   DateTime       @updatedAt
189
+  deletedAt   DateTime?
190
+
191
+  @@map("status_histories")
192
+}

+ 1 - 1
src/controllers/admin/HospitalController.js

@@ -13,7 +13,7 @@ exports.getAllHospital = async (req, res) => {
13 13
             page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status
14 14
         });
15 15
 
16
-        return HospitalCollection(req, res, hospitals, total, page, limit, 'Hospital data successfully retrieved');
16
+        return HospitalCollection({ req, res, data: hospitals, total, page, limit, message: 'Hospital data successfully retrieved' });
17 17
     } catch (err) {
18 18
         return errorResponse(res, err);
19 19
     }

+ 29 - 0
src/controllers/admin/StatusHistoryController.js

@@ -0,0 +1,29 @@
1
+const { PaginationParam } = require("../../utils/PaginationParams");
2
+const statusHistoryService = require('../../services/admin/StatusHistoryService.js');
3
+const { validateCreateStatusHisotryRequest } = require("../../validators/admin/status_history/StatusHistoryValidators.js");
4
+const { StatusHistoryCollection } = require("../../resources/admin/status_history/StatusHistoryCollection.js");
5
+const { errorResponse, messageSuccessResponse } = require("../../utils/Response.js");
6
+
7
+exports.getAllStatusHistory = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { status_histories, total } = await statusHistoryService.getAllStatusHistoryService({
12
+            page, limit, search, sortBy, orderBy
13
+        }, req);
14
+
15
+        return StatusHistoryCollection({ req, res, data: status_histories, total, page, limit, message: 'Status history successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.storeStatusHistory = async (req, res) => {
22
+    try {
23
+        const validatedData = validateCreateStatusHisotryRequest(req.body);
24
+        await statusHistoryService.storeStatusHistoryService(validatedData, req);
25
+        return messageSuccessResponse(res, 'Success added status history', 201);
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+}

+ 4 - 5
src/controllers/admin/VendorController.js

@@ -1,9 +1,8 @@
1 1
 const { VendorCollection } = require('../../resources/admin/vendor/VendorCollection.js');
2 2
 const { VendorResource } = require('../../resources/admin/vendor/VendorResource.js');
3 3
 const vendorService = require('../../services/admin/VendorService.js');
4
-const { ListResponse } = require('../../utils/ListResponse.js');
5 4
 const { PaginationParam } = require('../../utils/PaginationParams.js');
6
-const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { errorResponse, messageSuccessResponse } = require('../../utils/Response.js');
7 6
 const { validateStoreVendorRequest, validateUpdateVendorRequest } = require('../../validators/admin/vendor/VendorValidators.js');
8 7
 
9 8
 exports.getAllVendor = async (req, res) => {
@@ -14,7 +13,7 @@ exports.getAllVendor = async (req, res) => {
14 13
             page, limit, search, sortBy, orderBy
15 14
         });
16 15
 
17
-        return VendorCollection(req, res, vendors, total, page, limit, 'Vendor data successfully retrieved');
16
+        return VendorCollection({ req, res, data: vendors, total, page, limit, message: 'Vendor data successfully retrieved' });
18 17
     } catch (err) {
19 18
         return errorResponse(res, err);
20 19
     }
@@ -23,8 +22,8 @@ exports.getAllVendor = async (req, res) => {
23 22
 exports.showVendor = async (req, res) => {
24 23
     try {
25 24
         const id = req.params.id;
26
-        const data = await vendorService.showVendorService(id);
27
-        return VendorResource(res, data, 'Success show vendor');
25
+        const { vendor, userName } = await vendorService.showVendorService(id);
26
+        return VendorResource(res, vendor, userName, 'Success show vendor');
28 27
     } catch (err) {
29 28
         return errorResponse(res, err);
30 29
     }

+ 1 - 1
src/controllers/sales/AreaController.js

@@ -9,7 +9,7 @@ exports.getAllAreaByUser = async (req, res) => {
9 9
 
10 10
         const { areas, total } = await getAllAreaByUserService({ page, limit, search, sortBy, orderBy }, req);
11 11
 
12
-        return UserAreaCollection(req, res, areas, total, page, limit, 'Area data successfully retrieved');
12
+        return UserAreaCollection({ req, res, data: areas, total, page, limit, message: 'Area data successfully retrieved' });
13 13
     } catch (err) {
14 14
         return errorResponse(res, err);
15 15
     }

+ 3 - 3
src/controllers/sales/HospitalController.js

@@ -11,7 +11,7 @@ exports.getAllHospitalByArea = async (req, res) => {
11 11
 
12 12
         const { hospitals, total } = await getAllHospitalByAreaService({ page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status }, req);
13 13
 
14
-        return HospitalCollection(req, res, hospitals, total, page, limit, 'Hospital data successfully retrieved');
14
+        return HospitalCollection({ req, res, data: hospitals, total, page, limit, message: 'Hospital data successfully retrieved' });
15 15
     } catch (err) {
16 16
         return errorResponse(res, err);
17 17
     }
@@ -41,8 +41,8 @@ exports.updateHospital = async (req, res) => {
41 41
 exports.showHospital = async (req, res) => {
42 42
     try {
43 43
         const id = req.params.id;
44
-        const data = await showHospitalService(id);
45
-        return HospitalResource(res, data, 'Success show hospital');
44
+        const { hospital, userName } = await showHospitalService(id);
45
+        return HospitalResource(res, hospital, userName, 'Success show hospital');
46 46
     } catch (err) {
47 47
         return errorResponse(res, err);
48 48
     }

+ 29 - 0
src/controllers/sales/StatusHistoryController.js

@@ -0,0 +1,29 @@
1
+const { PaginationParam } = require("../../utils/PaginationParams");
2
+const statusHistoryService = require('../../services/sales/StatusHistoryService.js');
3
+const { validateCreateStatusHisotryRequest } = require("../../validators/admin/status_history/StatusHistoryValidators.js");
4
+const { StatusHistoryCollection } = require("../../resources/sales/status_history/StatusHistoryCollection.js");
5
+const { errorResponse, messageSuccessResponse } = require("../../utils/Response.js");
6
+
7
+exports.getAllStatusHistory = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { status_histories, total } = await statusHistoryService.getAllStatusHistoryService({
12
+            page, limit, search, sortBy, orderBy
13
+        }, req);
14
+
15
+        return StatusHistoryCollection({ req, res, data: status_histories, total, page, limit, message: 'Status history successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.storeStatusHistory = async (req, res) => {
22
+    try {
23
+        const validatedData = validateCreateStatusHisotryRequest(req.body);
24
+        await statusHistoryService.storeStatusHistoryService(validatedData, req);
25
+        return messageSuccessResponse(res, 'Success added status history', 201);
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+}

+ 21 - 0
src/middleware/CheckRoles.js

@@ -0,0 +1,21 @@
1
+module.exports = (allowedRoles = []) => {
2
+    return async (req, res, next) => {
3
+        try {
4
+            const tokenData = req.tokenData;
5
+            const roles = tokenData.realm_access?.roles || [];
6
+
7
+            const hasAccess = allowedRoles.some(role => roles.includes(role));
8
+
9
+            if (hasAccess) {
10
+                return next();
11
+            }
12
+
13
+            const error = new Error('Access Denied');
14
+            // const error = new Error(`Access Denied: You need one of the following roles: ${allowedRoles.join(", ")}`);
15
+            error.statusCode = 403;
16
+            throw error;
17
+        } catch (error) {
18
+            next(error);
19
+        }
20
+    };
21
+};

+ 22 - 0
src/middleware/ExtractToken.js

@@ -0,0 +1,22 @@
1
+const jwt = require("jsonwebtoken");
2
+
3
+module.exports = async (req, res, next) => {
4
+    try {
5
+        // Decode access token
6
+        const bearerToken = req.headers.authorization;
7
+        // bearerToken would return "Bearer <access_token>"
8
+
9
+        const token = bearerToken.split(" ");
10
+        // token would return ["Bearer", "<access_token>"]
11
+
12
+        const tokenData = jwt.decode(token[1]);
13
+        // tokenData would return user's data
14
+
15
+        // Store decoded token data in request
16
+        req.tokenData = tokenData;
17
+
18
+        next();
19
+    } catch (error) {
20
+        next(error);
21
+    }
22
+}

+ 12 - 0
src/middleware/Keycloak.js

@@ -0,0 +1,12 @@
1
+const Keycloak = require("keycloak-connect");
2
+require('dotenv').config();
3
+
4
+const config = {
5
+  "realm": process.env.KEYCLOAK_REALM,
6
+  "auth-server-url": `${process.env.KEYCLOAK_URL}`,
7
+  "ssl-required": "external",
8
+  "resource": process.env.KEYCLOAK_CLIENT,
9
+  "bearer-only": true
10
+}
11
+
12
+module.exports = new Keycloak({}, config);

+ 2 - 12
src/repository/admin/ExecutivesHistoryRepository.js

@@ -34,12 +34,7 @@ const ExecutivesHistoryRepository = {
34 34
                         image: true,
35 35
                         progress_status: true,
36 36
                         note: true,
37
-                        user: {
38
-                            select: {
39
-                                id: true,
40
-                                username: true
41
-                            }
42
-                        }
37
+                        created_by: true
43 38
                     }
44 39
                 },
45 40
                 executive_name: true,
@@ -90,12 +85,7 @@ const ExecutivesHistoryRepository = {
90 85
                         image: true,
91 86
                         progress_status: true,
92 87
                         note: true,
93
-                        user: {
94
-                            select: {
95
-                                id: true,
96
-                                username: true
97
-                            }
98
-                        }
88
+                        created_by: true
99 89
                     }
100 90
                 },
101 91
                 executive_name: true,

+ 63 - 36
src/repository/admin/HospitalRepository.js

@@ -2,7 +2,7 @@ const prisma = require('../../prisma/PrismaClient.js');
2 2
 
3 3
 const HospitalRepository = {
4 4
     findAll: async ({ skip, take, where, orderBy }) => {
5
-        return await prisma.hospital.findMany({
5
+        const hospitalsRaw = await prisma.hospital.findMany({
6 6
             where,
7 7
             skip,
8 8
             take,
@@ -21,27 +21,40 @@ const HospitalRepository = {
21 21
                 image: true,
22 22
                 progress_status: true,
23 23
                 note: true,
24
-                user: { select: { id: true, username: true } },
24
+                latitude: true,
25
+                longitude: true,
26
+                gmaps_url: true,
27
+                created_by: true,
25 28
                 createdAt: true,
26 29
                 updatedAt: true,
27
-                // vendor_histories: {
28
-                //     where: { deletedAt: null },
29
-                //     orderBy: { createdAt: 'desc' },
30
-                //     select: {
31
-                //         vendor: {
32
-                //             select: {
33
-                //                 id: true,
34
-                //                 name: true,
35
-                //                 name_pt: true,
36
-                //                 strengths: true,
37
-                //                 weaknesses: true,
38
-                //                 website: true,
39
-                //             }
40
-                //         }
41
-                //     }
42
-                // }
30
+                vendor_histories: {
31
+                    where: {
32
+                        status: "active",
33
+                        deletedAt: null
34
+                    },
35
+                    select: {
36
+                        vendor: {
37
+                            select: {
38
+                                id: true,
39
+                                name: true,
40
+                                name_pt: true,
41
+                                strengths: true,
42
+                                weaknesses: true,
43
+                                website: true,
44
+                            }
45
+                        }
46
+                    }
47
+                }
43 48
             }
44 49
         });
50
+
51
+        return hospitalsRaw.map(hospital => {
52
+            const { vendor_histories, ...rest } = hospital;
53
+            return {
54
+                ...rest,
55
+                vendor: vendor_histories?.[0]?.vendor || null
56
+            };
57
+        });
45 58
     },
46 59
 
47 60
     countAll: async (where) => {
@@ -49,7 +62,7 @@ const HospitalRepository = {
49 62
     },
50 63
 
51 64
     findById: async (id) => {
52
-        return prisma.hospital.findFirst({
65
+        const hospitalRaw = await prisma.hospital.findFirst({
53 66
             where: {
54 67
                 id,
55 68
                 deletedAt: null
@@ -68,27 +81,41 @@ const HospitalRepository = {
68 81
                 image: true,
69 82
                 progress_status: true,
70 83
                 note: true,
71
-                user: { select: { id: true, username: true } },
84
+                latitude: true,
85
+                longitude: true,
86
+                gmaps_url: true,
87
+                created_by: true,
72 88
                 createdAt: true,
73 89
                 updatedAt: true,
74
-                // vendor_histories: {
75
-                //     where: { deletedAt: null },
76
-                //     orderBy: { createdAt: 'desc' },
77
-                //     select: {
78
-                //         vendor: {
79
-                //             select: {
80
-                //                 id: true,
81
-                //                 name: true,
82
-                //                 name_pt: true,
83
-                //                 strengths: true,
84
-                //                 weaknesses: true,
85
-                //                 website: true,
86
-                //             }
87
-                //         }
88
-                //     }
89
-                // }
90
+                vendor_histories: {
91
+                    where: {
92
+                        status: "active",
93
+                        deletedAt: null
94
+                    },
95
+                    take: 1,
96
+                    select: {
97
+                        vendor: {
98
+                            select: {
99
+                                id: true,
100
+                                name: true,
101
+                                name_pt: true,
102
+                                strengths: true,
103
+                                weaknesses: true,
104
+                                website: true,
105
+                            }
106
+                        }
107
+                    }
108
+                }
90 109
             }
91 110
         });
111
+
112
+        if (!hospitalRaw) return null;
113
+
114
+        const { vendor_histories, ...rest } = hospitalRaw;
115
+        return {
116
+            ...rest,
117
+            vendor: vendor_histories?.[0]?.vendor || null
118
+        };
92 119
     },
93 120
 
94 121
     create: async (data) => {

+ 64 - 0
src/repository/admin/StatusHistoryRepository.js

@@ -0,0 +1,64 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const StatusHistoryRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return prisma.statusHistory.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                hospital: {
13
+                    select: {
14
+                        id: true,
15
+                        name: true,
16
+                        // hospital_code: true,
17
+                        // type: true,
18
+                        // ownership: true,
19
+                        // province: {
20
+                        //     select: {
21
+                        //         id: true,
22
+                        //         name: true
23
+                        //     }
24
+                        // },
25
+                        // city: {
26
+                        //     select: {
27
+                        //         id: true,
28
+                        //         name: true
29
+                        //     }
30
+                        // },
31
+                        // address: true,
32
+                        // simrs_type: true,
33
+                        // contact: true,
34
+                        // image: true,
35
+                        progress_status: true,
36
+                        // note: true,
37
+                        // user: {
38
+                        //     select: {
39
+                        //         id: true,
40
+                        //         username: true
41
+                        //     }
42
+                        // }
43
+                    }
44
+                },
45
+                user_id: true,
46
+                old_status: true,
47
+                new_status: true,
48
+                note: true,
49
+                createdAt: true,
50
+                updatedAt: true,
51
+            },
52
+        });
53
+    },
54
+
55
+    countAll: async (where) => {
56
+        return prisma.statusHistory.count({ where });
57
+    },
58
+
59
+    create: async (data) => {
60
+        return prisma.statusHistory.create({ data });
61
+    },
62
+};
63
+
64
+module.exports = StatusHistoryRepository;

+ 58 - 78
src/repository/admin/VendorHistoryRepository.js

@@ -9,39 +9,34 @@ const VendorHistoryRepository = {
9 9
             orderBy,
10 10
             select: {
11 11
                 id: true,
12
-                hospital: {
13
-                    select: {
14
-                        id: true,
15
-                        name: true,
16
-                        hospital_code: true,
17
-                        type: true,
18
-                        ownership: true,
19
-                        province: {
20
-                            select: {
21
-                                id: true,
22
-                                name: true
23
-                            }
24
-                        },
25
-                        city: {
26
-                            select: {
27
-                                id: true,
28
-                                name: true
29
-                            }
30
-                        },
31
-                        address: true,
32
-                        simrs_type: true,
33
-                        contact: true,
34
-                        image: true,
35
-                        progress_status: true,
36
-                        note: true,
37
-                        user: {
38
-                            select: {
39
-                                id: true,
40
-                                username: true
41
-                            }
42
-                        }
43
-                    }
44
-                },
12
+                // hospital: {
13
+                //     select: {
14
+                //         id: true,
15
+                //         name: true,
16
+                //         hospital_code: true,
17
+                //         type: true,
18
+                //         ownership: true,
19
+                //         province: {
20
+                //             select: {
21
+                //                 id: true,
22
+                //                 name: true
23
+                //             }
24
+                //         },
25
+                //         city: {
26
+                //             select: {
27
+                //                 id: true,
28
+                //                 name: true
29
+                //             }
30
+                //         },
31
+                //         address: true,
32
+                //         simrs_type: true,
33
+                //         contact: true,
34
+                //         image: true,
35
+                //         progress_status: true,
36
+                //         note: true,
37
+                //         created_by: true
38
+                //     }
39
+                // },
45 40
                 vendor: {
46 41
                     select: {
47 42
                         id: true,
@@ -50,12 +45,7 @@ const VendorHistoryRepository = {
50 45
                         strengths: true,
51 46
                         weaknesses: true,
52 47
                         website: true,
53
-                        user: {
54
-                            select: {
55
-                                id: true,
56
-                                username: true
57
-                            }
58
-                        }
48
+                        created_by: true,
59 49
                     }
60 50
                 },
61 51
                 vendor_impression: true,
@@ -80,39 +70,34 @@ const VendorHistoryRepository = {
80 70
             },
81 71
             select: {
82 72
                 id: true,
83
-                hospital: {
84
-                    select: {
85
-                        id: true,
86
-                        name: true,
87
-                        hospital_code: true,
88
-                        type: true,
89
-                        ownership: true,
90
-                        province: {
91
-                            select: {
92
-                                id: true,
93
-                                name: true
94
-                            }
95
-                        },
96
-                        city: {
97
-                            select: {
98
-                                id: true,
99
-                                name: true
100
-                            }
101
-                        },
102
-                        address: true,
103
-                        simrs_type: true,
104
-                        contact: true,
105
-                        image: true,
106
-                        progress_status: true,
107
-                        note: true,
108
-                        user: {
109
-                            select: {
110
-                                id: true,
111
-                                username: true
112
-                            }
113
-                        }
114
-                    }
115
-                },
73
+                // hospital: {
74
+                //     select: {
75
+                //         id: true,
76
+                //         name: true,
77
+                //         hospital_code: true,
78
+                //         type: true,
79
+                //         ownership: true,
80
+                //         province: {
81
+                //             select: {
82
+                //                 id: true,
83
+                //                 name: true
84
+                //             }
85
+                //         },
86
+                //         city: {
87
+                //             select: {
88
+                //                 id: true,
89
+                //                 name: true
90
+                //             }
91
+                //         },
92
+                //         address: true,
93
+                //         simrs_type: true,
94
+                //         contact: true,
95
+                //         image: true,
96
+                //         progress_status: true,
97
+                //         note: true,
98
+                //         created_by: true
99
+                //     }
100
+                // },
116 101
                 vendor: {
117 102
                     select: {
118 103
                         id: true,
@@ -121,12 +106,7 @@ const VendorHistoryRepository = {
121 106
                         strengths: true,
122 107
                         weaknesses: true,
123 108
                         website: true,
124
-                        user: {
125
-                            select: {
126
-                                id: true,
127
-                                username: true
128
-                            }
129
-                        }
109
+                        created_by: true,
130 110
                     }
131 111
                 },
132 112
                 vendor_impression: true,

+ 2 - 18
src/repository/admin/VendorRepository.js

@@ -15,15 +15,7 @@ const VendorRepository = {
15 15
                 strengths: true,
16 16
                 weaknesses: true,
17 17
                 website: true,
18
-                user: {
19
-                    select: {
20
-                        id: true,
21
-                        username: true,
22
-                        email: true,
23
-                        firstname: true,
24
-                        lastname: true,
25
-                    }
26
-                },
18
+                created_by: true,
27 19
                 _count: {
28 20
                     select: {
29 21
                         vendor_histories: {
@@ -66,15 +58,7 @@ const VendorRepository = {
66 58
                 strengths: true,
67 59
                 weaknesses: true,
68 60
                 website: true,
69
-                user: {
70
-                    select: {
71
-                        id: true,
72
-                        username: true,
73
-                        email: true,
74
-                        firstname: true,
75
-                        lastname: true,
76
-                    }
77
-                },
61
+                created_by: true,
78 62
                 _count: {
79 63
                     select: {
80 64
                         vendor_histories: {

+ 1 - 6
src/repository/sales/AreaRepository.js

@@ -9,12 +9,7 @@ const AreaRepository = {
9 9
             orderBy,
10 10
             select: {
11 11
                 id: true,
12
-                user: {
13
-                    select: {
14
-                        id: true,
15
-                        username: true
16
-                    }
17
-                },
12
+                user_id: true,
18 13
                 province: {
19 14
                     select: {
20 15
                         id: true,

+ 8 - 12
src/repository/sales/ExecutivesHistoryRepository.js

@@ -34,12 +34,13 @@ const ExecutivesHistoryRepository = {
34 34
                         image: true,
35 35
                         progress_status: true,
36 36
                         note: true,
37
-                        user: {
38
-                            select: {
39
-                                id: true,
40
-                                username: true
41
-                            }
42
-                        }
37
+                        // user: {
38
+                        //     select: {
39
+                        //         id: true,
40
+                        //         username: true
41
+                        //     }
42
+                        // }
43
+                        created_by: true
43 44
                     }
44 45
                 },
45 46
                 executive_name: true,
@@ -90,12 +91,7 @@ const ExecutivesHistoryRepository = {
90 91
                         image: true,
91 92
                         progress_status: true,
92 93
                         note: true,
93
-                        user: {
94
-                            select: {
95
-                                id: true,
96
-                                username: true
97
-                            }
98
-                        }
94
+                        created_by: true
99 95
                     }
100 96
                 },
101 97
                 executive_name: true,

+ 63 - 4
src/repository/sales/HospitalRepository.js

@@ -2,7 +2,7 @@ const prisma = require('../../prisma/PrismaClient.js');
2 2
 
3 3
 const HospitalRepository = {
4 4
     findAll: async ({ skip, take, where, orderBy }) => {
5
-        return await prisma.hospital.findMany({
5
+        const hospitalsRaw = await prisma.hospital.findMany({
6 6
             where,
7 7
             skip,
8 8
             take,
@@ -21,11 +21,40 @@ const HospitalRepository = {
21 21
                 image: true,
22 22
                 progress_status: true,
23 23
                 note: true,
24
-                user: { select: { id: true, username: true } },
24
+                latitude: true,
25
+                longitude: true,
26
+                gmaps_url: true,
27
+                created_by: true,
25 28
                 createdAt: true,
26 29
                 updatedAt: true,
30
+                vendor_histories: {
31
+                    where: {
32
+                        status: "active",
33
+                        deletedAt: null
34
+                    },
35
+                    select: {
36
+                        vendor: {
37
+                            select: {
38
+                                id: true,
39
+                                name: true,
40
+                                name_pt: true,
41
+                                strengths: true,
42
+                                weaknesses: true,
43
+                                website: true,
44
+                            }
45
+                        }
46
+                    }
47
+                }
27 48
             }
28 49
         });
50
+
51
+        return hospitalsRaw.map(hospital => {
52
+            const { vendor_histories, ...rest } = hospital;
53
+            return {
54
+                ...rest,
55
+                vendor: vendor_histories?.[0]?.vendor || null
56
+            };
57
+        });
29 58
     },
30 59
 
31 60
     countAll: async (where) => {
@@ -37,7 +66,7 @@ const HospitalRepository = {
37 66
     },
38 67
 
39 68
     findById: async (id) => {
40
-        return prisma.hospital.findFirst({
69
+        const hospitalRaw = await prisma.hospital.findFirst({
41 70
             where: {
42 71
                 id,
43 72
                 deletedAt: null
@@ -56,11 +85,41 @@ const HospitalRepository = {
56 85
                 image: true,
57 86
                 progress_status: true,
58 87
                 note: true,
59
-                user: { select: { id: true, username: true } },
88
+                latitude: true,
89
+                longitude: true,
90
+                gmaps_url: true,
91
+                created_by: true,
60 92
                 createdAt: true,
61 93
                 updatedAt: true,
94
+                vendor_histories: {
95
+                    where: {
96
+                        status: "active",
97
+                        deletedAt: null
98
+                    },
99
+                    take: 1,
100
+                    select: {
101
+                        vendor: {
102
+                            select: {
103
+                                id: true,
104
+                                name: true,
105
+                                name_pt: true,
106
+                                strengths: true,
107
+                                weaknesses: true,
108
+                                website: true,
109
+                            }
110
+                        }
111
+                    }
112
+                }
62 113
             }
63 114
         });
115
+
116
+        if (!hospitalRaw) return null;
117
+
118
+        const { vendor_histories, ...rest } = hospitalRaw;
119
+        return {
120
+            ...rest,
121
+            vendor: vendor_histories?.[0]?.vendor || null
122
+        };
64 123
     },
65 124
 
66 125
     // update: async (id, data) => {

+ 70 - 0
src/repository/sales/StatusHistoryRepository.js

@@ -0,0 +1,70 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const StatusHistoryRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return prisma.statusHistory.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                hospital: {
13
+                    select: {
14
+                        id: true,
15
+                        name: true,
16
+                        // hospital_code: true,
17
+                        // type: true,
18
+                        // ownership: true,
19
+                        // province: {
20
+                        //     select: {
21
+                        //         id: true,
22
+                        //         name: true
23
+                        //     }
24
+                        // },
25
+                        // city: {
26
+                        //     select: {
27
+                        //         id: true,
28
+                        //         name: true
29
+                        //     }
30
+                        // },
31
+                        // address: true,
32
+                        // simrs_type: true,
33
+                        // contact: true,
34
+                        // image: true,
35
+                        progress_status: true,
36
+                        // note: true,
37
+                        // user: {
38
+                        //     select: {
39
+                        //         id: true,
40
+                        //         username: true
41
+                        //     }
42
+                        // }
43
+                    }
44
+                },
45
+                // user: {
46
+                //     select: {
47
+                //         id: true,
48
+                //         username: true,
49
+                //     }
50
+                // },
51
+                user_id: true,
52
+                old_status: true,
53
+                new_status: true,
54
+                note: true,
55
+                createdAt: true,
56
+                updatedAt: true,
57
+            },
58
+        });
59
+    },
60
+
61
+    countAll: async (where) => {
62
+        return prisma.statusHistory.count({ where });
63
+    },
64
+
65
+    create: async (data) => {
66
+        return prisma.statusHistory.create({ data });
67
+    },
68
+};
69
+
70
+module.exports = StatusHistoryRepository;

+ 58 - 78
src/repository/sales/VendorHistoryRepository.js

@@ -9,39 +9,34 @@ const VendorHistoryRepository = {
9 9
             orderBy,
10 10
             select: {
11 11
                 id: true,
12
-                hospital: {
13
-                    select: {
14
-                        id: true,
15
-                        name: true,
16
-                        hospital_code: true,
17
-                        type: true,
18
-                        ownership: true,
19
-                        province: {
20
-                            select: {
21
-                                id: true,
22
-                                name: true
23
-                            }
24
-                        },
25
-                        city: {
26
-                            select: {
27
-                                id: true,
28
-                                name: true
29
-                            }
30
-                        },
31
-                        address: true,
32
-                        simrs_type: true,
33
-                        contact: true,
34
-                        image: true,
35
-                        progress_status: true,
36
-                        note: true,
37
-                        user: {
38
-                            select: {
39
-                                id: true,
40
-                                username: true
41
-                            }
42
-                        }
43
-                    }
44
-                },
12
+                // hospital: {
13
+                //     select: {
14
+                //         id: true,
15
+                //         name: true,
16
+                //         hospital_code: true,
17
+                //         type: true,
18
+                //         ownership: true,
19
+                //         province: {
20
+                //             select: {
21
+                //                 id: true,
22
+                //                 name: true
23
+                //             }
24
+                //         },
25
+                //         city: {
26
+                //             select: {
27
+                //                 id: true,
28
+                //                 name: true
29
+                //             }
30
+                //         },
31
+                //         address: true,
32
+                //         simrs_type: true,
33
+                //         contact: true,
34
+                //         image: true,
35
+                //         progress_status: true,
36
+                //         note: true,
37
+                //         created_by: true
38
+                //     }
39
+                // },
45 40
                 vendor: {
46 41
                     select: {
47 42
                         id: true,
@@ -50,12 +45,7 @@ const VendorHistoryRepository = {
50 45
                         strengths: true,
51 46
                         weaknesses: true,
52 47
                         website: true,
53
-                        user: {
54
-                            select: {
55
-                                id: true,
56
-                                username: true
57
-                            }
58
-                        }
48
+                        created_by: true,
59 49
                     }
60 50
                 },
61 51
                 vendor_impression: true,
@@ -80,39 +70,34 @@ const VendorHistoryRepository = {
80 70
             },
81 71
             select: {
82 72
                 id: true,
83
-                hospital: {
84
-                    select: {
85
-                        id: true,
86
-                        name: true,
87
-                        hospital_code: true,
88
-                        type: true,
89
-                        ownership: true,
90
-                        province: {
91
-                            select: {
92
-                                id: true,
93
-                                name: true
94
-                            }
95
-                        },
96
-                        city: {
97
-                            select: {
98
-                                id: true,
99
-                                name: true
100
-                            }
101
-                        },
102
-                        address: true,
103
-                        simrs_type: true,
104
-                        contact: true,
105
-                        image: true,
106
-                        progress_status: true,
107
-                        note: true,
108
-                        user: {
109
-                            select: {
110
-                                id: true,
111
-                                username: true
112
-                            }
113
-                        }
114
-                    }
115
-                },
73
+                // hospital: {
74
+                //     select: {
75
+                //         id: true,
76
+                //         name: true,
77
+                //         hospital_code: true,
78
+                //         type: true,
79
+                //         ownership: true,
80
+                //         province: {
81
+                //             select: {
82
+                //                 id: true,
83
+                //                 name: true
84
+                //             }
85
+                //         },
86
+                //         city: {
87
+                //             select: {
88
+                //                 id: true,
89
+                //                 name: true
90
+                //             }
91
+                //         },
92
+                //         address: true,
93
+                //         simrs_type: true,
94
+                //         contact: true,
95
+                //         image: true,
96
+                //         progress_status: true,
97
+                //         note: true,
98
+                //         created_by: true
99
+                //     }
100
+                // },
116 101
                 vendor: {
117 102
                     select: {
118 103
                         id: true,
@@ -121,12 +106,7 @@ const VendorHistoryRepository = {
121 106
                         strengths: true,
122 107
                         weaknesses: true,
123 108
                         website: true,
124
-                        user: {
125
-                            select: {
126
-                                id: true,
127
-                                username: true
128
-                            }
129
-                        }
109
+                        created_by: true,
130 110
                     }
131 111
                 },
132 112
                 vendor_impression: true,

+ 20 - 15
src/resources/admin/hospital/HospitalCollection.js

@@ -1,22 +1,27 @@
1 1
 const { ListResponse } = require("../../../utils/ListResponse");
2 2
 const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
+const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
3 4
 
4
-const formatItem = (item) => ({
5
-    ...item,
6
-    createdAt: formatISOWithoutTimezone(item.createdAt),
7
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
8
-});
5
+// Fungsi transform per item
6
+const transformHospitalList = async (data = []) => {
7
+    return Promise.all(data.map(async ({ created_by, ...rest }) => {
8
+        const name = await getUserNameById(created_by);
9 9
 
10
-exports.HospitalCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
11
-    const formattedData = data.map(formatItem);
10
+        return {
11
+            ...rest,
12
+            user: {
13
+                id: created_by,
14
+                name: name
15
+            },
16
+            createdAt: formatISOWithoutTimezone(rest.createdAt),
17
+            updatedAt: formatISOWithoutTimezone(rest.updatedAt)
18
+        };
19
+    }));
20
+};
12 21
 
13
-    if (typeof total !== 'number') {
14
-        return res.status(200).json({
15
-            success: true,
16
-            message,
17
-            data: Array.isArray(formattedData)
18
-        });
19
-    }
22
+// Collection yang async
23
+exports.HospitalCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
24
+    const formatted = await transformHospitalList(data);
20 25
 
21
-    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
26
+    return ListResponse({ req, res, data: formatted, total, page, limit, message });
22 27
 };

+ 27 - 0
src/resources/admin/status_history/StatusHistoryCollection.js

@@ -0,0 +1,27 @@
1
+const { ListResponse } = require("../../../utils/ListResponse");
2
+const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
+const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
4
+
5
+const transformStatusHistoryList = async (data = []) => {
6
+    return Promise.all(data.map(async ({ user_id, ...rest }) => {
7
+        const name = await getUserNameById(user_id);
8
+
9
+        return {
10
+            ...rest,
11
+            user: {
12
+                id: user_id,
13
+                name: name
14
+            },
15
+            note: rest.note?.trim() === '' ? null : rest.note,
16
+            createdAt: formatISOWithoutTimezone(rest.createdAt),
17
+            updatedAt: formatISOWithoutTimezone(rest.updatedAt)
18
+        };
19
+    }));
20
+};
21
+
22
+// Collection yang async
23
+exports.StatusHistoryCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
24
+    const formatted = await transformStatusHistoryList(data);
25
+
26
+    return ListResponse({ req, res, data: formatted, total, page, limit, message });
27
+};

+ 19 - 15
src/resources/admin/vendor/VendorCollection.js

@@ -1,22 +1,26 @@
1 1
 const { ListResponse } = require("../../../utils/ListResponse");
2 2
 const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
+const { getUserNameById } = require("../../../utils/CheckUserKeycloak");
3 4
 
4
-const formatItem = (item) => ({
5
-    ...item,
6
-    createdAt: formatISOWithoutTimezone(item.createdAt),
7
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
8
-});
5
+const transformVendorList = async (data = []) => {
6
+    return Promise.all(data.map(async ({ created_by, ...rest }) => {
7
+        const name = await getUserNameById(created_by);
9 8
 
10
-exports.VendorCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
11
-    const formattedData = data.map(formatItem);
9
+        return {
10
+            ...rest,
11
+            user: {
12
+                id: created_by,
13
+                name: name
14
+            },
15
+            createdAt: formatISOWithoutTimezone(rest.createdAt),
16
+            updatedAt: formatISOWithoutTimezone(rest.updatedAt)
17
+        };
18
+    }));
19
+};
12 20
 
13
-    if (typeof total !== 'number') {
14
-        return res.status(200).json({
15
-            success: true,
16
-            message,
17
-            data: Array.isArray(formattedData)
18
-        });
19
-    }
21
+// Collection yang async
22
+exports.VendorCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
23
+    const formatted = await transformVendorList(data);
20 24
 
21
-    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
25
+    return ListResponse({ req, res, data: formatted, total, page, limit, message });
22 26
 };

+ 16 - 8
src/resources/admin/vendor/VendorResource.js

@@ -1,17 +1,25 @@
1 1
 const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
2 2
 
3
-const formatItem = (item) => ({
4
-    ...item,
5
-    createdAt: formatISOWithoutTimezone(item.createdAt),
6
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
7
-});
3
+const formatItem = ({ item, userName }) => {
4
+    const { created_by, ...rest } = item;
8 5
 
9
-exports.VendorResource = (res, data, message = 'Success') => {
10
-    const formattedData = formatItem(data);
6
+    return {
7
+        ...rest,
8
+        user: {
9
+            id: created_by,
10
+            name: userName
11
+        },
12
+        createdAt: formatISOWithoutTimezone(item.createdAt),
13
+        updatedAt: formatISOWithoutTimezone(item.updatedAt)
14
+    };
15
+};
16
+
17
+exports.VendorResource = (res, item, userName, message = 'Success') => {
18
+    const formattedData = formatItem({ item, userName });
11 19
 
12 20
     return res.status(200).json({
13 21
         success: true,
14 22
         message,
15 23
         data: formattedData
16 24
     });
17
-};
25
+};

+ 18 - 9
src/resources/sales/area/UserAreaCollection.js

@@ -1,13 +1,22 @@
1
+const { getUserNameById } = require("../../../utils/CheckUserKeycloak");
1 2
 const { ListResponse } = require("../../../utils/ListResponse");
2 3
 
3
-exports.UserAreaCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
4
-    if (typeof total !== 'number') {
5
-        return res.status(200).json({
6
-            success: true,
7
-            message,
8
-            data: Array.isArray(data)
9
-        });
10
-    }
4
+const transformArea = async (data = []) => {
5
+    return Promise.all(data.map(async ({ user_id, ...rest }) => {
6
+        const name = await getUserNameById(user_id);
11 7
 
12
-    return ListResponse({ req, res, data, total, page, limit, message });
8
+        return {
9
+            ...rest,
10
+            user: {
11
+                id: user_id,
12
+                name: name
13
+            },
14
+        };
15
+    }));
16
+};
17
+
18
+exports.UserAreaCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
19
+    const formatted = await transformArea(data);
20
+
21
+    return ListResponse({ req, res, data: formatted, total, page, limit, message });
13 22
 };

+ 20 - 15
src/resources/sales/hospital/HospitalCollection.js

@@ -1,22 +1,27 @@
1 1
 const { ListResponse } = require("../../../utils/ListResponse");
2 2
 const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
+const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
3 4
 
4
-const formatItem = (item) => ({
5
-    ...item,
6
-    createdAt: formatISOWithoutTimezone(item.createdAt),
7
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
8
-});
5
+// Fungsi transform per item
6
+const transformHospitalList = async (data = []) => {
7
+    return Promise.all(data.map(async ({ created_by, ...rest }) => {
8
+        const name = await getUserNameById(created_by);
9 9
 
10
-exports.HospitalCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
11
-    const formattedData = data.map(formatItem);
10
+        return {
11
+            ...rest,
12
+            user: {
13
+                id: created_by,
14
+                name: name
15
+            },
16
+            createdAt: formatISOWithoutTimezone(rest.createdAt),
17
+            updatedAt: formatISOWithoutTimezone(rest.updatedAt)
18
+        };
19
+    }));
20
+};
12 21
 
13
-    if (typeof total !== 'number') {
14
-        return res.status(200).json({
15
-            success: true,
16
-            message,
17
-            data: Array.isArray(formattedData)
18
-        });
19
-    }
22
+// Collection yang async
23
+exports.HospitalCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
24
+    const formatted = await transformHospitalList(data);
20 25
 
21
-    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
26
+    return ListResponse({ req, res, data: formatted, total, page, limit, message });
22 27
 };

+ 16 - 8
src/resources/sales/hospital/HospitalResource.js

@@ -1,17 +1,25 @@
1 1
 const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
2 2
 
3
-const formatItem = (item) => ({
4
-    ...item,
5
-    createdAt: formatISOWithoutTimezone(item.createdAt),
6
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
7
-});
3
+const formatItem = ({ item, userName }) => {
4
+    const { created_by, ...rest } = item;
8 5
 
9
-exports.HospitalResource = (res, data, message = 'Success') => {
10
-    const formattedData = formatItem(data);
6
+    return {
7
+        ...rest,
8
+        user: {
9
+            id: created_by,
10
+            name: userName
11
+        },
12
+        createdAt: formatISOWithoutTimezone(item.createdAt),
13
+        updatedAt: formatISOWithoutTimezone(item.updatedAt)
14
+    };
15
+};
16
+
17
+exports.HospitalResource = (res, item, userName, message = 'Success') => {
18
+    const formattedData = formatItem({ item, userName });
11 19
 
12 20
     return res.status(200).json({
13 21
         success: true,
14 22
         message,
15 23
         data: formattedData
16 24
     });
17
-};
25
+};

+ 27 - 0
src/resources/sales/status_history/StatusHistoryCollection.js

@@ -0,0 +1,27 @@
1
+const { ListResponse } = require("../../../utils/ListResponse");
2
+const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
+const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
4
+
5
+const transformStatusHistoryList = async (data = []) => {
6
+    return Promise.all(data.map(async ({ user_id, ...rest }) => {
7
+        const name = await getUserNameById(user_id);
8
+
9
+        return {
10
+            ...rest,
11
+            user: {
12
+                id: user_id,
13
+                name: name
14
+            },
15
+            note: rest.note?.trim() === '' ? null : rest.note,
16
+            createdAt: formatISOWithoutTimezone(rest.createdAt),
17
+            updatedAt: formatISOWithoutTimezone(rest.updatedAt)
18
+        };
19
+    }));
20
+};
21
+
22
+// Collection yang async
23
+exports.StatusHistoryCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
24
+    const formatted = await transformStatusHistoryList(data);
25
+
26
+    return ListResponse({ req, res, data: formatted, total, page, limit, message });
27
+};

+ 16 - 7
src/routes/admin/CityRoute.js

@@ -1,13 +1,22 @@
1 1
 const express = require('express')
2 2
 const router = express.Router()
3 3
 const cityController = require('../../controllers/admin/CityController.js')
4
-const verifyJWT = require('../../middleware/VerifyJWT.js');
5
-const checkRole = require('../../middleware/CheckRole.js');
4
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+// const checkRole = require('../../middleware/CheckRole.js');
6
+const keycloak = require('../../middleware/Keycloak.js');
7
+const extractToken = require('../../middleware/ExtractToken.js');
8
+const checkRoles = require('../../middleware/CheckRoles.js');
6 9
 
7
-router.get('/', verifyJWT, checkRole(['admin', 'sales']), cityController.getAllCity);
8
-router.post('/', verifyJWT, checkRole(['admin']), cityController.storeCity);
9
-router.get('/:id', verifyJWT, checkRole(['admin', 'sales']), cityController.showCity);
10
-router.patch('/:id', verifyJWT, checkRole(['admin']), cityController.updateCity);
11
-router.delete('/:id', verifyJWT, checkRole(['admin']), cityController.deleteCity);
10
+// router.get('/', verifyJWT, checkRole(['admin', 'sales']), cityController.getAllCity);
11
+// router.post('/', verifyJWT, checkRole(['admin']), cityController.storeCity);
12
+// router.get('/:id', verifyJWT, checkRole(['admin', 'sales']), cityController.showCity);
13
+// router.patch('/:id', verifyJWT, checkRole(['admin']), cityController.updateCity);
14
+// router.delete('/:id', verifyJWT, checkRole(['admin']), cityController.deleteCity);
15
+
16
+router.get('/', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], cityController.getAllCity);
17
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], cityController.storeCity);
18
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], cityController.showCity);
19
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], cityController.updateCity);
20
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], cityController.deleteCity);
12 21
 
13 22
 module.exports = router;

+ 46 - 17
src/routes/admin/HospitalRoute.js

@@ -3,28 +3,57 @@ const router = express.Router()
3 3
 const hospitalController = require('../../controllers/admin/HospitalController.js')
4 4
 const vendorHistoryController = require('../../controllers/admin/VendorHistoryController.js')
5 5
 const executivesHistoryController = require('../../controllers/admin/ExecutivesHistoryController.js')
6
-const verifyJWT = require('../../middleware/VerifyJWT.js');
7
-const checkRole = require('../../middleware/CheckRole.js');
6
+const statusHistoriesController = require('../../controllers/admin/StatusHistoryController.js')
7
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
8
+// const checkRole = require('../../middleware/CheckRole.js');
8 9
 const upload = require('../../middleware/UploadImage.js');
9 10
 
10
-router.get('/', verifyJWT, checkRole(['admin']), hospitalController.getAllHospital);
11
-router.post('/', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.storeHospital);
12
-router.get('/:id', verifyJWT, checkRole(['admin']), hospitalController.showHospital);
13
-router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.updateHospital);
14
-router.delete('/:id', verifyJWT, checkRole(['admin']), hospitalController.deleteHospital);
11
+const keycloak = require('../../middleware/Keycloak.js');
12
+const extractToken = require('../../middleware/ExtractToken.js');
13
+const checkRoles = require('../../middleware/CheckRoles.js');
14
+
15
+// router.get('/', verifyJWT, checkRole(['admin']), hospitalController.getAllHospital);
16
+// router.post('/', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.storeHospital);
17
+// router.get('/:id', verifyJWT, checkRole(['admin']), hospitalController.showHospital);
18
+// router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.updateHospital);
19
+// router.delete('/:id', verifyJWT, checkRole(['admin']), hospitalController.deleteHospital);
20
+
21
+// // Vendor History
22
+// router.get('/:id/vendor-history', verifyJWT, checkRole(['admin']), vendorHistoryController.getAllVendorHistory);
23
+// router.post('/:id/vendor-history', verifyJWT, checkRole(['admin']), vendorHistoryController.storeVendorHistory);
24
+// router.get('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorHistoryController.showVendorHistory);
25
+// router.patch('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorHistoryController.updateVendorHistory);
26
+// router.delete('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorHistoryController.deleteVendorHistory);
27
+
28
+// // Executives History
29
+// router.get('/:id/executives-history', verifyJWT, checkRole(['admin']), executivesHistoryController.getAllExecutivesHistory);
30
+// router.post('/:id/executives-history', verifyJWT, checkRole(['admin']), executivesHistoryController.storeExecutivesHistory);
31
+// router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.showExecutivesHistory);
32
+// router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.updateExecutivesHistory);
33
+// router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.deleteExecutivesHistory);
34
+
35
+router.get('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], hospitalController.getAllHospital);
36
+router.post('/', [keycloak.protect(), extractToken, checkRoles(["admin", "admin"])], upload.single('image'), hospitalController.storeHospital);
37
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], hospitalController.showHospital);
38
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(["admin", "admin"])], upload.single('image'), hospitalController.updateHospital);
39
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], hospitalController.deleteHospital);
15 40
 
16 41
 // Vendor History
17
-router.get('/:id/vendor-history', verifyJWT, checkRole(['admin']), vendorHistoryController.getAllVendorHistory);
18
-router.post('/:id/vendor-history', verifyJWT, checkRole(['admin']), vendorHistoryController.storeVendorHistory);
19
-router.get('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorHistoryController.showVendorHistory);
20
-router.patch('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorHistoryController.updateVendorHistory);
21
-router.delete('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorHistoryController.deleteVendorHistory);
42
+router.get('/:id/vendor-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorHistoryController.getAllVendorHistory);
43
+router.post('/:id/vendor-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorHistoryController.storeVendorHistory);
44
+router.get('/:id/vendor-history/:id_vendor_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorHistoryController.showVendorHistory);
45
+router.patch('/:id/vendor-history/:id_vendor_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorHistoryController.updateVendorHistory);
46
+router.delete('/:id/vendor-history/:id_vendor_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorHistoryController.deleteVendorHistory);
22 47
 
23 48
 // Executives History
24
-router.get('/:id/executives-history', verifyJWT, checkRole(['admin']), executivesHistoryController.getAllExecutivesHistory);
25
-router.post('/:id/executives-history', verifyJWT, checkRole(['admin']), executivesHistoryController.storeExecutivesHistory);
26
-router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.showExecutivesHistory);
27
-router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.updateExecutivesHistory);
28
-router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.deleteExecutivesHistory);
49
+router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.getAllExecutivesHistory);
50
+router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.storeExecutivesHistory);
51
+router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.showExecutivesHistory);
52
+router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.updateExecutivesHistory);
53
+router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.deleteExecutivesHistory);
54
+
55
+// Status History
56
+router.get('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin"])], statusHistoriesController.getAllStatusHistory);
57
+router.post('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin"])], statusHistoriesController.storeStatusHistory);
29 58
 
30 59
 module.exports = router;

+ 12 - 7
src/routes/admin/ProvinceRoute.js

@@ -1,19 +1,24 @@
1 1
 const express = require('express')
2 2
 const router = express.Router()
3 3
 const provinceController = require('../../controllers/admin/ProvinceController.js')
4
-const verifyJWT = require('../../middleware/VerifyJWT.js');
5
-const checkRole = require('../../middleware/CheckRole.js');
4
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+// const checkRole = require('../../middleware/CheckRole.js');
6
+
7
+const keycloak = require('../../middleware/Keycloak.js');
8
+const extractToken = require('../../middleware/ExtractToken.js');
9
+const checkRoles = require('../../middleware/CheckRoles.js');
6 10
 
7 11
 // router.get('/', verifyJWT, checkRole(['admin', 'sales']), provinceController.getAllProvince);
12
+// router.get('/', verifyJWT, checkRole(['admin']), provinceController.getAllProvince);
8 13
 // router.post('/', verifyJWT, checkRole(['admin']), provinceController.storeProvince);
9 14
 // router.get('/:id', verifyJWT, checkRole(['admin']), provinceController.showProvince);
10 15
 // router.patch('/:id', verifyJWT, checkRole(['admin']), provinceController.updateProvince);
11 16
 // router.delete('/:id', verifyJWT, checkRole(['admin']), provinceController.deleteProvince);
12 17
 
13
-router.get('/', verifyJWT, checkRole(['admin']), provinceController.getAllProvince);
14
-router.post('/', verifyJWT, checkRole(['admin']), provinceController.storeProvince);
15
-router.get('/:id', verifyJWT, checkRole(['admin']), provinceController.showProvince);
16
-router.patch('/:id', verifyJWT, checkRole(['admin']), provinceController.updateProvince);
17
-router.delete('/:id', verifyJWT, checkRole(['admin']), provinceController.deleteProvince);
18
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], provinceController.getAllProvince);
19
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], provinceController.storeProvince);
20
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], provinceController.showProvince);
21
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], provinceController.updateProvince);
22
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], provinceController.deleteProvince);
18 23
 
19 24
 module.exports = router;

+ 17 - 7
src/routes/admin/SalesRoute.js

@@ -1,14 +1,24 @@
1 1
 const express = require('express');
2 2
 const salesControllers = require('../../controllers/admin/SalesController.js');
3
-const verifyJWT = require('../../middleware/VerifyJWT.js');
4
-const checkRole = require('../../middleware/CheckRole.js');
3
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
4
+// const checkRole = require('../../middleware/CheckRole.js');
5
+
6
+const keycloak = require('../../middleware/Keycloak.js');
7
+const extractToken = require('../../middleware/ExtractToken.js');
8
+const checkRoles = require('../../middleware/CheckRoles.js');
5 9
 
6 10
 const router = express.Router();
7 11
 
8
-router.post('/', verifyJWT, checkRole(['admin']), salesControllers.storeSales);
9
-router.get('/', verifyJWT, checkRole(['admin']), salesControllers.getAllSales);
10
-router.get('/:id', verifyJWT, checkRole(['admin']), salesControllers.showSales);
11
-router.patch('/:id', verifyJWT, checkRole(['admin']), salesControllers.updateSales);
12
-router.delete('/:id', verifyJWT, checkRole(['admin']), salesControllers.deleteSales);
12
+// router.post('/', verifyJWT, checkRole(['admin']), salesControllers.storeSales);
13
+// router.get('/', verifyJWT, checkRole(['admin']), salesControllers.getAllSales);
14
+// router.get('/:id', verifyJWT, checkRole(['admin']), salesControllers.showSales);
15
+// router.patch('/:id', verifyJWT, checkRole(['admin']), salesControllers.updateSales);
16
+// router.delete('/:id', verifyJWT, checkRole(['admin']), salesControllers.deleteSales);
17
+
18
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], salesControllers.storeSales);
19
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], salesControllers.getAllSales);
20
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], salesControllers.showSales);
21
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], salesControllers.updateSales);
22
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], salesControllers.deleteSales);
13 23
 
14 24
 module.exports = router;

+ 18 - 8
src/routes/admin/VendorRoute.js

@@ -1,13 +1,23 @@
1 1
 const express = require('express')
2 2
 const router = express.Router()
3 3
 const vendorController = require('../../controllers/admin/VendorController.js')
4
-const verifyJWT = require('../../middleware/VerifyJWT.js');
5
-const checkRole = require('../../middleware/CheckRole.js');
6
-
7
-router.get('/', verifyJWT, checkRole(['admin', 'sales']), vendorController.getAllVendor);
8
-router.post('/', verifyJWT, checkRole(['admin', 'sales']), vendorController.storeVendor);
9
-router.get('/:id', verifyJWT, checkRole(['admin', 'sales']), vendorController.showVendor);
10
-router.patch('/:id', verifyJWT, checkRole(['admin', 'sales']), vendorController.updateVendor);
11
-router.delete('/:id', verifyJWT, checkRole(['admin']), vendorController.deleteVendor);
4
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+// const checkRole = require('../../middleware/CheckRole.js');
6
+
7
+const keycloak = require('../../middleware/Keycloak.js');
8
+const extractToken = require('../../middleware/ExtractToken.js');
9
+const checkRoles = require('../../middleware/CheckRoles.js');
10
+
11
+// router.get('/', verifyJWT, checkRole(['admin', 'sales']), vendorController.getAllVendor);
12
+// router.post('/', verifyJWT, checkRole(['admin', 'sales']), vendorController.storeVendor);
13
+// router.get('/:id', verifyJWT, checkRole(['admin', 'sales']), vendorController.showVendor);
14
+// router.patch('/:id', verifyJWT, checkRole(['admin', 'sales']), vendorController.updateVendor);
15
+// router.delete('/:id', verifyJWT, checkRole(['admin']), vendorController.deleteVendor);
16
+
17
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['admin', 'sales'])], vendorController.getAllVendor);
18
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['admin', 'sales'])], vendorController.storeVendor);
19
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['admin', 'sales'])], vendorController.showVendor);
20
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['admin', 'sales'])], vendorController.updateVendor);
21
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], vendorController.deleteVendor);
12 22
 
13 23
 module.exports = router;

+ 11 - 4
src/routes/sales/AreaRoute.js

@@ -1,9 +1,16 @@
1 1
 const express = require('express')
2 2
 const router = express.Router()
3 3
 const areaController = require('../../controllers/sales/AreaController.js')
4
-const verifyJWT = require('../../middleware/VerifyJWT.js');
5
-const checkRole = require('../../middleware/CheckRole.js');
6
-const upload = require('../../middleware/UploadImage.js');
4
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+// const checkRole = require('../../middleware/CheckRole.js');
6
+// const upload = require('../../middleware/UploadImage.js');
7
+
8
+const keycloak = require('../../middleware/Keycloak.js');
9
+const extractToken = require('../../middleware/ExtractToken.js');
10
+const checkRoles = require('../../middleware/CheckRoles.js');
11
+
12
+// router.get('/', verifyJWT, checkRole(['sales']), areaController.getAllAreaByUser);
13
+
14
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], areaController.getAllAreaByUser);
7 15
 
8
-router.get('/', verifyJWT, checkRole(['sales']), areaController.getAllAreaByUser);
9 16
 module.exports = router;

+ 43 - 16
src/routes/sales/HospitalRoute.js

@@ -3,27 +3,54 @@ const router = express.Router()
3 3
 const hospitalController = require('../../controllers/sales/HospitalController.js')
4 4
 const vendorHistoryController = require('../../controllers/sales/VendorHistoryController.js')
5 5
 const executivesHistoryController = require('../../controllers/sales/ExecutivesHistoryController.js')
6
-const verifyJWT = require('../../middleware/VerifyJWT.js');
7
-const checkRole = require('../../middleware/CheckRole.js');
6
+const statusHistoriesController = require('../../controllers/sales/StatusHistoryController.js')
7
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
8
+// const checkRole = require('../../middleware/CheckRole.js');
8 9
 const upload = require('../../middleware/UploadImage.js');
9 10
 
10
-router.get('/', verifyJWT, checkRole(['sales']), hospitalController.getAllHospitalByArea);
11
-router.post('/', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.storeHospital);
12
-router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.updateHospital);
13
-router.get('/:id', verifyJWT, checkRole(['sales']), hospitalController.showHospital);
11
+const keycloak = require('../../middleware/Keycloak.js');
12
+const extractToken = require('../../middleware/ExtractToken.js');
13
+const checkRoles = require('../../middleware/CheckRoles.js');
14
+
15
+// router.get('/', verifyJWT, checkRole(['sales']), hospitalController.getAllHospitalByArea);
16
+// router.post('/', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.storeHospital);
17
+// router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.updateHospital);
18
+// router.get('/:id', verifyJWT, checkRole(['sales']), hospitalController.showHospital);
19
+
20
+// // Vendor History
21
+// router.get('/:id/vendor-history', verifyJWT, checkRole(['sales']), vendorHistoryController.getAllVendorHistory);
22
+// router.post('/:id/vendor-history', verifyJWT, checkRole(['sales']), vendorHistoryController.storeVendorHistory);
23
+// router.get('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.showVendorHistory);
24
+// router.patch('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.updateVendorHistory);
25
+// router.delete('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.deleteVendorHistory);
26
+
27
+// // Executives History
28
+// router.get('/:id/executives-history', verifyJWT, checkRole(['sales']), executivesHistoryController.getAllExecutivesHistory);
29
+// router.post('/:id/executives-history', verifyJWT, checkRole(['sales']), executivesHistoryController.storeExecutivesHistory);
30
+// router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.showExecutivesHistory);
31
+// router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.updateExecutivesHistory);
32
+// router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.deleteExecutivesHistory);
33
+
34
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], hospitalController.getAllHospitalByArea);
35
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], upload.single('image'), hospitalController.storeHospital);
36
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], upload.single('image'), hospitalController.updateHospital);
37
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], hospitalController.showHospital);
14 38
 
15 39
 // Vendor History
16
-router.get('/:id/vendor-history', verifyJWT, checkRole(['sales']), vendorHistoryController.getAllVendorHistory);
17
-router.post('/:id/vendor-history', verifyJWT, checkRole(['sales']), vendorHistoryController.storeVendorHistory);
18
-router.get('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.showVendorHistory);
19
-router.patch('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.updateVendorHistory);
20
-router.delete('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.deleteVendorHistory);
40
+router.get('/:id/vendor-history', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorHistoryController.getAllVendorHistory);
41
+router.post('/:id/vendor-history', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorHistoryController.storeVendorHistory);
42
+router.get('/:id/vendor-history/:id_vendor_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorHistoryController.showVendorHistory);
43
+router.patch('/:id/vendor-history/:id_vendor_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorHistoryController.updateVendorHistory);
44
+router.delete('/:id/vendor-history/:id_vendor_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorHistoryController.deleteVendorHistory);
21 45
 
22 46
 // Executives History
23
-router.get('/:id/executives-history', verifyJWT, checkRole(['sales']), executivesHistoryController.getAllExecutivesHistory);
24
-router.post('/:id/executives-history', verifyJWT, checkRole(['sales']), executivesHistoryController.storeExecutivesHistory);
25
-router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.showExecutivesHistory);
26
-router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.updateExecutivesHistory);
27
-router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.deleteExecutivesHistory);
47
+router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.getAllExecutivesHistory);
48
+router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.storeExecutivesHistory);
49
+router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.showExecutivesHistory);
50
+router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.updateExecutivesHistory);
51
+router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.deleteExecutivesHistory);
52
+
53
+router.get('/:id/status-history', [keycloak.protect(), extractToken, checkRoles(["sales"])], statusHistoriesController.getAllStatusHistory);
54
+router.post('/:id/status-history', [keycloak.protect(), extractToken, checkRoles(["sales"])], statusHistoriesController.storeStatusHistory);
28 55
 
29 56
 module.exports = router;

+ 7 - 3
src/routes/sales/VendorRoute.js

@@ -1,9 +1,13 @@
1 1
 const express = require('express')
2 2
 const router = express.Router()
3 3
 const vendorController = require('../../controllers/sales/VendorController.js')
4
-const verifyJWT = require('../../middleware/VerifyJWT.js');
5
-const checkRole = require('../../middleware/CheckRole.js');
4
+// const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+// const checkRole = require('../../middleware/CheckRole.js');
6 6
 
7
-router.get('/', verifyJWT, checkRole(['sales']), vendorController.getAllVendor);
7
+const keycloak = require('../../middleware/Keycloak.js');
8
+const extractToken = require('../../middleware/ExtractToken.js');
9
+const checkRoles = require('../../middleware/CheckRoles.js');
10
+
11
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorController.getAllVendor);
8 12
 
9 13
 module.exports = router;

+ 79 - 2
src/services/admin/HospitalService.js

@@ -40,7 +40,7 @@ exports.showHospitalService = async (id) => {
40 40
 };
41 41
 
42 42
 exports.storeHospitalService = async (validateData, req) => {
43
-    const creatorId = req.user.id;
43
+    const creatorId = req.tokenData.sub;
44 44
     const province = await ProvinceRepository.findById(validateData.province_id);
45 45
     if (!province) {
46 46
         throw new HttpException('Province not found', 404);
@@ -69,12 +69,45 @@ exports.storeHospitalService = async (validateData, req) => {
69 69
 
70 70
     const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
71 71
 
72
+    let latitude = validateData.latitude ?? null;
73
+    let longitude = validateData.longitude ?? null;
74
+    let gmapsUrl = validateData.gmaps_url ?? null;
75
+
76
+    if (gmapsUrl) {
77
+        if (gmapsUrl.includes("www.google.com/maps")) {
78
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
79
+            const match = gmapsUrl.match(regex);
80
+
81
+            if (match) {
82
+                latitude = parseFloat(match[1]);
83
+                longitude = parseFloat(match[2]);
84
+            } else {
85
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
86
+            }
87
+
88
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
89
+            latitude = null;
90
+            longitude = null;
91
+
92
+        } else {
93
+            // URL disediakan tapi bukan dari domain yang valid
94
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
95
+        }
96
+    } else if (latitude !== null && longitude !== null) {
97
+        gmapsUrl = null;
98
+    } else {
99
+        throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
100
+    }
101
+
72 102
     const payload = {
73 103
         ...validateData,
74 104
         image: imagePath,
75 105
         progress_status: "cari_data",
76 106
         // simrs_type: "-",
77
-        created_by: creatorId
107
+        created_by: creatorId,
108
+        latitude,
109
+        longitude,
110
+        gmaps_url: gmapsUrl,
78 111
     };
79 112
 
80 113
     const data = await HospitalRepository.create(payload);
@@ -131,10 +164,54 @@ exports.updateHospitalService = async (validateData, id, req) => {
131 164
         imagePath = `/storage/img/${req.file.filename}`; // path relatif
132 165
     }
133 166
 
167
+
168
+    // Handle koordinat dan gmaps_url
169
+    let latitude = hospital.latitude;
170
+    let longitude = hospital.longitude;
171
+    let gmapsUrl = hospital.gmaps_url;
172
+
173
+    if (
174
+        validateData.latitude !== undefined &&
175
+        validateData.longitude !== undefined &&
176
+        validateData.latitude !== null &&
177
+        validateData.longitude !== null
178
+    ) {
179
+        // Jika diberikan lat long langsung
180
+        latitude = validateData.latitude;
181
+        longitude = validateData.longitude;
182
+        gmapsUrl = validateData.gmaps_url || gmapsUrl;
183
+    } else if (
184
+        validateData.gmaps_url &&
185
+        typeof validateData.gmaps_url === "string" &&
186
+        validateData.gmaps_url.trim() !== ""
187
+    ) {
188
+        gmapsUrl = validateData.gmaps_url;
189
+
190
+        if (gmapsUrl.includes("www.google.com/maps")) {
191
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
192
+            const match = gmapsUrl.match(regex);
193
+            if (match) {
194
+                latitude = parseFloat(match[1]);
195
+                longitude = parseFloat(match[2]);
196
+            } else {
197
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
198
+            }
199
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
200
+            // Tidak bisa ambil koordinat langsung
201
+            latitude = null;
202
+            longitude = null;
203
+        } else {
204
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
205
+        }
206
+    }
207
+
134 208
     const payload = {
135 209
         ...validateData,
136 210
         image: imagePath,
137 211
         // created_by: req.user.id,
212
+        latitude,
213
+        longitude,
214
+        gmaps_url: gmapsUrl,
138 215
     };
139 216
 
140 217
     const data = await HospitalRepository.update(id, payload);

+ 76 - 0
src/services/admin/StatusHistoryService.js

@@ -0,0 +1,76 @@
1
+const HttpException = require('../../utils/HttpException.js');
2
+const prisma = require('../../prisma/PrismaClient.js');
3
+const { createLog } = require('../../utils/LogActivity.js');
4
+const StatusHistoryRepository = require('../../repository/admin/StatusHistoryRepository.js');
5
+
6
+exports.getAllStatusHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
7
+    const skip = (page - 1) * limit;
8
+
9
+    const hospitalId = req.params.id;
10
+    const hospital = await prisma.hospital.findFirst({
11
+        where: {
12
+            id: hospitalId
13
+        }
14
+    })
15
+    if (!hospital) {
16
+        throw new HttpException("Hospital not found", 404)
17
+    }
18
+
19
+    const where = {
20
+        hospital_id: req.params.id,
21
+        deletedAt: null
22
+    };
23
+
24
+    const [status_histories, total] = await Promise.all([
25
+        StatusHistoryRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
26
+        StatusHistoryRepository.countAll(where)
27
+    ]);
28
+
29
+    return { status_histories, total };
30
+};
31
+
32
+const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
33
+
34
+exports.storeStatusHistoryService = async (validateData, req) => {
35
+    const hospitalId = req.params.id;
36
+    const userId = req.tokenData.sub;
37
+
38
+    const hospital = await prisma.hospital.findFirst({
39
+        where: {
40
+            id: hospitalId
41
+        }
42
+    })
43
+    if (!hospital) {
44
+        throw new HttpException("Hospital not found", 404)
45
+    }
46
+
47
+    if (validateData.new_status && !validProgressStatuses.includes(validateData.new_status)) {
48
+        throw new HttpException(
49
+            `Invalid new_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
50
+            422
51
+        );
52
+    }
53
+
54
+    if (validateData.new_status === hospital.progress_status) {
55
+        throw new HttpException("New status change is the same as old status", 400)
56
+    }
57
+
58
+    const payload = {
59
+        hospital_id: hospitalId,
60
+        user_id: userId,
61
+        old_status: hospital.progress_status,
62
+        new_status: validateData.new_status,
63
+        note: validateData.note,
64
+    };
65
+
66
+    const data = await StatusHistoryRepository.create(payload);
67
+
68
+    await prisma.hospital.update({
69
+        where: { id: hospitalId },
70
+        data: {
71
+            progress_status: validateData.new_status
72
+        }
73
+    });
74
+
75
+    await createLog(req, data);
76
+};

+ 66 - 6
src/services/admin/VendorHistoryService.js

@@ -61,11 +61,21 @@ exports.storeVendorHistoryService = async (validateData, req) => {
61 61
         where: {
62 62
             id: hospitalId
63 63
         }
64
-    })
64
+    });
65
+
65 66
     if (!hospital) {
66 67
         throw new HttpException("Hospital not found", 404)
67 68
     }
68 69
 
70
+    const vendor = await prisma.vendor.findFirst({
71
+        where: {
72
+            id: validateData.vendor_id
73
+        }
74
+    });
75
+    if (!vendor) {
76
+        throw new HttpException("Vendor not found", 404)
77
+    }
78
+
69 79
     await prisma.hospital.update({
70 80
         where: { id: hospitalId },
71 81
         data: {
@@ -73,6 +83,31 @@ exports.storeVendorHistoryService = async (validateData, req) => {
73 83
         }
74 84
     });
75 85
 
86
+    if (validateData.simrs_type) {
87
+        const existingActiveVendors = await prisma.vendorHistory.findMany({
88
+            where: {
89
+                hospital_id: hospitalId,
90
+                status: "active",
91
+                deletedAt: null
92
+            }
93
+        });
94
+
95
+        if (existingActiveVendors.length > 0) {
96
+            await prisma.vendorHistory.updateMany({
97
+                where: {
98
+                    hospital_id: hospitalId,
99
+                    status: "active",
100
+                    deletedAt: null
101
+                },
102
+                data: {
103
+                    status: "inactive"
104
+                }
105
+            });
106
+        }
107
+
108
+        validateData.status = "active";
109
+    }
110
+
76 111
     const payload = {
77 112
         vendor_id: validateData.vendor_id,
78 113
         vendor_impression: validateData.vendor_impression,
@@ -99,11 +134,22 @@ exports.updateVendorHistoryService = async (validateData, req) => {
99 134
         throw new HttpException("Hospital not found", 404)
100 135
     }
101 136
 
102
-    const vendor = await VendorHistoryRepository.findById(id_vendor_history);
103
-    if (!vendor) {
137
+    const vendorHistory = await VendorHistoryRepository.findById(id_vendor_history);
138
+    if (!vendorHistory) {
104 139
         throw new HttpException("Vendor history not found", 404);
105 140
     }
106 141
 
142
+    if (validateData.vendor_id) {
143
+        const existVendor = await prisma.vendor.findFirst({
144
+            where: {
145
+                id: validateData.vendor_id
146
+            }
147
+        });
148
+        if (!existVendor) {
149
+            throw new HttpException("Vendor not found", 404)
150
+        }
151
+    }
152
+
107 153
     if (validateData.simrs_type) {
108 154
         await prisma.hospital.update({
109 155
             where: { id: id_hospital },
@@ -118,13 +164,27 @@ exports.updateVendorHistoryService = async (validateData, req) => {
118 164
             throw new HttpException("Contract expired date must be after contract date", 400)
119 165
         }
120 166
     }
121
-    
167
+
168
+    if (validateData.status === "active") {
169
+        await prisma.vendorHistory.updateMany({
170
+            where: {
171
+                hospital_id: id_hospital,
172
+                status: "active",
173
+                deletedAt: null,
174
+                NOT: { id: id_vendor_history },
175
+            },
176
+            data: {
177
+                status: "inactive",
178
+            },
179
+        });
180
+    }
181
+
122 182
     const payload = {
123 183
         vendor_id: validateData.vendor_id,
124 184
         vendor_impression: validateData.vendor_impression,
125 185
         status: validateData.status,
126
-        contract_date: validateData.contract_date ? new Date(validateData.contract_date) : vendor.contract_date,
127
-        contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendor.contract_expired_date,
186
+        contract_date: validateData.contract_date ? new Date(validateData.contract_date) : vendorHistory.contract_date,
187
+        contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendorHistory.contract_expired_date,
128 188
     };
129 189
 
130 190
     const data = await VendorHistoryRepository.update(id_vendor_history, payload);

+ 7 - 4
src/services/admin/VendorService.js

@@ -5,6 +5,7 @@ const { SearchFilter } = require('../../utils/SearchFilter.js');
5 5
 const timeLocal = require('../../utils/TimeLocal.js');
6 6
 const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
7 7
 const { formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
8
+const { getUserNameById } = require('../../utils/CheckUserKeycloak.js');
8 9
 
9 10
 exports.getAllVendorService = async ({ page, limit, search, sortBy, orderBy }) => {
10 11
     const skip = (page - 1) * limit;
@@ -28,11 +29,13 @@ exports.showVendorService = async (id) => {
28 29
         throw new HttpException("Data vendor not found", 404);
29 30
     }
30 31
 
31
-    return vendor;
32
+    const userName = await getUserNameById(vendor.created_by);
33
+
34
+    return { vendor, userName };
32 35
 };
33 36
 
34 37
 exports.storeVendorService = async (validateData, req) => {
35
-    const creatorId = req.user.id;
38
+    const creatorId = req.tokenData.sub;
36 39
 
37 40
     const name_vendor = await prisma.vendor.findFirst({
38 41
         where: {
@@ -55,7 +58,7 @@ exports.storeVendorService = async (validateData, req) => {
55 58
 };
56 59
 
57 60
 exports.updateVendorService = async (validateData, id, req) => {
58
-    const creatorId = req.user.id;
61
+    // const creatorId = req.user.id;
59 62
 
60 63
     const vendor = await VendorRepository.findById(id);
61 64
     if (!vendor) {
@@ -77,7 +80,7 @@ exports.updateVendorService = async (validateData, id, req) => {
77 80
 
78 81
     const payload = {
79 82
         ...validateData,
80
-        created_by: creatorId
83
+        // created_by: creatorId
81 84
     };
82 85
 
83 86
     const data = await VendorRepository.update(id, payload);

+ 1 - 1
src/services/sales/AreaService.js

@@ -5,7 +5,7 @@ exports.getAllAreaByUserService = async ({ page, limit, search, sortBy, orderBy
5 5
     const skip = (page - 1) * limit;
6 6
 
7 7
     const where = {
8
-        user_id: req.user.id,
8
+        user_id: req.tokenData.sub,
9 9
         deletedAt: null,
10 10
         ...SearchFilter(search, ['province.name']),
11 11
     };

+ 85 - 6
src/services/sales/HospitalService.js

@@ -7,12 +7,13 @@ const CityRepository = require('../../repository/admin/CityRepository.js');
7 7
 const HttpException = require('../../utils/HttpException.js');
8 8
 const HospitalRepository = require('../../repository/admin/HospitalRepository.js');
9 9
 const { formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
10
+const { getUserNameById } = require('../../utils/CheckUserKeycloak.js');
10 11
 
11 12
 exports.getAllHospitalByAreaService = async ({ page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status }, req) => {
12 13
     const skip = (page - 1) * limit;
13 14
 
14 15
     const userAreas = await prisma.userArea.findMany({
15
-        where: { user_id: req.user.id },
16
+        where: { user_id: req.tokenData.sub },
16 17
         select: { province_id: true },
17 18
     });
18 19
 
@@ -93,7 +94,7 @@ exports.getAllHospitalByAreaService = async ({ page, limit, search, sortBy, orde
93 94
 // };
94 95
 
95 96
 exports.storeHospitalService = async (validateData, req) => {
96
-    const creatorId = req.user.id;
97
+    const creatorId = req.tokenData.sub;
97 98
 
98 99
     const province = await ProvinceRepository.findById(validateData.province_id);
99 100
     if (!province) {
@@ -102,7 +103,7 @@ exports.storeHospitalService = async (validateData, req) => {
102 103
 
103 104
     const userArea = await prisma.userArea.findFirst({
104 105
         where: {
105
-            user_id: req.user.id,
106
+            user_id: req.tokenData.sub,
106 107
             province_id: validateData.province_id
107 108
         }
108 109
     });
@@ -134,12 +135,45 @@ exports.storeHospitalService = async (validateData, req) => {
134 135
 
135 136
     const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
136 137
 
138
+    let latitude = validateData.latitude ?? null;
139
+    let longitude = validateData.longitude ?? null;
140
+    let gmapsUrl = validateData.gmaps_url ?? null;
141
+
142
+    if (gmapsUrl) {
143
+        if (gmapsUrl.includes("www.google.com/maps")) {
144
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
145
+            const match = gmapsUrl.match(regex);
146
+
147
+            if (match) {
148
+                latitude = parseFloat(match[1]);
149
+                longitude = parseFloat(match[2]);
150
+            } else {
151
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
152
+            }
153
+
154
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
155
+            latitude = null;
156
+            longitude = null;
157
+
158
+        } else {
159
+            // URL disediakan tapi bukan dari domain yang valid
160
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
161
+        }
162
+    } else if (latitude !== null && longitude !== null) {
163
+        gmapsUrl = null;
164
+    } else {
165
+        throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
166
+    }
167
+
137 168
     const payload = {
138 169
         ...validateData,
139 170
         image: imagePath,
140 171
         progress_status: "cari_data",
141 172
         simrs_type: "-",
142
-        created_by: creatorId
173
+        created_by: creatorId,
174
+        latitude,
175
+        longitude,
176
+        gmaps_url: gmapsUrl,
143 177
     };
144 178
 
145 179
     const data = await salesHospitalRepository.create(payload);
@@ -156,7 +190,7 @@ exports.updateHospitalService = async (validateData, id, req) => {
156 190
 
157 191
     const userArea = await prisma.userArea.findFirst({
158 192
         where: {
159
-            user_id: req.user.id,
193
+            user_id: req.tokenData.sub,
160 194
             province_id: validateData.province_id
161 195
         }
162 196
     });
@@ -207,10 +241,53 @@ exports.updateHospitalService = async (validateData, id, req) => {
207 241
         imagePath = `/storage/img/${req.file.filename}`; // path relatif
208 242
     }
209 243
 
244
+    // Handle koordinat dan gmaps_url
245
+    let latitude = hospital.latitude;
246
+    let longitude = hospital.longitude;
247
+    let gmapsUrl = hospital.gmaps_url;
248
+
249
+    if (
250
+        validateData.latitude !== undefined &&
251
+        validateData.longitude !== undefined &&
252
+        validateData.latitude !== null &&
253
+        validateData.longitude !== null
254
+    ) {
255
+        // Jika diberikan lat long langsung
256
+        latitude = validateData.latitude;
257
+        longitude = validateData.longitude;
258
+        gmapsUrl = validateData.gmaps_url || gmapsUrl;
259
+    } else if (
260
+        validateData.gmaps_url &&
261
+        typeof validateData.gmaps_url === "string" &&
262
+        validateData.gmaps_url.trim() !== ""
263
+    ) {
264
+        gmapsUrl = validateData.gmaps_url;
265
+
266
+        if (gmapsUrl.includes("www.google.com/maps")) {
267
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
268
+            const match = gmapsUrl.match(regex);
269
+            if (match) {
270
+                latitude = parseFloat(match[1]);
271
+                longitude = parseFloat(match[2]);
272
+            } else {
273
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
274
+            }
275
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
276
+            // Tidak bisa ambil koordinat langsung
277
+            latitude = null;
278
+            longitude = null;
279
+        } else {
280
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
281
+        }
282
+    }
283
+
210 284
     const payload = {
211 285
         ...validateData,
212 286
         image: imagePath,
213 287
         // created_by: req.user.id,
288
+        latitude,
289
+        longitude,
290
+        gmaps_url: gmapsUrl,
214 291
     };
215 292
 
216 293
     const data = await salesHospitalRepository.update(id, payload);
@@ -223,5 +300,7 @@ exports.showHospitalService = async (id) => {
223 300
         throw new HttpException("Data hospital not found", 404);
224 301
     }
225 302
 
226
-    return hospital;
303
+    const userName = await getUserNameById(hospital.created_by);
304
+
305
+    return { hospital, userName };
227 306
 };

+ 99 - 0
src/services/sales/StatusHistoryService.js

@@ -0,0 +1,99 @@
1
+const HttpException = require('../../utils/HttpException.js');
2
+const prisma = require('../../prisma/PrismaClient.js');
3
+const { createLog } = require('../../utils/LogActivity.js');
4
+const StatusHistoryRepository = require('../../repository/sales/StatusHistoryRepository.js');
5
+
6
+exports.getAllStatusHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
7
+    const skip = (page - 1) * limit;
8
+
9
+    const userId = req.tokenData.sub;
10
+    const hospitalId = req.params.id;
11
+    const hospital = await prisma.hospital.findFirst({
12
+        where: {
13
+            id: hospitalId
14
+        }
15
+    })
16
+    if (!hospital) {
17
+        throw new HttpException("Hospital not found", 404)
18
+    }
19
+
20
+    const userAreas = await prisma.userArea.findMany({
21
+        where: { user_id: userId },
22
+        select: { province_id: true }
23
+    });
24
+
25
+    const userProvinceIds = userAreas.map(ua => ua.province_id);
26
+
27
+    if (!userProvinceIds.includes(hospital.province_id)) {
28
+        throw new HttpException("This hospital is not your area", 403);
29
+    }
30
+
31
+    const where = {
32
+        hospital_id: req.params.id,
33
+        deletedAt: null
34
+    };
35
+
36
+    const [status_histories, total] = await Promise.all([
37
+        StatusHistoryRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
38
+        StatusHistoryRepository.countAll(where)
39
+    ]);
40
+
41
+    return { status_histories, total };
42
+};
43
+
44
+const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
45
+
46
+exports.storeStatusHistoryService = async (validateData, req) => {
47
+    const hospitalId = req.params.id;
48
+    const userId = req.tokenData.sub;
49
+
50
+    const hospital = await prisma.hospital.findFirst({
51
+        where: {
52
+            id: hospitalId
53
+        }
54
+    })
55
+    if (!hospital) {
56
+        throw new HttpException("Hospital not found", 404)
57
+    }
58
+
59
+    const userAreas = await prisma.userArea.findMany({
60
+        where: { user_id: userId },
61
+        select: { province_id: true }
62
+    });
63
+
64
+    const userProvinceIds = userAreas.map(ua => ua.province_id);
65
+
66
+    if (!userProvinceIds.includes(hospital.province_id)) {
67
+        throw new HttpException("This hospital is not your area", 403);
68
+    }
69
+
70
+    if (validateData.new_status && !validProgressStatuses.includes(validateData.new_status)) {
71
+        throw new HttpException(
72
+            `Invalid new_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
73
+            422
74
+        );
75
+    }
76
+
77
+    if (validateData.new_status === hospital.progress_status) {
78
+        throw new HttpException("New status change is the same as old status", 400)
79
+    }
80
+
81
+    const payload = {
82
+        hospital_id: hospitalId,
83
+        user_id: userId,
84
+        old_status: hospital.progress_status,
85
+        new_status: validateData.new_status,
86
+        note: validateData.note,
87
+    };
88
+
89
+    const data = await StatusHistoryRepository.create(payload);
90
+
91
+    await prisma.hospital.update({
92
+        where: { id: hospitalId },
93
+        data: {
94
+            progress_status: validateData.new_status
95
+        }
96
+    });
97
+
98
+    await createLog(req, data);
99
+};

+ 64 - 5
src/services/sales/VendorHistoryService.js

@@ -61,11 +61,20 @@ exports.storeVendorHistoryService = async (validateData, req) => {
61 61
         where: {
62 62
             id: hospitalId
63 63
         }
64
-    })
64
+    });
65 65
     if (!hospital) {
66 66
         throw new HttpException("Hospital not found", 404)
67 67
     }
68 68
 
69
+    const vendor = await prisma.vendor.findFirst({
70
+        where: {
71
+            id: validateData.vendor_id
72
+        }
73
+    });
74
+    if (!vendor) {
75
+        throw new HttpException("Vendor not found", 404)
76
+    }
77
+
69 78
     await prisma.hospital.update({
70 79
         where: { id: hospitalId },
71 80
         data: {
@@ -73,6 +82,31 @@ exports.storeVendorHistoryService = async (validateData, req) => {
73 82
         }
74 83
     });
75 84
 
85
+    if (validateData.simrs_type) {
86
+        const existingActiveVendors = await prisma.vendorHistory.findMany({
87
+            where: {
88
+                hospital_id: hospitalId,
89
+                status: "active",
90
+                deletedAt: null
91
+            }
92
+        });
93
+
94
+        if (existingActiveVendors.length > 0) {
95
+            await prisma.vendorHistory.updateMany({
96
+                where: {
97
+                    hospital_id: hospitalId,
98
+                    status: "active",
99
+                    deletedAt: null
100
+                },
101
+                data: {
102
+                    status: "inactive"
103
+                }
104
+            });
105
+        }
106
+
107
+        validateData.status = "active";
108
+    }
109
+
76 110
     const payload = {
77 111
         vendor_id: validateData.vendor_id,
78 112
         vendor_impression: validateData.vendor_impression,
@@ -99,11 +133,22 @@ exports.updateVendorHistoryService = async (validateData, req) => {
99 133
         throw new HttpException("Hospital not found", 404)
100 134
     }
101 135
 
102
-    const vendor = await VendorHistoryRepository.findById(id_vendor_history);
103
-    if (!vendor) {
136
+    const vendorHistory = await VendorHistoryRepository.findById(id_vendor_history);
137
+    if (!vendorHistory) {
104 138
         throw new HttpException("Vendor history not found", 404);
105 139
     }
106 140
 
141
+    if (validateData.vendor_id) {
142
+        const existVendor = await prisma.vendor.findFirst({
143
+            where: {
144
+                id: validateData.vendor_id
145
+            }
146
+        });
147
+        if (!existVendor) {
148
+            throw new HttpException("Vendor not found", 404)
149
+        }
150
+    }
151
+
107 152
     await prisma.hospital.update({
108 153
         where: { id: id_hospital },
109 154
         data: {
@@ -117,12 +162,26 @@ exports.updateVendorHistoryService = async (validateData, req) => {
117 162
         }
118 163
     }
119 164
 
165
+    if (validateData.status === "active") {
166
+        await prisma.vendorHistory.updateMany({
167
+            where: {
168
+                hospital_id: id_hospital,
169
+                status: "active",
170
+                deletedAt: null,
171
+                NOT: { id: id_vendor_history },
172
+            },
173
+            data: {
174
+                status: "inactive",
175
+            },
176
+        });
177
+    }
178
+
120 179
     const payload = {
121 180
         vendor_id: validateData.vendor_id,
122 181
         vendor_impression: validateData.vendor_impression,
123 182
         status: validateData.status,
124
-        contract_date: validateData.contract_date ? new Date(validateData.contract_date) : vendor.contract_date,
125
-        contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendor.contract_expired_date,
183
+        contract_date: validateData.contract_date ? new Date(validateData.contract_date) : vendorHistory.contract_date,
184
+        contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendorHistory.contract_expired_date,
126 185
     };
127 186
 
128 187
     const data = await VendorHistoryRepository.update(id_vendor_history, payload);

+ 1 - 1
src/services/sales/VendorService.js

@@ -5,7 +5,7 @@ const { SearchFilter } = require('../../utils/SearchFilter.js');
5 5
 
6 6
 exports.getAllVendorService = async ({ page, limit, search, sortBy, orderBy }, req) => {
7 7
     const skip = (page - 1) * limit;
8
-    const userId = req.user.id;
8
+    const userId = req.tokenData.sub;
9 9
 
10 10
     // 1. Ambil provinsi yang menjadi area kerja sales
11 11
     const userAreas = await prisma.userArea.findMany({

+ 47 - 0
src/utils/CheckUserKeycloak.js

@@ -0,0 +1,47 @@
1
+const axios = require('axios');
2
+require('dotenv').config();
3
+
4
+const getKeycloakAdminToken = async () => {
5
+    const response = await axios.post(`${process.env.KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`, new URLSearchParams({
6
+        client_id: 'admin-cli',
7
+        username: process.env.KEYCLOAK_ADMIN_USERNAME,
8
+        password: process.env.KEYCLOAK_ADMIN_PASSWORD,
9
+        grant_type: 'password'
10
+    }), {
11
+        headers: {
12
+            'Content-Type': 'application/x-www-form-urlencoded'
13
+        }
14
+    });
15
+
16
+    return response.data.access_token;
17
+};
18
+
19
+const getUserInfoById = async (userId) => {
20
+    try {
21
+        const token = await getKeycloakAdminToken();
22
+
23
+        const response = await axios.get(`${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`, {
24
+            headers: {
25
+                Authorization: `Bearer ${token}`
26
+            }
27
+        });
28
+
29
+        return response.data;
30
+    } catch (error) {
31
+        console.error("Failed to get user info from Keycloak:", error.message);
32
+        return null;
33
+    }
34
+};
35
+
36
+const getUserNameById = async (userId) => {
37
+    const userInfo = await getUserInfoById(userId);
38
+    if (!userInfo) return null;
39
+
40
+    const firstName = userInfo.firstName || '';
41
+    const lastName = userInfo.lastName || '';
42
+    const fullName = `${firstName} ${lastName}`.trim();
43
+
44
+    return fullName || null;
45
+};
46
+
47
+module.exports = { getUserNameById };

+ 66 - 29
src/utils/LogActivity.js

@@ -1,43 +1,80 @@
1 1
 const jwt = require('jsonwebtoken');
2 2
 const prisma = require('../prisma/PrismaClient.js');
3
-const timeLocal = require('../utils/TimeLocal.js')
3
+const timeLocal = require('../utils/TimeLocal.js');
4
+const { getUserNameById } = require('./CheckUserKeycloak.js');
4 5
 
5
-const baseLog = async ({ req, action, data }) => {
6
-    // try {
7
-    //     const authHeader = req.headers.authorization;
8
-    //     if (!authHeader || !authHeader.startsWith('Bearer ')) return;
6
+// const baseLog = async ({ req, action, data }) => {
7
+// 1
8
+// try {
9
+//     const authHeader = req.headers.authorization;
10
+//     if (!authHeader || !authHeader.startsWith('Bearer ')) return;
11
+
12
+//     const token = authHeader.split(' ')[1];
13
+//     const decoded = jwt.decode(token);
14
+//     if (!decoded?.id || !decoded?.username) return;
15
+
16
+//     await prisma.activityLog.create({
17
+//         data: {
18
+//             user_id: decoded.id,
19
+//             username: decoded.username,
20
+//             action: JSON.stringify({ [action]: data }),
21
+//             createdAt: timeLocal.now().toDate(),
22
+//             updatedAt: timeLocal.now().toDate(),
23
+//             deletedAt: null,
24
+//         }
25
+//     });
26
+// } catch (err) {
27
+//     console.error('Failed to log activity:', err.message);
28
+// }
29
+
30
+// 2
31
+// try {
32
+//     let userId, username;
9 33
 
10
-    //     const token = authHeader.split(' ')[1];
11
-    //     const decoded = jwt.decode(token);
12
-    //     if (!decoded?.id || !decoded?.username) return;
34
+//     // Coba ambil dari Bearer token jika ada
35
+//     const authHeader = req?.headers?.authorization;
36
+//     if (authHeader?.startsWith('Bearer ')) {
37
+//         const token = authHeader.split(' ')[1];
38
+//         const decoded = jwt.decode(token);
39
+//         userId = decoded?.id;
40
+//         username = decoded?.username;
41
+//     }
13 42
 
14
-    //     await prisma.activityLog.create({
15
-    //         data: {
16
-    //             user_id: decoded.id,
17
-    //             username: decoded.username,
18
-    //             action: JSON.stringify({ [action]: data }),
19
-    //             createdAt: timeLocal.now().toDate(),
20
-    //             updatedAt: timeLocal.now().toDate(),
21
-    //             deletedAt: null,
22
-    //         }
23
-    //     });
24
-    // } catch (err) {
25
-    //     console.error('Failed to log activity:', err.message);
26
-    // }
43
+//     // Fallback ke data langsung jika belum dapat dari token
44
+//     if (!userId || !username) {
45
+//         userId = data?.id;
46
+//         username = data?.username;
47
+//     }
27 48
 
49
+//     if (!userId || !username) return;
50
+
51
+//     await prisma.activityLog.create({
52
+//         data: {
53
+//             user_id: userId,
54
+//             username,
55
+//             action: JSON.stringify({ [action]: data }),
56
+//             createdAt: timeLocal.now().toDate(),
57
+//             updatedAt: timeLocal.now().toDate(),
58
+//             deletedAt: null,
59
+//         }
60
+//     });
61
+// } catch (err) {
62
+//     console.error('Failed to log activity:', err.message);
63
+// }
64
+
65
+// };
66
+// 3
67
+const baseLog = async ({ req, action, data }) => {
28 68
     try {
29 69
         let userId, username;
30 70
 
31
-        // Coba ambil dari Bearer token jika ada
32
-        const authHeader = req?.headers?.authorization;
33
-        if (authHeader?.startsWith('Bearer ')) {
34
-            const token = authHeader.split(' ')[1];
35
-            const decoded = jwt.decode(token);
36
-            userId = decoded?.id;
37
-            username = decoded?.username;
71
+        // Ambil userId dari token Keycloak
72
+        if (req?.tokenData?.sub) {
73
+            userId = req.tokenData.sub;
74
+            username = await getUserNameById(userId);
38 75
         }
39 76
 
40
-        // Fallback ke data langsung jika belum dapat dari token
77
+        // Fallback kalau tokenData tidak ada
41 78
         if (!userId || !username) {
42 79
             userId = data?.id;
43 80
             username = data?.username;

+ 10 - 3
src/validators/admin/hospital/HospitalValidators.js

@@ -1,7 +1,7 @@
1 1
 const HttpException = require('../../../utils/HttpException.js');
2 2
 
3 3
 exports.validateStoreHospitalRequest = (body) => {
4
-    const { name, hospital_code, type, ownership, province_id, city_id, address, contact, note } = body;
4
+    const { name, hospital_code, type, ownership, province_id, city_id, address, contact, note, gmaps_url, latitude, longitude } = body;
5 5
 
6 6
     const errors = {};
7 7
 
@@ -60,11 +60,14 @@ exports.validateStoreHospitalRequest = (body) => {
60 60
         // simrs_type: simrs_type.trim(),
61 61
         contact: contact.trim(),
62 62
         note: note.trim(),
63
+        gmaps_url: gmaps_url ? gmaps_url.trim() : null,
64
+        latitude: latitude !== undefined && latitude !== null ? parseFloat(latitude) : null,
65
+        longitude: longitude !== undefined && longitude !== null ? parseFloat(longitude) : null,
63 66
     };
64 67
 };
65 68
 
66 69
 exports.validateUpdateHospitalRequest = (body) => {
67
-    const { name, hospital_code, type, ownership, province_id, city_id, address, progress_status, contact, note } = body;
70
+    const { name, hospital_code, type, ownership, province_id, city_id, address, contact, note, gmaps_url, latitude, longitude } = body;
68 71
 
69 72
     return {
70 73
         name,
@@ -74,8 +77,12 @@ exports.validateUpdateHospitalRequest = (body) => {
74 77
         province_id,
75 78
         city_id,
76 79
         address,
77
-        progress_status,
80
+        // simrs_type: simrs_type.trim(),
81
+        // progress_status: progress_status.trim(),
78 82
         contact,
79 83
         note,
84
+        gmaps_url: gmaps_url ? gmaps_url.trim() : null,
85
+        latitude: latitude !== undefined && latitude !== null ? parseFloat(latitude) : null,
86
+        longitude: longitude !== undefined && longitude !== null ? parseFloat(longitude) : null,
80 87
     };
81 88
 };

+ 19 - 0
src/validators/admin/status_history/StatusHistoryValidators.js

@@ -0,0 +1,19 @@
1
+const HttpException = require('../../../utils/HttpException.js');
2
+
3
+exports.validateCreateStatusHisotryRequest = (body) => {
4
+    const { new_status, note } = body;
5
+    const errors = {};
6
+
7
+    if (!new_status || new_status.trim() === '') {
8
+        errors.new_status = ['new_status is required'];
9
+    }
10
+
11
+    if (Object.keys(errors).length > 0) {
12
+        throw new HttpException(errors, 422);
13
+    }
14
+
15
+    return {
16
+        new_status: new_status.trim(),
17
+        note: note.trim()
18
+    };
19
+};

+ 9 - 0
src/validators/admin/vendor_history/VendorHistoriValidators.js

@@ -75,6 +75,15 @@ exports.validateStoreVendorHistoryRequest = (body) => {
75 75
 exports.validateUpdateVendorHistoryRequest = (body) => {
76 76
     const { simrs_type, vendor_id, vendor_impression, status, contract_date, contract_expired_date } = body;
77 77
 
78
+    if (simrs_type !== undefined && simrs_type !== null && simrs_type.trim() !== '') {
79
+        const allowedTypes = ['vendor', 'gratis', 'in house'];
80
+        const simrsTypeValue = simrs_type.trim().toLowerCase();
81
+
82
+        if (!allowedTypes.includes(simrsTypeValue)) {
83
+            throw new HttpException('Simrs type must be either "vendor", "gratis", or "in house"', 422);
84
+        }
85
+    }
86
+
78 87
     return {
79 88
         simrs_type,
80 89
         vendor_id,

+ 9 - 0
src/validators/sales/vendor_history/VendorHistoriValidators.js

@@ -75,6 +75,15 @@ exports.validateStoreVendorHistoryRequest = (body) => {
75 75
 exports.validateUpdateVendorHistoryRequest = (body) => {
76 76
     const { simrs_type, vendor_id, vendor_impression, status, contract_date, contract_expired_date } = body;
77 77
 
78
+    if (simrs_type !== undefined && simrs_type !== null && simrs_type.trim() !== '') {
79
+        const allowedTypes = ['vendor', 'gratis', 'in house'];
80
+        const simrsTypeValue = simrs_type.trim().toLowerCase();
81
+
82
+        if (!allowedTypes.includes(simrsTypeValue)) {
83
+            throw new HttpException('Simrs type must be either "vendor", "gratis", or "in house"', 422);
84
+        }
85
+    }
86
+
78 87
     return {
79 88
         simrs_type,
80 89
         vendor_id,