Merge branch 'dev'

This commit is contained in:
Methapon2001 2024-04-19 10:25:22 +07:00
commit 2b843d7e13
44 changed files with 4242 additions and 105 deletions

View file

@ -7,4 +7,15 @@ KC_SERVICE_ACCOUNT_SECRET=
APP_HOST=0.0.0.0
APP_PORT=3000
MINIO_HOST=192.168.1.20
MINIO_PORT=9000
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=jws-dev
ELASTICSEARCH_PROTOCOL=http
ELASTICSEARCH_HOST=192.168.1.20
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_INDEX=jws-log-index
DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public

74
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,74 @@
name: release-test
run-name: release-test ${{ github.actor }}
on:
push:
tags:
- "version-[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
env:
REGISTRY: docker.frappet.com
IMAGE_NAME: jws/jws-backend
DEPLOY_HOST: 49.0.91.80
COMPOSE_PATH: /home/frappet/docker/jws
jobs:
# act workflow_dispatch -W .github/workflows/release.yaml --input IMAGE_VER=test-v1 -s DOCKER_USER=sorawit -s DOCKER_PASS=P@ssword -s SSH_PASSWORD=P@ssw0rd
release-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# skip Set up QEMU because it fail on act and container
# Gen Version try to get version from tag or inut
- name: Set output tags
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Gen Version
id: gen_ver
run: |
if [[ $GITHUB_REF == 'refs/tags/'* ]]; then
IMAGE_VER=${{ steps.vars.outputs.tag }}
else
IMAGE_VER=${{ github.event.inputs.IMAGE_VER }}
fi
if [[ $IMAGE_VER == '' ]]; then
IMAGE_VER='test-vBeta'
fi
echo '::set-output name=image_ver::'$IMAGE_VER
- name: Check Version
run: |
echo $GITHUB_REF
echo ${{ steps.gen_ver.outputs.image_ver }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{env.REGISTRY}}
username: ${{secrets.DOCKER_USER}}
password: ${{secrets.DOCKER_PASS}}
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{ steps.gen_ver.outputs.image_ver }},${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest
- uses: snow-actions/line-notify@v1.1.0
if: success()
with:
access_token: ${{ secrets.TOKEN_LINE }}
message: |
-Success✅✅✅
Image: ${{env.IMAGE_NAME}}
Version: ${{ steps.gen_ver.outputs.IMAGE_VER }}
By: ${{ github.actor }}
- uses: snow-actions/line-notify@v1.1.0
if: failure()
with:
access_token: ${{ secrets.TOKEN_LINE }}
message: |
-Failure❌❌❌
Image: ${{env.IMAGE_NAME}}
Version: ${{ steps.gen_ver.outputs.IMAGE_VER }}
By: ${{ github.actor }}

View file

@ -4,6 +4,8 @@ ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apt-get update && apt-get install -y openssl
RUN pnpm i -g prisma
WORKDIR /app
@ -11,6 +13,7 @@ COPY . .
FROM base AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
RUN pnpm prisma generate
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
@ -18,9 +21,13 @@ RUN pnpm prisma generate
RUN pnpm run build
FROM base as prod
ENV NODE_ENV="production"
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
COPY --from=base /app/static /app/static
CMD ["pnpm", "run", "start"]
RUN chmod u+x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

25
compose.yaml Normal file
View file

@ -0,0 +1,25 @@
version: "3.8"
services:
app:
build: .
restart: unless-stopped
ports:
- 3000:3000
environment:
- KC_URL=http://192.168.1.20:8080
- KC_REALM=dev
- KC_SERVICE_ACCOUNT_CLIENT_ID=
- KC_SERVICE_ACCOUNT_SECRET=
- APP_HOST=0.0.0.0
- APP_PORT=3000
- MINIO_HOST=192.168.1.20
- MINIO_PORT=9000
- MINIO_ACCESS_KEY=
- MINIO_SECRET_KEY=
- MINIO_BUCKET=jws-dev
- ELASTICSEARCH_PROTOCOL=http
- ELASTICSEARCH_HOST=192.168.1.20
- ELASTICSEARCH_PORT=9200
- ELASTICSEARCH_INDEX=jws-log-index
- DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public

4
entrypoint.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/sh
pnpm prisma migrate deploy
pnpm run start

View file

@ -1,5 +1,5 @@
{
"name": "template",
"name": "jws-backend",
"version": "1.0.0",
"description": "",
"main": "./dist/app.js",
@ -24,12 +24,13 @@
"@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",
"prisma": "^5.11.0",
"prisma": "^5.12.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.3"
},
"dependencies": {
"@prisma/client": "5.11.0",
"@elastic/elasticsearch": "^8.13.0",
"@prisma/client": "5.12.1",
"@tsoa/runtime": "^6.2.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",

105
pnpm-lock.yaml generated
View file

@ -5,9 +5,12 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@elastic/elasticsearch':
specifier: ^8.13.0
version: 8.13.0
'@prisma/client':
specifier: 5.11.0
version: 5.11.0(prisma@5.11.0)
specifier: 5.12.1
version: 5.12.1(prisma@5.12.1)
'@tsoa/runtime':
specifier: ^6.2.0
version: 6.2.0
@ -56,8 +59,8 @@ devDependencies:
specifier: ^3.2.5
version: 3.2.5
prisma:
specifier: ^5.11.0
version: 5.11.0
specifier: ^5.12.1
version: 5.12.1
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.12.2)(typescript@5.4.3)
@ -74,6 +77,30 @@ packages:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@elastic/elasticsearch@8.13.0:
resolution: {integrity: sha512-OAYgzqArPqgDaIJ1yT0RX31YCgr1lleo53zL+36i23PFjHu08CA6Uq+BmBzEV05yEidl+ILPdeSfF3G8hPG/JQ==}
engines: {node: '>=18'}
dependencies:
'@elastic/transport': 8.5.0
tslib: 2.6.2
transitivePeerDependencies:
- supports-color
dev: false
/@elastic/transport@8.5.0:
resolution: {integrity: sha512-T+zSUHXBfrqlj/E9pJiaEgKoTdGykBCohzNBt6omDfI6EQtaNT240oMO03oXo35T8rwrCVonSMSoedbmToncVA==}
engines: {node: '>=18'}
dependencies:
debug: 4.3.4(supports-color@5.5.0)
hpagent: 1.2.0
ms: 2.1.3
secure-json-parse: 2.7.0
tslib: 2.6.2
undici: 6.11.1
transitivePeerDependencies:
- supports-color
dev: false
/@hapi/accept@6.0.3:
resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==}
dependencies:
@ -339,8 +366,8 @@ packages:
dev: false
optional: true
/@prisma/client@5.11.0(prisma@5.11.0):
resolution: {integrity: sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==}
/@prisma/client@5.12.1(prisma@5.12.1):
resolution: {integrity: sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA==}
engines: {node: '>=16.13'}
requiresBuild: true
peerDependencies:
@ -349,35 +376,35 @@ packages:
prisma:
optional: true
dependencies:
prisma: 5.11.0
prisma: 5.12.1
dev: false
/@prisma/debug@5.11.0:
resolution: {integrity: sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==}
/@prisma/debug@5.12.1:
resolution: {integrity: sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A==}
/@prisma/engines-version@5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102:
resolution: {integrity: sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==}
/@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab:
resolution: {integrity: sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==}
/@prisma/engines@5.11.0:
resolution: {integrity: sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==}
/@prisma/engines@5.12.1:
resolution: {integrity: sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA==}
requiresBuild: true
dependencies:
'@prisma/debug': 5.11.0
'@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102
'@prisma/fetch-engine': 5.11.0
'@prisma/get-platform': 5.11.0
'@prisma/debug': 5.12.1
'@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab
'@prisma/fetch-engine': 5.12.1
'@prisma/get-platform': 5.12.1
/@prisma/fetch-engine@5.11.0:
resolution: {integrity: sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==}
/@prisma/fetch-engine@5.12.1:
resolution: {integrity: sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA==}
dependencies:
'@prisma/debug': 5.11.0
'@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102
'@prisma/get-platform': 5.11.0
'@prisma/debug': 5.12.1
'@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab
'@prisma/get-platform': 5.12.1
/@prisma/get-platform@5.11.0:
resolution: {integrity: sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==}
/@prisma/get-platform@5.12.1:
resolution: {integrity: sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ==}
dependencies:
'@prisma/debug': 5.11.0
'@prisma/debug': 5.12.1
/@tsconfig/node10@1.0.11:
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@ -892,7 +919,6 @@ packages:
dependencies:
ms: 2.1.2
supports-color: 5.5.0
dev: true
/decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
@ -1322,7 +1348,6 @@ packages:
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: true
/has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
@ -1354,6 +1379,11 @@ packages:
function-bind: 1.1.2
dev: false
/hpagent@1.2.0:
resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==}
engines: {node: '>=14'}
dev: false
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@ -1715,7 +1745,6 @@ packages:
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1832,13 +1861,13 @@ packages:
hasBin: true
dev: true
/prisma@5.11.0:
resolution: {integrity: sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==}
/prisma@5.12.1:
resolution: {integrity: sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q==}
engines: {node: '>=16.13'}
hasBin: true
requiresBuild: true
dependencies:
'@prisma/engines': 5.11.0
'@prisma/engines': 5.12.1
/promise.any@2.0.6:
resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==}
@ -1963,6 +1992,10 @@ packages:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
dev: false
/secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
dev: false
/semver@7.6.0:
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
engines: {node: '>=10'}
@ -2165,7 +2198,6 @@ packages:
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: true
/swagger-ui-dist@5.13.0:
resolution: {integrity: sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q==}
@ -2242,6 +2274,10 @@ packages:
yn: 3.1.1
dev: true
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
/tsoa@6.2.0:
resolution: {integrity: sha512-EX/RyoU+4hD1rLM5NjYG+I7lEhqx1yuLgcHs/gyWQpkX/RL9cVR9hFA9LKQrK6PE+WTg1SEahn1MK3l/+6pVKw==}
engines: {node: '>=18.0.0', yarn: '>=1.9.4'}
@ -2334,6 +2370,11 @@ packages:
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/undici@6.11.1:
resolution: {integrity: sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==}
engines: {node: '>=18.0'}
dev: false
/universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}

View file

@ -0,0 +1,458 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('CREATED', 'ACTIVE', 'INACTIVE');
-- CreateEnum
CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY');
-- CreateTable
CREATE TABLE "Province" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Province_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "District" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"provinceId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "District_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SubDistrict" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"zipCode" TEXT NOT NULL,
"districtId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Branch" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"taxNo" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL,
"isHeadOffice" BOOLEAN NOT NULL DEFAULT false,
"headOfficeId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Branch_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchContact" (
"id" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"lineId" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchContact_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchUser" (
"id" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"keycloakId" TEXT NOT NULL,
"code" TEXT,
"firstName" TEXT NOT NULL,
"firstNameEN" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"lastNameEN" TEXT NOT NULL,
"gender" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"registrationNo" TEXT,
"startDate" TIMESTAMP(3),
"retireDate" TIMESTAMP(3),
"userType" "UserType" NOT NULL,
"userRole" TEXT NOT NULL,
"discountCondition" TEXT,
"licenseNo" TEXT,
"licenseIssueDate" TIMESTAMP(3),
"licenseExpireDate" TIMESTAMP(3),
"sourceNationality" TEXT,
"importNationality" TEXT,
"trainingPlace" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Customer" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"customerType" TEXT NOT NULL,
"customerName" TEXT NOT NULL,
"customerNameEN" TEXT NOT NULL,
"imageUrl" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CustomerBranch" (
"id" TEXT NOT NULL,
"branchNo" TEXT NOT NULL,
"legalPersonNo" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"taxNo" TEXT NOT NULL,
"registerName" TEXT NOT NULL,
"registerDate" TIMESTAMP(3) NOT NULL,
"authorizedCapital" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerBranch_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Employee" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"nrcNo" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"firstNameEN" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"lastNameEN" TEXT NOT NULL,
"dateOfBirth" TIMESTAMP(3) NOT NULL,
"gender" TEXT NOT NULL,
"nationality" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"arrivalBarricade" TEXT NOT NULL,
"arrivalCardNo" TEXT NOT NULL,
"customerBranchId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Employee_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmployeeCheckup" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"checkupResult" TEXT NOT NULL,
"checkupType" TEXT NOT NULL,
"provinceId" TEXT,
"hospitalName" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"medicalBenefitScheme" TEXT NOT NULL,
"insuranceCompany" TEXT NOT NULL,
"coverageStartDate" TIMESTAMP(3) NOT NULL,
"coverageExpireDate" TIMESTAMP(3) NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeCheckup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmployeeWork" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"ownerName" TEXT NOT NULL,
"positionName" TEXT NOT NULL,
"jobType" TEXT NOT NULL,
"workplace" TEXT NOT NULL,
"workPermitNo" TEXT NOT NULL,
"workPermitIssuDate" TIMESTAMP(3) NOT NULL,
"workPermitExpireDate" TIMESTAMP(3) NOT NULL,
"workEndDate" TIMESTAMP(3) NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeWork_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmployeeOtherInfo" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"citizenId" TEXT NOT NULL,
"fatherFullName" TEXT NOT NULL,
"motherFullName" TEXT NOT NULL,
"birthPlace" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeOtherInfo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Service" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Work" (
"id" TEXT NOT NULL,
"order" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Work_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WorkProduct" (
"id" TEXT NOT NULL,
"workId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductGroup" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductGroup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductType" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"process" TEXT NOT NULL,
"price" INTEGER NOT NULL,
"agentPrice" INTEGER NOT NULL,
"serviceCharge" INTEGER NOT NULL,
"imageUrl" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"productTypeId" TEXT,
"productGroupId" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "District" ADD CONSTRAINT "District_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SubDistrict" ADD CONSTRAINT "SubDistrict_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_headOfficeId_fkey" FOREIGN KEY ("headOfficeId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchContact" ADD CONSTRAINT "BranchContact_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_customerBranchId_fkey" FOREIGN KEY ("customerBranchId") REFERENCES "CustomerBranch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeWork" ADD CONSTRAINT "EmployeeWork_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeOtherInfo" ADD CONSTRAINT "EmployeeOtherInfo_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Work" ADD CONSTRAINT "Work_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "Work"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_productTypeId_fkey" FOREIGN KEY ("productTypeId") REFERENCES "ProductType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the column `imageUrl` on the `Customer` table. All the data in the column will be lost.
- Changed the type of `customerType` on the `Customer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- CreateEnum
CREATE TYPE "CustomerType" AS ENUM ('CORP', 'PERS');
-- AlterTable
ALTER TABLE "Customer" DROP COLUMN "imageUrl",
DROP COLUMN "customerType",
ADD COLUMN "customerType" "CustomerType" NOT NULL;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "birtDate" TIMESTAMP(3),
ADD COLUMN "responsibleArea" TEXT;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `birtDate` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "birtDate",
ADD COLUMN "birthDate" TIMESTAMP(3);

View file

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `keycloakId` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "keycloakId";

View file

@ -0,0 +1,11 @@
/*
Warnings:
- You are about to drop the column `lineId` on the `BranchContact` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "lineId" TEXT;
-- AlterTable
ALTER TABLE "BranchContact" DROP COLUMN "lineId";

View file

@ -0,0 +1,119 @@
/*
Warnings:
- You are about to drop the column `telephoneNo` on the `Branch` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Branch" DROP COLUMN "telephoneNo";
-- CreateTable
CREATE TABLE "Menu" (
"id" TEXT NOT NULL,
"caption" TEXT NOT NULL,
"captionEN" TEXT NOT NULL,
"menuType" TEXT NOT NULL,
"url" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"parentId" TEXT,
CONSTRAINT "Menu_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RoleMenuPermission" (
"id" TEXT NOT NULL,
"userRole" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RoleMenuPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserMenuPermission" (
"id" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserMenuPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MenuComponent" (
"id" TEXT NOT NULL,
"componentId" TEXT NOT NULL,
"componentTag" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MenuComponent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RoleMenuComponentPermission" (
"id" TEXT NOT NULL,
"componentId" TEXT NOT NULL,
"componentTag" TEXT NOT NULL,
"menuComponentId" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RoleMenuComponentPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserMenuComponentPermission" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"menuComponentId" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserMenuComponentPermission_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Menu" ADD CONSTRAINT "Menu_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Menu"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleMenuPermission" ADD CONSTRAINT "RoleMenuPermission_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuPermission" ADD CONSTRAINT "UserMenuPermission_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuPermission" ADD CONSTRAINT "UserMenuPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MenuComponent" ADD CONSTRAINT "MenuComponent_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleMenuComponentPermission" ADD CONSTRAINT "RoleMenuComponentPermission_menuComponentId_fkey" FOREIGN KEY ("menuComponentId") REFERENCES "MenuComponent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuComponentPermission" ADD CONSTRAINT "UserMenuComponentPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuComponentPermission" ADD CONSTRAINT "UserMenuComponentPermission_menuComponentId_fkey" FOREIGN KEY ("menuComponentId") REFERENCES "MenuComponent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "contactName" TEXT;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "checkpoint" TEXT,
ADD COLUMN "checkpointEN" TEXT;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "username" TEXT NOT NULL;

View file

@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `componentId` on the `RoleMenuComponentPermission` table. All the data in the column will be lost.
- You are about to drop the column `componentTag` on the `RoleMenuComponentPermission` table. All the data in the column will be lost.
- Added the required column `userRole` to the `RoleMenuComponentPermission` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "RoleMenuComponentPermission" DROP COLUMN "componentId",
DROP COLUMN "componentTag",
ADD COLUMN "userRole" TEXT NOT NULL;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "telephoneHq" TEXT;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `telephoneHq` on table `Branch` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Branch" ALTER COLUMN "telephoneHq" SET NOT NULL;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `telephoneHq` on the `Branch` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Branch" DROP COLUMN "telephoneHq",
ADD COLUMN "telephoneNo" TEXT;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `telephoneNo` on table `Branch` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Branch" ALTER COLUMN "telephoneNo" SET NOT NULL;

View file

@ -7,9 +7,112 @@ datasource db {
url = env("DATABASE_URL")
}
model Menu {
id String @id @default(uuid())
caption String
captionEN String
menuType String
url String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
parentId String?
children Menu[] @relation(name: "MenuRelation")
roleMenuPermission RoleMenuPermission[]
userMenuPermission UserMenuPermission[]
userComponent MenuComponent[]
}
model RoleMenuPermission {
id String @id @default(uuid())
userRole String
permission String
menu Menu @relation(fields: [menuId], references: [id])
menuId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model UserMenuPermission {
id String @id @default(uuid())
permission String
menu Menu @relation(fields: [menuId], references: [id])
menuId String
user User @relation(fields: [userId], references: [id])
userId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model MenuComponent {
id String @id @default(uuid())
componentId String
componentTag String
menu Menu @relation(fields: [menuId], references: [id])
menuId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
roleMenuComponentPermission RoleMenuComponentPermission[]
userMennuComponentPermission UserMenuComponentPermission[]
}
model RoleMenuComponentPermission {
id String @id @default(uuid())
userRole String
permission String
menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id])
menuComponentId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model UserMenuComponentPermission {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id])
menuComponentId String
permission String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model Province {
id String @id @default(uuid())
nameTH String
name String
nameEN String
createdBy String?
@ -27,7 +130,7 @@ model Province {
model District {
id String @id @default(uuid())
nameTH String
name String
nameEN String
provinceId String
@ -47,7 +150,7 @@ model District {
model SubDistrict {
id String @id @default(uuid())
nameTH String
name String
nameEN String
zipCode String
@ -65,14 +168,21 @@ model SubDistrict {
employee Employee[]
}
enum Status {
CREATED
ACTIVE
INACTIVE
}
model Branch {
id String @id @default(uuid())
code String
taxNo String
nameTH String
nameEN String
addressTH String
addressEN String
id String @id @default(uuid())
code String
taxNo String
name String
nameEN String
address String
addressEN String
telephoneNo String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
@ -86,7 +196,8 @@ model Branch {
zipCode String
email String
telephoneNo String
contactName String?
lineId String?
latitude String
longitude String
@ -96,7 +207,7 @@ model Branch {
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
headOfficeId String?
status String?
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
@ -109,10 +220,8 @@ model Branch {
}
model BranchContact {
id String @id @default(uuid())
telephoneNo String
lineId String
qrCodeImageUrl String?
id String @id @default(uuid())
telephoneNo String
branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
branchId String
@ -138,16 +247,25 @@ model BranchUser {
updatedAt DateTime @updatedAt
}
enum UserType {
USER
MESSENGER
DELEGATE
AGENCY
}
model User {
id String @id @default(uuid())
code String
firstNameTH String
code String?
firstName String
firstNameEN String
lastNameTH String
lastName String
lastNameEN String
username String
gender String
addressTH String
address String
addressEN String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
@ -164,46 +282,56 @@ model User {
email String
telephoneNo String
registrationNo String
registrationNo String?
startDate DateTime
retireDate DateTime
startDate DateTime?
retireDate DateTime?
profileImageUrl String?
checkpoint String?
checkpointEN String?
userType String
userType UserType
userRole String
discountCondition String
discountCondition String?
licenseNo String
licenseIssueDate DateTime
licenseExpireDate DateTime
licenseNo String?
licenseIssueDate DateTime?
licenseExpireDate DateTime?
sourceNationality String
importNationality String
sourceNationality String?
importNationality String?
trainingPlace String
trainingPlace String?
responsibleArea String?
status String?
birthDate DateTime?
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
branch BranchUser[]
branch BranchUser[]
userMenuPermission UserMenuPermission[]
userMenuComponentPermission UserMenuComponentPermission[]
}
enum CustomerType {
CORP
PERS
}
model Customer {
id String @id @default(uuid())
id String @id @default(uuid())
code String
customerType String
customerNameTH String
customerType CustomerType
customerName String
customerNameEN String
imageUrl String?
status String?
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
@ -218,7 +346,7 @@ model CustomerBranch {
branchNo String
legalPersonNo String
nameTH String
name String
nameEN String
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
@ -229,6 +357,7 @@ model CustomerBranch {
registerDate DateTime
authorizedCapital String
address String
addressEN String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
@ -248,6 +377,8 @@ model CustomerBranch {
latitude String
longitude String
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
@ -260,13 +391,17 @@ model Employee {
id String @id @default(uuid())
code String
fullNameTH String
fullNameEN String
nrcNo String
firstName String
firstNameEN String
lastName String
lastNameEN String
dateOfBirth DateTime
gender String
nationality String
addressTH String
address String
addressEN String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
@ -285,12 +420,11 @@ model Employee {
arrivalBarricade String
arrivalCardNo String
profileImageUrl String
customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull)
customerBranchId String?
status String?
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
@ -299,7 +433,7 @@ model Employee {
employeeCheckup EmployeeCheckup[]
employeeWork EmployeeWork[]
EmployeeOtherInfo EmployeeOtherInfo[]
employeeOtherInfo EmployeeOtherInfo[]
}
model EmployeeCheckup {
@ -368,11 +502,11 @@ model EmployeeOtherInfo {
model Service {
id String @id @default(uuid())
code String
name String
detail String
imageUrl String
status String?
code String
name String
detail String
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
@ -384,12 +518,14 @@ model Service {
model Work {
id String @id @default(uuid())
order String
order Int
name String
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId String
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
@ -416,7 +552,8 @@ model ProductGroup {
name String
detail String
remark String
status String?
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
@ -432,7 +569,8 @@ model ProductType {
name String
detail String
remark String
status String?
status Status @default(CREATED)
createdBy String?
createdAt DateTime @default(now())
@ -453,7 +591,8 @@ model Product {
agentPrice Int
serviceCharge Int
imageUrl String
status String?
status Status @default(CREATED)
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
productTypeId String?

View file

@ -5,6 +5,7 @@ import swaggerUi from "swagger-ui-express";
import swaggerDocument from "./swagger.json";
import error from "./middlewares/error";
import { RegisterRoutes } from "./routes";
import logMiddleware from "./middlewares/log";
const APP_HOST = process.env.APP_HOST || "0.0.0.0";
const APP_PORT = +(process.env.APP_PORT || 3000);
@ -15,11 +16,22 @@ const APP_PORT = +(process.env.APP_PORT || 3000);
app.use(cors());
app.use(json());
app.use(urlencoded({ extended: true }));
app.use(logMiddleware);
app.use("/", express.static("static"));
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
RegisterRoutes(app);
app.get("*", (_, res) =>
res.status(404).send({
status: 404,
message: "Route not found.",
devMessage: "unknown_url",
}),
);
app.use(error);
app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));

View file

@ -0,0 +1,56 @@
import { Controller, Get, Path, Route, Tags } from "tsoa";
import prisma from "../db";
@Route("api/address")
@Tags("Address")
export class AddressController extends Controller {
@Get("province")
async getProvince() {
return await prisma.province.findMany();
}
@Get("province/{provinceId}")
async getProvinceById(@Path() provinceId: string) {
return await prisma.province.findFirst({
where: { id: provinceId },
});
}
@Get("province/{provinceId}/district")
async getDistrictOfProvince(@Path() provinceId: string) {
return await prisma.district.findMany({
where: { provinceId },
});
}
@Get("district")
async getDistrict() {
return await prisma.district.findMany();
}
@Get("district/{districtId}")
async getDistrictOfId(@Path() districtId: string) {
return await prisma.province.findFirst({
where: { id: districtId },
});
}
@Get("district/{districtId}/sub-district")
async getSubDistrictOfDistrict(@Path() districtId: string) {
return await prisma.subDistrict.findMany({
where: { districtId },
});
}
@Get("sub-district")
async getSubDistrict() {
return await prisma.subDistrict.findMany();
}
@Get("sub-district/{subDistrictId}")
async getSubDistrictOfId(@Path() subDistrictId: string) {
return await prisma.subDistrict.findFirst({
where: { id: subDistrictId },
});
}
}

View file

@ -0,0 +1,132 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type BranchContactCreate = {
telephoneNo: string;
};
type BranchContactUpdate = {
telephoneNo?: string;
};
@Route("api/branch/{branchId}/contact")
@Tags("Branch Contact")
@Security("keycloak")
export class BranchContactController extends Controller {
@Get()
async getBranchContact(
@Path() branchId: string,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const [result, total] = await prisma.$transaction([
prisma.branchContact.findMany({
orderBy: { createdAt: "asc" },
where: { branchId },
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branchContact.count({ where: { branchId } }),
]);
return {
result,
page,
pageSize,
total,
};
}
@Get("{contactId}")
async getBranchContactById(@Path() branchId: string, @Path() contactId: string) {
const record = await prisma.branchContact.findFirst({ where: { id: contactId, branchId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Branch contact cannot be found.",
"data_not_found",
);
}
return record;
}
@Post()
async createBranchContact(
@Request() req: RequestWithUser,
@Path() branchId: string,
@Body() body: BranchContactCreate,
) {
if (!(await prisma.branch.findFirst({ where: { id: branchId } }))) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Branch not found.",
"missing_or_invalid_parameter",
);
}
const record = await prisma.branchContact.create({
data: { ...body, branchId, createdBy: req.user.name, updateBy: req.user.name },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{contactId}")
async editBranchContact(
@Request() req: RequestWithUser,
@Body() body: BranchContactUpdate,
@Path() branchId: string,
@Path() contactId: string,
) {
if (
!(await prisma.branchContact.findUnique({
where: { id: contactId, branchId },
}))
) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Branch contact cannot be found.",
"data_not_found",
);
}
const record = await prisma.branchContact.update({
data: { ...body, updateBy: req.user.name },
where: { id: contactId, branchId },
});
return record;
}
@Delete("{contactId}")
async deleteBranchContact(@Path() branchId: string, @Path() contactId: string) {
const result = await prisma.branchContact.deleteMany({ where: { id: contactId, branchId } });
if (result.count <= 0) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Branch contact cannot be found.",
"data_not_found",
);
}
}
}

View file

@ -0,0 +1,447 @@
import { Prisma, Status, UserType } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
import minio from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type BranchCreate = {
status?: Status;
taxNo: string;
nameEN: string;
name: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
contactName?: string | null;
contact?: string | string[] | null;
telephoneNo: string;
lineId?: string | null;
longitude: string;
latitude: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
headOfficeId?: string | null;
};
type BranchUpdate = {
status?: "ACTIVE" | "INACTIVE";
taxNo?: string;
nameEN?: string;
name?: string;
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo: string;
contactName?: string;
contact?: string | string[] | null;
lineId?: string;
longitude?: string;
latitude?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
headOfficeId?: string | null;
};
function lineImageLoc(id: string) {
return `branch/line-qr-${id}`;
}
function branchImageLoc(id: string) {
return `branch/branch-img-${id}`;
}
@Route("api/branch")
@Tags("Branch")
@Security("keycloak")
export class BranchController extends Controller {
@Get("stats")
async getStats() {
const list = await prisma.branch.groupBy({
_count: true,
by: "isHeadOffice",
});
return list.reduce<Record<"hq" | "br", number>>(
(a, c) => {
a[c.isHeadOffice ? "hq" : "br"] = c._count;
return a;
},
{ hq: 0, br: 0 },
);
}
@Get("user-stats")
async getUserStat(@Query() userType?: UserType) {
const list = await prisma.branchUser.groupBy({
_count: true,
where: { user: { userType } },
by: "branchId",
});
const record = await prisma.branch.findMany({
select: {
id: true,
nameEN: true,
name: true,
isHeadOffice: true,
},
});
return record.map((a) =>
Object.assign(a, {
count: list.find((b) => b.branchId === a.id)?._count ?? 0,
}),
);
}
@Get()
async getBranch(
@Query() zipCode?: string,
@Query() filter?: "head" | "sub",
@Query() headOfficeId?: string,
@Query() tree?: boolean,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
AND: {
headOfficeId: headOfficeId ?? (filter === "head" || tree ? null : undefined),
NOT: { headOfficeId: filter === "sub" && !headOfficeId ? null : undefined },
},
OR: [
{ nameEN: { contains: query }, zipCode },
{ name: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
{ telephoneNo: { contains: query }, zipCode },
],
} satisfies Prisma.BranchWhereInput;
const [result, total] = await prisma.$transaction([
prisma.branch.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
contact: true,
branch: tree && {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branch.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{branchId}")
async getBranchById(
@Path() branchId: string,
@Query() includeSubBranch?: boolean,
@Query() includeContact?: boolean,
) {
const record = await prisma.branch.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
branch: includeSubBranch && {
include: {
province: true,
district: true,
subDistrict: true,
},
},
contact: includeContact,
},
where: { id: branchId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)),
});
}
@Post()
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {
const [province, district, subDistrict, head] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.headOfficeId && !head)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Head branch cannot be found.",
"missing_or_invalid_parameter",
);
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
const year = new Date().getFullYear();
const last = await prisma.branch.findFirst({
orderBy: { createdAt: "desc" },
where: { headOfficeId: headOfficeId ?? null },
});
const code = !headOfficeId
? `HQ${year.toString().slice(2)}${+(last?.code.slice(-1) || 0) + 1}`
: `BR${head?.code.slice(2, 5)}${(+(last?.code.slice(-2) || 0) + 1).toString().padStart(2, "0")}`;
const record = await prisma.branch.create({
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
code,
isHeadOffice: !headOfficeId,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
if (headOfficeId) {
await prisma.branch.updateMany({
where: { id: headOfficeId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
if (record && contact) {
await prisma.branchContact.createMany({
data:
typeof contact === "string"
? [{ telephoneNo: contact, branchId: record.id }]
: contact.map((v) => ({ telephoneNo: v, branchId: record.id })),
});
}
return Object.assign(record, {
contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }),
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
qrCodeImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
qrCodeImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
});
}
@Put("{branchId}")
async editBranch(
@Request() req: RequestWithUser,
@Body() body: BranchUpdate,
@Path() branchId: string,
) {
if (body.headOfficeId === branchId)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Cannot make this as head office and branch at the same time.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) {
const [province, district, subDistrict, branch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.headOfficeId && !branch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Head branch cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const record = await prisma.branch.update({
include: { province: true, district: true, subDistrict: true },
data: {
...rest,
isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined,
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
headOffice: {
connect: headOfficeId ? { id: headOfficeId } : undefined,
disconnect: headOfficeId === null || undefined,
},
updateBy: req.user.name,
},
where: { id: branchId },
});
if (record && contact !== undefined) {
await prisma.branchContact.deleteMany({ where: { branchId } });
contact &&
(await prisma.branchContact.createMany({
data:
typeof contact === "string"
? [{ telephoneNo: contact, branchId }]
: contact.map((v) => ({ telephoneNo: v, branchId })),
}));
}
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
qrCodeImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
qrCodeImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
});
}
@Delete("{branchId}")
async deleteBranch(@Path() branchId: string) {
const record = await prisma.branch.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: branchId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used");
}
await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), {
forceDelete: true,
});
await minio.removeObject(MINIO_BUCKET, branchImageLoc(branchId), {
forceDelete: true,
});
return await prisma.branch.delete({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: branchId },
});
}
}

View file

@ -0,0 +1,290 @@
import { Prisma, Status, UserType } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type BranchUserBody = { user: string[] };
@Route("api/branch/{branchId}/user")
@Tags("Branch User")
@Security("keycloak")
export class BranchUserController extends Controller {
@Get()
async getBranchUser(
@Path() branchId: string,
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ user: { firstName: { contains: query }, zipCode }, branchId },
{ user: { firstNameEN: { contains: query }, zipCode }, branchId },
{ user: { lastName: { contains: query }, zipCode }, branchId },
{ user: { lastNameEN: { contains: query }, zipCode }, branchId },
{ user: { email: { contains: query }, zipCode }, branchId },
{ user: { telephoneNo: { contains: query }, zipCode }, branchId },
],
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.branchUser.findMany({
orderBy: { user: { createdAt: "asc" } },
include: {
user: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branchUser.count({ where }),
]);
return { result: result.map((v) => v.user), page, pageSize, total };
}
@Post()
async createBranchUser(
@Request() req: RequestWithUser,
@Path() branchId: string,
@Body() body: BranchUserBody,
) {
const [branch, user] = await prisma.$transaction([
prisma.branch.findUnique({
where: { id: branchId },
}),
prisma.user.findMany({
include: { branch: true },
where: { id: { in: body.user } },
}),
]);
if (!branch) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Branch cannot be found.",
"missing_or_invalid_parameter",
);
}
if (user.length !== body.user.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more user cannot be found.",
"missing_or_invalid_parameter",
);
}
await prisma.$transaction([
prisma.user.updateMany({
where: { id: { in: body.user }, status: Status.CREATED },
data: { status: Status.ACTIVE },
}),
prisma.branchUser.createMany({
data: user
.filter((a) => !a.branch.some((b) => b.branchId === branchId))
.map((v) => ({
branchId,
userId: v.id,
createdBy: req.user.name,
updateBy: req.user.name,
})),
}),
]);
const group: Record<UserType, string[]> = {
USER: [],
AGENCY: [],
DELEGATE: [],
MESSENGER: [],
};
for (const u of user) group[u.userType].push(u.id);
for (const g of Object.values(UserType)) {
if (group[g].length === 0) continue;
const last = await prisma.branchUser.findFirst({
orderBy: { createdAt: "desc" },
include: { user: true },
where: {
branchId,
user: {
userType: g,
code: { startsWith: `${branch.code.slice(4).padEnd(3, "0")}` },
},
},
});
const code = (idx: number) =>
`${branch.code.slice(4).padEnd(3, "0")}${g !== "USER" ? g.charAt(0) : ""}${(+(last?.user.code?.slice(-4) || 0) + idx + 1).toString().padStart(4, "0")}`;
await prisma.$transaction(
group[g].map((v, i) =>
prisma.user.updateMany({
where: { id: v, code: null },
data: { code: code(i) },
}),
),
);
}
}
@Delete()
async deleteBranchUser(@Path() branchId: string, @Body() body: BranchUserBody) {
await prisma.$transaction(
body.user.map((v) => prisma.branchUser.deleteMany({ where: { branchId, userId: v } })),
);
await prisma.user.updateMany({
where: {
branch: { none: {} },
},
data: { code: null },
});
}
@Delete("{userId}")
async deleteBranchUserById(@Path() branchId: string, @Path() userId: string) {
await prisma.branchUser.deleteMany({
where: { branchId, userId },
});
await prisma.user.updateMany({
where: {
id: userId,
branch: { none: {} },
},
data: { code: null },
});
}
}
type UserBranchBody = { branch: string[] };
@Route("api/user/{userId}/branch")
@Tags("Branch User")
@Security("keycloak")
export class UserBranchController extends Controller {
@Get()
async getUserBranch(
@Path() userId: string,
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ branch: { name: { contains: query }, zipCode }, userId },
{ branch: { nameEN: { contains: query }, zipCode }, userId },
],
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.branchUser.findMany({
orderBy: { branch: { createdAt: "asc" } },
include: {
branch: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branchUser.count({ where }),
]);
return { result: result.map((v) => v.branch), page, pageSize, total };
}
@Post()
async createUserBranch(
@Request() req: RequestWithUser,
@Path() userId: string,
@Body() body: UserBranchBody,
) {
const branch = await prisma.branch.findMany({
include: { user: true },
where: { id: { in: body.branch } },
});
if (branch.length !== body.branch.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more branch cannot be found.",
"missing_or_invalid_parameter",
);
}
await prisma.branch.updateMany({
where: { id: { in: body.branch }, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
await prisma.branchUser.createMany({
data: branch
.filter((a) => !a.user.some((b) => b.userId === userId))
.map((v) => ({
branchId: v.id,
userId,
createdBy: req.user.name,
updateBy: req.user.name,
})),
});
this.setStatus(HttpStatus.CREATED);
}
@Delete()
async deleteUserBranch(@Path() userId: string, @Body() body: UserBranchBody) {
await prisma.$transaction(
body.branch.map((v) => prisma.branchUser.deleteMany({ where: { userId, branchId: v } })),
);
await prisma.user.updateMany({
where: {
branch: { none: {} },
},
data: { code: null },
});
}
@Delete("{branchId}")
async deleteUserBranchById(@Path() branchId: string, @Path() userId: string) {
await prisma.branchUser.deleteMany({
where: { branchId, userId },
});
await prisma.user.updateMany({
where: {
id: userId,
branch: { none: {} },
},
data: { code: null },
});
}
}

View file

@ -0,0 +1,356 @@
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import minio from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
function imageLocation(id: string) {
return `employee/profile-img-${id}`;
}
type CustomerBranchCreate = {
customerId: string;
status?: Status;
legalPersonNo: string;
taxNo: string;
name: string;
nameEN: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
longitude: string;
latitude: string;
registerName: string;
registerDate: Date;
authorizedCapital: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type CustomerBranchUpdate = {
customerId?: string;
status?: "ACTIVE" | "INACTIVE";
legalPersonNo?: string;
taxNo?: string;
name?: string;
nameEN?: string;
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
longitude?: string;
latitude?: string;
registerName?: string;
registerDate?: Date;
authorizedCapital?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
@Route("api/customer-branch")
@Tags("Customer Branch")
@Security("keycloak")
export class CustomerBranchController extends Controller {
@Get()
async list(
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ nameEN: { contains: query }, zipCode },
{ name: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.BranchWhereInput;
const [result, total] = await prisma.$transaction([
prisma.customerBranch.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.customerBranch.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{branchId}")
async getById(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: branchId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
return record;
}
@Get("{branchId}/employee")
async listEmployee(
@Path() branchId: string,
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
AND: { customerBranchId: branchId },
OR: [
{ firstName: { contains: query }, zipCode },
{ firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.employee.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.employee.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) {
const [province, district, subDistrict, customer] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerId && !customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
const count = await prisma.customerBranch.count({
where: { customerId },
});
const record = await prisma.customerBranch.create({
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
branchNo: `${count + 1}`,
customer: { connect: { id: customerId } },
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
await prisma.customer.updateMany({
where: { id: customerId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{branchId}")
async editById(
@Request() req: RequestWithUser,
@Body() body: CustomerBranchUpdate,
@Path() branchId: string,
) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) {
const [province, district, subDistrict, customer] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerId && !customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
if (!(await prisma.customerBranch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const record = await prisma.customerBranch.update({
where: { id: branchId },
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
customer: { connect: customerId ? { id: customerId } : undefined },
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{branchId}")
async delete(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({ where: { id: branchId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Customer branch cannot be found.",
"data_not_found",
);
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Customer branch is in used.", "data_in_used");
}
return await prisma.customerBranch.delete({ where: { id: branchId } });
}
}

View file

@ -0,0 +1,178 @@
import { CustomerType, Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import minio from "../services/minio";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
export type CustomerCreate = {
status?: Status;
customerType: CustomerType;
customerName: string;
customerNameEN: string;
};
export type CustomerUpdate = {
status?: "ACTIVE" | "INACTIVE";
customerType?: CustomerType;
customerName?: string;
customerNameEN?: string;
};
function imageLocation(id: string) {
return `customer/img-${id}`;
}
@Route("api/customer")
@Tags("Customer")
@Security("keycloak")
export class CustomerController extends Controller {
@Get()
async list(
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [{ customerName: { contains: query } }, { customerNameEN: { contains: query } }],
} satisfies Prisma.CustomerWhereInput;
const [result, total] = await prisma.$transaction([
prisma.customer.findMany({
orderBy: { createdAt: "asc" },
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.customer.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(v.id), 12 * 60 * 60),
})),
),
page,
pageSize,
total,
};
}
@Get("{customerId}")
async getById(@Path() customerId: string) {
const record = await prisma.customer.findFirst({ where: { id: customerId } });
if (!record)
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
const last = await prisma.customer.findFirst({
orderBy: { createdAt: "desc" },
where: { customerType: body.customerType },
});
const code = `${body.customerType}${(+(last?.code.slice(-6) || 0) + 1).toString().padStart(6, "0")}`;
const record = await prisma.customer.create({
data: {
...body,
code,
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{customerId}")
async editById(
@Path() customerId: string,
@Request() req: RequestWithUser,
@Body() body: CustomerUpdate,
) {
if (!(await prisma.customer.findUnique({ where: { id: customerId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
}
const record = await prisma.customer.update({
where: { id: customerId },
data: {
...body,
createdBy: req.user.name,
updateBy: req.user.name,
},
});
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{customerId}")
async deleteById(@Path() customerId: string) {
const record = await prisma.customer.findFirst({ where: { id: customerId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Customer is in used.", "data_in_used");
}
return await prisma.customer.delete({ where: { id: customerId } });
}
}

View file

@ -0,0 +1,183 @@
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
type EmployeeCheckupCreate = {
checkupType: string;
checkupResult: string;
provinceId?: string | null;
hospitalName: string;
remark: string;
medicalBenefitScheme: string;
insuranceCompany: string;
coverageStartDate: Date;
coverageExpireDate: Date;
};
type EmployeeCheckupEdit = {
checkupType?: string;
checkupResult?: string;
provinceId?: string | null;
hospitalName?: string;
remark?: string;
medicalBenefitScheme?: string;
insuranceCompany?: string;
coverageStartDate?: Date;
coverageExpireDate?: Date;
};
@Route("api/employee/{employeeId}/checkup")
@Tags("Employee Checkup")
@Security("keycloak")
export class EmployeeCheckupController extends Controller {
@Get()
async list(@Path() employeeId: string) {
return prisma.employeeCheckup.findMany({
orderBy: { createdAt: "asc" },
where: { employeeId },
});
}
@Get("{checkupId}")
async getById(@Path() employeeId: string, @Path() checkupId: string) {
const record = await prisma.employeeCheckup.findFirst({
where: { id: checkupId, employeeId },
});
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
);
}
return record;
}
@Post()
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeCheckupCreate,
) {
if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.employee.findFirst({ where: { id: employeeId } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (!employee)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, ...rest } = body;
const record = await prisma.employeeCheckup.create({
include: { province: true },
data: {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{checkupId}")
async editById(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() checkupId: string,
@Body() body: EmployeeCheckupEdit,
) {
if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.employee.findFirst({ where: { id: employeeId } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (!employee)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, ...rest } = body;
if (!(await prisma.employeeCheckup.findUnique({ where: { id: checkupId, employeeId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
);
}
const record = await prisma.employeeCheckup.update({
include: { province: true },
where: { id: checkupId, employeeId },
data: {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{checkupId}")
async deleteById(@Path() employeeId: string, @Path() checkupId: string) {
const record = await prisma.employeeCheckup.findFirst({ where: { id: checkupId, employeeId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
);
}
return await prisma.employeeCheckup.delete({ where: { id: checkupId, employeeId } });
}
}

View file

@ -0,0 +1,330 @@
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import minio from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
function imageLocation(id: string) {
return `employee/profile-img-${id}`;
}
type EmployeeCreate = {
customerBranchId: string;
status?: Status;
code: string;
nrcNo: string;
dateOfBirth: Date;
gender: string;
nationality: string;
firstName: string;
firstNameEN: string;
lastName: string;
lastNameEN: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
arrivalBarricade: string;
arrivalCardNo: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type EmployeeUpdate = {
customerBranchId?: string;
status?: "ACTIVE" | "INACTIVE";
code?: string;
nrcNo?: string;
dateOfBirth?: Date;
gender?: string;
nationality?: string;
firstName?: string;
firstNameEN?: string;
lastName?: string;
lastNameEN?: string;
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
arrivalBarricade?: string;
arrivalCardNo?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
@Route("api/employee")
@Tags("Employee")
@Security("keycloak")
export class EmployeeController extends Controller {
@Get()
async list(
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ firstName: { contains: query }, zipCode },
{ firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.employee.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.employee.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{employeeId}")
async getById(@Path() employeeId: string) {
const record = await prisma.employee.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: employeeId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found");
}
return record;
}
@Post()
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) {
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerBranchId && !customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer Branch cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body;
const record = await prisma.employee.create({
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
customerBranch: { connect: { id: customerBranchId } },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
await prisma.customerBranch.updateMany({
where: { id: customerBranchId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{employeeId}")
async editById(
@Request() req: RequestWithUser,
@Body() body: EmployeeUpdate,
@Path() employeeId: string,
) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) {
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerBranchId && !customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body;
const record = await prisma.employee.update({
where: { id: employeeId },
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
customerBranch: { connect: customerBranchId ? { id: customerBranchId } : undefined },
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{employeeId}")
async delete(@Path() employeeId: string) {
const record = await prisma.employee.findFirst({ where: { id: employeeId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(
HttpStatus.FORBIDDEN,
"Emplyee is in used.",
"missing_or_invalid_parameter",
);
}
return await prisma.employee.delete({ where: { id: employeeId } });
}
}

View file

@ -0,0 +1,127 @@
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type EmployeeOtherInfoCreate = {
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
};
type EmployeeOtherInfoUpdate = {
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
};
@Route("api/employee/{employeeId}/other-info")
@Tags("Employee Other Info")
@Security("keycloak")
export class EmployeeOtherInfo extends Controller {
@Get()
async list(@Path() employeeId: string) {
return prisma.employeeOtherInfo.findMany({
orderBy: { createdAt: "asc" },
where: { employeeId },
});
}
@Get("{otherInfoId}")
async getById(@Path() employeeId: string, @Path() otherInfoId: string) {
const record = await prisma.employeeOtherInfo.findFirst({
where: { id: otherInfoId, employeeId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee info cannot be found.", "data_not_found");
}
return record;
}
@Post()
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeOtherInfoCreate,
) {
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
);
const record = await prisma.employeeOtherInfo.create({
data: {
...body,
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{otherInfoId}")
async editById(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() otherInfoId: string,
@Body() body: EmployeeOtherInfoUpdate,
) {
if (!(await prisma.employeeOtherInfo.findUnique({ where: { id: otherInfoId, employeeId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee other info cannot be found.",
"data_not_found",
);
}
const record = await prisma.employeeOtherInfo.update({
where: { id: otherInfoId, employeeId },
data: { ...body, createdBy: req.user.name, updateBy: req.user.name },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{otherInfoId}")
async deleteById(@Path() employeeId: string, @Path() otherInfoId: string) {
const record = await prisma.employeeOtherInfo.findFirst({
where: { id: otherInfoId, employeeId },
});
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee other info cannot be found.",
"data_not_found",
);
}
return await prisma.employeeOtherInfo.delete({ where: { id: otherInfoId, employeeId } });
}
}

View file

@ -0,0 +1,76 @@
import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } from "tsoa";
import {
addUserRoles,
createUser,
deleteUser,
editUser,
getRoles,
removeUserRoles,
} from "../services/keycloak";
@Route("api/keycloak")
@Tags("Single-Sign On")
@Security("keycloak")
export class KeycloakController extends Controller {
@Post("user")
async createUser(
@Body() body: { username: string; password: string; firstName?: string; lastName?: string },
) {
return await createUser(body.username, body.password, {
firstName: body.firstName,
lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"],
});
}
@Put("user/{userId}")
async editUser(
@Path() userId: string,
@Body() body: { username?: string; password?: string; firstName?: string; lastName?: string },
) {
return await editUser(userId, body);
}
@Delete("user/{userId}")
async deleteUser(@Path() userId: string) {
return await deleteUser(userId);
}
@Get("role")
async getRole() {
const role = await getRoles();
if (Array.isArray(role))
return role.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)),
);
throw new Error("Failed. Cannot get role.");
}
@Post("{userId}/role")
async addRole(@Path() userId: string, @Body() body: { role: string[] }) {
const list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
const result = await addUserRoles(
userId,
list.filter((v) => body.role.includes(v.id)),
);
if (!result) throw new Error("Failed. Cannot set user's role.");
}
@Delete("{userId}/role/{roleId}")
async deleteRole(@Path() userId: string, @Path() roleId: string) {
const list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
const result = await removeUserRoles(
userId,
list.filter((v) => roleId === v.id),
);
if (!result) throw new Error("Failed. Cannot remove user's role.");
}
}

View file

@ -0,0 +1,178 @@
import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
type MenuCreate = {
caption: string;
captionEN: string;
menuType: string;
url: string;
parentId?: string;
};
type MenuEdit = {
caption: string;
captionEN: string;
menuType: string;
url: string;
};
@Route("v1/permission/menu")
@Tags("Permission")
@Security("keycloak")
export class MenuController extends Controller {
@Get()
async listMenu() {
const record = await prisma.menu.findMany({
include: { children: true, roleMenuPermission: true },
orderBy: { createdAt: "asc" },
});
return record;
}
@Post()
async createMenu(@Body() body: MenuCreate) {
if (body.parentId) {
const parent = await prisma.menu.findFirst({ where: { id: body.parentId } });
if (!parent) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Parent menu not found.",
"missing_or_invalid_parameter",
);
}
}
const record = await prisma.menu.create({
include: { parent: true },
data: body,
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{menuId}")
async editMenu(@Path("menuId") id: string, @Body() body: MenuEdit) {
const record = await prisma.menu
.update({
include: { parent: true },
where: { id },
data: body,
})
.catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found");
}
throw new Error(e);
});
return record;
}
@Delete("{menuId}")
async deleteMenu(@Path("menuId") id: string) {
const record = await prisma.menu.deleteMany({ where: { id } });
if (record.count <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found");
}
}
}
type MenuComponentCreate = {
componentId: string;
componentTag: string;
menuId: string;
};
type MenuComponentEdit = {
componentId?: string;
componentTag?: string;
menuId?: string;
};
@Route("v1/permission/menu-component")
@Tags("Permission")
@Security("keycloak")
export class MenuComponentController extends Controller {
@Get()
async listMenuComponent() {
const record = await prisma.menuComponent.findMany({
include: { roleMenuComponentPermission: true },
orderBy: { createdAt: "asc" },
});
return record;
}
@Post()
async createMenuComponent(@Body() body: MenuComponentCreate) {
const menu = await prisma.menu.findFirst({ where: { id: body.menuId } });
if (!menu) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"missing_or_invalid_parameter",
);
}
const record = await prisma.menuComponent.create({
data: body,
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{menuComponentId}")
async editMenuComponent(@Path("menuComponentId") id: string, @Body() body: MenuComponentEdit) {
if (body.menuId) {
const menu = await prisma.menu.findFirst({ where: { id: body.menuId } });
if (!menu) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"missing_or_invalid_parameter",
);
}
}
const record = await prisma.menuComponent
.update({
include: { roleMenuComponentPermission: true },
where: { id },
data: body,
})
.catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Menu component cannot be found.",
"data_not_found",
);
}
throw new Error(e);
});
return record;
}
@Delete("{menuComponentId}")
async deleteMenuComponent(@Path("menuComponentId") id: string) {
const record = await prisma.menuComponent.deleteMany({ where: { id } });
if (record.count <= 0) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Menu component cannot be found.",
"data_not_found",
);
}
}
}

View file

@ -0,0 +1,576 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { Prisma, Status, UserType } from "@prisma/client";
import prisma from "../db";
import minio from "../services/minio";
import { RequestWithUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import {
addUserRoles,
createUser,
deleteUser,
editUser,
getRoles,
getUserRoles,
removeUserRoles,
} from "../services/keycloak";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type UserCreate = {
status?: Status;
userType: UserType;
userRole: string;
username: string;
firstName: string;
firstNameEN: string;
lastName: string;
lastNameEN: string;
gender: string;
checkpoint?: string | null;
checkpointEN?: string | null;
registrationNo?: string | null;
startDate?: Date | null;
retireDate?: Date | null;
discountCondition?: string | null;
licenseNo?: string | null;
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string | null;
birthDate?: Date | null;
address: string;
addressEN: string;
zipCode: string;
email: string;
telephoneNo: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type UserUpdate = {
status?: "ACTIVE" | "INACTIVE";
username?: string;
userType?: UserType;
userRole?: string;
firstName?: string;
firstNameEN?: string;
lastName?: string;
lastNameEN?: string;
gender?: string;
checkpoint?: string | null;
checkpointEN?: string | null;
registrationNo?: string | null;
startDate?: Date | null;
retireDate?: Date | null;
discountCondition?: string | null;
licenseNo?: string | null;
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string | null;
birthDate?: Date | null;
address?: string;
addressEN?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
function imageLocation(id: string) {
return `user/profile-img-${id}`;
}
@Route("api/user")
@Tags("User")
@Security("keycloak")
export class UserController extends Controller {
@Get("type-stats")
async getUserTypeStats() {
const list = await prisma.user.groupBy({
by: "userType",
_count: true,
});
return list.reduce<Record<UserType, number>>(
(a, c) => {
a[c.userType] = c._count;
return a;
},
{
USER: 0,
MESSENGER: 0,
DELEGATE: 0,
AGENCY: 0,
},
);
}
@Get()
async getUser(
@Query() userType?: UserType,
@Query() zipCode?: string,
@Query() includeBranch: boolean = false,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ firstName: { contains: query }, zipCode, userType },
{ firstNameEN: { contains: query }, zipCode, userType },
{ lastName: { contains: query }, zipCode, userType },
{ lastNameEN: { contains: query }, zipCode, userType },
{ email: { contains: query }, zipCode, userType },
{ telephoneNo: { contains: query }, zipCode, userType },
],
} satisfies Prisma.UserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.user.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
branch: { include: { branch: includeBranch } },
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.user.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{userId}")
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record)
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
60 * 60,
),
});
}
@Post()
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
if (body.provinceId || body.districtId || body.subDistrictId) {
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
]);
if (body.provinceId && !province) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
}
if (body.districtId && !district) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
}
if (body.subDistrictId && !subDistrict) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
}
}
const { provinceId, districtId, subDistrictId, username, ...rest } = body;
let list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
if (Array.isArray(list)) {
list = list.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)),
);
}
const userId = await createUser(username, username, {
firstName: body.firstName,
lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"],
});
if (!userId || typeof userId !== "string") {
throw new Error("Cannot create user with keycloak service.");
}
const role = list.find((v) => v.name === body.userRole);
const resultAddRole = role && (await addUserRoles(userId, [role]));
if (!resultAddRole) {
await deleteUser(userId);
throw new Error("Failed. Cannot set user's role.");
}
const record = await prisma.user.create({
include: { province: true, district: true, subDistrict: true },
data: {
id: userId,
...rest,
username,
userRole: role.name,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{userId}")
async editUser(
@Request() req: RequestWithUser,
@Body() body: UserUpdate,
@Path() userId: string,
) {
if (body.subDistrictId || body.districtId || body.provinceId) {
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
}
let userRole: string | undefined;
if (body.userRole) {
let list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
if (Array.isArray(list)) {
list = list.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) =>
a.name.includes(b),
),
);
}
const currentRole = await getUserRoles(userId);
const role = list.find((v) => v.name === body.userRole);
const resultAddRole = role && (await addUserRoles(userId, [role]));
if (!resultAddRole) {
throw new Error("Failed. Cannot set user's role.");
} else {
if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole);
}
userRole = role.name;
}
if (body.username) {
await editUser(userId, { username: body.username });
}
const { provinceId, districtId, subDistrictId, ...rest } = body;
const user = await prisma.user.findFirst({
where: { id: userId },
});
if (!user) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const lastUserOfType =
body.userType &&
body.userType !== user.userType &&
user.code &&
(await prisma.user.findFirst({
orderBy: { createdAt: "desc" },
where: {
userType: body.userType,
code: { startsWith: `${user.code?.slice(0, 3)}` },
},
}));
const record = await prisma.user.update({
include: { province: true, district: true, subDistrict: true },
data: {
...rest,
userRole,
code:
(lastUserOfType &&
`${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) ||
undefined,
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
updateBy: req.user.name,
},
where: { id: userId },
});
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{userId}")
async deleteUser(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used");
}
await minio.removeObject(MINIO_BUCKET, imageLocation(userId), {
forceDelete: true,
});
new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`);
stream.on("data", (v) => v && v.name && item.push(v.name));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("MinIO error.")));
}).then((list) => {
list.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
});
});
await deleteUser(userId);
return await prisma.user.delete({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
}
}
function attachmentLocation(uid: string) {
return `user-attachment/${uid}`;
}
@Route("api/user/{userId}/attachment")
@Tags("User")
@Security("keycloak")
export class UserAttachmentController extends Controller {
@Get()
async listAttachment(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
const list = await new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`);
stream.on("data", (v) => v && v.name && item.push(v.name));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("MinIO error.")));
});
return await Promise.all(
list.map(async (v) => ({
name: v.split("/").at(-1) as string,
url: await minio.presignedGetObject(MINIO_BUCKET, v, 12 * 60 * 60),
})),
);
}
@Post()
async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
return await Promise.all(
payload.file.map(async (v) => ({
name: v,
url: await minio.presignedGetObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`),
uploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
`${attachmentLocation(userId)}/${v}`,
12 * 60 * 60,
),
})),
);
}
@Delete()
async deleteAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
await Promise.all(
payload.file.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
}),
);
}
}

View file

@ -1,18 +1,29 @@
import HttpStatus from "./http-status";
type DevMessage =
| "missing_or_invalid_parameter"
| "data_exists"
| "data_in_used"
| "no_permission"
| "unknown_url"
| "data_not_found"
| "unauthorized";
class HttpError extends Error {
/**
* HTTP Status Code
*/
status: HttpStatus;
message: string;
devMessage?: DevMessage;
constructor(status: HttpStatus, message: string) {
constructor(status: HttpStatus, message: string, devMessage?: DevMessage) {
super(message);
this.name = "HttpError";
this.status = status;
this.message = message;
this.devMessage = devMessage;
}
}

View file

@ -16,11 +16,7 @@ const jwtVerify = createVerifier({
const jwtDecode = createDecoder();
export async function keycloakAuth(
request: Express.Request,
_securityName?: string,
_scopes?: string[],
) {
export async function keycloakAuth(request: Express.Request, roles?: string[]) {
const token = request.headers["authorization"]?.includes("Bearer ")
? request.headers["authorization"].split(" ")[1]
: request.headers["authorization"];
@ -39,7 +35,7 @@ export async function keycloakAuth(
payload = await verifyOffline(token);
break;
default:
if (process.env.KC_REALM_URL) {
if (process.env.KC_URL) {
payload = await verifyOnline(token);
break;
}
@ -49,6 +45,12 @@ export async function keycloakAuth(
}
}
if (Array.isArray(payload.roles) && Array.isArray(roles) && roles.length > 0) {
if (!roles.some((a: string) => payload.roles.includes(a))) {
throw new HttpError(HttpStatus.FORBIDDEN, "คุณไม่มีสิทธิในการเข้าถึงข้อมูลดังกล่าว");
}
}
return payload;
}

View file

@ -6,11 +6,11 @@ import { keycloakAuth } from "./auth-provider/keycloak";
export async function expressAuthentication(
request: Express.Request,
securityName: string,
_scopes?: string[],
scopes?: string[],
) {
switch (securityName) {
case "keycloak":
return keycloakAuth(request);
return keycloakAuth(request, scopes);
default:
throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "ไม่ทราบวิธียืนยันตัวตน");
}

View file

@ -8,6 +8,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction)
return res.status(error.status).json({
status: error.status,
message: error.message,
devMessage: error.devMessage,
});
}
@ -16,6 +17,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction)
status: HttpStatus.UNPROCESSABLE_ENTITY,
message: "Validation error(s).",
detail: error.fields,
devMessage: "missing_or_invalid_parameter",
});
}
@ -24,6 +26,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction)
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: error.message,
devMessage: "system_error",
});
}

79
src/middlewares/log.ts Normal file
View file

@ -0,0 +1,79 @@
import { NextFunction, Request, Response } from "express";
import elasticsearch from "../services/elasticsearch";
if (!process.env.ELASTICSEARCH_INDEX) {
throw new Error("Require ELASTICSEARCH_INDEX to store log.");
}
const ELASTICSEARCH_INDEX = process.env.ELASTICSEARCH_INDEX;
const LOG_LEVEL_MAP: Record<string, number> = {
debug: 4,
info: 3,
warning: 2,
error: 1,
none: 0,
};
async function logMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.url.startsWith("/api/")) return next();
let data: any;
const originalJson = res.json;
res.json = function (v: any) {
data = v;
return originalJson.call(this, v);
};
const timestamp = new Date().toString();
const start = performance.now();
req.app.locals.logData = {};
res.on("finish", () => {
if (!req.url.startsWith("/api/")) return;
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "info"] || 1;
if (level === 1 && res.statusCode < 500) return;
if (level === 2 && res.statusCode < 400) return;
if (level === 3 && res.statusCode < 200) return;
const obj = {
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
systemName: "JWS-SOS",
startTimeStamp: timestamp,
endTimeStamp: new Date().toString(),
processTime: performance.now() - start,
host: req.hostname,
sessionId: req.headers["x-session-id"],
rtId: req.headers["x-rtid"],
tId: req.headers["x-tid"],
method: req.method,
endpoint: req.url,
responseCode: res.statusCode,
responseDescription:
data?.devMessage !== undefined
? data.devMessage
: { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[
res.statusCode
],
input: (level === 4 && JSON.stringify(req.body, null, 2)) || undefined,
output: (level === 4 && JSON.stringify(data, null, 2)) || undefined,
...req.app.locals.logData,
};
console.log(obj);
elasticsearch.index({
index: ELASTICSEARCH_INDEX,
document: obj,
});
});
return next();
}
export default logMiddleware;

View file

@ -0,0 +1,7 @@
import { Client } from "@elastic/elasticsearch";
const elasticsearch = new Client({
node: `${process.env.ELASTICSEARCH_PROTOCOL}://${process.env.ELASTICSEARCH_HOST}:${process.env.ELASTICSEARCH_PORT}`,
});
export default elasticsearch;

View file

@ -91,6 +91,63 @@ export async function createUser(username: string, password: string, opts?: Reco
return id || true;
}
/**
* Update keycloak user by uuid
*
* Client must have permission to manage realm's user
*
* @returns user uuid or true if success, false otherwise.
*/
export async function editUser(userId: string, opts: Record<string, any>) {
const { password, ...rest } = opts;
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "PUT",
body: JSON.stringify({
enabled: true,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
...rest,
}),
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false;
if (!res.ok) {
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
}
const path = res.headers.get("Location");
const id = path?.split("/").at(-1);
return id || true;
}
/**
* Delete keycloak user by uuid
*
* Client must have permission to manage realm's user
*
* @returns user uuid or true if success, false otherwise.
*/
export async function deleteUser(userId: string) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "DELETE",
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false;
if (!res.ok) {
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
}
}
/**
* Get roles list or specific role data
*
@ -121,7 +178,46 @@ export async function getRoles(name?: string) {
const data = await res.json();
if (Array.isArray(data)) {
return data.map((v: Record<string, any>) => ({ id: v.id, name: v.name }));
return data.map((v: Record<string, string>) => ({ id: v.id, name: v.name }));
}
return {
id: data.id,
name: data.name,
};
}
/**
* Get roles list of user
*
* Client must have permission to get realms roles
*
* @returns role's info (array if not specify name) if success, null if not found, false otherwise.
*/
export async function getUserRoles(userId: string) {
const res = await fetch(
`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`,
{
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
},
},
).catch((e) => console.log(e));
if (!res) return false;
if (!res.ok && res.status !== 404) {
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
}
if (res.status === 404) {
return null;
}
const data = await res.json();
if (Array.isArray(data)) {
return data.map((v: Record<string, string>) => ({ id: v.id, name: v.name }));
}
return {
@ -137,7 +233,7 @@ export async function getRoles(name?: string) {
*
* @returns true if success, false otherwise.
*/
export async function addUserRoles(userId: string, roleId: string[]) {
export async function addUserRoles(userId: string, roles: { id: string; name: string }[]) {
const res = await fetch(
`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`,
{
@ -147,7 +243,7 @@ export async function addUserRoles(userId: string, roleId: string[]) {
"content-type": `application/json`,
},
method: "POST",
body: JSON.stringify(roleId.map((v) => ({ id: v }))),
body: JSON.stringify(roles),
},
).catch((e) => console.log(e));
@ -165,7 +261,7 @@ export async function addUserRoles(userId: string, roleId: string[]) {
*
* @returns true if success, false otherwise.
*/
export async function removeUserRoles(userId: string, roleId: string[]) {
export async function removeUserRoles(userId: string, roles: { id: string; name: string }[]) {
const res = await fetch(
`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`,
{
@ -175,7 +271,7 @@ export async function removeUserRoles(userId: string, roleId: string[]) {
"content-type": `application/json`,
},
method: "DELETE",
body: JSON.stringify(roleId.map((v) => ({ id: v }))),
body: JSON.stringify(roles),
},
).catch((e) => console.log(e));

View file

@ -12,6 +12,28 @@
"description": "Keycloak Bearer Token",
"in": "header"
}
},
"spec": {
"tags": [
{ "name": "OpenAPI" },
{ "name": "Single-Sign On" },
{ "name": "Permission" },
{ "name": "Address" },
{ "name": "Branch" },
{ "name": "Branch Contact" },
{ "name": "User" },
{ "name": "Branch User" },
{ "name": "Customer" },
{ "name": "Customer Branch" },
{ "name": "Employee" },
{ "name": "Employee Checkup" },
{ "name": "Employee Work" },
{ "name": "Employee Other Info" },
{ "name": "Service" },
{ "name": "Work" },
{ "name": "Product Type" },
{ "name": "Product Group" }
]
}
},
"routes": {