diff --git a/.gitignore b/.gitignore index 7492949..f2b1712 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode/ .DS_Store +/uploads/ node_modules/ build/ diff --git a/src/controllers/hospital_information.ts b/src/controllers/hospital_information.ts new file mode 100644 index 0000000..c826d08 --- /dev/null +++ b/src/controllers/hospital_information.ts @@ -0,0 +1,298 @@ +import { HospitalInformation } from "entity"; +import { NextFunction, Response } from "express"; +import { Request } from "express-jwt"; +import Joi from "joi"; +import { ILogObj, Logger } from "tslog"; +import CommonHelper from "../helpers/common"; +import { ReturnHelper } from "../helpers/express/return"; +import { Language } from "../langs/lang"; +import { OrmHelper } from "../helpers/orm"; +import moment from "moment"; +import dayjs from 'dayjs'; +import fileUpload from "express-fileupload"; +import { v4 as uuidv4 } from 'uuid'; +import config from "config"; +import fs from "fs"; +import path from "path"; + + +const log: Logger = new Logger({ + name: "[HospitalInformationController]", + type: "pretty", +}); + +const hospitalCreateUpdateSchema = { + name: Joi.string().required().label("Hospital Name"), + address: Joi.string().required().label("Hospital Address"), + phone: Joi.string().required().label("Hospital Phone"), + logo: Joi.string().required().label("Hospital Logo"), +}; + +interface FileUploadRequest extends Request { + files?: fileUpload.FileArray | null; + body: any; +} + +export class HospitalInformationController { + + static async view(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Hospital Information'] + #swagger.security = [{ "bearerAuth": [] }] + */ + try { + const hospital = await OrmHelper.DB.getRepository(HospitalInformation).findOne({ + where: {} + }); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, hospital); + + } catch (e: unknown) { + log.error(e); + const err = e as Error; + return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); + } + } + + static async create(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Hospital Information'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/hospitalinformation" } } } } + */ + const queryRunner = OrmHelper.DB.createQueryRunner(); + await queryRunner.startTransaction(); + try { + const schema = Joi.object().keys({ + ...hospitalCreateUpdateSchema, + }); + + let param: any = await schema.validateAsync(req.body); + + const hospital = await OrmHelper.DB.getRepository(HospitalInformation).findOne({ + where: {} + }); + + if (hospital) throw { message: "Hospital is already found, you cannot create a new one" }; + + let hospitalinformation = new HospitalInformation(); + hospitalinformation.name = param.name; + hospitalinformation.address = param.address; + hospitalinformation.phone = param.phone; + hospitalinformation.logo = param.logo; + hospitalinformation.created_by = req.auth.data.name; + hospitalinformation.updated_by = req.auth.data.name; + await queryRunner.manager.save(hospitalinformation); + + await queryRunner.commitTransaction(); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, hospitalinformation); + } catch (e: unknown) { + await queryRunner.rollbackTransaction(); + log.error(e); + const err = e as Error; + return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); + } finally { + await queryRunner.release(); + } + } + + static async update(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Hospital Information'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['id'] = { description: '', in: 'path', type: 'string' } + #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/hospitalinformation" } } } } + */ + const queryRunner = OrmHelper.DB.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + req.body.id = req.params["id"]; + + const schema = Joi.object().keys({ + id: Joi.string().uuid().required().label("Hospital Information ID"), + ...hospitalCreateUpdateSchema, + }); + + let param: any = await schema.validateAsync(req.body); + + const hospital = await OrmHelper.DB.getRepository(HospitalInformation).findOne({ + where: { id: param.id } + }); + + if (!hospital) throw { message: "Hospital " + Language.lang.failed_not_found }; + + let hospitalinformation = new HospitalInformation(); + hospitalinformation.id = param.id; + hospitalinformation.name = param.name; + hospitalinformation.address = param.address; + hospitalinformation.phone = param.phone; + hospitalinformation.logo = param.logo; + hospitalinformation.updated_by = req.auth.data.name; + await queryRunner.manager.save(hospitalinformation); + + await queryRunner.commitTransaction(); + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, hospitalinformation); + } catch (e: unknown) { + await queryRunner.rollbackTransaction(); + log.error(e); + const err = e as Error; + return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); + } finally { + await queryRunner.release(); + } + } + + static async uploadlogo(req: FileUploadRequest, res: Response, next: NextFunction): Promise { + /* #swagger.tags = ['Hospital Information'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.consumes = ['multipart/form-data'] + #swagger.requestBody = { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + file: { + type: "string", + format: "binary", + description: "Image file (max 1MB, jpg/png/webp)" + }, + path: { + type: "string", + description: 'File path', + default: 'uploads/hospital-logo' + }, + application: { + type: "string", + description: 'Application name', + default: 'saude' + } + }, + required: ["file", "path", "application"] + } + } + } + } + */ + try { + // Validation schema + const schema = Joi.object().keys({ + path: Joi.string().label('File Path'), + application: Joi.string().label('Application'), + }); + + log.info("body", req.body); + + const param: { path: string, application: string } = await schema.validateAsync(req.body); + + // Check if file exists + if (!req.files || !req.files.file) { + return ReturnHelper.errorResponse(res, 400, 404, Language.lang.failed_insert, "File not found"); + } + + const file = req.files.file as fileUpload.UploadedFile; + + // Validate file size (max 1MB) + const maxSize = 1024 * 1024; // 1MB + if (file.size > maxSize) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "File size exceeds 1MB limit"); + } + + // Validate file type + const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + if (!allowedMimes.includes(file.mimetype)) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid file type. Only JPG, PNG, and WEBP are allowed"); + } + + // Normalize path + param.path = param.path[param.path.length - 1] !== '/' ? param.path + '/' : param.path; + + if (param.path == undefined || param.path == null) param.path = 'uploads/hospital-logo/'; + + // Generate unique filename + const ext = file.name.split('.'); + const name = uuidv4() + '.' + ext[ext.length - 1]; + + const result: { url: string, path: string, file: string, mime: string, size: number, md5: string } = { + url: "", + path: "", + file: "", + mime: file.mimetype, + size: file.size, + md5: file.md5 + }; + + const folder_path = param.path; + const file_path = path.join(folder_path, name); + + // Create directory if it doesn't exist + if (!fs.existsSync(folder_path)) { + fs.mkdirSync(folder_path, { recursive: true }); + } + + // Move file to destination + await file.mv(file_path); + + result.path = param.path[param.path.length - 1] === '/' ? param.path.substring(0, param.path.length - 1) : param.path; + result.file = name; + result.url = `${result.path}/${name}`; // Generate URL + + log.info(`File uploaded successfully: ${file_path}`); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, result); + + } catch (e: unknown) { + // Proper error logging to avoid tslog serialization issues + if (e instanceof Error) { + log.error("File upload failed:", { + message: e.message, + stack: e.stack, + name: e.name + }); + return ReturnHelper.errorResponse(res, 500, 500, Language.lang.failed_insert, e.message); + } else { + log.error("File upload failed with unknown error:", String(e)); + return ReturnHelper.errorResponse(res, 500, 500, Language.lang.failed_insert, "Unknown error occurred"); + } + } + } + + static async deletelogo(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Hospital Information'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/files" } } } } + */ + + try { + const schema = Joi.object().keys({ + file_name: Joi.string().required().label('File Name'), + }); + + const param: { file_name: string } = await schema.validateAsync(req.body); + + // const env = process.env.NODE_ENV + '/' + const fixedPath = 'uploads/hospital-logo/'; + const file_path = fixedPath + param.file_name; + + // Check if file exists + if (!fs.existsSync(file_path)) { + return ReturnHelper.errorResponse(res, 404, 404, Language.lang.failed_not_found, "File not found"); + } + + // Delete file + fs.unlinkSync(file_path); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {}); + } catch (e: unknown) { + log.error(e); + const err = e as Error; + + return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_delete, err.message); + } + } + +} diff --git a/src/helpers/express/upload.ts b/src/helpers/express/upload.ts new file mode 100644 index 0000000..1b8820d --- /dev/null +++ b/src/helpers/express/upload.ts @@ -0,0 +1,12 @@ +import config from 'config'; +import express from 'express'; +import fileUpload from 'express-fileupload' + +export class UploadHelper { + static setup(app: express.Application) { + app.use(fileUpload({ + limits: { fileSize: 10485760 }, + abortOnLimit: true, + })); + } +} \ No newline at end of file diff --git a/src/helpers/orm.ts b/src/helpers/orm.ts index bffa9ba..5a47346 100644 --- a/src/helpers/orm.ts +++ b/src/helpers/orm.ts @@ -1,7 +1,7 @@ import config from 'config'; import { DataSource } from "typeorm"; import { ILogObj, Logger } from 'tslog'; -import { Administrativu, Aldeia, District, Menu, Munisipo, Suco, Application, Nationality, Diagnosis, DiagnosticProcedure, Room, ServiceType, ServiceClass, Service, ServiceFare, ServicePackage, Department, DoctorSchedule, MeasurementUnit, ReferenceRange, QueueMonitoring, ExaminationDetail, ExaminationType, User, UserRole, HrmsEmployee, HrmsDepartment, HrmsPosition, HrmsShift } from 'entity' +import { Administrativu, Aldeia, District, Menu, Munisipo, Suco, Application, Nationality, Diagnosis, DiagnosticProcedure, Room, ServiceType, ServiceClass, Service, ServiceFare, ServicePackage, Department, DoctorSchedule, MeasurementUnit, ReferenceRange, QueueMonitoring, ExaminationDetail, ExaminationType, User, UserRole, HrmsEmployee, HrmsDepartment, HrmsPosition, HrmsShift,HospitalInformation } from 'entity' export class OrmHelper { static DB: DataSource = null @@ -20,7 +20,7 @@ export class OrmHelper { database: String(config.get("database.database")), synchronize: true, logging: config.get('database.logging'), - entities: [Menu, Administrativu, Aldeia, District, Munisipo, Suco, Application, Nationality, Diagnosis, DiagnosticProcedure, Room, ServiceType, ServiceClass, Service, ServiceFare, ServicePackage, Department, DoctorSchedule, MeasurementUnit, ReferenceRange, QueueMonitoring, ExaminationDetail, ExaminationType, User, UserRole, HrmsEmployee, HrmsDepartment, HrmsPosition, HrmsShift], + entities: [Menu, Administrativu, Aldeia, District, Munisipo, Suco, Application, Nationality, Diagnosis, DiagnosticProcedure, Room, ServiceType, ServiceClass, Service, ServiceFare, ServicePackage, Department, DoctorSchedule, MeasurementUnit, ReferenceRange, QueueMonitoring, ExaminationDetail, ExaminationType, User, UserRole, HrmsEmployee, HrmsDepartment, HrmsPosition, HrmsShift,HospitalInformation], subscribers: [], migrations: [], }) diff --git a/src/routes/private.ts b/src/routes/private.ts index d15ed4a..5a823fb 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -27,6 +27,7 @@ import { ExaminationDetailController } from '../controllers/examination_detail'; import { ExaminationTypeController } from '../controllers/examination_type'; import { ServiceExaminationController } from '../controllers/service_examination'; import { LaboratoryController } from '../controllers/laboratory'; +import { HospitalInformationController } from '../controllers/hospital_information'; export class RoutePrivate { static setup(app: express.Application) { @@ -111,7 +112,7 @@ export class RoutePrivate { app.put('/api/room/update/:id', RoomController.update) app.delete('/api/room/delete/:id/:hard', RoomController.delete) app.put('/api/room/restore/:id', RoomController.restore) - + app.get('/api/laboratory/list', LaboratoryController.list) app.get('/api/room-services/list', RoomServicesController.list) @@ -167,7 +168,7 @@ export class RoutePrivate { app.put('/api/doctor-schedule/update/:id', DoctorScheduleController.update) app.delete('/api/doctor-schedule/delete/:id/:hard', DoctorScheduleController.delete) app.put('/api/doctor-schedule/restore/:id', DoctorScheduleController.restore) - + app.get('/api/measurement-unit/list', MeasurementUnitController.list) app.post('/api/measurement-unit/create', MeasurementUnitController.create) app.get('/api/measurement-unit/detail/:id', MeasurementUnitController.detail) @@ -207,5 +208,11 @@ export class RoutePrivate { app.get('/api/service-examination/list', ServiceExaminationController.list) app.put('/api/service-examination/:id', ServiceExaminationController.update) + app.get('/api/hospital-information/view', HospitalInformationController.view) + app.post('/api/hospital-information/create', HospitalInformationController.create) + app.put('/api/hospital-information/update/:id', HospitalInformationController.update) + app.post('/api/hospital-information/upload-logo', HospitalInformationController.uploadlogo) + app.delete('/api/hospital-information/delete-logo', HospitalInformationController.deletelogo) + } } \ No newline at end of file diff --git a/src/routes/public.ts b/src/routes/public.ts index 90b7c8f..415a0d5 100644 --- a/src/routes/public.ts +++ b/src/routes/public.ts @@ -1,9 +1,53 @@ +// import express from 'express'; +// import { Language } from '../langs/lang'; + +// export class RoutePublic { +// static setup(app: express.Application) { + +// app.use(Language.apply) +// } +// } + +// import express from 'express'; +// import { Language } from '../langs/lang'; + +// export class RoutePublic { +// static setup(app: express.Application) { + +// app.use(Language.apply) +// } +// } + import express from 'express'; +import path from 'path'; +import fs from 'fs'; import { Language } from '../langs/lang'; export class RoutePublic { static setup(app: express.Application) { + app.use(Language.apply); - app.use(Language.apply) + // ⚠️ TAMBAHKAN INI - Serve static files untuk uploads + app.use('/uploads', express.static(path.join(__dirname, '../../uploads'))); + + // Atau dengan custom error handling (pilih salah satu): + /* + app.get('/uploads/*', (req, res) => { + const filePath = path.join(__dirname, '../../', req.path); + + if (fs.existsSync(filePath)) { + res.set({ + 'Cache-Control': 'public, max-age=86400', + 'Access-Control-Allow-Origin': '*' + }); + res.sendFile(filePath); + } else { + res.status(404).json({ + success: false, + message: 'File not found' + }); + } + }); + */ } } \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 6e62151..8a587f2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,9 +9,10 @@ import { RoutePrivate } from './routes/private'; import { RoutePublic } from './routes/public'; import { Language } from './langs/lang'; import { SwaggerHelper } from './helpers/express/swagger'; - +import { UploadHelper } from './helpers/express/upload'; const app = express(); +UploadHelper.setup(app) CorsHelper.setup(app); CompressionHelper.setup(app); MorganHelper.setup(app); diff --git a/src/swagger/builder.js b/src/swagger/builder.js index 305ea53..386e87c 100644 --- a/src/swagger/builder.js +++ b/src/swagger/builder.js @@ -192,6 +192,15 @@ const doc = { serviceExamination: { $examination_detail_ids: ["uuid-string"] }, + hospitalinformation: { + $name: "Hospital Name", + $address: "Hospital Address", + $phone: "Hospital Phone", + $logo: "Hospital Logo", + }, + files: { + $file_name: "File Name", + }, }, parameters: {