import { Prisma, PrismaClient, ProgressStatus } from '@prisma/client'; import { SearchFilter } from '../../utils/SearchFilter'; import { createLog, updateLog } from '../../utils/LogActivity'; import { HttpException } from '../../utils/HttpException'; import salesHospitalRepository from '../../repository/sales/HospitalRepository'; import ProvinceRepository from '../../repository/admin/ProvinceRepository'; import CityRepository from '../../repository/admin/CityRepository'; import HospitalRepository from '../../repository/admin/HospitalRepository'; import { CustomRequest } from '../../types/token/CustomRequest'; import { HospitalRequestDTO } from '../../types/sales/hospital/HospitalDTO'; import path from 'path'; import fs from 'fs/promises'; import sharp from 'sharp'; // import { storeCategoryLinkService, updateCategoryLinkService } from '../admin/CategoryLinkService'; const prisma = new PrismaClient(); export interface PaginationParams { page: number; limit: number; search?: string; sortBy: string; orderBy: 'asc' | 'desc'; province?: string; city?: string; type?: string; ownership?: string; progress_status?: string; } export const getAllHospitalByAreaService = async (params: PaginationParams, req: CustomRequest) => { const { page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status } = params; const skip = (page - 1) * limit; const userAreas = await prisma.userArea.findMany({ where: { user_id: req.tokenData.sub }, select: { province_id: true }, }); const provinceIds = userAreas.map((ua) => ua.province_id); const where: any = { ...SearchFilter(search, ['name', 'province.id', 'city.id', 'type', 'ownership']), province_id: { in: provinceIds }, ...(province ? { province_id: province } : {}), ...(city ? { city_id: city } : {}), ...(type ? { type } : {}), ...(ownership ? { ownership } : {}), ...(progress_status ? { progress_status } : {}), deletedAt: null, }; const [hospitals, total] = await Promise.all([ salesHospitalRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }), salesHospitalRepository.countAll(where), ]); return { hospitals, total }; }; export const storeHospitalService = async (validateData: HospitalRequestDTO, req: CustomRequest) => { const creatorId = req.tokenData.sub; const province = await ProvinceRepository.findById(validateData.province_id!); if (!province) throw new HttpException('Province not found', 404); const userArea = await prisma.userArea.findFirst({ where: { user_id: creatorId, province_id: validateData.province_id }, }); if (!userArea) throw new HttpException('You are not allowed to add hospital to this province', 403); const city = await CityRepository.findById(validateData.city_id!); if (!city) throw new HttpException('City not found', 404); const existingHospital = await prisma.hospital.findFirst({ where: { name: validateData.name, city_id: validateData.city_id, deletedAt: null, }, }); if (existingHospital) throw new HttpException('Hospital with same name in this city already exists', 400); let imagePath: string | null = null; if (req.file) { const ext = '.jpg'; const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`; const fullPath = path.join(__dirname, '../../../storage/img', filename); let outputBuffer = await sharp(req.file.buffer) .resize({ width: 1280, withoutEnlargement: true }) .toFormat('jpeg', { quality: 80 }) .toBuffer(); // Pastikan size <= 500 KB let quality = 80; while (outputBuffer.length > 500 * 1024 && quality > 20) { quality -= 10; outputBuffer = await sharp(req.file.buffer) .resize({ width: 1280, withoutEnlargement: true }) .toFormat('jpeg', { quality }) .toBuffer(); } await fs.writeFile(fullPath, outputBuffer); imagePath = `/storage/img/${filename}`; } let { latitude = null, longitude = null, gmaps_url: gmapsUrl = null } = validateData; if (gmapsUrl) { if (gmapsUrl.includes('www.google.com/maps')) { const match = gmapsUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/); if (match) { latitude = parseFloat(match[1]); longitude = parseFloat(match[2]); } else { throw new HttpException('Unable to extract coordinates from gmaps_url', 400); } } else if (!gmapsUrl.includes('maps.app.goo.gl')) { throw new HttpException('gmaps_url must be a valid Google Maps URL', 400); } } else if (latitude === null || longitude === null) { throw new HttpException('Either gmaps_url or coordinates must be provided', 400); } const payload = { name: validateData.name!, hospital_code: validateData.hospital_code, type: validateData.type, ownership: validateData.ownership, address: validateData.address, contact: validateData.contact, note: validateData.note, image: imagePath, latitude, longitude, gmaps_url: gmapsUrl, progress_status: ProgressStatus.cari_data, province: { connect: { id: validateData.province_id }, }, city: { connect: { id: validateData.city_id }, }, user: { connect: { id: creatorId }, }, }; const data = await salesHospitalRepository.create(payload); // if (validateData.tags?.length) { // await storeCategoryLinkService( // validateData.tags, // 'hospital_notes', // data.id, // req // ); // } await createLog(req, data); }; export const updateHospitalService = async (validateData: Partial, id: string, req: CustomRequest) => { const hospital = await HospitalRepository.findById(id); if (!hospital) throw new HttpException('Hospital data not found', 404); const isOwner = hospital.created_by === req.tokenData.sub; // Cek apakah user memiliki akses ke provinsi hospital const userArea = await prisma.userArea.findFirst({ where: { user_id: req.tokenData.sub, province_id: hospital.province.id, }, }); const hasProvinceAccess = !!userArea; if (!isOwner && !hasProvinceAccess) { throw new HttpException('You are not authorized to access this hospital', 403); } if (validateData.province_id) { const province = await ProvinceRepository.findById(validateData.province_id); if (!province) throw new HttpException('Province not found', 404); } if (validateData.city_id) { const city = await CityRepository.findById(validateData.city_id); if (!city) throw new HttpException('City not found', 404); } if ( validateData.progress_status && !Object.values(ProgressStatus).includes(validateData.progress_status as ProgressStatus) ) { throw new HttpException( `Invalid progress_status. Allowed values are: ${Object.values(ProgressStatus).join(', ')}`, 422 ); } if (validateData.name && validateData.city_id) { const existingHospital = await prisma.hospital.findFirst({ where: { name: validateData.name, city_id: validateData.city_id, deletedAt: null, NOT: { id }, }, }); if (existingHospital) { throw new HttpException('Hospital with same name in this city already exists', 400); } } let imagePath = hospital.image; if (req.file) { const ext = '.jpg'; const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`; const fullPath = path.join(__dirname, '../../../storage/img', filename); let outputBuffer = await sharp(req.file.buffer) .resize({ width: 1280, withoutEnlargement: true }) .toFormat('jpeg', { quality: 80 }) .toBuffer(); // Pastikan size <= 500 KB let quality = 80; while (outputBuffer.length > 500 * 1024 && quality > 20) { quality -= 10; outputBuffer = await sharp(req.file.buffer) .resize({ width: 1280, withoutEnlargement: true }) .toFormat('jpeg', { quality }) .toBuffer(); } await fs.writeFile(fullPath, outputBuffer); imagePath = `/storage/img/${filename}`; } let latitude = hospital.latitude; let longitude = hospital.longitude; let gmapsUrl = hospital.gmaps_url; if ( validateData.latitude !== undefined && validateData.longitude !== undefined && validateData.latitude !== null && validateData.longitude !== null ) { latitude = validateData.latitude; longitude = validateData.longitude; gmapsUrl = validateData.gmaps_url || gmapsUrl; } else if (validateData.gmaps_url?.trim()) { gmapsUrl = validateData.gmaps_url; if (gmapsUrl.includes('www.google.com/maps')) { const match = gmapsUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/); if (match) { latitude = parseFloat(match[1]); longitude = parseFloat(match[2]); } else { throw new HttpException('Unable to extract coordinates from gmaps_url', 400); } } else if (!gmapsUrl.includes('maps.app.goo.gl')) { throw new HttpException('gmaps_url must be a valid Google Maps URL', 400); } } const payload: Prisma.HospitalUpdateInput = { ...(validateData.name && { name: validateData.name }), ...(validateData.hospital_code && { hospital_code: validateData.hospital_code }), ...(validateData.type && { type: validateData.type }), ...(validateData.ownership && { ownership: validateData.ownership }), ...(validateData.address && { address: validateData.address }), ...(validateData.contact && { contact: validateData.contact }), ...(validateData.note !== undefined && { note: validateData.note }), ...(validateData.progress_status && { progress_status: validateData.progress_status }), ...(imagePath && { image: imagePath }), ...(latitude !== undefined && { latitude }), ...(longitude !== undefined && { longitude }), ...(gmapsUrl !== undefined && { gmaps_url: gmapsUrl }), ...(validateData.province_id && { province: { connect: { id: validateData.province_id } } }), ...(validateData.city_id && { city: { connect: { id: validateData.city_id } } }), }; const data = await salesHospitalRepository.update(id, payload); // if (validateData.tags?.length) { // await updateCategoryLinkService( // validateData.tags, // 'hospital_notes', // id, // req // ); // } await updateLog(req, data); }; export const showHospitalService = async (id: string, req: CustomRequest) => { const hospital = await salesHospitalRepository.findById(id); if (!hospital) throw new HttpException('Data hospital not found', 404); const isOwner = hospital.created_by === req.tokenData.sub; // Cek apakah user memiliki akses ke provinsi hospital const userArea = await prisma.userArea.findFirst({ where: { user_id: req.tokenData.sub, province_id: hospital.province.id, }, }); const hasProvinceAccess = !!userArea; if (!isOwner && !hasProvinceAccess) { throw new HttpException('You are not authorized to access this hospital', 403); } return { hospital }; };