Sfoglia il codice sorgente

add feature import by excel vendor & hospital

pearlgw 1 settimana fa
parent
commit
15cf2d602d

+ 112 - 0
package-lock.json

@@ -30,6 +30,7 @@
30 30
         "pg": "^8.16.2",
31 31
         "qs": "^6.14.0",
32 32
         "sharp": "^0.34.3",
33
+        "xlsx": "^0.18.5",
33 34
         "zod": "^4.0.5"
34 35
       },
35 36
       "devDependencies": {
@@ -42,6 +43,7 @@
42 43
         "@types/multer": "^2.0.0",
43 44
         "@types/node": "^24.0.14",
44 45
         "@types/sharp": "^0.31.1",
46
+        "@types/xlsx": "^0.0.35",
45 47
         "nodemon": "^3.1.10",
46 48
         "prisma": "^6.10.1",
47 49
         "ts-node": "^10.9.2",
@@ -878,6 +880,13 @@
878 880
         "@types/node": "*"
879 881
       }
880 882
     },
883
+    "node_modules/@types/xlsx": {
884
+      "version": "0.0.35",
885
+      "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.35.tgz",
886
+      "integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==",
887
+      "dev": true,
888
+      "license": "MIT"
889
+    },
881 890
     "node_modules/@types/yauzl": {
882 891
       "version": "2.10.3",
883 892
       "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -927,6 +936,15 @@
927 936
         "node": ">=0.4.0"
928 937
       }
929 938
     },
939
+    "node_modules/adler-32": {
940
+      "version": "1.3.1",
941
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
942
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
943
+      "license": "Apache-2.0",
944
+      "engines": {
945
+        "node": ">=0.8"
946
+      }
947
+    },
930 948
     "node_modules/agent-base": {
931 949
       "version": "7.1.3",
932 950
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
@@ -1177,6 +1195,19 @@
1177 1195
         "url": "https://github.com/sponsors/ljharb"
1178 1196
       }
1179 1197
     },
1198
+    "node_modules/cfb": {
1199
+      "version": "1.2.2",
1200
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
1201
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
1202
+      "license": "Apache-2.0",
1203
+      "dependencies": {
1204
+        "adler-32": "~1.3.0",
1205
+        "crc-32": "~1.2.0"
1206
+      },
1207
+      "engines": {
1208
+        "node": ">=0.8"
1209
+      }
1210
+    },
1180 1211
     "node_modules/chokidar": {
1181 1212
       "version": "3.6.0",
1182 1213
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -1225,6 +1256,15 @@
1225 1256
         "node": ">=20"
1226 1257
       }
1227 1258
     },
1259
+    "node_modules/codepage": {
1260
+      "version": "1.15.0",
1261
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
1262
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
1263
+      "license": "Apache-2.0",
1264
+      "engines": {
1265
+        "node": ">=0.8"
1266
+      }
1267
+    },
1228 1268
     "node_modules/color": {
1229 1269
       "version": "4.2.3",
1230 1270
       "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -1359,6 +1399,18 @@
1359 1399
         "node": ">= 0.10"
1360 1400
       }
1361 1401
     },
1402
+    "node_modules/crc-32": {
1403
+      "version": "1.2.2",
1404
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
1405
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
1406
+      "license": "Apache-2.0",
1407
+      "bin": {
1408
+        "crc32": "bin/crc32.njs"
1409
+      },
1410
+      "engines": {
1411
+        "node": ">=0.8"
1412
+      }
1413
+    },
1362 1414
     "node_modules/create-require": {
1363 1415
       "version": "1.1.1",
1364 1416
       "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -1843,6 +1895,15 @@
1843 1895
         "node": ">= 0.6"
1844 1896
       }
1845 1897
     },
1898
+    "node_modules/frac": {
1899
+      "version": "1.1.2",
1900
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
1901
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
1902
+      "license": "Apache-2.0",
1903
+      "engines": {
1904
+        "node": ">=0.8"
1905
+      }
1906
+    },
1846 1907
     "node_modules/fresh": {
1847 1908
       "version": "2.0.0",
1848 1909
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -3336,6 +3397,18 @@
3336 3397
       "license": "BSD-3-Clause",
3337 3398
       "optional": true
3338 3399
     },
3400
+    "node_modules/ssf": {
3401
+      "version": "0.11.2",
3402
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
3403
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
3404
+      "license": "Apache-2.0",
3405
+      "dependencies": {
3406
+        "frac": "~1.1.2"
3407
+      },
3408
+      "engines": {
3409
+        "node": ">=0.8"
3410
+      }
3411
+    },
3339 3412
     "node_modules/statuses": {
3340 3413
       "version": "2.0.2",
3341 3414
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -3573,12 +3646,51 @@
3573 3646
         "node": ">= 0.8"
3574 3647
       }
3575 3648
     },
3649
+    "node_modules/wmf": {
3650
+      "version": "1.0.2",
3651
+      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
3652
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
3653
+      "license": "Apache-2.0",
3654
+      "engines": {
3655
+        "node": ">=0.8"
3656
+      }
3657
+    },
3658
+    "node_modules/word": {
3659
+      "version": "0.3.0",
3660
+      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
3661
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
3662
+      "license": "Apache-2.0",
3663
+      "engines": {
3664
+        "node": ">=0.8"
3665
+      }
3666
+    },
3576 3667
     "node_modules/wrappy": {
3577 3668
       "version": "1.0.2",
3578 3669
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
3579 3670
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
3580 3671
       "license": "ISC"
3581 3672
     },
3673
+    "node_modules/xlsx": {
3674
+      "version": "0.18.5",
3675
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
3676
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
3677
+      "license": "Apache-2.0",
3678
+      "dependencies": {
3679
+        "adler-32": "~1.3.0",
3680
+        "cfb": "~1.2.1",
3681
+        "codepage": "~1.15.0",
3682
+        "crc-32": "~1.2.1",
3683
+        "ssf": "~0.11.2",
3684
+        "wmf": "~1.0.1",
3685
+        "word": "~0.3.0"
3686
+      },
3687
+      "bin": {
3688
+        "xlsx": "bin/xlsx.njs"
3689
+      },
3690
+      "engines": {
3691
+        "node": ">=0.8"
3692
+      }
3693
+    },
3582 3694
     "node_modules/xtend": {
3583 3695
       "version": "4.0.2",
3584 3696
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

+ 2 - 0
package.json

@@ -35,6 +35,7 @@
35 35
     "pg": "^8.16.2",
36 36
     "qs": "^6.14.0",
37 37
     "sharp": "^0.34.3",
38
+    "xlsx": "^0.18.5",
38 39
     "zod": "^4.0.5"
39 40
   },
40 41
   "devDependencies": {
@@ -47,6 +48,7 @@
47 48
     "@types/multer": "^2.0.0",
48 49
     "@types/node": "^24.0.14",
49 50
     "@types/sharp": "^0.31.1",
51
+    "@types/xlsx": "^0.0.35",
50 52
     "nodemon": "^3.1.10",
51 53
     "prisma": "^6.10.1",
52 54
     "ts-node": "^10.9.2",

+ 2 - 0
prisma/migrations/20250828033126_add_field_email_in_hospital/migration.sql

@@ -0,0 +1,2 @@
1
+-- AlterTable
2
+ALTER TABLE "hospitals" ADD COLUMN     "email" TEXT;

+ 1 - 0
prisma/schema.prisma

@@ -108,6 +108,7 @@ model Hospital {
108 108
   // simrs_type           String?
109 109
   contact              String?
110 110
   image                String?
111
+  email                String?
111 112
   progress_status      ProgressStatus
112 113
   note                 String?
113 114
   created_by           String

+ 13 - 0
src/controllers/admin/HospitalController.ts

@@ -6,6 +6,7 @@ import { PaginationParam } from '../../utils/PaginationParams';
6 6
 import { errorResponse, messageSuccessResponse } from '../../utils/Response';
7 7
 import { validateStoreHospitalRequest, validateUpdateHospitalRequest } from '../../validators/admin/hospital/HospitalValidators';
8 8
 import { CustomRequest } from '../../types/token/CustomRequest';
9
+import { HttpException } from '../../utils/HttpException';
9 10
 
10 11
 export const getAllHospital = async (req: Request, res: Response): Promise<Response> => {
11 12
     try {
@@ -80,4 +81,16 @@ export const deleteHospital = async (req: Request, res: Response): Promise<Respo
80 81
     } catch (err) {
81 82
         return errorResponse(res, err);
82 83
     }
84
+};
85
+
86
+export const importHospital = async (req: Request, res: Response): Promise<Response> => {
87
+    try {
88
+        if (!req.file) {
89
+            throw new HttpException("No file uploaded", 400);
90
+        }
91
+        await HospitalService.importHospitalService(req.file, req as CustomRequest);
92
+        return messageSuccessResponse(res, "Success import hospital", 201);
93
+    } catch (err) {
94
+        return errorResponse(res, err);
95
+    }
83 96
 };

+ 12 - 62
src/controllers/admin/VendorController.ts

@@ -6,6 +6,7 @@ import { PaginationParam } from '../../utils/PaginationParams';
6 6
 import { errorResponse, messageSuccessResponse } from '../../utils/Response';
7 7
 import { validateStoreVendorRequest, validateUpdateVendorRequest } from '../../validators/admin/vendor/VendorValidators';
8 8
 import { CustomRequest } from '../../types/token/CustomRequest';
9
+import { HttpException } from '../../utils/HttpException';
9 10
 
10 11
 export const getAllVendor = async (req: Request, res: Response): Promise<Response> => {
11 12
     try {
@@ -61,65 +62,14 @@ export const deleteVendor = async (req: Request, res: Response): Promise<Respons
61 62
     }
62 63
 };
63 64
 
64
-
65
-// const { VendorCollection } = require('../../resources/admin/vendor/VendorCollection.js');
66
-// const { VendorResource } = require('../../resources/admin/vendor/VendorResource.js');
67
-// const vendorService = require('../../services/admin/VendorService.js');
68
-// const { PaginationParam } = require('../../utils/PaginationParams.js');
69
-// const { errorResponse, messageSuccessResponse } = require('../../utils/Response.js');
70
-// const { validateStoreVendorRequest, validateUpdateVendorRequest } = require('../../validators/admin/vendor/VendorValidators.js');
71
-
72
-// exports.getAllVendor = async (req, res) => {
73
-//     try {
74
-//         const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
75
-
76
-//         const { vendors, total } = await vendorService.getAllVendorService({
77
-//             page, limit, search, sortBy, orderBy
78
-//         });
79
-
80
-//         return VendorCollection({ req, res, data: vendors, total, page, limit, message: 'Vendor data successfully retrieved' });
81
-//     } catch (err) {
82
-//         return errorResponse(res, err);
83
-//     }
84
-// };
85
-
86
-// exports.showVendor = async (req, res) => {
87
-//     try {
88
-//         const id = req.params.id;
89
-//         const { vendor } = await vendorService.showVendorService(id);
90
-//         return VendorResource(res, vendor, 'Success show vendor');
91
-//     } catch (err) {
92
-//         return errorResponse(res, err);
93
-//     }
94
-// };
95
-
96
-// exports.storeVendor = async (req, res) => {
97
-//     try {
98
-//         const validatedData = validateStoreVendorRequest(req.body);
99
-//         await vendorService.storeVendorService(validatedData, req);
100
-//         return messageSuccessResponse(res, 'Success added vendor', 201);
101
-//     } catch (err) {
102
-//         return errorResponse(res, err);
103
-//     }
104
-// }
105
-
106
-// exports.updateVendor = async (req, res) => {
107
-//     try {
108
-//         const id = req.params.id;
109
-//         const validatedData = validateUpdateVendorRequest(req.body);
110
-//         await vendorService.updateVendorService(validatedData, id, req);
111
-//         return messageSuccessResponse(res, 'Success update vendor');
112
-//     } catch (err) {
113
-//         return errorResponse(res, err);
114
-//     }
115
-// }
116
-
117
-// exports.deleteVendor = async (req, res) => {
118
-//     try {
119
-//         const id = req.params.id;
120
-//         await vendorService.deleteVendorService(id, req);
121
-//         return messageSuccessResponse(res, 'Success delete vendor');
122
-//     } catch (err) {
123
-//         return errorResponse(res, err);
124
-//     }
125
-// };
65
+export const importVendor = async (req: Request, res: Response): Promise<Response> => {
66
+    try {
67
+        if (!req.file) {
68
+            throw new HttpException("No file uploaded", 400);
69
+        }
70
+        await VendorService.importVendorService(req.file, req as CustomRequest);
71
+        return messageSuccessResponse(res, "Success import vendor", 201);
72
+    } catch (err) {
73
+        return errorResponse(res, err);
74
+    }
75
+};

+ 14 - 158
src/repository/admin/HospitalRepository.ts

@@ -1,5 +1,5 @@
1 1
 import prisma from '../../prisma/PrismaClient';
2
-import { Prisma } from '@prisma/client';
2
+import { Hospital, Prisma } from '@prisma/client';
3 3
 
4 4
 interface FindAllParams {
5 5
     skip: number;
@@ -26,6 +26,7 @@ const HospitalRepository = {
26 26
                 address: true,
27 27
                 // simrs_type: true,
28 28
                 contact: true,
29
+                email: true,
29 30
                 image: true,
30 31
                 progress_status: true,
31 32
                 note: true,
@@ -87,6 +88,7 @@ const HospitalRepository = {
87 88
                 address: true,
88 89
                 // simrs_type: true,
89 90
                 contact: true,
91
+                email: true,
90 92
                 image: true,
91 93
                 progress_status: true,
92 94
                 note: true,
@@ -138,163 +140,17 @@ const HospitalRepository = {
138 140
             data,
139 141
         });
140 142
     },
141
-};
142
-
143
-export default HospitalRepository;
144
-
145
-
146
-// const prisma = require('../../prisma/PrismaClient.js');
147
-
148
-// const HospitalRepository = {
149
-//     findAll: async ({ skip, take, where, orderBy }) => {
150
-//         const hospitalsRaw = await prisma.hospital.findMany({
151
-//             where,
152
-//             skip,
153
-//             take,
154
-//             orderBy,
155
-//             select: {
156
-//                 id: true,
157
-//                 name: true,
158
-//                 hospital_code: true,
159
-//                 type: true,
160
-//                 ownership: true,
161
-//                 province: { select: { id: true, name: true } },
162
-//                 city: { select: { id: true, name: true } },
163
-//                 address: true,
164
-//                 // simrs_type: true,
165
-//                 contact: true,
166
-//                 image: true,
167
-//                 progress_status: true,
168
-//                 note: true,
169
-//                 latitude: true,
170
-//                 longitude: true,
171
-//                 gmaps_url: true,
172
-//                 created_by: true,
173
-//                 createdAt: true,
174
-//                 updatedAt: true,
175
-//                 user: { select: { id: true, fullname: true } },
176
-//                 vendor_experiences: {
177
-//                     where: {
178
-//                         status: "active",
179
-//                         deletedAt: null
180
-//                     },
181
-//                     select: {
182
-//                         vendor: {
183
-//                             select: {
184
-//                                 id: true,
185
-//                                 name: true,
186
-//                                 name_pt: true,
187
-//                                 strengths: true,
188
-//                                 weaknesses: true,
189
-//                                 website: true,
190
-//                             }
191
-//                         }
192
-//                     }
193
-//                 }
194
-//             }
195
-//         });
196
-
197
-//         return hospitalsRaw.map(hospital => {
198
-//             const { vendor_experiences, ...rest } = hospital;
199
-//             return {
200
-//                 ...rest,
201
-//                 vendor: vendor_experiences?.[0]?.vendor || null
202
-//             };
203
-//         });
204
-//     },
205
-
206
-//     countAll: async (where) => {
207
-//         return prisma.hospital.count({ where });
208
-//     },
209
-
210
-//     findById: async (id) => {
211
-//         const hospitalRaw = await prisma.hospital.findFirst({
212
-//             where: {
213
-//                 id,
214
-//                 deletedAt: null
215
-//             },
216
-//             select: {
217
-//                 id: true,
218
-//                 name: true,
219
-//                 hospital_code: true,
220
-//                 type: true,
221
-//                 ownership: true,
222
-//                 province: { select: { id: true, name: true } },
223
-//                 city: { select: { id: true, name: true } },
224
-//                 address: true,
225
-//                 // simrs_type: true,
226
-//                 contact: true,
227
-//                 image: true,
228
-//                 progress_status: true,
229
-//                 note: true,
230
-//                 latitude: true,
231
-//                 longitude: true,
232
-//                 gmaps_url: true,
233
-//                 created_by: true,
234
-//                 createdAt: true,
235
-//                 updatedAt: true,
236
-//                 user: { select: { id: true, fullname: true } },
237
-//                 vendor_experiences: {
238
-//                     where: {
239
-//                         status: "active",
240
-//                         deletedAt: null
241
-//                     },
242
-//                     take: 1,
243
-//                     select: {
244
-//                         vendor: {
245
-//                             select: {
246
-//                                 id: true,
247
-//                                 name: true,
248
-//                                 name_pt: true,
249
-//                                 strengths: true,
250
-//                                 weaknesses: true,
251
-//                                 website: true,
252
-//                             }
253
-//                         }
254
-//                     }
255
-//                 }
256
-//             }
257
-//         });
258
-
259
-//         if (!hospitalRaw) return null;
260 143
 
261
-//         const { vendor_experiences, ...rest } = hospitalRaw;
262
-//         return {
263
-//             ...rest,
264
-//             vendor: vendor_experiences?.[0]?.vendor || null
265
-//         };
266
-//     },
267 144
 
268
-//     create: async (data) => {
269
-//         return prisma.hospital.create({ data });
270
-//     },
271
-
272
-//     // update: async (id, data) => {
273
-//     //     return prisma.hospital.update({
274
-//     //         where: { id },
275
-//     //         data: {
276
-//     //             name: data.name,
277
-//     //             hospital_code: data.hospital_code,
278
-//     //             type: data.type,
279
-//     //             ownership: data.ownership,
280
-//     //             province: { connect: { id: data.province_id } },
281
-//     //             city: { connect: { id: data.city_id } },
282
-//     //             address: data.address,
283
-//     //             // simrs_type: data.simrs_type,
284
-//     //             contact: data.contact,
285
-//     //             note: data.note,
286
-//     //             image: data.image,
287
-//     //             progress_status: data.progress_status,
288
-//     //         }
289
-//     //     });
290
-//     // },
291
-
292
-//     update: async (id, data) => {
293
-//         return prisma.hospital.update({
294
-//             where: { id },
295
-//             data
296
-//         });
297
-//     },
298
-// };
145
+    findByName: async (name: string, hospital_code: string): Promise<Hospital | null> => {
146
+        return prisma.hospital.findFirst({
147
+            where: {
148
+                name,
149
+                hospital_code,
150
+                deletedAt: null,
151
+            },
152
+        });
153
+    },
154
+};
299 155
 
300
-// module.exports = HospitalRepository;
156
+export default HospitalRepository;

+ 12 - 122
src/repository/admin/VendorRepository.ts

@@ -1,6 +1,6 @@
1 1
 import prisma from '../../prisma/PrismaClient';
2 2
 import { now } from '../../utils/TimeLocal';
3
-import { Prisma } from '@prisma/client';
3
+import { Prisma, Vendor } from '@prisma/client';
4 4
 
5 5
 type FindAllOptions = {
6 6
     skip?: number;
@@ -122,126 +122,16 @@ const VendorRepository = {
122 122
 
123 123
         return vendor;
124 124
     },
125
-};
126
-
127
-export default VendorRepository;
128
-
129
-
130
-// const prisma = require('../../prisma/PrismaClient.js');
131
-// const timeLocal = require('../../utils/TimeLocal.js');
132
-
133
-// const VendorRepository = {
134
-//     findAll: async ({ skip, take, where, orderBy }) => {
135
-//         const vendors = await prisma.vendor.findMany({
136
-//             where,
137
-//             skip,
138
-//             take,
139
-//             orderBy,
140
-//             select: {
141
-//                 id: true,
142
-//                 name: true,
143
-//                 name_pt: true,
144
-//                 strengths: true,
145
-//                 weaknesses: true,
146
-//                 website: true,
147
-//                 user: { select: { id: true, fullname: true } },
148
-//                 _count: {
149
-//                     select: {
150
-//                         vendor_experiences: {
151
-//                             where: {
152
-//                                 deletedAt: null
153
-//                             }
154
-//                         }
155
-//                     }
156
-//                 },
157
-//                 createdAt: true,
158
-//                 updatedAt: true,
159
-//             },
160
-//         });
161
-
162
-//         const formattedVendors = vendors.map(v => {
163
-//             const { _count, ...rest } = v;
164
-//             return {
165
-//                 ...rest,
166
-//                 count_hospitals: _count.vendor_experiences,
167
-//             };
168
-//         });
169
-
170
-//         return formattedVendors;
171
-//     },
172
-
173
-//     countAll: async (where) => {
174
-//         return prisma.vendor.count({ where });
175
-//     },
176
-
177
-//     findById: async (id) => {
178
-//         const vendor = await prisma.vendor.findFirst({
179
-//             where: {
180
-//                 id,
181
-//                 deletedAt: null
182
-//             },
183
-//             select: {
184
-//                 id: true,
185
-//                 name: true,
186
-//                 name_pt: true,
187
-//                 strengths: true,
188
-//                 weaknesses: true,
189
-//                 website: true,
190
-//                 user: { select: { id: true, fullname: true } },
191
-//                 _count: {
192
-//                     select: {
193
-//                         vendor_experiences: {
194
-//                             where: {
195
-//                                 deletedAt: null
196
-//                             }
197
-//                         }
198
-//                     }
199
-//                 },
200
-//                 createdAt: true,
201
-//                 updatedAt: true,
202
-//             },
203
-//         });
204
-
205
-//         const { _count, ...rest } = vendor;
206
-//         return {
207
-//             ...rest,
208
-//             count_hospitals: _count.vendor_experiences
209
-//         };
210
-//     },
211 125
 
212
-//     create: async (data) => {
213
-//         return prisma.vendor.create({ data });
214
-//     },
215
-
216
-//     update: async (id, data) => {
217
-//         return prisma.vendor.update({
218
-//             where: { id },
219
-//             data
220
-//         });
221
-//     },
222
-
223
-//     delete: async (id) => {
224
-//         // delete vendor
225
-//         const vendor = await prisma.vendor.update({
226
-//             where: { id },
227
-//             data: {
228
-//                 deletedAt: timeLocal.now().toDate()
229
-//             }
230
-//         });
231
-
232
-//         // Unlink vendor_id di vendor_histories
233
-//         await prisma.vendorExperience.updateMany({
234
-//             where: {
235
-//                 vendor_id: id,
236
-//                 deletedAt: null
237
-//             },
238
-//             data: {
239
-//                 vendor_id: null
240
-//             }
241
-//         });
242
-
243
-//         return vendor;
244
-//     },
245
-// };
126
+    findByName: async (name: string, name_pt: string): Promise<Vendor | null> => {
127
+        return prisma.vendor.findFirst({
128
+            where: {
129
+                name,
130
+                name_pt,
131
+                deletedAt: null,
132
+            },
133
+        });
134
+    },
135
+};
246 136
 
247
-// module.exports = VendorRepository;
137
+export default VendorRepository;

+ 2 - 0
src/routes/admin/HospitalRoute.ts

@@ -17,6 +17,8 @@ router.get('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], Ho
17 17
 router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], upload.single('image'), HospitalController.updateHospital);
18 18
 router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], HospitalController.deleteHospital);
19 19
 
20
+router.post('/import', [keycloak.protect(), extractToken, checkRoles(['admin'])], upload.single("file"), HospitalController.importHospital);
21
+
20 22
 // Vendor Experience
21 23
 router.get('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.getAllVendorExperience);
22 24
 router.post('/:id/vendor-experience', [keycloak.protect(), extractToken, checkRoles(["admin"])], VendorExperienceController.storeVendorExperience);

+ 3 - 0
src/routes/admin/VendorRoute.ts

@@ -3,6 +3,7 @@ import * as VendorController from '../../controllers/admin/VendorController';
3 3
 import keycloak from '../../middleware/Keycloak';
4 4
 import { extractToken } from '../../middleware/ExtractToken';
5 5
 import checkRoles from '../../middleware/CheckRoles';
6
+import upload from '../../middleware/UploadImage';
6 7
 
7 8
 const router: Router = express.Router();
8 9
 
@@ -12,4 +13,6 @@ router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['admin', 'sale
12 13
 router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['admin', 'sales'])], VendorController.updateVendor);
13 14
 router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], VendorController.deleteVendor);
14 15
 
16
+router.post('/import', [keycloak.protect(), extractToken, checkRoles(['admin'])], upload.single("file"), VendorController.importVendor);
17
+
15 18
 export default router;

+ 101 - 231
src/services/admin/HospitalService.ts

@@ -12,7 +12,9 @@ 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 * as XLSX from "xlsx";
15 16
 import { storeCategoryLinkService, updateCategoryLinkService } from './CategoryLinkService';
17
+import { validateStoreHospitalRequest } from '../../validators/admin/hospital/HospitalValidators';
16 18
 
17 19
 interface PaginationParams {
18 20
     page: number;
@@ -151,6 +153,7 @@ export const storeHospitalService = async (validateData: HospitalRequestDTO, req
151 153
         ownership: validateData.ownership,
152 154
         address: validateData.address,
153 155
         contact: validateData.contact,
156
+        email: validateData.email,
154 157
         image: imagePath,
155 158
         latitude,
156 159
         longitude,
@@ -278,6 +281,7 @@ export const updateHospitalService = async (validateData: Partial<HospitalReques
278 281
         ...(validateData.ownership && { ownership: validateData.ownership }),
279 282
         ...(validateData.address && { address: validateData.address }),
280 283
         ...(validateData.contact && { contact: validateData.contact }),
284
+        ...(validateData.email && { email: validateData.email }),
281 285
         ...(validateData.note !== undefined && { note: validateData.note }),
282 286
         ...(validateData.progress_status && { progress_status: validateData.progress_status }),
283 287
         ...(imagePath && { image: imagePath }),
@@ -319,236 +323,102 @@ export const deleteHospitalService = async (id: string, req: CustomRequest) => {
319 323
     await deleteLog(req, data);
320 324
 };
321 325
 
322
-// const HospitalRepository = require('../../repository/admin/HospitalRepository.js');
323
-// const HttpException = require('../../utils/HttpException.js');
324
-// const { SearchFilter } = require('../../utils/SearchFilter.js');
325
-// const timeLocal = require('../../utils/TimeLocal.js');
326
-// const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
327
-// const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js');
328
-// const CityRepository = require('../../repository/admin/CityRepository.js');
329
-// const { BASE_URL } = require('../../../config/config.js');
330
-// const prisma = require('../../prisma/PrismaClient.js');
331
-// const { formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
332
-
333
-// exports.getAllHospitalService = async ({ page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status }) => {
334
-//     const skip = (page - 1) * limit;
335
-
336
-//     const where = {
337
-//         ...SearchFilter(search, ['name', 'province.id', 'city.id', 'type', 'ownership', 'simrs_type']),
338
-//         ...(province ? { province_id: province } : {}),
339
-//         ...(city ? { city_id: city } : {}),
340
-//         ...(type ? { type: type } : {}),
341
-//         ...(ownership ? { ownership: ownership } : {}),
342
-//         ...(progress_status ? { progress_status: progress_status } : {}),
343
-//         deletedAt: null
344
-//     };
345
-
346
-//     const [hospitals, total] = await Promise.all([
347
-//         HospitalRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
348
-//         HospitalRepository.countAll(where)
349
-//     ]);
350
-
351
-//     return { hospitals, total };
352
-// };
353
-
354
-// exports.showHospitalService = async (id) => {
355
-//     const hospital = await HospitalRepository.findById(id);
356
-//     if (!hospital) {
357
-//         throw new HttpException("Data hospital not found", 404);
358
-//     }
359
-
360
-//     return hospital;
361
-// };
362
-
363
-// exports.storeHospitalService = async (validateData, req) => {
364
-//     const creatorId = req.tokenData.sub;
365
-//     const province = await ProvinceRepository.findById(validateData.province_id);
366
-//     if (!province) {
367
-//         throw new HttpException('Province not found', 404);
368
-//     }
369
-
370
-//     const city = await CityRepository.findById(validateData.city_id);
371
-//     if (!city) {
372
-//         throw new HttpException('City not found', 404);
373
-//     }
374
-
375
-//     if (!req.file) {
376
-//         throw new HttpException({ image: ['image file is required'] }, 422);
377
-//     }
378
-
379
-//     const existingHospital = await prisma.hospital.findFirst({
380
-//         where: {
381
-//             name: validateData.name,
382
-//             city_id: validateData.city_id,
383
-//             deletedAt: null
384
-//         }
385
-//     });
386
-
387
-//     if (existingHospital) {
388
-//         throw new HttpException('Hospital with same name in this city already exists', 400);
389
-//     }
390
-
391
-//     const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
392
-
393
-//     let latitude = validateData.latitude ?? null;
394
-//     let longitude = validateData.longitude ?? null;
395
-//     let gmapsUrl = validateData.gmaps_url ?? null;
396
-
397
-//     if (gmapsUrl) {
398
-//         if (gmapsUrl.includes("www.google.com/maps")) {
399
-//             const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
400
-//             const match = gmapsUrl.match(regex);
401
-
402
-//             if (match) {
403
-//                 latitude = parseFloat(match[1]);
404
-//                 longitude = parseFloat(match[2]);
405
-//             } else {
406
-//                 throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
407
-//             }
408
-
409
-//         } else if (gmapsUrl.includes("maps.app.goo.gl")) {
410
-//             latitude = null;
411
-//             longitude = null;
412
-
413
-//         } else {
414
-//             // URL disediakan tapi bukan dari domain yang valid
415
-//             throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
416
-//         }
417
-//     } else if (latitude !== null && longitude !== null) {
418
-//         gmapsUrl = null;
419
-//     } else {
420
-//         throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
421
-//     }
422
-
423
-//     const payload = {
424
-//         ...validateData,
425
-//         image: imagePath,
426
-//         progress_status: "cari_data",
427
-//         // simrs_type: "-",
428
-//         created_by: creatorId,
429
-//         latitude,
430
-//         longitude,
431
-//         gmaps_url: gmapsUrl,
432
-//     };
433
-
434
-//     const data = await HospitalRepository.create(payload);
435
-//     await createLog(req, data);
436
-// };
326
+export const importHospitalService = async (file: Express.Multer.File, req: CustomRequest) => {
327
+    const creatorId = req.tokenData.sub;
437 328
 
438
-// const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
329
+    const workbook = XLSX.read(file.buffer, { type: "buffer" });
330
+    const sheetName = workbook.SheetNames[0];
331
+    const sheet = workbook.Sheets[sheetName];
332
+    const rows: string[] = XLSX.utils.sheet_to_json(sheet);
333
+
334
+    for (const row of rows) {
335
+        const validatedData = validateStoreHospitalRequest(row);
336
+
337
+        // Cek provinsi
338
+        const province = await ProvinceRepository.findById(validatedData.province_id!);
339
+        if (!province) throw new HttpException('Province not found', 404);
340
+
341
+        // Cek kota
342
+        const city = await CityRepository.findById(validatedData.city_id!);
343
+        if (!city) throw new HttpException('City not found', 404);
439 344
 
440
-// exports.updateHospitalService = async (validateData, id, req) => {
441
-//     const hospital = await HospitalRepository.findById(id);
442
-//     if (!hospital) {
443
-//         throw new HttpException("Hospital data not found", 404);
444
-//     }
445
-
446
-//     if (validateData.province_id) {
447
-//         const province = await ProvinceRepository.findById(validateData.province_id);
448
-//         if (!province) {
449
-//             throw new HttpException('Province not found', 404);
450
-//         }
451
-//     }
452
-
453
-//     if (validateData.city_id) {
454
-//         const city = await CityRepository.findById(validateData.city_id);
455
-//         if (!city) {
456
-//             throw new HttpException('City not found', 404);
457
-//         }
458
-//     }
459
-
460
-//     if (validateData.progress_status && !validProgressStatuses.includes(validateData.progress_status)) {
461
-//         throw new HttpException(
462
-//             `Invalid progress_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
463
-//             422
464
-//         );
465
-//     }
466
-
467
-//     if (validateData.name && validateData.city_id) {
468
-//         const existingHospital = await prisma.hospital.findFirst({
469
-//             where: {
470
-//                 name: validateData.name,
471
-//                 city_id: validateData.city_id,
472
-//                 deletedAt: null,
473
-//                 NOT: { id }
474
-//             }
475
-//         });
476
-
477
-//         if (existingHospital) {
478
-//             throw new HttpException('Hospital with same name in this city already exists', 400);
479
-//         }
480
-//     }
481
-
482
-//     // Jika ada file baru, replace image
483
-//     let imagePath = hospital.image; // pakai yang lama
484
-//     if (req.file) {
485
-//         imagePath = `/storage/img/${req.file.filename}`; // path relatif
486
-//     }
487
-
488
-
489
-//     // Handle koordinat dan gmaps_url
490
-//     let latitude = hospital.latitude;
491
-//     let longitude = hospital.longitude;
492
-//     let gmapsUrl = hospital.gmaps_url;
493
-
494
-//     if (
495
-//         validateData.latitude !== undefined &&
496
-//         validateData.longitude !== undefined &&
497
-//         validateData.latitude !== null &&
498
-//         validateData.longitude !== null
499
-//     ) {
500
-//         // Jika diberikan lat long langsung
501
-//         latitude = validateData.latitude;
502
-//         longitude = validateData.longitude;
503
-//         gmapsUrl = validateData.gmaps_url || gmapsUrl;
504
-//     } else if (
505
-//         validateData.gmaps_url &&
506
-//         typeof validateData.gmaps_url === "string" &&
507
-//         validateData.gmaps_url.trim() !== ""
508
-//     ) {
509
-//         gmapsUrl = validateData.gmaps_url;
510
-
511
-//         if (gmapsUrl.includes("www.google.com/maps")) {
512
-//             const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
513
-//             const match = gmapsUrl.match(regex);
514
-//             if (match) {
515
-//                 latitude = parseFloat(match[1]);
516
-//                 longitude = parseFloat(match[2]);
517
-//             } else {
518
-//                 throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
519
-//             }
520
-//         } else if (gmapsUrl.includes("maps.app.goo.gl")) {
521
-//             // Tidak bisa ambil koordinat langsung
522
-//             latitude = null;
523
-//             longitude = null;
524
-//         } else {
525
-//             throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
526
-//         }
527
-//     }
528
-
529
-//     const payload = {
530
-//         ...validateData,
531
-//         image: imagePath,
532
-//         // created_by: req.user.id,
533
-//         latitude,
534
-//         longitude,
535
-//         gmaps_url: gmapsUrl,
536
-//     };
537
-
538
-//     const data = await HospitalRepository.update(id, payload);
539
-//     await updateLog(req, data);
540
-// };
541
-
542
-// exports.deleteHospitalService = async (id, req) => {
543
-//     const hospital = await HospitalRepository.findById(id);
544
-
545
-//     if (!hospital) {
546
-//         throw new HttpException('Hospital not found', 404);
547
-//     }
548
-
549
-//     const data = await HospitalRepository.update(id, {
550
-//         deletedAt: timeLocal.now().toDate()
551
-//     });
552
-
553
-//     await deleteLog(req, data);
554
-// };
345
+        // Cek duplikat hospital (berdasarkan nama + city)
346
+        const existingHospital = await prisma.hospital.findFirst({
347
+            where: {
348
+                name: validatedData.name,
349
+                city_id: validatedData.city_id,
350
+                contact: validatedData.contact,
351
+                deletedAt: null
352
+            }
353
+        });
354
+        if (existingHospital) {
355
+            throw new HttpException(`Hospital ${validatedData.name} in this city already exists`, 400);
356
+        }
357
+
358
+        // Atur imagePath (karena import via file excel biasanya tidak ada gambar)
359
+        let imagePath: string | null = null;
360
+
361
+        // Ambil lat-long dari gmaps_url kalau ada
362
+        let { latitude, longitude, gmaps_url: gmapsUrl } = validatedData;
363
+        if (gmapsUrl) {
364
+            if (gmapsUrl.includes("www.google.com/maps")) {
365
+                const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
366
+                const match = gmapsUrl.match(regex);
367
+                if (match) {
368
+                    latitude = parseFloat(match[1]);
369
+                    longitude = parseFloat(match[2]);
370
+                } else {
371
+                    throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
372
+                }
373
+            } else if (gmapsUrl.includes("maps.app.goo.gl")) {
374
+                latitude = null;
375
+                longitude = null;
376
+            } else {
377
+                throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
378
+            }
379
+        } else if (latitude !== null && longitude !== null) {
380
+            gmapsUrl = null;
381
+        } else {
382
+            throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
383
+        }
384
+
385
+        const payload: Prisma.HospitalCreateInput = {
386
+            name: validatedData.name!,
387
+            hospital_code: validatedData.hospital_code,
388
+            type: validatedData.type,
389
+            ownership: validatedData.ownership,
390
+            address: validatedData.address,
391
+            contact: validatedData.contact,
392
+            email: validatedData.email,
393
+            image: imagePath,
394
+            latitude,
395
+            longitude,
396
+            gmaps_url: gmapsUrl,
397
+            progress_status: ProgressStatus.cari_data,
398
+            note: validatedData.note ?? '',
399
+            province: {
400
+                connect: { id: validatedData.province_id! },
401
+            },
402
+            city: {
403
+                connect: { id: validatedData.city_id! },
404
+            },
405
+            user: {
406
+                connect: { id: creatorId },
407
+            },
408
+        };
409
+
410
+        const data = await HospitalRepository.create(payload);
411
+
412
+        // tags kategori kalau ada
413
+        if (validatedData.tags?.length) {
414
+            await storeCategoryLinkService(
415
+                validatedData.tags,
416
+                'hospital_notes',
417
+                data.id,
418
+                req
419
+            );
420
+        }
421
+
422
+        await createLog(req, data);
423
+    }
424
+};

+ 40 - 100
src/services/admin/VendorService.ts

@@ -7,7 +7,9 @@ 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 * as XLSX from "xlsx";
10 11
 import { storeCategoryLinkService, updateCategoryLinkService } from './CategoryLinkService';
12
+import { validateStoreVendorRequest } from '../../validators/admin/vendor/VendorValidators';
11 13
 
12 14
 interface PaginationParams {
13 15
     page: number;
@@ -158,103 +160,41 @@ export const deleteVendorService = async (id: string, req: CustomRequest) => {
158 160
     await deleteLog(req, data);
159 161
 };
160 162
 
161
-// const VendorRepository = require('../../repository/admin/VendorRepository.js');
162
-// const HttpException = require('../../utils/HttpException.js');
163
-// const prisma = require('../../prisma/PrismaClient.js');
164
-// const { SearchFilter } = require('../../utils/SearchFilter.js');
165
-// const timeLocal = require('../../utils/TimeLocal.js');
166
-// const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
167
-// const { formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
168
-// const { getUserNameById } = require('../../utils/CheckUserKeycloak.js');
169
-
170
-// exports.getAllVendorService = async ({ page, limit, search, sortBy, orderBy }) => {
171
-//     const skip = (page - 1) * limit;
172
-
173
-//     const where = {
174
-//         ...SearchFilter(search, ['name', 'name_pt']),
175
-//         deletedAt: null
176
-//     };
177
-
178
-//     const [vendors, total] = await Promise.all([
179
-//         VendorRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
180
-//         VendorRepository.countAll(where)
181
-//     ]);
182
-
183
-//     return { vendors, total };
184
-// };
185
-
186
-// exports.showVendorService = async (id) => {
187
-//     const vendor = await VendorRepository.findById(id);
188
-//     if (!vendor) {
189
-//         throw new HttpException("Data vendor not found", 404);
190
-//     }
191
-
192
-//     // const userName = await getUserNameById(vendor.created_by);
193
-
194
-//     return { vendor };
195
-// };
196
-
197
-// exports.storeVendorService = async (validateData, req) => {
198
-//     const creatorId = req.tokenData.sub;
199
-
200
-//     const name_vendor = await prisma.vendor.findFirst({
201
-//         where: {
202
-//             name: validateData.name,
203
-//             deletedAt: null
204
-//         }
205
-//     });
206
-
207
-//     if (name_vendor) {
208
-//         throw new HttpException("Vendor name must be unique", 400);
209
-//     }
210
-
211
-//     const payload = {
212
-//         ...validateData,
213
-//         created_by: creatorId
214
-//     };
215
-
216
-//     const data = await VendorRepository.create(payload);
217
-//     await createLog(req, data);
218
-// };
219
-
220
-// exports.updateVendorService = async (validateData, id, req) => {
221
-//     // const creatorId = req.user.id;
222
-
223
-//     const vendor = await VendorRepository.findById(id);
224
-//     if (!vendor) {
225
-//         throw new HttpException("Data vendor not found", 404);
226
-//     }
227
-
228
-//     if (validateData.name && validateData.name !== vendor.name) {
229
-//         const name_vendor = await prisma.vendor.findFirst({
230
-//             where: {
231
-//                 name: validateData.name,
232
-//                 deletedAt: null
233
-//             }
234
-//         });
235
-
236
-//         if (name_vendor) {
237
-//             throw new HttpException("Vendor name must be unique", 400);
238
-//         }
239
-//     }
240
-
241
-//     const payload = {
242
-//         ...validateData,
243
-//         // created_by: creatorId
244
-//     };
245
-
246
-//     const data = await VendorRepository.update(id, payload);
247
-//     await updateLog(req, data);
248
-// };
249
-
250
-// exports.deleteVendorService = async (id, req) => {
251
-//     const vendor = await VendorRepository.findById(id);
252
-
253
-//     if (!vendor) {
254
-//         throw new HttpException('Vendor not found', 404);
255
-//     }
256
-
257
-//     const data = await VendorRepository.delete(id);
258
-
259
-//     await deleteLog(req, data);
260
-// };
163
+export const importVendorService = async (file: Express.Multer.File, req: CustomRequest) => {
164
+    const workbook = XLSX.read(file.buffer, { type: "buffer" });
165
+    const sheetName = workbook.SheetNames[0];
166
+    const sheet = workbook.Sheets[sheetName];
167
+    const rows: string[] = XLSX.utils.sheet_to_json(sheet);
168
+
169
+    for (const row of rows) {
170
+        const validatedData = validateStoreVendorRequest(row);
171
+
172
+        const existingVendor = await VendorRepository.findByName(validatedData.name, validatedData.name_pt);
173
+        if (existingVendor) {
174
+            throw new HttpException(`Vendor ${validatedData.name} already exists`, 400);
175
+        }
176
+
177
+        const payload: Prisma.VendorCreateInput = {
178
+            ...validatedData,
179
+            user: {
180
+                connect: {
181
+                    id: req.tokenData.sub,
182
+                },
183
+            },
184
+            strengths: validatedData.strengths?.note,
185
+            weaknesses: validatedData.weaknesses?.note,
186
+        };
187
+
188
+        const data = await VendorRepository.create(payload);
189
+
190
+        // category links
191
+        if (validatedData.strengths?.tags?.length) {
192
+            await storeCategoryLinkService(validatedData.strengths.tags, "vendor_strength_notes", data.id, req);
193
+        }
194
+        if (validatedData.weaknesses?.tags?.length) {
195
+            await storeCategoryLinkService(validatedData.weaknesses.tags, "vendor_weaknesses_notes", data.id, req);
196
+        }
197
+
198
+        await createLog(req, data);
199
+    }
200
+};

+ 2 - 0
src/types/admin/hospital/HospitalDTO.ts

@@ -9,6 +9,7 @@ export interface HospitalRequestDTO {
9 9
     city_id?: string;
10 10
     address?: string | null;
11 11
     contact?: string | null;
12
+    email?: string | null;
12 13
     gmaps_url?: string | null;
13 14
     latitude?: number | null;
14 15
     longitude?: number | null;
@@ -34,6 +35,7 @@ export interface HospitalDTO {
34 35
     address: string | null;
35 36
     contact: string | null;
36 37
     image: string | null;
38
+    email: string | null;
37 39
     progress_status: ProgressStatus;
38 40
     note: string | null;
39 41
     note_tags?: string[];

+ 9 - 12
src/validators/admin/hospital/HospitalValidators.ts

@@ -6,27 +6,23 @@ export const storeHospitalSchema = Joi.object({
6 6
     name: Joi.string().trim().required().messages({
7 7
         'string.empty': 'name is required',
8 8
     }),
9
-    hospital_code: Joi.string().trim().required().messages({
10
-        'string.empty': 'hospital code is required',
11
-    }),
9
+    hospital_code: Joi.alternatives().try(Joi.string().trim(), Joi.number()).required()
10
+        .messages({ 'any.required': 'hospital code is required' })
11
+        .custom((value) => String(value).trim()),
12 12
     type: Joi.string().trim().required().messages({
13 13
         'string.empty': 'type is required',
14 14
     }),
15
-    ownership: Joi.string().trim().required().messages({
16
-        'string.empty': 'ownership is required',
17
-    }),
15
+    ownership: Joi.string().trim().optional(),
18 16
     province_id: Joi.string().trim().required().messages({
19 17
         'string.empty': 'province id is required',
20 18
     }),
21 19
     city_id: Joi.string().trim().required().messages({
22 20
         'string.empty': 'city id is required',
23 21
     }),
24
-    address: Joi.string().trim().required().messages({
25
-        'string.empty': 'address is required',
26
-    }),
27
-    contact: Joi.string().trim().required().messages({
28
-        'string.empty': 'contact is required',
29
-    }),
22
+    address: Joi.string().trim().optional(),
23
+    // contact: Joi.string().trim().optional(),
24
+    contact: Joi.alternatives().try(Joi.string().trim(), Joi.number()).optional().custom((value) => String(value).trim()),
25
+    email: Joi.string().trim().optional(),
30 26
     note: Joi.string().allow('', null),
31 27
     tags: Joi.array().items(Joi.string()).optional().default([]),
32 28
     gmaps_url: Joi.string().trim().optional().allow(null, ''),
@@ -44,6 +40,7 @@ export const updateHospitalSchema = Joi.object({
44 40
     city_id: Joi.string().trim().optional(),
45 41
     address: Joi.string().trim().optional(),
46 42
     contact: Joi.string().trim().optional(),
43
+    email: Joi.string().trim().optional(),
47 44
     note: Joi.string().allow('', null),
48 45
     tags: Joi.array().items(Joi.string()).optional().default([]),
49 46
     image: Joi.any().optional().allow(null, ''),

+ 1 - 15
src/validators/admin/vendor/VendorValidators.ts

@@ -9,15 +9,7 @@ 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
-    // }),
18
-    website: Joi.string().trim().required().messages({
19
-        'string.empty': 'Vendor website is required',
20
-    }),
12
+    website: Joi.string().trim().optional(),
21 13
     strengths: Joi.object({
22 14
         note: Joi.string().allow('', null),
23 15
         tags: Joi.array().items(Joi.string()).optional().default([]),
@@ -36,12 +28,6 @@ const updateVendorSchema = Joi.object<Partial<VendorRequestDTO>>({
36 28
     name_pt: Joi.string().trim().optional().messages({
37 29
         'string.empty': 'Vendor name_pt is required',
38 30
     }),
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
-    // }),
45 31
     website: Joi.string().trim().optional().messages({
46 32
         'string.empty': 'Vendor website is required',
47 33
     }),