From 2a13cb25df97906cd92dcfac121cbb34b6394410 Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Mon, 9 Mar 2026 10:57:17 +0700 Subject: [PATCH] update: add new field picture and location --- src/controllers/hospital_information.ts | 1 - src/controllers/room.ts | 240 ++++++++++++++++++++---- src/routes/private.ts | 2 + src/swagger/builder.js | 4 +- 4 files changed, 213 insertions(+), 34 deletions(-) diff --git a/src/controllers/hospital_information.ts b/src/controllers/hospital_information.ts index cbef8aa..15a31c6 100644 --- a/src/controllers/hospital_information.ts +++ b/src/controllers/hospital_information.ts @@ -15,7 +15,6 @@ import config from "config"; import fs from "fs"; import path from "path"; - const log: Logger = new Logger({ name: "[HospitalInformationController]", type: "pretty", diff --git a/src/controllers/room.ts b/src/controllers/room.ts index ee96f0f..3787ef5 100644 --- a/src/controllers/room.ts +++ b/src/controllers/room.ts @@ -10,24 +10,33 @@ import { RoomModel } from "../model/room"; import { DepartmentModel } from "../model/department"; import { OrmHelper } from "../helpers/orm"; import moment from "moment"; +import fileUpload from "express-fileupload"; +import { v4 as uuidv4 } from 'uuid'; +import fs from "fs"; +import path from "path"; const log: Logger = new Logger({ name: "[RoomController]", type: "pretty", }); +interface FileUploadRequest extends Request { + files?: fileUpload.FileArray | null; + body: any; +} + export class RoomController { static async list(req: Request, res: Response, next: NextFunction): Promise { /* - #swagger.tags = ['Room'] - #swagger.security = [{ "bearerAuth": [] }] - #swagger.parameters['filter'] = { description: 'Filter: plain text or JSON object (status, code, departmentId, etc.)', in: 'query', type: 'string' } - #swagger.parameters['limit'] = { in: 'query', required: true, type: 'number' } - #swagger.parameters['page'] = { in: 'query', required: true, type: 'number' } - #swagger.parameters['with_deleted'] = { in: 'query', required: true, type: 'boolean' } - #swagger.parameters['order_field'] = { in: 'query', required: true, type: 'string' } - #swagger.parameters['order_direction'] = { in: 'query', required: true, schema: { '@enum': ['ASC', 'DESC'] } } - */ + #swagger.tags = ['Room'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['filter'] = { description: 'Filter: plain text or JSON object (status, code, departmentId, etc.)', in: 'query', type: 'string' } + #swagger.parameters['limit'] = { in: 'query', required: true, type: 'number' } + #swagger.parameters['page'] = { in: 'query', required: true, type: 'number' } + #swagger.parameters['with_deleted'] = { in: 'query', required: true, type: 'boolean' } + #swagger.parameters['order_field'] = { in: 'query', required: true, type: 'string' } + #swagger.parameters['order_direction'] = { in: 'query', required: true, schema: { '@enum': ['ASC', 'DESC'] } } + */ try { const schema = Joi.object().keys({ filter: Joi.string().allow("").optional().label("Filter"), @@ -56,12 +65,20 @@ export class RoomController { res_list.withDeleted(); } + const photoBaseUrl = "https://his.shiblysolution.id/service-master-data/uploads/room-picture/"; + const current_page = param.page; const total_count_data = await res_count.getCount(); const list_data = await res_list.getMany(); + const mapped_data = list_data.map((item: any) => ({ + ...item, + picture: item.picture + ? photoBaseUrl + item.picture + : null + })); const count_data = CommonHelper.countObject(list_data); - return ReturnHelper.successResponselist(res, 200, Language.lang.success_view, count_data, current_page, total_count_data, list_data); + return ReturnHelper.successResponselist(res, 200, Language.lang.success_view, count_data, current_page, total_count_data, mapped_data); } catch (e: unknown) { log.error(e); const err = e as Error; @@ -71,10 +88,10 @@ export class RoomController { static async create(req: Request, res: Response, next: NextFunction): Promise { /* - #swagger.tags = ['Room'] - #swagger.security = [{ "bearerAuth": [] }] - #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/room" } } } } - */ + #swagger.tags = ['Room'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/room" } } } } + */ const queryRunner = OrmHelper.DB.createQueryRunner(); await queryRunner.startTransaction(); try { @@ -84,6 +101,8 @@ export class RoomController { description: Joi.string().allow("").max(512).optional().label("Description"), department_id: Joi.string().uuid().required().label("Department ID"), status: Joi.string().required().label("Status"), + location: Joi.string().required().label("Location"), + picture: Joi.string().required().label("picture"), }); let param: any = await schema.validateAsync(req.body); @@ -100,6 +119,8 @@ export class RoomController { room.description = param.description || null; room.department = department; room.status = param.status; + room.location = param.location; + room.picture = param.picture; room.created_by = req.auth.data.name; room.updated_by = req.auth.data.name; await queryRunner.manager.save(room); @@ -118,10 +139,10 @@ export class RoomController { static async detail(req: Request, res: Response, next: NextFunction): Promise { /* - #swagger.tags = ['Room'] - #swagger.security = [{ "bearerAuth": [] }] - #swagger.parameters['id'] = { in: 'path', type: 'string' } - */ + #swagger.tags = ['Room'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['id'] = { in: 'path', type: 'string' } + */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("Room Id"), @@ -140,11 +161,11 @@ export class RoomController { static async update(req: Request, res: Response, next: NextFunction): Promise { /* - #swagger.tags = ['Room'] - #swagger.security = [{ "bearerAuth": [] }] - #swagger.parameters['id'] = { in: 'path', type: 'string' } - #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/room" } } } } - */ + #swagger.tags = ['Room'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['id'] = { in: 'path', type: 'string' } + #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/room" } } } } + */ const queryRunner = OrmHelper.DB.createQueryRunner(); await queryRunner.startTransaction(); try { @@ -156,6 +177,8 @@ export class RoomController { description: Joi.string().allow("").max(512).optional().label("Description"), department_id: Joi.string().uuid().required().label("Department ID"), status: Joi.string().required().label("Status"), + location: Joi.string().required().label("Location"), + picture: Joi.string().required().label("picture"), }); let param: any = await schema.validateAsync(req.body); @@ -173,6 +196,8 @@ export class RoomController { room.description = param.description || null; room.department = department; room.status = param.status; + room.location = param.location; + room.picture = param.picture; room.updated_by = req.auth.data.name; await queryRunner.manager.save(room); @@ -190,11 +215,11 @@ export class RoomController { static async delete(req: Request, res: Response, next: NextFunction): Promise { /* - #swagger.tags = ['Room'] - #swagger.security = [{ "bearerAuth": [] }] - #swagger.parameters['id'] = { in: 'path', required: true, type: 'string' } - #swagger.parameters['hard'] = { in: 'path', required: false, type: 'boolean' } - */ + #swagger.tags = ['Room'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['id'] = { in: 'path', required: true, type: 'string' } + #swagger.parameters['hard'] = { in: 'path', required: false, type: 'boolean' } + */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("ID"), @@ -229,10 +254,10 @@ export class RoomController { static async restore(req: Request, res: Response, next: NextFunction): Promise { /* - #swagger.tags = ['Room'] - #swagger.security = [{ "bearerAuth": [] }] - #swagger.parameters['id'] = { in: 'path', required: true, type: 'string' } - */ + #swagger.tags = ['Room'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['id'] = { in: 'path', required: true, type: 'string' } + */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("ID"), @@ -251,4 +276,155 @@ export class RoomController { return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_restore, err.message); } } + + static async uploadpicture(req: FileUploadRequest, res: Response, next: NextFunction): Promise { + /* #swagger.tags = ['Room'] + #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/room-picture' + }, + 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/room-picture/'; + + // Generate unique filename + const ext = file.name.split('.'); + const name = uuidv4() + '.' + ext[ext.length - 1]; + + const result: { file: 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 deletepicture(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Room'] + #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/room-picture/'; + 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/routes/private.ts b/src/routes/private.ts index 2cd6074..74ff9c6 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -135,6 +135,8 @@ 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.post('/api/room/upload-picture', RoomController.uploadpicture) + app.delete('/api/room/delete-picture', RoomController.deletepicture) app.get('/api/laboratory/list', LaboratoryController.list) diff --git a/src/swagger/builder.js b/src/swagger/builder.js index 596523b..436a78b 100644 --- a/src/swagger/builder.js +++ b/src/swagger/builder.js @@ -111,7 +111,9 @@ const doc = { $code: "R001", $description: "Optional description", $department_id: "uuid-string", - $status: "Y" + $status: "Y", + $location: "Room location", + $picture: "Room picture URL" }, laboratory: { $room: "Room name",