import { Room, Paging, Service } 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 { RoomModel } from "../model/room"; import { DepartmentModel } from "../model/department"; import { ServiceClassModel } from "../model/service_class"; 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"; import { Brackets } from "typeorm"; 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'] } } */ try { const schema = Joi.object().keys({ filter: Joi.string().allow("").optional().label("Filter"), page: Joi.number().required().min(1).label("Page"), limit: Joi.number().required().min(1).label("Limit"), with_deleted: Joi.bool().required().label("With Deleted"), order_field: Joi.string().required().label("Order Field"), order_direction: Joi.string().allow("asc", "desc").required().label("Order Direction"), }); let param: Paging = await schema.validateAsync(req.query); let filter = param.filter && param.filter !== "" ? JSON.parse(param.filter) : {}; const filterAny = filter.any && filter.any !== "" ? String(filter.any) : ""; let filterObj = { ...filter } as any; delete filterObj.any; var query = await RoomModel.list(filterObj); if (filterAny !== "") { const anyVal = filterAny.includes("%") ? filterAny : `%${filterAny}%`; query.andWhere( new Brackets((qb) => { qb.where("Room.code ILIKE :any", { any: anyVal }).orWhere("Room.room ILIKE :any", { any: anyVal }); }), ); } const offset = (param.page - 1) * param.limit; const res_count = query; const res_list = query .leftJoinAndSelect("Room.department", "department") .leftJoinAndSelect("Room.serviceclass", "serviceclass") .orderBy("Room." + param.order_field, param.order_direction) .offset(offset) .limit(param.limit); if (param.with_deleted) { res_count.withDeleted(); 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, mapped_data); } 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 option(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Room'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['id'] = { in: 'query', type: 'string', required: false, description: 'Department Id (optional)' } */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().optional().label("Department Id"), }); let param: any = await schema.validateAsync(req.query); const query = await RoomModel.list(); query.select(["Room.id", "Room.room"]).orderBy("Room.room", "ASC"); // ✅ optional filter by departmentId if (param.id) { query.andWhere("Room.departmentId = :departmentId", { departmentId: param.id, }); } const total_count_data = await query.getCount(); const list_data = await query.getMany(); const count_data = CommonHelper.countObject(list_data); return ReturnHelper.successResponselist(res, 200, Language.lang.success_view, count_data, 1, total_count_data, list_data); } 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 = ['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 { const schema = Joi.object().keys({ room: Joi.string().max(256).required().label("Room"), code: Joi.string().max(128).required().label("Code"), description: Joi.string().allow("").max(512).optional().label("Description"), department_id: Joi.string().uuid().required().label("Department ID"), serviceclass: Joi.string().uuid().required().label("Service Class 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); let exist = await RoomModel.list({ code: param.code }).then((q) => q.getOne()); if (exist) throw { message: "Room " + Language.lang.failed_duplicate }; let department = await DepartmentModel.list({ id: param.department_id }).then((q) => q.getOne()); if (!department) throw { message: "Department not found" }; let serviceclass = await ServiceClassModel.list({ id: param.serviceclass }).then((q) => q.getOne()); if (!serviceclass) throw { message: "Class not found" }; let room = new Room(); room.room = param.room; room.code = param.code; room.description = param.description || null; room.department = department; room.serviceclass = param.serviceclass; 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); await queryRunner.commitTransaction(); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, room); } 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 detail(req: Request, res: Response, next: NextFunction): Promise { /* #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"), }); let param: any = await schema.validateAsync(req.params); let room = await RoomModel.list({ "Room.id": param.id }).then((q) => q.leftJoinAndSelect("Room.department", "department").leftJoinAndSelect("Room.serviceclass", "serviceclass").getOne()); if (!room) throw { message: "Room Not Found" }; return ReturnHelper.successResponseAny(res, 200, Language.lang.success_view, room); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_not_found, err.message); } } 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" } } } } */ 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("Room ID"), room: Joi.string().max(256).required().label("Room"), code: Joi.string().max(128).required().label("Code"), description: Joi.string().allow("").max(512).optional().label("Description"), department_id: Joi.string().uuid().required().label("Department ID"), serviceclass: Joi.string().uuid().required().label("Service Class 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); let exist = await RoomModel.list({ code: param.code }).then((q) => q.getOne()); if (exist && exist.id != param.id) throw { message: "Room " + Language.lang.failed_duplicate }; let department = await DepartmentModel.list({ id: param.department_id }).then((q) => q.getOne()); if (!department) throw { message: "Department not found" }; let serviceclass = await ServiceClassModel.list({ id: param.serviceclass }).then((q) => q.getOne()); if (!serviceclass) throw { message: "Class not found" }; let room = await RoomModel.list({ "Room.id": param.id }).then((q) => q.getOne()); if (!room) throw { message: "Room " + Language.lang.failed_not_found }; room.room = param.room; room.code = param.code; room.description = param.description || null; room.serviceclass = param.serviceclass; 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); await queryRunner.commitTransaction(); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, room); } 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 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' } */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("ID"), hard: Joi.bool().optional().allow("").label("Is hard delete?"), }); const param: { id: string; hard: boolean } = await schema.validateAsync(req.params); let affected = 0; if (!param.hard) { let room: Room = await RoomModel.list({ "Room.id": param.id }).then((q) => q.getOne()); if (!room) throw { message: "Room " + Language.lang.failed_not_found }; room.deleted_by = req.auth.data.name; room.deleted_at = moment().toDate(); await OrmHelper.DB.manager.save(room); affected = 1; } else { const repo = OrmHelper.DB.getRepository(Room); affected = (await repo.delete({ id: param.id })).affected; } if (affected > 0) { return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {}); } else { return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, ""); } } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_delete, err.message); } } 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' } */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("ID"), }); const param: any = await schema.validateAsync(req.params); const repo = OrmHelper.DB.getRepository(Room); const affected = (await repo.restore({ id: param.id })).affected; if (affected > 0) { return ReturnHelper.successResponseAny(res, 200, Language.lang.success_restore, {}); } else { return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, ""); } } catch (e: unknown) { log.error(e); const err = e as Error; 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)" }, }, required: ["file"] } } } } */ 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 = "uploads/room-picture/"; 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); // Prevent deletion of files starting with "template_" if (param.file_name.startsWith("template_")) { return ReturnHelper.errorResponse(res, 403, 403, Language.lang.failed_delete, "Cannot delete files that start with 'template_'"); } // 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); } } static async gettemplatepicture(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Room'] #swagger.security = [{ "bearerAuth": [] }] #swagger.description = 'Get all room pictures from uploads/room-picture/template directory".' */ try { const filePath = "uploads/room-picture/template/"; if (!fs.existsSync(filePath)) { return ReturnHelper.successResponseAny(res, 200, Language.lang.success_view, []); } const files = fs.readdirSync(filePath); const photoBaseUrl = "https://his.shiblysolution.id/service-master-data/uploads/room-picture/template/"; const filesWithUrl = files.map((filename, index) => ({ id: index, name: `template/${filename}`, url: photoBaseUrl + filename, })); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_view, filesWithUrl); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 500, 401, Language.lang.failed_view, err.message); } } }