Compare commits

..

52 commits

Author SHA1 Message Date
5727c79cc2 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m1s
2026-04-08 15:14:05 +07:00
2bcd7506a4 ตัดชื่อไฟล์ยาวเกิน #2397 2026-04-08 15:13:29 +07:00
adisak
31c16befa7 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-04-07 18:12:52 +07:00
adisak
e6779f8ec7 2365 ชั่วคราว 2026-04-07 18:12:34 +07:00
adisak
ce4a8aa94a Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m7s
2026-04-07 17:44:12 +07:00
adisak
e4e7edff3a #2365 2026-04-07 17:43:36 +07:00
harid
9b01ff3a04 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 54s
2026-04-03 11:53:32 +07:00
harid
e0fae329ee fix ไม่บันทึก log (Endpoint ไฟล์) #223 2026-04-03 11:51:38 +07:00
harid
523bc0aa81 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-03-26 12:20:58 +07:00
harid
6c60c3636c แก้ไข middleware log ให้รองรับกรณี response เป็น array #223 2026-03-26 12:20:26 +07:00
harid
161e3ba71c Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 58s
2026-03-23 09:37:56 +07:00
harid
a22d4cf515 แก้ไข middleware log ให้ใช้จาก token #223 2026-03-23 09:37:27 +07:00
harid
f5766f302d build tag 2026-03-05 14:02:28 +07:00
fc0304c221 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
* develop:
2026-02-20 17:32:19 +07:00
d45980e434 Merge branch 'develop' of github.com:Frappet/bma-ehr-salary into develop
* 'develop' of github.com:Frappet/bma-ehr-salary:
  แก้ไขสิทธิ์ PARENT ให้เห็นข้อมูลทั้งหมดทุกหน่วยงาน #54
2026-02-20 17:32:03 +07:00
harid
a45faaff8f Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m11s
2026-02-19 15:15:49 +07:00
harid
4f841566d7 แก้ไขสิทธิ์ PARENT ให้เห็นข้อมูลทั้งหมดทุกหน่วยงาน #54 2026-02-19 15:15:16 +07:00
ab5e915a59 Merge branch 'dev' of https://192.168.1.60/hrms-bangkok/hrms-api-salary into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m1s
* 'dev' of https://192.168.1.60/hrms-bangkok/hrms-api-salary:
2026-01-29 11:36:24 +07:00
7fd6531f53 Merge branch 'develop' into dev
* develop:
  add: checkRootDna
2026-01-29 11:35:58 +07:00
418cad4817 Merge branch 'develop' of github.com:Frappet/bma-ehr-salary into develop
* 'develop' of github.com:Frappet/bma-ehr-salary:
  add: checkRootDna
  test script emp changetype-mutl
  test script change-multi
2026-01-29 00:43:28 +07:00
8ee823a4d8 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 55s
2026-01-28 17:46:22 +07:00
001a026e43 add: checkRootDna 2026-01-28 17:46:07 +07:00
1fec0fcc8c Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 59s
2026-01-19 14:27:10 +07:00
83964e1198 test script emp changetype-mutl 2026-01-19 14:26:56 +07:00
1682f8ec5a Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 2m4s
2026-01-19 13:40:16 +07:00
b47c37e325 test script change-multi 2026-01-19 13:21:46 +07:00
harid
7bc7645443 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 48s
2025-12-12 09:57:44 +07:00
kittapath
cbf5f2599d add permission brother 2025-12-12 01:37:27 +07:00
10ae9c0d13 revert รอแก้ไขคำนวน 2025-11-30 13:12:46 +07:00
dfb3e82676 test performace change/type multi 2025-11-30 12:45:54 +07:00
3aeb893d55 revert
Some checks failed
release / release (push) Failing after 8s
2025-11-28 17:59:44 +07:00
aeecbd8ae9 test type-multi
Some checks failed
release / release (push) Failing after 9s
2025-11-28 17:23:51 +07:00
0d639a0f8a Merge branch 'dev' of https://192.168.1.60/hrms-bangkok/hrms-api-salary into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 52s
* 'dev' of https://192.168.1.60/hrms-bangkok/hrms-api-salary:
2025-11-26 21:40:48 +07:00
cbcdae7b71 Merge branch 'develop' into dev
* develop:
  #2034
  #2030
2025-11-26 21:40:43 +07:00
72916bd518 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 50s
2025-11-25 17:14:39 +07:00
508cb56430 #2034 2025-11-25 17:14:10 +07:00
cd862df3b1 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 49s
2025-11-25 14:39:45 +07:00
6c7b7d333e Merge branch 'dev' of https://192.168.1.60/hrms-bangkok/hrms-api-salary into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 48s
* 'dev' of https://192.168.1.60/hrms-bangkok/hrms-api-salary:
2025-11-04 09:08:10 +07:00
0446269014 Merge branch 'develop' into dev
* develop:
  revert code
  add length name validate post("{subId}")
  fix
  fix to thorw
2025-11-04 09:08:05 +07:00
ae36f73ebe Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 59s
2025-10-31 18:57:23 +07:00
3d0f228b8e Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 59s
2025-10-31 18:52:12 +07:00
d8be027583 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 52s
2025-10-31 18:41:19 +07:00
f4a2993a32 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 52s
2025-10-31 18:33:14 +07:00
d5e7a721ac Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 49s
2025-10-31 16:34:42 +07:00
harid
d299f86f86 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 51s
2025-10-24 17:26:42 +07:00
6479cbe331 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 48s
* develop:
  Merge pull request #53 from Frappet/issue/#1883
  sort เงินเดือน/ค่าจ้าง
  sort
  update cronjob snapshot
2025-10-22 11:07:30 +07:00
1130b31ee5 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 53s
2025-09-30 16:06:32 +07:00
ffe9e03a91 Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 48s
2025-09-22 10:09:21 +07:00
DESKTOP-1R2VSQH\Lenovo ThinkPad E490
592a40ae5c feat: Add Discord Notification
Some checks failed
Build & Deploy on Dev / build (push) Failing after 38s
2025-09-16 14:36:51 +07:00
DESKTOP-1R2VSQH\Lenovo ThinkPad E490
261d05f88d Merge branch 'develop' into dev 2025-09-16 14:35:30 +07:00
6ec0085e31 Add .forgejo/workflows/ci-cd.yml 2025-09-10 11:57:57 +07:00
DESKTOP-1R2VSQH\Lenovo ThinkPad E490
248a38a610 forgejo 2025-09-09 09:47:02 +07:00
14 changed files with 1537 additions and 320 deletions

View file

@ -0,0 +1,50 @@
# /.forgejo/workflows/build.yml
name: Build
# on:
# push:
# tags:
# - "v[0-9]+.[0-9]+.[0-9]+"
# - "v[0-9]+.[0-9]+.[0-9]+*"
# workflow_dispatch:
env:
REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}
IMAGE_VERSION: build
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
ca=["/etc/ssl/certs/ca-certificates.crt"]
- name: Tag Version
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
echo "IMAGE_VERSION=${{ github.ref_name }}" | sed 's/v//g' >> $GITHUB_ENV
else
echo "IMAGE_VERSION=${{ env.IMAGE_VERSION }}-${{ github.run_number }}" >> $GITHUB_ENV
fi
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
platforms: linux/amd64
context: .
file: ./docker/Dockerfile
tags: ${{ env.CONTAINER_IMAGE_NAME }}:latest,${{ env.CONTAINER_IMAGE_NAME }}:${{ env.IMAGE_VERSION }}
push: true

View file

@ -0,0 +1,83 @@
# /.forgejo/workflows/build.yml
name: Build & Deploy on Dev
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
env:
REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}
IMAGE_VERSION: latest
DISCORD_WEBHOOK: ${{ vars.DISCORD_WEBHOOK }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
ca=["/etc/ssl/certs/ca-certificates.crt"]
- name: Tag Version
run: |
if [ "${{ github.ref_type }}" == "tag" ]; then
echo "IMAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
else
echo "IMAGE_VERSION=latest" >> $GITHUB_ENV
fi
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
platforms: linux/amd64
context: .
file: ./docker/Dockerfile
tags: ${{ env.CONTAINER_IMAGE_NAME }}:latest,${{ env.CONTAINER_IMAGE_NAME }}:${{ env.IMAGE_VERSION }}
push: true
- name: Remote Deploy
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ vars.SSH_DEPLOY_HOST }}
port: ${{ vars.SSH_DEPLOY_PORT }}
username: ${{ secrets.SSH_DEPLOY_USER }}
password: ${{ secrets.SSH_DEPLOY_PASSWORD }}
script: |
cd ~/repo
./replace-env.sh API_SALARY "${{ env.IMAGE_VERSION }}"
./deploy.sh hrms-api-salary
- name: Discord Notification
if: always()
run: |
STATUS="${{ job.status == 'success' && '✅ Success' || '❌ Failed' }}"
COLOR="${{ job.status == 'success' && '3066993' || '15158332' }}"
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -H "Content-Type: application/json" \
-X POST \
-d "{
\"embeds\": [{
\"title\": \"$STATUS\",
\"description\": \"**Build & Deploy**\\n- Image: \`${{ env.CONTAINER_IMAGE_NAME }}\`\\n- Version: \`${{ env.IMAGE_VERSION }}\`\\n- By: \`${{ github.actor }}\`\",
\"color\": $COLOR,
\"footer\": {
\"text\": \"Release Notification\",
\"icon_url\": \"https://example.com/success-icon.png\"
},
\"timestamp\": \"$TIMESTAMP\"
}]
}" \
${{ env.DISCORD_WEBHOOK }}

View file

@ -0,0 +1,29 @@
name: Build
on:
workflow_dispatch:
inputs:
version:
description: "Version to deploy"
type: string
required: false
default: "latest"
env:
IMAGE_VERSION: build
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Remote Deploy
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ vars.SSH_DEPLOY_HOST }}
port: ${{ vars.SSH_DEPLOY_PORT }}
username: ${{ secrets.SSH_DEPLOY_USER }}
password: ${{ secrets.SSH_DEPLOY_PASSWORD }}
script: |
cd ~/repo
./replace-env.sh API_SALARY "${{ inputs.version }}"
./deploy.sh hrms-api-salary "${{ inputs.version }}"

2
.gitignore vendored
View file

@ -131,3 +131,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.claude

View file

@ -22,7 +22,7 @@ import {
listFile,
updateFile,
} from "../services/edm";
import { s3DeleteFile, s3DownloadFile, s3ListFile, s3UploadFile } from "../services/minio";
import { s3DeleteFile, s3DownloadFile, s3ListFile, s3UploadFile, truncateFileName } from "../services/minio";
@Route("api/v1/salary/file/{name}/{group}")
@Security("bearerAuth")
@ -257,7 +257,7 @@ export class FileController extends Controller {
const map = fileList.map(async ({ fileName, ...props }) => [
fileName,
await createFile(path, fileName, props),
await createFile(path, truncateFileName(fileName), props),
]);
const result = await Promise.all(map).catch((e) =>
@ -607,7 +607,7 @@ export class SubFileController extends Controller {
const map = fileList.map(async ({ fileName, ...props }) => [
fileName,
await createFile(path, fileName, props),
await createFile(path, truncateFileName(fileName), props),
]);
const result = await Promise.all(map).catch((e) =>

File diff suppressed because it is too large Load diff

View file

@ -70,68 +70,68 @@ export class SalaryPeriodEmployeeController extends Controller {
const data = {
group1id:
salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
? null
: salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.id,
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.id,
group1IsClose:
salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
? null
: salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.isClose,
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP1" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.isClose,
group2id:
salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
? null
: salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.id,
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.id,
group2IsClose:
salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
) == null
? null
: salaryPeriod.salaryOrgEmployees &&
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.isClose,
salaryPeriod.salaryOrgEmployees.find(
(x) =>
x.group == "GROUP2" &&
x.rootId == body.rootId &&
x.snapshot == body.snapshot.toLocaleUpperCase(),
)?.isClose,
effectiveDate: salaryPeriod.effectiveDate,
period: salaryPeriod.period,
};
@ -430,7 +430,7 @@ export class SalaryPeriodEmployeeController extends Controller {
const Level = await this.posLevelRepository.findOne({
where: {
posTypeId: Type.id,
posLevelName: salaryProfile.posLevel,
posLevelName: salaryProfile.posLevel?.split("/")[0],
},
});
if (!Level) {
@ -750,7 +750,7 @@ export class SalaryPeriodEmployeeController extends Controller {
const Level = await this.posLevelRepository.findOne({
where: {
posTypeId: Type.id,
posLevelName: salaryProfile.posLevel,
posLevelName: salaryProfile.posLevel?.split("/")[0],
},
});
if (!Level) {
@ -762,7 +762,7 @@ export class SalaryPeriodEmployeeController extends Controller {
let type = salaryProfile.type;
salaryProfile = await this.calSalaryNew(type, salaryProfile);
salaryProfile.lastUpdateUserId = req.user.sub;
salaryProfile.lastUpdateFullName = req.user.name;
salaryProfile.lastUpdatedAt = new Date();
@ -879,8 +879,8 @@ export class SalaryPeriodEmployeeController extends Controller {
* @param {string} id profile Id
* @param {string} type NONE-> HAFT-> FULL->1 FULLHAFT->1.5
*/
@Post("change/type-multi")
async changeTypeMulti(
@Post("oldchange/type-multi")
async oldchangeTypeMulti(
@Body() body: { profileId: string[]; type: string; isReserve: boolean; remark?: string | null },
@Request() req: RequestWithUser,
) {
@ -941,7 +941,7 @@ export class SalaryPeriodEmployeeController extends Controller {
const Level = await this.posLevelRepository.findOne({
where: {
posTypeId: Type.id,
posLevelName: salaryProfile.posLevel,
posLevelName: salaryProfile.posLevel?.split("/")[0],
},
});
if (!Level) {
@ -952,7 +952,7 @@ export class SalaryPeriodEmployeeController extends Controller {
salaryProfile.remark = body.remark == null ? _null : body.remark;
let type = salaryProfile.type;
salaryProfile = await this.calSalary(type, salaryProfile);
salaryProfile = await this.calSalaryNew(type, salaryProfile);
salaryProfile.lastUpdateUserId = req.user.sub;
salaryProfile.lastUpdateFullName = req.user.name;
@ -1063,6 +1063,264 @@ export class SalaryPeriodEmployeeController extends Controller {
return new HttpSuccess();
}
private chunkArray<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
private async processEmployeeProfile(
profileId: string,
body: any,
req: RequestWithUser,
affectedOrgIds: Set<string>,
) {
let salaryProfile = await this.salaryProfileRepository.findOne({
relations: ["salaryOrg", "salaryOrg.salaryPeriod"],
where: { id: profileId },
});
if (!salaryProfile) {
throw new HttpError(
HttpStatusCode.NOT_FOUND,
"ไม่พบข้อมูลการขอเงินเดือนผู้ใช้งานนี้ในระบบ",
);
}
// ===== FULLHAFT CHECK (เดิม) =====
if (body.type === "FULLHAFT") {
if (salaryProfile.salaryOrg.salaryPeriod.period === "OCT") {
const checkPreviousType =
await this.salaryProfileRepository.findOne({
relations: ["salaryOrg", "salaryOrg.salaryPeriod"],
where: {
citizenId: salaryProfile.citizenId,
salaryOrg: {
salaryPeriod: {
period: "APR",
year: salaryProfile.salaryOrg.salaryPeriod.year,
},
snapshot: "SNAP2",
},
type: "FULL",
},
});
if (checkPreviousType) {
throw new HttpError(
HttpStatusCode.NOT_FOUND,
"ไม่สามารถเลื่อนขั้นบุคลากรเกิน 2 ขั้นต่อปีได้",
);
}
}
}
// ===== isReserve (เดิม) =====
salaryProfile.isReserve =
body.type === "FULL" ? body.isReserve : false;
// ===== Type & Level check (เดิม) =====
const Type = await this.posTypeRepository.findOne({
where: { posTypeName: salaryProfile.posType },
});
if (!Type) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทตำแหน่ง");
}
const Level = await this.posLevelRepository.findOne({
where: {
posTypeId: Type.id,
posLevelName: salaryProfile.posLevel?.split("/")[0],
},
});
if (!Level) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบระดับตำแหน่ง");
}
// ===== type / remark =====
salaryProfile.type = body.type;
salaryProfile.remark = body.remark == null ? null : body.remark;
// ===== CALC SALARY (เดิม 100%) =====
salaryProfile = await this.calSalaryNew(
salaryProfile.type,
salaryProfile,
);
// ===== audit =====
salaryProfile.lastUpdateUserId = req.user.sub;
salaryProfile.lastUpdateFullName = req.user.name;
salaryProfile.lastUpdatedAt = new Date();
const before = structuredClone(salaryProfile);
await this.salaryProfileRepository.save(salaryProfile, {
data: req,
});
setLogDataDiff(req, { before, after: salaryProfile });
affectedOrgIds.add(salaryProfile.salaryOrg.id);
}
private async recalculateSalaryOrgEmployee(
orgId: string,
req: RequestWithUser,
) {
const salaryOrg = await this.salaryOrgRepository.findOne({
where: { id: orgId },
relations: ["salaryProfiles", "salaryPeriod"],
});
if (!salaryOrg) return;
if (salaryOrg.snapshot !== "SNAP1") return;
// ===================== APR =====================
if (salaryOrg.salaryPeriod.period === "APR") {
const amountFullType =
await this.salaryProfileRepository.count({
where: {
salaryOrgId: salaryOrg.id,
type: "FULL",
},
});
salaryOrg.total = salaryOrg.salaryProfiles.length;
salaryOrg.fifteenPercent = Math.floor(
(salaryOrg.total * 15) / 100,
);
salaryOrg.fifteenPoint = (salaryOrg.total * 15) % 100;
salaryOrg.quantityUsed = amountFullType;
salaryOrg.remainQuota =
salaryOrg.fifteenPercent - amountFullType;
salaryOrg.lastUpdateUserId = req.user.sub;
salaryOrg.lastUpdateFullName = req.user.name;
salaryOrg.lastUpdatedAt = new Date();
await this.salaryOrgRepository.save(salaryOrg, { data: req });
return;
}
// ===================== OCT =====================
if (salaryOrg.salaryPeriod.period === "OCT") {
const totalProfileAmount = Extension.sumObjectValues(
salaryOrg.salaryProfiles,
"amount",
);
salaryOrg.currentAmount = totalProfileAmount;
salaryOrg.total = salaryOrg.salaryProfiles.length;
salaryOrg.sixPercentAmount = totalProfileAmount * 0.06;
// ===== APR SNAP2 spentAmount =====
let totalAmount = 0;
const salaryPeriodAPROld =
await this.salaryPeriodRepository.findOne({
where: {
year: salaryOrg.salaryPeriod.year,
period: "APR",
},
});
if (salaryPeriodAPROld) {
const salaryOrgSnap2Old =
await this.salaryOrgRepository.findOne({
where: {
salaryPeriodId: salaryPeriodAPROld.id,
rootId: salaryOrg.rootId,
group: salaryOrg.group,
snapshot: "SNAP2",
},
relations: ["salaryProfiles"],
});
totalAmount =
salaryOrgSnap2Old == null
? 0
: Extension.sumObjectValues(
salaryOrgSnap2Old.salaryProfiles,
"amountUse",
);
}
salaryOrg.spentAmount = totalAmount;
// ===== sumAmountUse (current OCT SNAP1) =====
const sumAmountUse =
await AppDataSource.getRepository(
SalaryProfileEmployee,
)
.createQueryBuilder("salaryProfileEmployee")
.select(
"SUM(salaryProfileEmployee.amountUse)",
"totalAmount",
)
.where({
salaryOrgId: salaryOrg.id,
type: In(["HAFT", "FULL", "FULLHAFT"]),
})
.getRawOne();
salaryOrg.useAmount =
sumAmountUse?.totalAmount ?? 0;
salaryOrg.remainingAmount =
salaryOrg.sixPercentAmount -
salaryOrg.useAmount -
salaryOrg.spentAmount;
salaryOrg.lastUpdateUserId = req.user.sub;
salaryOrg.lastUpdateFullName = req.user.name;
salaryOrg.lastUpdatedAt = new Date();
await this.salaryOrgRepository.save(salaryOrg, { data: req });
return;
}
}
@Post("change/type-multi")
async changeTypeMulti(
@Body()
body: {
profileId: string[];
type: string;
isReserve: boolean;
remark?: string | null;
},
@Request() req: RequestWithUser,
) {
await new permission().PermissionCreate(req, "SYS_WAGE");
body.type = body.type.toUpperCase();
const BATCH_SIZE = 20;
const affectedOrgIds = new Set<string>();
const batches = this.chunkArray(body.profileId, BATCH_SIZE);
for (const batch of batches) {
await Promise.all(
batch.map(profileId =>
this.processEmployeeProfile(
profileId,
body,
req,
affectedOrgIds,
),
),
);
}
// === recalc salaryOrg ทีเดียวต่อ org ===
for (const orgId of affectedOrgIds) {
await this.recalculateSalaryOrgEmployee(orgId, req);
}
return new HttpSuccess();
}
/**
* API
*
@ -1159,21 +1417,21 @@ export class SalaryPeriodEmployeeController extends Controller {
}),
)
if (body.sortBy) {
if(body.sortBy === "posLevel"){
query = query
.orderBy( `profile.posTypeShort`,body.descending ? "DESC" : "ASC")
.addOrderBy( `profile.posLevel`,body.descending ? "DESC" : "ASC");
}else{
query = query.orderBy(
`profile.${body.sortBy}`,
body.descending ? "DESC" : "ASC"
);
}
}else{
query = query.orderBy("profile.citizenId", "ASC")
.addOrderBy("profile.isReserve", "ASC")
if (body.sortBy) {
if (body.sortBy === "posLevel") {
query = query
.orderBy(`profile.posTypeShort`, body.descending ? "DESC" : "ASC")
.addOrderBy(`profile.posLevel`, body.descending ? "DESC" : "ASC");
} else {
query = query.orderBy(
`profile.${body.sortBy}`,
body.descending ? "DESC" : "ASC"
);
}
} else {
query = query.orderBy("profile.citizenId", "ASC")
.addOrderBy("profile.isReserve", "ASC")
}
const [salaryProfile, total] = await query
.skip((body.page - 1) * body.pageSize)
@ -1339,7 +1597,7 @@ export class SalaryPeriodEmployeeController extends Controller {
const posLevel = await this.posLevelRepository.findOne({
where: {
posTypeId: posType.id,
posLevelName: salaryProfile.posLevel,
posLevelName: salaryProfile.posLevel?.split("/")[0],
},
});
if (!posLevel) {
@ -2249,7 +2507,7 @@ export class SalaryPeriodEmployeeController extends Controller {
salaryProfile.positionSalaryAmountPer = 0;
salaryProfile.amountSpecial = 0;
}
} else {
} else {
if (
salaryFormula != null &&
salaryFormula.salaryMax != null &&
@ -2451,29 +2709,29 @@ export class SalaryPeriodEmployeeController extends Controller {
},
relations: ["salaryEmployee_"],
});
if (!salaryCurrentRanks) {
salaryCurrentRanks = await this.salaryRankRepository.findOne({
where: {
salaryMonth: MoreThanOrEqual(salaryProfile.amount),
},
order: {
salaryMonth: "ASC",
salaryMonth: "ASC",
},
relations: ["salaryEmployee_"],
});
}
if (salaryCurrentRanks) {
group = salaryCurrentRanks.salaryEmployee_.group;
step = salaryCurrentRanks.step;
}
}
//console.log("group", group);
//console.log("step", step);
//console.log("group", group);
//console.log("step", step);
if (type == "HAFT") {
step = step + 0.5;
stepUp = 0.5;
@ -2484,7 +2742,7 @@ export class SalaryPeriodEmployeeController extends Controller {
step = step + 1.5;
stepUp = 1.5;
}
//console.log("step+type", step);
//console.log("step+type", step);
//หาขั้นสูงสุดในกลุ่มนั้น
let salaryRankMax = await this.salaryRankRepository.findOne({
where: {
@ -2496,13 +2754,13 @@ export class SalaryPeriodEmployeeController extends Controller {
order: { step: "DESC" },
});
//console.log("salaryRankMax.step", salaryRankMax?.step);
//console.log("salaryProfile.amount", salaryProfile.amount);
//console.log("salaryFormula.salaryMax", salaryFormula?.salaryMax);
//console.log("salaryRankMax.step", salaryRankMax?.step);
//console.log("salaryProfile.amount", salaryProfile.amount);
//console.log("salaryFormula.salaryMax", salaryFormula?.salaryMax);
//เงินเดือนเกินตาราง
//****หา shot ที่ +ขั้น แล้วแก้เป็นหาเงินเดือนที่ใกล้เคียงกับขั้นผังเก่าก่อนแล้วค่อย +ขั้นที่เลื่อนเข้าไป ex.เงินตันที่ 20000 ไปหาผังใหม่ได้ใกล้เคียง 20100 ยึดตัวเลขนี้ไว้แล้วค่อย +ขั้นในผังใหม่ขึ้นไป
if (
salaryRankMax != null &&
salaryRankMax != null &&
step > salaryRankMax.step &&
(salaryFormula == null ||
(salaryFormula != null &&
@ -2510,8 +2768,8 @@ export class SalaryPeriodEmployeeController extends Controller {
salaryFormula.salaryMax != null &&
salaryFormula.salaryMax > salaryProfile.amount))
) {
//console.log("in function เกินตาราง");
//console.log("in function เกินตาราง");
group = group + 1;
//เงินเดือนในกลุ่มต่อไป
let salaryRankAmount = await this.salaryRankRepository.findOne({
@ -2533,7 +2791,7 @@ export class SalaryPeriodEmployeeController extends Controller {
// (step - (salaryRankMax == null ? 0 : salaryRankMax.step) - 0.5);
step = (salaryRankAmount == null ? 1 : salaryRankAmount.step) + stepUp; //****หาขั้นของผังใหม่แล้ว + ด้วยขั้นที่ได้เลื่อน
//console.log("step in if", step);
//console.log("step in if", step);
}
let whereCondition: any = {

View file

@ -12,9 +12,9 @@ enum EmployeePosLevelAuthoritys {
export class EmployeePosLevel extends EntityBase {
@Column({
comment: "ชื่อระดับชั้นงาน",
type: "int",
length: 255,
})
posLevelName: number;
posLevelName: string;
@Column({
comment: "ระดับของระดับชั้นงาน",

View file

@ -134,7 +134,7 @@ export class SalaryProfileEmployee extends EntityBase {
comment: "ระดับตำแหน่ง",
default: null,
})
posLevel: number;
posLevel: string;
@Column({
type: "double",
@ -482,7 +482,7 @@ export class CreateSalaryProfileEmployee {
posType: string | null;
@Column()
posLevel: number | null;
posLevel: string | null;
@Column()
group: number | null;

View file

@ -94,6 +94,17 @@ class CheckAuth {
child4: null,
privilege: "ROOT",
};
} else if (privilege == "PARENT") {
data = {
// root: [x.orgRootId],
// child1: [null],
root: null,
child1: null,
child2: null,
child3: null,
child4: null,
privilege: "PARENT",
};
} else if (privilege == "CHILD") {
data = {
root: node >= 0 ? [x.orgRootId] : null,
@ -103,6 +114,15 @@ class CheckAuth {
child4: node >= 4 ? [x.orgChild4Id] : null,
privilege: "CHILD",
};
} else if (privilege == "BROTHER") {
data = {
// root: node >= 0 ? [x.orgRootId] : null,
root: node >= 0 ? [x.orgRootId] : null,
child1: node >= 2 ? [x.orgChild1Id] : null,
child2: node >= 3 ? [x.orgChild2Id] : null,
child3: node >= 4 ? [x.orgChild3Id] : null,
privilege: "BROTHER",
};
} else if (privilege == "NORMAL") {
data = {
root: [x.orgRootId],
@ -185,38 +205,70 @@ class CheckAuth {
}
public async checkOrg(token: any, keycloakId: string) {
const redisClient = await this.redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
})
const getAsync = promisify(redisClient.get).bind(redisClient)
try {
let reply = await getAsync("org_" + keycloakId)
if (reply != null) {
reply = JSON.parse(reply)
} else {
if (!keycloakId) throw new Error("No KeycloakId provided")
const x = await new CallAPI().GetData(
{
headers: { authorization: token },
},
`/org/permission/checkOrg/${keycloakId}`,
false
)
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const getAsync = promisify(redisClient.get).bind(redisClient);
try {
let reply = await getAsync("org_" + keycloakId);
if (reply != null) {
reply = JSON.parse(reply);
} else {
if (!keycloakId) throw new Error("No KeycloakId provided");
const x = await new CallAPI().GetData(
{
headers: { authorization: token },
},
`/org/permission/checkOrg/${keycloakId}`,
false,
);
const data = {
orgRootId: x.orgRootId,
orgChild1Id: x.orgChild1Id,
orgChild2Id: x.orgChild2Id,
orgChild3Id: x.orgChild3Id,
orgChild4Id: x.orgChild4Id,
}
const data = {
orgRootId: x.orgRootId,
orgChild1Id: x.orgChild1Id,
orgChild2Id: x.orgChild2Id,
orgChild3Id: x.orgChild3Id,
orgChild4Id: x.orgChild4Id,
};
return data
}
} catch (error) {
console.error("Error calling API:", error)
throw error
}
return data;
}
} catch (error) {
console.error("Error calling API:", error);
throw error;
}
}
public async checkRootDna(token: any, keycloakId: string) {
const redisClient = await this.redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const getAsync = promisify(redisClient.get).bind(redisClient);
try {
let reply = await getAsync("org_" + keycloakId);
if (reply != null) {
reply = JSON.parse(reply);
} else {
if (!keycloakId) throw new Error("No KeycloakId provided");
const x = await new CallAPI().GetData(
{
headers: { authorization: token },
},
`/org/dotnet/user-logs/${keycloakId}`,
false,
);
const data = {
rootDnaId: x.rootDnaId,
};
return data;
}
} catch (error) {
console.error("Error calling API:", error);
throw error;
}
}
public async PermissionCreate(req: RequestWithUser, system: string) {
return await this.Permission(req, system, "CREATE");

View file

@ -67,6 +67,16 @@ export async function expressAuthentication(
request.app.locals.logData.userName = payload.name;
request.app.locals.logData.user = payload.preferred_username;
// เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี)
request.app.locals.logData.profileId = payload.profileId ?? "";
request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? "";
request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? "";
request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? "";
request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? "";
request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? "";
request.app.locals.logData.empType = payload.empType ?? "";
request.app.locals.logData.prefix = payload.prefix ?? "";
return payload;
}

View file

@ -45,21 +45,35 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
if (level === 2 && res.statusCode < 400) return;
if (level === 3 && res.statusCode < 200) return;
const token = req.headers["authorization"];
let rootId = null;
// const token = req.headers["authorization"];
// let rootId = null;
try {
rootId = token
? await new permission().checkOrg(token, req.app.locals.logData.userId)
: null;
} catch (err) {
console.warn("Error fetching rootId:", err);
// try {
// rootId = token
// ? await new permission().checkRootDna(token, req.app.locals.logData.userId)
// : null;
// } catch (err) {
// console.warn("Error fetching rootId:", err);
// }
// Get rootId from token
const rootId = req.app.locals.logData?.orgRootDnaId;
let _msg = data?.message;
if (!_msg) {
if (res.statusCode >= 500) {
_msg = "ไม่สำเร็จ";
} else if (res.statusCode >= 400) {
_msg = "พบข้อผิดพลาด";
} else if (res.statusCode >= 200) {
_msg = "สำเร็จ";
}
}
const obj = {
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
ip: req.ip,
rootId: rootId?.orgRootId ?? null,
rootId: rootId ?? null,
systemName: "salary",
startTimeStamp: timestamp,
endTimeStamp: new Date().toISOString(),
@ -68,7 +82,8 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
method: req.method,
endpoint: req.url,
responseCode: String(res.statusCode === 304 ? 200 : res.statusCode),
responseDescription: data?.message,
// responseDescription: data?.message,
responseDescription: _msg,
input: level === 4 ? JSON.stringify(req.body, null, 2) : undefined,
output: level === 4 ? JSON.stringify(data, null, 2) : undefined,
...req.app.locals.logData,

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdateTablePosLevelToString20260407112136 implements MigrationInterface {
name = 'UpdateTablePosLevelToString20260407112136'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`salaryProfileEmployee\` CHANGE \`posLevel\` \`posLevel\` varchar(255) NULL COMMENT 'ระดับตำแหน่ง'`);
await queryRunner.query(`ALTER TABLE \`employeePosLevel\` CHANGE \`posLevelName\` \`posLevelName\` varchar(255) NOT NULL COMMENT 'ชื่อระดับชั้นงาน'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`salaryProfileEmployee\` CHANGE \`posLevel\` \`posLevel\` double NULL COMMENT 'ระดับตำแหน่ง'`);
await queryRunner.query(`ALTER TABLE \`employeePosLevel\` CHANGE \`posLevelName\` \`posLevelName\` int NOT NULL COMMENT 'ชื่อระดับชั้นงาน'`);
}
}

View file

@ -16,6 +16,26 @@ const minio = new Client({
export default minio;
/**
* limit
* @param fileName -
* @param maxLength - (default: 50 )
* @returns
*/
export function truncateFileName(fileName: string, maxLength: number = 50): string {
const dot = fileName.lastIndexOf(".");
const name = dot !== -1 && !fileName.startsWith(".") ? fileName.slice(0, dot) : fileName;
const ext = dot !== -1 && !fileName.startsWith(".") ? fileName.slice(dot) : "";
if (name.length <= maxLength) {
return fileName;
}
// ตัดชื่อและเติม "..." ตามด้วย 5 ตัวสุดท้ายของชื่อเดิม
const truncated = name.slice(0, maxLength - 8) + "..." + name.slice(-5);
return truncated + ext;
}
function exception(e: any): never {
console.error(e);
throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
@ -74,6 +94,11 @@ export async function s3DeleteFolder(path: string) {
export async function s3UploadFile(replace: boolean, ...pathname: string[]) {
if (!pathname.length) return;
// ตัดชื่อไฟล์ถ้ายาวเกิน
const originalFileName = pathname.at(-1) as string;
const truncatedFileName = truncateFileName(originalFileName);
pathname = [...pathname.slice(0, -1), truncatedFileName];
if (!replace) {
const list = await s3ListFile(...pathname.slice(0, -1)).catch(exception);
@ -146,7 +171,7 @@ export async function s3DownloadFile(...pathname: string[]) {
}
export async function duplicateAvatarFile(refId: string, prefix: string, fileName: string) {
try {
try {
await minio.statObject(MINIO_BUCKET, refId);
const avatar = `${prefix}/${fileName}`;
await minio.copyObject(
@ -159,4 +184,4 @@ export async function duplicateAvatarFile(refId: string, prefix: string, fileNam
}
}
export const fileLocation = (...path: string[]) => path.join("/");
export const fileLocation = (...path: string[]) => path.join("/");