Browse Source

add fitur epic 7

pearlgw 1 month ago
parent
commit
2fb1050fcf
69 changed files with 2033 additions and 2598 deletions
  1. 2 0
      index.ts
  2. 28 0
      prisma/migrations/20250802022241_add_table_epic_7/migration.sql
  3. 9 0
      prisma/migrations/20250802023321_update_field_in_table_category/migration.sql
  4. 9 0
      prisma/migrations/20250802040342_update_field_in_table_category_link/migration.sql
  5. 11 0
      prisma/migrations/20250802085021_update_table_category_link/migration.sql
  6. 11 0
      prisma/migrations/20250804032035_add_category_id_in_category_link/migration.sql
  7. 43 18
      prisma/schema.prisma
  8. 2 2
      prisma/seeders/HospitalSeeder.ts
  9. 2 2
      prisma/seeders/UserAreaSeeder.ts
  10. 1 1
      prisma/seeders/VendorSeeder.ts
  11. 92 0
      src/controllers/admin/CategoryController.ts
  12. 1 64
      src/controllers/admin/HospitalController.ts
  13. 1 2
      src/controllers/admin/VendorController.ts
  14. 2 2
      src/controllers/admin/VendorExperienceController.ts
  15. 2 4
      src/controllers/sales/HospitalController.ts
  16. 32 0
      src/repository/admin/CategoryLinkRepository.ts
  17. 151 0
      src/repository/admin/CategoryRepository.ts
  18. 24 0
      src/resources/admin/category/CategoryCollection.ts
  19. 37 0
      src/resources/admin/category/CategoryResource.ts
  20. 92 0
      src/resources/admin/category/CategoryUseCollection.ts
  21. 63 0
      src/resources/admin/category/TransformCategoryLinkUse.ts
  22. 42 99
      src/resources/admin/hospital/HospitalCollection.ts
  23. 25 50
      src/resources/admin/hospital/HospitalResource.ts
  24. 0 22
      src/resources/admin/sales/SalesCollection.js
  25. 0 17
      src/resources/admin/sales/SalesResource.js
  26. 42 108
      src/resources/admin/status_history/StatusHistoryCollection.ts
  27. 64 82
      src/resources/admin/vendor/VendorCollection.ts
  28. 30 74
      src/resources/admin/vendor/VendorResource.ts
  29. 53 67
      src/resources/admin/vendor_experience/VendorExperienceCollection.ts
  30. 24 73
      src/resources/admin/vendor_experience/VendorExperienceResource.ts
  31. 1 75
      src/resources/sales/area/UserAreaCollection.ts
  32. 1 27
      src/resources/sales/executives_history/ExecutivesHistoriCollection.ts
  33. 1 21
      src/resources/sales/executives_history/ExecutivesHistoriResource.ts
  34. 58 66
      src/resources/sales/hospital/HospitalCollection.ts
  35. 31 43
      src/resources/sales/hospital/HospitalResource.ts
  36. 42 31
      src/resources/sales/status_history/StatusHistoryCollection.ts
  37. 0 22
      src/resources/sales/vendor/VendorCollection.js
  38. 54 29
      src/resources/sales/vendor_experience/VendorExperienceCollection.ts
  39. 24 34
      src/resources/sales/vendor_experience/VendorExperienceResource.ts
  40. 16 0
      src/routes/admin/CategoryRoute.ts
  41. 13 74
      src/routes/admin/HospitalRoute.ts
  42. 13 70
      src/routes/sales/HospitalRoute.ts
  43. 60 0
      src/services/admin/CategoryLinkService.ts
  44. 203 0
      src/services/admin/CategoryService.ts
  45. 45 43
      src/services/admin/HospitalService.ts
  46. 12 79
      src/services/admin/StatusHistoryService.ts
  47. 47 7
      src/services/admin/VendorExperienceService.ts
  48. 50 5
      src/services/admin/VendorService.ts
  49. 43 289
      src/services/sales/HospitalService.ts
  50. 12 102
      src/services/sales/StatusHistoryService.ts
  51. 50 481
      src/services/sales/VendorExperienceService.ts
  52. 19 0
      src/types/admin/category/CategoryDTO.ts
  53. 62 0
      src/types/admin/category_link/CategoryLinkDTO.ts
  54. 4 1
      src/types/admin/hospital/HospitalDTO.ts
  55. 5 1
      src/types/admin/status_history/StatusHistoryDTO.ts
  56. 10 2
      src/types/admin/vendor/VendorDTO.ts
  57. 10 2
      src/types/admin/vendor_experience/VendorExperienceDTO.ts
  58. 5 2
      src/types/sales/hospital/HospitalDTO.ts
  59. 5 1
      src/types/sales/status_history/StatusHistoryDTO.ts
  60. 10 2
      src/types/sales/vendor_experience/VendorExperienceDTO.ts
  61. 61 0
      src/utils/GetSourceDataByType.ts
  62. 1 15
      src/utils/TimeLocal.ts
  63. 49 0
      src/validators/admin/category/CategoryValidators.ts
  64. 5 178
      src/validators/admin/hospital/HospitalValidators.ts
  65. 6 27
      src/validators/admin/status_history/StatusHistoryValidators.ts
  66. 30 12
      src/validators/admin/vendor/VendorValidators.ts
  67. 19 74
      src/validators/admin/vendor_experience/VendorExperienceValidators.ts
  68. 6 27
      src/validators/sales/status_history/StatusHistoryValidators.ts
  69. 55 69
      src/validators/sales/vendor_experience/VendorExperienceValidators.ts

+ 2 - 0
index.ts

@@ -13,6 +13,7 @@ import vendorRoutes from './src/routes/admin/VendorRoute';
13 13
 import hospitalRoutes from './src/routes/admin/HospitalRoute';
14 14
 import salesHospitalRoutes from './src/routes/sales/HospitalRoute';
15 15
 import areaRoutes from './src/routes/sales/AreaRoute';
16
+import CategoryRoutes from './src/routes/admin/CategoryRoute';
16 17
 
17 18
 import './src/utils/Scheduler';
18 19
 
@@ -32,6 +33,7 @@ apiV1.use('/hospital', hospitalRoutes);
32 33
 apiV1.use('/hospital-area', salesHospitalRoutes);
33 34
 apiV1.use('/vendor', vendorRoutes);
34 35
 apiV1.use('/area', areaRoutes);
36
+apiV1.use('/category', CategoryRoutes);
35 37
 // apiV1.use('/vendor-sales', vendorSalesRoutes);
36 38
 
37 39
 app.get('/', (req: Request, res: Response) => {

+ 28 - 0
prisma/migrations/20250802022241_add_table_epic_7/migration.sql

@@ -0,0 +1,28 @@
1
+-- CreateTable
2
+CREATE TABLE "categories" (
3
+    "id" TEXT NOT NULL,
4
+    "tag" TEXT NOT NULL,
5
+    "deskripsi" TEXT,
6
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
7
+    "updatedAt" TIMESTAMP(3) NOT NULL,
8
+    "deletedAt" TIMESTAMP(3),
9
+
10
+    CONSTRAINT "categories_pkey" PRIMARY KEY ("id")
11
+);
12
+
13
+-- CreateTable
14
+CREATE TABLE "category_links" (
15
+    "id" TEXT NOT NULL,
16
+    "category_id" TEXT NOT NULL,
17
+    "source_type" TEXT NOT NULL,
18
+    "source_id" TEXT,
19
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+    "updatedAt" TIMESTAMP(3) NOT NULL,
21
+    "deletedAt" TIMESTAMP(3),
22
+    "categoryId" TEXT,
23
+
24
+    CONSTRAINT "category_links_pkey" PRIMARY KEY ("id")
25
+);
26
+
27
+-- AddForeignKey
28
+ALTER TABLE "category_links" ADD CONSTRAINT "category_links_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 9 - 0
prisma/migrations/20250802023321_update_field_in_table_category/migration.sql

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

+ 9 - 0
prisma/migrations/20250802040342_update_field_in_table_category_link/migration.sql

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

+ 11 - 0
prisma/migrations/20250802085021_update_table_category_link/migration.sql

@@ -0,0 +1,11 @@
1
+-- AddForeignKey
2
+ALTER TABLE "category_links" ADD CONSTRAINT "fk_categorylink_vendor" FOREIGN KEY ("source_id") REFERENCES "vendors"("id") ON DELETE SET NULL ON UPDATE CASCADE;
3
+
4
+-- AddForeignKey
5
+ALTER TABLE "category_links" ADD CONSTRAINT "fk_categorylink_vendor_experience" FOREIGN KEY ("source_id") REFERENCES "vendor_experiences"("id") ON DELETE SET NULL ON UPDATE CASCADE;
6
+
7
+-- AddForeignKey
8
+ALTER TABLE "category_links" ADD CONSTRAINT "fk_categorylink_status_history" FOREIGN KEY ("source_id") REFERENCES "status_histories"("id") ON DELETE SET NULL ON UPDATE CASCADE;
9
+
10
+-- AddForeignKey
11
+ALTER TABLE "category_links" ADD CONSTRAINT "fk_categorylink_hospital" FOREIGN KEY ("source_id") REFERENCES "hospitals"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 11 - 0
prisma/migrations/20250804032035_add_category_id_in_category_link/migration.sql

@@ -0,0 +1,11 @@
1
+-- DropForeignKey
2
+ALTER TABLE "category_links" DROP CONSTRAINT "fk_categorylink_hospital";
3
+
4
+-- DropForeignKey
5
+ALTER TABLE "category_links" DROP CONSTRAINT "fk_categorylink_status_history";
6
+
7
+-- DropForeignKey
8
+ALTER TABLE "category_links" DROP CONSTRAINT "fk_categorylink_vendor";
9
+
10
+-- DropForeignKey
11
+ALTER TABLE "category_links" DROP CONSTRAINT "fk_categorylink_vendor_experience";

+ 43 - 18
prisma/schema.prisma

@@ -175,7 +175,7 @@ model UserArea {
175 175
 // }
176 176
 
177 177
 model VendorExperience {
178
-  id                    String    @id @default(uuid())
178
+  id                    String         @id @default(uuid())
179 179
   hospital_id           String
180 180
   vendor_id             String?
181 181
   status                String?
@@ -183,13 +183,13 @@ model VendorExperience {
183 183
   contract_expired_date DateTime?
184 184
   contract_value_min    BigInt?
185 185
   contract_value_max    BigInt?
186
-  positive_notes        String?   @db.Text
187
-  negative_notes        String?   @db.Text
186
+  positive_notes        String?        @db.Text
187
+  negative_notes        String?        @db.Text
188 188
   simrs_type            String
189
-  hospital              Hospital  @relation(fields: [hospital_id], references: [id])
190
-  vendor                Vendor?   @relation(fields: [vendor_id], references: [id])
191
-  createdAt             DateTime  @default(now())
192
-  updatedAt             DateTime  @updatedAt
189
+  hospital              Hospital       @relation(fields: [hospital_id], references: [id])
190
+  vendor                Vendor?        @relation(fields: [vendor_id], references: [id])
191
+  createdAt             DateTime       @default(now())
192
+  updatedAt             DateTime       @updatedAt
193 193
   deletedAt             DateTime?
194 194
 
195 195
   @@map("vendor_experiences")
@@ -212,17 +212,42 @@ model ExecutivesHistory {
212 212
 }
213 213
 
214 214
 model StatusHistory {
215
-  id          String         @id @default(uuid())
216
-  hospital_id String
217
-  user_id     String
218
-  old_status  ProgressStatus
219
-  new_status  ProgressStatus
220
-  note        String?        @db.Text
221
-  hospital    Hospital       @relation(fields: [hospital_id], references: [id])
222
-  user        UserKeycloak   @relation(fields: [user_id], references: [id])
223
-  createdAt   DateTime       @default(now())
224
-  updatedAt   DateTime       @updatedAt
225
-  deletedAt   DateTime?
215
+  id           String         @id @default(uuid())
216
+  hospital_id  String
217
+  user_id      String
218
+  old_status   ProgressStatus
219
+  new_status   ProgressStatus
220
+  note         String?        @db.Text
221
+  hospital     Hospital       @relation(fields: [hospital_id], references: [id])
222
+  user         UserKeycloak   @relation(fields: [user_id], references: [id])
223
+  createdAt    DateTime       @default(now())
224
+  updatedAt    DateTime       @updatedAt
225
+  deletedAt    DateTime?
226 226
 
227 227
   @@map("status_histories")
228 228
 }
229
+
230
+model Category {
231
+  id           String         @id @default(uuid())
232
+  tag          String
233
+  description  String?        @db.Text
234
+  CategoryLink CategoryLink[]
235
+  createdAt    DateTime       @default(now())
236
+  updatedAt    DateTime       @updatedAt
237
+  deletedAt    DateTime?
238
+
239
+  @@map("categories")
240
+}
241
+
242
+model CategoryLink {
243
+  id               String            @id @default(uuid())
244
+  category_id      String
245
+  source_type      String?
246
+  source_id        String?
247
+  createdAt        DateTime          @default(now())
248
+  updatedAt        DateTime          @updatedAt
249
+  deletedAt        DateTime?
250
+  Category         Category?         @relation(fields: [category_id], references: [id])
251
+
252
+  @@map("category_links")
253
+}

+ 2 - 2
prisma/seeders/HospitalSeeder.ts

@@ -5,8 +5,8 @@ const prisma = new PrismaClient();
5 5
 
6 6
 export async function seedHospitals(): Promise<void> {
7 7
     try {
8
-        const sales1 = await prisma.userKeycloak.findFirst({ where: { id: 'd4c7941b-a180-46dd-ab2c-e9e4d4a53e96' } });
9
-        const sales2 = await prisma.userKeycloak.findFirst({ where: { id: '9a9833ec-dd95-4445-b64e-343335815039' } });
8
+        const sales1 = await prisma.userKeycloak.findFirst({ where: { id: '00c7e427-cbb7-408c-af44-e04747fd61fd' } });
9
+        const sales2 = await prisma.userKeycloak.findFirst({ where: { id: 'd27994a4-07b9-4ea8-bd62-3d6c51afed0e' } });
10 10
 
11 11
         if (!sales1 || !sales2) {
12 12
             throw new Error('User sales1 or sales2 not found');

+ 2 - 2
prisma/seeders/UserAreaSeeder.ts

@@ -3,8 +3,8 @@ import prisma from '../../src/prisma/PrismaClient';
3 3
 export async function seedUserAreas(): Promise<void> {
4 4
     try {
5 5
         // Ambil user dengan username 'sales1' dan 'sales2'
6
-        const sales1 = await prisma.userKeycloak.findFirst({ where: { id: 'd4c7941b-a180-46dd-ab2c-e9e4d4a53e96' } });
7
-        const sales2 = await prisma.userKeycloak.findFirst({ where: { id: '9a9833ec-dd95-4445-b64e-343335815039' } });
6
+        const sales1 = await prisma.userKeycloak.findFirst({ where: { id: '00c7e427-cbb7-408c-af44-e04747fd61fd' } });
7
+        const sales2 = await prisma.userKeycloak.findFirst({ where: { id: 'd27994a4-07b9-4ea8-bd62-3d6c51afed0e' } });
8 8
 
9 9
         if (!sales1 || !sales2) {
10 10
             throw new Error('User sales1 or sales2 not found');

+ 1 - 1
prisma/seeders/VendorSeeder.ts

@@ -13,7 +13,7 @@ export async function seedVendors(): Promise<void> {
13 13
         // Cari user dengan username admin1
14 14
         const adminUser = await prisma.userKeycloak.findFirst({
15 15
             where: {
16
-                id: '9727ba1a-2266-4699-b3b9-9656f16c9785',
16
+                id: '06ccc7a6-9f6d-437f-9e1f-501a36077796',
17 17
             },
18 18
         });
19 19
 

+ 92 - 0
src/controllers/admin/CategoryController.ts

@@ -0,0 +1,92 @@
1
+import { Request, Response } from 'express';
2
+import * as CategoryService from '../../services/admin/CategoryService';
3
+import { PaginationParam } from '../../utils/PaginationParams';
4
+import { errorResponse, messageSuccessResponse } from '../../utils/Response';
5
+import { validateCategoryStoreRequest, validateCategoryUpdateRequest, validateMergeCategoryStoreRequest } from '../../validators/admin/category/CategoryValidators';
6
+import { CustomRequest } from '../../types/token/CustomRequest';
7
+import { CategoryCollection } from '../../resources/admin/category/CategoryCollection';
8
+import { CategoryResource } from '../../resources/admin/category/CategoryResource';
9
+import { CategoryLinkCollection } from '../../resources/admin/category/CategoryUseCollection';
10
+
11
+export const getAllCategory = async (req: Request, res: Response): Promise<Response> => {
12
+    try {
13
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
14
+
15
+        const { categories, total } = await CategoryService.getAllCategoryService({
16
+            page, limit, search, sortBy, orderBy
17
+        });
18
+
19
+        return CategoryCollection(req, res, categories, total, page, limit, 'Category data successfully retrieved');
20
+    } catch (err) {
21
+        return errorResponse(res, err);
22
+    }
23
+};
24
+
25
+export const showCategory = async (req: Request, res: Response): Promise<Response> => {
26
+    try {
27
+        const id = req.params.id;
28
+        const data = await CategoryService.showCategoryService(id);
29
+        return CategoryResource(res, data, 'Success show category');
30
+    } catch (err) {
31
+        return errorResponse(res, err);
32
+    }
33
+};
34
+
35
+export const storeCategory = async (req: Request, res: Response): Promise<Response> => {
36
+    try {
37
+        const validatedData = validateCategoryStoreRequest(req.body);
38
+        await CategoryService.storeCategoryService(validatedData, req as CustomRequest);
39
+        return messageSuccessResponse(res, 'Success added category', 201);
40
+    } catch (err) {
41
+        return errorResponse(res, err);
42
+    }
43
+};
44
+
45
+export const updateCategory = async (req: Request, res: Response): Promise<Response> => {
46
+    try {
47
+        const id = req.params.id;
48
+        const validatedData = validateCategoryUpdateRequest(req.body);
49
+        await CategoryService.updateCategoryService(validatedData, id, req as CustomRequest);
50
+        return messageSuccessResponse(res, 'Success update category');
51
+    } catch (err) {
52
+        return errorResponse(res, err);
53
+    }
54
+};
55
+
56
+export const deleteCategory = async (req: Request, res: Response): Promise<Response> => {
57
+    try {
58
+        const id = req.params.id;
59
+        await CategoryService.deleteCategoryService(id, req as CustomRequest);
60
+        return messageSuccessResponse(res, 'Success delete category');
61
+    } catch (err) {
62
+        return errorResponse(res, err);
63
+    }
64
+};
65
+
66
+export const showUseCategory = async (req: Request, res: Response): Promise<Response> => {
67
+    try {
68
+        const categoryId = req.params.id;
69
+        const { page, limit, search } = PaginationParam(req);
70
+
71
+        const { data, total } = await CategoryService.showUseCategoryService({
72
+            page,
73
+            limit,
74
+            search,
75
+            categoryId,
76
+        });
77
+
78
+        return CategoryLinkCollection(req, res, data, total, page, limit, 'Category usage data successfully retrieved');
79
+    } catch (err) {
80
+        return errorResponse(res, err);
81
+    }
82
+};
83
+
84
+export const mergeCategory = async (req: Request, res: Response): Promise<Response> => {
85
+    try {
86
+        const validatedData = validateMergeCategoryStoreRequest(req.body);
87
+        await CategoryService.mergeCategoryService(validatedData, req as CustomRequest);
88
+        return messageSuccessResponse(res, 'Success merge category', 200);
89
+    } catch (err) {
90
+        return errorResponse(res, err);
91
+    }
92
+};

+ 1 - 64
src/controllers/admin/HospitalController.ts

@@ -80,67 +80,4 @@ export const deleteHospital = async (req: Request, res: Response): Promise<Respo
80 80
     } catch (err) {
81 81
         return errorResponse(res, err);
82 82
     }
83
-};
84
-
85
-
86
-// const { HospitalCollection } = require('../../resources/admin/hospital/HospitalCollection.js');
87
-// const { HospitalResource } = require('../../resources/admin/hospital/HospitalResource.js');
88
-// const hospitalService = require('../../services/admin/HospitalService.js');
89
-// const { PaginationParam } = require('../../utils/PaginationParams.js');
90
-// const { errorResponse, messageSuccessResponse } = require('../../utils/Response.js');
91
-// const { validateStoreHospitalRequest, validateUpdateHospitalRequest } = require('../../validators/admin/hospital/HospitalValidators.js');
92
-
93
-// exports.getAllHospital = async (req, res) => {
94
-//     try {
95
-//         const { page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status } = PaginationParam(req, ['province', 'city', 'type', 'ownership', 'progress_status']);
96
-
97
-//         const { hospitals, total } = await hospitalService.getAllHospitalService({
98
-//             page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status
99
-//         });
100
-
101
-//         return HospitalCollection({ req, res, data: hospitals, total, page, limit, message: 'Hospital data successfully retrieved' });
102
-//     } catch (err) {
103
-//         return errorResponse(res, err);
104
-//     }
105
-// };
106
-
107
-// exports.showHospital = async (req, res) => {
108
-//     try {
109
-//         const id = req.params.id;
110
-//         const data = await hospitalService.showHospitalService(id);
111
-//         return HospitalResource(res, data, 'Success show hospital');
112
-//     } catch (err) {
113
-//         return errorResponse(res, err);
114
-//     }
115
-// };
116
-
117
-// exports.storeHospital = async (req, res) => {
118
-//     try {
119
-//         const validatedData = validateStoreHospitalRequest(req.body);
120
-//         await hospitalService.storeHospitalService(validatedData, req);
121
-//         return messageSuccessResponse(res, 'Success added hospital', 201);
122
-//     } catch (err) {
123
-//         return errorResponse(res, err);
124
-//     }
125
-// }
126
-
127
-// exports.updateHospital = async (req, res) => {
128
-//     try {
129
-//         const id = req.params.id;
130
-//         const validatedData = validateUpdateHospitalRequest(req.body);
131
-//         await hospitalService.updateHospitalService(validatedData, id, req);
132
-//         return messageSuccessResponse(res, 'Success update hospital');
133
-//     } catch (err) {
134
-//         return errorResponse(res, err);
135
-//     }
136
-// }
137
-
138
-// exports.deleteHospital = async (req, res) => {
139
-//     try {
140
-//         const id = req.params.id;
141
-//         await hospitalService.deleteHospitalService(id, req);
142
-//         return messageSuccessResponse(res, 'Success delete hospital');
143
-//     } catch (err) {
144
-//         return errorResponse(res, err);
145
-//     }
146
-// };
83
+};

+ 1 - 2
src/controllers/admin/VendorController.ts

@@ -43,9 +43,8 @@ export const storeVendor = async (req: Request, res: Response): Promise<Response
43 43
 
44 44
 export const updateVendor = async (req: Request, res: Response): Promise<Response> => {
45 45
     try {
46
-        const id: string = req.params.id;
47 46
         const validatedData = validateUpdateVendorRequest(req.body);
48
-        await VendorService.updateVendorService(validatedData, id, req as CustomRequest);
47
+        await VendorService.updateVendorService(validatedData, req as CustomRequest);
49 48
         return messageSuccessResponse(res, 'Success update vendor');
50 49
     } catch (err) {
51 50
         return errorResponse(res, err);

+ 2 - 2
src/controllers/admin/VendorExperienceController.ts

@@ -16,7 +16,7 @@ export const getAllVendorExperience = async (req: Request, res: Response): Promi
16 16
             req
17 17
         );
18 18
 
19
-        return VendorExperienceCollection(req, res, vendor_experiences, total, page, limit, 'Vendor experience successfully retrieved');
19
+        return await VendorExperienceCollection(req, res, vendor_experiences, total, page, limit, 'Vendor experience successfully retrieved');
20 20
     } catch (err) {
21 21
         return errorResponse(res, err);
22 22
     }
@@ -25,7 +25,7 @@ export const getAllVendorExperience = async (req: Request, res: Response): Promi
25 25
 export const showVendorExperience = async (req: Request, res: Response) => {
26 26
     try {
27 27
         const data = await VendorExperienceService.showVendorExperienceService(req);
28
-        return VendorExperienceResource(res, data, 'Success show vendor experience');
28
+        return await VendorExperienceResource(res, data, 'Success show vendor experience');
29 29
     } catch (err) {
30 30
         return errorResponse(res, err);
31 31
     }

+ 2 - 4
src/controllers/sales/HospitalController.ts

@@ -24,24 +24,22 @@ export const getAllHospitalByArea = async (req: any, res: Response): Promise<Res
24 24
     }
25 25
 };
26 26
 
27
-// POST Store Hospital
28 27
 export const storeHospital = async (req: Request, res: Response): Promise<Response> => {
29 28
     try {
30 29
         const validatedData = validateStoreHospitalRequest(req.body);
31 30
         await HospitalController.storeHospitalService(validatedData, req as CustomRequest);
32
-        return messageSuccessResponse(res, "Success added hospital", 201);
31
+        return messageSuccessResponse(res, 'Success added hospital', 201);
33 32
     } catch (err) {
34 33
         return errorResponse(res, err);
35 34
     }
36 35
 };
37 36
 
38
-// PATCH/PUT Update Hospital
39 37
 export const updateHospital = async (req: Request, res: Response): Promise<Response> => {
40 38
     try {
41 39
         const id = req.params.id;
42 40
         const validatedData = validateUpdateHospitalRequest(req.body);
43 41
         await HospitalController.updateHospitalService(validatedData, id, req as CustomRequest);
44
-        return messageSuccessResponse(res, "Success update hospital");
42
+        return messageSuccessResponse(res, 'Success update hospital');
45 43
     } catch (err) {
46 44
         return errorResponse(res, err);
47 45
     }

+ 32 - 0
src/repository/admin/CategoryLinkRepository.ts

@@ -0,0 +1,32 @@
1
+import prisma from '../../prisma/PrismaClient';
2
+import { Prisma } from '@prisma/client';
3
+import { now } from '../../utils/TimeLocal';
4
+
5
+const CategoryLinkRepository = {
6
+    create: async (data: Prisma.CategoryLinkUncheckedCreateInput) => {
7
+        return prisma.categoryLink.create({ data });
8
+    },
9
+
10
+    findBySource: async (source_type: string, source_id: string) => {
11
+        return prisma.categoryLink.findMany({
12
+            where: {
13
+                source_id,
14
+                source_type,
15
+                deletedAt: null,
16
+            },
17
+        });
18
+    },
19
+
20
+    deleteById: async (id: string) => {
21
+        return prisma.categoryLink.update({
22
+            where: {
23
+                id,
24
+            },
25
+            data: {
26
+                deletedAt: now().toDate(),
27
+            },
28
+        });
29
+    },
30
+};
31
+
32
+export default CategoryLinkRepository;

+ 151 - 0
src/repository/admin/CategoryRepository.ts

@@ -0,0 +1,151 @@
1
+import prisma from '../../prisma/PrismaClient';
2
+import { Prisma } from '@prisma/client';
3
+
4
+interface FindAllParams {
5
+    skip?: number;
6
+    take?: number;
7
+    where?: Prisma.CategoryWhereInput;
8
+    orderBy?: Prisma.CategoryOrderByWithRelationInput;
9
+}
10
+
11
+interface FindAllCategoryLinkParams {
12
+    skip?: number;
13
+    take?: number;
14
+    where?: Prisma.CategoryLinkWhereInput;
15
+    orderBy?: Prisma.CategoryLinkOrderByWithRelationInput;
16
+}
17
+
18
+const CategoryRepository = {
19
+    findAll: async ({ skip, take, where, orderBy }: FindAllParams) => {
20
+        const categories = await prisma.category.findMany({
21
+            where,
22
+            skip,
23
+            take,
24
+            orderBy,
25
+            select: {
26
+                id: true,
27
+                tag: true,
28
+                description: true,
29
+                createdAt: true,
30
+                updatedAt: true,
31
+                _count: {
32
+                    select: {
33
+                        CategoryLink: {
34
+                            where: {
35
+                                deletedAt: null,
36
+                            },
37
+                        },
38
+                    },
39
+                },
40
+            },
41
+        });
42
+
43
+        const formattedCategories = categories.map(v => {
44
+            const { _count, ...rest } = v;
45
+            return {
46
+                ...rest,
47
+                count_use_tags: _count.CategoryLink || 0,
48
+            };
49
+        });
50
+
51
+        return formattedCategories;
52
+    },
53
+
54
+    countAll: async (where?: Prisma.CategoryWhereInput): Promise<number> => {
55
+        return prisma.category.count({ where });
56
+    },
57
+
58
+    findById: async (id: string) => {
59
+        const category = await prisma.category.findFirst({
60
+            where: {
61
+                id,
62
+                deletedAt: null,
63
+            },
64
+            select: {
65
+                id: true,
66
+                tag: true,
67
+                description: true,
68
+                createdAt: true,
69
+                updatedAt: true,
70
+                _count: {
71
+                    select: {
72
+                        CategoryLink: {
73
+                            where: {
74
+                                deletedAt: null,
75
+                            },
76
+                        },
77
+                    },
78
+                },
79
+            },
80
+        });
81
+
82
+        if (!category) return null;
83
+
84
+        const { _count, ...rest } = category;
85
+        return {
86
+            ...rest,
87
+            count_use: _count.CategoryLink || 0,
88
+        };
89
+    },
90
+
91
+    findByTag: async (tag: string) => {
92
+        return prisma.category.findFirst({
93
+            where: {
94
+                tag: {
95
+                    equals: tag,
96
+                    mode: "insensitive",
97
+                },
98
+                deletedAt: null,
99
+            },
100
+        });
101
+    },
102
+
103
+    create: async (data: Prisma.CategoryCreateInput) => {
104
+        return prisma.category.create({ data });
105
+    },
106
+
107
+    update: async (id: string, data: Prisma.CategoryUpdateInput) => {
108
+        return prisma.category.update({
109
+            where: { id },
110
+            data,
111
+        });
112
+    },
113
+
114
+    findByCategoryId: async (categoryId: string) => {
115
+        return prisma.categoryLink.findMany({
116
+            where: {
117
+                category_id: categoryId,
118
+                deletedAt: null
119
+            },
120
+            select: {
121
+                id: true,
122
+                source_id: true,
123
+                source_type: true,
124
+                // category_id: true,
125
+                createdAt: true,
126
+                updatedAt: true,
127
+            }
128
+        });
129
+    },
130
+
131
+    findAllCategoryLink: async ({ skip, take, where, }: FindAllCategoryLinkParams) => {
132
+        return prisma.categoryLink.findMany({
133
+            where,
134
+            skip,
135
+            take,
136
+            select: {
137
+                id: true,
138
+                source_id: true,
139
+                source_type: true,
140
+                createdAt: true,
141
+                updatedAt: true,
142
+            },
143
+        });
144
+    },
145
+
146
+    countAllCategoryLink: async (where: Prisma.CategoryLinkWhereInput) => {
147
+        return prisma.categoryLink.count({ where });
148
+    },
149
+};
150
+
151
+export default CategoryRepository;

+ 24 - 0
src/resources/admin/category/CategoryCollection.ts

@@ -0,0 +1,24 @@
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import { CategoryDTO } from '../../../types/admin/category/CategoryDTO';
5
+
6
+const formatItem = (item: CategoryDTO) => ({
7
+    ...item,
8
+    createdAt: formatISOWithoutTimezone(item.createdAt),
9
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
10
+});
11
+
12
+export const CategoryCollection = (req: Request, res: Response, data: CategoryDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success') => {
13
+    const formattedData = data.map(formatItem);
14
+
15
+    if (typeof total !== 'number') {
16
+        return res.status(200).json({
17
+            success: true,
18
+            message,
19
+            data: Array.isArray(formattedData),
20
+        });
21
+    }
22
+
23
+    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
24
+};

+ 37 - 0
src/resources/admin/category/CategoryResource.ts

@@ -0,0 +1,37 @@
1
+import { Response } from 'express';
2
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
3
+import { CategoryDTO } from '../../../types/admin/category/CategoryDTO';
4
+
5
+const formatItem = (item: CategoryDTO) => ({
6
+    ...item,
7
+    createdAt: formatISOWithoutTimezone(item.createdAt),
8
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
9
+});
10
+
11
+export const CategoryResource = (res: Response, data: CategoryDTO, message: string = 'Success') => {
12
+    const formattedData = formatItem(data);
13
+
14
+    return res.status(200).json({
15
+        success: true,
16
+        message,
17
+        data: formattedData,
18
+    });
19
+};
20
+
21
+// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
22
+
23
+// const formatItem = (item) => ({
24
+//     ...item,
25
+//     createdAt: formatISOWithoutTimezone(item.createdAt),
26
+//     updatedAt: formatISOWithoutTimezone(item.updatedAt)
27
+// });
28
+
29
+// exports.ProvinceResource = (res, data, message = 'Success') => {
30
+//     const formattedData = formatItem(data);
31
+
32
+//     return res.status(200).json({
33
+//         success: true,
34
+//         message,
35
+//         data: formattedData
36
+//     });
37
+// };

+ 92 - 0
src/resources/admin/category/CategoryUseCollection.ts

@@ -0,0 +1,92 @@
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import { CategoryLinkUseDTO } from '../../../types/admin/category_link/CategoryLinkDTO';
5
+
6
+const formatItem = (item: CategoryLinkUseDTO) => {
7
+    const base = {
8
+        id: item.id,
9
+        source_type: item.source_type
10
+            ? item.source_type
11
+                .replace(/_/g, ' ')
12
+                .toLowerCase()
13
+                .replace(/^./, (l) => l.toUpperCase())
14
+            : undefined,
15
+        createdAt: formatISOWithoutTimezone(item.createdAt),
16
+        updatedAt: formatISOWithoutTimezone(item.updatedAt),
17
+    };
18
+
19
+    if (item.vendor_experience) {
20
+        return {
21
+            ...base,
22
+            vendor_experience: {
23
+                id: item.vendor_experience.id,
24
+                status: item.vendor_experience.status,
25
+                simrs_type: item.vendor_experience.simrs_type,
26
+                contract_start_date: formatDateOnly(item.vendor_experience.contract_start_date),
27
+                contract_expired_date: formatDateOnly(item.vendor_experience.contract_expired_date),
28
+                contract_value_min: item.vendor_experience.contract_value_min,
29
+                contract_value_max: item.vendor_experience.contract_value_max,
30
+                positive_notes: item.vendor_experience.positive_notes,
31
+                negative_notes: item.vendor_experience.negative_notes,
32
+            },
33
+        };
34
+    }
35
+
36
+    if (item.status_history) {
37
+        return {
38
+            ...base,
39
+            status_history: {
40
+                id: item.status_history.id,
41
+                old_status: item.status_history.old_status,
42
+                new_status: item.status_history.new_status,
43
+                note: item.status_history.note,
44
+            },
45
+        };
46
+    }
47
+
48
+    if (item.hospital) {
49
+        return {
50
+            ...base,
51
+            hospital: {
52
+                id: item.hospital.id,
53
+                name: item.hospital.name,
54
+                hospital_code: item.hospital.hospital_code,
55
+                ownership: item.hospital.ownership,
56
+                address: item.hospital.address,
57
+                contact: item.hospital.contact,
58
+                progress_status: item.hospital.progress_status,
59
+                note: item.hospital.note,
60
+            },
61
+        };
62
+    }
63
+
64
+    if (item.vendor) {
65
+        return {
66
+            ...base,
67
+            vendor: {
68
+                id: item.vendor.id,
69
+                name: item.vendor.name,
70
+                name_pt: item.vendor.name_pt,
71
+                strengths: item.vendor.strengths,
72
+                weaknesses: item.vendor.weaknesses,
73
+                website: item.vendor.website,
74
+            },
75
+        };
76
+    }
77
+
78
+    return base;
79
+};
80
+
81
+export const CategoryLinkCollection = (
82
+    req: Request,
83
+    res: Response,
84
+    data: CategoryLinkUseDTO[] = [],
85
+    total: number | undefined,
86
+    page: number = 1,
87
+    limit: number = 10,
88
+    message: string = 'Success'
89
+) => {
90
+    const formattedData = data.map(formatItem);
91
+    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
92
+};

+ 63 - 0
src/resources/admin/category/TransformCategoryLinkUse.ts

@@ -0,0 +1,63 @@
1
+import { CategoryLinkUseDTO } from '../../../types/admin/category_link/CategoryLinkDTO';
2
+
3
+export const TransformCategoryLinkUse = (
4
+    link: any,
5
+    source_type: string,
6
+    data: any
7
+): CategoryLinkUseDTO => {
8
+    return {
9
+        id: link.id,
10
+        source_type: link.source_type,
11
+        category_id: link.category_id,
12
+        createdAt: link.createdAt,
13
+        updatedAt: link.updatedAt,
14
+        vendor_experience:
15
+            (source_type === 'vendor_experience_positive_notes' ||
16
+                source_type === 'vendor_experience_negative_notes') && data
17
+                ? {
18
+                    id: data.id,
19
+                    status: data.status,
20
+                    contract_start_date: data.contract_start_date,
21
+                    contract_expired_date: data.contract_expired_date,
22
+                    contract_value_min: data.contract_value_min,
23
+                    contract_value_max: data.contract_value_max,
24
+                    positive_notes: data.positive_notes,
25
+                    negative_notes: data.negative_notes,
26
+                    simrs_type: data.simrs_type,
27
+                }
28
+                : null,
29
+        status_history:
30
+            source_type === 'status_history_notes' && data
31
+                ? {
32
+                    id: data.id,
33
+                    old_status: data.old_status,
34
+                    new_status: data.new_status,
35
+                    note: data.note,
36
+                }
37
+                : null,
38
+        hospital:
39
+            source_type === 'hospital_notes' && data
40
+                ? {
41
+                    id: data.id,
42
+                    name: data.name,
43
+                    hospital_code: data.hospital_code,
44
+                    ownership: data.ownership,
45
+                    address: data.address,
46
+                    contact: data.contact,
47
+                    progress_status: data.progress_status,
48
+                    note: data.note,
49
+                }
50
+                : null,
51
+        vendor:
52
+            (source_type === 'vendor_strength_notes' || source_type === 'vendor_weaknesses_notes') && data
53
+                ? {
54
+                    id: data.id,
55
+                    name: data.name,
56
+                    name_pt: data.name_pt,
57
+                    strengths: data.strengths,
58
+                    weaknesses: data.weaknesses,
59
+                    website: data.website,
60
+                }
61
+                : null,
62
+    };
63
+};

+ 42 - 99
src/resources/admin/hospital/HospitalCollection.ts

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3 3
 import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4 4
 import { HospitalDTO } from '../../../types/admin/hospital/HospitalDTO';
5
+import prisma from '../../../prisma/PrismaClient';
5 6
 
6 7
 const formatItem = (item: HospitalDTO) => ({
7 8
     ...item,
@@ -9,8 +10,46 @@ const formatItem = (item: HospitalDTO) => ({
9 10
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
10 11
 });
11 12
 
12
-export const HospitalCollection = (req: Request, res: Response, data: HospitalDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
13
-    const formattedData = data.map(formatItem);
13
+export const HospitalCollection = async (req: Request, res: Response, data: HospitalDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Promise<Response> => {
14
+    const ids = data.map(item => item.id);
15
+
16
+    // Ambil tags dari category_links yang relevan
17
+    const allTags = await prisma.categoryLink.findMany({
18
+        where: {
19
+            source_id: { in: ids },
20
+            source_type: { in: ['hospital_notes'] },
21
+            deletedAt: null,
22
+        },
23
+        include: {
24
+            Category: true,
25
+        },
26
+    });
27
+
28
+    // Kelompokkan tags berdasarkan source_type dan source_id
29
+    const tagMap = new Map<string, { note: string[] }>();
30
+    for (const id of ids) {
31
+        tagMap.set(id, { note: [] });
32
+    }
33
+
34
+    for (const tag of allTags) {
35
+        const id = tag.source_id!;
36
+        const categoryTag = tag.Category?.tag ?? '';
37
+        const current = tagMap.get(id);
38
+        if (!current) continue;
39
+
40
+        if (tag.source_type === 'hospital_notes') {
41
+            current.note.push(categoryTag);
42
+        }
43
+    }
44
+
45
+    const formattedData = data.map(item => {
46
+        const { created_by, ...rest } = item;
47
+        const tags = tagMap.get(item.id);
48
+        return formatItem({
49
+            ...rest,
50
+            note_tags: tags?.note ?? [],
51
+        });
52
+    });
14 53
 
15 54
     if (typeof total !== 'number') {
16 55
         return res.status(200).json({
@@ -29,100 +68,4 @@ export const HospitalCollection = (req: Request, res: Response, data: HospitalDT
29 68
         limit,
30 69
         message,
31 70
     });
32
-};
33
-
34
-
35
-// ==============================================
36
-
37
-// import { Request, Response } from "express";
38
-// import { ListResponse } from "../../../utils/ListResponse";
39
-// import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
40
-// import { getUserNameById } from "../../../utils/CheckUserKeycloak";
41
-
42
-// // Tipe untuk item rumah sakit
43
-// interface HospitalItem {
44
-//     id: string;
45
-//     name: string;
46
-//     created_by?: string;
47
-//     createdAt: Date | string;
48
-//     updatedAt: Date | string;
49
-//     [key: string]: any;
50
-// }
51
-
52
-// // Fungsi transform per item
53
-// const transformHospitalList = async (data: HospitalItem[] = []): Promise<any[]> => {
54
-//     return Promise.all(
55
-//         data.map(async ({ created_by, ...rest }) => {
56
-//             // const name = await getUserNameById(created_by);
57
-
58
-//             return {
59
-//                 ...rest,
60
-//                 // user: {
61
-//                 //   id: created_by,
62
-//                 //   name: name,
63
-//                 // },
64
-//                 createdAt: formatISOWithoutTimezone(rest.createdAt),
65
-//                 updatedAt: formatISOWithoutTimezone(rest.updatedAt),
66
-//             };
67
-//         })
68
-//     );
69
-// };
70
-
71
-// // Collection yang async
72
-// export const HospitalCollection = async ({
73
-//     req,
74
-//     res,
75
-//     data = [],
76
-//     total = 0,
77
-//     page = 1,
78
-//     limit = 10,
79
-//     message = "Success",
80
-// }: {
81
-//     req: Request;
82
-//     res: Response;
83
-//     data: HospitalItem[];
84
-//     total: number;
85
-//     page: number;
86
-//     limit: number;
87
-//     message?: string;
88
-// }) => {
89
-//     const formatted = await transformHospitalList(data);
90
-
91
-//     return ListResponse({
92
-//         req,
93
-//         res,
94
-//         data: formatted,
95
-//         total,
96
-//         page,
97
-//         limit,
98
-//         message,
99
-//     });
100
-// };
101
-
102
-// const { ListResponse } = require("../../../utils/ListResponse");
103
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
104
-// const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
105
-
106
-// // Fungsi transform per item
107
-// const transformHospitalList = async (data = []) => {
108
-//     return Promise.all(data.map(async ({ created_by, ...rest }) => {
109
-//         // const name = await getUserNameById(created_by);
110
-
111
-//         return {
112
-//             ...rest,
113
-//             // user: {
114
-//             //     id: created_by,
115
-//             //     name: name
116
-//             // },
117
-//             createdAt: formatISOWithoutTimezone(rest.createdAt),
118
-//             updatedAt: formatISOWithoutTimezone(rest.updatedAt)
119
-//         };
120
-//     }));
121
-// };
122
-
123
-// // Collection yang async
124
-// exports.HospitalCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
125
-//     const formatted = await transformHospitalList(data);
126
-
127
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
128
-// };
71
+};

+ 25 - 50
src/resources/admin/hospital/HospitalResource.ts

@@ -1,6 +1,7 @@
1 1
 import { Response } from 'express';
2 2
 import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
3 3
 import { HospitalDTO } from '../../../types/admin/hospital/HospitalDTO';
4
+import prisma from '../../../prisma/PrismaClient';
4 5
 
5 6
 const formatItem = (item: HospitalDTO) => ({
6 7
     ...item,
@@ -8,58 +9,32 @@ const formatItem = (item: HospitalDTO) => ({
8 9
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
9 10
 });
10 11
 
11
-export const HospitalResource = (res: Response, data: HospitalDTO, message: string = 'Success'): Response => {
12
-    const formattedData = formatItem(data);
13
-
14
-    return res.status(200).json({
15
-        success: true,
16
-        message,
17
-        data: formattedData,
12
+export const HospitalResource = async (res: Response, data: HospitalDTO, message: string = 'Success'): Promise<Response> => {
13
+    const tags = await prisma.categoryLink.findMany({
14
+        where: {
15
+            source_id: data.id,
16
+            source_type: { in: ['hospital_notes'] },
17
+            deletedAt: null,
18
+        },
19
+        include: {
20
+            Category: true,
21
+        },
18 22
     });
19
-};
20
-// ==================================================
21
-
22
-// import { Response } from 'express';
23
-// import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
24
-
25
-// interface HospitalItem {
26
-//     createdAt: string | Date;
27
-//     updatedAt: string | Date;
28
-//     [key: string]: any;
29
-// }
30 23
 
31
-// export const HospitalResource = (
32
-//     res: Response,
33
-//     data: HospitalItem,
34
-//     message: string = 'Success'
35
-// ) => {
36
-//     const formattedData = {
37
-//         ...data,
38
-//         createdAt: formatISOWithoutTimezone(data.createdAt),
39
-//         updatedAt: formatISOWithoutTimezone(data.updatedAt)
40
-//     };
24
+    const note_tags = tags
25
+        .filter(t => t.source_type === 'hospital_notes')
26
+        .map(t => t.Category?.tag ?? '');
41 27
 
42
-//     return res.status(200).json({
43
-//         success: true,
44
-//         message,
45
-//         data: formattedData
46
-//     });
47
-// };
28
+    const { created_by, ...cleanedData } = data;
48 29
 
49
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
30
+    const formatted = {
31
+        ...formatItem(cleanedData),
32
+        note_tags,
33
+    };
50 34
 
51
-// const formatItem = (item) => ({
52
-//     ...item,
53
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
54
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt)
55
-// });
56
-
57
-// exports.HospitalResource = (res, data, message = 'Success') => {
58
-//     const formattedData = formatItem(data);
59
-
60
-//     return res.status(200).json({
61
-//         success: true,
62
-//         message,
63
-//         data: formattedData
64
-//     });
65
-// };
35
+    return res.status(200).json({
36
+        success: true,
37
+        message,
38
+        data: formatted,
39
+    });
40
+};

+ 0 - 22
src/resources/admin/sales/SalesCollection.js

@@ -1,22 +0,0 @@
1
-const { ListResponse } = require("../../../utils/ListResponse");
2
-const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
-
4
-const formatItem = (item) => ({
5
-    ...item,
6
-    createdAt: formatISOWithoutTimezone(item.createdAt),
7
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
8
-});
9
-
10
-exports.SalesCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
11
-    const formattedData = data.map(formatItem);
12
-
13
-    if (typeof total !== 'number') {
14
-        return res.status(200).json({
15
-            success: true,
16
-            message,
17
-            data: Array.isArray(formattedData)
18
-        });
19
-    }
20
-
21
-    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
22
-};

+ 0 - 17
src/resources/admin/sales/SalesResource.js

@@ -1,17 +0,0 @@
1
-const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
2
-
3
-const formatItem = (item) => ({
4
-    ...item,
5
-    createdAt: formatISOWithoutTimezone(item.createdAt),
6
-    updatedAt: formatISOWithoutTimezone(item.updatedAt)
7
-});
8
-
9
-exports.SalesResource = (res, data, message = 'Success') => {
10
-    const formattedData = formatItem(data);
11
-
12
-    return res.status(200).json({
13
-        success: true,
14
-        message,
15
-        data: formattedData
16
-    });
17
-};

+ 42 - 108
src/resources/admin/status_history/StatusHistoryCollection.ts

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3 3
 import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4 4
 import { StatusHistoryDTO } from '../../../types/admin/status_history/StatusHistoryDTO';
5
+import prisma from '../../../prisma/PrismaClient';
5 6
 
6 7
 const formatItem = (item: StatusHistoryDTO) => ({
7 8
     ...item,
@@ -9,8 +10,46 @@ const formatItem = (item: StatusHistoryDTO) => ({
9 10
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
10 11
 });
11 12
 
12
-export const StatusHistoryCollection = (req: Request, res: Response, data: StatusHistoryDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
13
-    const formattedData = data.map(formatItem);
13
+export const StatusHistoryCollection = async (req: Request, res: Response, data: StatusHistoryDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Promise<Response> => {
14
+    const ids = data.map(item => item.id);
15
+
16
+    // Ambil tags dari category_links yang relevan
17
+    const allTags = await prisma.categoryLink.findMany({
18
+        where: {
19
+            source_id: { in: ids },
20
+            source_type: { in: ['status_history_notes'] },
21
+            deletedAt: null,
22
+        },
23
+        include: {
24
+            Category: true,
25
+        },
26
+    });
27
+
28
+    // Kelompokkan tags berdasarkan source_type dan source_id
29
+    const tagMap = new Map<string, { note: string[]; }>();
30
+    for (const id of ids) {
31
+        tagMap.set(id, { note: [] });
32
+    }
33
+
34
+    for (const tag of allTags) {
35
+        const id = tag.source_id!;
36
+        const categoryTag = tag.Category?.tag ?? '';
37
+        const current = tagMap.get(id);
38
+        if (!current) continue;
39
+
40
+        if (tag.source_type === 'status_history_notes') {
41
+            current.note.push(categoryTag);
42
+        }
43
+    }
44
+
45
+    // Gabungkan dan format
46
+    const formattedData = data.map(item => {
47
+        const tags = tagMap.get(item.id);
48
+        return formatItem({
49
+            ...item,
50
+            note_tags: tags?.note ?? [],
51
+        });
52
+    });
14 53
 
15 54
     if (typeof total !== 'number') {
16 55
         return res.status(200).json({
@@ -29,109 +68,4 @@ export const StatusHistoryCollection = (req: Request, res: Response, data: Statu
29 68
         limit,
30 69
         message,
31 70
     });
32
-};
33
-
34
-// import { ListResponse } from "../../../utils/ListResponse";
35
-// import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
36
-// import { getUserNameById } from "../../../utils/CheckUserKeycloak";
37
-
38
-// // interface StatusHistory {
39
-// //     id: string;
40
-// //     hospital_id: string;
41
-// //     user_id: string;
42
-// //     old_status: 'cari_data' | 'dihubungi' | 'negosiasi' | 'follow_up' | 'mou' | 'onboarded' | 'tidak_berminat';
43
-// //     new_status: 'cari_data' | 'dihubungi' | 'negosiasi' | 'follow_up' | 'mou' | 'onboarded' | 'tidak_berminat';
44
-// //     note?: string | null;
45
-// //     createdAt: string | Date | null;
46
-// //     updatedAt: string | Date | null;
47
-// // }
48
-
49
-// interface StatusHistoryWithRelations {
50
-//     id: string;
51
-//     old_status: string;
52
-//     new_status: string;
53
-//     note: string | null;
54
-//     createdAt: Date | string | null;
55
-//     updatedAt: Date | string | null;
56
-//     hospital: {
57
-//         id: string;
58
-//         name: string;
59
-//         progress_status: string;
60
-//     };
61
-//     user: {
62
-//         id: string;
63
-//         fullname: string;
64
-//     };
65
-// }
66
-
67
-
68
-// const transformStatusHistoryList = async (data: StatusHistoryWithRelations[] = []): Promise<StatusHistoryWithRelations[]> => {
69
-//     return Promise.all(
70
-//         data.map(async (rest): Promise<StatusHistoryWithRelations> => {
71
-//             // const name = await getUserNameById(rest.user_id);
72
-
73
-//             return {
74
-//                 ...rest,
75
-//                 // user: {
76
-//                 //   id: rest.user_id,
77
-//                 //   name,
78
-//                 // },
79
-//                 note: rest.note?.trim() === '' ? null : rest.note ?? null,
80
-//                 createdAt: formatISOWithoutTimezone(rest.createdAt),
81
-//                 updatedAt: formatISOWithoutTimezone(rest.updatedAt),
82
-//             };
83
-//         })
84
-//     );
85
-// };
86
-
87
-// interface StatusHistoryCollectionParams {
88
-//     req: any;
89
-//     res: any;
90
-//     data?: StatusHistoryWithRelations[];
91
-//     total?: number;
92
-//     page?: number;
93
-//     limit?: number;
94
-//     message?: string;
95
-// }
96
-
97
-// export const StatusHistoryCollection = async ({
98
-//     req,
99
-//     res,
100
-//     data = [],
101
-//     total = 0,
102
-//     page = 1,
103
-//     limit = 10,
104
-//     message = 'Success',
105
-// }: StatusHistoryCollectionParams) => {
106
-//     const formatted = await transformStatusHistoryList(data);
107
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
108
-// };
109
-
110
-
111
-// const { ListResponse } = require("../../../utils/ListResponse");
112
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
113
-// const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
114
-
115
-// const transformStatusHistoryList = async (data = []) => {
116
-//     return Promise.all(data.map(async ({ ...rest }) => {
117
-//         // const name = await getUserNameById(user_id);
118
-
119
-//         return {
120
-//             ...rest,
121
-//             // user: {
122
-//             //     id: user_id,
123
-//             //     name: name
124
-//             // },
125
-//             note: rest.note?.trim() === '' ? null : rest.note,
126
-//             createdAt: formatISOWithoutTimezone(rest.createdAt),
127
-//             updatedAt: formatISOWithoutTimezone(rest.updatedAt)
128
-//         };
129
-//     }));
130
-// };
131
-
132
-// // Collection yang async
133
-// exports.StatusHistoryCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
134
-//     const formatted = await transformStatusHistoryList(data);
135
-
136
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
137
-// };
71
+};

+ 64 - 82
src/resources/admin/vendor/VendorCollection.ts

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3 3
 import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4 4
 import { VendorDTO } from '../../../types/admin/vendor/VendorDTO';
5
+import prisma from '../../../prisma/PrismaClient';
5 6
 
6 7
 const formatItem = (item: VendorDTO) => ({
7 8
     ...item,
@@ -9,8 +10,49 @@ const formatItem = (item: VendorDTO) => ({
9 10
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
10 11
 });
11 12
 
12
-export const VendorCollection = (req: Request, res: Response, data: VendorDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
13
-    const formattedData = data.map(formatItem);
13
+export const VendorCollection = async (req: Request, res: Response, data: VendorDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Promise<Response> => {
14
+    const ids = data.map(item => item.id);
15
+
16
+    // Ambil tags dari category_links yang relevan
17
+    const allTags = await prisma.categoryLink.findMany({
18
+        where: {
19
+            source_id: { in: ids },
20
+            source_type: { in: ['vendor_strength_notes', 'vendor_weaknesses_notes'] },
21
+            deletedAt: null,
22
+        },
23
+        include: {
24
+            Category: true,
25
+        },
26
+    });
27
+
28
+    // Kelompokkan tags berdasarkan source_type dan source_id
29
+    const tagMap = new Map<string, { strength: string[]; weaknesses: string[] }>();
30
+    for (const id of ids) {
31
+        tagMap.set(id, { strength: [], weaknesses: [] });
32
+    }
33
+
34
+    for (const tag of allTags) {
35
+        const id = tag.source_id!;
36
+        const categoryTag = tag.Category?.tag ?? '';
37
+        const current = tagMap.get(id);
38
+        if (!current) continue;
39
+
40
+        if (tag.source_type === 'vendor_strength_notes') {
41
+            current.strength.push(categoryTag);
42
+        } else if (tag.source_type === 'vendor_weaknesses_notes') {
43
+            current.weaknesses.push(categoryTag);
44
+        }
45
+    }
46
+
47
+    // Gabungkan dan format
48
+    const formattedData = data.map(item => {
49
+        const tags = tagMap.get(item.id);
50
+        return formatItem({
51
+            ...item,
52
+            strengths_tags: tags?.strength ?? [],
53
+            weaknesses_tags: tags?.weaknesses ?? [],
54
+        });
55
+    });
14 56
 
15 57
     if (typeof total !== 'number') {
16 58
         return res.status(200).json({
@@ -29,84 +71,24 @@ export const VendorCollection = (req: Request, res: Response, data: VendorDTO[]
29 71
         limit,
30 72
         message,
31 73
     });
32
-};
33
-
34
-// import { ListResponse } from "../../../utils/ListResponse";
35
-// import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
36
-// import { getUserNameById } from "../../../utils/CheckUserKeycloak";
37
-
38
-// // Jika tahu struktur data vendor, ganti `any` dengan interface Vendor
39
-// interface Vendor {
40
-//     createdAt: Date | string | null;
41
-//     updatedAt: Date | string | null;
42
-//     [key: string]: any; // untuk properti lainnya
43
-// }
44
-
45
-// const transformVendorList = async (data: Vendor[] = []): Promise<Vendor[]> => {
46
-//     return Promise.all(
47
-//         data.map(async ({ ...rest }) => {
48
-//             // const name = await getUserNameById(rest.created_by);
49
-
50
-//             return {
51
-//                 ...rest,
52
-//                 // user: {
53
-//                 //     id: rest.created_by,
54
-//                 //     name: name
55
-//                 // },
56
-//                 createdAt: formatISOWithoutTimezone(rest.createdAt),
57
-//                 updatedAt: formatISOWithoutTimezone(rest.updatedAt),
58
-//             };
59
-//         })
60
-//     );
61
-// };
62
-
63
-// interface VendorCollectionParams {
64
-//     req: any;
65
-//     res: any;
66
-//     data?: Vendor[];
67
-//     total?: number;
68
-//     page?: number;
69
-//     limit?: number;
70
-//     message?: string;
71
-// }
72
-
73
-// export const VendorCollection = async ({
74
-//     req,
75
-//     res,
76
-//     data = [],
77
-//     total = 0,
78
-//     page = 1,
79
-//     limit = 10,
80
-//     message = "Success",
81
-// }: VendorCollectionParams): Promise<any> => {
82
-//     const formatted = await transformVendorList(data);
83
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
84
-// };
85
-
86
-
87
-// const { ListResponse } = require("../../../utils/ListResponse");
88
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
89
-// const { getUserNameById } = require("../../../utils/CheckUserKeycloak");
90
-
91
-// const transformVendorList = async (data = []) => {
92
-//     return Promise.all(data.map(async ({ ...rest }) => {
93
-//         // const name = await getUserNameById(created_by);
94
-
95
-//         return {
96
-//             ...rest,
97
-//             // user: {
98
-//             //     id: created_by,
99
-//             //     name: name
100
-//             // },
101
-//             createdAt: formatISOWithoutTimezone(rest.createdAt),
102
-//             updatedAt: formatISOWithoutTimezone(rest.updatedAt)
103
-//         };
104
-//     }));
105
-// };
106
-
107
-// // Collection yang async
108
-// exports.VendorCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
109
-//     const formatted = await transformVendorList(data);
110 74
 
111
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
112
-// };
75
+    // const formattedData = data.map(formatItem);
76
+
77
+    // if (typeof total !== 'number') {
78
+    //     return res.status(200).json({
79
+    //         success: true,
80
+    //         message,
81
+    //         data: Array.isArray(formattedData),
82
+    //     });
83
+    // }
84
+
85
+    // return ListResponse({
86
+    //     req,
87
+    //     res,
88
+    //     data: formattedData,
89
+    //     total,
90
+    //     page,
91
+    //     limit,
92
+    //     message,
93
+    // });
94
+};

+ 30 - 74
src/resources/admin/vendor/VendorResource.ts

@@ -1,85 +1,41 @@
1 1
 import { Response } from 'express';
2 2
 import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
3 3
 import { VendorDTO } from '../../../types/admin/vendor/VendorDTO';
4
+import prisma from '../../../prisma/PrismaClient';
5
+
6
+export const VendorResource = async (res: Response, data: VendorDTO, message: string = 'Success'): Promise<Response> => {
7
+    const tags = await prisma.categoryLink.findMany({
8
+        where: {
9
+            source_id: data.id,
10
+            source_type: { in: ['vendor_strength_notes', 'vendor_weaknesses_notes'] },
11
+            deletedAt: null,
12
+        },
13
+        include: {
14
+            Category: true,
15
+        },
16
+    });
17
+
18
+    const strengths_tags = tags
19
+        .filter(t => t.source_type === 'vendor_strength_notes')
20
+        .map(t => t.Category?.tag ?? '');
4 21
 
5
-const formatItem = (item: VendorDTO) => ({
6
-    ...item,
7
-    createdAt: formatISOWithoutTimezone(item.createdAt),
8
-    updatedAt: formatISOWithoutTimezone(item.updatedAt),
9
-});
22
+    const weaknesses_tags = tags
23
+        .filter(t => t.source_type === 'vendor_weaknesses_notes')
24
+        .map(t => t.Category?.tag ?? '');
10 25
 
11
-export const VendorResource = (res: Response, data: VendorDTO, message: string = 'Success'): Response => {
12
-    const formattedData = formatItem(data);
26
+    const formatted = {
27
+        ...data,
28
+        createdAt: formatISOWithoutTimezone(data.createdAt),
29
+        updatedAt: formatISOWithoutTimezone(data.updatedAt),
30
+        strengths: data.strengths,
31
+        weaknesses: data.weaknesses,
32
+        strengths_tags,
33
+        weaknesses_tags,
34
+    };
13 35
 
14 36
     return res.status(200).json({
15 37
         success: true,
16 38
         message,
17
-        data: formattedData,
39
+        data: formatted,
18 40
     });
19 41
 };
20
-
21
-// import { Response } from "express";
22
-// import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
23
-// import { VendorDTO } from '../../../types/admin/vendor/VendorDTO';
24
-
25
-// // Kalau ada struktur pasti dari vendor, ganti `any` dengan tipe interface
26
-// interface Vendor {
27
-//     createdAt: Date | string | null;
28
-//     updatedAt: Date | string | null;
29
-//     [key: string]: any;
30
-// }
31
-
32
-// const formatItem = ({ item }: { item: Vendor }): Vendor => {
33
-//     const { ...rest } = item;
34
-
35
-//     return {
36
-//         ...rest,
37
-//         // user: {
38
-//         //     id: item.created_by,
39
-//         //     name: userName
40
-//         // },
41
-//         createdAt: formatISOWithoutTimezone(item.createdAt),
42
-//         updatedAt: formatISOWithoutTimezone(item.updatedAt)
43
-//     };
44
-// };
45
-
46
-// export const VendorResource = (
47
-//     res: Response,
48
-//     item: Vendor,
49
-//     message = "Success"
50
-// ): Response => {
51
-//     const formattedData = formatItem({ item });
52
-
53
-//     return res.status(200).json({
54
-//         success: true,
55
-//         message,
56
-//         data: formattedData
57
-//     });
58
-// };
59
-
60
-
61
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
62
-
63
-// const formatItem = ({ item }) => {
64
-//     const { ...rest } = item;
65
-
66
-//     return {
67
-//         ...rest,
68
-//         // user: {
69
-//         //     id: created_by,
70
-//         //     name: userName
71
-//         // },
72
-//         createdAt: formatISOWithoutTimezone(item.createdAt),
73
-//         updatedAt: formatISOWithoutTimezone(item.updatedAt)
74
-//     };
75
-// };
76
-
77
-// exports.VendorResource = (res, item, message = 'Success') => {
78
-//     const formattedData = formatItem({ item });
79
-
80
-//     return res.status(200).json({
81
-//         success: true,
82
-//         message,
83
-//         data: formattedData
84
-//     });
85
-// };

+ 53 - 67
src/resources/admin/vendor_experience/VendorExperienceCollection.ts

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3 3
 import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
4 4
 import { VendorExperienceDTO } from '../../../types/admin/vendor_experience/VendorExperienceDTO';
5
+import prisma from '../../../prisma/PrismaClient';
5 6
 
6 7
 const formatItem = (item: VendorExperienceDTO) => ({
7 8
     ...item,
@@ -13,8 +14,57 @@ const formatItem = (item: VendorExperienceDTO) => ({
13 14
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
14 15
 });
15 16
 
16
-export const VendorExperienceCollection = (req: Request, res: Response, data: VendorExperienceDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
17
-    const formattedData = data.map(formatItem);
17
+export const VendorExperienceCollection = async (
18
+    req: Request,
19
+    res: Response,
20
+    data: VendorExperienceDTO[] = [],
21
+    total: number | null = null,
22
+    page: number = 1,
23
+    limit: number = 10,
24
+    message: string = 'Success'
25
+): Promise<Response> => {
26
+    const ids = data.map(item => item.id);
27
+
28
+    // Ambil tags dari category_links yang relevan
29
+    const allTags = await prisma.categoryLink.findMany({
30
+        where: {
31
+            source_id: { in: ids },
32
+            source_type: { in: ['vendor_experience_positive_notes', 'vendor_experience_negative_notes'] },
33
+            deletedAt: null,
34
+        },
35
+        include: {
36
+            Category: true,
37
+        },
38
+    });
39
+
40
+    // Kelompokkan tags berdasarkan source_type dan source_id
41
+    const tagMap = new Map<string, { positive: string[]; negative: string[] }>();
42
+    for (const id of ids) {
43
+        tagMap.set(id, { positive: [], negative: [] });
44
+    }
45
+
46
+    for (const tag of allTags) {
47
+        const id = tag.source_id!;
48
+        const categoryTag = tag.Category?.tag ?? '';
49
+        const current = tagMap.get(id);
50
+        if (!current) continue;
51
+
52
+        if (tag.source_type === 'vendor_experience_positive_notes') {
53
+            current.positive.push(categoryTag);
54
+        } else if (tag.source_type === 'vendor_experience_negative_notes') {
55
+            current.negative.push(categoryTag);
56
+        }
57
+    }
58
+
59
+    // Gabungkan dan format
60
+    const formattedData = data.map(item => {
61
+        const tags = tagMap.get(item.id);
62
+        return formatItem({
63
+            ...item,
64
+            positive_notes_tags: tags?.positive ?? [],
65
+            negative_notes_tags: tags?.negative ?? [],
66
+        });
67
+    });
18 68
 
19 69
     if (typeof total !== 'number') {
20 70
         return res.status(200).json({
@@ -33,68 +83,4 @@ export const VendorExperienceCollection = (req: Request, res: Response, data: Ve
33 83
         limit,
34 84
         message,
35 85
     });
36
-};
37
-
38
-// import { Request, Response } from 'express';
39
-// import { ListResponse } from '../../../utils/ListResponse';
40
-// import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
41
-// import { VendorHistoryDTO } from '../../../types/VendorHistoryDTO';
42
-
43
-// const formatItem = (item: VendorHistoryDTO): VendorHistoryDTO => ({
44
-//     ...item,
45
-//     contract_value_min: item.contract_value_min !== null ? Number(item.contract_value_min) : null,
46
-//     contract_value_max: item.contract_value_max !== null ? Number(item.contract_value_max) : null,
47
-//     contract_start_date: formatDateOnly(item.contract_start_date),
48
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
49
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
50
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
51
-// });
52
-
53
-// export const VendorExperienceCollection = (
54
-//     req: Request,
55
-//     res: Response,
56
-//     data: any[] = [],
57
-//     total: number | null = null,
58
-//     page: number = 1,
59
-//     limit: number = 10,
60
-//     message: string = 'Success'
61
-// ): Response => {
62
-//     const formattedData: VendorHistoryDTO[] = data.map(formatItem);
63
-
64
-//     if (typeof total !== 'number') {
65
-//         return res.status(200).json({
66
-//             success: true,
67
-//             message,
68
-//             data: Array.isArray(formattedData),
69
-//         });
70
-//     }
71
-
72
-//     return ListResponse({ req, res, data: formattedData, total, page, limit, message });
73
-// };
74
-
75
-// const { ListResponse } = require("../../../utils/ListResponse.js");
76
-// const { formatISOWithoutTimezone, formatDateOnly } = require("../../../utils/FormatDate.js");
77
-
78
-// const formatItem = (item) => ({
79
-//     ...item,
80
-//     contract_value_min: item.contract_value_min !== null ? Number(item.contract_value_min) : null,
81
-//     contract_value_max: item.contract_value_max !== null ? Number(item.contract_value_max) : null,
82
-//     contract_start_date: formatDateOnly(item.contract_start_date),
83
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
84
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
85
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
86
-// });
87
-
88
-// exports.VendorExperienceCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
89
-//     const formattedData = data.map(formatItem);
90
-
91
-//     if (typeof total !== 'number') {
92
-//         return res.status(200).json({
93
-//             success: true,
94
-//             message,
95
-//             data: Array.isArray(formattedData)
96
-//         });
97
-//     }
98
-
99
-//     return ListResponse({ req, res, data: formattedData, total, page, limit, message });
100
-// };
86
+};

+ 24 - 73
src/resources/admin/vendor_experience/VendorExperienceResource.ts

@@ -1,8 +1,28 @@
1 1
 import { Response } from 'express';
2 2
 import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
3 3
 import { VendorExperienceDTO } from '../../../types/admin/vendor_experience/VendorExperienceDTO';
4
+import prisma from '../../../prisma/PrismaClient';
5
+
6
+export const VendorExperienceResource = async (res: Response, data: VendorExperienceDTO, message: string = 'Success'): Promise<Response> => {
7
+    const tags = await prisma.categoryLink.findMany({
8
+        where: {
9
+            source_id: data.id,
10
+            source_type: { in: ['vendor_experience_positive_notes', 'vendor_experience_negative_notes'] },
11
+            deletedAt: null,
12
+        },
13
+        include: {
14
+            Category: true,
15
+        },
16
+    });
17
+
18
+    const positive_notes_tags = tags
19
+        .filter(t => t.source_type === 'vendor_experience_positive_notes')
20
+        .map(t => t.Category?.tag ?? '');
21
+
22
+    const negative_notes_tags = tags
23
+        .filter(t => t.source_type === 'vendor_experience_negative_notes')
24
+        .map(t => t.Category?.tag ?? '');
4 25
 
5
-export const VendorExperienceResource = (res: Response, data: VendorExperienceDTO, message: string = 'Success'): Response => {
6 26
     const { vendor_id, ...restData } = data;
7 27
 
8 28
     const formatted = {
@@ -20,10 +40,10 @@ export const VendorExperienceResource = (res: Response, data: VendorExperienceDT
20 40
             : null,
21 41
         contract_start_date: formatDateOnly(data.contract_start_date),
22 42
         contract_expired_date: formatDateOnly(data.contract_expired_date),
23
-        // contract_value_min: Number(data.contract_value_min),
24
-        // contract_value_max: Number(data.contract_value_max),
25 43
         contract_value_min: data.contract_value_min !== null ? Number(data.contract_value_min) : null,
26 44
         contract_value_max: data.contract_value_max !== null ? Number(data.contract_value_max) : null,
45
+        positive_notes_tags,
46
+        negative_notes_tags,
27 47
         createdAt: formatISOWithoutTimezone(data.createdAt),
28 48
         updatedAt: formatISOWithoutTimezone(data.updatedAt),
29 49
     };
@@ -33,73 +53,4 @@ export const VendorExperienceResource = (res: Response, data: VendorExperienceDT
33 53
         message,
34 54
         data: formatted,
35 55
     });
36
-};
37
-
38
-// src/resources/VendorExperienceResource.ts
39
-// import { Response } from 'express';
40
-// import { VendorHistoryDTO } from '../../../types/VendorHistoryDTO';
41
-// import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
42
-
43
-// export const formatVendorExperience = (item: VendorHistoryDTO): VendorHistoryDTO => ({
44
-//     ...item,
45
-//     contract_value_min: item.contract_value_min !== null ? Number(item.contract_value_min) : null,
46
-//     contract_value_max: item.contract_value_max !== null ? Number(item.contract_value_max) : null,
47
-//     contract_start_date: formatDateOnly(item.contract_start_date),
48
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
49
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
50
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
51
-// });
52
-
53
-// export const VendorExperienceResource = (res: Response, data: any, message: string) => {
54
-//     const { vendor_id, ...restData } = data;
55
-
56
-//     const formatted = {
57
-//         ...restData,
58
-//         vendor: data.vendor
59
-//             ? {
60
-//                 id: data.vendor.id,
61
-//                 name: data.vendor.name,
62
-//                 name_pt: data.vendor.name_pt,
63
-//                 strengths: data.vendor.strengths,
64
-//                 weaknesses: data.vendor.weaknesses,
65
-//                 website: data.vendor.website,
66
-//                 created_by: data.vendor.created_by,
67
-//             }
68
-//             : null,
69
-//         contract_start_date: formatDateOnly(data.contract_start_date),
70
-//         contract_expired_date: formatDateOnly(data.contract_expired_date),
71
-//         contract_value_min: Number(data.contract_value_min),
72
-//         contract_value_max: Number(data.contract_value_max),
73
-//         createdAt: formatISOWithoutTimezone(data.createdAt),
74
-//         updatedAt: formatISOWithoutTimezone(data.updatedAt),
75
-//     };
76
-
77
-//     return res.status(200).json({
78
-//         success: true,
79
-//         message,
80
-//         data: formatted,
81
-//     });
82
-// };
83
-
84
-
85
-// const { formatISOWithoutTimezone, formatDateOnly } = require("../../../utils/FormatDate");
86
-
87
-// const formatItem = (item) => ({
88
-//     ...item,
89
-//     contract_value_min: item.contract_value_min !== null ? Number(item.contract_value_min) : null,
90
-//     contract_value_max: item.contract_value_max !== null ? Number(item.contract_value_max) : null,
91
-//     contract_start_date: formatDateOnly(item.contract_start_date),
92
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
93
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
94
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
95
-// });
96
-
97
-// exports.VendorExperienceResource = (res, data, message = 'Success') => {
98
-//     const formattedData = formatItem(data);
99
-
100
-//     return res.status(200).json({
101
-//         success: true,
102
-//         message,
103
-//         data: formattedData
104
-//     });
105
-// };
56
+};

+ 1 - 75
src/resources/sales/area/UserAreaCollection.ts

@@ -29,78 +29,4 @@ export const UserAreaCollection = (req: Request, res: Response, data: AreaSalesD
29 29
         limit,
30 30
         message,
31 31
     });
32
-};
33
-
34
-// import { Request, Response } from 'express';
35
-// import { ListResponse } from '../../../utils/ListResponse';
36
-// import { getUserNameById } from '../../../utils/CheckUserKeycloak';
37
-// import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
38
-
39
-// interface Province {
40
-//     id: string;
41
-//     name: string;
42
-// }
43
-
44
-// interface Area {
45
-//     id: string;
46
-//     province: Province;
47
-//     createdAt: string | Date | null;
48
-//     updatedAt: string | Date | null;
49
-// }
50
-
51
-// interface UserAreaCollectionParams {
52
-//     req: Request;
53
-//     res: Response;
54
-//     data?: Area[];
55
-//     total?: number;
56
-//     page?: number;
57
-//     limit?: number;
58
-//     message?: string;
59
-// }
60
-
61
-// const transformArea = async (data: Area[] = []): Promise<Area[]> => {
62
-//     return Promise.all(data.map(async (item) => {
63
-//         return {
64
-//             ...item,
65
-//             createdAt: formatISOWithoutTimezone(item.createdAt),
66
-//             updatedAt: formatISOWithoutTimezone(item.updatedAt),
67
-//         };
68
-//     }));
69
-// };
70
-
71
-// export const UserAreaCollection = async ({
72
-//     req,
73
-//     res,
74
-//     data = [],
75
-//     total = 0,
76
-//     page = 1,
77
-//     limit = 10,
78
-//     message = 'Success',
79
-// }: UserAreaCollectionParams): Promise<Response> => {
80
-//     const formatted = await transformArea(data);
81
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
82
-// };
83
-
84
-
85
-// const { getUserNameById } = require("../../../utils/CheckUserKeycloak");
86
-// const { ListResponse } = require("../../../utils/ListResponse");
87
-
88
-// const transformArea = async (data = []) => {
89
-//     return Promise.all(data.map(async ({ ...rest }) => {
90
-//         // const name = await getUserNameById(user_id);
91
-
92
-//         return {
93
-//             ...rest,
94
-//             // user: {
95
-//             //     id: user_id,
96
-//             //     name: name
97
-//             // },
98
-//         };
99
-//     }));
100
-// };
101
-
102
-// exports.UserAreaCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
103
-//     const formatted = await transformArea(data);
104
-
105
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
106
-// };
32
+};

+ 1 - 27
src/resources/sales/executives_history/ExecutivesHistoriCollection.ts

@@ -31,30 +31,4 @@ export const ExecutivesHistoriCollection = (
31 31
     }
32 32
 
33 33
     return ListResponse({ req, res, data: formattedData, total, page, limit, message });
34
-};
35
-
36
-
37
-// const { ListResponse } = require("../../../utils/ListResponse");
38
-// const { formatISOWithoutTimezone, formatDateOnly } = require("../../../utils/FormatDate.js");
39
-
40
-// const formatItem = (item) => ({
41
-//     ...item,
42
-//     start_term: formatDateOnly(item.start_term),
43
-//     end_term: formatDateOnly(item.end_term),
44
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
45
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
46
-// });
47
-
48
-// exports.ExecutivesHistoriCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
49
-//     const formattedData = data.map(formatItem);
50
-
51
-//     if (typeof total !== 'number') {
52
-//         return res.status(200).json({
53
-//             success: true,
54
-//             message,
55
-//             data: Array.isArray(formattedData)
56
-//         });
57
-//     }
58
-
59
-//     return ListResponse({ req, res, data: formattedData, total, page, limit, message });
60
-// };
34
+};

+ 1 - 21
src/resources/sales/executives_history/ExecutivesHistoriResource.ts

@@ -22,24 +22,4 @@ export const ExecutivesHistoriResource = (
22 22
         message,
23 23
         data: formattedData
24 24
     });
25
-};
26
-
27
-// const { formatISOWithoutTimezone, formatDateOnly } = require("../../../utils/FormatDate");
28
-
29
-// const formatItem = (item) => ({
30
-//     ...item,
31
-//     start_term: formatDateOnly(item.start_term),
32
-//     end_term: formatDateOnly(item.end_term),
33
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
34
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
35
-// });
36
-
37
-// exports.ExecutivesHistoriResource = (res, data, message = 'Success') => {
38
-//     const formattedData = formatItem(data);
39
-
40
-//     return res.status(200).json({
41
-//         success: true,
42
-//         message,
43
-//         data: formattedData
44
-//     });
45
-// };
25
+};

+ 58 - 66
src/resources/sales/hospital/HospitalCollection.ts

@@ -1,38 +1,15 @@
1
-import { Request, Response } from "express";
2
-import { ListResponse } from "../../../utils/ListResponse";
3
-import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
4
-import { getUserNameById } from "../../../utils/CheckUserKeycloak";
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import prisma from '../../../prisma/PrismaClient';
5
+import { HospitalDTO } from '../../../types/sales/hospital/HospitalDTO';
5 6
 
6
-// Tipe untuk item rumah sakit
7
-interface HospitalItem {
8
-    id: string;
9
-    name: string;
10
-    created_by?: string;
11
-    createdAt: Date | string;
12
-    updatedAt: Date | string;
13
-    [key: string]: any;
14
-}
7
+const formatItem = (item: HospitalDTO) => ({
8
+    ...item,
9
+    createdAt: formatISOWithoutTimezone(item.createdAt),
10
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
11
+});
15 12
 
16
-// Fungsi transform per item
17
-const transformHospitalList = async (data: HospitalItem[] = []): Promise<any[]> => {
18
-    return Promise.all(
19
-        data.map(async ({ created_by, ...rest }) => {
20
-            // const name = await getUserNameById(created_by);
21
-
22
-            return {
23
-                ...rest,
24
-                // user: {
25
-                //   id: created_by,
26
-                //   name: name,
27
-                // },
28
-                createdAt: formatISOWithoutTimezone(rest.createdAt),
29
-                updatedAt: formatISOWithoutTimezone(rest.updatedAt),
30
-            };
31
-        })
32
-    );
33
-};
34
-
35
-// Collection yang async
36 13
 export const HospitalCollection = async ({
37 14
     req,
38 15
     res,
@@ -44,50 +21,65 @@ export const HospitalCollection = async ({
44 21
 }: {
45 22
     req: Request;
46 23
     res: Response;
47
-    data: HospitalItem[];
24
+    data: HospitalDTO[];
48 25
     total: number;
49 26
     page: number;
50 27
     limit: number;
51 28
     message?: string;
52 29
 }) => {
53
-    const formatted = await transformHospitalList(data);
30
+    const ids = data.map(item => item.id);
31
+
32
+    // Ambil tags dari category_links yang relevan
33
+    const allTags = await prisma.categoryLink.findMany({
34
+        where: {
35
+            source_id: { in: ids },
36
+            source_type: 'hospital_notes',
37
+            deletedAt: null,
38
+        },
39
+        include: {
40
+            Category: true,
41
+        },
42
+    });
43
+
44
+    // Kelompokkan tags berdasarkan source_id
45
+    const tagMap = new Map<string, { note: string[] }>();
46
+    for (const id of ids) {
47
+        tagMap.set(id, { note: [] });
48
+    }
49
+
50
+    for (const tag of allTags) {
51
+        const id = tag.source_id!;
52
+        const categoryTag = tag.Category?.tag ?? '';
53
+        const current = tagMap.get(id);
54
+        if (!current) continue;
55
+
56
+        current.note.push(categoryTag);
57
+    }
58
+
59
+    const formattedData = data.map(item => {
60
+        const { created_by, ...rest } = item;
61
+        const tags = tagMap.get(item.id);
62
+        return formatItem({
63
+            ...rest,
64
+            note_tags: tags?.note ?? [],
65
+        });
66
+    });
67
+
68
+    if (typeof total !== 'number') {
69
+        return res.status(200).json({
70
+            success: true,
71
+            message,
72
+            data: Array.isArray(formattedData),
73
+        });
74
+    }
54 75
 
55 76
     return ListResponse({
56 77
         req,
57 78
         res,
58
-        data: formatted,
79
+        data: formattedData,
59 80
         total,
60 81
         page,
61 82
         limit,
62 83
         message,
63 84
     });
64
-};
65
-
66
-
67
-// const { ListResponse } = require("../../../utils/ListResponse");
68
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
69
-// const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
70
-
71
-// // Fungsi transform per item
72
-// const transformHospitalList = async (data = []) => {
73
-//     return Promise.all(data.map(async ({ created_by, ...rest }) => {
74
-//         // const name = await getUserNameById(created_by);
75
-
76
-//         return {
77
-//             ...rest,
78
-//             // user: {
79
-//             //     id: created_by,
80
-//             //     name: name
81
-//             // },
82
-//             createdAt: formatISOWithoutTimezone(rest.createdAt),
83
-//             updatedAt: formatISOWithoutTimezone(rest.updatedAt)
84
-//         };
85
-//     }));
86
-// };
87
-
88
-// // Collection yang async
89
-// exports.HospitalCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
90
-//     const formatted = await transformHospitalList(data);
91
-
92
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
93
-// };
85
+};

+ 31 - 43
src/resources/sales/hospital/HospitalResource.ts

@@ -1,52 +1,40 @@
1 1
 import { Response } from 'express';
2
-import { formatISOWithoutTimezone } from "../../../utils/FormatDate";
2
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
3
+import { HospitalDTO } from '../../../types/sales/hospital/HospitalDTO';
4
+import prisma from '../../../prisma/PrismaClient';
5
+
6
+const formatItem = (item: HospitalDTO) => ({
7
+    ...item,
8
+    createdAt: formatISOWithoutTimezone(item.createdAt),
9
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
10
+});
11
+
12
+export const HospitalResource = async (res: Response, data: HospitalDTO, message: string = 'Success'): Promise<Response> => {
13
+    const tags = await prisma.categoryLink.findMany({
14
+        where: {
15
+            source_id: data.id,
16
+            source_type: { in: ['hospital_notes'] },
17
+            deletedAt: null,
18
+        },
19
+        include: {
20
+            Category: true,
21
+        },
22
+    });
23
+
24
+    const note_tags = tags
25
+        .filter(t => t.source_type === 'hospital_notes')
26
+        .map(t => t.Category?.tag ?? '');
3 27
 
4
-interface HospitalItem {
5
-    createdAt: string | Date;
6
-    updatedAt: string | Date;
7
-    [key: string]: any;
8
-}
28
+    const { created_by, ...cleanedData } = data;
9 29
 
10
-export const HospitalResource = (
11
-    res: Response,
12
-    data: HospitalItem,
13
-    message: string = 'Success'
14
-) => {
15
-    const formattedData = {
16
-        ...data,
17
-        createdAt: formatISOWithoutTimezone(data.createdAt),
18
-        updatedAt: formatISOWithoutTimezone(data.updatedAt)
30
+    const formatted = {
31
+        ...formatItem(cleanedData),
32
+        note_tags,
19 33
     };
20 34
 
21 35
     return res.status(200).json({
22 36
         success: true,
23 37
         message,
24
-        data: formattedData
38
+        data: formatted,
25 39
     });
26
-};
27
-
28
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate");
29
-
30
-// const formatItem = ({ item }) => {
31
-//     const { created_by, ...rest } = item;
32
-
33
-//     return {
34
-//         ...rest,
35
-//         // user: {
36
-//         //     id: created_by,
37
-//         //     name: userName
38
-//         // },
39
-//         createdAt: formatISOWithoutTimezone(item.createdAt),
40
-//         updatedAt: formatISOWithoutTimezone(item.updatedAt)
41
-//     };
42
-// };
43
-
44
-// exports.HospitalResource = (res, item, message = 'Success') => {
45
-//     const formattedData = formatItem({ item });
46
-
47
-//     return res.status(200).json({
48
-//         success: true,
49
-//         message,
50
-//         data: formattedData
51
-//     });
52
-// };
40
+};

+ 42 - 31
src/resources/sales/status_history/StatusHistoryCollection.ts

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3 3
 import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4 4
 import { StatusHistoryDTO } from '../../../types/sales/status_history/StatusHistoryDTO';
5
+import prisma from '../../../prisma/PrismaClient';
5 6
 
6 7
 const formatItem = (item: StatusHistoryDTO) => ({
7 8
     ...item,
@@ -9,8 +10,46 @@ const formatItem = (item: StatusHistoryDTO) => ({
9 10
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
10 11
 });
11 12
 
12
-export const StatusHistoryCollection = (req: Request, res: Response, data: StatusHistoryDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
13
-    const formattedData = data.map(formatItem);
13
+export const StatusHistoryCollection = async (req: Request, res: Response, data: StatusHistoryDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Promise<Response> => {
14
+    const ids = data.map(item => item.id);
15
+
16
+    // Ambil tags dari category_links yang relevan
17
+    const allTags = await prisma.categoryLink.findMany({
18
+        where: {
19
+            source_id: { in: ids },
20
+            source_type: { in: ['status_history_notes'] },
21
+            deletedAt: null,
22
+        },
23
+        include: {
24
+            Category: true,
25
+        },
26
+    });
27
+
28
+    // Kelompokkan tags berdasarkan source_type dan source_id
29
+    const tagMap = new Map<string, { note: string[]; }>();
30
+    for (const id of ids) {
31
+        tagMap.set(id, { note: [] });
32
+    }
33
+
34
+    for (const tag of allTags) {
35
+        const id = tag.source_id!;
36
+        const categoryTag = tag.Category?.tag ?? '';
37
+        const current = tagMap.get(id);
38
+        if (!current) continue;
39
+
40
+        if (tag.source_type === 'status_history_notes') {
41
+            current.note.push(categoryTag);
42
+        }
43
+    }
44
+
45
+    // Gabungkan dan format
46
+    const formattedData = data.map(item => {
47
+        const tags = tagMap.get(item.id);
48
+        return formatItem({
49
+            ...item,
50
+            note_tags: tags?.note ?? [],
51
+        });
52
+    });
14 53
 
15 54
     if (typeof total !== 'number') {
16 55
         return res.status(200).json({
@@ -29,32 +68,4 @@ export const StatusHistoryCollection = (req: Request, res: Response, data: Statu
29 68
         limit,
30 69
         message,
31 70
     });
32
-};
33
-
34
-// const { ListResponse } = require("../../../utils/ListResponse");
35
-// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
36
-// const { getUserNameById } = require("../../../utils/CheckUserKeycloak.js");
37
-
38
-// const transformStatusHistoryList = async (data = []) => {
39
-//     return Promise.all(data.map(async ({ ...rest }) => {
40
-//         // const name = await getUserNameById(user_id);
41
-
42
-//         return {
43
-//             ...rest,
44
-//             // user: {
45
-//             //     id: user_id,
46
-//             //     name: name
47
-//             // },
48
-//             note: rest.note?.trim() === '' ? null : rest.note,
49
-//             createdAt: formatISOWithoutTimezone(rest.createdAt),
50
-//             updatedAt: formatISOWithoutTimezone(rest.updatedAt)
51
-//         };
52
-//     }));
53
-// };
54
-
55
-// // Collection yang async
56
-// exports.StatusHistoryCollection = async ({ req, res, data = [], total = 0, page = 1, limit = 10, message = 'Success' }) => {
57
-//     const formatted = await transformStatusHistoryList(data);
58
-
59
-//     return ListResponse({ req, res, data: formatted, total, page, limit, message });
60
-// };
71
+};

+ 0 - 22
src/resources/sales/vendor/VendorCollection.js

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

+ 54 - 29
src/resources/sales/vendor_experience/VendorExperienceCollection.ts

@@ -1,7 +1,8 @@
1 1
 import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3 3
 import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
-import { VendorExperienceDTO } from '../../../types/admin/vendor_experience/VendorExperienceDTO';
4
+import { VendorExperienceDTO } from '../../../types/sales/vendor_experience/VendorExperienceDTO';
5
+import prisma from '../../../prisma/PrismaClient';
5 6
 
6 7
 const formatItem = (item: VendorExperienceDTO) => ({
7 8
     ...item,
@@ -13,8 +14,57 @@ const formatItem = (item: VendorExperienceDTO) => ({
13 14
     updatedAt: formatISOWithoutTimezone(item.updatedAt),
14 15
 });
15 16
 
16
-export const VendorExperienceCollection = (req: Request, res: Response, data: VendorExperienceDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
17
-    const formattedData = data.map(formatItem);
17
+export const VendorExperienceCollection = async (
18
+    req: Request,
19
+    res: Response,
20
+    data: VendorExperienceDTO[] = [],
21
+    total: number | null = null,
22
+    page: number = 1,
23
+    limit: number = 10,
24
+    message: string = 'Success'
25
+): Promise<Response> => {
26
+    const ids = data.map(item => item.id);
27
+
28
+    // Ambil tags dari category_links yang relevan
29
+    const allTags = await prisma.categoryLink.findMany({
30
+        where: {
31
+            source_id: { in: ids },
32
+            source_type: { in: ['vendor_experience_positive_notes', 'vendor_experience_negative_notes'] },
33
+            deletedAt: null,
34
+        },
35
+        include: {
36
+            Category: true,
37
+        },
38
+    });
39
+
40
+    // Kelompokkan tags berdasarkan source_type dan source_id
41
+    const tagMap = new Map<string, { positive: string[]; negative: string[] }>();
42
+    for (const id of ids) {
43
+        tagMap.set(id, { positive: [], negative: [] });
44
+    }
45
+
46
+    for (const tag of allTags) {
47
+        const id = tag.source_id!;
48
+        const categoryTag = tag.Category?.tag ?? '';
49
+        const current = tagMap.get(id);
50
+        if (!current) continue;
51
+
52
+        if (tag.source_type === 'vendor_experience_positive_notes') {
53
+            current.positive.push(categoryTag);
54
+        } else if (tag.source_type === 'vendor_experience_negative_notes') {
55
+            current.negative.push(categoryTag);
56
+        }
57
+    }
58
+
59
+    // Gabungkan dan format
60
+    const formattedData = data.map(item => {
61
+        const tags = tagMap.get(item.id);
62
+        return formatItem({
63
+            ...item,
64
+            positive_notes_tags: tags?.positive ?? [],
65
+            negative_notes_tags: tags?.negative ?? [],
66
+        });
67
+    });
18 68
 
19 69
     if (typeof total !== 'number') {
20 70
         return res.status(200).json({
@@ -33,29 +83,4 @@ export const VendorExperienceCollection = (req: Request, res: Response, data: Ve
33 83
         limit,
34 84
         message,
35 85
     });
36
-};
37
-
38
-// const { ListResponse } = require("../../../utils/ListResponse");
39
-// const { formatISOWithoutTimezone, formatDateOnly } = require("../../../utils/FormatDate.js");
40
-
41
-// const formatItem = (item) => ({
42
-//     ...item,
43
-//     contract_date: formatDateOnly(item.contract_date),
44
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
45
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
46
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
47
-// });
48
-
49
-// exports.VendorHistoriCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
50
-//     const formattedData = data.map(formatItem);
51
-
52
-//     if (typeof total !== 'number') {
53
-//         return res.status(200).json({
54
-//             success: true,
55
-//             message,
56
-//             data: Array.isArray(formattedData)
57
-//         });
58
-//     }
59
-
60
-//     return ListResponse({ req, res, data: formattedData, total, page, limit, message });
61
-// };
86
+};

+ 24 - 34
src/resources/sales/vendor_experience/VendorExperienceResource.ts

@@ -1,18 +1,28 @@
1 1
 import { Response } from 'express';
2 2
 import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
3
-import { VendorExperienceDTO } from '../../../types/admin/vendor_experience/VendorExperienceDTO';
3
+import { VendorExperienceDTO } from '../../../types/sales/vendor_experience/VendorExperienceDTO';
4
+import prisma from '../../../prisma/PrismaClient';
5
+
6
+export const VendorExperienceResource = async (res: Response, data: VendorExperienceDTO, message: string = 'Success'): Promise<Response> => {
7
+    const tags = await prisma.categoryLink.findMany({
8
+        where: {
9
+            source_id: data.id,
10
+            source_type: { in: ['vendor_experience_positive_notes', 'vendor_experience_negative_notes'] },
11
+            deletedAt: null,
12
+        },
13
+        include: {
14
+            Category: true,
15
+        },
16
+    });
17
+
18
+    const positive_notes_tags = tags
19
+        .filter(t => t.source_type === 'vendor_experience_positive_notes')
20
+        .map(t => t.Category?.tag ?? '');
4 21
 
5
-// const formatItem = (item: VendorExperienceDTO) => ({
6
-//     ...item,
7
-//     contract_value_min: item.contract_value_min !== null ? Number(item.contract_value_min) : null,
8
-//     contract_value_max: item.contract_value_max !== null ? Number(item.contract_value_max) : null,
9
-//     contract_start_date: formatDateOnly(item.contract_start_date),
10
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
11
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
12
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
13
-// });
22
+    const negative_notes_tags = tags
23
+        .filter(t => t.source_type === 'vendor_experience_negative_notes')
24
+        .map(t => t.Category?.tag ?? '');
14 25
 
15
-export const VendorExperienceResource = (res: Response, data: VendorExperienceDTO, message: string = 'Success'): Response => {
16 26
     const { vendor_id, ...restData } = data;
17 27
 
18 28
     const formatted = {
@@ -30,10 +40,10 @@ export const VendorExperienceResource = (res: Response, data: VendorExperienceDT
30 40
             : null,
31 41
         contract_start_date: formatDateOnly(data.contract_start_date),
32 42
         contract_expired_date: formatDateOnly(data.contract_expired_date),
33
-        // contract_value_min: Number(data.contract_value_min),
34
-        // contract_value_max: Number(data.contract_value_max),
35 43
         contract_value_min: data.contract_value_min !== null ? Number(data.contract_value_min) : null,
36 44
         contract_value_max: data.contract_value_max !== null ? Number(data.contract_value_max) : null,
45
+        positive_notes_tags,
46
+        negative_notes_tags,
37 47
         createdAt: formatISOWithoutTimezone(data.createdAt),
38 48
         updatedAt: formatISOWithoutTimezone(data.updatedAt),
39 49
     };
@@ -43,24 +53,4 @@ export const VendorExperienceResource = (res: Response, data: VendorExperienceDT
43 53
         message,
44 54
         data: formatted,
45 55
     });
46
-};
47
-
48
-// const { formatISOWithoutTimezone, formatDateOnly } = require("../../../utils/FormatDate");
49
-
50
-// const formatItem = (item) => ({
51
-//     ...item,
52
-//     contract_date: formatDateOnly(item.contract_date),
53
-//     contract_expired_date: formatDateOnly(item.contract_expired_date),
54
-//     createdAt: formatISOWithoutTimezone(item.createdAt),
55
-//     updatedAt: formatISOWithoutTimezone(item.updatedAt),
56
-// });
57
-
58
-// exports.VendorExperienceResource = (res, data, message = 'Success') => {
59
-//     const formattedData = formatItem(data);
60
-
61
-//     return res.status(200).json({
62
-//         success: true,
63
-//         message,
64
-//         data: formattedData
65
-//     });
66
-// };
56
+};

+ 16 - 0
src/routes/admin/CategoryRoute.ts

@@ -0,0 +1,16 @@
1
+import express, { Router } from 'express';
2
+import * as CategoryController from '../../controllers/admin/CategoryController';
3
+import keycloak from '../../middleware/Keycloak';
4
+import { extractToken } from '../../middleware/ExtractToken';
5
+import checkRoles from '../../middleware/CheckRoles';
6
+const router: Router = express.Router();
7
+
8
+router.get('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.getAllCategory);
9
+router.post('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.storeCategory);
10
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.showCategory);
11
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.updateCategory);
12
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.deleteCategory);
13
+
14
+router.get('/:id/use', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.showUseCategory);
15
+router.post('/merge', [keycloak.protect(), extractToken, checkRoles(["admin"])], CategoryController.mergeCategory);
16
+export default router;

+ 13 - 74
src/routes/admin/HospitalRoute.ts

@@ -18,82 +18,21 @@ router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])],
18 18
 router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], HospitalController.deleteHospital);
19 19
 
20 20
 // Vendor Experience
21
-router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.getAllVendorExperience);
22
-router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.storeVendorExperience);
23
-router.get('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.showVendorExperience);
24
-router.patch('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.updateVendorExperience);
25
-router.delete('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.deleteVendorExperience);
21
+router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.getAllVendorExperience);
22
+router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.storeVendorExperience);
23
+router.get('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.showVendorExperience);
24
+router.patch('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.updateVendorExperience);
25
+router.delete('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.deleteVendorExperience);
26 26
 
27 27
 // Executives History
28
-router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.getAllExecutivesHistory);
29
-router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.storeExecutivesHistory);
30
-router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.showExecutivesHistory);
31
-router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.updateExecutivesHistory);
32
-router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.deleteExecutivesHistory);
28
+router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], ExecutivesHistoryController.getAllExecutivesHistory);
29
+router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], ExecutivesHistoryController.storeExecutivesHistory);
30
+router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], ExecutivesHistoryController.showExecutivesHistory);
31
+router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], ExecutivesHistoryController.updateExecutivesHistory);
32
+router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], ExecutivesHistoryController.deleteExecutivesHistory);
33 33
 
34 34
 // Status History
35
-router.get('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], StatusHistoryController.getAllStatusHistory);
36
-router.post('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], StatusHistoryController.storeStatusHistory);
35
+router.get('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin"])], StatusHistoryController.getAllStatusHistory);
36
+router.post('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin"])], StatusHistoryController.storeStatusHistory);
37 37
 
38
-export default router;
39
-
40
-
41
-// const express = require('express')
42
-// const router = express.Router()
43
-// const hospitalController = require('../../controllers/admin/HospitalController.js')
44
-// const vendorExperienceController = require('../../controllers/admin/VendorExperienceController.js')
45
-// const executivesHistoryController = require('../../controllers/admin/ExecutivesHistoryController.js')
46
-// const statusHistoriesController = require('../../controllers/admin/StatusHistoryController.js')
47
-// const verifyJWT = require('../../middleware/VerifyJWT.js');
48
-// const checkRole = require('../../middleware/CheckRole.js');
49
-// const upload = require('../../middleware/UploadImage.js');
50
-
51
-// const keycloak = require('../../middleware/Keycloak.js');
52
-// const extractToken = require('../../middleware/ExtractToken.js');
53
-// const checkRoles = require('../../middleware/CheckRoles.js');
54
-
55
-// router.get('/', verifyJWT, checkRole(['admin']), hospitalController.getAllHospital);
56
-// router.post('/', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.storeHospital);
57
-// router.get('/:id', verifyJWT, checkRole(['admin']), hospitalController.showHospital);
58
-// router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.updateHospital);
59
-// router.delete('/:id', verifyJWT, checkRole(['admin']), hospitalController.deleteHospital);
60
-
61
-// // Vendor History
62
-// router.get('/:id/vendor-history', verifyJWT, checkRole(['admin']), vendorExperienceController.getAllVendorHistory);
63
-// router.post('/:id/vendor-history', verifyJWT, checkRole(['admin']), vendorExperienceController.storeVendorHistory);
64
-// router.get('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorExperienceController.showVendorHistory);
65
-// router.patch('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorExperienceController.updateVendorHistory);
66
-// router.delete('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['admin']), vendorExperienceController.deleteVendorHistory);
67
-
68
-// // Executives History
69
-// router.get('/:id/executives-history', verifyJWT, checkRole(['admin']), executivesHistoryController.getAllExecutivesHistory);
70
-// router.post('/:id/executives-history', verifyJWT, checkRole(['admin']), executivesHistoryController.storeExecutivesHistory);
71
-// router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.showExecutivesHistory);
72
-// router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.updateExecutivesHistory);
73
-// router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.deleteExecutivesHistory);
74
-
75
-// router.get('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], hospitalController.getAllHospital);
76
-// router.post('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], upload.single('image'), hospitalController.storeHospital);
77
-// router.get('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], hospitalController.showHospital);
78
-// router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], upload.single('image'), hospitalController.updateHospital);
79
-// router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], hospitalController.deleteHospital);
80
-
81
-// // Vendor History
82
-// router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorExperienceController.getAllVendorHistory);
83
-// router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorExperienceController.storeVendorHistory);
84
-// router.get('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorExperienceController.showVendorHistory);
85
-// router.patch('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorExperienceController.updateVendorHistory);
86
-// router.delete('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], vendorExperienceController.deleteVendorHistory);
87
-
88
-// // Executives History
89
-// router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.getAllExecutivesHistory);
90
-// router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.storeExecutivesHistory);
91
-// router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.showExecutivesHistory);
92
-// router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.updateExecutivesHistory);
93
-// router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin"])], executivesHistoryController.deleteExecutivesHistory);
94
-
95
-// // Status History
96
-// router.get('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin"])], statusHistoriesController.getAllStatusHistory);
97
-// router.post('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin"])], statusHistoriesController.storeStatusHistory);
98
-
99
-// module.exports = router;
38
+export default router;

+ 13 - 70
src/routes/sales/HospitalRoute.ts

@@ -17,78 +17,21 @@ router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])],
17 17
 router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], HospitalController.showHospital);
18 18
 
19 19
 // Vendor Experience
20
-router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.getAllVendorExperience);
21
-router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.storeVendorExperience);
22
-router.get('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.showVendorExperience);
23
-router.patch('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.updateVendorExperience);
24
-router.delete('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], VendorExperienceController.deleteVendorExperience);
20
+router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["sales"])], VendorExperienceController.getAllVendorExperience);
21
+router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["sales"])], VendorExperienceController.storeVendorExperience);
22
+router.get('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["sales"])], VendorExperienceController.showVendorExperience);
23
+router.patch('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["sales"])], VendorExperienceController.updateVendorExperience);
24
+router.delete('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(["sales"])], VendorExperienceController.deleteVendorExperience);
25 25
 
26 26
 // Executives History
27
-router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.getAllExecutivesHistory);
28
-router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.storeExecutivesHistory);
29
-router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.showExecutivesHistory);
30
-router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.updateExecutivesHistory);
31
-router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], ExecutivesHistoryController.deleteExecutivesHistory);
27
+router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["sales"])], ExecutivesHistoryController.getAllExecutivesHistory);
28
+router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(["sales"])], ExecutivesHistoryController.storeExecutivesHistory);
29
+router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["sales"])], ExecutivesHistoryController.showExecutivesHistory);
30
+router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["sales"])], ExecutivesHistoryController.updateExecutivesHistory);
31
+router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(["sales"])], ExecutivesHistoryController.deleteExecutivesHistory);
32 32
 
33 33
 // // Status History
34
-router.get('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], StatusHistoryController.getAllStatusHistory);
35
-router.post('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["admin", "sales"])], StatusHistoryController.storeStatusHistory);
34
+router.get('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["sales"])], StatusHistoryController.getAllStatusHistory);
35
+router.post('/:id/status-histories', [keycloak.protect(), extractToken, checkRoles(["sales"])], StatusHistoryController.storeStatusHistory);
36 36
 
37
-export default router;
38
-
39
-// const express = require('express')
40
-// const router = express.Router()
41
-// const hospitalController = require('../../controllers/sales/HospitalController.js')
42
-// const vendorExperienceController = require('../../controllers/sales/VendorExperienceController.js')
43
-// const executivesHistoryController = require('../../controllers/sales/ExecutivesHistoryController.js')
44
-// const statusHistoriesController = require('../../controllers/sales/StatusHistoryController.js')
45
-// const verifyJWT = require('../../middleware/VerifyJWT.js');
46
-// const checkRole = require('../../middleware/CheckRole.js');
47
-// const upload = require('../../middleware/UploadImage.js');
48
-
49
-// const keycloak = require('../../middleware/Keycloak.js');
50
-// const extractToken = require('../../middleware/ExtractToken.js');
51
-// const checkRoles = require('../../middleware/CheckRoles.js');
52
-
53
-// router.get('/', verifyJWT, checkRole(['sales']), hospitalController.getAllHospitalByArea);
54
-// router.post('/', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.storeHospital);
55
-// router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.updateHospital);
56
-// router.get('/:id', verifyJWT, checkRole(['sales']), hospitalController.showHospital);
57
-
58
-// // Vendor History
59
-// router.get('/:id/vendor-history', verifyJWT, checkRole(['sales']), vendorHistoryController.getAllVendorHistory);
60
-// router.post('/:id/vendor-history', verifyJWT, checkRole(['sales']), vendorHistoryController.storeVendorHistory);
61
-// router.get('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.showVendorHistory);
62
-// router.patch('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.updateVendorHistory);
63
-// router.delete('/:id/vendor-history/:id_vendor_history', verifyJWT, checkRole(['sales']), vendorHistoryController.deleteVendorHistory);
64
-
65
-// // Executives History
66
-// router.get('/:id/executives-history', verifyJWT, checkRole(['sales']), executivesHistoryController.getAllExecutivesHistory);
67
-// router.post('/:id/executives-history', verifyJWT, checkRole(['sales']), executivesHistoryController.storeExecutivesHistory);
68
-// router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.showExecutivesHistory);
69
-// router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.updateExecutivesHistory);
70
-// router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.deleteExecutivesHistory);
71
-
72
-// router.get('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], hospitalController.getAllHospitalByArea);
73
-// router.post('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], upload.single('image'), hospitalController.storeHospital);
74
-// router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], upload.single('image'), hospitalController.updateHospital);
75
-// router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], hospitalController.showHospital);
76
-
77
-// // Vendor History
78
-// router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorExperienceController.getAllVendorHistory);
79
-// router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorExperienceController.storeVendorHistory);
80
-// router.get('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorExperienceController.showVendorHistory);
81
-// router.patch('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorExperienceController.updateVendorHistory);
82
-// router.delete('/:id/vendor-experience/:id_vendor_experience', [keycloak.protect(), extractToken, checkRoles(['sales'])], vendorExperienceController.deleteVendorHistory);
83
-
84
-// // Executives History
85
-// router.get('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.getAllExecutivesHistory);
86
-// router.post('/:id/executives-history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.storeExecutivesHistory);
87
-// router.get('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.showExecutivesHistory);
88
-// router.patch('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.updateExecutivesHistory);
89
-// router.delete('/:id/executives-history/:id_executives_history', [keycloak.protect(), extractToken, checkRoles(['sales'])], executivesHistoryController.deleteExecutivesHistory);
90
-
91
-// router.get('/:id/status-history', [keycloak.protect(), extractToken, checkRoles(["sales"])], statusHistoriesController.getAllStatusHistory);
92
-// router.post('/:id/status-history', [keycloak.protect(), extractToken, checkRoles(["sales"])], statusHistoriesController.storeStatusHistory);
93
-
94
-// module.exports = router;
37
+export default router;

+ 60 - 0
src/services/admin/CategoryLinkService.ts

@@ -0,0 +1,60 @@
1
+import CategoryRepository from '../../repository/admin/CategoryRepository';
2
+import CategoryLinkRepository from '../../repository/admin/CategoryLinkRepository';
3
+import { createLog, updateLog } from '../../utils/LogActivity';
4
+import { CustomRequest } from '../../types/token/CustomRequest';
5
+
6
+export const storeCategoryLinkService = async (tags: string[], source_type: string, source_id: string, req: CustomRequest) => {
7
+    const result = [];
8
+
9
+    for (let i = 0; i < tags.length; i++) {
10
+        const tag = tags[i];
11
+
12
+        const existCategoryTag = await CategoryRepository.findByTag(tag);
13
+
14
+        let categoryId: string = "";
15
+        if (!existCategoryTag) {
16
+            const created = await CategoryRepository.create({ tag: tag, description: null });
17
+            categoryId = created.id;
18
+            await createLog(req, created);
19
+        } else {
20
+            categoryId = existCategoryTag.id;
21
+        }
22
+
23
+        const data = await CategoryLinkRepository.create({
24
+            category_id: categoryId,
25
+            source_type: source_type,
26
+            source_id: source_id,
27
+        });
28
+
29
+        await createLog(req, data);
30
+        result.push(data);
31
+    }
32
+};
33
+export const updateCategoryLinkService = async (newTags: string[], source_type: string, source_id: string, req: CustomRequest) => {
34
+    const result = [];
35
+
36
+    const oldLinks = await CategoryLinkRepository.findBySource(source_type, source_id);
37
+
38
+    for (const link of oldLinks) {
39
+        await CategoryLinkRepository.deleteById(link.id);
40
+    }
41
+
42
+    for (const tag of newTags) {
43
+        let category = await CategoryRepository.findByTag(tag);
44
+        if (!category) {
45
+            category = await CategoryRepository.create({ tag, description: null });
46
+            await createLog(req, category);
47
+        }
48
+
49
+        const newLink = await CategoryLinkRepository.create({
50
+            category_id: category.id,
51
+            source_type,
52
+            source_id,
53
+        });
54
+
55
+        await updateLog(req, newLink);
56
+        result.push(newLink);
57
+    }
58
+
59
+    return result;
60
+};

+ 203 - 0
src/services/admin/CategoryService.ts

@@ -0,0 +1,203 @@
1
+import CategoryRepository from '../../repository/admin/CategoryRepository';
2
+import { HttpException } from '../../utils/HttpException';
3
+import { createLog, updateLog, deleteLog } from '../../utils/LogActivity';
4
+import { SearchFilter } from '../../utils/SearchFilter';
5
+import { now } from '../../utils/TimeLocal';
6
+import { Prisma } from '@prisma/client';
7
+import { CustomRequest } from '../../types/token/CustomRequest';
8
+import { CategoryRequestDTO, MergeCategoryRequestDTO } from '../../types/admin/category/CategoryDTO';
9
+import prisma from '../../prisma/PrismaClient';
10
+import { GetSourceDataByType } from '../../utils/GetSourceDataByType';
11
+import { TransformCategoryLinkUse } from '../../resources/admin/category/TransformCategoryLinkUse';
12
+import { CategoryLinkUseDTO } from '../../types/admin/category_link/CategoryLinkDTO';
13
+
14
+interface GetAllCategoryParams {
15
+    page: number;
16
+    limit: number;
17
+    search?: string;
18
+    sortBy: string;
19
+    orderBy: 'asc' | 'desc';
20
+}
21
+
22
+interface GetAllCategoryLinkParams {
23
+    page: number;
24
+    limit: number;
25
+    search?: string;
26
+    categoryId: string;
27
+}
28
+
29
+export const getAllCategoryService = async ({ page, limit, search, sortBy, orderBy }: GetAllCategoryParams) => {
30
+    const skip = (page - 1) * limit;
31
+
32
+    const where: Prisma.CategoryWhereInput = {
33
+        ...SearchFilter(search, ['tag']),
34
+        deletedAt: null,
35
+    };
36
+
37
+    const [categories, total] = await Promise.all([
38
+        CategoryRepository.findAll({
39
+            skip,
40
+            take: limit,
41
+            where,
42
+            orderBy: { [sortBy]: orderBy },
43
+        }),
44
+        CategoryRepository.countAll(where),
45
+    ]);
46
+
47
+    return { categories, total };
48
+};
49
+
50
+export const showCategoryService = async (id: string) => {
51
+    const category = await CategoryRepository.findById(id);
52
+    if (!category) {
53
+        throw new HttpException('Data category not found', 404);
54
+    }
55
+
56
+    return category;
57
+};
58
+
59
+export const storeCategoryService = async (validateData: CategoryRequestDTO, req: CustomRequest) => {
60
+    const existingCategory = await CategoryRepository.findByTag(validateData.tag);
61
+    if (existingCategory && !existingCategory.deletedAt) {
62
+        throw new HttpException('Category with this tag already exists and may not be duplicated.', 400);
63
+    }
64
+
65
+    const data = await CategoryRepository.create(validateData);
66
+    await createLog(req, data);
67
+    return data;
68
+};
69
+
70
+export const updateCategoryService = async (validateData: CategoryRequestDTO, id: string, req: CustomRequest) => {
71
+    const category = await CategoryRepository.findById(id);
72
+    if (!category) {
73
+        throw new HttpException('Data category not found', 404);
74
+    }
75
+
76
+    if (validateData.tag) {
77
+        const existingCategory = await CategoryRepository.findByTag(validateData.tag);
78
+        if (existingCategory && existingCategory.id !== id) {
79
+            throw new HttpException('Category with this tag already exists and may not be duplicated.', 400);
80
+        }
81
+    }
82
+
83
+    const data = await CategoryRepository.update(id, validateData);
84
+    await updateLog(req, data);
85
+    return data;
86
+};
87
+
88
+export const deleteCategoryService = async (id: string, req: CustomRequest) => {
89
+    const category = await CategoryRepository.findById(id);
90
+    if (!category) {
91
+        throw new HttpException('Data category not found', 404);
92
+    }
93
+
94
+    const data = await CategoryRepository.update(id, {
95
+        deletedAt: now().toDate(),
96
+    });
97
+
98
+    await deleteLog(req, data);
99
+    return data;
100
+};
101
+
102
+export const showUseCategoryService = async ({ page, limit, search, categoryId }: GetAllCategoryLinkParams) => {
103
+    const skip = (page - 1) * limit;
104
+
105
+    const where: Prisma.CategoryLinkWhereInput = {
106
+        ...SearchFilter(search, ['source_type']),
107
+        category_id: categoryId,
108
+        deletedAt: null,
109
+    };
110
+
111
+    const [links, total] = await Promise.all([
112
+        CategoryRepository.findAllCategoryLink({ skip, take: limit, where }),
113
+        CategoryRepository.countAllCategoryLink(where),
114
+    ]);
115
+
116
+    const result: CategoryLinkUseDTO[] = [];
117
+
118
+    for (const link of links) {
119
+        const { source_id, source_type } = link;
120
+        if (!source_id || !source_type) continue;
121
+
122
+        const sourceData = await GetSourceDataByType(source_type, source_id);
123
+        if (!sourceData) continue;
124
+
125
+        result.push(TransformCategoryLinkUse(link, source_type, sourceData));
126
+    }
127
+
128
+    return { data: result, total, page, limit };
129
+};
130
+
131
+export const mergeCategoryService = async (validateData: MergeCategoryRequestDTO, req: CustomRequest) => {
132
+    // Soft delete old categories
133
+    for (const id of validateData.category_id) {
134
+        const category = await CategoryRepository.findById(id);
135
+        if (!category) {
136
+            throw new HttpException(`Category with tag ${validateData.tag} not found`, 404);
137
+        }
138
+    }
139
+
140
+    // Check tag uniqueness
141
+    const existingCategory = await CategoryRepository.findByTag(validateData.tag);
142
+    if (existingCategory && !existingCategory.deletedAt) {
143
+        throw new HttpException('Category with this tag already exists and may not be duplicated.', 400);
144
+    }
145
+
146
+    // Create new merged category
147
+    const data = await CategoryRepository.create({
148
+        tag: validateData.tag,
149
+        description: validateData.description ?? null,
150
+    });
151
+
152
+    await createLog(req, data);
153
+
154
+    await prisma.categoryLink.updateMany({
155
+        where: {
156
+            category_id: { in: validateData.category_id },
157
+            deletedAt: null,
158
+        },
159
+        data: {
160
+            category_id: data.id,
161
+        },
162
+    });
163
+
164
+    await prisma.category.updateMany({
165
+        where: {
166
+            id: { in: validateData.category_id },
167
+            deletedAt: null,
168
+        },
169
+        data: {
170
+            deletedAt: now().toDate(),
171
+        },
172
+    });
173
+
174
+    // Remove duplicate category links (same category_id, source_type, source_id, deletedAt: null)
175
+    const links = await prisma.categoryLink.findMany({
176
+        where: {
177
+            category_id: data.id,
178
+            deletedAt: null,
179
+        },
180
+    });
181
+
182
+    // Group by source_type + source_id
183
+    const grouped: Record<string, typeof links> = {};
184
+    for (const link of links) {
185
+        const key = `${link.source_type}_${link.source_id}`;
186
+        if (!grouped[key]) grouped[key] = [];
187
+        grouped[key].push(link);
188
+    }
189
+
190
+    for (const group of Object.values(grouped)) {
191
+        if (group.length > 1) {
192
+            // Sort by createdAt, keep the earliest, delete the rest
193
+            const sorted = group.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
194
+            const toDelete = sorted.slice(1);
195
+            for (const link of toDelete) {
196
+                await prisma.categoryLink.update({
197
+                    where: { id: link.id },
198
+                    data: { deletedAt: now().toDate() },
199
+                });
200
+            }
201
+        }
202
+    }
203
+};

+ 45 - 43
src/services/admin/HospitalService.ts

@@ -6,12 +6,13 @@ import { createLog, updateLog, deleteLog } from '../../utils/LogActivity';
6 6
 import ProvinceRepository from '../../repository/admin/ProvinceRepository';
7 7
 import CityRepository from '../../repository/admin/CityRepository';
8 8
 import prisma from '../../prisma/PrismaClient';
9
-import { ProgressStatus } from '@prisma/client';
9
+import { Prisma, ProgressStatus } from '@prisma/client';
10 10
 import { CustomRequest } from '../../types/token/CustomRequest';
11 11
 import { HospitalRequestDTO } from '../../types/admin/hospital/HospitalDTO';
12 12
 import path from 'path';
13 13
 import fs from 'fs/promises';
14 14
 import sharp from 'sharp';
15
+import { storeCategoryLinkService, updateCategoryLinkService } from './CategoryLinkService';
15 16
 
16 17
 interface PaginationParams {
17 18
     page: number;
@@ -79,11 +80,6 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
79 80
         );
80 81
     }
81 82
 
82
-    // Cek file image
83
-    // if (!req.file) {
84
-    //     throw new HttpException({ image: ['image file is required'] }, 422);
85
-    // }
86
-
87 83
     // Cek duplikasi rumah sakit (yang belum terhapus)
88 84
     const existingHospital = await prisma.hospital.findFirst({
89 85
         where: {
@@ -97,22 +93,7 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
97 93
         throw new HttpException('Hospital with same name in this city already exists', 400);
98 94
     }
99 95
 
100
-    // const imagePath = `/storage/img/${req.file.filename}`;
101
-
102 96
     let imagePath: string | null = null;
103
-    // if (req.file) {
104
-    //     imagePath = `/storage/img/${req.file.filename}`;
105
-    // }
106
-
107
-    // if (req.file) {
108
-    //     const ext = path.extname(req.file.originalname);
109
-    //     const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
110
-    //     const fullPath = path.join(__dirname, '../../../storage/img', filename);
111
-
112
-    //     await fs.promises.writeFile(fullPath, req.file.buffer);
113
-
114
-    //     imagePath = `/storage/img/${filename}`;
115
-    // }
116 97
 
117 98
     if (req.file) {
118 99
         const ext = '.jpg';
@@ -163,7 +144,6 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
163 144
         throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
164 145
     }
165 146
 
166
-    // Payload untuk membuat hospital
167 147
     const payload = {
168 148
         name: validateData.name!,
169 149
         hospital_code: validateData.hospital_code,
@@ -171,12 +151,12 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
171 151
         ownership: validateData.ownership,
172 152
         address: validateData.address,
173 153
         contact: validateData.contact,
174
-        note: validateData.note,
175 154
         image: imagePath,
176 155
         latitude,
177 156
         longitude,
178 157
         gmaps_url: gmapsUrl,
179 158
         progress_status: ProgressStatus.cari_data,
159
+        note: validateData.note ?? '',
180 160
         province: {
181 161
             connect: { id: validateData.province_id! },
182 162
         },
@@ -189,12 +169,19 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
189 169
     };
190 170
 
191 171
     const data = await HospitalRepository.create(payload);
192
-    await createLog(req, data);
172
+    if (validateData.tags?.length) {
173
+        await storeCategoryLinkService(
174
+            validateData.tags,
175
+            'hospital_notes',
176
+            data.id,
177
+            req
178
+        );
179
+    }
193 180
 
194
-    return data;
181
+    await createLog(req, data);
195 182
 };
196 183
 
197
-export const updateHospitalService = async (validateData: HospitalRequestDTO, id: string, req: CustomRequest) => {
184
+export const updateHospitalService = async (validateData: Partial<HospitalRequestDTO>, id: string, req: CustomRequest) => {
198 185
     const hospital = await HospitalRepository.findById(id);
199 186
     if (!hospital) throw new HttpException("Hospital data not found", 404);
200 187
 
@@ -231,16 +218,6 @@ export const updateHospitalService = async (validateData: HospitalRequestDTO, id
231 218
     }
232 219
 
233 220
     let imagePath = hospital.image;
234
-    // if (req.file) imagePath = `/storage/img/${req.file.filename}`;
235
-    // if (req.file) {
236
-    //     const ext = path.extname(req.file.originalname);
237
-    //     const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
238
-    //     const fullPath = path.join(__dirname, '../../../storage/img', filename);
239
-
240
-    //     await fs.promises.writeFile(fullPath, req.file.buffer);
241
-
242
-    //     imagePath = `/storage/img/${filename}`;
243
-    // }
244 221
 
245 222
     if (req.file) {
246 223
         const ext = '.jpg';
@@ -293,16 +270,41 @@ export const updateHospitalService = async (validateData: HospitalRequestDTO, id
293 270
         }
294 271
     }
295 272
 
296
-    const payload = {
297
-        ...validateData,
298
-        image: imagePath,
299
-        latitude,
300
-        longitude,
301
-        gmaps_url: gmapsUrl,
302
-        progress_status: validateData.progress_status as ProgressStatus,
273
+    // Final payload
274
+    const payload: Prisma.HospitalUpdateInput = {
275
+        ...(validateData.name && { name: validateData.name }),
276
+        ...(validateData.hospital_code && { hospital_code: validateData.hospital_code }),
277
+        ...(validateData.type && { type: validateData.type }),
278
+        ...(validateData.ownership && { ownership: validateData.ownership }),
279
+        ...(validateData.address && { address: validateData.address }),
280
+        ...(validateData.contact && { contact: validateData.contact }),
281
+        ...(validateData.note !== undefined && { note: validateData.note }),
282
+        ...(validateData.progress_status && { progress_status: validateData.progress_status }),
283
+        ...(imagePath && { image: imagePath }),
284
+        ...(latitude !== undefined && { latitude }),
285
+        ...(longitude !== undefined && { longitude }),
286
+        ...(gmapsUrl !== undefined && { gmaps_url: gmapsUrl }),
287
+        ...(validateData.province_id && {
288
+            province: {
289
+                connect: { id: validateData.province_id }
290
+            }
291
+        }),
292
+        ...(validateData.city_id && {
293
+            city: {
294
+                connect: { id: validateData.city_id }
295
+            }
296
+        }),
303 297
     };
304 298
 
305 299
     const data = await HospitalRepository.update(id, payload);
300
+    if (validateData.tags?.length) {
301
+        await updateCategoryLinkService(
302
+            validateData.tags,
303
+            'hospital_notes',
304
+            id,
305
+            req
306
+        );
307
+    }
306 308
     await updateLog(req, data);
307 309
 };
308 310
 

+ 12 - 79
src/services/admin/StatusHistoryService.ts

@@ -5,6 +5,7 @@ import { createLog } from '../../utils/LogActivity';
5 5
 import StatusHistoryRepository from '../../repository/admin/StatusHistoryRepository';
6 6
 import { CustomRequest } from '../../types/token/CustomRequest';
7 7
 import { StatusHistoryRequestDTO } from '../../types/admin/status_history/StatusHistoryDTO';
8
+import { storeCategoryLinkService } from './CategoryLinkService';
8 9
 
9 10
 const prisma = new PrismaClient();
10 11
 
@@ -84,7 +85,7 @@ export const storeStatusHistoryService = async (validateData: StatusHistoryReque
84 85
         },
85 86
         old_status: hospital.progress_status,
86 87
         new_status: validateData.new_status as ProgressStatus,
87
-        note: validateData.note,
88
+        note: validateData.note?.note,
88 89
     };
89 90
 
90 91
     const data = await StatusHistoryRepository.create(payload);
@@ -94,82 +95,14 @@ export const storeStatusHistoryService = async (validateData: StatusHistoryReque
94 95
         data: { progress_status: validateData.new_status as ProgressStatus },
95 96
     });
96 97
 
97
-    await createLog(req, data);
98
-};
98
+    if (validateData.note?.tags?.length) {
99
+        await storeCategoryLinkService(
100
+            validateData.note.tags,
101
+            'status_history_notes',
102
+            data.id,
103
+            req
104
+        );
105
+    }
99 106
 
100
-// const HttpException = require('../../utils/HttpException.js');
101
-// const prisma = require('../../prisma/PrismaClient.js');
102
-// const { createLog } = require('../../utils/LogActivity.js');
103
-// const StatusHistoryRepository = require('../../repository/admin/StatusHistoryRepository.js');
104
-
105
-// exports.getAllStatusHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
106
-//     const skip = (page - 1) * limit;
107
-
108
-//     const hospitalId = req.params.id;
109
-//     const hospital = await prisma.hospital.findFirst({
110
-//         where: {
111
-//             id: hospitalId
112
-//         }
113
-//     })
114
-//     if (!hospital) {
115
-//         throw new HttpException("Hospital not found", 404)
116
-//     }
117
-
118
-//     const where = {
119
-//         hospital_id: req.params.id,
120
-//         deletedAt: null
121
-//     };
122
-
123
-//     const [status_histories, total] = await Promise.all([
124
-//         StatusHistoryRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
125
-//         StatusHistoryRepository.countAll(where)
126
-//     ]);
127
-
128
-//     return { status_histories, total };
129
-// };
130
-
131
-// const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
132
-
133
-// exports.storeStatusHistoryService = async (validateData, req) => {
134
-//     const hospitalId = req.params.id;
135
-//     const userId = req.tokenData.sub;
136
-
137
-//     const hospital = await prisma.hospital.findFirst({
138
-//         where: {
139
-//             id: hospitalId
140
-//         }
141
-//     })
142
-//     if (!hospital) {
143
-//         throw new HttpException("Hospital not found", 404)
144
-//     }
145
-
146
-//     if (validateData.new_status && !validProgressStatuses.includes(validateData.new_status)) {
147
-//         throw new HttpException(
148
-//             `Invalid new_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
149
-//             422
150
-//         );
151
-//     }
152
-
153
-//     if (validateData.new_status === hospital.progress_status) {
154
-//         throw new HttpException("New status change is the same as old status", 400)
155
-//     }
156
-
157
-//     const payload = {
158
-//         hospital_id: hospitalId,
159
-//         user_id: userId,
160
-//         old_status: hospital.progress_status,
161
-//         new_status: validateData.new_status,
162
-//         note: validateData.note,
163
-//     };
164
-
165
-//     const data = await StatusHistoryRepository.create(payload);
166
-
167
-//     await prisma.hospital.update({
168
-//         where: { id: hospitalId },
169
-//         data: {
170
-//             progress_status: validateData.new_status
171
-//         }
172
-//     });
173
-
174
-//     await createLog(req, data);
175
-// };
107
+    await createLog(req, data);
108
+};

+ 47 - 7
src/services/admin/VendorExperienceService.ts

@@ -6,6 +6,7 @@ import { createLog, updateLog, deleteLog } from '../../utils/LogActivity';
6 6
 import VendorExperienceRepository from '../../repository/admin/VendorExperienceRepository';
7 7
 import { CustomRequest } from '../../types/token/CustomRequest';
8 8
 import { VendorExperienceRequestDTO } from '../../types/admin/vendor_experience/VendorExperienceDTO';
9
+import { storeCategoryLinkService, updateCategoryLinkService } from './CategoryLinkService';
9 10
 
10 11
 interface PaginationParams {
11 12
     page: number;
@@ -113,14 +114,33 @@ export const storeVendorExperienceService = async (validateData: VendorExperienc
113 114
         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : null,
114 115
         contract_value_min: validateData.contract_value_min ? Number(validateData.contract_value_min) : null,
115 116
         contract_value_max: validateData.contract_value_max ? Number(validateData.contract_value_max) : null,
116
-        positive_notes: validateData.positive_notes,
117
-        negative_notes: validateData.negative_notes,
117
+        positive_notes: validateData.positive_notes?.note,
118
+        negative_notes: validateData.negative_notes?.note,
118 119
         hospital: {
119 120
             connect: { id: hospitalId }
120 121
         },
121 122
     };
122 123
 
123 124
     const data = await VendorExperienceRepository.create(payload);
125
+
126
+    // Setelah data berhasil disimpan
127
+    if (validateData.positive_notes?.tags?.length) {
128
+        await storeCategoryLinkService(
129
+            validateData.positive_notes.tags,
130
+            'vendor_experience_positive_notes',
131
+            data.id,
132
+            req
133
+        );
134
+    }
135
+
136
+    if (validateData.negative_notes?.tags?.length) {
137
+        await storeCategoryLinkService(
138
+            validateData.negative_notes.tags,
139
+            'vendor_experience_negative_notes',
140
+            data.id,
141
+            req
142
+        );
143
+    }
124 144
     await createLog(req, data);
125 145
 };
126 146
 
@@ -167,21 +187,21 @@ export const updateVendorExperienceService = async (validateData: Partial<Vendor
167 187
     ) {
168 188
         throw new HttpException('Contract value min must be less than contract value max.', 400);
169 189
     }
170
-    
190
+
171 191
     if (
172 192
         validateData.contract_value_max &&
173 193
         validateData.contract_value_max <= vendorHistory.contract_value_min!
174 194
     ) {
175 195
         throw new HttpException('Contract value max must be greater than contract value min.', 400);
176 196
     }
177
-    
197
+
178 198
     if (
179 199
         validateData.contract_start_date &&
180 200
         validateData.contract_start_date >= vendorHistory.contract_expired_date!
181 201
     ) {
182 202
         throw new HttpException('Contract start date must be before contract expired date.', 400);
183 203
     }
184
-    
204
+
185 205
     if (
186 206
         validateData.contract_expired_date &&
187 207
         validateData.contract_expired_date <= vendorHistory.contract_start_date!
@@ -221,14 +241,34 @@ export const updateVendorExperienceService = async (validateData: Partial<Vendor
221 241
         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendorHistory.contract_expired_date,
222 242
         contract_value_min: validateData.contract_value_min ? Number(validateData.contract_value_min) : vendorHistory.contract_value_min,
223 243
         contract_value_max: validateData.contract_value_max ? Number(validateData.contract_value_max) : vendorHistory.contract_value_max,
224
-        positive_notes: validateData.positive_notes,
225
-        negative_notes: validateData.negative_notes,
244
+        positive_notes: validateData.positive_notes?.note || vendorHistory.positive_notes,
245
+        negative_notes: validateData.negative_notes?.note || vendorHistory.negative_notes,
226 246
         simrs_type: validateData.simrs_type,
227 247
         vendor_id: validateData.vendor_id
228 248
     };
229 249
 
230 250
     const data = await VendorExperienceRepository.update(id_vendor_experience, payload);
231 251
     await updateLog(req, data);
252
+
253
+    // Positive Notes Tags
254
+    if (validateData.positive_notes?.tags?.length) {
255
+        await updateCategoryLinkService(
256
+            validateData.positive_notes.tags,
257
+            'vendor_experience_positive_notes',
258
+            id_vendor_experience,
259
+            req
260
+        );
261
+    }
262
+
263
+    // Negative Notes Tags
264
+    if (validateData.negative_notes?.tags?.length) {
265
+        await updateCategoryLinkService(
266
+            validateData.negative_notes.tags,
267
+            'vendor_experience_negative_notes',
268
+            id_vendor_experience,
269
+            req
270
+        );
271
+    }
232 272
 };
233 273
 
234 274
 export const deleteVendorExperienceService = async (req: CustomRequest) => {

+ 50 - 5
src/services/admin/VendorService.ts

@@ -7,6 +7,7 @@ import { Request } from 'express';
7 7
 import { Prisma } from '@prisma/client';
8 8
 import { VendorRequestDTO } from '../../types/admin/vendor/VendorDTO';
9 9
 import { CustomRequest } from '../../types/token/CustomRequest';
10
+import { storeCategoryLinkService, updateCategoryLinkService } from './CategoryLinkService';
10 11
 
11 12
 interface PaginationParams {
12 13
     page: number;
@@ -68,15 +69,38 @@ export const storeVendorService = async (validateData: VendorRequestDTO, req: Cu
68 69
                 id: creatorId,
69 70
             },
70 71
         },
72
+        strengths: validateData.strengths?.note,
73
+        weaknesses: validateData.weaknesses?.note,
71 74
     };
72 75
 
73 76
 
74 77
     const data = await VendorRepository.create(payload);
75 78
     await createLog(req, data);
79
+    // Setelah data berhasil disimpan
80
+    if (validateData.strengths?.tags?.length) {
81
+        await storeCategoryLinkService(
82
+            validateData.strengths.tags,
83
+            'vendor_strength_notes',
84
+            data.id,
85
+            req
86
+        );
87
+    }
88
+
89
+    if (validateData.weaknesses?.tags?.length) {
90
+        await storeCategoryLinkService(
91
+            validateData.weaknesses.tags,
92
+            'vendor_weaknesses_notes',
93
+            data.id,
94
+            req
95
+        );
96
+    }
97
+    await createLog(req, data);
76 98
 };
77 99
 
78
-export const updateVendorService = async (validateData: Partial<VendorRequestDTO>, id: string, req: CustomRequest) => {
79
-    const vendor = await VendorRepository.findById(id);
100
+export const updateVendorService = async (validateData: Partial<VendorRequestDTO>, req: CustomRequest) => {
101
+    const id_vendor: string = req.params.id;
102
+
103
+    const vendor = await VendorRepository.findById(id_vendor);
80 104
     if (!vendor) {
81 105
         throw new HttpException('Data vendor not found', 404);
82 106
     }
@@ -94,12 +118,33 @@ export const updateVendorService = async (validateData: Partial<VendorRequestDTO
94 118
         }
95 119
     }
96 120
 
97
-    const payload: Partial<VendorRequestDTO> = {
98
-        ...validateData
121
+    const payload: Prisma.VendorUpdateInput = {
122
+        ...validateData,
123
+        strengths: validateData.strengths?.note ?? vendor.strengths,
124
+        weaknesses: validateData.weaknesses?.note ?? vendor.weaknesses,
99 125
     };
100 126
 
101
-    const data = await VendorRepository.update(id, payload);
127
+    const data = await VendorRepository.update(id_vendor, payload);
102 128
     await updateLog(req, data);
129
+
130
+
131
+    if (validateData.strengths?.tags?.length) {
132
+        await updateCategoryLinkService(
133
+            validateData.strengths.tags,
134
+            'vendor_strength_notes',
135
+            id_vendor,
136
+            req
137
+        );
138
+    }
139
+
140
+    if (validateData.weaknesses?.tags?.length) {
141
+        await updateCategoryLinkService(
142
+            validateData.weaknesses.tags,
143
+            'vendor_weaknesses_notes',
144
+            id_vendor,
145
+            req
146
+        );
147
+    }
103 148
 };
104 149
 
105 150
 export const deleteVendorService = async (id: string, req: CustomRequest) => {

+ 43 - 289
src/services/sales/HospitalService.ts

@@ -1,4 +1,4 @@
1
-import { PrismaClient, ProgressStatus } from '@prisma/client';
1
+import { Prisma, PrismaClient, ProgressStatus } from '@prisma/client';
2 2
 import { SearchFilter } from '../../utils/SearchFilter';
3 3
 import { createLog, updateLog } from '../../utils/LogActivity';
4 4
 import { HttpException } from '../../utils/HttpException';
@@ -9,9 +9,9 @@ import HospitalRepository from '../../repository/admin/HospitalRepository';
9 9
 import { CustomRequest } from '../../types/token/CustomRequest';
10 10
 import { HospitalRequestDTO } from '../../types/sales/hospital/HospitalDTO';
11 11
 import path from 'path';
12
-// import fs from 'fs';
13 12
 import fs from 'fs/promises';
14 13
 import sharp from 'sharp';
14
+import { storeCategoryLinkService, updateCategoryLinkService } from '../admin/CategoryLinkService';
15 15
 
16 16
 const prisma = new PrismaClient();
17 17
 
@@ -81,23 +81,7 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
81 81
     });
82 82
     if (existingHospital) throw new HttpException('Hospital with same name in this city already exists', 400);
83 83
 
84
-    // if (!req.file) throw new HttpException({ image: ['image file is required'] }, 422);
85
-    // const imagePath = `/storage/img/${req.file.filename}`;
86
-
87 84
     let imagePath: string | null = null;
88
-    // if (req.file) {
89
-    //     imagePath = `/storage/img/${req.file.filename}`;
90
-    // }
91
-
92
-    // if (req.file) {
93
-    //     const ext = path.extname(req.file.originalname);
94
-    //     const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
95
-    //     const fullPath = path.join(__dirname, '../../../storage/img', filename);
96
-
97
-    //     await fs.promises.writeFile(fullPath, req.file.buffer);
98
-
99
-    //     imagePath = `/storage/img/${filename}`;
100
-    // }
101 85
 
102 86
     if (req.file) {
103 87
         const ext = '.jpg';
@@ -166,10 +150,18 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
166 150
     };
167 151
 
168 152
     const data = await salesHospitalRepository.create(payload);
153
+    if (validateData.tags?.length) {
154
+        await storeCategoryLinkService(
155
+            validateData.tags,
156
+            'hospital_notes',
157
+            data.id,
158
+            req
159
+        );
160
+    }
169 161
     await createLog(req, data);
170 162
 };
171 163
 
172
-export const updateHospitalService = async (validateData: HospitalRequestDTO, id: string, req: CustomRequest) => {
164
+export const updateHospitalService = async (validateData: Partial<HospitalRequestDTO>, id: string, req: CustomRequest) => {
173 165
     const hospital = await HospitalRepository.findById(id);
174 166
     if (!hospital) throw new HttpException('Hospital data not found', 404);
175 167
 
@@ -224,16 +216,6 @@ export const updateHospitalService = async (validateData: HospitalRequestDTO, id
224 216
     }
225 217
 
226 218
     let imagePath = hospital.image;
227
-    // if (req.file) imagePath = `/storage/img/${req.file.filename}`;
228
-    // if (req.file) {
229
-    //     const ext = path.extname(req.file.originalname);
230
-    //     const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
231
-    //     const fullPath = path.join(__dirname, '../../../storage/img', filename);
232
-
233
-    //     await fs.promises.writeFile(fullPath, req.file.buffer);
234
-
235
-    //     imagePath = `/storage/img/${filename}`;
236
-    // }
237 219
 
238 220
     if (req.file) {
239 221
         const ext = '.jpg';
@@ -287,16 +269,40 @@ export const updateHospitalService = async (validateData: HospitalRequestDTO, id
287 269
         }
288 270
     }
289 271
 
290
-    const payload = {
291
-        ...validateData,
292
-        image: imagePath,
293
-        latitude,
294
-        longitude,
295
-        gmaps_url: gmapsUrl,
296
-        progress_status: validateData.progress_status as ProgressStatus,
272
+    const payload: Prisma.HospitalUpdateInput = {
273
+        ...(validateData.name && { name: validateData.name }),
274
+        ...(validateData.hospital_code && { hospital_code: validateData.hospital_code }),
275
+        ...(validateData.type && { type: validateData.type }),
276
+        ...(validateData.ownership && { ownership: validateData.ownership }),
277
+        ...(validateData.address && { address: validateData.address }),
278
+        ...(validateData.contact && { contact: validateData.contact }),
279
+        ...(validateData.note !== undefined && { note: validateData.note }),
280
+        ...(validateData.progress_status && { progress_status: validateData.progress_status }),
281
+        ...(imagePath && { image: imagePath }),
282
+        ...(latitude !== undefined && { latitude }),
283
+        ...(longitude !== undefined && { longitude }),
284
+        ...(gmapsUrl !== undefined && { gmaps_url: gmapsUrl }),
285
+        ...(validateData.province_id && {
286
+            province: {
287
+                connect: { id: validateData.province_id }
288
+            }
289
+        }),
290
+        ...(validateData.city_id && {
291
+            city: {
292
+                connect: { id: validateData.city_id }
293
+            }
294
+        }),
297 295
     };
298 296
 
299 297
     const data = await salesHospitalRepository.update(id, payload);
298
+    if (validateData.tags?.length) {
299
+        await updateCategoryLinkService(
300
+            validateData.tags,
301
+            'hospital_notes',
302
+            id,
303
+            req
304
+        );
305
+    }
300 306
     await updateLog(req, data);
301 307
 };
302 308
 
@@ -322,256 +328,4 @@ export const showHospitalService = async (id: string, req: CustomRequest) => {
322 328
     }
323 329
 
324 330
     return { hospital };
325
-};
326
-
327
-// const salesHospitalRepository = require('../../repository/sales/HospitalRepository.js');
328
-// const { SearchFilter } = require('../../utils/SearchFilter.js');
329
-// const prisma = require('../../prisma/PrismaClient.js');
330
-// const { createLog, updateLog } = require('../../utils/LogActivity.js');
331
-// const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js');
332
-// const CityRepository = require('../../repository/admin/CityRepository.js');
333
-// const HttpException = require('../../utils/HttpException.js');
334
-// const HospitalRepository = require('../../repository/admin/HospitalRepository.js');
335
-// const { formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
336
-// const { getUserNameById } = require('../../utils/CheckUserKeycloak.js');
337
-
338
-// exports.getAllHospitalByAreaService = async ({ page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status }, req) => {
339
-//     const skip = (page - 1) * limit;
340
-
341
-//     const userAreas = await prisma.userArea.findMany({
342
-//         where: { user_id: req.tokenData.sub },
343
-//         select: { province_id: true },
344
-//     });
345
-
346
-//     const provinceIds = userAreas.map(ua => ua.province_id);
347
-
348
-//     const where = {
349
-//         ...SearchFilter(search, ['name', 'province.id', 'city.id', 'type', 'ownership']),
350
-//         province_id: { in: provinceIds },
351
-//         ...(province ? { province_id: province } : {}),
352
-//         ...(city ? { city_id: city } : {}),
353
-//         ...(type ? { type: type } : {}),
354
-//         ...(ownership ? { ownership: ownership } : {}),
355
-//         ...(progress_status ? { progress_status: progress_status } : {}),
356
-//         deletedAt: null
357
-//     };
358
-
359
-//     const [hospitals, total] = await Promise.all([
360
-//         salesHospitalRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
361
-//         salesHospitalRepository.countAll(where)
362
-//     ]);
363
-
364
-//     return { hospitals, total };
365
-// };
366
-
367
-// exports.storeHospitalService = async (validateData, req) => {
368
-//     const creatorId = req.tokenData.sub;
369
-
370
-//     const province = await ProvinceRepository.findById(validateData.province_id);
371
-//     if (!province) {
372
-//         throw new HttpException('Province not found', 404);
373
-//     }
374
-
375
-//     const userArea = await prisma.userArea.findFirst({
376
-//         where: {
377
-//             user_id: req.tokenData.sub,
378
-//             province_id: validateData.province_id
379
-//         }
380
-//     });
381
-
382
-//     if (!userArea) {
383
-//         throw new HttpException('You are not allowed to add hospital to this province', 403);
384
-//     }
385
-
386
-//     const city = await CityRepository.findById(validateData.city_id);
387
-//     if (!city) {
388
-//         throw new HttpException('City not found', 404);
389
-//     }
390
-
391
-//     const existingHospital = await prisma.hospital.findFirst({
392
-//         where: {
393
-//             name: validateData.name,
394
-//             city_id: validateData.city_id,
395
-//             deletedAt: null
396
-//         }
397
-//     });
398
-
399
-//     if (existingHospital) {
400
-//         throw new HttpException('Hospital with same name in this city already exists', 400);
401
-//     }
402
-
403
-//     if (!req.file) {
404
-//         throw new HttpException({ image: ['image file is required'] }, 422);
405
-//     }
406
-
407
-//     const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
408
-
409
-//     let latitude = validateData.latitude ?? null;
410
-//     let longitude = validateData.longitude ?? null;
411
-//     let gmapsUrl = validateData.gmaps_url ?? null;
412
-
413
-//     if (gmapsUrl) {
414
-//         if (gmapsUrl.includes("www.google.com/maps")) {
415
-//             const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
416
-//             const match = gmapsUrl.match(regex);
417
-
418
-//             if (match) {
419
-//                 latitude = parseFloat(match[1]);
420
-//                 longitude = parseFloat(match[2]);
421
-//             } else {
422
-//                 throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
423
-//             }
424
-
425
-//         } else if (gmapsUrl.includes("maps.app.goo.gl")) {
426
-//             latitude = null;
427
-//             longitude = null;
428
-
429
-//         } else {
430
-//             // URL disediakan tapi bukan dari domain yang valid
431
-//             throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
432
-//         }
433
-//     } else if (latitude !== null && longitude !== null) {
434
-//         gmapsUrl = null;
435
-//     } else {
436
-//         throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
437
-//     }
438
-
439
-//     const payload = {
440
-//         ...validateData,
441
-//         image: imagePath,
442
-//         progress_status: "cari_data",
443
-//         // simrs_type: "-",
444
-//         created_by: creatorId,
445
-//         latitude,
446
-//         longitude,
447
-//         gmaps_url: gmapsUrl,
448
-//     };
449
-
450
-//     const data = await salesHospitalRepository.create(payload);
451
-//     await createLog(req, data);
452
-// };
453
-
454
-// const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
455
-
456
-// exports.updateHospitalService = async (validateData, id, req) => {
457
-//     const hospital = await HospitalRepository.findById(id);
458
-//     if (!hospital) {
459
-//         throw new HttpException("Hospital data not found", 404);
460
-//     }
461
-
462
-//     const userArea = await prisma.userArea.findFirst({
463
-//         where: {
464
-//             user_id: req.tokenData.sub,
465
-//             province_id: validateData.province_id
466
-//         }
467
-//     });
468
-
469
-//     if (!userArea) {
470
-//         throw new HttpException("You are not authorized to update hospital in this province", 403);
471
-//     }
472
-
473
-//     if (validateData.province_id) {
474
-//         const province = await ProvinceRepository.findById(validateData.province_id);
475
-//         if (!province) {
476
-//             throw new HttpException('Province not found', 404);
477
-//         }
478
-//     }
479
-
480
-//     if (validateData.city_id) {
481
-//         const city = await CityRepository.findById(validateData.city_id);
482
-//         if (!city) {
483
-//             throw new HttpException('City not found', 404);
484
-//         }
485
-//     }
486
-
487
-//     if (validateData.progress_status && !validProgressStatuses.includes(validateData.progress_status)) {
488
-//         throw new HttpException(
489
-//             `Invalid progress_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
490
-//             422
491
-//         );
492
-//     }
493
-
494
-//     if (validateData.name && validateData.city_id) {
495
-//         const existingHospital = await prisma.hospital.findFirst({
496
-//             where: {
497
-//                 name: validateData.name,
498
-//                 city_id: validateData.city_id,
499
-//                 deletedAt: null,
500
-//                 NOT: { id }
501
-//             }
502
-//         });
503
-
504
-//         if (existingHospital) {
505
-//             throw new HttpException('Hospital with same name in this city already exists', 400);
506
-//         }
507
-//     }
508
-
509
-//     // Jika ada file baru, replace image
510
-//     let imagePath = hospital.image; // pakai yang lama
511
-//     if (req.file) {
512
-//         imagePath = `/storage/img/${req.file.filename}`; // path relatif
513
-//     }
514
-
515
-//     // Handle koordinat dan gmaps_url
516
-//     let latitude = hospital.latitude;
517
-//     let longitude = hospital.longitude;
518
-//     let gmapsUrl = hospital.gmaps_url;
519
-
520
-//     if (
521
-//         validateData.latitude !== undefined &&
522
-//         validateData.longitude !== undefined &&
523
-//         validateData.latitude !== null &&
524
-//         validateData.longitude !== null
525
-//     ) {
526
-//         // Jika diberikan lat long langsung
527
-//         latitude = validateData.latitude;
528
-//         longitude = validateData.longitude;
529
-//         gmapsUrl = validateData.gmaps_url || gmapsUrl;
530
-//     } else if (
531
-//         validateData.gmaps_url &&
532
-//         typeof validateData.gmaps_url === "string" &&
533
-//         validateData.gmaps_url.trim() !== ""
534
-//     ) {
535
-//         gmapsUrl = validateData.gmaps_url;
536
-
537
-//         if (gmapsUrl.includes("www.google.com/maps")) {
538
-//             const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
539
-//             const match = gmapsUrl.match(regex);
540
-//             if (match) {
541
-//                 latitude = parseFloat(match[1]);
542
-//                 longitude = parseFloat(match[2]);
543
-//             } else {
544
-//                 throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
545
-//             }
546
-//         } else if (gmapsUrl.includes("maps.app.goo.gl")) {
547
-//             // Tidak bisa ambil koordinat langsung
548
-//             latitude = null;
549
-//             longitude = null;
550
-//         } else {
551
-//             throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
552
-//         }
553
-//     }
554
-
555
-//     const payload = {
556
-//         ...validateData,
557
-//         image: imagePath,
558
-//         // created_by: req.user.id,
559
-//         latitude,
560
-//         longitude,
561
-//         gmaps_url: gmapsUrl,
562
-//     };
563
-
564
-//     const data = await salesHospitalRepository.update(id, payload);
565
-//     await updateLog(req, data);
566
-// };
567
-
568
-// exports.showHospitalService = async (id) => {
569
-//     const hospital = await salesHospitalRepository.findById(id);
570
-//     if (!hospital) {
571
-//         throw new HttpException("Data hospital not found", 404);
572
-//     }
573
-
574
-//     // const userName = await getUserNameById(hospital.created_by);
575
-
576
-//     return { hospital };
577
-// };
331
+};

+ 12 - 102
src/services/sales/StatusHistoryService.ts

@@ -4,6 +4,7 @@ import { createLog } from '../../utils/LogActivity';
4 4
 import StatusHistoryRepository from '../../repository/sales/StatusHistoryRepository';
5 5
 import { CustomRequest } from '../../types/token/CustomRequest';
6 6
 import { StatusHistoryRequestDTO } from '../../types/sales/status_history/StatusHistoryDTO';
7
+import { storeCategoryLinkService } from '../admin/CategoryLinkService';
7 8
 
8 9
 const prisma = new PrismaClient();
9 10
 
@@ -115,7 +116,7 @@ export const storeStatusHistoryService = async (validateData: StatusHistoryReque
115 116
         },
116 117
         old_status: hospital.progress_status,
117 118
         new_status: validateData.new_status as ProgressStatus,
118
-        note: validateData.note,
119
+        note: validateData.note?.note,
119 120
     };
120 121
 
121 122
     const data = await StatusHistoryRepository.create(payload);
@@ -125,105 +126,14 @@ export const storeStatusHistoryService = async (validateData: StatusHistoryReque
125 126
         data: { progress_status: validateData.new_status as ProgressStatus },
126 127
     });
127 128
 
128
-    await createLog(req, data);
129
-};
129
+    if (validateData.note?.tags?.length) {
130
+        await storeCategoryLinkService(
131
+            validateData.note.tags,
132
+            'status_history_notes',
133
+            data.id,
134
+            req
135
+        );
136
+    }
130 137
 
131
-// const HttpException = require('../../utils/HttpException.js');
132
-// const prisma = require('../../prisma/PrismaClient.js');
133
-// const { createLog } = require('../../utils/LogActivity.js');
134
-// const StatusHistoryRepository = require('../../repository/sales/StatusHistoryRepository.js');
135
-
136
-// exports.getAllStatusHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
137
-//     const skip = (page - 1) * limit;
138
-
139
-//     const userId = req.tokenData.sub;
140
-//     const hospitalId = req.params.id;
141
-//     const hospital = await prisma.hospital.findFirst({
142
-//         where: {
143
-//             id: hospitalId
144
-//         }
145
-//     })
146
-//     if (!hospital) {
147
-//         throw new HttpException("Hospital not found", 404)
148
-//     }
149
-
150
-//     const userAreas = await prisma.userArea.findMany({
151
-//         where: { user_id: userId },
152
-//         select: { province_id: true }
153
-//     });
154
-
155
-//     const userProvinceIds = userAreas.map(ua => ua.province_id);
156
-
157
-//     if (!userProvinceIds.includes(hospital.province_id)) {
158
-//         throw new HttpException("This hospital is not your area", 403);
159
-//     }
160
-
161
-//     const where = {
162
-//         hospital_id: req.params.id,
163
-//         deletedAt: null
164
-//     };
165
-
166
-//     const [status_histories, total] = await Promise.all([
167
-//         StatusHistoryRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
168
-//         StatusHistoryRepository.countAll(where)
169
-//     ]);
170
-
171
-//     return { status_histories, total };
172
-// };
173
-
174
-// const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
175
-
176
-// exports.storeStatusHistoryService = async (validateData, req) => {
177
-//     const hospitalId = req.params.id;
178
-//     const userId = req.tokenData.sub;
179
-
180
-//     const hospital = await prisma.hospital.findFirst({
181
-//         where: {
182
-//             id: hospitalId
183
-//         }
184
-//     })
185
-//     if (!hospital) {
186
-//         throw new HttpException("Hospital not found", 404)
187
-//     }
188
-
189
-//     const userAreas = await prisma.userArea.findMany({
190
-//         where: { user_id: userId },
191
-//         select: { province_id: true }
192
-//     });
193
-
194
-//     const userProvinceIds = userAreas.map(ua => ua.province_id);
195
-
196
-//     if (!userProvinceIds.includes(hospital.province_id)) {
197
-//         throw new HttpException("This hospital is not your area", 403);
198
-//     }
199
-
200
-//     if (validateData.new_status && !validProgressStatuses.includes(validateData.new_status)) {
201
-//         throw new HttpException(
202
-//             `Invalid new_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
203
-//             422
204
-//         );
205
-//     }
206
-
207
-//     if (validateData.new_status === hospital.progress_status) {
208
-//         throw new HttpException("New status change is the same as old status", 400)
209
-//     }
210
-
211
-//     const payload = {
212
-//         hospital_id: hospitalId,
213
-//         user_id: userId,
214
-//         old_status: hospital.progress_status,
215
-//         new_status: validateData.new_status,
216
-//         note: validateData.note,
217
-//     };
218
-
219
-//     const data = await StatusHistoryRepository.create(payload);
220
-
221
-//     await prisma.hospital.update({
222
-//         where: { id: hospitalId },
223
-//         data: {
224
-//             progress_status: validateData.new_status
225
-//         }
226
-//     });
227
-
228
-//     await createLog(req, data);
229
-// };
138
+    await createLog(req, data);
139
+};

+ 50 - 481
src/services/sales/VendorExperienceService.ts

@@ -5,6 +5,7 @@ import { createLog, updateLog, deleteLog } from '../../utils/LogActivity';
5 5
 import VendorExperienceRepository from '../../repository/sales/VendorExperienceRepository';
6 6
 import { CustomRequest } from '../../types/token/CustomRequest';
7 7
 import { VendorExperienceRequestDTO } from '../../types/sales/vendor_experience/VendorExperienceDTO';
8
+import { storeCategoryLinkService, updateCategoryLinkService } from '../admin/CategoryLinkService';
8 9
 
9 10
 interface PaginationParams {
10 11
     page: number;
@@ -160,14 +161,33 @@ export const storeVendorExperienceService = async (validateData: VendorExperienc
160 161
         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : null,
161 162
         contract_value_min: validateData.contract_value_min ? Number(validateData.contract_value_min) : null,
162 163
         contract_value_max: validateData.contract_value_max ? Number(validateData.contract_value_max) : null,
163
-        positive_notes: validateData.positive_notes,
164
-        negative_notes: validateData.negative_notes,
164
+        positive_notes: validateData.positive_notes?.note,
165
+        negative_notes: validateData.negative_notes?.note,
165 166
         hospital: {
166 167
             connect: { id: hospitalId }
167 168
         },
168 169
     };
169 170
 
170 171
     const data = await VendorExperienceRepository.create(payload);
172
+
173
+    if (validateData.positive_notes?.tags?.length) {
174
+        await storeCategoryLinkService(
175
+            validateData.positive_notes.tags,
176
+            'vendor_experience_positive_notes',
177
+            data.id,
178
+            req
179
+        );
180
+    }
181
+
182
+    if (validateData.negative_notes?.tags?.length) {
183
+        await storeCategoryLinkService(
184
+            validateData.negative_notes.tags,
185
+            'vendor_experience_negative_notes',
186
+            data.id,
187
+            req
188
+        );
189
+    }
190
+
171 191
     await createLog(req, data);
172 192
 };
173 193
 
@@ -224,27 +244,27 @@ export const updateVendorExperienceService = async (validateData: Partial<Vendor
224 244
         });
225 245
     }
226 246
 
227
-   if (
247
+    if (
228 248
         validateData.contract_value_min &&
229 249
         validateData.contract_value_min >= vendorHistory.contract_value_max!
230 250
     ) {
231 251
         throw new HttpException('Contract value min must be less than contract value max.', 400);
232 252
     }
233
-    
253
+
234 254
     if (
235 255
         validateData.contract_value_max &&
236 256
         validateData.contract_value_max <= vendorHistory.contract_value_min!
237 257
     ) {
238 258
         throw new HttpException('Contract value max must be greater than contract value min.', 400);
239 259
     }
240
-    
260
+
241 261
     if (
242 262
         validateData.contract_start_date &&
243 263
         validateData.contract_start_date >= vendorHistory.contract_expired_date!
244 264
     ) {
245 265
         throw new HttpException('Contract start date must be before contract expired date.', 400);
246 266
     }
247
-    
267
+
248 268
     if (
249 269
         validateData.contract_expired_date &&
250 270
         validateData.contract_expired_date <= vendorHistory.contract_start_date!
@@ -284,13 +304,34 @@ export const updateVendorExperienceService = async (validateData: Partial<Vendor
284 304
         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendorHistory.contract_expired_date,
285 305
         contract_value_min: validateData.contract_value_min ? Number(validateData.contract_value_min) : vendorHistory.contract_value_min,
286 306
         contract_value_max: validateData.contract_value_max ? Number(validateData.contract_value_max) : vendorHistory.contract_value_max,
287
-        positive_notes: validateData.positive_notes,
288
-        negative_notes: validateData.negative_notes,
307
+        positive_notes: validateData.positive_notes?.note,
308
+        negative_notes: validateData.negative_notes?.note,
289 309
         simrs_type: validateData.simrs_type,
290 310
         vendor_id: validateData.vendor_id
291 311
     };
292 312
 
293 313
     const data = await VendorExperienceRepository.update(id_vendor_experience, payload);
314
+
315
+    // Positive Notes Tags
316
+    if (validateData.positive_notes?.tags?.length) {
317
+        await updateCategoryLinkService(
318
+            validateData.positive_notes.tags,
319
+            'vendor_experience_positive_notes',
320
+            id_vendor_experience,
321
+            req
322
+        );
323
+    }
324
+
325
+    // Negative Notes Tags
326
+    if (validateData.negative_notes?.tags?.length) {
327
+        await updateCategoryLinkService(
328
+            validateData.negative_notes.tags,
329
+            'vendor_experience_negative_notes',
330
+            id_vendor_experience,
331
+            req
332
+        );
333
+    }
334
+
294 335
     await updateLog(req, data);
295 336
 };
296 337
 
@@ -325,476 +366,4 @@ export const deleteVendorExperienceService = async (req: CustomRequest) => {
325 366
     });
326 367
 
327 368
     await deleteLog(req, data);
328
-};
329
-
330
-// const VendorExperienceRepository = require('../../repository/sales/VendorExperienceRepository.js');
331
-// const HttpException = require('../../utils/HttpException.js');
332
-// const prisma = require('../../prisma/PrismaClient.js');
333
-// const timeLocal = require('../../utils/TimeLocal.js');
334
-// const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
335
-
336
-// exports.getAllVendorHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
337
-//     const skip = (page - 1) * limit;
338
-
339
-//     const hospitalId = req.params.id;
340
-//     const hospital = await prisma.hospital.findFirst({
341
-//         where: {
342
-//             id: hospitalId
343
-//         }
344
-//     })
345
-//     if (!hospital) {
346
-//         throw new HttpException("Hospital not found", 404)
347
-//     }
348
-
349
-//     const where = {
350
-//         // ...SearchFilter(search, ['name', 'name_pt']),
351
-//         hospital_id: req.params.id,
352
-//         deletedAt: null
353
-//     };
354
-
355
-//     const [vendor_histories, total] = await Promise.all([
356
-//         VendorExperienceRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
357
-//         VendorExperienceRepository.countAll(where)
358
-//     ]);
359
-
360
-//     return { vendor_histories, total };
361
-// };
362
-
363
-// exports.showVendorHistoryService = async (req) => {
364
-//     const id_hospital = req.params.id;
365
-//     const id_vendor_experience = req.params.id_vendor_experience;
366
-
367
-//     const hospital = await prisma.hospital.findFirst({
368
-//         where: {
369
-//             id: id_hospital
370
-//         }
371
-//     })
372
-//     if (!hospital) {
373
-//         throw new HttpException("Hospital not found", 404)
374
-//     }
375
-
376
-//     const vendorHistory = await VendorExperienceRepository.findById(id_vendor_experience);
377
-//     if (!vendorHistory) {
378
-//         throw new HttpException("Vendor experience not found", 404);
379
-//     }
380
-
381
-//     return vendorHistory;
382
-// };
383
-
384
-// exports.storeVendorHistoryService = async (validateData, req) => {
385
-//     const hospitalId = req.params.id;
386
-
387
-//     const hospital = await prisma.hospital.findFirst({
388
-//         where: {
389
-//             id: hospitalId
390
-//         }
391
-//     });
392
-
393
-//     if (!hospital) {
394
-//         throw new HttpException("Hospital not found", 404)
395
-//     }
396
-
397
-//     if (validateData.vendor_id) {
398
-//         const vendor = await prisma.vendor.findFirst({
399
-//             where: {
400
-//                 id: validateData.vendor_id
401
-//             }
402
-//         });
403
-
404
-//         if (!vendor) {
405
-//             throw new HttpException("Vendor not found", 404)
406
-//         }
407
-//     }
408
-
409
-//     const SimrsType = ["vendor", "in house", "gratis"];
410
-//     if (validateData.simrs_type && !SimrsType.includes(validateData.simrs_type)) {
411
-//         throw new HttpException("Simrs type must be vendor, in house, or gratis", 400);
412
-//     }
413
-
414
-//     if (validateData.contract_start_date && validateData.contract_expired_date) {
415
-//         if (validateData.contract_start_date >= validateData.contract_expired_date) {
416
-//             throw new HttpException("Contract expired date must be after contract date", 400)
417
-//         }
418
-//     }
419
-
420
-//     if (validateData.contract_value_min && validateData.contract_value_max) {
421
-//         if (validateData.contract_value_min >= validateData.contract_value_max) {
422
-//             throw new HttpException("Contract value max must be after contract value min", 400)
423
-//         }
424
-//     }
425
-
426
-//     // await prisma.hospital.update({
427
-//     //     where: { id: hospitalId },
428
-//     //     data: {
429
-//     //         simrs_type: validateData.simrs_type
430
-//     //     }
431
-//     // });
432
-
433
-//     if (validateData.simrs_type) {
434
-//         const existingActiveVendors = await prisma.vendorExperience.findMany({
435
-//             where: {
436
-//                 hospital_id: hospitalId,
437
-//                 status: "active",
438
-//                 deletedAt: null
439
-//             }
440
-//         });
441
-
442
-//         if (existingActiveVendors.length > 0) {
443
-//             await prisma.vendorExperience.updateMany({
444
-//                 where: {
445
-//                     hospital_id: hospitalId,
446
-//                     status: "active",
447
-//                     deletedAt: null
448
-//                 },
449
-//                 data: {
450
-//                     status: "inactive"
451
-//                 }
452
-//             });
453
-//         }
454
-
455
-//         validateData.status = "active";
456
-//     }
457
-
458
-//     const payload = {
459
-//         vendor_id: validateData.vendor_id,
460
-//         simrs_type: validateData.simrs_type,
461
-//         status: validateData.status,
462
-//         contract_start_date: validateData.contract_start_date ? new Date(validateData.contract_start_date) : null,
463
-//         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : null,
464
-//         contract_value_min: validateData.contract_value_min ? Number(validateData.contract_value_min) : null,
465
-//         contract_value_max: validateData.contract_value_max ? Number(validateData.contract_value_max) : null,
466
-//         positive_notes: validateData.positive_notes,
467
-//         negative_notes: validateData.negative_notes,
468
-//         hospital_id: hospitalId
469
-//     };
470
-
471
-//     const data = await VendorExperienceRepository.create(payload);
472
-//     await createLog(req, data);
473
-// };
474
-
475
-// exports.updateVendorHistoryService = async (validateData, req) => {
476
-//     const id_hospital = req.params.id;
477
-//     const id_vendor_experience = req.params.id_vendor_experience;
478
-
479
-//     const hospital = await prisma.hospital.findFirst({
480
-//         where: {
481
-//             id: id_hospital
482
-//         }
483
-//     })
484
-//     if (!hospital) {
485
-//         throw new HttpException("Hospital not found", 404)
486
-//     }
487
-
488
-//     const vendorHistory = await VendorExperienceRepository.findById(id_vendor_experience);
489
-//     if (!vendorHistory) {
490
-//         throw new HttpException("Vendor experience not found", 404);
491
-//     }
492
-
493
-//     if (validateData.vendor_id) {
494
-//         const existVendor = await prisma.vendor.findFirst({
495
-//             where: {
496
-//                 id: validateData.vendor_id
497
-//             }
498
-//         });
499
-//         if (!existVendor) {
500
-//             throw new HttpException("Vendor not found", 404)
501
-//         }
502
-//     }
503
-
504
-//     const SimrsType = ["vendor", "in house", "gratis"];
505
-//     if (validateData.simrs_type && !SimrsType.includes(validateData.simrs_type)) {
506
-//         throw new HttpException("Simrs type must be vendor, in house, or gratis", 400);
507
-//     }
508
-
509
-//     if (
510
-//         validateData.simrs_type &&
511
-//         vendorHistory.simrs_type === "vendor" &&
512
-//         vendorHistory.vendor_id !== null &&
513
-//         validateData.simrs_type !== "vendor"
514
-//     ) {
515
-//         await prisma.vendorExperience.update({
516
-//             where: {
517
-//                 id: id_vendor_experience,
518
-//                 deletedAt: null
519
-//             },
520
-//             data: {
521
-//                 vendor_id: null
522
-//             }
523
-//         });
524
-//     }
525
-
526
-//     if (validateData.contract_start_date && validateData.contract_expired_date) {
527
-//         if (validateData.contract_start_date >= validateData.contract_expired_date) {
528
-//             throw new HttpException("Contract expired date must be after contract date", 400)
529
-//         }
530
-//     }
531
-
532
-//     if (validateData.contract_value_min && validateData.contract_value_max) {
533
-//         if (validateData.contract_value_min >= validateData.contract_value_max) {
534
-//             throw new HttpException("Contract value max must be after contract value min", 400)
535
-//         }
536
-//     }
537
-
538
-//     if (validateData.status === "active") {
539
-//         await prisma.vendorExperience.updateMany({
540
-//             where: {
541
-//                 hospital_id: id_hospital,
542
-//                 status: "active",
543
-//                 deletedAt: null,
544
-//                 NOT: { id: id_vendor_experience },
545
-//             },
546
-//             data: {
547
-//                 status: "inactive",
548
-//             },
549
-//         });
550
-//     }
551
-
552
-//     const payload = {
553
-//         status: validateData.status,
554
-//         contract_start_date: validateData.contract_start_date ? new Date(validateData.contract_start_date) : vendorHistory.contract_start_date,
555
-//         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendorHistory.contract_expired_date,
556
-//         contract_value_min: validateData.contract_value_min ? Number(validateData.contract_value_min) : vendorHistory.contract_value_min,
557
-//         contract_value_max: validateData.contract_value_max ? Number(validateData.contract_value_max) : vendorHistory.contract_value_max,
558
-//         positive_notes: validateData.positive_notes,
559
-//         negative_notes: validateData.negative_notes,
560
-//         simrs_type: validateData.simrs_type,
561
-//         vendor_id: validateData.vendor_id,
562
-//     };
563
-
564
-//     const data = await VendorExperienceRepository.update(id_vendor_experience, payload);
565
-//     await updateLog(req, data);
566
-// };
567
-
568
-// exports.deleteVendorHistoryService = async (req) => {
569
-//     const id_hospital = req.params.id;
570
-//     const id_vendor_experience = req.params.id_vendor_experience;
571
-
572
-//     const hospital = await prisma.hospital.findFirst({
573
-//         where: {
574
-//             id: id_hospital
575
-//         }
576
-//     })
577
-//     if (!hospital) {
578
-//         throw new HttpException("Hospital not found", 404)
579
-//     }
580
-
581
-//     const vendor = await VendorExperienceRepository.findById(id_vendor_experience);
582
-//     if (!vendor) {
583
-//         throw new HttpException("Vendor experience not found", 404);
584
-//     }
585
-
586
-//     const data = await VendorExperienceRepository.update(id_vendor_experience, {
587
-//         deletedAt: timeLocal.now().toDate()
588
-//     });
589
-
590
-//     await deleteLog(req, data);
591
-// };
592
-
593
-// ==============================================================
594
-
595
-// exports.getAllVendorHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
596
-//     const skip = (page - 1) * limit;
597
-
598
-//     const hospitalId = req.params.id;
599
-//     const hospital = await prisma.hospital.findFirst({
600
-//         where: {
601
-//             id: hospitalId
602
-//         }
603
-//     })
604
-//     if (!hospital) {
605
-//         throw new HttpException("Hospital not found", 404)
606
-//     }
607
-
608
-//     const where = {
609
-//         // ...SearchFilter(search, ['name', 'name_pt']),
610
-//         hospital_id: req.params.id,
611
-//         deletedAt: null
612
-//     };
613
-
614
-//     const [vendor_histories, total] = await Promise.all([
615
-//         VendorExperienceRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
616
-//         VendorExperienceRepository.countAll(where)
617
-//     ]);
618
-
619
-//     return { vendor_histories, total };
620
-// };
621
-
622
-// exports.showVendorHistoryService = async (req) => {
623
-//     const id_hospital = req.params.id;
624
-//     const id_vendor_experience = req.params.id_vendor_experience;
625
-
626
-//     const hospital = await prisma.hospital.findFirst({
627
-//         where: {
628
-//             id: id_hospital
629
-//         }
630
-//     })
631
-//     if (!hospital) {
632
-//         throw new HttpException("Hospital not found", 404)
633
-//     }
634
-
635
-//     VendorExperienceRepository.findById(id_vendor_experience);
636
-//     if (!vendorHistory) {
637
-//         throw new HttpException("Vendor history not found", 404);
638
-//     }
639
-
640
-//     return vendorHistory;
641
-// };
642
-
643
-// exports.storeVendorHistoryService = async (validateData, req) => {
644
-//     const hospitalId = req.params.id;
645
-
646
-//     const hospital = await prisma.hospital.findFirst({
647
-//         where: {
648
-//             id: hospitalId
649
-//         }
650
-//     });
651
-//     if (!hospital) {
652
-//         throw new HttpException("Hospital not found", 404)
653
-//     }
654
-
655
-//     const vendor = await prisma.vendor.findFirst({
656
-//         where: {
657
-//             id: validateData.vendor_id
658
-//         }
659
-//     });
660
-//     if (!vendor) {
661
-//         throw new HttpException("Vendor not found", 404)
662
-//     }
663
-
664
-//     await prisma.hospital.update({
665
-//         where: { id: hospitalId },
666
-//         data: {
667
-//             simrs_type: validateData.simrs_type
668
-//         }
669
-//     });
670
-
671
-//     if (validateData.simrs_type) {
672
-//         const existingActiveVendors = await prisma.vendorHistory.findMany({
673
-//             where: {
674
-//                 hospital_id: hospitalId,
675
-//                 status: "active",
676
-//                 deletedAt: null
677
-//             }
678
-//         });
679
-
680
-//         if (existingActiveVendors.length > 0) {
681
-//             await prisma.vendorHistory.updateMany({
682
-//                 where: {
683
-//                     hospital_id: hospitalId,
684
-//                     status: "active",
685
-//                     deletedAt: null
686
-//                 },
687
-//                 data: {
688
-//                     status: "inactive"
689
-//                 }
690
-//             });
691
-//         }
692
-
693
-//         validateData.status = "active";
694
-//     }
695
-
696
-//     const payload = {
697
-//         vendor_id: validateData.vendor_id,
698
-//         vendor_impression: validateData.vendor_impression,
699
-//         status: validateData.status,
700
-//         contract_date: validateData.contract_date,
701
-//         contract_expired_date: validateData.contract_expired_date,
702
-//         hospital_id: hospitalId
703
-//     };
704
-
705
-//     VendorExperienceRepository.create(payload);
706
-//     await createLog(req, data);
707
-// };
708
-
709
-// exports.updateVendorHistoryService = async (validateData, req) => {
710
-//     const id_hospital = req.params.id;
711
-//     const id_vendor_experience = req.params.id_vendor_experience;
712
-
713
-//     const hospital = await prisma.hospital.findFirst({
714
-//         where: {
715
-//             id: id_hospital
716
-//         }
717
-//     })
718
-//     if (!hospital) {
719
-//         throw new HttpException("Hospital not found", 404)
720
-//     }
721
-
722
-//     VendorExperienceRepository.findById(id_vendor_experience);
723
-//     if (!vendorHistory) {
724
-//         throw new HttpException("Vendor history not found", 404);
725
-//     }
726
-
727
-//     if (validateData.vendor_id) {
728
-//         const existVendor = await prisma.vendor.findFirst({
729
-//             where: {
730
-//                 id: validateData.vendor_id
731
-//             }
732
-//         });
733
-//         if (!existVendor) {
734
-//             throw new HttpException("Vendor not found", 404)
735
-//         }
736
-//     }
737
-
738
-//     await prisma.hospital.update({
739
-//         where: { id: id_hospital },
740
-//         data: {
741
-//             simrs_type: validateData.simrs_type
742
-//         }
743
-//     });
744
-
745
-//     if (validateData.contract_date && validateData.contract_expired_date) {
746
-//         if (validateData.contract_date >= validateData.contract_expired_date) {
747
-//             throw new HttpException("Contract expired date must be after contract date", 400)
748
-//         }
749
-//     }
750
-
751
-//     if (validateData.status === "active") {
752
-//         await prisma.vendorHistory.updateMany({
753
-//             where: {
754
-//                 hospital_id: id_hospital,
755
-//                 status: "active",
756
-//                 deletedAt: null,
757
-//                 NOT: { id: id_vendor_experience },
758
-//             },
759
-//             data: {
760
-//                 status: "inactive",
761
-//             },
762
-//         });
763
-//     }
764
-
765
-//     const payload = {
766
-//         vendor_id: validateData.vendor_id,
767
-//         vendor_impression: validateData.vendor_impression,
768
-//         status: validateData.status,
769
-//         contract_date: validateData.contract_date ? new Date(validateData.contract_date) : vendorHistory.contract_date,
770
-//         contract_expired_date: validateData.contract_expired_date ? new Date(validateData.contract_expired_date) : vendorHistory.contract_expired_date,
771
-//     };
772
-
773
-//     VendorExperienceRepository.update(id_vendor_experience, payload);
774
-//     await updateLog(req, data);
775
-// };
776
-
777
-// exports.deleteVendorHistoryService = async (req) => {
778
-//     const id_hospital = req.params.id;
779
-//     const id_vendor_experience = req.params.id_vendor_experience;
780
-
781
-//     const hospital = await prisma.hospital.findFirst({
782
-//         where: {
783
-//             id: id_hospital
784
-//         }
785
-//     })
786
-//     if (!hospital) {
787
-//         throw new HttpException("Hospital not found", 404)
788
-//     }
789
-
790
-//     VendorExperienceRepository.findById(id_vendor_experience);
791
-//     if (!vendor) {
792
-//         throw new HttpException("Vendor history not found", 404);
793
-//     }
794
-
795
-//     VendorExperienceRepository.update(id_vendor_experience, {
796
-//         deletedAt: timeLocal.now().toDate()
797
-//     });
798
-
799
-//     await deleteLog(req, data);
800
-// };
369
+};

+ 19 - 0
src/types/admin/category/CategoryDTO.ts

@@ -0,0 +1,19 @@
1
+export interface CategoryRequestDTO {
2
+    tag: string;
3
+    description?: string | null;
4
+}
5
+
6
+export interface CategoryDTO {
7
+    id: string;
8
+    tag: string;
9
+    description?: string | null;
10
+    count_use_tags?: number | null;
11
+    createdAt: Date;
12
+    updatedAt: Date;
13
+}
14
+
15
+export interface MergeCategoryRequestDTO {
16
+    category_id: string[];
17
+    tag: string;
18
+    description?: string | null;
19
+}

+ 62 - 0
src/types/admin/category_link/CategoryLinkDTO.ts

@@ -0,0 +1,62 @@
1
+export interface CategoryLinkRequestDTO {
2
+    category_id: string;
3
+    source_type?: string | null;
4
+    source_id?: string | null;
5
+}
6
+
7
+export interface CategoryLinkDTO {
8
+    id: string;
9
+    category_id: string;
10
+    source_type?: string | null;
11
+    source_id?: string | null;
12
+    createdAt: Date;
13
+    updatedAt: Date;
14
+}
15
+
16
+export interface CategoryLinkUseDTO {
17
+    id: string;
18
+    category_id: string;
19
+    source_type?: string | null;
20
+    source_id?: string | null;
21
+    createdAt: Date;
22
+    updatedAt?: Date;
23
+
24
+    vendor_experience?: {
25
+        id: string;
26
+        status?: string;
27
+        contract_start_date?: Date;
28
+        contract_expired_date?: Date;
29
+        contract_value_min?: number;
30
+        contract_value_max?: number;
31
+        positive_notes?: string;
32
+        negative_notes?: string;
33
+        simrs_type: string;
34
+    } | null;
35
+
36
+    status_history?: {
37
+        id: string;
38
+        old_status: string;
39
+        new_status: string;
40
+        note: string;
41
+    } | null;
42
+
43
+    hospital?: {
44
+        id: string,
45
+        name: string,
46
+        hospital_code: string,
47
+        ownership: string,
48
+        address: string,
49
+        contact: string,
50
+        progress_status: string,
51
+        note: string,
52
+    } | null;
53
+
54
+    vendor?: {
55
+        id: string;
56
+        name: string;
57
+        name_pt: string;
58
+        strengths: string;
59
+        weaknesses: string;
60
+        website: string;
61
+    } | null;
62
+}

+ 4 - 1
src/types/admin/hospital/HospitalDTO.ts

@@ -9,11 +9,12 @@ export interface HospitalRequestDTO {
9 9
     city_id?: string;
10 10
     address?: string | null;
11 11
     contact?: string | null;
12
-    note?: string | null;
13 12
     gmaps_url?: string | null;
14 13
     latitude?: number | null;
15 14
     longitude?: number | null;
16 15
     progress_status?: ProgressStatus;
16
+    note: string;
17
+    tags: string[];
17 18
 }
18 19
 
19 20
 export interface HospitalDTO {
@@ -35,6 +36,7 @@ export interface HospitalDTO {
35 36
     image: string | null;
36 37
     progress_status: ProgressStatus;
37 38
     note: string | null;
39
+    note_tags?: string[];
38 40
     latitude: number | null;
39 41
     longitude: number | null;
40 42
     gmaps_url: string | null;
@@ -52,4 +54,5 @@ export interface HospitalDTO {
52 54
         weaknesses: string | null;
53 55
         website: string | null;
54 56
     } | null;
57
+    created_by?: string;
55 58
 }

+ 5 - 1
src/types/admin/status_history/StatusHistoryDTO.ts

@@ -2,7 +2,10 @@ import { ProgressStatus } from "@prisma/client";
2 2
 
3 3
 export interface StatusHistoryRequestDTO {
4 4
     new_status: string;
5
-    note?: string | null;
5
+    note?: {
6
+        note: string;
7
+        tags: string[];
8
+    };
6 9
 }
7 10
 
8 11
 export interface StatusHistoryDTO {
@@ -14,6 +17,7 @@ export interface StatusHistoryDTO {
14 17
     old_status: ProgressStatus;
15 18
     new_status: ProgressStatus;
16 19
     note: string | null;
20
+    note_tags?: string[];
17 21
     createdAt: Date;
18 22
     updatedAt: Date;
19 23
 }

+ 10 - 2
src/types/admin/vendor/VendorDTO.ts

@@ -1,9 +1,15 @@
1 1
 export interface VendorRequestDTO {
2 2
     name: string;
3 3
     name_pt: string;
4
-    strengths: string;
5
-    weaknesses: string;
6 4
     website: string;
5
+    strengths?: {
6
+        note: string;
7
+        tags: string[];
8
+    };
9
+    weaknesses?: {
10
+        note: string;
11
+        tags: string[];
12
+    };
7 13
 }
8 14
 
9 15
 export interface VendorDTO {
@@ -20,4 +26,6 @@ export interface VendorDTO {
20 26
     createdAt: Date;
21 27
     updatedAt: Date;
22 28
     count_hospitals: number;
29
+    strengths_tags?: string[];
30
+    weaknesses_tags?: string[];
23 31
 }

+ 10 - 2
src/types/admin/vendor_experience/VendorExperienceDTO.ts

@@ -6,8 +6,14 @@ export interface VendorExperienceRequestDTO {
6 6
     contract_expired_date?: Date | null;
7 7
     contract_value_min?: number | null;
8 8
     contract_value_max?: number | null;
9
-    positive_notes?: string;
10
-    negative_notes?: string;
9
+    positive_notes?: {
10
+        note: string;
11
+        tags: string[];
12
+    };
13
+    negative_notes?: {
14
+        note: string;
15
+        tags: string[];
16
+    };
11 17
 }
12 18
 
13 19
 export interface VendorExperienceDTO {
@@ -18,6 +24,8 @@ export interface VendorExperienceDTO {
18 24
     contract_value_max: bigint | null;
19 25
     positive_notes: string | null;
20 26
     negative_notes: string | null;
27
+    positive_notes_tags?: string[];
28
+    negative_notes_tags?: string[];
21 29
     status: string | null;
22 30
     simrs_type: string;
23 31
     createdAt: Date;

+ 5 - 2
src/types/sales/hospital/HospitalDTO.ts

@@ -9,11 +9,12 @@ export interface HospitalRequestDTO {
9 9
     city_id?: string;
10 10
     address?: string | null;
11 11
     contact?: string | null;
12
-    note?: string | null;
13 12
     gmaps_url?: string | null;
14 13
     latitude?: number | null;
15 14
     longitude?: number | null;
16 15
     progress_status?: ProgressStatus;
16
+    note: string;
17
+    tags: string[];
17 18
 }
18 19
 
19 20
 export interface HospitalDTO {
@@ -35,9 +36,11 @@ export interface HospitalDTO {
35 36
     image: string | null;
36 37
     progress_status: ProgressStatus;
37 38
     note: string | null;
39
+    note_tags?: string[];
38 40
     latitude: number | null;
39 41
     longitude: number | null;
40 42
     gmaps_url: string | null;
43
+    created_by?: string
41 44
     createdAt: Date;
42 45
     updatedAt: Date;
43 46
     user: {
@@ -52,4 +55,4 @@ export interface HospitalDTO {
52 55
         weaknesses: string | null;
53 56
         website: string | null;
54 57
     } | null;
55
-}
58
+}

+ 5 - 1
src/types/sales/status_history/StatusHistoryDTO.ts

@@ -2,7 +2,10 @@ import { ProgressStatus } from "@prisma/client";
2 2
 
3 3
 export interface StatusHistoryRequestDTO {
4 4
     new_status: string;
5
-    note?: string | null;
5
+    note?: {
6
+        note: string;
7
+        tags: string[];
8
+    };
6 9
 }
7 10
 
8 11
 export interface StatusHistoryDTO {
@@ -14,6 +17,7 @@ export interface StatusHistoryDTO {
14 17
     old_status: ProgressStatus;
15 18
     new_status: ProgressStatus;
16 19
     note: string | null;
20
+    note_tags?: string[];
17 21
     createdAt: Date;
18 22
     updatedAt: Date;
19 23
 }

+ 10 - 2
src/types/sales/vendor_experience/VendorExperienceDTO.ts

@@ -6,8 +6,14 @@ export interface VendorExperienceRequestDTO {
6 6
     contract_expired_date?: Date | null;
7 7
     contract_value_min?: number | null;
8 8
     contract_value_max?: number | null;
9
-    positive_notes?: string;
10
-    negative_notes?: string;
9
+    positive_notes?: {
10
+        note: string;
11
+        tags: string[];
12
+    };
13
+    negative_notes?: {
14
+        note: string;
15
+        tags: string[];
16
+    };
11 17
 }
12 18
 
13 19
 export interface VendorExperienceDTO {
@@ -18,6 +24,8 @@ export interface VendorExperienceDTO {
18 24
     contract_value_max: bigint | null;
19 25
     positive_notes: string | null;
20 26
     negative_notes: string | null;
27
+    positive_notes_tags?: string[];
28
+    negative_notes_tags?: string[];
21 29
     status: string | null;
22 30
     simrs_type: string;
23 31
     createdAt: Date;

+ 61 - 0
src/utils/GetSourceDataByType.ts

@@ -0,0 +1,61 @@
1
+import prisma from '../prisma/PrismaClient';
2
+
3
+export const GetSourceDataByType = async (source_type: string, source_id: string) => {
4
+    switch (source_type) {
5
+        case 'vendor_experience_negative_notes':
6
+        case 'vendor_experience_positive_notes':
7
+            return await prisma.vendorExperience.findFirst({
8
+                where: { id: source_id },
9
+                select: {
10
+                    id: true,
11
+                    status: true,
12
+                    contract_start_date: true,
13
+                    contract_expired_date: true,
14
+                    contract_value_min: true,
15
+                    contract_value_max: true,
16
+                    positive_notes: true,
17
+                    negative_notes: true,
18
+                    simrs_type: true,
19
+                }
20
+            });
21
+        case 'status_history_notes':
22
+            return await prisma.statusHistory.findFirst({
23
+                where: { id: source_id },
24
+                select: {
25
+                    id: true,
26
+                    old_status: true,
27
+                    new_status: true,
28
+                    note: true
29
+                }
30
+            });
31
+        case 'hospital_notes':
32
+            return await prisma.hospital.findFirst({
33
+                where: { id: source_id },
34
+                select: {
35
+                    id: true,
36
+                    name: true,
37
+                    hospital_code: true,
38
+                    ownership: true,
39
+                    address: true,
40
+                    contact: true,
41
+                    progress_status: true,
42
+                    note: true,
43
+                }
44
+            });
45
+        case 'vendor_strength_notes':
46
+        case 'vendor_weaknesses_notes':
47
+            return await prisma.vendor.findFirst({
48
+                where: { id: source_id },
49
+                select: {
50
+                    id: true,
51
+                    name: true,
52
+                    name_pt: true,
53
+                    strengths: true,
54
+                    weaknesses: true,
55
+                    website: true,
56
+                }
57
+            });
58
+        default:
59
+            return null;
60
+    }
61
+};

+ 1 - 15
src/utils/TimeLocal.ts

@@ -7,20 +7,6 @@ import timezone from 'dayjs/plugin/timezone';
7 7
 dayjs.extend(utc);
8 8
 dayjs.extend(timezone);
9 9
 
10
-// Fungsi untuk mendapatkan waktu sekarang di zona waktu Asia/Jakarta
11 10
 const now = (): dayjs.Dayjs => dayjs().tz('Asia/Jakarta');
12 11
 
13
-export { now };
14
-
15
-
16
-// // utils/TimeLocal.js
17
-// const dayjs = require('dayjs');
18
-// const utc = require('dayjs/plugin/utc');
19
-// const timezone = require('dayjs/plugin/timezone');
20
-
21
-// dayjs.extend(utc);
22
-// dayjs.extend(timezone);
23
-
24
-// const now = () => dayjs().tz('Asia/Jakarta');
25
-
26
-// module.exports = { now };
12
+export { now };

+ 49 - 0
src/validators/admin/category/CategoryValidators.ts

@@ -0,0 +1,49 @@
1
+import Joi from 'joi';
2
+import { validateWithSchema } from '../../ValidateSchema';
3
+import { CategoryRequestDTO, MergeCategoryRequestDTO } from '../../../types/admin/category/CategoryDTO';
4
+
5
+const storeCategorySchema = Joi.object<CategoryRequestDTO>({
6
+    tag: Joi.string().trim().required().messages({
7
+        'string.empty': 'Tag is required',
8
+    }),
9
+    description: Joi.string().trim().optional().allow(''),
10
+});
11
+
12
+const updateCategorySchema = Joi.object<Partial<CategoryRequestDTO>>({
13
+    tag: Joi.string().trim().optional().messages({
14
+        'string.empty': 'Tag is required',
15
+    }),
16
+    description: Joi.string().trim().optional().allow(''),
17
+});
18
+
19
+const mergeCategorySchema = Joi.object<MergeCategoryRequestDTO>({
20
+    category_id: Joi.array()
21
+        .items(Joi.string().trim().min(2))
22
+        .min(1)
23
+        .required()
24
+        .messages({
25
+            'any.required': 'Category ID is required',
26
+            'array.base': 'Category ID must be an array of strings',
27
+            'array.min': 'At least one category ID is required',
28
+            'string.empty': 'Each Category ID must not be empty',
29
+            'string.min': 'Each Category ID must not be empty',
30
+        }),
31
+
32
+    tag: Joi.string().trim().required().messages({
33
+        'string.empty': 'Tag is required',
34
+        'any.required': 'Tag is required',
35
+    }),
36
+    description: Joi.string().trim().optional().allow(''),
37
+});
38
+
39
+export const validateCategoryStoreRequest = (body: unknown): CategoryRequestDTO => {
40
+    return validateWithSchema<CategoryRequestDTO>(storeCategorySchema, body);
41
+};
42
+
43
+export const validateCategoryUpdateRequest = (body: unknown): CategoryRequestDTO => {
44
+    return validateWithSchema<CategoryRequestDTO>(updateCategorySchema, body);
45
+}
46
+
47
+export const validateMergeCategoryStoreRequest = (body: unknown): MergeCategoryRequestDTO => {
48
+    return validateWithSchema<MergeCategoryRequestDTO>(mergeCategorySchema, body);
49
+};

+ 5 - 178
src/validators/admin/hospital/HospitalValidators.ts

@@ -27,9 +27,8 @@ export const storeHospitalSchema = Joi.object({
27 27
     contact: Joi.string().trim().required().messages({
28 28
         'string.empty': 'contact is required',
29 29
     }),
30
-    note: Joi.string().trim().required().messages({
31
-        'string.empty': 'note is required',
32
-    }),
30
+    note: Joi.string().allow('', null),
31
+    tags: Joi.array().items(Joi.string()).optional().default([]),
33 32
     gmaps_url: Joi.string().trim().optional().allow(null, ''),
34 33
     image: Joi.any().optional().allow(null, ''),
35 34
     latitude: Joi.number().optional().allow(null),
@@ -45,7 +44,8 @@ export const updateHospitalSchema = Joi.object({
45 44
     city_id: Joi.string().trim().optional(),
46 45
     address: Joi.string().trim().optional(),
47 46
     contact: Joi.string().trim().optional(),
48
-    note: Joi.string().trim().optional(),
47
+    note: Joi.string().allow('', null),
48
+    tags: Joi.array().items(Joi.string()).optional().default([]),
49 49
     image: Joi.any().optional().allow(null, ''),
50 50
     gmaps_url: Joi.string().trim().optional().allow(null, ''),
51 51
     latitude: Joi.number().optional().allow(null),
@@ -58,177 +58,4 @@ export const validateStoreHospitalRequest = (body: unknown): HospitalRequestDTO
58 58
 
59 59
 export const validateUpdateHospitalRequest = (body: unknown): Partial<HospitalRequestDTO> => {
60 60
     return validateWithSchema<Partial<HospitalRequestDTO>>(updateHospitalSchema, body);
61
-}
62
-
63
-// import { HospitalRequestDTO } from '../../../types/admin/hospital/HospitalDTO';
64
-// import { HttpException } from '../../../utils/HttpException';
65
-
66
-// const parseToNullableFloat = (value: unknown): number | null => {
67
-//     if (typeof value === 'number') return value;
68
-//     if (typeof value === 'string' && value.trim() !== '') return parseFloat(value);
69
-//     return null;
70
-// };
71
-
72
-// const safeTrim = (value: unknown): string | undefined => {
73
-//     return typeof value === 'string' ? value.trim() : undefined;
74
-// };
75
-
76
-// export const validateStoreHospitalRequest = (body: HospitalRequestDTO): HospitalRequestDTO => {
77
-//     const {
78
-//         name,
79
-//         hospital_code,
80
-//         type,
81
-//         ownership,
82
-//         province_id,
83
-//         city_id,
84
-//         address,
85
-//         contact,
86
-//         note,
87
-//         gmaps_url,
88
-//         latitude,
89
-//         longitude,
90
-//     } = body;
91
-
92
-//     const errors: Record<string, string[]> = {};
93
-
94
-//     if (!name?.trim()) errors.name = ['name is required'];
95
-//     if (!hospital_code?.trim()) errors.hospital_code = ['hospital code is required'];
96
-//     if (!type?.trim()) errors.type = ['type is required'];
97
-//     if (!ownership?.trim()) errors.ownership = ['ownership is required'];
98
-//     if (!province_id?.trim()) errors.province_id = ['province id is required'];
99
-//     if (!city_id?.trim()) errors.city_id = ['city id is required'];
100
-//     if (!address?.trim()) errors.address = ['address is required'];
101
-//     if (!contact?.trim()) errors.contact = ['contact is required'];
102
-//     if (!note?.trim()) errors.note = ['note is required'];
103
-
104
-//     if (Object.keys(errors).length > 0) {
105
-//         throw new HttpException(errors, 422);
106
-//     }
107
-
108
-//     return {
109
-//         name: safeTrim(name)!,
110
-//         hospital_code: safeTrim(hospital_code)!,
111
-//         type: safeTrim(type)!,
112
-//         ownership: safeTrim(ownership)!,
113
-//         province_id: safeTrim(province_id)!,
114
-//         city_id: safeTrim(city_id)!,
115
-//         address: safeTrim(address)!,
116
-//         contact: safeTrim(contact)!,
117
-//         note: safeTrim(note)!,
118
-//         gmaps_url: safeTrim(gmaps_url) ?? null,
119
-//         latitude: parseToNullableFloat(latitude),
120
-//         longitude: parseToNullableFloat(longitude),
121
-//     };
122
-// };
123
-
124
-// export const validateUpdateHospitalRequest = (body: HospitalRequestDTO): Partial<HospitalRequestDTO> => {
125
-//     return {
126
-//         ...body,
127
-//         gmaps_url: safeTrim(body.gmaps_url) ?? null,
128
-//         latitude: parseToNullableFloat(body.latitude),
129
-//         longitude: parseToNullableFloat(body.longitude),
130
-//     };
131
-// };
132
-
133
-// import { HttpException } from '../../../utils/HttpException';
134
-
135
-// interface HospitalDTO {
136
-//     name: string;
137
-//     hospital_code: string;
138
-//     type: string;
139
-//     ownership: string;
140
-//     province_id: string;
141
-//     city_id: string;
142
-//     address: string;
143
-//     contact: string;
144
-//     note: string;
145
-//     gmaps_url?: string | null;
146
-//     latitude?: string | number | null;
147
-//     longitude?: string | number | null;
148
-// }
149
-
150
-// interface HospitalUpdateDTO extends Partial<HospitalDTO> { }
151
-
152
-// interface ErrorObject {
153
-//     [key: string]: string[];
154
-// }
155
-
156
-// export const validateStoreHospitalRequest = (body: Partial<HospitalDTO>): HospitalDTO => {
157
-//     const {
158
-//         name,
159
-//         hospital_code,
160
-//         type,
161
-//         ownership,
162
-//         province_id,
163
-//         city_id,
164
-//         address,
165
-//         contact,
166
-//         note,
167
-//         gmaps_url,
168
-//         latitude,
169
-//         longitude
170
-//     } = body;
171
-
172
-//     const errors: ErrorObject = {};
173
-
174
-//     if (!name?.trim()) errors.name = ['name is required'];
175
-//     if (!hospital_code?.trim()) errors.hospital_code = ['hospital code is required'];
176
-//     if (!type?.trim()) errors.type = ['type is required'];
177
-//     if (!ownership?.trim()) errors.ownership = ['ownership is required'];
178
-//     if (!province_id?.trim()) errors.province_id = ['province id is required'];
179
-//     if (!city_id?.trim()) errors.city_id = ['city id is required'];
180
-//     if (!address?.trim()) errors.address = ['address is required'];
181
-//     if (!contact?.trim()) errors.contact = ['contact is required'];
182
-//     if (!note?.trim()) errors.note = ['note is required'];
183
-
184
-//     if (Object.keys(errors).length > 0) {
185
-//         throw new HttpException(errors, 422);
186
-//     }
187
-
188
-//     return {
189
-//         name: name!.trim(),
190
-//         hospital_code: hospital_code!.trim(),
191
-//         type: type!.trim(),
192
-//         ownership: ownership!.trim(),
193
-//         province_id: province_id!.trim(),
194
-//         city_id: city_id!.trim(),
195
-//         address: address!.trim(),
196
-//         contact: contact!.trim(),
197
-//         note: note!.trim(),
198
-//         gmaps_url: gmaps_url?.trim() || null,
199
-//         latitude: latitude != null ? parseFloat(latitude as string) : null,
200
-//         longitude: longitude != null ? parseFloat(longitude as string) : null,
201
-//     };
202
-// };
203
-
204
-// export const validateUpdateHospitalRequest = (body: HospitalUpdateDTO): HospitalUpdateDTO => {
205
-//     const {
206
-//         name,
207
-//         hospital_code,
208
-//         type,
209
-//         ownership,
210
-//         province_id,
211
-//         city_id,
212
-//         address,
213
-//         contact,
214
-//         note,
215
-//         gmaps_url,
216
-//         latitude,
217
-//         longitude
218
-//     } = body;
219
-
220
-//     return {
221
-//         name,
222
-//         hospital_code,
223
-//         type,
224
-//         ownership,
225
-//         province_id,
226
-//         city_id,
227
-//         address,
228
-//         contact,
229
-//         note,
230
-//         gmaps_url: gmaps_url ? gmaps_url.trim() : null,
231
-//         latitude: latitude !== undefined && latitude !== null ? parseFloat(latitude as string) : null,
232
-//         longitude: longitude !== undefined && longitude !== null ? parseFloat(longitude as string) : null,
233
-//     };
234
-// };
61
+}

+ 6 - 27
src/validators/admin/status_history/StatusHistoryValidators.ts

@@ -13,34 +13,13 @@ const statusHistorySchema = Joi.object<StatusHistoryRequestDTO>({
13 13
             'any.only': `new_status must be one of: ${allowedStatuses.join(', ')}`,
14 14
             'string.base': 'new_status must be a string',
15 15
         }),
16
-    note: Joi.string().trim().allow(null, '').optional(),
16
+    // note: Joi.string().trim().allow(null, '').optional(),
17
+    note: Joi.object({
18
+        note: Joi.string().allow('', null),
19
+        tags: Joi.array().items(Joi.string()).optional().default([]),
20
+    }).optional().allow(null),
17 21
 });
18 22
 
19 23
 export const validateCreateStatusHisotryRequest = (body: unknown): StatusHistoryRequestDTO => {
20 24
     return validateWithSchema<StatusHistoryRequestDTO>(statusHistorySchema, body);
21
-};
22
-
23
-// import { StatusHistoryRequestDTO } from '../../../types/admin/status_history/StatusHistoryDTO';
24
-// import { HttpException } from '../../../utils/HttpException';
25
-
26
-// export const validateCreateStatusHisotryRequest = (
27
-//     body: StatusHistoryRequestDTO
28
-// ): StatusHistoryRequestDTO => {
29
-//     const errors: Record<string, string[]> = {};
30
-
31
-//     const new_status = body.new_status?.trim();
32
-//     const note = body.note?.trim();
33
-
34
-//     if (!new_status) {
35
-//         errors.new_status = ['new_status is required'];
36
-//     }
37
-
38
-//     if (Object.keys(errors).length > 0) {
39
-//         throw new HttpException(errors, 422);
40
-//     }
41
-
42
-//     return {
43
-//         new_status,
44
-//         note: note ? note : null,
45
-//     };
46
-// };
25
+};

+ 30 - 12
src/validators/admin/vendor/VendorValidators.ts

@@ -9,15 +9,24 @@ const vendorSchema = Joi.object<VendorRequestDTO>({
9 9
     name_pt: Joi.string().trim().required().messages({
10 10
         'string.empty': 'Vendor name pt is required',
11 11
     }),
12
-    strengths: Joi.string().trim().required().messages({
13
-        'string.empty': 'Vendor strengths is required',
14
-    }),
15
-    weaknesses: Joi.string().trim().required().messages({
16
-        'string.empty': 'Vendor weaknesses is required',
17
-    }),
12
+    // strengths: Joi.string().trim().required().messages({
13
+    //     'string.empty': 'Vendor strengths is required',
14
+    // }),
15
+    // weaknesses: Joi.string().trim().required().messages({
16
+    //     'string.empty': 'Vendor weaknesses is required',
17
+    // }),
18 18
     website: Joi.string().trim().required().messages({
19 19
         'string.empty': 'Vendor website is required',
20 20
     }),
21
+    strengths: Joi.object({
22
+        note: Joi.string().allow('', null),
23
+        tags: Joi.array().items(Joi.string()).optional().default([]),
24
+    }).optional().allow(null),
25
+
26
+    weaknesses: Joi.object({
27
+        note: Joi.string().allow('', null),
28
+        tags: Joi.array().items(Joi.string()).optional().default([]),
29
+    }).optional().allow(null),
21 30
 });
22 31
 
23 32
 const updateVendorSchema = Joi.object<Partial<VendorRequestDTO>>({
@@ -27,15 +36,24 @@ const updateVendorSchema = Joi.object<Partial<VendorRequestDTO>>({
27 36
     name_pt: Joi.string().trim().optional().messages({
28 37
         'string.empty': 'Vendor name_pt is required',
29 38
     }),
30
-    strengths: Joi.string().trim().optional().messages({
31
-        'string.empty': 'Vendor strengths is required',
32
-    }),
33
-    weaknesses: Joi.string().trim().optional().messages({
34
-        'string.empty': 'Vendor weaknesses is required',
35
-    }),
39
+    // strengths: Joi.string().trim().optional().messages({
40
+    //     'string.empty': 'Vendor strengths is required',
41
+    // }),
42
+    // weaknesses: Joi.string().trim().optional().messages({
43
+    //     'string.empty': 'Vendor weaknesses is required',
44
+    // }),
36 45
     website: Joi.string().trim().optional().messages({
37 46
         'string.empty': 'Vendor website is required',
38 47
     }),
48
+    strengths: Joi.object({
49
+        note: Joi.string().allow('', null),
50
+        tags: Joi.array().items(Joi.string()).optional().default([]),
51
+    }).optional().allow(null),
52
+
53
+    weaknesses: Joi.object({
54
+        note: Joi.string().allow('', null),
55
+        tags: Joi.array().items(Joi.string()).optional().default([]),
56
+    }).optional().allow(null),
39 57
 });
40 58
 
41 59
 export const validateStoreVendorRequest = (body: unknown): VendorRequestDTO => {

+ 19 - 74
src/validators/admin/vendor_experience/VendorExperienceValidators.ts

@@ -16,8 +16,15 @@ export const storeVendorExperienceSchema = Joi.object({
16 16
     contract_expired_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract expired date must be a valid date' }),
17 17
     contract_value_min: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value min must be a number' }),
18 18
     contract_value_max: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value max must be a number' }),
19
-    positive_notes: Joi.string().optional().allow('', null),
20
-    negative_notes: Joi.string().optional().allow('', null),
19
+    positive_notes: Joi.object({
20
+        note: Joi.string().allow('', null),
21
+        tags: Joi.array().items(Joi.string()).optional().default([]),
22
+    }).optional().allow(null),
23
+
24
+    negative_notes: Joi.object({
25
+        note: Joi.string().allow('', null),
26
+        tags: Joi.array().items(Joi.string()).optional().default([]),
27
+    }).optional().allow(null),
21 28
 });
22 29
 
23 30
 export const updateVendorExperienceSchema = Joi.object({
@@ -28,8 +35,15 @@ export const updateVendorExperienceSchema = Joi.object({
28 35
     contract_expired_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract expired date must be a valid date' }),
29 36
     contract_value_min: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value min must be a number' }),
30 37
     contract_value_max: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value max must be a number' }),
31
-    positive_notes: Joi.string().optional().allow('', null),
32
-    negative_notes: Joi.string().optional().allow('', null),
38
+    positive_notes: Joi.object({
39
+        note: Joi.string().allow('', null),
40
+        tags: Joi.array().items(Joi.string()).optional().default([]),
41
+    }).optional().allow(null),
42
+
43
+    negative_notes: Joi.object({
44
+        note: Joi.string().allow('', null),
45
+        tags: Joi.array().items(Joi.string()).optional().default([]),
46
+    }).optional().allow(null),
33 47
 });
34 48
 
35 49
 export const validateStoreVendorExperienceRequest = (body: unknown): VendorExperienceRequestDTO => {
@@ -38,73 +52,4 @@ export const validateStoreVendorExperienceRequest = (body: unknown): VendorExper
38 52
 
39 53
 export const validateUpdateVendorExperienceRequest = (body: unknown): Partial<VendorExperienceRequestDTO> => {
40 54
     return validateWithSchema<Partial<VendorExperienceRequestDTO>>(updateVendorExperienceSchema, body);
41
-}
42
-
43
-// import { VendorExperienceRequestDTO } from '../../../types/admin/vendor_experience/VendorExperienceDTO';
44
-// import { HttpException } from '../../../utils/HttpException';
45
-
46
-// export const validateStoreVendorExperienceRequest = (body: VendorExperienceRequestDTO): VendorExperienceRequestDTO => {
47
-//     const {
48
-//         simrs_type,
49
-//         vendor_id,
50
-//         status,
51
-//         contract_start_date,
52
-//         contract_expired_date,
53
-//         contract_value_min,
54
-//         contract_value_max,
55
-//         positive_notes,
56
-//         negative_notes,
57
-//     } = body;
58
-
59
-//     const errors: Record<string, string[]> = {};
60
-
61
-//     if (!simrs_type || simrs_type.trim() === '') {
62
-//         errors.simrs_type = ['simrs type is required'];
63
-//     }
64
-
65
-//     if (!status || status.trim() === '') {
66
-//         errors.status = ['Status is required'];
67
-//     }
68
-
69
-//     if (Object.keys(errors).length > 0) {
70
-//         throw new HttpException(errors, 422);
71
-//     }
72
-
73
-//     return {
74
-//         simrs_type,
75
-//         vendor_id,
76
-//         status,
77
-//         contract_start_date,
78
-//         contract_expired_date,
79
-//         contract_value_min,
80
-//         contract_value_max,
81
-//         positive_notes,
82
-//         negative_notes,
83
-//     };
84
-// };
85
-
86
-// export const validateUpdateVendorExperienceRequest = (body: VendorExperienceRequestDTO): Partial<VendorExperienceRequestDTO> => {
87
-//     const {
88
-//         simrs_type,
89
-//         vendor_id,
90
-//         status,
91
-//         contract_start_date,
92
-//         contract_expired_date,
93
-//         contract_value_min,
94
-//         contract_value_max,
95
-//         positive_notes,
96
-//         negative_notes,
97
-//     } = body;
98
-
99
-//     return {
100
-//         simrs_type,
101
-//         vendor_id,
102
-//         status,
103
-//         contract_start_date,
104
-//         contract_expired_date,
105
-//         contract_value_min,
106
-//         contract_value_max,
107
-//         positive_notes,
108
-//         negative_notes,
109
-//     };
110
-// };
55
+}

+ 6 - 27
src/validators/sales/status_history/StatusHistoryValidators.ts

@@ -13,34 +13,13 @@ const statusHistorySchema = Joi.object<StatusHistoryRequestDTO>({
13 13
             'any.only': `new_status must be one of: ${allowedStatuses.join(', ')}`,
14 14
             'string.base': 'new_status must be a string',
15 15
         }),
16
-    note: Joi.string().trim().allow(null, '').optional(),
16
+    // note: Joi.string().trim().allow(null, '').optional(),
17
+    note: Joi.object({
18
+        note: Joi.string().allow('', null),
19
+        tags: Joi.array().items(Joi.string()).optional().default([]),
20
+    }).optional().allow(null),
17 21
 });
18 22
 
19 23
 export const validateCreateStatusHisotryRequest = (body: unknown): StatusHistoryRequestDTO => {
20 24
     return validateWithSchema<StatusHistoryRequestDTO>(statusHistorySchema, body);
21
-};
22
-
23
-// import { StatusHistoryRequestDTO } from '../../../types/sales/status_history/StatusHistoryDTO';
24
-// import { HttpException } from '../../../utils/HttpException';
25
-
26
-// export const validateCreateStatusHisotryRequest = (
27
-//     body: StatusHistoryRequestDTO
28
-// ): StatusHistoryRequestDTO => {
29
-//     const errors: Record<string, string[]> = {};
30
-
31
-//     const new_status = body.new_status?.trim();
32
-//     const note = body.note?.trim();
33
-
34
-//     if (!new_status) {
35
-//         errors.new_status = ['new_status is required'];
36
-//     }
37
-
38
-//     if (Object.keys(errors).length > 0) {
39
-//         throw new HttpException(errors, 422);
40
-//     }
41
-
42
-//     return {
43
-//         new_status,
44
-//         note: note ? note : null,
45
-//     };
46
-// };
25
+};

+ 55 - 69
src/validators/sales/vendor_experience/VendorExperienceValidators.ts

@@ -1,6 +1,6 @@
1 1
 import Joi from 'joi';
2 2
 import { validateWithSchema } from '../../ValidateSchema';
3
-import { VendorExperienceRequestDTO } from '../../../types/sales/vendor_experience/VendorExperienceDTO';
3
+import { VendorExperienceRequestDTO } from '../../../types/admin/vendor_experience/VendorExperienceDTO';
4 4
 
5 5
 export const storeVendorExperienceSchema = Joi.object({
6 6
     simrs_type: Joi.string().trim().required().messages({
@@ -16,8 +16,15 @@ export const storeVendorExperienceSchema = Joi.object({
16 16
     contract_expired_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract expired date must be a valid date' }),
17 17
     contract_value_min: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value min must be a number' }),
18 18
     contract_value_max: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value max must be a number' }),
19
-    positive_notes: Joi.string().optional().allow('', null),
20
-    negative_notes: Joi.string().optional().allow('', null),
19
+    positive_notes: Joi.object({
20
+        note: Joi.string().allow('', null),
21
+        tags: Joi.array().items(Joi.string()).optional().default([]),
22
+    }).optional().allow(null),
23
+
24
+    negative_notes: Joi.object({
25
+        note: Joi.string().allow('', null),
26
+        tags: Joi.array().items(Joi.string()).optional().default([]),
27
+    }).optional().allow(null),
21 28
 });
22 29
 
23 30
 export const updateVendorExperienceSchema = Joi.object({
@@ -28,8 +35,15 @@ export const updateVendorExperienceSchema = Joi.object({
28 35
     contract_expired_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract expired date must be a valid date' }),
29 36
     contract_value_min: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value min must be a number' }),
30 37
     contract_value_max: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value max must be a number' }),
31
-    positive_notes: Joi.string().optional().allow('', null),
32
-    negative_notes: Joi.string().optional().allow('', null),
38
+    positive_notes: Joi.object({
39
+        note: Joi.string().allow('', null),
40
+        tags: Joi.array().items(Joi.string()).optional().default([]),
41
+    }).optional().allow(null),
42
+
43
+    negative_notes: Joi.object({
44
+        note: Joi.string().allow('', null),
45
+        tags: Joi.array().items(Joi.string()).optional().default([]),
46
+    }).optional().allow(null),
33 47
 });
34 48
 
35 49
 export const validateStoreVendorExperienceRequest = (body: unknown): VendorExperienceRequestDTO => {
@@ -40,72 +54,44 @@ export const validateUpdateVendorExperienceRequest = (body: unknown): Partial<Ve
40 54
     return validateWithSchema<Partial<VendorExperienceRequestDTO>>(updateVendorExperienceSchema, body);
41 55
 }
42 56
 
43
-
57
+// import Joi from 'joi';
58
+// import { validateWithSchema } from '../../ValidateSchema';
44 59
 // import { VendorExperienceRequestDTO } from '../../../types/sales/vendor_experience/VendorExperienceDTO';
45
-// import { HttpException } from '../../../utils/HttpException';
46
-
47
-// export const validateStoreVendorExperienceRequest = (body: VendorExperienceRequestDTO): VendorExperienceRequestDTO => {
48
-//     const {
49
-//         simrs_type,
50
-//         vendor_id,
51
-//         status,
52
-//         contract_start_date,
53
-//         contract_expired_date,
54
-//         contract_value_min,
55
-//         contract_value_max,
56
-//         positive_notes,
57
-//         negative_notes,
58
-//     } = body;
59
-
60
-//     const errors: Record<string, string[]> = {};
61
-
62
-//     if (!simrs_type || simrs_type.trim() === '') {
63
-//         errors.simrs_type = ['simrs type is required'];
64
-//     }
65
-
66
-//     if (!status || status.trim() === '') {
67
-//         errors.status = ['Status is required'];
68
-//     }
69 60
 
70
-//     if (Object.keys(errors).length > 0) {
71
-//         throw new HttpException(errors, 422);
72
-//     }
61
+// export const storeVendorExperienceSchema = Joi.object({
62
+//     simrs_type: Joi.string().trim().required().messages({
63
+//         'string.empty': 'Simrs type is required',
64
+//         'string.base': 'Simrs type must be a string',
65
+//     }),
66
+//     vendor_id: Joi.string().uuid().optional(),
67
+//     status: Joi.string().trim().required().messages({
68
+//         'string.empty': 'Status is required',
69
+//         'string.base': 'Status must be a string',
70
+//     }),
71
+//     contract_start_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract start date must be a valid date' }),
72
+//     contract_expired_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract expired date must be a valid date' }),
73
+//     contract_value_min: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value min must be a number' }),
74
+//     contract_value_max: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value max must be a number' }),
75
+//     positive_notes: Joi.string().optional().allow('', null),
76
+//     negative_notes: Joi.string().optional().allow('', null),
77
+// });
73 78
 
74
-//     return {
75
-//         simrs_type,
76
-//         vendor_id,
77
-//         status,
78
-//         contract_start_date,
79
-//         contract_expired_date,
80
-//         contract_value_min,
81
-//         contract_value_max,
82
-//         positive_notes,
83
-//         negative_notes,
84
-//     };
85
-// };
79
+// export const updateVendorExperienceSchema = Joi.object({
80
+//     simrs_type: Joi.string().trim().optional(),
81
+//     vendor_id: Joi.string().uuid().optional(),
82
+//     status: Joi.string().trim().optional(),
83
+//     contract_start_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract start date must be a valid date' }),
84
+//     contract_expired_date: Joi.date().optional().allow(null).messages({ 'date.format': 'Contract expired date must be a valid date' }),
85
+//     contract_value_min: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value min must be a number' }),
86
+//     contract_value_max: Joi.number().optional().allow(null).messages({ 'number.format': 'Contract value max must be a number' }),
87
+//     positive_notes: Joi.string().optional().allow('', null),
88
+//     negative_notes: Joi.string().optional().allow('', null),
89
+// });
86 90
 
87
-// export const validateUpdateVendorExperienceRequest = (body: VendorExperienceRequestDTO): Partial<VendorExperienceRequestDTO> => {
88
-//     const {
89
-//         simrs_type,
90
-//         vendor_id,
91
-//         status,
92
-//         contract_start_date,
93
-//         contract_expired_date,
94
-//         contract_value_min,
95
-//         contract_value_max,
96
-//         positive_notes,
97
-//         negative_notes,
98
-//     } = body;
91
+// export const validateStoreVendorExperienceRequest = (body: unknown): VendorExperienceRequestDTO => {
92
+//     return validateWithSchema<VendorExperienceRequestDTO>(storeVendorExperienceSchema, body);
93
+// }
99 94
 
100
-//     return {
101
-//         simrs_type,
102
-//         vendor_id,
103
-//         status,
104
-//         contract_start_date,
105
-//         contract_expired_date,
106
-//         contract_value_min,
107
-//         contract_value_max,
108
-//         positive_notes,
109
-//         negative_notes,
110
-//     };
111
-// };
95
+// export const validateUpdateVendorExperienceRequest = (body: unknown): Partial<VendorExperienceRequestDTO> => {
96
+//     return validateWithSchema<Partial<VendorExperienceRequestDTO>>(updateVendorExperienceSchema, body);
97
+// }