|
|
@@ -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
|
+};
|