HospitalService.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import { Prisma, PrismaClient, ProgressStatus } from '@prisma/client';
  2. import { SearchFilter } from '../../utils/SearchFilter';
  3. import { createLog, updateLog } from '../../utils/LogActivity';
  4. import { HttpException } from '../../utils/HttpException';
  5. import salesHospitalRepository from '../../repository/sales/HospitalRepository';
  6. import ProvinceRepository from '../../repository/admin/ProvinceRepository';
  7. import CityRepository from '../../repository/admin/CityRepository';
  8. import HospitalRepository from '../../repository/admin/HospitalRepository';
  9. import { CustomRequest } from '../../types/token/CustomRequest';
  10. import { HospitalRequestDTO } from '../../types/sales/hospital/HospitalDTO';
  11. import path from 'path';
  12. import fs from 'fs/promises';
  13. import sharp from 'sharp';
  14. // import { storeCategoryLinkService, updateCategoryLinkService } from '../admin/CategoryLinkService';
  15. const prisma = new PrismaClient();
  16. export interface PaginationParams {
  17. page: number;
  18. limit: number;
  19. search?: string;
  20. sortBy: string;
  21. orderBy: 'asc' | 'desc';
  22. province?: string;
  23. city?: string;
  24. type?: string;
  25. ownership?: string;
  26. progress_status?: string;
  27. }
  28. export const getAllHospitalByAreaService = async (params: PaginationParams, req: CustomRequest) => {
  29. const { page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status } = params;
  30. const skip = (page - 1) * limit;
  31. const userAreas = await prisma.userArea.findMany({
  32. where: { user_id: req.tokenData.sub },
  33. select: { province_id: true },
  34. });
  35. const provinceIds = userAreas.map((ua) => ua.province_id);
  36. const where: any = {
  37. ...SearchFilter(search, ['name', 'province.id', 'city.id', 'type', 'ownership']),
  38. province_id: { in: provinceIds },
  39. ...(province ? { province_id: province } : {}),
  40. ...(city ? { city_id: city } : {}),
  41. ...(type ? { type } : {}),
  42. ...(ownership ? { ownership } : {}),
  43. ...(progress_status ? { progress_status } : {}),
  44. deletedAt: null,
  45. };
  46. const [hospitals, total] = await Promise.all([
  47. salesHospitalRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
  48. salesHospitalRepository.countAll(where),
  49. ]);
  50. return { hospitals, total };
  51. };
  52. export const storeHospitalService = async (validateData: HospitalRequestDTO, req: CustomRequest) => {
  53. const creatorId = req.tokenData.sub;
  54. const province = await ProvinceRepository.findById(validateData.province_id!);
  55. if (!province) throw new HttpException('Province not found', 404);
  56. const userArea = await prisma.userArea.findFirst({
  57. where: { user_id: creatorId, province_id: validateData.province_id },
  58. });
  59. if (!userArea) throw new HttpException('You are not allowed to add hospital to this province', 403);
  60. const city = await CityRepository.findById(validateData.city_id!);
  61. if (!city) throw new HttpException('City not found', 404);
  62. const existingHospital = await prisma.hospital.findFirst({
  63. where: {
  64. name: validateData.name,
  65. city_id: validateData.city_id,
  66. deletedAt: null,
  67. },
  68. });
  69. if (existingHospital) throw new HttpException('Hospital with same name in this city already exists', 400);
  70. let imagePath: string | null = null;
  71. if (req.file) {
  72. const ext = '.jpg';
  73. const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
  74. const fullPath = path.join(__dirname, '../../../storage/img', filename);
  75. let outputBuffer = await sharp(req.file.buffer)
  76. .resize({ width: 1280, withoutEnlargement: true })
  77. .toFormat('jpeg', { quality: 80 })
  78. .toBuffer();
  79. // Pastikan size <= 500 KB
  80. let quality = 80;
  81. while (outputBuffer.length > 500 * 1024 && quality > 20) {
  82. quality -= 10;
  83. outputBuffer = await sharp(req.file.buffer)
  84. .resize({ width: 1280, withoutEnlargement: true })
  85. .toFormat('jpeg', { quality })
  86. .toBuffer();
  87. }
  88. await fs.writeFile(fullPath, outputBuffer);
  89. imagePath = `/storage/img/${filename}`;
  90. }
  91. let { latitude = null, longitude = null, gmaps_url: gmapsUrl = null } = validateData;
  92. if (gmapsUrl) {
  93. if (gmapsUrl.includes('www.google.com/maps')) {
  94. const match = gmapsUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
  95. if (match) {
  96. latitude = parseFloat(match[1]);
  97. longitude = parseFloat(match[2]);
  98. } else {
  99. throw new HttpException('Unable to extract coordinates from gmaps_url', 400);
  100. }
  101. } else if (!gmapsUrl.includes('maps.app.goo.gl')) {
  102. throw new HttpException('gmaps_url must be a valid Google Maps URL', 400);
  103. }
  104. } else if (latitude === null || longitude === null) {
  105. throw new HttpException('Either gmaps_url or coordinates must be provided', 400);
  106. }
  107. const payload = {
  108. name: validateData.name!,
  109. hospital_code: validateData.hospital_code,
  110. type: validateData.type,
  111. ownership: validateData.ownership,
  112. address: validateData.address,
  113. contact: validateData.contact,
  114. note: validateData.note,
  115. image: imagePath,
  116. latitude,
  117. longitude,
  118. gmaps_url: gmapsUrl,
  119. progress_status: ProgressStatus.cari_data,
  120. province: {
  121. connect: { id: validateData.province_id },
  122. },
  123. city: {
  124. connect: { id: validateData.city_id },
  125. },
  126. user: {
  127. connect: { id: creatorId },
  128. },
  129. };
  130. const data = await salesHospitalRepository.create(payload);
  131. // if (validateData.tags?.length) {
  132. // await storeCategoryLinkService(
  133. // validateData.tags,
  134. // 'hospital_notes',
  135. // data.id,
  136. // req
  137. // );
  138. // }
  139. await createLog(req, data);
  140. };
  141. export const updateHospitalService = async (validateData: Partial<HospitalRequestDTO>, id: string, req: CustomRequest) => {
  142. const hospital = await HospitalRepository.findById(id);
  143. if (!hospital) throw new HttpException('Hospital data not found', 404);
  144. const isOwner = hospital.created_by === req.tokenData.sub;
  145. // Cek apakah user memiliki akses ke provinsi hospital
  146. const userArea = await prisma.userArea.findFirst({
  147. where: {
  148. user_id: req.tokenData.sub,
  149. province_id: hospital.province.id,
  150. },
  151. });
  152. const hasProvinceAccess = !!userArea;
  153. if (!isOwner && !hasProvinceAccess) {
  154. throw new HttpException('You are not authorized to access this hospital', 403);
  155. }
  156. if (validateData.province_id) {
  157. const province = await ProvinceRepository.findById(validateData.province_id);
  158. if (!province) throw new HttpException('Province not found', 404);
  159. }
  160. if (validateData.city_id) {
  161. const city = await CityRepository.findById(validateData.city_id);
  162. if (!city) throw new HttpException('City not found', 404);
  163. }
  164. if (
  165. validateData.progress_status &&
  166. !Object.values(ProgressStatus).includes(validateData.progress_status as ProgressStatus)
  167. ) {
  168. throw new HttpException(
  169. `Invalid progress_status. Allowed values are: ${Object.values(ProgressStatus).join(', ')}`,
  170. 422
  171. );
  172. }
  173. if (validateData.name && validateData.city_id) {
  174. const existingHospital = await prisma.hospital.findFirst({
  175. where: {
  176. name: validateData.name,
  177. city_id: validateData.city_id,
  178. deletedAt: null,
  179. NOT: { id },
  180. },
  181. });
  182. if (existingHospital) {
  183. throw new HttpException('Hospital with same name in this city already exists', 400);
  184. }
  185. }
  186. let imagePath = hospital.image;
  187. if (req.file) {
  188. const ext = '.jpg';
  189. const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
  190. const fullPath = path.join(__dirname, '../../../storage/img', filename);
  191. let outputBuffer = await sharp(req.file.buffer)
  192. .resize({ width: 1280, withoutEnlargement: true })
  193. .toFormat('jpeg', { quality: 80 })
  194. .toBuffer();
  195. // Pastikan size <= 500 KB
  196. let quality = 80;
  197. while (outputBuffer.length > 500 * 1024 && quality > 20) {
  198. quality -= 10;
  199. outputBuffer = await sharp(req.file.buffer)
  200. .resize({ width: 1280, withoutEnlargement: true })
  201. .toFormat('jpeg', { quality })
  202. .toBuffer();
  203. }
  204. await fs.writeFile(fullPath, outputBuffer);
  205. imagePath = `/storage/img/${filename}`;
  206. }
  207. let latitude = hospital.latitude;
  208. let longitude = hospital.longitude;
  209. let gmapsUrl = hospital.gmaps_url;
  210. if (
  211. validateData.latitude !== undefined &&
  212. validateData.longitude !== undefined &&
  213. validateData.latitude !== null &&
  214. validateData.longitude !== null
  215. ) {
  216. latitude = validateData.latitude;
  217. longitude = validateData.longitude;
  218. gmapsUrl = validateData.gmaps_url || gmapsUrl;
  219. } else if (validateData.gmaps_url?.trim()) {
  220. gmapsUrl = validateData.gmaps_url;
  221. if (gmapsUrl.includes('www.google.com/maps')) {
  222. const match = gmapsUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
  223. if (match) {
  224. latitude = parseFloat(match[1]);
  225. longitude = parseFloat(match[2]);
  226. } else {
  227. throw new HttpException('Unable to extract coordinates from gmaps_url', 400);
  228. }
  229. } else if (!gmapsUrl.includes('maps.app.goo.gl')) {
  230. throw new HttpException('gmaps_url must be a valid Google Maps URL', 400);
  231. }
  232. }
  233. const payload: Prisma.HospitalUpdateInput = {
  234. ...(validateData.name && { name: validateData.name }),
  235. ...(validateData.hospital_code && { hospital_code: validateData.hospital_code }),
  236. ...(validateData.type && { type: validateData.type }),
  237. ...(validateData.ownership && { ownership: validateData.ownership }),
  238. ...(validateData.address && { address: validateData.address }),
  239. ...(validateData.contact && { contact: validateData.contact }),
  240. ...(validateData.note !== undefined && { note: validateData.note }),
  241. ...(validateData.progress_status && { progress_status: validateData.progress_status }),
  242. ...(imagePath && { image: imagePath }),
  243. ...(latitude !== undefined && { latitude }),
  244. ...(longitude !== undefined && { longitude }),
  245. ...(gmapsUrl !== undefined && { gmaps_url: gmapsUrl }),
  246. ...(validateData.province_id && {
  247. province: {
  248. connect: { id: validateData.province_id }
  249. }
  250. }),
  251. ...(validateData.city_id && {
  252. city: {
  253. connect: { id: validateData.city_id }
  254. }
  255. }),
  256. };
  257. const data = await salesHospitalRepository.update(id, payload);
  258. // if (validateData.tags?.length) {
  259. // await updateCategoryLinkService(
  260. // validateData.tags,
  261. // 'hospital_notes',
  262. // id,
  263. // req
  264. // );
  265. // }
  266. await updateLog(req, data);
  267. };
  268. export const showHospitalService = async (id: string, req: CustomRequest) => {
  269. const hospital = await salesHospitalRepository.findById(id);
  270. if (!hospital) throw new HttpException('Data hospital not found', 404);
  271. const isOwner = hospital.created_by === req.tokenData.sub;
  272. // Cek apakah user memiliki akses ke provinsi hospital
  273. const userArea = await prisma.userArea.findFirst({
  274. where: {
  275. user_id: req.tokenData.sub,
  276. province_id: hospital.province.id,
  277. },
  278. });
  279. const hasProvinceAccess = !!userArea;
  280. if (!isOwner && !hasProvinceAccess) {
  281. throw new HttpException('You are not authorized to access this hospital', 403);
  282. }
  283. return { hospital };
  284. };