Merge branch 'develop' into dev

* develop:
  update
  #29
  test throw report api
  add \n
  add \n orgName
  fix word
  add noti and test
  fix word
  #28
  revert code to normal
  revert checkOrg
  test safeStringify
  revert
  test
  try catch
  fix
  update timezone
This commit is contained in:
Warunee Tamkoo 2025-11-04 09:06:54 +07:00
commit 9a282ab2be
7 changed files with 248 additions and 91 deletions

View file

@ -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(

View file

@ -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) => ({

View file

@ -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,

View file

@ -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();

View file

@ -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:

View file

@ -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");
};
}
}

View file

@ -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");