diff --git a/src/app.ts b/src/app.ts index 7b3f7e0..af15c9d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,10 +9,14 @@ import error from "./middlewares/error"; import { AppDataSource } from "./database/data-source"; import { RegisterRoutes } from "./routes"; import logMiddleware from "./middlewares/logs"; +import { DateSerializer } from "./interfaces/date-serializer"; async function main() { await AppDataSource.initialize(); + // Setup custom Date serialization for local timezone + DateSerializer.setupDateSerialization(); + const app = express(); app.use( diff --git a/src/controllers/KpiCapacityController.ts b/src/controllers/KpiCapacityController.ts index 7fc7146..ab7c9ca 100644 --- a/src/controllers/KpiCapacityController.ts +++ b/src/controllers/KpiCapacityController.ts @@ -438,11 +438,11 @@ export class kpiCapacityController extends Controller { async listKpiCapacity( @Request() request: RequestWithUser, @Query("page") page: number = 1, - @Query("pageSize") pageSize: number = 10, + @Query("pageSize") pageSize?: number, @Query("type") type?: string, @Query("keyword") keyword?: string, ) { - const [kpiCapacity, total] = await AppDataSource.getRepository(KpiCapacity) + let query = await AppDataSource.getRepository(KpiCapacity) .createQueryBuilder("kpiCapacity") .leftJoinAndSelect("kpiCapacity.kpiCapacityDetails", "kpiCapacityDetail") .andWhere( @@ -452,8 +452,13 @@ export class kpiCapacityController extends Controller { ) .andWhere(type == undefined ? "1=1" : { type: type }) .orderBy("kpiCapacity.createdAt", "ASC") - .skip((page - 1) * pageSize) - .take(pageSize) + + if (pageSize) { + query = query.skip((page - 1) * pageSize) + .take(pageSize) + } + + const [kpiCapacity, total] = await query .getManyAndCount(); const mapFormula = kpiCapacity.map((item) => ({ diff --git a/src/controllers/KpiUserDevelopmentController.ts b/src/controllers/KpiUserDevelopmentController.ts index 5512e8f..897aa26 100644 --- a/src/controllers/KpiUserDevelopmentController.ts +++ b/src/controllers/KpiUserDevelopmentController.ts @@ -533,7 +533,7 @@ export class KpiUserDevelopmentController extends Controller { const organization = fullNameParts .filter((part) => part !== undefined && part !== null) - .join(" "); + .join("\n"); return { id: item.id, diff --git a/src/controllers/KpiUserEvaluationController.ts b/src/controllers/KpiUserEvaluationController.ts index b311bae..4a95262 100644 --- a/src/controllers/KpiUserEvaluationController.ts +++ b/src/controllers/KpiUserEvaluationController.ts @@ -421,7 +421,7 @@ export class KpiUserEvaluationController extends Controller { const organization = fullNameParts .filter((part) => part !== undefined && part !== null) - .join(" "); + .join("\n"); return { id: item.id, @@ -667,6 +667,14 @@ export class KpiUserEvaluationController extends Controller { @Body() requestBody: createKpiUserEvaluation, @Request() request: RequestWithUser, ) { + if(requestBody.evaluatorId == requestBody.commanderId || + requestBody.commanderId == requestBody.commanderHighId || + requestBody.evaluatorId == requestBody.commanderHighId){ + throw new HttpError( + HttpStatusCode.NOT_FOUND, + "ไม่สามารถเลือกผู้ประเมินหรือผู้บังคับบัญชาซ้ำกันได้", + ); + } // await new permission().PermissionCreate(request, "SYS_KPI_LIST"); const kpiPeriod = await this.kpiPeriodRepository.findOne({ where: { id: requestBody.kpiPeriodId }, @@ -1117,6 +1125,25 @@ export class KpiUserEvaluationController extends Controller { "ไม่พบข้อมูลรายการประเมินผลการปฏิบัติราชการระดับบุคคลนี้", ); } + if (requestBody.status.trim().toUpperCase() == "EVALUATOR") { + await new CallAPI() + .PostData(request, "/placement/noti/profiles", { + subject: `${kpiUserEvaluation.prefix}${kpiUserEvaluation.firstName} ${kpiUserEvaluation.lastName} ส่งคำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลให้พิจารณา`, + body: `${kpiUserEvaluation.prefix}${kpiUserEvaluation.firstName} ${kpiUserEvaluation.lastName} ส่งคำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลให้พิจารณา`, + receiverUserIds: [ + { + receiverUserId: kpiUserEvaluation.evaluatorId, + notiLink: `${process.env.VITE_URL_USER}/KPI-evaluator/${kpiUserEvaluation.id}`, + }, + ], + payload: "", + isSendMail: true, + isSendInbox: true, + isSendNotification: true, + }) + .then(() => {}) + .catch(() => {}); + } const before = structuredClone(kpiUserEvaluation); kpiUserEvaluation.evaluationReqEdit = requestBody.status.trim().toUpperCase(); kpiUserEvaluation.lastUpdateUserId = request.user.sub; @@ -1318,60 +1345,73 @@ export class KpiUserEvaluationController extends Controller { @Body() requestBody: { reason: string; actor: string }, @Request() request: RequestWithUser, ) { - const kpiUserEvaluation = await this.kpiUserEvalutionRepository.findOne({ - where: { id: id }, - }); - if (!kpiUserEvaluation) { + try{ + const kpiUserEvaluation = await this.kpiUserEvalutionRepository.findOne({ + where: { id: id }, + }); + if (!kpiUserEvaluation) { + throw new HttpError( + HttpStatusCode.NOT_FOUND, + "ไม่พบข้อมูลรายการประเมินผลการปฏิบัติราชการระดับบุคคลนี้", + ); + } + await new CallAPI() + .PostData(request, "/placement/noti/profiles", { + subject: `${kpiUserEvaluation.prefix}${kpiUserEvaluation.firstName} ${kpiUserEvaluation.lastName} มีความเห็นต่างเนื่องจาก: ${requestBody.reason}`, + body: `${kpiUserEvaluation.prefix}${kpiUserEvaluation.firstName} ${kpiUserEvaluation.lastName} มีความเห็นต่างเนื่องจาก: ${requestBody.reason}`, + receiverUserIds: [ + { + receiverUserId: kpiUserEvaluation.evaluatorId, + notiLink: `${process.env.VITE_URL_USER}/KPI-evaluator/${kpiUserEvaluation.id}`, + }, + ], + payload: "", + isSendMail: true, + isSendInbox: true, + isSendNotification: true, + }) + .then(() => {}) + .catch(() => {}); + const before = structuredClone(kpiUserEvaluation); + let _null: any = null; + kpiUserEvaluation.evaluationStatus = "EVALUATING_EVALUATOR"; + kpiUserEvaluation.isReasonCommander = _null; + kpiUserEvaluation.reasonCommander = _null; + kpiUserEvaluation.reasonReject = requestBody.reason; + kpiUserEvaluation.actorReject = requestBody.actor; + kpiUserEvaluation.actorNameReject = request.user.name; + kpiUserEvaluation.lastUpdateUserId = request.user.sub; + kpiUserEvaluation.lastUpdateFullName = request.user.name; + kpiUserEvaluation.lastUpdatedAt = new Date(); + let kpiReject = { + kpiUserEvaluationId: kpiUserEvaluation.id, + reason: requestBody.reason, + actor: requestBody.actor, + fullname: `${kpiUserEvaluation.prefixEvaluator}${kpiUserEvaluation.firstNameEvaluator} ${kpiUserEvaluation.lastNameEvaluator}`, + profileId: kpiUserEvaluation.evaluatorId, + createdUserId: request.user.sub, + createdFullName: request.user.name, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + await this.kpiUserEvalutionRepository.save(kpiUserEvaluation, { data: request }); + await this.kpiUserRejectResultRepository.save(kpiReject); + setLogDataDiff(request, { before, after: kpiUserEvaluation }); + return new HttpSuccess(kpiUserEvaluation.id); + } catch (error: any) { + console.error("เกิดข้อผิดพลาดระหว่างการประมวลผล:", error); + + if (error instanceof HttpError) { + throw error; + } + throw new HttpError( - HttpStatusCode.NOT_FOUND, - "ไม่พบข้อมูลรายการประเมินผลการปฏิบัติราชการระดับบุคคลนี้", + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาดภายในระบบ กรุณาลองใหม่อีกครั้ง", ); } - await new CallAPI() - .PostData(request, "/placement/noti/profiles", { - subject: `${kpiUserEvaluation.prefix}${kpiUserEvaluation.firstName} ${kpiUserEvaluation.lastName} มีความเห็นต่างเนื่องจาก: ${requestBody.reason}`, - body: `${kpiUserEvaluation.prefix}${kpiUserEvaluation.firstName} ${kpiUserEvaluation.lastName} มีความเห็นต่างเนื่องจาก: ${requestBody.reason}`, - receiverUserIds: [ - { - receiverUserId: kpiUserEvaluation.evaluatorId, - notiLink: `${process.env.VITE_URL_USER}/KPI-evaluator/${kpiUserEvaluation.id}`, - }, - ], - payload: "", - isSendMail: true, - isSendInbox: true, - isSendNotification: true, - }) - .then(() => {}) - .catch(() => {}); - const before = structuredClone(kpiUserEvaluation); - let _null: any = null; - kpiUserEvaluation.evaluationStatus = "EVALUATING_EVALUATOR"; - kpiUserEvaluation.isReasonCommander = _null; - kpiUserEvaluation.reasonCommander = _null; - kpiUserEvaluation.reasonReject = requestBody.reason; - kpiUserEvaluation.actorReject = requestBody.actor; - kpiUserEvaluation.actorNameReject = request.user.name; - kpiUserEvaluation.lastUpdateUserId = request.user.sub; - kpiUserEvaluation.lastUpdateFullName = request.user.name; - kpiUserEvaluation.lastUpdatedAt = new Date(); - let kpiReject = { - kpiUserEvaluationId: kpiUserEvaluation.id, - reason: requestBody.reason, - actor: requestBody.actor, - fullname: `${kpiUserEvaluation.prefixEvaluator}${kpiUserEvaluation.firstNameEvaluator} ${kpiUserEvaluation.lastNameEvaluator}`, - profileId: kpiUserEvaluation.evaluatorId, - createdUserId: request.user.sub, - createdFullName: request.user.name, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - await this.kpiUserEvalutionRepository.save(kpiUserEvaluation, { data: request }); - await this.kpiUserRejectResultRepository.save(kpiReject); - setLogDataDiff(request, { before, after: kpiUserEvaluation }); - return new HttpSuccess(kpiUserEvaluation.id); } /** @@ -1817,6 +1857,23 @@ export class KpiUserEvaluationController extends Controller { item.evaluationStatus = "NEW"; } else { item.evaluationReqEdit = "COMMANDER"; + await new CallAPI() + .PostData(request, "/placement/noti/profiles", { + subject: `${item.prefix}${item.firstName} ${item.lastName} ส่งคำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลให้พิจารณา`, + body: `${item.prefix}${item.firstName} ${item.lastName} ส่งคำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลให้พิจารณา`, + receiverUserIds: [ + { + receiverUserId: item.commanderId, + notiLink: `${process.env.VITE_URL_USER}/KPI-evaluator/${item.id}`, + }, + ], + payload: "", + isSendMail: true, + isSendInbox: true, + isSendNotification: true, + }) + .then(() => {}) + .catch(() => {}); } } } else if (role == "COMMANDER") { @@ -1826,11 +1883,45 @@ export class KpiUserEvaluationController extends Controller { item.evaluationStatus = "NEW"; } else { item.evaluationReqEdit = "COMMANDER_HIGH"; + await new CallAPI() + .PostData(request, "/placement/noti/profiles", { + subject: `${item.prefix}${item.firstName} ${item.lastName} ส่งคำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลให้พิจารณา`, + body: `${item.prefix}${item.firstName} ${item.lastName} ส่งคำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลให้พิจารณา`, + receiverUserIds: [ + { + receiverUserId: item.commanderHighId, + notiLink: `${process.env.VITE_URL_USER}/KPI-evaluator/${item.id}`, + }, + ], + payload: "", + isSendMail: true, + isSendInbox: true, + isSendNotification: true, + }) + .then(() => {}) + .catch(() => {}); } } } else { item.evaluationReqEdit = requestBody.status.trim().toUpperCase(); item.evaluationStatus = "NEW"; + await new CallAPI() + .PostData(request, "/placement/noti/profiles", { + subject: `คำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลได้รับการพิจารณาแล้ว`, + body: `คำขอแก้ไขข้อตกลงการประเมินผลการปฏิบัติราชการระดับบุคคลได้รับการพิจารณาแล้ว`, + receiverUserIds: [ + { + receiverUserId: item.profileId, + notiLink: `${process.env.VITE_URL_USER}/KPI-evaluator/${item.id}`, + }, + ], + payload: "", + isSendMail: true, + isSendInbox: true, + isSendNotification: true, + }) + .then(() => {}) + .catch(() => {}); } } else { item.evaluationReqEdit = requestBody.status.trim().toUpperCase(); diff --git a/src/controllers/ReportController.ts b/src/controllers/ReportController.ts index 49fa8c3..e3349f3 100644 --- a/src/controllers/ReportController.ts +++ b/src/controllers/ReportController.ts @@ -12,6 +12,7 @@ import { KpiCapacity } from "../entities/kpiCapacity"; import { KpiPlan } from "../entities/kpiPlan"; import { KpiRole } from "../entities/kpiRole"; import CallAPI from "../interfaces/call-api"; +import { throws } from "assert"; @Route("api/v1/kpi/report") @Tags("Report") @Security("bearerAuth") @@ -571,7 +572,9 @@ export class ReportController extends Controller { "AVG(kpiUserEvaluation.summaryPoint) as avgSummaryPoint" ]) .getRawMany(); - + if(!profileEvaluationNowYearIds || profileEvaluationNowYearIds.length === 0){ + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรายงานการประเมินผลฯ ระดับบุคคลของบุคคลนี้"); + } // const profileEvaluations = await this.kpiUserEvaluationRepository.find({ // relations: ["kpiPeriod"], // where: { id: In(profileEvaluationNowYearIds.map((evaluation) => evaluation.id)) }, @@ -775,6 +778,9 @@ export class ReportController extends Controller { relations: ["kpiPeriod"], where: { id: In(profileEvaluationIds.map((evaluation) => evaluation.id)) }, }); + if(!profileEvaluation || profileEvaluation.length === 0){ + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรายงานการประเมินผลฯ ระดับบุคคลของบุคคลนี้"); + } const combinedData: KPIData = profileEvaluation.reduce( (acc: KPIData, x) => { @@ -1155,6 +1161,9 @@ export class ReportController extends Controller { relations: ["kpiPeriod"], where: { id: In(profileEvaluationIds.map((evaluation) => evaluation.id)) }, }); + if(!profileEvaluation || profileEvaluation.length === 0){ + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรายงานการประเมินผลฯ ระดับบุคคลของบุคคลนี้"); + } const combinedData: KPIData = profileEvaluation.reduce( (acc: KPIData, x) => { @@ -1493,6 +1502,9 @@ export class ReportController extends Controller { .groupBy("kpiUserEvaluation.kpiPeriodId") .select("MIN(kpiUserEvaluation.id) as id") .getRawMany(); + if(!profileEvaluationIds || profileEvaluationIds.length === 0){ + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรายงานการประเมินผลฯ ระดับบุคคลของบุคคลนี้"); + } if (profileEvaluationIds.length > 0) { userInfo = await this.kpiUserEvaluationRepository.find({ where: { @@ -1525,7 +1537,6 @@ export class ReportController extends Controller { const dev20text = "การเรียนรู้จากผู้อื่น (Coach/Mentor/Consulting)"; const dev70text = "การลงมือปฏิบัติ (โดยผู้บังคับบัญชามอบหมาย)"; const combianText = [dev10text, dev20text, dev70text]; - formattedUserDevelopmentLists = userDevelopmentLists.map( (development: any, index: number) => ({ no: Extension.ToThaiNumber((index + 1).toString()), @@ -1671,7 +1682,7 @@ export class ReportController extends Controller { const date = new Date(); formattedData = { root: data && data.rootName != null ? data.rootName : "-", - period: data?.durationKPI == "APR" ? "๑" : data?.durationKPI == "OCT" ? "๒" : "-", + period: data?.durationKPI == "APR" ? "๑ เมษายน" : data?.durationKPI == "OCT" ? "๑ ตุลาคม" : "-", year: data.year ? Extension.ToThaiNumber((data.year + 543).toString()) : "-", date: Extension.ToThaiNumber(Extension.ToThaiFullDate2(date).toString()), userEvaluations: diff --git a/src/interfaces/date-serializer.ts b/src/interfaces/date-serializer.ts new file mode 100644 index 0000000..5c876ff --- /dev/null +++ b/src/interfaces/date-serializer.ts @@ -0,0 +1,24 @@ +// Custom Date serializer for local timezone +export class DateSerializer { + static toLocalTime(date: Date): string | null { + if (!date) return null; + + // Convert UTC date to Thailand timezone (+07:00) + const offset = 7 * 60; // Thailand is UTC+7 + const localTime = new Date(date.getTime() + offset * 60 * 1000); + + // Format as ISO string but replace Z with +07:00 + const isoString = localTime.toISOString(); + return isoString.replace("Z", "+07:00"); + } + + static setupDateSerialization() { + // Override Date.prototype.toJSON to use local time + Date.prototype.toJSON = function () { + const offset = 7 * 60; // Thailand timezone offset in minutes + const localTime = new Date(this.getTime() + offset * 60 * 1000); + const isoString = localTime.toISOString(); + return isoString.replace("Z", "+07:00"); + }; + } +} diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index 6ff8977..cfe76e6 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -184,39 +184,61 @@ class CheckAuth { }); } public async checkOrg(token: any, keycloakId: string) { - const redisClient = await this.redis.createClient({ - host: process.env.REDIS_HOST, - port: process.env.REDIS_PORT, - }) - const getAsync = promisify(redisClient.get).bind(redisClient) - try { - let reply = await getAsync("org_" + keycloakId) - if (reply != null) { - reply = JSON.parse(reply) - } else { - if (!keycloakId) throw new Error("No KeycloakId provided") - const x = await new CallAPI().GetData( - { - headers: { authorization: token }, - }, - `/org/permission/checkOrg/${keycloakId}`, - false - ) + try { + // Validate required environment variables + const REDIS_HOST = process.env.REDIS_HOST; + const REDIS_PORT = process.env.REDIS_PORT ? Number(process.env.REDIS_PORT) : 6379; - const data = { - orgRootId: x.orgRootId, - orgChild1Id: x.orgChild1Id, - orgChild2Id: x.orgChild2Id, - orgChild3Id: x.orgChild3Id, - orgChild4Id: x.orgChild4Id, - } + if (!REDIS_HOST) { + throw new Error("REDIS_HOST is not set in environment variables"); + } - return data - } - } catch (error) { - console.error("Error calling API:", error) - throw error - } + console.log(`[REDIS] Connecting to Redis at ${REDIS_HOST}:${REDIS_PORT}`); + + // Create Redis client + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: REDIS_PORT, + }, + }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); + console.log("[REDIS] Connected successfully!"); + + const getAsync = promisify(redisClient.get).bind(redisClient); + + let reply = await getAsync("org_" + keycloakId); + if (reply != null) { + reply = JSON.parse(reply); + } else { + if (!keycloakId) throw new Error("No KeycloakId provided"); + const x = await new CallAPI().GetData( + { + headers: { authorization: token }, + }, + `/org/permission/checkOrg/${keycloakId}`, + false, + ); + + const data = { + orgRootId: x.orgRootId, + orgChild1Id: x.orgChild1Id, + orgChild2Id: x.orgChild2Id, + orgChild3Id: x.orgChild3Id, + orgChild4Id: x.orgChild4Id, + }; + + return data; + } + } catch (error) { + console.error("Error calling API:", error); + throw error; + } } public async PermissionCreate(req: RequestWithUser, system: string) { return await this.Permission(req, system, "CREATE");