Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c30c558a | ||
| 22ea54aeb3 | |||
| d3e1707cc2 | |||
| d10b7e11f0 | |||
| c356a06899 | |||
| 26732420c2 |
5 changed files with 285 additions and 22 deletions
134
CLAUDE.md
Normal file
134
CLAUDE.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
TypeScript REST API for **Bangkok Metropolitan Administration (BMA)** — a Human Resource
|
||||||
|
Management System (HRMS) module managing organization structure (โครงสร้างอัตรากำลัง),
|
||||||
|
positions, personnel profiles, salary/tenure calculations, and related HR workflows.
|
||||||
|
|
||||||
|
**Stack:** Express.js · TypeORM (MySQL) · tsoa (controller-first OpenAPI generation) · Keycloak ·
|
||||||
|
RabbitMQ · Redis · Elasticsearch · WebSocket (socket.io) · node-cron
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install # install deps (npm is canonical — CI/Docker use npm install, not pnpm)
|
||||||
|
npm run dev # dev server with hot reload (nodemon)
|
||||||
|
npm run build # tsoa spec-and-routes (regenerates src/routes.ts, src/swagger.json) + tsc
|
||||||
|
npm run check # tsc --noEmit
|
||||||
|
npm start # run compiled dist/app.js
|
||||||
|
npm run format # prettier --write .
|
||||||
|
|
||||||
|
npm run migration:generate src/migration/<name> # generate a TypeORM migration
|
||||||
|
npm run migration:run # run pending migrations
|
||||||
|
node scripts/clean-migration-fk-idx.js # strip FK_/idx_ lines from generated migrations (run after every generate)
|
||||||
|
|
||||||
|
npm test # jest
|
||||||
|
npm run test:watch # jest --watch
|
||||||
|
npm run test:coverage # jest --coverage
|
||||||
|
npx jest src/__tests__/unit/OrganizationController.spec.ts # run a single test file
|
||||||
|
```
|
||||||
|
|
||||||
|
A repo note (README) documents building/testing the release pipeline locally with `act`
|
||||||
|
(`act workflow_dispatch -W .github/workflows/release.yaml ...`), but the actual CI is Forgejo
|
||||||
|
Actions (`.forgejo/workflows/`) and an OneDev buildspec (`.onedev-buildspec.yml`); both run
|
||||||
|
`npm install && npm run build` and build `docker/Dockerfile`.
|
||||||
|
|
||||||
|
**Note:** despite `pnpm-lock.yaml` being present in the repo, all CI/Docker paths use `npm`. Use
|
||||||
|
`npm` for scripts and dependency changes unless told otherwise.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Request flow: tsoa controllers → (optional) services → TypeORM repositories
|
||||||
|
|
||||||
|
- Routes are **not** hand-written. `tsoa.json` globs `src/controllers/**/*Controller.ts` and
|
||||||
|
`npm run build` regenerates `src/routes.ts` (Express route registration, called via
|
||||||
|
`RegisterRoutes(app)` in `src/app.ts`) and `src/swagger.json` from tsoa decorators. **Any time
|
||||||
|
you add/change a controller method or its decorators, you must run `npm run build`** (or at
|
||||||
|
least `tsoa spec-and-routes`) or the route/swagger changes won't take effect.
|
||||||
|
- In practice most controllers get their repositories directly via
|
||||||
|
`AppDataSource.getRepository(Entity)` in the constructor/method body rather than going through a
|
||||||
|
service layer — the codebase is controller-heavy (`OrganizationController.ts`,
|
||||||
|
`ImportDataController.ts`, `PositionController.ts`, `ReportController.ts` are each hundreds of
|
||||||
|
KB). `src/services/` exists for genuinely cross-cutting or heavier business logic (org command
|
||||||
|
execution, salary/tenure batch jobs, RabbitMQ, WebSocket) but is not a strict mandatory layer for
|
||||||
|
every endpoint.
|
||||||
|
- All entities (`src/entities/`) extend `EntityBase` (`src/entities/base/Base.ts`), which supplies
|
||||||
|
`id` (UUID), `createdAt`/`lastUpdatedAt`, and creator/updater id+name audit columns — every table
|
||||||
|
is audited by default. `src/entities/mis/` holds legacy/external MIS-system table mappings
|
||||||
|
(read-mostly, prefixed `HR_*`); `src/entities/view/` holds TypeORM entities mapped onto SQL
|
||||||
|
views (`viewCurrentTenure*`, `viewDirector*`, etc.) used for computed/reporting reads.
|
||||||
|
- Responses are wrapped in `HttpSuccess` (`src/interfaces/http-success.ts`, `{status, message,
|
||||||
|
result}`, Thai success message by default) or thrown as `HttpError` (`src/interfaces/
|
||||||
|
http-error.ts`, carries an `HttpStatus` + Thai message) — the global error middleware
|
||||||
|
(`src/middlewares/error.ts`) catches thrown `HttpError`/exceptions and formats the response.
|
||||||
|
`src/interfaces/http-status.ts` is the status-code enum used everywhere instead of magic numbers.
|
||||||
|
|
||||||
|
### Auth (three schemes, selected per-route via `@Security(...)`)
|
||||||
|
|
||||||
|
All resolved in `expressAuthentication` (`src/middlewares/auth.ts`), which tsoa calls per-request
|
||||||
|
based on the controller's `@Security` decorator:
|
||||||
|
|
||||||
|
- `bearerAuth` — Keycloak JWT in `Authorization: Bearer <token>`. Verified either **offline**
|
||||||
|
(`AUTH_PUBLIC_KEY`, local RS256 verification via `fast-jwt`) or **online** (`AUTH_REALM_URL`
|
||||||
|
userinfo endpoint), selected by `AUTH_PREFERRED_MODE`. Populates `req.app.locals.logData` with
|
||||||
|
user/org-tree ids from the token for logging. Type: `RequestWithUser` (`src/middlewares/user.ts`).
|
||||||
|
- `webServiceAuth` — `X-API-Key` header, resolved against the `ApiKey` entity in
|
||||||
|
`src/middlewares/authWebService.ts` (looks up allowed `apiNames`/org scope). Type:
|
||||||
|
`RequestWithUserWebService`.
|
||||||
|
- `internalAuth` — `api-key`/`api_key`/`apikey` header checked against the `API_KEY` env var
|
||||||
|
(`src/middlewares/authInternal.ts`), for trusted internal services (e.g. the .NET HRMS system).
|
||||||
|
- If `NODE_ENV !== "production"` and `AUTH_BYPASS` is set, auth is skipped entirely
|
||||||
|
(`{preferred_username: "bypassed"}`) — dev/test convenience only.
|
||||||
|
- Role gating within `bearerAuth` routes uses `authRole()` (`src/middlewares/role.ts`) and the
|
||||||
|
`permission` helper (`src/interfaces/permission.ts`) which calls out to an external `/org/permission`
|
||||||
|
check via `CallAPI`.
|
||||||
|
|
||||||
|
### Background work
|
||||||
|
|
||||||
|
- **Cron jobs** are all registered inline in `src/app.ts` via `node-cron` (6-field: seconds
|
||||||
|
included), each wrapping a controller/service call in try/catch that only logs on failure —
|
||||||
|
daily org revision cache refresh, retirement status updates (Oct 1st), org DNA sync, tenure
|
||||||
|
recalculation, and posting retirement data to an external "Exprofile" system.
|
||||||
|
- **RabbitMQ** (`src/services/rabbitmq.ts`) publishes org-structure change events
|
||||||
|
(`sendToQueueOrg` / `sendToQueueOrgDraft`); connects with infinite retry
|
||||||
|
(`setTimeout(runMessageQueue, 1000)` on failure) — don't add redundant retry logic around it.
|
||||||
|
Fire-and-forget from callers; don't block HTTP responses on publish.
|
||||||
|
- **WebSocket** (`src/services/webSocket.ts`, `initWebSocket()`) for real-time push, initialized
|
||||||
|
before the HTTP server starts listening.
|
||||||
|
- **`OrgStructureCache`** (`src/utils/OrgStructureCache.ts`) is an in-memory TTL cache (30 min) for
|
||||||
|
org-tree reads, keyed by revision+root id, initialized/destroyed alongside the app lifecycle. Use
|
||||||
|
it instead of re-querying the same org structure repeatedly within a request.
|
||||||
|
- **`LogMemoryStore`** (`src/utils/LogMemoryStore.ts`) plus `src/middlewares/logs.ts` build a
|
||||||
|
per-request log sequence (including DB queries logged via the custom TypeORM `Logger` in
|
||||||
|
`src/database/data-source.ts`) — this is what's shipped to Elasticsearch for auditing.
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- MySQL via TypeORM, connection pool configured in `src/database/data-source.ts`
|
||||||
|
(`connectionLimit`, `poolSize`, `maxQueryExecutionTime` all env-tunable). `synchronize` is
|
||||||
|
always `false` — schema changes go through migrations only.
|
||||||
|
- Timezone is pinned to Bangkok (`+07:00`) at the connection level; store/compare datetimes
|
||||||
|
accordingly rather than assuming UTC.
|
||||||
|
- After `migration:generate`, always run `node scripts/clean-migration-fk-idx.js` to strip
|
||||||
|
auto-generated `FK_*`/`idx_*` lines from the migration's `up`/`down` — this is a hard project
|
||||||
|
convention, not optional cleanup.
|
||||||
|
|
||||||
|
### Data dictionary
|
||||||
|
|
||||||
|
`docs/data-dictionary/` holds a generated schema data dictionary (`.docx`); the `data-dictionary`
|
||||||
|
skill / `scripts/generate-docx.py` regenerate it from `docs/data-dictionary/generate-prompt.md`.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Files/classes: PascalCase (`OrganizationController.ts`). Variables/functions: camelCase.
|
||||||
|
- DB column comments and HTTP error/success messages are written in **Thai** — keep this
|
||||||
|
consistent when adding columns or throwing `HttpError`.
|
||||||
|
- Services must not import from `controllers/`; keep services HTTP-agnostic (no `HttpError`/
|
||||||
|
`HttpSuccess` imports in `src/services/`).
|
||||||
|
- Don't use `moment` for new code (native `Date`/`Intl` only — `moment` remains for legacy call
|
||||||
|
sites). Don't bypass tsoa validation with manual `req.body` casting.
|
||||||
|
- `tsconfig.json` excludes `src/__tests__/**` and `*.spec.ts`/`*.test.ts` from the production
|
||||||
|
build; tests only run under `ts-jest` via `jest.config.js` (path alias `@/` → `src/`).
|
||||||
|
|
@ -23,7 +23,7 @@ interface CachedToken {
|
||||||
}
|
}
|
||||||
const API_URL_BANGKOK = "https://exprofile.bangkok.go.th/API";
|
const API_URL_BANGKOK = "https://exprofile.bangkok.go.th/API";
|
||||||
const clientId = "e5f6ad6ce374177eef023bf5d0c018b6";
|
const clientId = "e5f6ad6ce374177eef023bf5d0c018b6";
|
||||||
const clientSecret = "5EhOvN5DwHOKakupqT9FmCk7MOwpT3zLqLPkPh4ZhJpxBN2nMG@2022";
|
const clientSecret = "5EhOvN5DwHOKakupqT9FmCk7MOwpT3zLqLPkPh4ZhJpxBN2nMG";
|
||||||
|
|
||||||
class TokenCache {
|
class TokenCache {
|
||||||
private static cache: Map<string, CachedToken> = new Map();
|
private static cache: Map<string, CachedToken> = new Map();
|
||||||
|
|
@ -43,6 +43,35 @@ class TokenCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// network/DNS error codes ที่ถือว่าเป็น transient (ควร retry)
|
||||||
|
const TRANSIENT_NETWORK_CODES = [
|
||||||
|
"EAI_AGAIN", // DNS lookup ล้มเหลวชั่วคราว
|
||||||
|
"ENOTFOUND",
|
||||||
|
"ECONNRESET",
|
||||||
|
"ETIMEDOUT",
|
||||||
|
"ECONNREFUSED",
|
||||||
|
"EHOSTUNREACH",
|
||||||
|
"ENETUNREACH",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ตรวจว่า error ควร retry: ครอบ network/DNS (ไม่มี response หรือ code ตรง TRANSIENT) และ HTTP 5xx
|
||||||
|
function isTransientError(error: any): boolean {
|
||||||
|
if (!error) return false;
|
||||||
|
if (!error.response || TRANSIENT_NETWORK_CODES.includes(error.code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return error.response?.status >= 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// หน่วงเวลา (backoff) ระหว่าง retry — ใช้รูปแบบ setTimeout เดียวกับ keycloak/index.ts
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// config retry สำหรับเรียก Exprofile
|
||||||
|
const EXPROFILE_MAX_RETRIES = 3; // จากเดิม 2
|
||||||
|
const EXPROFILE_BACKOFF_BASE_MS = 1000; // backoff = base * 2^retryCount → 1s, 2s
|
||||||
|
|
||||||
@Route("api/v1/org/ex/retirement")
|
@Route("api/v1/org/ex/retirement")
|
||||||
@Tags("ExRetirement")
|
@Tags("ExRetirement")
|
||||||
@Security("bearerAuth")
|
@Security("bearerAuth")
|
||||||
|
|
@ -60,7 +89,7 @@ export class ExRetirementController extends Controller {
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 2;
|
const maxRetries = EXPROFILE_MAX_RETRIES;
|
||||||
|
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -92,11 +121,14 @@ export class ExRetirementController extends Controller {
|
||||||
// return res.data;
|
// return res.data;
|
||||||
return new HttpSuccess(res.data.data);
|
return new HttpSuccess(res.data.data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
|
if (isTransientError(error) && retryCount < maxRetries - 1) {
|
||||||
TokenCache.delete(`${clientId}:${clientSecret}`);
|
TokenCache.delete(`${clientId}:${clientSecret}`);
|
||||||
|
await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount);
|
||||||
retryCount++;
|
retryCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// log error message
|
||||||
|
console.error('getData getOfficerRetireData error:', error);
|
||||||
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +137,7 @@ export class ExRetirementController extends Controller {
|
||||||
@Get("/document/{documentId}")
|
@Get("/document/{documentId}")
|
||||||
async getDocument(@Path("documentId") officerDocumentID: string, @Request() req: any) {
|
async getDocument(@Path("documentId") officerDocumentID: string, @Request() req: any) {
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 2;
|
const maxRetries = EXPROFILE_MAX_RETRIES;
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
try {
|
try {
|
||||||
const token = await getToken(clientId, clientSecret);
|
const token = await getToken(clientId, clientSecret);
|
||||||
|
|
@ -134,11 +166,13 @@ export class ExRetirementController extends Controller {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
|
if (isTransientError(error) && retryCount < maxRetries - 1) {
|
||||||
TokenCache.delete(`${clientId}:${clientSecret}`);
|
TokenCache.delete(`${clientId}:${clientSecret}`);
|
||||||
|
await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount);
|
||||||
retryCount++;
|
retryCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
console.error('getData document error:', error);
|
||||||
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +202,13 @@ async function getToken(ClientID: string, ClientSecret: string): Promise<string>
|
||||||
TokenCache.set(cacheKey, token);
|
TokenCache.set(cacheKey, token);
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject({ message: "Error occurred", error });
|
// log แบบกระชับ (ลด log noise จากการ dump object ใหญ่ตามที่เห็นใน log จริง)
|
||||||
|
console.error(
|
||||||
|
"getToken error:",
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}` : error,
|
||||||
|
);
|
||||||
|
// โยน AxiosError ตัวจริงออกไปให้ caller อ่าน code / response / status เพื่อตัดสินใจ retry
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,7 +234,7 @@ export async function PostRetireToExprofile(
|
||||||
}
|
}
|
||||||
|
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 2;
|
const maxRetries = EXPROFILE_MAX_RETRIES;
|
||||||
|
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -231,8 +271,9 @@ export async function PostRetireToExprofile(
|
||||||
|
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
|
if (isTransientError(error) && retryCount < maxRetries - 1) {
|
||||||
TokenCache.delete(`${clientId}:${clientSecret}`);
|
TokenCache.delete(`${clientId}:${clientSecret}`);
|
||||||
|
await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount);
|
||||||
retryCount++;
|
retryCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +292,7 @@ export async function PostRetireToExprofile(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error('importOfficerRetireData error', error);
|
||||||
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11451,11 +11451,10 @@ export class ProfileController extends Controller {
|
||||||
system?: string;
|
system?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
// comment ออกก่อนเพราะยังไม่ได้ใช้
|
// ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ
|
||||||
// // ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ
|
let _system: string = "SYS_REGISTRY_OFFICER";
|
||||||
// let _system: string = "SYS_REGISTRY_OFFICER";
|
if (body.system) _system = body.system;
|
||||||
// if (body.system) _system = body.system;
|
let _data = await new permission().PermissionOrgList(request, _system);
|
||||||
// let _data = await new permission().PermissionOrgList(request, _system);
|
|
||||||
const findRevision = await this.orgRevisionRepo.findOne({
|
const findRevision = await this.orgRevisionRepo.findOne({
|
||||||
where: { orgRevisionIsCurrent: true },
|
where: { orgRevisionIsCurrent: true },
|
||||||
});
|
});
|
||||||
|
|
@ -11500,10 +11499,50 @@ export class ProfileController extends Controller {
|
||||||
.leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
|
.leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
|
||||||
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
|
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
|
||||||
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
|
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
|
||||||
.where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", {
|
.where("profile.isActive = :isActive AND profile.isDelete = :isDelete", {
|
||||||
isActive: false,
|
isActive: true,
|
||||||
isDelete: true,
|
isDelete: false,
|
||||||
})
|
})
|
||||||
|
.andWhere(
|
||||||
|
_data.root != undefined && _data.root != null
|
||||||
|
? _data.root[0] != null
|
||||||
|
? `current_holders.orgRootId IN (:...root)`
|
||||||
|
: `current_holders.orgRootId is null`
|
||||||
|
: "1=1",
|
||||||
|
{ root: _data.root },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child1 != undefined && _data.child1 != null
|
||||||
|
? _data.child1[0] != null
|
||||||
|
? `current_holders.orgChild1Id IN (:...child1)`
|
||||||
|
: `current_holders.orgChild1Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child1: _data.child1 },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child2 != undefined && _data.child2 != null
|
||||||
|
? _data.child2[0] != null
|
||||||
|
? `current_holders.orgChild2Id IN (:...child2)`
|
||||||
|
: `current_holders.orgChild2Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child2: _data.child2 },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child3 != undefined && _data.child3 != null
|
||||||
|
? _data.child3[0] != null
|
||||||
|
? `current_holders.orgChild3Id IN (:...child3)`
|
||||||
|
: `current_holders.orgChild3Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child3: _data.child3 },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child4 != undefined && _data.child4 != null
|
||||||
|
? _data.child4[0] != null
|
||||||
|
? `current_holders.orgChild4Id IN (:...child4)`
|
||||||
|
: `current_holders.orgChild4Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child4: _data.child4 },
|
||||||
|
)
|
||||||
.andWhere(
|
.andWhere(
|
||||||
new Brackets((qb) => {
|
new Brackets((qb) => {
|
||||||
qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` });
|
qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` });
|
||||||
|
|
|
||||||
|
|
@ -6172,6 +6172,7 @@ export class ProfileEmployeeController extends Controller {
|
||||||
*/
|
*/
|
||||||
@Post("search-personal-no-keycloak")
|
@Post("search-personal-no-keycloak")
|
||||||
async getProfileBySearchKeywordNoKeyCloak(
|
async getProfileBySearchKeywordNoKeyCloak(
|
||||||
|
@Request() request: RequestWithUser,
|
||||||
@Query("page") page: number = 1,
|
@Query("page") page: number = 1,
|
||||||
@Query("pageSize") pageSize: number = 10,
|
@Query("pageSize") pageSize: number = 10,
|
||||||
@Body()
|
@Body()
|
||||||
|
|
@ -6181,6 +6182,10 @@ export class ProfileEmployeeController extends Controller {
|
||||||
system?: string;
|
system?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
// ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ
|
||||||
|
let _system: string = "SYS_REGISTRY_EMP";
|
||||||
|
if (body.system) _system = body.system;
|
||||||
|
let _data = await new permission().PermissionOrgList(request, _system);
|
||||||
const findRevision = await this.orgRevisionRepo.findOne({
|
const findRevision = await this.orgRevisionRepo.findOne({
|
||||||
where: { orgRevisionIsCurrent: true },
|
where: { orgRevisionIsCurrent: true },
|
||||||
});
|
});
|
||||||
|
|
@ -6225,10 +6230,50 @@ export class ProfileEmployeeController extends Controller {
|
||||||
.leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
|
.leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
|
||||||
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
|
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
|
||||||
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
|
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
|
||||||
.where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", {
|
.where("profile.isActive = :isActive AND profile.isDelete = :isDelete", {
|
||||||
isActive: false,
|
isActive: true,
|
||||||
isDelete: true,
|
isDelete: false,
|
||||||
})
|
})
|
||||||
|
.andWhere(
|
||||||
|
_data.root != undefined && _data.root != null
|
||||||
|
? _data.root[0] != null
|
||||||
|
? `current_holders.orgRootId IN (:...root)`
|
||||||
|
: `current_holders.orgRootId is null`
|
||||||
|
: "1=1",
|
||||||
|
{ root: _data.root },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child1 != undefined && _data.child1 != null
|
||||||
|
? _data.child1[0] != null
|
||||||
|
? `current_holders.orgChild1Id IN (:...child1)`
|
||||||
|
: `current_holders.orgChild1Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child1: _data.child1 },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child2 != undefined && _data.child2 != null
|
||||||
|
? _data.child2[0] != null
|
||||||
|
? `current_holders.orgChild2Id IN (:...child2)`
|
||||||
|
: `current_holders.orgChild2Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child2: _data.child2 },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child3 != undefined && _data.child3 != null
|
||||||
|
? _data.child3[0] != null
|
||||||
|
? `current_holders.orgChild3Id IN (:...child3)`
|
||||||
|
: `current_holders.orgChild3Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child3: _data.child3 },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
_data.child4 != undefined && _data.child4 != null
|
||||||
|
? _data.child4[0] != null
|
||||||
|
? `current_holders.orgChild4Id IN (:...child4)`
|
||||||
|
: `current_holders.orgChild4Id is null`
|
||||||
|
: "1=1",
|
||||||
|
{ child4: _data.child4 },
|
||||||
|
)
|
||||||
.andWhere(
|
.andWhere(
|
||||||
new Brackets((qb) => {
|
new Brackets((qb) => {
|
||||||
qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` });
|
qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` });
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,6 @@ export class KeycloakController extends Controller {
|
||||||
private profileRepo = AppDataSource.getRepository(Profile);
|
private profileRepo = AppDataSource.getRepository(Profile);
|
||||||
private profileEmpRepo = AppDataSource.getRepository(ProfileEmployee);
|
private profileEmpRepo = AppDataSource.getRepository(ProfileEmployee);
|
||||||
private posMasterRepository = AppDataSource.getRepository(PosMaster);
|
private posMasterRepository = AppDataSource.getRepository(PosMaster);
|
||||||
private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
|
|
||||||
private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak);
|
private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak);
|
||||||
|
|
||||||
@Get("user/{id}")
|
@Get("user/{id}")
|
||||||
|
|
@ -387,12 +386,16 @@ export class KeycloakController extends Controller {
|
||||||
);
|
);
|
||||||
if (!result) throw new Error("Failed. Cannot remove user's role.");
|
if (!result) throw new Error("Failed. Cannot remove user's role.");
|
||||||
|
|
||||||
if (profileId) {
|
// delete authRoleId in posMaster if roleId is "f1fff8db-0795-47c1-9952-f3c18d5b6172"
|
||||||
|
if (profileId && roleId === "f1fff8db-0795-47c1-9952-f3c18d5b6172") {
|
||||||
|
let _null: any = null;
|
||||||
|
const authRoleId = _null;
|
||||||
|
console.log(`Clearing authRoleId on position records for profile ${profileId}`);
|
||||||
try {
|
try {
|
||||||
await this.posMasterRepository
|
await this.posMasterRepository
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update(PosMaster)
|
.update(PosMaster)
|
||||||
.set({ authRoleId: '' })
|
.set({ authRoleId: authRoleId })
|
||||||
.where("current_holderId = :profileId", { profileId })
|
.where("current_holderId = :profileId", { profileId })
|
||||||
.execute();
|
.execute();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue