From e7a973b764013458f548e75cdbe670af4c8d796e Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 16 Apr 2026 15:59:36 +0700 Subject: [PATCH 01/18] fix issues #2428 #2383 --- src/controllers/ExRetirementController.ts | 4 +++- src/controllers/ProfileSalaryTempController.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index 128cb4d1..c8ffe5da 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -15,6 +15,7 @@ import { import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { addLogSequence } from "../interfaces/utils"; +import HttpSuccess from "../interfaces/http-success"; interface CachedToken { token: string; @@ -88,7 +89,8 @@ export class ExRetirementController extends Controller { }, }); - return res.data; + // return res.data; + return new HttpSuccess(res.data.data); } catch (error: any) { if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index fc6a9df5..49c757dc 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1433,10 +1433,10 @@ export class ProfileSalaryTempController extends Controller { profileEmployeeId: x.profileEmployeeId, dateStart: x.commandDateAffect, dateEnd: null, - posNo: `${x.posNoAbb} ${x.posNo}`, + posNo: `${x.posNoAbb ?? ""} ${x.posNo ?? ""}`.trim(), position: x.positionName, commandId: x.commandId, - refCommandNo: `${x.commandNo}/${x.commandYear}`, + refCommandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined, refCommandDate: x.commandDateAffect, status: false, isDeleted: false, @@ -1456,7 +1456,7 @@ export class ProfileSalaryTempController extends Controller { dateStart: x.commandDateAffect, dateEnd: null, commandId: x.commandId, - commandNo: `${x.commandNo}/${x.commandYear}`, + commandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined, commandName: x.commandName ?? "ให้ช่วยราชการ", refCommandDate: x.commandDateSign, refId: x.refId, From 99bd789702449ec249ddafc78582c1ee2533dda2 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 17 Apr 2026 14:00:00 +0700 Subject: [PATCH 02/18] =?UTF-8?q?fixed#230=20noti=20=E0=B9=80=E0=B8=9E?= =?UTF-8?q?=E0=B8=B4=E0=B9=88=E0=B8=A1=E0=B8=A5=E0=B8=B4=E0=B9=89=E0=B8=87?= =?UTF-8?q?=E0=B8=84=E0=B9=8C=E0=B9=84=E0=B8=9B=E0=B8=AB=E0=B8=99=E0=B9=89?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=A5=E0=B8=B0=E0=B9=80?= =?UTF-8?q?=E0=B8=AD=E0=B8=B5=E0=B8=A2=E0=B8=94=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B9=84=E0=B8=82=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9?= =?UTF-8?q?=E0=B8=A5=20"=E0=B8=82=E0=B8=AD=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B9=84=E0=B8=82=E0=B8=97=E0=B8=B0=E0=B9=80=E0=B8=9A=E0=B8=B5?= =?UTF-8?q?=E0=B8=A2=E0=B8=99=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=A7=E0=B8=B1?= =?UTF-8?q?=E0=B8=95=E0=B8=B4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/WorkflowController.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index 0609c932..bc1d37c4 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -237,11 +237,18 @@ export class WorkflowController extends Controller { savedStates.find((state) => state.id === so.stateId && state.order === 1), ); + // add link sysName = REGISTRY_PROFILE or REGISTRY_PROFILE_EMP + let notiLink = ''; + if (body.sysName === 'REGISTRY_PROFILE') { + notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`; + } else if (body.sysName === 'REGISTRY_PROFILE_EMP') { + notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`; + } const notificationReceivers = stateOperatorUsersToCreate .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) .map((user) => ({ receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, - notiLink: "", + notiLink: notiLink, })); // ส่ง notification แบบ fire-and-forget From 7f3408e2f5e6f882d22ad2e73bb3eaaf7021a19e Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 17 Apr 2026 14:18:54 +0700 Subject: [PATCH 03/18] API permission with acting positions --- src/controllers/PermissionController.ts | 59 ++++++++ src/services/ActingPositionService.ts | 186 ++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 src/services/ActingPositionService.ts diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 801d4b97..ed8fc343 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -15,6 +15,7 @@ import permission from "../interfaces/permission"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { OrgRevision } from "../entities/OrgRevision"; +import { actingPositionService } from "../services/ActingPositionService"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -254,6 +255,64 @@ export class PermissionController extends Controller { return new HttpSuccess(res); } + /** + * API permission with acting positions + * @summary permission with acting positions (dotnet api) + * @param {string} action action + * @param {string} system authSysId + */ + @Get("dotnet-acting/{action}/{system}") + public async dotnetActing( + @Request() req: RequestWithUser, + @Path() action: string, + @Path() system: string, + ) { + if (!["CREATE", "DELETE", "GET", "LIST", "UPDATE"].includes(action)) { + throw new HttpError(HttpStatus.NOT_FOUND, "Action ไม่ถูกต้อง"); + } + // ดึง privilege ตามปกติ + let privilege = await new permission().Permission(req, system.toLocaleUpperCase(), action); + + // ดึงข้อมูล profile และ orgRevision + let profile: any = await this.profileRepo.findOne({ + select: ["id"], + where: { keycloak: req.user.sub }, + }); + + if (!profile) { + profile = await this.profileEmployeeRepo.findOne({ + select: ["id"], + where: { keycloak: req.user.sub }, + }); + if (!profile) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); + } + } + + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); + + // ดึงข้อมูลตำแหน่งที่รักษาการ + const actingData = await actingPositionService.getActingPositionsWithPrivilege( + profile.id, + orgRevision?.id, + action, + system.toLocaleUpperCase() + ); + + // ส่งค่ากลับเหมือน dotnet endpoint แต่เพิ่ม isAct และ posMasterActs + return new HttpSuccess({ + privilege, + isAct: actingData.isAct, + posMasterActs: actingData.posMasterActs, + }); + } + /** * API permission (dotnet api) * @summary permission (dotnet api) diff --git a/src/services/ActingPositionService.ts b/src/services/ActingPositionService.ts new file mode 100644 index 00000000..e5a0b601 --- /dev/null +++ b/src/services/ActingPositionService.ts @@ -0,0 +1,186 @@ +import { AppDataSource } from "../database/data-source"; +import { AuthRoleAttr } from "../entities/AuthRoleAttr"; +import { PosMasterAct } from "../entities/PosMasterAct"; + +export interface ActingPositionData { + isAct: boolean; + posMasterActs: Array<{ + privilege: string | null; + posNo: string | null; + rootDnaId: string | null; + child1DnaId: string | null; + child2DnaId: string | null; + child3DnaId: string | null; + child4DnaId: string | null; + }>; +} + +export interface ActingPositionWithPrivilegeData extends ActingPositionData { + privilege?: string | null; +} + +/** + * Service สำหรับจัดการข้อมูลตำแหน่งที่รักษาการและ privilege + */ +export class ActingPositionService { + private posMasterActRepo = AppDataSource.getRepository(PosMasterAct); + private authRoleAttrRepo = AppDataSource.getRepository(AuthRoleAttr); + + /** + * ดึงข้อมูลตำแหน่งที่รักษาการและ privilege + * + * @param profileId - ID ของ profile ที่ต้องการตรวจสอบ + * @param orgRevisionId - ID ของ orgRevision ปัจจุบัน + * @param action - Action ที่ต้องการตรวจสอบสิทธิ์ (CREATE, DELETE, GET, LIST, UPDATE) + * @param system - System ID ที่ต้องการตรวจสอบสิทธิ์ (authSysId) + * @returns ข้อมูลตำแหน่งที่รักษาการและ privilege + */ + async getActingPositionsWithPrivilege( + profileId: string, + orgRevisionId: string | undefined, + action?: string, + system?: string + ): Promise { + // ดึงข้อมูล posMasterAct โดย join กับ posMaster (ตำแหน่งที่ถูกรักษาการ) + const posMasterActs = await this.posMasterActRepo + .createQueryBuilder("posMasterAct") + .leftJoinAndSelect("posMasterAct.posMaster", "posMaster") + .addSelect([ + "posMaster.authRoleId", // เพิ่มการดึง authRoleId จากตำแหน่งที่ถูกรักษาการ + "posMaster.posMasterNo", // เพิ่มการดึงเลขที่ตำแหน่ง + "posMaster.posMasterNoPrefix", // เพิ่มการดึง prefix ของเลขที่ตำแหน่ง + "posMaster.posMasterNoSuffix" // เพิ่มการดึง suffix ของเลขที่ตำแหน่ง + ]) + .leftJoinAndSelect("posMaster.orgRoot", "orgRoot") + .leftJoinAndSelect("posMaster.orgChild1", "orgChild1") + .leftJoinAndSelect("posMaster.orgChild2", "orgChild2") + .leftJoinAndSelect("posMaster.orgChild3", "orgChild3") + .leftJoinAndSelect("posMaster.orgChild4", "orgChild4") + .leftJoinAndSelect("posMaster.orgRevision", "orgRevision") + .leftJoinAndSelect("posMasterAct.posMasterChild", "posMasterChild") + .leftJoinAndSelect("posMasterChild.current_holder", "profileChild") + .where("profileChild.id = :profileId", { profileId }) + .andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("orgRevision.orgRevisionIsCurrent = true") + .andWhere("orgRevision.orgRevisionIsDraft = false") + .getMany(); + + if (posMasterActs.length === 0) { + return { + isAct: false, + posMasterActs: [], + }; + } + + // วนลูปแต่ละ posMasterAct เพื่อดึง privilege ของตำแหน่งที่รักษาการ + const posMasterActsResponse = await Promise.all( + posMasterActs.map(async (act) => { + let privilege: string | null = null; + let privileges: Record = {}; + + if (act.posMaster?.authRoleId) { + // ถ้าระบุ action และ system มา ให้ดึงเฉพาะ privilege ของระบบนั้นๆ + if (action && system) { + const roleAttr = await this.authRoleAttrRepo + .createQueryBuilder("authRoleAttr") + .select(["authRoleAttr.attrPrivilege", "authRoleAttr.attrIsCreate", "authRoleAttr.attrIsDelete", "authRoleAttr.attrIsGet", "authRoleAttr.attrIsList", "authRoleAttr.attrIsUpdate"]) + .where("authRoleAttr.authRoleId = :authRoleId", { + authRoleId: act.posMaster.authRoleId, + }) + .andWhere("authRoleAttr.authSysId = :system", { system }) + .getOne(); + + if (roleAttr) { + // ตรวจสอบสิทธิ์ตาม action + let hasPermission = false; + const actionUpper = action.trim().toUpperCase(); + + switch (actionUpper) { + case "CREATE": + hasPermission = roleAttr.attrIsCreate; + break; + case "DELETE": + hasPermission = roleAttr.attrIsDelete; + break; + case "GET": + hasPermission = roleAttr.attrIsGet; + break; + case "LIST": + hasPermission = roleAttr.attrIsList; + break; + case "UPDATE": + hasPermission = roleAttr.attrIsUpdate; + break; + } + + if (hasPermission) { + privilege = roleAttr.attrPrivilege; + } + } + } else { + // ดึงข้อมูล AuthRoleAttr สำหรับทุกระบบ + const roleAttrs = await this.authRoleAttrRepo + .createQueryBuilder("authRoleAttr") + .select(["authRoleAttr.authSysId", "authRoleAttr.attrPrivilege"]) + .where("authRoleAttr.authRoleId = :authRoleId", { + authRoleId: act.posMaster.authRoleId, + }) + .getMany(); + + privileges = roleAttrs.reduce((acc, attr) => { + acc[attr.authSysId] = attr.attrPrivilege; + return acc; + }, {} as Record); + } + } + + // จัดรูปแบบเลขที่ตำแหน่งตามรูปแบบ shortName ที่ใช้ในระบบ + const holder = act.posMaster; + const posNo = !holder + ? null + : holder.orgChild4 != null + ? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}` + : holder.orgChild3 != null + ? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}` + : holder.orgChild2 != null + ? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}` + : holder.orgChild1 != null + ? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}` + : holder.orgRoot != null + ? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}` + : null; + + return { + posNo: posNo, + privilege: action && system ? privilege : JSON.stringify(privileges), + rootDnaId: act.posMaster?.orgRoot?.ancestorDNA ?? null, + child1DnaId: act.posMaster?.orgChild1?.ancestorDNA ?? null, + child2DnaId: act.posMaster?.orgChild2?.ancestorDNA ?? null, + child3DnaId: act.posMaster?.orgChild3?.ancestorDNA ?? null, + child4DnaId: act.posMaster?.orgChild4?.ancestorDNA ?? null, + }; + }) + ); + + // ถ้าระบุ action และ system มา ให้ดึง privilege ของตำแหน่งแรก + let specificPrivilege: string | null = null; + if (action && system && posMasterActsResponse.length > 0) { + specificPrivilege = posMasterActsResponse[0].privilege; + } + + const response: ActingPositionWithPrivilegeData = { + isAct: true, + posMasterActs: posMasterActsResponse, + }; + + // ถ้าระบุ action และ system มา ให้เพิ่ม privilege เข้าไปใน response ด้วย + if (action && system) { + response.privilege = specificPrivilege ?? null; + } + + return response; + } +} + +// Export singleton instance +export const actingPositionService = new ActingPositionService(); From 28b5408d5be725f45fa1faae25c3cd9fcdc11ab8 Mon Sep 17 00:00:00 2001 From: adisak Date: Mon, 20 Apr 2026 08:05:16 +0700 Subject: [PATCH 04/18] #2427 and migration --- src/controllers/CommandController.ts | 23 +++++++-- src/controllers/PositionController.ts | 10 +++- src/entities/Profile.ts | 48 +++++++++++++++++++ ...08026834-add_position_fields_to_profile.ts | 37 ++++++++++++++ src/services/rabbitmq.ts | 7 +++ src/utils/org-formatting.ts | 39 +++++++++++++++ 6 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 src/migration/1776308026834-add_position_fields_to_profile.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index da0ed3fe..e505aa3a 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -48,6 +48,7 @@ import { import { Position } from "../entities/Position"; import { PosMaster } from "../entities/PosMaster"; import { EmployeePosition } from "../entities/EmployeePosition"; +import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { ProfileDiscipline } from "../entities/ProfileDiscipline"; import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory"; @@ -3660,6 +3661,7 @@ export class CommandController extends Controller { const posMaster = await this.posMasterRepository.findOne({ where: { id: item.posmasterId }, + relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], }); if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -3715,6 +3717,7 @@ export class CommandController extends Controller { id: item.positionId, posMasterId: item.posmasterId, }, + relations: ["posExecutive"], }); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { @@ -3723,6 +3726,12 @@ export class CommandController extends Controller { profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); } profile.amount = item.amount ?? null; profile.amountSpecial = item.amountSpecial ?? null; @@ -6876,7 +6885,7 @@ export class CommandController extends Controller { where: { id: item.bodyPosition.posmasterId, }, - relations: { orgRevision: true } + relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true } }); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ @@ -6893,9 +6902,8 @@ export class CommandController extends Controller { orgRevisionIsDraft: false } }, - relations: { orgRevision: true } - }); - } + relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true } + }); } if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -6983,6 +6991,7 @@ export class CommandController extends Controller { id: item.bodyPosition.positionId, posMasterId: posMaster.id, }, + relations: ["posExecutive"], }); } @@ -6993,6 +7002,12 @@ export class CommandController extends Controller { profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); // profile.dateStart = new Date(); await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 75d6c2a0..23806e6c 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -39,6 +39,7 @@ import { AuthRole } from "../entities/AuthRole"; import { RequestWithUser } from "../middlewares/user"; import permission from "../interfaces/permission"; import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils"; +import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import { PosMasterAssign } from "../entities/PosMasterAssign"; import { Assign } from "../entities/Assign"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @@ -3793,7 +3794,7 @@ export class PositionController extends Controller { await new permission().PermissionUpdate(request, "SYS_ORG"); const dataMaster = await this.posMasterRepository.findOne({ where: { id: requestBody.posMaster }, - relations: ["positions"], + relations: ["positions", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], }); if (!dataMaster) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -3825,6 +3826,7 @@ export class PositionController extends Controller { if (_profile) { let _position = await this.positionRepository.findOne({ where: { id: requestBody.position, posMasterId: requestBody.posMaster }, + relations: ["posExecutive"], }); if (_position) { // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ @@ -3832,6 +3834,12 @@ export class PositionController extends Controller { _profile.position = _position.positionName; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; + _profile.positionField = _position.positionField ?? undefined; + _profile.posExecutive = _position.posExecutive?.posExecutiveName ?? undefined; + _profile.positionArea = _position.positionArea ?? undefined; + _profile.positionExecutiveField = _position.positionExecutiveField ?? undefined; + _profile.posMasterNo = getPosMasterNo(dataMaster); + _profile.org = getOrgFullName(dataMaster); await this.profileRepository.save(_profile); setLogDataDiff(request, { before, after: _profile }); } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index 72a1d505..a875a969 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -140,6 +140,54 @@ export class Profile extends EntityBase { }) posTypeId: string | null; + @Column({ + nullable: true, + comment: "สายงาน", + length: 45, + default: null, + }) + positionField: string; + + @Column({ + nullable: true, + comment: "ตำแหน่งทางการบริหาร", + length: 255, + default: null, + }) + posExecutive?: string; + + @Column({ + nullable: true, + comment: "ด้าน/สาขา", + length: 255, + default: null, + }) + positionArea?: string; + + @Column({ + nullable: true, + comment: "ด้านทางการบริหาร", + length: 255, + default: null, + }) + positionExecutiveField?: string; + + @Column({ + nullable: true, + comment: "เลขที่ตำแหน่ง", + length: 255, + default: null, + }) + posMasterNo?: string; + + @Column({ + nullable: true, + comment: "สังกัด", + type: "text", + default: null, + }) + org?: string; + @Column({ nullable: true, length: 255, diff --git a/src/migration/1776308026834-add_position_fields_to_profile.ts b/src/migration/1776308026834-add_position_fields_to_profile.ts new file mode 100644 index 00000000..9b214460 --- /dev/null +++ b/src/migration/1776308026834-add_position_fields_to_profile.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddPositionFieldsToProfile1776308026834 implements MigrationInterface { + name = 'AddPositionFieldsToProfile1776308026834' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionField\` varchar(45) NULL COMMENT 'สายงาน'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`posExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`posMasterNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`org\` text NULL COMMENT 'สังกัด'`); + + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionField\` varchar(45) NULL COMMENT 'สายงาน'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`posExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`posMasterNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`org\` text NULL COMMENT 'สังกัด'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`org\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`posMasterNo\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionExecutiveField\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionArea\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`posExecutive\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionField\``); + + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`org\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`posMasterNo\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionExecutiveField\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionArea\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`posExecutive\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionField\``); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index a8011900..5341d03c 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -3,6 +3,7 @@ import { AppDataSource } from "../database/data-source"; import { Command } from "../entities/Command"; import { chunkArray, commandTypePath } from "../interfaces/utils"; import CallAPI from "../interfaces/call-api"; +import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { PosMaster } from "../entities/PosMaster"; @@ -668,6 +669,12 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { profile.posLevelId = position?.posLevelId ?? _null; profile.posTypeId = position?.posTypeId ?? _null; profile.position = position?.positionName ?? _null; + profile.positionField = position?.positionField ?? _null; + profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null; + profile.positionArea = position?.positionArea ?? _null; + profile.positionExecutiveField = position?.positionExecutiveField ?? _null; + profile.posMasterNo = getPosMasterNo(item) ?? _null; + profile.org = getOrgFullName(item) ?? _null; await repoProfile.save(profile); } } diff --git a/src/utils/org-formatting.ts b/src/utils/org-formatting.ts index 701fb478..8b460a2f 100644 --- a/src/utils/org-formatting.ts +++ b/src/utils/org-formatting.ts @@ -68,3 +68,42 @@ export function filterPosMasters( ): PosMaster[] { return posMasters.filter((x) => x[childLevelIdKey] == null && x.isDirector === true); } + +/** + * สร้าง orgShortName จาก posMaster (ต้อง load org relations มาก่อน) + */ +export function getOrgShortName(posMaster: PosMaster): string { + if (posMaster.orgChild1Id === null) { + return posMaster.orgRoot?.orgRootShortName ?? ""; + } else if (posMaster.orgChild2Id === null) { + return posMaster.orgChild1?.orgChild1ShortName ?? ""; + } else if (posMaster.orgChild3Id === null) { + return posMaster.orgChild2?.orgChild2ShortName ?? ""; + } else if (posMaster.orgChild4Id === null) { + return posMaster.orgChild3?.orgChild3ShortName ?? ""; + } else { + return posMaster.orgChild4?.orgChild4ShortName ?? ""; + } +} + +/** + * สร้างชื่อสังกัดเต็ม จาก posMaster (join ด้วย \n) + */ +export function getOrgFullName(posMaster: PosMaster): string { + const parts = [ + posMaster.orgChild4?.orgChild4Name, + posMaster.orgChild3?.orgChild3Name, + posMaster.orgChild2?.orgChild2Name, + posMaster.orgChild1?.orgChild1Name, + posMaster.orgRoot?.orgRootName, + ]; + return parts.filter((part) => part !== undefined && part !== null).join("\n"); +} + +/** + * สร้างเลขที่ตำแหน่ง เช่น "กทม. 1234" + */ +export function getPosMasterNo(posMaster: PosMaster): string { + const orgShortName = getOrgShortName(posMaster); + return `${orgShortName} ${posMaster.posMasterNo}`; +} From f1c8ecf6993269d008fd537b11589f916e2f562d Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 20 Apr 2026 16:01:38 +0700 Subject: [PATCH 05/18] insert position to profile --- src/controllers/CommandController.ts | 14 ++++++---- src/controllers/PositionController.ts | 9 +++--- src/services/rabbitmq.ts | 40 +++++++++++++++------------ 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index e505aa3a..e97abce5 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -3722,6 +3722,9 @@ export class CommandController extends Controller { // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); if(!posMaster.isSit){ profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; @@ -3730,8 +3733,6 @@ export class CommandController extends Controller { profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; profile.positionArea = positionNew.positionArea ?? null; profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); } profile.amount = item.amount ?? null; profile.amountSpecial = item.amountSpecial ?? null; @@ -6998,6 +6999,9 @@ export class CommandController extends Controller { // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); if(!posMaster.isSit){ profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; @@ -7006,12 +7010,10 @@ export class CommandController extends Controller { profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; profile.positionArea = positionNew.positionArea ?? null; profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); // profile.dateStart = new Date(); - await this.profileRepository.save(profile, { data: req }); - setLogDataDiff(req, { before, after: profile }); } + await this.profileRepository.save(profile, { data: req }); + setLogDataDiff(req, { before, after: profile }); await this.positionRepository.save(positionNew, { data: req }); } // await CreatePosMasterHistoryOfficer(posMaster.id, req); diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 23806e6c..a1378892 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -3829,6 +3829,9 @@ export class PositionController extends Controller { relations: ["posExecutive"], }); if (_position) { + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + _profile.posMasterNo = getPosMasterNo(dataMaster); + _profile.org = getOrgFullName(dataMaster); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if(!dataMaster.isSit){ _profile.position = _position.positionName; @@ -3838,11 +3841,9 @@ export class PositionController extends Controller { _profile.posExecutive = _position.posExecutive?.posExecutiveName ?? undefined; _profile.positionArea = _position.positionArea ?? undefined; _profile.positionExecutiveField = _position.positionExecutiveField ?? undefined; - _profile.posMasterNo = getPosMasterNo(dataMaster); - _profile.org = getOrgFullName(dataMaster); - await this.profileRepository.save(_profile); - setLogDataDiff(request, { before, after: _profile }); } + await this.profileRepository.save(_profile); + setLogDataDiff(request, { before, after: _profile }); } } dataMaster.current_holderId = requestBody.profileId; diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 5341d03c..6ba40258 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -652,29 +652,33 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { await posMasterAssignRepository.save(newAssigns); } - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (item.next_holderId != null && !item.isSit) { + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + if (item.next_holderId != null) { const profile = await repoProfile.findOne({ where: { id: item.next_holderId == null ? "" : item.next_holderId }, }); - if (profile != null && item.positions.length > 0) { - let position = await item.positions.find((x) => x.positionIsSelected == true); - if (position == null) { - position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId); - if (position == null) { - position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0]; - } - } - - profile.posLevelId = position?.posLevelId ?? _null; - profile.posTypeId = position?.posTypeId ?? _null; - profile.position = position?.positionName ?? _null; - profile.positionField = position?.positionField ?? _null; - profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null; - profile.positionArea = position?.positionArea ?? _null; - profile.positionExecutiveField = position?.positionExecutiveField ?? _null; + if (profile != null) { profile.posMasterNo = getPosMasterNo(item) ?? _null; profile.org = getOrgFullName(item) ?? _null; + + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (!item.isSit && item.positions.length > 0) { + let position = await item.positions.find((x) => x.positionIsSelected == true); + if (position == null) { + position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId); + if (position == null) { + position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0]; + } + } + + profile.posLevelId = position?.posLevelId ?? _null; + profile.posTypeId = position?.posTypeId ?? _null; + profile.position = position?.positionName ?? _null; + profile.positionField = position?.positionField ?? _null; + profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null; + profile.positionArea = position?.positionArea ?? _null; + profile.positionExecutiveField = position?.positionExecutiveField ?? _null; + } await repoProfile.save(profile); } } From 5e52206987ff9a1b50b7a91708bb1853164daf61 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 20 Apr 2026 17:23:15 +0700 Subject: [PATCH 06/18] update --- src/controllers/OrganizationController.ts | 24 +++++++++++++++++------ src/controllers/PositionController.ts | 17 +++++++++++++++- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 39752b7e..ee8f3413 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -66,7 +66,7 @@ import { import { orgStructureCache } from "../utils/OrgStructureCache"; import { OrgIdMapping, AllOrgMappings, SavePosMasterHistory } from "../interfaces/OrgMapping"; import { OrgPermissionData, NodeLevel } from "../interfaces/OrgTypes"; -import { formatPosMaster, generateLabelName, filterPosMasters } from "../utils/org-formatting"; +import { formatPosMaster, generateLabelName, filterPosMasters, getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; @Route("api/v1/org") @Tags("Organization") @@ -8933,13 +8933,25 @@ export class OrganizationController extends Controller { const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; // Collect profile update for the selected position + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + if (nextHolderId != null && draftPos.positionIsSelected) { + const _null: any = null; + profileUpdates.set(nextHolderId, { + posMasterNo: draftPosMaster ? (getPosMasterNo(draftPosMaster as PosMaster) ?? _null) : _null, + org: draftPosMaster ? (getOrgFullName(draftPosMaster as PosMaster) ?? _null) : _null, + }); + } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (nextHolderId != null && draftPos.positionIsSelected && !draftPosMaster?.isSit) { - profileUpdates.set(nextHolderId, { - position: draftPos.positionName, - posTypeId: draftPos.posTypeId, - posLevelId: draftPos.posLevelId, - }); + const existing = profileUpdates.get(nextHolderId) || {}; + existing.position = draftPos.positionName; + existing.posTypeId = draftPos.posTypeId; + existing.posLevelId = draftPos.posLevelId; + existing.positionField = draftPos.positionField ?? null; + existing.posExecutive = (draftPos as any).posExecutive?.posExecutiveName ?? null; + existing.positionArea = draftPos.positionArea ?? null; + existing.positionExecutiveField = draftPos.positionExecutiveField ?? null; + profileUpdates.set(nextHolderId, existing); if (draftPosMaster && draftPosMaster.ancestorDNA) { // Find the selected position from draft positions const selectedPos = diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index a1378892..7b1e1594 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -1257,7 +1257,7 @@ export class PositionController extends Controller { ) { await new permission().PermissionUpdate(request, "SYS_ORG"); const posMaster = await this.posMasterRepository.findOne({ - relations: ["positions", "orgRevision"], + relations: ["positions", "orgRevision", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], where: { id: id }, }); if (!posMaster) { @@ -1452,6 +1452,17 @@ export class PositionController extends Controller { }), ); + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + if (posMaster.orgRevision?.orgRevisionIsCurrent == true && posMaster.current_holderId) { + const _profile = await this.profileRepository.findOne({ + where: { id: posMaster.current_holderId }, + }); + if (_profile) { + _profile.posMasterNo = getPosMasterNo(posMaster); + _profile.org = getOrgFullName(posMaster); + await this.profileRepository.save(_profile); + } + } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (posMaster.orgRevision?.orgRevisionIsCurrent == true && !posMaster.isSit) { const _position = requestBody.positions.find((p) => p.positionIsSelected == true); @@ -1464,6 +1475,10 @@ export class PositionController extends Controller { _profile.position = _position.posDictName ?? _null; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; + _profile.positionField = _position.posDictField ?? _null; + _profile.posExecutive = _position.posExecutiveId ?? _null; + _profile.positionArea = _position.posDictArea ?? _null; + _profile.positionExecutiveField = _position.posDictExecutiveField ?? _null; await this.profileRepository.save(_profile); } } From 7e3982a96d2b639b58bb6e6d01ea49671f02b270 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 20 Apr 2026 18:20:20 +0700 Subject: [PATCH 07/18] =?UTF-8?q?fixed=20calculate=20tenure=20(=E0=B8=AA?= =?UTF-8?q?=E0=B8=B9=E0=B8=95=E0=B8=A3=E0=B8=84=E0=B8=B3=E0=B8=99=E0=B8=A7?= =?UTF-8?q?=E0=B8=99=E0=B8=AD=E0=B8=B2=E0=B8=A2=E0=B8=B8=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=8A=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=88=E0=B8=B2=E0=B8=81?= =?UTF-8?q?=20diff=20date)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileSalaryController.ts | 90 +++++++++---------- .../ProfileSalaryEmployeeController.ts | 29 +++--- src/utils/tenure.ts | 23 +++++ 3 files changed, 85 insertions(+), 57 deletions(-) create mode 100644 src/utils/tenure.ts diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index c8193750..8cc9d376 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -23,6 +23,7 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { calculateTenure } from "../utils/tenure"; import { TenurePositionOfficer } from "../entities/TenurePositionOfficer"; import { TenureLevelOfficer } from "../entities/TenureLevelOfficer"; import { TenurePositionEmployee } from "../entities/TenurePositionEmployee"; @@ -92,16 +93,14 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionName: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionName: calDayDiff.positionName, days_diff: calDayDiff.days_diff, - // Years: (calDayDiff.days_diff / 365.2524).toFixed(4), - // Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - // Days: (calDayDiff.days_diff % 30.4375).toFixed(4), - Years: Math.floor(calDayDiff.days_diff / 365.2524), - Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12), - Days: Math.floor(calDayDiff.days_diff % 30.4375), + Years: year, + Months: month, + Days: day, }; // data.push(_mapData); await this.positionOfficerRepo.save(mapData); @@ -143,16 +142,14 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionName: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileEmployeeId: x.id, positionName: calDayDiff.positionName, days_diff: calDayDiff.days_diff, - // Years: (calDayDiff.days_diff / 365.2524).toFixed(4), - // Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - // Days: (calDayDiff.days_diff % 30.4375).toFixed(4), - Years: Math.floor(calDayDiff.days_diff / 365.2524), - Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12), - Days: Math.floor(calDayDiff.days_diff % 30.4375), + Years: year, + Months: month, + Days: day, }; // data.push(_mapData); await this.positionEmployeeRepo.save(mapData); @@ -202,15 +199,16 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4), - Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: x.posLevel == null ? 0 : year.toFixed(4), + Months: x.posLevel == null ? 0 : month.toFixed(4), + Days: x.posLevel == null ? 0 : day.toFixed(4), }; // data.push(_mapData); await this.levelOfficerRepo.save(mapData); @@ -260,15 +258,16 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileEmployeeId: x.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4), - Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: x.posLevel == null ? 0 : year.toFixed(4), + Months: x.posLevel == null ? 0 : month.toFixed(4), + Days: x.posLevel == null ? 0 : day.toFixed(4), }; // data.push(_mapData); await this.levelEmployeeRepo.save(mapData); @@ -331,13 +330,14 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionExecutive: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionExecutiveName: calDayDiff.positionExecutive, days_diff: calDayDiff.days_diff, - Years: (calDayDiff.days_diff / 365.2524).toFixed(4), - Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - Days: (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: year.toFixed(4), + Months: month.toFixed(4), + Days: day.toFixed(4), }; await this.positionExecutiveOfficerRepo.save(mapData); } @@ -602,10 +602,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -641,10 +641,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -675,10 +675,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -739,10 +739,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -782,10 +782,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -819,10 +819,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 5b87003c..7428e913 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -27,6 +27,7 @@ import { Profile } from "../entities/Profile"; import { In, LessThan, IsNull, MoreThan } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { calculateTenure } from "../utils/tenure"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import Extension from "../interfaces/extension"; @@ -175,9 +176,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -211,9 +213,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -266,9 +269,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -302,9 +306,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, diff --git a/src/utils/tenure.ts b/src/utils/tenure.ts new file mode 100644 index 00000000..577d314b --- /dev/null +++ b/src/utils/tenure.ts @@ -0,0 +1,23 @@ +/** + * คำนวณอายุงานจากจำนวนวันรวม + * @param totalDays จำนวนวันรวม + * @returns { year, month, day } ปี เดือน วัน + */ +export function calculateTenure(totalDays: number) { + // 1. แปลงเป็น year เต็ม + const year = Math.floor(totalDays / 365.2524); + + // 2. วันที่เหลือหลังหัก year ออก + const remainAfterYear = totalDays - year * 365.2524; + + // 3. แปลงเป็น month เต็ม + const month = Math.floor(remainAfterYear / 30.4375); + + // 4. วันที่เหลือหลังหัก month ออก + const remainAfterMonth = remainAfterYear - month * 30.4375; + + // 5. ปัดลง เฉพาะวัน + const day = Math.floor(remainAfterMonth); + + return { year, month, day }; +} From 194d79bf048980273d5514a7d0909f8b1cf74e2f Mon Sep 17 00:00:00 2001 From: adisak Date: Tue, 21 Apr 2026 17:37:17 +0700 Subject: [PATCH 08/18] =?UTF-8?q?#231=20=E0=B9=81=E0=B8=A5=E0=B8=B0=20#243?= =?UTF-8?q?8=20checkpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProfileGovernmentController.ts | 308 +++--------------- update_profile_position_fields.sql | 154 +++++++++ 2 files changed, 203 insertions(+), 259 deletions(-) create mode 100644 update_profile_position_fields.sql diff --git a/src/controllers/ProfileGovernmentController.ts b/src/controllers/ProfileGovernmentController.ts index 8caaff28..9af40339 100644 --- a/src/controllers/ProfileGovernmentController.ts +++ b/src/controllers/ProfileGovernmentController.ts @@ -6,8 +6,6 @@ import HttpError from "../interfaces/http-error"; import { RequestWithUser } from "../middlewares/user"; import { Profile } from "../entities/Profile"; import { ProfileGovernment, UpdateProfileGovernment } from "../entities/ProfileGovernment"; -import { Position } from "../entities/Position"; -import { PosMaster } from "../entities/PosMaster"; import { calculateAge, calculateGovAge, @@ -15,7 +13,6 @@ import { setLogDataDiff, } from "../interfaces/utils"; import permission from "../interfaces/permission"; -import { OrgRevision } from "../entities/OrgRevision"; import { In } from "typeorm"; @Route("api/v1/org/profile/government") @Tags("ProfileGovernment") @@ -23,9 +20,6 @@ import { In } from "typeorm"; export class ProfileGovernmentHistoryController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); private govRepo = AppDataSource.getRepository(ProfileGovernment); - private positionRepo = AppDataSource.getRepository(Position); - private posMasterRepo = AppDataSource.getRepository(PosMaster); - private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); /** * * @summary ข้อมูลราชการ @@ -33,13 +27,6 @@ export class ProfileGovernmentHistoryController extends Controller { */ @Get("user") public async getGovHistoryUser(@Request() request: { user: Record }) { - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); @@ -51,79 +38,19 @@ export class ProfileGovernmentHistoryController extends Controller { posLevel: true, }, }); - const posMaster = await this.posMasterRepo.findOne({ - where: { - // orgRevision: { - // orgRevisionIsCurrent: true, - // orgRevisionIsDraft: false, - // }, - orgRevisionId: orgRevision?.id, - current_holderId: profile.id, - }, - order: { createdAt: "DESC" }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - // orgRevision: { - // orgRevisionIsCurrent: true, - // orgRevisionIsDraft: false, - // }, - orgRevisionId: orgRevision?.id, - current_holderId: profile.id, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, - }); - if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const fullNameParts = [ - posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, - posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, - posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, - posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, - posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, - ]; - const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); - let orgShortName = ""; - if (posMaster != null) { - if (posMaster.orgChild1Id === null) { - orgShortName = posMaster.orgRoot?.orgRootShortName; - } else if (posMaster.orgChild2Id === null) { - orgShortName = posMaster.orgChild1?.orgChild1ShortName; - } else if (posMaster.orgChild3Id === null) { - orgShortName = posMaster.orgChild2?.orgChild2ShortName; - } else if (posMaster.orgChild4Id === null) { - orgShortName = posMaster.orgChild3?.orgChild3ShortName; - } else { - orgShortName = posMaster.orgChild4?.orgChild4ShortName; - } - } - //posMaster?.isSit แก้ไขชั่วคราว + + // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว const data = { - org: org, //สังกัด - positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน + org: record.org ?? null, //สังกัด + positionField: record.positionField ?? null, //สายงาน position: record.position, //ตำแหน่ง posLevel: record.posLevel == null ? null : record.posLevel.posLevelName, //ระดับ - posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNo}`, //เลขที่ตำแหน่ง + posMasterNo: record.posMasterNo ?? null, //เลขที่ตำแหน่ง posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท - posExecutive: - position == null || position.posExecutive == null || posMaster?.isSit - ? null - : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร + posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร + positionArea: record.positionArea ?? null, //ด้าน/สาขา + positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate), dateRetireLaw: record.dateRetireLaw ?? null, // govAge: record.dateStart == null ? null : calculateAge(record.dateStart), @@ -135,10 +62,10 @@ export class ProfileGovernmentHistoryController extends Controller { govAgePlus: record.govAgePlus, reasonSameDate: record.reasonSameDate, }; - + return new HttpSuccess(data); } - + /** * * @summary ข้อมูลราชการ @@ -150,25 +77,17 @@ export class ProfileGovernmentHistoryController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_OFFICER"); if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); - - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); - + // ค้นหา profile ก่อน const record = await this.profileRepo.findOne({ where: { id: profileId }, relations: ["posType", "posLevel"], }); - + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile"); } - + // ค้นหา profileSalary แยกต่างหาก const profileWithSalary = await this.profileRepo.findOne({ where: { @@ -201,70 +120,13 @@ export class ProfileGovernmentHistoryController extends Controller { }, }, }); - + // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ record.profileSalary = profileWithSalary?.profileSalary || []; - const posMaster = await this.posMasterRepo.findOne({ - where: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - order: { createdAt: "DESC" }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, - }); - - // if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const fullNameParts = [ - posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, - posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, - posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, - posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, - posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, - ]; - const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); - let orgShortName = ""; - if (posMaster != null) { - if (posMaster.orgChild1Id === null) { - orgShortName = posMaster.orgRoot?.orgRootShortName ?? ""; - } else if (posMaster.orgChild2Id === null) { - orgShortName = posMaster.orgChild1?.orgChild1ShortName ?? ""; - } else if (posMaster.orgChild3Id === null) { - orgShortName = posMaster.orgChild2?.orgChild2ShortName ?? ""; - } else if (posMaster.orgChild4Id === null) { - orgShortName = posMaster.orgChild3?.orgChild3ShortName ?? ""; - } else { - orgShortName = posMaster.orgChild4?.orgChild4ShortName ?? ""; - } - } + let _OrgLeave: any = []; let _profileSalary: any = null; if (record?.isLeave && record?.profileSalary.length > 0) { - // _OrgLeave = [ - // record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null, - // record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null, - // record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null, - // record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null, - // record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null, - // ]; if (record.leaveType == "RETIRE") { _profileSalary = record?.profileSalary.length > 1 @@ -288,27 +150,23 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); - //posMaster?.isSit แก้ไขชั่วคราว + + // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว const data = { - org: record?.isLeave == false ? org : orgLeave, //สังกัด - positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน + org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด + positionField: record.positionField ?? null, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: record?.isLeave == false - ? posMaster == null - ? null - : `${orgShortName} ${posMaster.posMasterNo}` + ? record.posMasterNo ?? null : _profileSalary != null ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท - posExecutive: - position == null || position.posExecutive == null || posMaster?.isSit - ? null - : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร + posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร + positionArea: record.positionArea ?? null, //ด้าน/สาขา + positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -320,30 +178,22 @@ export class ProfileGovernmentHistoryController extends Controller { govAgePlus: record?.govAgePlus, reasonSameDate: record?.reasonSameDate, }; - + return new HttpSuccess(data); } - + @Get("admin/{profileId}") public async getGovHistoryAdmin(@Path() profileId: string) { - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); - // ค้นหา profile ก่อน const record = await this.profileRepo.findOne({ where: { id: profileId }, relations: ["posType", "posLevel"], }); - + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile"); } - + // ค้นหา profileSalary แยกต่างหาก const profileWithSalary = await this.profileRepo.findOne({ where: { @@ -376,70 +226,13 @@ export class ProfileGovernmentHistoryController extends Controller { }, }, }); - + // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ record.profileSalary = profileWithSalary?.profileSalary || []; - const posMaster = await this.posMasterRepo.findOne({ - where: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - order: { createdAt: "DESC" }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, - }); - - // if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const fullNameParts = [ - posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, - posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, - posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, - posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, - posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, - ]; - const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); - let orgShortName = ""; - if (posMaster != null) { - if (posMaster.orgChild1Id === null) { - orgShortName = posMaster.orgRoot?.orgRootShortName; - } else if (posMaster.orgChild2Id === null) { - orgShortName = posMaster.orgChild1?.orgChild1ShortName; - } else if (posMaster.orgChild3Id === null) { - orgShortName = posMaster.orgChild2?.orgChild2ShortName; - } else if (posMaster.orgChild4Id === null) { - orgShortName = posMaster.orgChild3?.orgChild3ShortName; - } else { - orgShortName = posMaster.orgChild4?.orgChild4ShortName; - } - } + let _OrgLeave: any = []; let _profileSalary: any = null; if (record?.isLeave && record?.profileSalary.length > 0) { - // _OrgLeave = [ - // record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null, - // record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null, - // record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null, - // record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null, - // record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null, - // ]; if (record.leaveType == "RETIRE") { _profileSalary = record?.profileSalary.length > 1 @@ -463,27 +256,23 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); - //posMaster?.isSit แก้ไขชั่วคราว + + // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว const data = { - org: record?.isLeave == false ? org : orgLeave, //สังกัด - positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน + org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด + positionField: record.positionField ?? null, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: record?.isLeave == false - ? posMaster == null - ? null - : `${orgShortName} ${posMaster.posMasterNo}` + ? record.posMasterNo ?? null : _profileSalary != null ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท - posExecutive: - position == null || position.posExecutive == null || posMaster?.isSit - ? null - : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร + posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร + positionArea: record.positionArea ?? null, //ด้าน/สาขา + positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -496,10 +285,10 @@ export class ProfileGovernmentHistoryController extends Controller { reasonSameDate: record?.reasonSameDate, isLeave: record?.isLeave, }; - + return new HttpSuccess(data); } - + /** * * @summary ประวัติข้อมูลราชการ by keycloak @@ -517,7 +306,7 @@ export class ProfileGovernmentHistoryController extends Controller { }); return new HttpSuccess(record); } - + /** * * @summary ประวัติข้อมูลราชการ @@ -533,12 +322,12 @@ export class ProfileGovernmentHistoryController extends Controller { order: { lastUpdatedAt: "DESC" }, where: { profileId: profileId }, }); - + // record.pop(); - + return new HttpSuccess(record); } - + /** * * @summary แก้ไขข้อมูลราชการ @@ -554,14 +343,14 @@ export class ProfileGovernmentHistoryController extends Controller { const record = await this.profileRepo.findOne({ where: { id: profileId }, }); - + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const before = structuredClone(record); const history = new ProfileGovernment(); - + Object.assign(record, body); Object.assign(history, { ...record, id: undefined }); - + history.profileId = profileId; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; @@ -572,13 +361,14 @@ export class ProfileGovernmentHistoryController extends Controller { history.createdFullName = req.user.name; history.createdAt = new Date(); history.lastUpdatedAt = new Date(); - + await Promise.all([ this.profileRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), this.govRepo.save(history, { data: req }), ]); - + return new HttpSuccess(); } } + \ No newline at end of file diff --git a/update_profile_position_fields.sql b/update_profile_position_fields.sql new file mode 100644 index 00000000..37ccd16d --- /dev/null +++ b/update_profile_position_fields.sql @@ -0,0 +1,154 @@ +-- ===================================================== +-- Update position fields in profile table +-- อัพเดทฟิลด์ตำแหน่งในตาราง profile +-- +-- Fields: +-- - positionField (สายงาน) +-- - posExecutive (ตำแหน่งทางการบริหาร) +-- - positionArea (ด้าน/สาขา) +-- - positionExecutiveField (ด้านทางการบริหาร) +-- - posMasterNo (เลขที่ตำแหน่ง) - format: orgShortName + space + number +-- - org (สังกัด) +-- +-- Run each query separately to verify results +-- ===================================================== + +-- 1. Update positionField (สายงาน) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +SET p.positionField = pos.positionField +WHERE p.positionField IS NULL; + +-- 2. Update posExecutive (ตำแหน่งทางการบริหาร) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +INNER JOIN posExecutive pe ON pos.posExecutiveId = pe.id +SET p.posExecutive = pe.posExecutiveName +WHERE p.posExecutive IS NULL; + +-- 3. Update positionArea (ด้าน/สาขา) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +SET p.positionArea = pos.positionArea +WHERE p.positionArea IS NULL; + +-- 4. Update positionExecutiveField (ด้านทางการบริหาร) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +SET p.positionExecutiveField = pos.positionExecutiveField +WHERE p.positionExecutiveField IS NULL; + +-- 5. Update posMasterNo (เลขที่ตำแหน่ง) - format: orgShortName + space + number +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +LEFT JOIN orgRoot r ON pm.orgRootId = r.id +LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id +LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id +LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id +LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id +SET p.posMasterNo = TRIM(CONCAT( + CASE + WHEN pm.orgChild1Id IS NULL THEN r.orgRootShortName + WHEN pm.orgChild2Id IS NULL THEN c1.orgChild1ShortName + WHEN pm.orgChild3Id IS NULL THEN c2.orgChild2ShortName + WHEN pm.orgChild4Id IS NULL THEN c3.orgChild3ShortName + ELSE c4.orgChild4ShortName + END, + ' ', + pm.posMasterNo +)) +WHERE p.posMasterNo IS NULL; + +-- 6. Update org (สังกัด) - combine all org levels +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +LEFT JOIN orgRoot r ON pm.orgRootId = r.id +LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id +LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id +LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id +LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id +SET p.org = TRIM(CONCAT_WS( + ' ', + r.orgRootName, + c1.orgChild1Name, + c2.orgChild2Name, + c3.orgChild3Name, + c4.orgChild4Name +)) +WHERE p.org IS NULL; + +-- ===================================================== +-- เช็คผลลัพธ์ (Check results) +-- ===================================================== + +-- เช็คจำนวนที่ update ได้ +SELECT + COUNT(CASE WHEN positionField IS NOT NULL THEN 1 END) AS has_positionField, + COUNT(CASE WHEN posExecutive IS NOT NULL THEN 1 END) AS has_posExecutive, + COUNT(CASE WHEN positionArea IS NOT NULL THEN 1 END) AS has_positionArea, + COUNT(CASE WHEN positionExecutiveField IS NOT NULL THEN 1 END) AS has_positionExecutiveField, + COUNT(CASE WHEN posMasterNo IS NOT NULL THEN 1 END) AS has_posMasterNo, + COUNT(CASE WHEN org IS NOT NULL THEN 1 END) AS has_org +FROM profile; + +-- ===================================================== +-- SELECT query สำหรับทดสอบก่อนรัน (Test before run) +-- ===================================================== + +SELECT + p.id, + p.firstName, + p.lastName, + p.citizenId, + + p.positionField as old_positionField, + p.posExecutive as old_posExecutive, + p.positionArea as old_positionArea, + p.positionExecutiveField as old_positionExecutiveField, + p.posMasterNo as old_posMasterNo, + p.org as old_org, + + pos.positionField as new_positionField, + pe.posExecutiveName as new_posExecutive, + pos.positionArea as new_positionArea, + pos.positionExecutiveField as new_positionExecutiveField, + + TRIM(CONCAT( + CASE + WHEN pm.orgChild1Id IS NULL THEN r.orgRootShortName + WHEN pm.orgChild2Id IS NULL THEN c1.orgChild1ShortName + WHEN pm.orgChild3Id IS NULL THEN c2.orgChild2ShortName + WHEN pm.orgChild4Id IS NULL THEN c3.orgChild3ShortName + ELSE c4.orgChild4ShortName + END, + ' ', + pm.posMasterNo + )) as new_posMasterNo, + + TRIM(CONCAT_WS(' ', r.orgRootName, c1.orgChild1Name, c2.orgChild2Name, c3.orgChild3Name, c4.orgChild4Name)) as new_org + +FROM profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +LEFT JOIN posExecutive pe ON pos.posExecutiveId = pe.id +LEFT JOIN orgRoot r ON pm.orgRootId = r.id +LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id +LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id +LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id +LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id + +-- ใส่ WHERE ทดสอบ 1 คน (Test 1 person) +WHERE p.id = 'ใส่ profile_id ที่ต้องการทดสอบ' +-- หรือทดสอบ 10 คน (Test 10 persons) +-- LIMIT 10; From 8912e832271813997a74c2de3ce3ec8e73f5badf Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 23 Apr 2026 16:31:22 +0700 Subject: [PATCH 09/18] api import profileSalaryTemp #1570 & Fix Report KK1 #2439 --- src/controllers/ImportDataController.ts | 501 ++++++++++++++++++- src/controllers/ProfileController.ts | 93 +++- src/controllers/ProfileEmployeeController.ts | 93 +++- 3 files changed, 636 insertions(+), 51 deletions(-) diff --git a/src/controllers/ImportDataController.ts b/src/controllers/ImportDataController.ts index 5b8ca808..3e1a1b2b 100644 --- a/src/controllers/ImportDataController.ts +++ b/src/controllers/ImportDataController.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Route, Security, Tags, Request, UploadedFile } from "tsoa"; +import { Controller, Post, Route, Security, Tags, Request, UploadedFile, Path } from "tsoa"; import { AppDataSource } from "../database/data-source"; import { In, IsNull, LessThanOrEqual, Not, Between } from "typeorm"; import HttpSuccess from "../interfaces/http-success"; @@ -105,6 +105,7 @@ import { positionOfficer } from "../entities/mis/positionOfficer"; import { ProvinceMaster } from "../entities/ProvinceMaster"; import { SubDistrictMaster } from "../entities/SubDistrictMaster"; import { DistrictMaster } from "../entities/DistrictMaster"; +import { RequestWithUser } from "../middlewares/user"; @Route("api/v1/org/upload") @Tags("UPLOAD") @Security("bearerAuth") @@ -6815,4 +6816,502 @@ export class ImportDataController extends Controller { // await repo.save(entities); // return entities; // } + + /** + * @summary Import ข้อมูลประวัติตำแหน่งเงินเดือนของข้าราชการเข้าตาราง ProfileSalaryTemp + * @param profileId Id โปรไฟล์ข้าราชการ + * @param file Excel file with salary history data + */ + @Post("office-profileSalaryTemp/{profileId}") + @UseInterceptors(FileInterceptor("file")) + async UploadProfileSalaryTemp( + @Path() profileId: string, + @Request() req: RequestWithUser, + @UploadedFile() file: Express.Multer.File, + ) { + if (!profileId) { + throw new Error("profileId is required"); + } + + // อ่านไฟล์ Excel ก่อน (นอก transaction) + const workbook = xlsx.read(file.buffer, { type: "buffer" }); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; + + let salaryTemps: ProfileSalaryTemp[] = []; + let dateTime = new Date(); + + // เริ่มจาก index 1 เพื่อข้าม header row + for (let i = 1; i < getExcel.length; i++) { + const row = getExcel[i]; + + // ข้าม empty rows + if (!row || row.length === 0) { + continue; + } + + // ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง) + if (!row[0]) { + continue; + } + + const salaryTemp = new ProfileSalaryTemp(); + + // ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number + const parseExcelDate = (value: any): Date | null => { + if (!value) return null; + + // กรณี 1: Excel serial number (ตัวเลข) + if (typeof value === "number") { + // Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900 + // แปลงเป็น JavaScript Date (epoch 1970) + let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds() + ); + } + return jsDate; + } + + // กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy) + const dateStr = value.toString().trim(); + + // ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่ + if (/^\d+$/.test(dateStr)) { + const serialNum = parseInt(dateStr); + let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds() + ); + } + return jsDate; + } + + // String format ปกติ (dd/mm/yyyy) + const dateParts = dateStr.split("/"); + if (dateParts.length === 3) { + // แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก + const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0"); + const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0"); + let year = parseInt(dateParts[2].trim()); + if (year > 2500) { + year -= 543; + } + const result = new Date(`${year}-${month}-${day}`); + return result; + } + + return null; + }; + + // Index 1: วันที่คำสั่งมีผล + let commandDateAffect: Date | null = null; + if (row[1]) { + commandDateAffect = parseExcelDate(row[1]); + } + + // Index 25: วันที่ลงนาม + let commandDateSign: Date | null = null; + if (row[25]) { + commandDateSign = parseExcelDate(row[25]); + } + + // Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column + // ข้อมูลระบบ + salaryTemp.profileId = profileId; + salaryTemp.profileEmployeeId = null as any; + + // Index 0: ลำดับ + salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any); + + // Index 1: วันที่คำสั่งมีผล + salaryTemp.commandDateAffect = commandDateAffect as any; + + // Index 2: ตำแหน่งในสายงาน + salaryTemp.positionName = row[2] || null; + + // Index 3: ตำแหน่งประเภท + salaryTemp.positionType = row[3] || null; + + // Index 4: ระดับ + salaryTemp.positionLevel = row[4] || null; + + // Index 5: ระดับซี + salaryTemp.positionCee = row[5] || null; + + // Index 6: สายงาน + salaryTemp.positionLine = row[6] || null; + + // Index 7: ด้าน/สาขา + salaryTemp.positionPathSide = row[7] || null; + + // Index 8: ตำแหน่งทางการบริหาร + salaryTemp.positionExecutive = row[8] || null; + + // Index 9: ด้านทางการบริหาร + salaryTemp.positionExecutiveField = row[9] || null; + + // Index 10: เงินเดือน + salaryTemp.amount = row[10] || 0; + + // Index 11: เงินค่าตอบแทนรายเดือน + salaryTemp.mouthSalaryAmount = row[11] || 0; + + // Index 12: เงินประจำตำแหน่ง + salaryTemp.positionSalaryAmount = row[12] || 0; + + // Index 13: เงินค่าตอบแทนพิเศษ + salaryTemp.amountSpecial = row[13] || 0; + + // Index 14: หน่วยงาน + salaryTemp.orgRoot = row[14] || null; + + // Index 15: ส่วนราชการระดับ 1 + salaryTemp.orgChild1 = row[15] || null; + + // Index 16: ส่วนราชการระดับ 2 + salaryTemp.orgChild2 = row[16] || null; + + // Index 17: ส่วนราชการระดับ 3 + salaryTemp.orgChild3 = row[17] || null; + + // Index 18: ส่วนราชการระดับ 4 + salaryTemp.orgChild4 = row[18] || null; + + // Index 19: ตัวย่อเลขที่ตำแหน่ง + salaryTemp.posNoAbb = row[19] || null; + + // Index 20: เลขที่ตำแหน่ง + salaryTemp.posNo = row[20] ? row[20].toString() : null; + + // Index 21: หน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSit = row[21] || null; + + // Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSitAbb = row[22] || null; + + // Index 23: เลขที่คำสั่ง + salaryTemp.commandNo = row[23] || null; + + // Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.) + let commandYearValue: number | null = null; + if (row[24]) { + commandYearValue = parseInt(row[24].toString()); + // ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ. + if (commandYearValue > 2500) { + commandYearValue -= 543; + } + } + salaryTemp.commandYear = commandYearValue as any; + + // Index 25: วันที่ลงนาม (แปลงแล้ว) + salaryTemp.commandDateSign = commandDateSign as any; + + // Index 26: ประเภทคำสั่ง + salaryTemp.commandName = row[26] || null; + + // Index 27: หมายเหตุ + salaryTemp.remark = row[27] || null; + + // Index 28: commandId + salaryTemp.commandId = row[28] || null; + + // Index 29: commandCode + salaryTemp.commandCode = row[29] || null; + + // ข้อมูลระบบ + salaryTemp.isDelete = false; + salaryTemp.isEdit = false; + salaryTemp.isGovernment = false; + salaryTemp.isEntry = false; + salaryTemp.createdAt = dateTime; + salaryTemp.createdUserId = req.user?.sub || ""; + salaryTemp.createdFullName = req.user?.name || "System Administrator"; + salaryTemp.lastUpdatedAt = dateTime; + salaryTemp.lastUpdateUserId = req.user?.sub || ""; + salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator"; + + salaryTemps.push(salaryTemp); + } + + // ใช้ Transaction เพื่อความปลอดภัย + await AppDataSource.transaction(async (transactionalEntityManager) => { + // ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileId นั้น + await transactionalEntityManager.delete(ProfileSalaryTemp, { profileId }); + // Insert ข้อมูลใหม่ + await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps); + }); + + return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length }); + } + + /** + * @summary Import ข้อมูลประวัติตำแหน่งเงินเดือนของลูกจ้างประจำเข้าตาราง ProfileSalaryTemp + * @param profileEmployeeId Id โปรไฟล์ลูกจ้างประจำ + * @param file Excel file with salary history data + */ + @Post("employee-profileSalaryTemp/{profileEmployeeId}") + @UseInterceptors(FileInterceptor("file")) + async UploadProfileEmployeeSalaryTemp( + @Path() profileEmployeeId: string, + @Request() req: RequestWithUser, + @UploadedFile() file: Express.Multer.File, + ) { + if (!profileEmployeeId) { + throw new Error("profileEmployeeId is required"); + } + + // อ่านไฟล์ Excel ก่อน (นอก transaction) + const workbook = xlsx.read(file.buffer, { type: "buffer" }); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; + + let salaryTemps: ProfileSalaryTemp[] = []; + let dateTime = new Date(); + + // เริ่มจาก index 1 เพื่อข้าม header row + for (let i = 1; i < getExcel.length; i++) { + const row = getExcel[i]; + + // ข้าม empty rows + if (!row || row.length === 0) { + continue; + } + + // ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง) + if (!row[0]) { + continue; + } + + const salaryTemp = new ProfileSalaryTemp(); + + // ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number + const parseExcelDate = (value: any): Date | null => { + if (!value) return null; + + // กรณี 1: Excel serial number (ตัวเลข) + if (typeof value === "number") { + // Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900 + // แปลงเป็น JavaScript Date (epoch 1970) + let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds() + ); + } + return jsDate; + } + + // กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy) + const dateStr = value.toString().trim(); + + // ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่ + if (/^\d+$/.test(dateStr)) { + const serialNum = parseInt(dateStr); + let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds() + ); + } + return jsDate; + } + + // String format ปกติ (dd/mm/yyyy) + const dateParts = dateStr.split("/"); + if (dateParts.length === 3) { + // แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก + const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0"); + const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0"); + let year = parseInt(dateParts[2].trim()); + if (year > 2500) { + year -= 543; + } + const result = new Date(`${year}-${month}-${day}`); + return result; + } + + return null; + }; + + // Index 1: วันที่คำสั่งมีผล + let commandDateAffect: Date | null = null; + if (row[1]) { + commandDateAffect = parseExcelDate(row[1]); + } + + // Index 25: วันที่ลงนาม + let commandDateSign: Date | null = null; + if (row[25]) { + commandDateSign = parseExcelDate(row[25]); + } + + // Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column + // ข้อมูลระบบ + salaryTemp.profileEmployeeId = profileEmployeeId; + salaryTemp.profileId = null as any; + + // Index 0: ลำดับ + salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any); + + // Index 1: วันที่คำสั่งมีผล + salaryTemp.commandDateAffect = commandDateAffect as any; + + // Index 2: ตำแหน่งในสายงาน + salaryTemp.positionName = row[2] || null; + + // Index 3: ตำแหน่งประเภท + salaryTemp.positionType = row[3] || null; + + // Index 4: ระดับ + salaryTemp.positionLevel = row[4] || null; + + // Index 5: ระดับซี + salaryTemp.positionCee = row[5] || null; + + // Index 6: สายงาน + salaryTemp.positionLine = row[6] || null; + + // Index 7: ด้าน/สาขา + salaryTemp.positionPathSide = row[7] || null; + + // Index 8: ตำแหน่งทางการบริหาร + salaryTemp.positionExecutive = row[8] || null; + + // Index 9: ด้านทางการบริหาร + salaryTemp.positionExecutiveField = row[9] || null; + + // Index 10: เงินเดือน + salaryTemp.amount = row[10] || 0; + + // Index 11: เงินค่าตอบแทนรายเดือน + salaryTemp.mouthSalaryAmount = row[11] || 0; + + // Index 12: เงินประจำตำแหน่ง + salaryTemp.positionSalaryAmount = row[12] || 0; + + // Index 13: เงินค่าตอบแทนพิเศษ + salaryTemp.amountSpecial = row[13] || 0; + + // Index 14: หน่วยงาน + salaryTemp.orgRoot = row[14] || null; + + // Index 15: ส่วนราชการระดับ 1 + salaryTemp.orgChild1 = row[15] || null; + + // Index 16: ส่วนราชการระดับ 2 + salaryTemp.orgChild2 = row[16] || null; + + // Index 17: ส่วนราชการระดับ 3 + salaryTemp.orgChild3 = row[17] || null; + + // Index 18: ส่วนราชการระดับ 4 + salaryTemp.orgChild4 = row[18] || null; + + // Index 19: ตัวย่อเลขที่ตำแหน่ง + salaryTemp.posNoAbb = row[19] || null; + + // Index 20: เลขที่ตำแหน่ง + salaryTemp.posNo = row[20] ? row[20].toString() : null; + + // Index 21: หน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSit = row[21] || null; + + // Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSitAbb = row[22] || null; + + // Index 23: เลขที่คำสั่ง + salaryTemp.commandNo = row[23] || null; + + // Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.) + let commandYearValue: number | null = null; + if (row[24]) { + commandYearValue = parseInt(row[24].toString()); + // ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ. + if (commandYearValue > 2500) { + commandYearValue -= 543; + } + } + salaryTemp.commandYear = commandYearValue as any; + + // Index 25: วันที่ลงนาม (แปลงแล้ว) + salaryTemp.commandDateSign = commandDateSign as any; + + // Index 26: ประเภทคำสั่ง + salaryTemp.commandName = row[26] || null; + + // Index 27: หมายเหตุ + salaryTemp.remark = row[27] || null; + + // Index 28: commandId + salaryTemp.commandId = row[28] || null; + + // Index 29: commandCode + salaryTemp.commandCode = row[29] || null; + + // ข้อมูลระบบ + salaryTemp.isDelete = false; + salaryTemp.isEdit = false; + salaryTemp.isGovernment = false; + salaryTemp.isEntry = false; + salaryTemp.createdAt = dateTime; + salaryTemp.createdUserId = req.user?.sub || ""; + salaryTemp.createdFullName = req.user?.name || "System Administrator"; + salaryTemp.lastUpdatedAt = dateTime; + salaryTemp.lastUpdateUserId = req.user?.sub || ""; + salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator"; + + salaryTemps.push(salaryTemp); + } + + // ใช้ Transaction เพื่อความปลอดภัย + await AppDataSource.transaction(async (transactionalEntityManager) => { + // ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileEmployeeId นั้น + await transactionalEntityManager.delete(ProfileSalaryTemp, { profileEmployeeId }); + // Insert ข้อมูลใหม่ + await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps); + }); + + return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length }); + } } diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 15461d31..65cdee0c 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1679,35 +1679,78 @@ export class ProfileController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); - // todo: รอข้อสรุป - // const retire_raw = await this.salaryRepo.findOne({ - // where: { - // profileId: id, - // commandCode: In(["12", "15", "16"]), - // }, - // order: { order: "desc" }, - // }); - // if (retire_raw) { - // const startDate = retire_raw.commandDateAffect; + // commandCode ที่ถือว่าออกจากราชการ + const retireCommandCodes = ["12", "15", "16"]; - // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน - // let daysCount = 0; - // if (startDate) { - // const start = new Date(startDate); - // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - // } + // ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ + const salaries = await this.salaryRepo.find({ + where: { profileId: id }, + order: { order: "ASC" }, + }); - // const startDateStr = startDate - // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - // : "-"; + // มีคำสั่งพ้นราชการหรือไม่ + if (salaries.length > 0 && salaries.some((s) => s.commandCode && + retireCommandCodes.includes(s.commandCode))) { + // กรองข้อมูลซ้ำตาม commandDateAffect + const uniqueSalaries = salaries.filter((item, index, self) => + index === self.findIndex((t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime()) + ); - // retires.push({ - // date: `${startDateStr}`, - // detail: retire_raw.commandName ?? "-", - // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - // }); - // } + // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" + for (let i = 0; i < uniqueSalaries.length; i++) { + const current = uniqueSalaries[i]; + + // เป็นคำสั่งออกจากราชการหรือไม่ + if (current.commandCode && retireCommandCodes.includes(current.commandCode)) { + const startDate = current.commandDateAffect; + let endDate: Date | null = null; + let endRecord = null; + + // หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ) + for (let j = i + 1; j < uniqueSalaries.length; j++) { + const next = uniqueSalaries[j]; + if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) { + endDate = next.commandDateAffect; + endRecord = next; + break; + } + } + + // ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน + if (!endDate) { + endDate = currentDate; + } + + // คำนวณจำนวนวัน + let daysCount = 0; + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + } + + // สร้าง detail จาก commandName + remark + const commandName = current.commandName || ""; + const remark = current.remark || ""; + const detail = `${commandName} ${remark}`.trim(); + + // แปลงวันที่เป็น format ไทย + const startDateStr = startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + : "-"; + const endDateStr = endDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) + : "-"; + + retires.push({ + date: `${startDateStr} - ${endDateStr}`, + detail: detail || "-", + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); + } + } + } // กรณีไม่มีข้อมูล if (retires.length === 0) { diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 2a7fba38..8ae134c1 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1950,35 +1950,78 @@ export class ProfileEmployeeController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); - // todo: รอข้อสรุป - // const retire_raw = await this.salaryRepo.findOne({ - // where: { - // profileEmployeeId: id, - // commandCode: In(["12", "15", "16"]), - // }, - // order: { order: "desc" }, - // }); - // if (retire_raw) { - // const startDate = retire_raw.commandDateAffect; + // commandCode ที่ถือว่าออกจากราชการ + const retireCommandCodes = ["12", "15", "16"]; - // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน - // let daysCount = 0; - // if (startDate) { - // const start = new Date(startDate); - // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - // } + // ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ + const salaries = await this.salaryRepo.find({ + where: { profileEmployeeId: id }, + order: { order: "ASC" }, + }); - // const startDateStr = startDate - // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - // : "-"; + // มีคำสั่งพ้นราชการหรือไม่ + if (salaries.length > 0 && salaries.some((s) => s.commandCode && + retireCommandCodes.includes(s.commandCode))) { + // กรองข้อมูลซ้ำตาม commandDateAffect + const uniqueSalaries = salaries.filter((item, index, self) => + index === self.findIndex((t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime()) + ); - // retires.push({ - // date: `${startDateStr} - ปัจจุบัน`, - // detail: retire_raw.commandName ?? "-", - // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - // }); - // } + // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" + for (let i = 0; i < uniqueSalaries.length; i++) { + const current = uniqueSalaries[i]; + + // เป็นคำสั่งออกจากราชการหรือไม่ + if (current.commandCode && retireCommandCodes.includes(current.commandCode)) { + const startDate = current.commandDateAffect; + let endDate: Date | null = null; + let endRecord = null; + + // หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ) + for (let j = i + 1; j < uniqueSalaries.length; j++) { + const next = uniqueSalaries[j]; + if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) { + endDate = next.commandDateAffect; + endRecord = next; + break; + } + } + + // ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน + if (!endDate) { + endDate = currentDate; + } + + // คำนวณจำนวนวัน + let daysCount = 0; + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + } + + // สร้าง detail จาก commandName + remark + const commandName = current.commandName || ""; + const remark = current.remark || ""; + const detail = `${commandName} ${remark}`.trim(); + + // แปลงวันที่เป็น format ไทย + const startDateStr = startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + : "-"; + const endDateStr = endDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) + : "-"; + + retires.push({ + date: `${startDateStr} - ${endDateStr}`, + detail: detail || "-", + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); + } + } + } // กรณีไม่มีข้อมูล if (retires.length === 0) { From 8f83ab781b3c3d765587b11d923587a887cbe424 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 24 Apr 2026 10:28:30 +0700 Subject: [PATCH 10/18] fix.save batch insert --- src/controllers/ProfileSalaryController.ts | 24 ++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 8cc9d376..03814161 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -102,10 +102,9 @@ export class ProfileSalaryController extends Controller { Months: month, Days: day, }; - // data.push(_mapData); - await this.positionOfficerRepo.save(mapData); + data.push(mapData); } - // await this.positionOfficerRepo.save(data); + await this.positionOfficerRepo.save(data); return new HttpSuccess(); } @@ -151,10 +150,9 @@ export class ProfileSalaryController extends Controller { Months: month, Days: day, }; - // data.push(_mapData); - await this.positionEmployeeRepo.save(mapData); + data.push(mapData); } - // await this.positionEmployeeRepo.save(data); + await this.positionEmployeeRepo.save(data); return new HttpSuccess(); } @@ -210,10 +208,9 @@ export class ProfileSalaryController extends Controller { Months: x.posLevel == null ? 0 : month.toFixed(4), Days: x.posLevel == null ? 0 : day.toFixed(4), }; - // data.push(_mapData); - await this.levelOfficerRepo.save(mapData); + data.push(mapData); } - // await this.levelOfficerRepo.save(data); + await this.levelOfficerRepo.save(data); return new HttpSuccess(); } @@ -269,16 +266,16 @@ export class ProfileSalaryController extends Controller { Months: x.posLevel == null ? 0 : month.toFixed(4), Days: x.posLevel == null ? 0 : day.toFixed(4), }; - // data.push(_mapData); - await this.levelEmployeeRepo.save(mapData); + data.push(mapData); } - // await this.levelEmployeeRepo.save(data); + await this.levelEmployeeRepo.save(data); return new HttpSuccess(); } @Get("TenurePositionExecutiveOfficer") public async cronjobTenureExecutivePositionOfficer() { + let data: any = []; await this.positionExecutiveOfficerRepo.clear(); const profile = await this.profileRepo.find(); const orgRevision = await this.orgRevisionRepository.findOne({ @@ -339,8 +336,9 @@ export class ProfileSalaryController extends Controller { Months: month.toFixed(4), Days: day.toFixed(4), }; - await this.positionExecutiveOfficerRepo.save(mapData); + data.push(mapData); } + await this.positionExecutiveOfficerRepo.save(data); return new HttpSuccess(); } From 1d16f781322b59ccdbc1b63b6ddc6a3983346bc9 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 24 Apr 2026 10:31:42 +0700 Subject: [PATCH 11/18] update path sql script --- .../update_profile_position_fields.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename update_profile_position_fields.sql => sql_seed/update_profile_position_fields.sql (100%) diff --git a/update_profile_position_fields.sql b/sql_seed/update_profile_position_fields.sql similarity index 100% rename from update_profile_position_fields.sql rename to sql_seed/update_profile_position_fields.sql From da4fd18e089bbd0bd6c1943bffcab2fe55c76047 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 24 Apr 2026 11:07:55 +0700 Subject: [PATCH 12/18] fix bug --- src/controllers/PositionController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 7b1e1594..430da630 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -3852,10 +3852,10 @@ export class PositionController extends Controller { _profile.position = _position.positionName; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; - _profile.positionField = _position.positionField ?? undefined; - _profile.posExecutive = _position.posExecutive?.posExecutiveName ?? undefined; - _profile.positionArea = _position.positionArea ?? undefined; - _profile.positionExecutiveField = _position.positionExecutiveField ?? undefined; + _profile.positionField = _position.positionField ?? _null; + _profile.posExecutive = _position.posExecutive?.posExecutiveName ?? _null; + _profile.positionArea = _position.positionArea ?? _null; + _profile.positionExecutiveField = _position.positionExecutiveField ?? _null; } await this.profileRepository.save(_profile); setLogDataDiff(request, { before, after: _profile }); From 5980c140f0455900ced139c4ed483539f063eabe Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 24 Apr 2026 11:38:45 +0700 Subject: [PATCH 13/18] =?UTF-8?q?fixed#1568=20=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B9=84=E0=B8=82=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A3=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=87=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B9=80=E0=B8=87=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=99=E0=B9=84=E0=B8=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/PositionController.ts | 181 ++++++++++++++------------ 1 file changed, 96 insertions(+), 85 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 7b1e1594..a040ea2f 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -1257,7 +1257,15 @@ export class PositionController extends Controller { ) { await new permission().PermissionUpdate(request, "SYS_ORG"); const posMaster = await this.posMasterRepository.findOne({ - relations: ["positions", "orgRevision", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], + relations: [ + "positions", + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + ], where: { id: id }, }); if (!posMaster) { @@ -2403,16 +2411,16 @@ export class PositionController extends Controller { ? "posMaster.orgRootId IN (:...root)" : "posMaster.orgRootId is null" : "1=1", - { root: _data.root } + { root: _data.root }, ) .andWhere( _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? "posMaster.orgChild1Id IN (:...child1)" - // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `posMaster.orgChild1Id is null` + : // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + `posMaster.orgChild1Id is null` : "1=1", - { child1: _data.child1 } + { child1: _data.child1 }, ) .andWhere( _data.child2 != undefined && _data.child2 != null @@ -2443,26 +2451,27 @@ export class PositionController extends Controller { { child4: _data.child4, }, - ) + ); // .andWhere(checkChildConditions) // .andWhere(typeCondition) // .andWhere(revisionCondition); if (body.keyword != null && body.keyword != "") { - query.orWhere( - new Brackets((qb) => { - qb.andWhere( - body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` - : "1=1", - ) - .andWhere(checkChildConditions) - .andWhere(typeCondition) - .andWhere(revisionCondition); - }), - ) + query + .orWhere( + new Brackets((qb) => { + qb.andWhere( + body.keyword != null && body.keyword != "" + ? body.isAll == false + ? searchShortName + : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + : "1=1", + ) + .andWhere(checkChildConditions) + .andWhere(typeCondition) + .andWhere(revisionCondition); + }), + ) .orWhere( new Brackets((qb) => { qb.andWhere( @@ -2971,50 +2980,50 @@ export class PositionController extends Controller { const type0LastPosMasterNo = requestBody.type == 0 ? await this.posMasterRepository.find({ - where: { - orgRootId: requestBody.id, - orgChild1Id: IsNull(), - }, - }) + where: { + orgRootId: requestBody.id, + orgChild1Id: IsNull(), + }, + }) : []; const type1LastPosMasterNo = requestBody.type == 1 ? await this.posMasterRepository.find({ - where: { - orgChild1Id: requestBody.id, - orgChild2Id: IsNull(), - }, - }) + where: { + orgChild1Id: requestBody.id, + orgChild2Id: IsNull(), + }, + }) : []; const type2LastPosMasterNo = requestBody.type == 2 ? await this.posMasterRepository.find({ - where: { - orgChild2Id: requestBody.id, - orgChild3Id: IsNull(), - }, - }) + where: { + orgChild2Id: requestBody.id, + orgChild3Id: IsNull(), + }, + }) : []; const type3LastPosMasterNo = requestBody.type == 3 ? await this.posMasterRepository.find({ - where: { - orgChild3Id: requestBody.id, - orgChild4Id: IsNull(), - }, - }) + where: { + orgChild3Id: requestBody.id, + orgChild4Id: IsNull(), + }, + }) : []; const type4LastPosMasterNo = requestBody.type == 4 ? await this.posMasterRepository.find({ - where: { - orgChild4Id: requestBody.id, - }, - }) + where: { + orgChild4Id: requestBody.id, + }, + }) : []; const allLastPosMasterNo = [ @@ -3848,7 +3857,7 @@ export class PositionController extends Controller { _profile.posMasterNo = getPosMasterNo(dataMaster); _profile.org = getOrgFullName(dataMaster); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if(!dataMaster.isSit){ + if (!dataMaster.isSit) { _profile.position = _position.positionName; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; @@ -5193,9 +5202,9 @@ export class PositionController extends Controller { } /** - * API รายการอัตรากำลัง + * API รายการตำแหน่งติดเงื่อนไข * - * @summary ORG_070 - รายการอัตรากำลัง (ADMIN) #56 + * @summary รายการตำแหน่งติดเงื่อนไข * */ @Post("master/position-condition") @@ -5206,7 +5215,7 @@ export class PositionController extends Controller { id: string; revisionId: string; type: number; - isAll: boolean; + isAll: boolean; // true คือเลือกเฉพาะตำแหน่งติดเงื่อนไข / false คือเลือกตำแหน่งทั้งหมด page: number; pageSize: number; keyword?: string; @@ -5226,7 +5235,7 @@ export class PositionController extends Controller { let level: any = resolveNodeLevel(orgDna); const cannotViewRootPosMaster = - (_data.privilege === "PARENT") || + _data.privilege === "PARENT" || (_data.privilege === "BROTHER" && level > 1) || (_data.privilege === "CHILD" && level > 0) || (_data.privilege === "NORMAL" && level != 0); @@ -5258,46 +5267,46 @@ export class PositionController extends Controller { typeCondition = { ...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild1Id: IsNull(), - }; - searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild1Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 1) { typeCondition = { ...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild2Id: IsNull(), - }; - searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild2Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 2) { typeCondition = { ...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild3Id: IsNull(), - }; - searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild3Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 3) { typeCondition = { ...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild4Id: IsNull(), - }; - searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild4Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 4) { typeCondition = { ...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }), @@ -5370,7 +5379,7 @@ export class PositionController extends Controller { (masterId.length > 0 ? { id: In(masterId) } : { posMasterNo: Like(`%${body.keyword}%`) })), - current_holderId: IsNull(), + ...(!body.isAll && { isCondition: true }), }, ]; let [posMaster, total] = await AppDataSource.getRepository(PosMaster) @@ -5439,15 +5448,15 @@ export class PositionController extends Controller { new Brackets((qb) => { qb.andWhere( body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + ? `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` : "1=1", ) .andWhere(checkChildConditions) .andWhere(typeCondition) - .andWhere(revisionCondition) - .andWhere({ current_holderId: IsNull() }); + .andWhere(revisionCondition); + if (!body.isAll) { + qb.andWhere({ isCondition: true }); + } }), ) .orWhere( @@ -5457,8 +5466,10 @@ export class PositionController extends Controller { ) .andWhere(checkChildConditions) .andWhere(typeCondition) - .andWhere(revisionCondition) - .andWhere({ current_holderId: IsNull() }); + .andWhere(revisionCondition); + if (!body.isAll) { + qb.andWhere({ isCondition: true }); + } }), ) .orderBy("orgRoot.orgRootOrder", "ASC") From ec6b4a7ac8bc02ea27b59f2a759abfa2ff92ba42 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 24 Apr 2026 12:19:17 +0700 Subject: [PATCH 14/18] fix calculate --- src/app.ts | 3 +- src/controllers/ProfileSalaryController.ts | 40 ++++++++++++++-------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/app.ts b/src/app.ts index 06f76548..6ee54a80 100644 --- a/src/app.ts +++ b/src/app.ts @@ -98,7 +98,8 @@ async function main() { }); // Cron job for updating tenure - every day at 04:00:00 - const cronTime_Tenure = "0 0 4 * * *"; + // const cronTime_Tenure = "0 0 4 * * *"; + const cronTime_Tenure = "0 30 12 * * *"; // test 12:30 cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 03814161..4e3e9e3b 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -66,10 +66,12 @@ export class ProfileSalaryController extends Controller { await this.positionOfficerRepo.clear(); const profile = await this.profileRepo.find(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ x.id, @@ -113,11 +115,13 @@ export class ProfileSalaryController extends Controller { let data: any = []; await this.positionEmployeeRepo.clear(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; const profile = await this.profileEmployeeRepo.find(); for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const position = await AppDataSource.query("CALL GetProfileEmployeeSalaryPosition(?, ?)", [ x.id, @@ -162,10 +166,12 @@ export class ProfileSalaryController extends Controller { await this.levelOfficerRepo.clear(); const profile = await this.profileRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const positionLevel = await AppDataSource.query("CALL GetProfileSalaryLevel(?, ?)", [ x.id, @@ -220,10 +226,12 @@ export class ProfileSalaryController extends Controller { await this.levelEmployeeRepo.clear(); const profile = await this.profileEmployeeRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const positionLevel = await AppDataSource.query("CALL GetProfileEmployeeSalaryLevel(?, ?)", [ x.id, @@ -286,10 +294,12 @@ export class ProfileSalaryController extends Controller { }, }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const position = await this.positionRepo.findOne({ where: { From b9b73ca9947b5a8ba18907a704d271f41d1f1a4c Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 24 Apr 2026 13:07:26 +0700 Subject: [PATCH 15/18] rollback code cronjob time --- src/app.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index 6ee54a80..06f76548 100644 --- a/src/app.ts +++ b/src/app.ts @@ -98,8 +98,7 @@ async function main() { }); // Cron job for updating tenure - every day at 04:00:00 - // const cronTime_Tenure = "0 0 4 * * *"; - const cronTime_Tenure = "0 30 12 * * *"; // test 12:30 + const cronTime_Tenure = "0 0 4 * * *"; cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); From 2cbc6569e3dcd65de4c36f901b9b08e2f779306e Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 24 Apr 2026 13:41:10 +0700 Subject: [PATCH 16/18] update script sql --- sql_seed/update_profile_position_fields.sql | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sql_seed/update_profile_position_fields.sql b/sql_seed/update_profile_position_fields.sql index 37ccd16d..950f0123 100644 --- a/sql_seed/update_profile_position_fields.sql +++ b/sql_seed/update_profile_position_fields.sql @@ -12,7 +12,7 @@ -- -- Run each query separately to verify results -- ===================================================== - +USE hrms_organization; -- 1. Update positionField (สายงาน) UPDATE profile p INNER JOIN posMaster pm ON pm.current_holderId = p.id @@ -78,12 +78,12 @@ LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id SET p.org = TRIM(CONCAT_WS( - ' ', - r.orgRootName, - c1.orgChild1Name, - c2.orgChild2Name, + CHAR(10), + c4.orgChild4Name, c3.orgChild3Name, - c4.orgChild4Name + c2.orgChild2Name, + c1.orgChild1Name, + r.orgRootName )) WHERE p.org IS NULL; @@ -135,7 +135,7 @@ SELECT pm.posMasterNo )) as new_posMasterNo, - TRIM(CONCAT_WS(' ', r.orgRootName, c1.orgChild1Name, c2.orgChild2Name, c3.orgChild3Name, c4.orgChild4Name)) as new_org + TRIM(CONCAT_WS(CHAR(10), c4.orgChild4Name, c3.orgChild3Name, c2.orgChild2Name, c1.orgChild1Name, r.orgRootName)) as new_org FROM profile p INNER JOIN posMaster pm ON pm.current_holderId = p.id From 8705d1abf5444fc9e4712131c7dace1b2203f783 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 24 Apr 2026 16:15:47 +0700 Subject: [PATCH 17/18] update --- sql_seed/update_profile_position_fields.sql | 2 +- src/utils/org-formatting.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sql_seed/update_profile_position_fields.sql b/sql_seed/update_profile_position_fields.sql index 950f0123..e9e999dc 100644 --- a/sql_seed/update_profile_position_fields.sql +++ b/sql_seed/update_profile_position_fields.sql @@ -64,7 +64,7 @@ SET p.posMasterNo = TRIM(CONCAT( ELSE c4.orgChild4ShortName END, ' ', - pm.posMasterNo + CONCAT_WS('', pm.posMasterNoPrefix, pm.posMasterNo, pm.posMasterNoSuffix) )) WHERE p.posMasterNo IS NULL; diff --git a/src/utils/org-formatting.ts b/src/utils/org-formatting.ts index 8b460a2f..fd61f33b 100644 --- a/src/utils/org-formatting.ts +++ b/src/utils/org-formatting.ts @@ -101,9 +101,14 @@ export function getOrgFullName(posMaster: PosMaster): string { } /** - * สร้างเลขที่ตำแหน่ง เช่น "กทม. 1234" + * สร้างเลขที่ตำแหน่ง เช่น "กทม. กบ.1234ช" */ export function getPosMasterNo(posMaster: PosMaster): string { const orgShortName = getOrgShortName(posMaster); - return `${orgShortName} ${posMaster.posMasterNo}`; + const parts = [ + posMaster.posMasterNoPrefix, + posMaster.posMasterNo, + posMaster.posMasterNoSuffix, + ].filter((part) => part !== null && part !== undefined); + return `${orgShortName} ${parts.join('')}`; } From 28319f443f84fc85644e030ce8c2950104ba279a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 27 Apr 2026 19:13:21 +0700 Subject: [PATCH 18/18] add api get profile keycloak/position-checkin --- src/controllers/ProfileController.ts | 102 +++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 65cdee0c..dbbecb80 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1690,11 +1690,17 @@ export class ProfileController extends Controller { }); // มีคำสั่งพ้นราชการหรือไม่ - if (salaries.length > 0 && salaries.some((s) => s.commandCode && - retireCommandCodes.includes(s.commandCode))) { + if ( + salaries.length > 0 && + salaries.some((s) => s.commandCode && retireCommandCodes.includes(s.commandCode)) + ) { // กรองข้อมูลซ้ำตาม commandDateAffect - const uniqueSalaries = salaries.filter((item, index, self) => - index === self.findIndex((t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime()) + const uniqueSalaries = salaries.filter( + (item, index, self) => + index === + self.findIndex( + (t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime(), + ), ); // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" @@ -1746,7 +1752,7 @@ export class ProfileController extends Controller { retires.push({ date: `${startDateStr} - ${endDateStr}`, detail: detail || "-", - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-", }); } } @@ -12008,4 +12014,90 @@ export class ProfileController extends Controller { return new HttpSuccess(); } + + /** + * API ข้อมูลทะเบียนประวัติตาม keycloak สำหรับเช็คอินเข้าใช้งานระบบ + * + * @summary ข้อมูลทะเบียนประวัติตาม keycloak สำหรับเช็คอินเข้าใช้งานระบบ + * + */ + @Get("keycloak/position-checkin") + async getProfileByKeycloakForCheckin(@Request() request: { user: Record }) { + const userSub = request.user.sub; + const relations = [ + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ]; + + const [officerProfile, orgRevisionPublish] = await Promise.all([ + this.profileRepo.findOne({ + where: { keycloak: userSub }, + relations, + }), + this.orgRevisionRepo.findOne({ + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }), + ]); + + if (!orgRevisionPublish) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบแบบร่างโครงสร้าง"); + } + + let profile: any = officerProfile; + let profileType: "OFFICER" | "EMPLOYEE" = "OFFICER"; + + if (!profile) { + profile = await this.profileEmpRepo.findOne({ + where: { keycloak: userSub }, + relations, + }); + profileType = "EMPLOYEE"; + } + + if (!profile) { + if (request.user.role.includes("SUPER_ADMIN")) { + return new HttpSuccess(null); + } + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); + } + + const currentHolder = + profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevisionPublish.id) ?? null; + const root = currentHolder?.orgRoot ?? null; + const child1 = currentHolder?.orgChild1 ?? null; + const child2 = currentHolder?.orgChild2 ?? null; + const child3 = currentHolder?.orgChild3 ?? null; + const child4 = currentHolder?.orgChild4 ?? null; + + const _profile: any = { + profileId: profile.id, + keycloak: profile.keycloak, + prefix: profile.prefix, + avatar: profile.avatar, + profileType, + isProbation: profile.isProbation, + avatarName: profile.avatarName, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + root: root?.orgRootName ?? null, + child1: child1?.orgChild1Name ?? null, + child2: child2?.orgChild2Name ?? null, + child3: child3?.orgChild3Name ?? null, + child4: child4?.orgChild4Name ?? null, + privacyCheckin: profile.privacyCheckin, + privacyUser: profile.privacyUser, + privacyMgt: profile.privacyMgt, + ...(profileType !== "OFFICER" ? { type: profile.employeeClass } : {}), + }; + + return new HttpSuccess(_profile); + } }