add pharmacy info

This commit is contained in:
Rayhan Ardiansyah
2026-03-02 12:31:03 +07:00
parent 57083ad9fe
commit 8df10aa2e3
5 changed files with 682 additions and 2 deletions

View File

@ -0,0 +1,631 @@
import { Response, NextFunction, Application } from "express";
import { Request } from "express-jwt";
import { ReturnHelper } from "../../helpers/express/return";
import { OrmHelper } from "../../helpers/orm";
import Joi from "joi";
import { ILogObj, Logger } from "tslog";
import { Language } from "../../langs/lang";
import { PharmacyInfo, Paging } from "entity";
import CommonHelper from "../../helpers/common";
import * as fastcsv from 'fast-csv';
import dayjs from "dayjs";
import fileUpload from "express-fileupload";
import fs from "fs";
import path from "path";
import { v4 as uuidv4 } from 'uuid';
import { PharmacyInfoModel } from "../../model/pharmacy_info";
interface FileUploadRequest extends Request {
files?: fileUpload.FileArray | null;
body: any;
}
const log: Logger<ILogObj> = new Logger({ name: '[PharmacyInfoController]', type: 'pretty' });
export class PharmacyInfoController {
static async list(req: Request, res: Response, next: NextFunction): Promise<void | Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{ "bearerAuth": [] }]
#swagger.parameters['filter'] = { description: '', 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) : {};
var query = await PharmacyInfoModel.list(filter);
const offset = (param.page - 1) * param.limit;
let limit = param.limit;
const res_count = query;
const res_list = query
.orderBy("pharmacy_info." + param.order_field, param.order_direction)
.offset(offset)
.limit(limit);
if (param.with_deleted) {
res_count.withDeleted();
res_list.withDeleted();
}
const current_page = param.page;
const total_count_data = await res_count.getCount();
const list_data = await res_list.getMany();
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);
} 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 export(req: Request, res: Response, next: NextFunction): Promise<void | Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['filter'] = {
description: 'Filter with 2 format : <ul><li>Simple format use text plaint will filter eq:code or like %name%</li><li>Advance format using field existing {status:\'Y\', any:\'SAME AS PLAINT LOGIC\', etc...}</li></ul>',
in: 'query',
type: 'string'
}
#swagger.parameters['filter'] = {
in: 'query',
type: 'string'
}
#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'),
order_field: Joi.string().required().label('Order Field'),
order_direction: Joi.string().allow('asc', 'desc').required().label('Order Direction'),
});
const param: Paging = await schema.validateAsync(req.query);
let filter = param.filter && param.filter !== "" ? JSON.parse(param.filter) : {};
var query = await PharmacyInfoModel.list(filter);
const data = await query.getMany();
const filename = "pharmacy_info.csv";
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=' + filename);
const csvStream = fastcsv.format({
headers: true,
writeHeaders: true,
transform: (row: PharmacyInfo): any => ({
...row,
created_at: dayjs(row.created_at).format('DD-MM-YYYY HH:MM:ss'),
})
});
csvStream.pipe(res);
const limit = 50;
const fetchAndWrite = async (page: any) => {
if (CommonHelper.countObject(data) === 0) {
csvStream.end();
} else {
data.forEach((item: any) => csvStream.write(item));
if (CommonHelper.countObject(data) == limit) {
fetchAndWrite(page + 1);
} else {
csvStream.end();
}
}
};
fetchAndWrite(1);
} 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<Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.requestBody = {
required: true,
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/pharmacy_info"
}
}
}
}
*/
try {
const schema = Joi.object().keys({
logo: Joi.string()
.max(256)
.required()
.label('Logo'),
name: Joi.string()
.max(256)
.required()
.label('Name'),
address: Joi.string()
.max(500)
.required()
.label('Address'),
munisipiu: Joi.string()
.uuid()
.required()
.label('Munisipiu'),
postu_admin: Joi.string()
.uuid()
.required()
.label('Postu Admin'),
suco: Joi.string()
.uuid()
.required()
.label('Suco'),
aldeia: Joi.string()
.uuid()
.required()
.label('Aldeia'),
phone: Joi.string()
.max(30)
.optional()
.label('Phone'),
default_room_id: Joi.string()
.uuid()
.required()
.label('Default Room'),
});
const param: any = await schema.validateAsync(req.body);
const data = new PharmacyInfo()
data.name = param.name,
data.address = param.address
data.munisipiu = param.munisipiu,
data.postu_admin = param.postu_admin
data.suco = param.suco
data.aldeia = param.aldeia
data.phone = param.phone
data.default_room = param.default_room_id
data.created_by = req.auth.data.name;
await OrmHelper.DB.manager.save(data);
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, data);
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 401, Language.lang.failed_insert, err.message);
}
}
static async update(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['id'] = {
in: 'path',
description: 'PharmacyInfo ID.',
required: true,
type: 'string'
}
#swagger.requestBody = {
required: true,
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/pharmacy_info"
}
}
}
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID'),
logo: Joi.string()
.max(256)
.required()
.label('Logo'),
name: Joi.string()
.max(256)
.required()
.label('Name'),
address: Joi.string()
.max(500)
.required()
.label('Address'),
munisipiu: Joi.string()
.uuid()
.required()
.label('Munisipiu'),
postu_admin: Joi.string()
.uuid()
.required()
.label('Postu Admin'),
suco: Joi.string()
.uuid()
.required()
.label('Suco'),
aldeia: Joi.string()
.uuid()
.required()
.label('Aldeia'),
phone: Joi.string()
.max(30)
.optional()
.label('Phone'),
default_room_id: Joi.string()
.uuid()
.required()
.label('Default Room'),
});
req.body.id = req.params['id'];
const param: any = await schema.validateAsync(req.body);
const repo = OrmHelper.DB.getRepository(PharmacyInfo);
const data = await repo.findOneBy({ id: param.id });
if (data != null) {
data.name = param.name,
data.address = param.address
data.munisipiu = param.munisipiu,
data.postu_admin = param.postu_admin
data.suco = param.suco
data.aldeia = param.aldeia
data.phone = param.phone
data.default_room = param.default_room_id
data.updated_by = req.auth.data.name;
await repo.save(data);
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, data);
} 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_update, err.message);
}
}
static async delete(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['id'] = {
in: 'path',
description: 'Pharmacy Info ID.',
required: true,
type: 'string'
}
#swagger.parameters['hard'] = {
in: 'path',
description: 'Is Hard Delete',
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);
const repo = OrmHelper.DB.getRepository(PharmacyInfo);
const existData = await repo.findOne({ where: { id: param.id } });
if (existData && !param.hard) {
existData.deleted_by = req.auth.data.name;
await OrmHelper.DB.manager.save(existData);
}
const affected = (!param.hard ? await repo.softDelete({ id: param.id }) : 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<Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['id'] = {
in: 'path',
description: 'PharmacyInfo ID.',
required: true,
type: 'string'
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID')
});
const param: PharmacyInfo = await schema.validateAsync(req.params);
const repo = OrmHelper.DB.getRepository(PharmacyInfo);
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 uploadlogo(req: FileUploadRequest, res: Response, next: NextFunction): Promise<Response> {
/* #swagger.tags = ['Pharmacy - PharmacyInfo']
#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/pharmacy-logo/';
// 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 deletelogo(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#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/pharmacy-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);
}
}
static async view(req: Request, res: Response, next: NextFunction): Promise<void | Response> {
/*
#swagger.tags = ['Pharmacy - PharmacyInfo']
#swagger.security = [{ "bearerAuth": [] }]
*/
try {
const data = await OrmHelper.DB
.getRepository(PharmacyInfo)
.findOne({
where: {}
});
if (!data) return ReturnHelper.errorResponse(res, 404, 404, Language.lang.failed_view, "data not found");
const result = {
id: data.id,
name: data.name,
address: data.address,
phone: data.phone,
logo: 'http://his.shiblysolution.id:3011/service-master-data/uploads/hospital-logo/' + data.logo
};
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_view, result);
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res,400,401,Language.lang.failed_view,err.message);
}
}
}