Merge branch 'dev'

This commit is contained in:
Methapon2001 2024-07-01 08:52:19 +07:00
commit 3d9f0881c3
50 changed files with 6949 additions and 2203 deletions

View file

@ -1,8 +1,8 @@
KC_URL=http://192.168.1.20:8080 KC_URL=http://192.168.1.20:8080
KC_REALM=dev KC_REALM=dev
KC_SERVICE_ACCOUNT_CLIENT_ID=dev-service KC_ADMIN_USERNAME=admin
KC_SERVICE_ACCOUNT_SECRET= KC_ADMIN_PASSWORD=
APP_HOST=0.0.0.0 APP_HOST=0.0.0.0
APP_PORT=3000 APP_PORT=3000

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.DS_S .DS_S
node_modules node_modules
/src/generated
.env .env
.env.* .env.*

View file

@ -5,7 +5,7 @@ ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable
RUN apt-get update && apt-get install -y openssl RUN apt-get update && apt-get install -y openssl
RUN pnpm i -g prisma RUN pnpm i -g prisma prisma-kysely
WORKDIR /app WORKDIR /app

View file

@ -22,21 +22,24 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^20.12.2", "@types/node": "^20.12.2",
"@types/swagger-ui-express": "^4.1.6", "@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.1.0", "nodemon": "^3.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prisma": "^5.12.1", "prisma": "^5.16.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.3" "typescript": "^5.4.3"
}, },
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "^8.13.0", "@elastic/elasticsearch": "^8.13.0",
"@prisma/client": "5.12.1", "@prisma/client": "^5.16.0",
"@tsoa/runtime": "^6.2.0", "@tsoa/runtime": "^6.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"fast-jwt": "^4.0.0", "fast-jwt": "^4.0.0",
"kysely": "^0.27.3",
"minio": "^7.1.3", "minio": "^7.1.3",
"prisma-extension-kysely": "^2.1.0",
"promise.any": "^2.0.6", "promise.any": "^2.0.6",
"swagger-ui-express": "^5.0.0", "swagger-ui-express": "^5.0.0",
"tsoa": "^6.2.0" "tsoa": "^6.2.0"

4664
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +0,0 @@
/*
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

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

View file

@ -1,9 +0,0 @@
/*
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

@ -1,8 +0,0 @@
/*
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

@ -1,11 +0,0 @@
/*
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

@ -1,119 +0,0 @@
/*
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

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

View file

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

View file

@ -1,8 +0,0 @@
/*
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

@ -1,12 +0,0 @@
/*
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

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

View file

@ -1,8 +0,0 @@
/*
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

@ -1,9 +0,0 @@
/*
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

@ -1,8 +0,0 @@
/*
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

@ -4,6 +4,103 @@ CREATE TYPE "Status" AS ENUM ('CREATED', 'ACTIVE', 'INACTIVE');
-- CreateEnum -- CreateEnum
CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY'); CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY');
-- CreateEnum
CREATE TYPE "CustomerType" AS ENUM ('CORP', 'PERS');
-- 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,
"updatedBy" 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,
"updatedBy" 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,
"updatedBy" 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,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MenuComponent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RoleMenuComponentPermission" (
"id" TEXT NOT NULL,
"userRole" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"menuComponentId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RoleMenuComponentPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RunningNo" (
"key" TEXT NOT NULL,
"value" INTEGER NOT NULL,
CONSTRAINT "RunningNo_pkey" PRIMARY KEY ("key")
);
-- 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,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserMenuComponentPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "Province" ( CREATE TABLE "Province" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
@ -11,7 +108,7 @@ CREATE TABLE "Province" (
"nameEN" TEXT NOT NULL, "nameEN" TEXT NOT NULL,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Province_pkey" PRIMARY KEY ("id") CONSTRAINT "Province_pkey" PRIMARY KEY ("id")
@ -25,7 +122,7 @@ CREATE TABLE "District" (
"provinceId" TEXT NOT NULL, "provinceId" TEXT NOT NULL,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "District_pkey" PRIMARY KEY ("id") CONSTRAINT "District_pkey" PRIMARY KEY ("id")
@ -40,7 +137,7 @@ CREATE TABLE "SubDistrict" (
"districtId" TEXT NOT NULL, "districtId" TEXT NOT NULL,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id") CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id")
@ -55,20 +152,23 @@ CREATE TABLE "Branch" (
"nameEN" TEXT NOT NULL, "nameEN" TEXT NOT NULL,
"address" TEXT NOT NULL, "address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL, "addressEN" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"provinceId" TEXT, "provinceId" TEXT,
"districtId" TEXT, "districtId" TEXT,
"subDistrictId" TEXT, "subDistrictId" TEXT,
"zipCode" TEXT NOT NULL, "zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL, "email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL, "contactName" TEXT,
"lineId" TEXT,
"latitude" TEXT NOT NULL, "latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL, "longitude" TEXT NOT NULL,
"isHeadOffice" BOOLEAN NOT NULL DEFAULT false, "isHeadOffice" BOOLEAN NOT NULL DEFAULT false,
"headOfficeId" TEXT, "headOfficeId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Branch_pkey" PRIMARY KEY ("id") CONSTRAINT "Branch_pkey" PRIMARY KEY ("id")
@ -78,11 +178,10 @@ CREATE TABLE "Branch" (
CREATE TABLE "BranchContact" ( CREATE TABLE "BranchContact" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL, "telephoneNo" TEXT NOT NULL,
"lineId" TEXT NOT NULL,
"branchId" TEXT NOT NULL, "branchId" TEXT NOT NULL,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchContact_pkey" PRIMARY KEY ("id") CONSTRAINT "BranchContact_pkey" PRIMARY KEY ("id")
@ -95,7 +194,7 @@ CREATE TABLE "BranchUser" (
"userId" TEXT NOT NULL, "userId" TEXT NOT NULL,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchUser_pkey" PRIMARY KEY ("id") CONSTRAINT "BranchUser_pkey" PRIMARY KEY ("id")
@ -104,12 +203,12 @@ CREATE TABLE "BranchUser" (
-- CreateTable -- CreateTable
CREATE TABLE "User" ( CREATE TABLE "User" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"keycloakId" TEXT NOT NULL,
"code" TEXT, "code" TEXT,
"firstName" TEXT NOT NULL, "firstName" TEXT NOT NULL,
"firstNameEN" TEXT NOT NULL, "firstNameEN" TEXT NOT NULL,
"lastName" TEXT NOT NULL, "lastName" TEXT NOT NULL,
"lastNameEN" TEXT NOT NULL, "lastNameEN" TEXT NOT NULL,
"username" TEXT NOT NULL,
"gender" TEXT NOT NULL, "gender" TEXT NOT NULL,
"address" TEXT NOT NULL, "address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL, "addressEN" TEXT NOT NULL,
@ -122,6 +221,8 @@ CREATE TABLE "User" (
"registrationNo" TEXT, "registrationNo" TEXT,
"startDate" TIMESTAMP(3), "startDate" TIMESTAMP(3),
"retireDate" TIMESTAMP(3), "retireDate" TIMESTAMP(3),
"checkpoint" TEXT,
"checkpointEN" TEXT,
"userType" "UserType" NOT NULL, "userType" "UserType" NOT NULL,
"userRole" TEXT NOT NULL, "userRole" TEXT NOT NULL,
"discountCondition" TEXT, "discountCondition" TEXT,
@ -131,10 +232,13 @@ CREATE TABLE "User" (
"sourceNationality" TEXT, "sourceNationality" TEXT,
"importNationality" TEXT, "importNationality" TEXT,
"trainingPlace" TEXT, "trainingPlace" TEXT,
"responsibleArea" TEXT,
"birthDate" TIMESTAMP(3),
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id") CONSTRAINT "User_pkey" PRIMARY KEY ("id")
@ -144,14 +248,17 @@ CREATE TABLE "User" (
CREATE TABLE "Customer" ( CREATE TABLE "Customer" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"code" TEXT NOT NULL, "code" TEXT NOT NULL,
"customerType" TEXT NOT NULL, "personName" TEXT NOT NULL,
"personNameEN" TEXT,
"customerType" "CustomerType" NOT NULL,
"customerName" TEXT NOT NULL, "customerName" TEXT NOT NULL,
"customerNameEN" TEXT NOT NULL, "customerNameEN" TEXT NOT NULL,
"imageUrl" TEXT, "taxNo" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id") CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
@ -160,12 +267,13 @@ CREATE TABLE "Customer" (
-- CreateTable -- CreateTable
CREATE TABLE "CustomerBranch" ( CREATE TABLE "CustomerBranch" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"branchNo" TEXT NOT NULL, "branchNo" INTEGER NOT NULL,
"code" TEXT NOT NULL,
"legalPersonNo" TEXT NOT NULL, "legalPersonNo" TEXT NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL, "nameEN" TEXT NOT NULL,
"customerId" TEXT NOT NULL, "customerId" TEXT NOT NULL,
"taxNo" TEXT NOT NULL, "taxNo" TEXT,
"registerName" TEXT NOT NULL, "registerName" TEXT NOT NULL,
"registerDate" TIMESTAMP(3) NOT NULL, "registerDate" TIMESTAMP(3) NOT NULL,
"authorizedCapital" TEXT NOT NULL, "authorizedCapital" TEXT NOT NULL,
@ -177,12 +285,20 @@ CREATE TABLE "CustomerBranch" (
"zipCode" TEXT NOT NULL, "zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL, "email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL, "telephoneNo" TEXT NOT NULL,
"latitude" TEXT NOT NULL, "employmentOffice" TEXT NOT NULL,
"longitude" TEXT NOT NULL, "bussinessType" TEXT NOT NULL,
"bussinessTypeEN" TEXT NOT NULL,
"jobPosition" TEXT NOT NULL,
"jobPositionEN" TEXT NOT NULL,
"jobDescription" TEXT NOT NULL,
"saleEmployee" TEXT NOT NULL,
"payDate" TIMESTAMP(3) NOT NULL,
"wageRate" INTEGER NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerBranch_pkey" PRIMARY KEY ("id") CONSTRAINT "CustomerBranch_pkey" PRIMARY KEY ("id")
@ -200,42 +316,70 @@ CREATE TABLE "Employee" (
"dateOfBirth" TIMESTAMP(3) NOT NULL, "dateOfBirth" TIMESTAMP(3) NOT NULL,
"gender" TEXT NOT NULL, "gender" TEXT NOT NULL,
"nationality" TEXT NOT NULL, "nationality" TEXT NOT NULL,
"address" TEXT NOT NULL, "address" TEXT,
"addressEN" TEXT NOT NULL, "addressEN" TEXT,
"provinceId" TEXT, "provinceId" TEXT,
"districtId" TEXT, "districtId" TEXT,
"subDistrictId" TEXT, "subDistrictId" TEXT,
"zipCode" TEXT NOT NULL, "zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL, "passportType" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL, "passportNumber" TEXT NOT NULL,
"arrivalBarricade" TEXT NOT NULL, "passportIssueDate" TIMESTAMP(3) NOT NULL,
"arrivalCardNo" TEXT NOT NULL, "passportExpiryDate" TIMESTAMP(3) NOT NULL,
"passportIssuingCountry" TEXT NOT NULL,
"passportIssuingPlace" TEXT NOT NULL,
"previousPassportReference" TEXT,
"visaType" TEXT,
"visaNumber" TEXT,
"visaIssueDate" TIMESTAMP(3),
"visaExpiryDate" TIMESTAMP(3),
"visaIssuingPlace" TEXT,
"visaStayUntilDate" TIMESTAMP(3),
"tm6Number" TEXT,
"entryDate" TIMESTAMP(3),
"workerStatus" TEXT,
"customerBranchId" TEXT, "customerBranchId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Employee_pkey" PRIMARY KEY ("id") CONSTRAINT "Employee_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "EmployeeHistory" (
"id" TEXT NOT NULL,
"field" TEXT NOT NULL,
"valueBefore" JSONB NOT NULL,
"valueAfter" JSONB NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedByUserId" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"masterId" TEXT NOT NULL,
CONSTRAINT "EmployeeHistory_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "EmployeeCheckup" ( CREATE TABLE "EmployeeCheckup" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL, "employeeId" TEXT NOT NULL,
"checkupResult" TEXT NOT NULL, "checkupResult" TEXT,
"checkupType" TEXT NOT NULL, "checkupType" TEXT,
"provinceId" TEXT, "provinceId" TEXT,
"hospitalName" TEXT NOT NULL, "hospitalName" TEXT,
"remark" TEXT NOT NULL, "remark" TEXT,
"medicalBenefitScheme" TEXT NOT NULL, "medicalBenefitScheme" TEXT,
"insuranceCompany" TEXT NOT NULL, "insuranceCompany" TEXT,
"coverageStartDate" TIMESTAMP(3) NOT NULL, "coverageStartDate" TIMESTAMP(3),
"coverageExpireDate" TIMESTAMP(3) NOT NULL, "coverageExpireDate" TIMESTAMP(3),
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeCheckup_pkey" PRIMARY KEY ("id") CONSTRAINT "EmployeeCheckup_pkey" PRIMARY KEY ("id")
@ -245,17 +389,18 @@ CREATE TABLE "EmployeeCheckup" (
CREATE TABLE "EmployeeWork" ( CREATE TABLE "EmployeeWork" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL, "employeeId" TEXT NOT NULL,
"ownerName" TEXT NOT NULL, "ownerName" TEXT,
"positionName" TEXT NOT NULL, "positionName" TEXT,
"jobType" TEXT NOT NULL, "jobType" TEXT,
"workplace" TEXT NOT NULL, "workplace" TEXT,
"workPermitNo" TEXT NOT NULL, "workPermitNo" TEXT,
"workPermitIssuDate" TIMESTAMP(3) NOT NULL, "workPermitIssuDate" TIMESTAMP(3),
"workPermitExpireDate" TIMESTAMP(3) NOT NULL, "workPermitExpireDate" TIMESTAMP(3),
"workEndDate" TIMESTAMP(3) NOT NULL, "workEndDate" TIMESTAMP(3),
"remark" TEXT,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeWork_pkey" PRIMARY KEY ("id") CONSTRAINT "EmployeeWork_pkey" PRIMARY KEY ("id")
@ -265,13 +410,20 @@ CREATE TABLE "EmployeeWork" (
CREATE TABLE "EmployeeOtherInfo" ( CREATE TABLE "EmployeeOtherInfo" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL, "employeeId" TEXT NOT NULL,
"citizenId" TEXT NOT NULL, "citizenId" TEXT,
"fatherFullName" TEXT NOT NULL, "fatherBirthPlace" TEXT,
"motherFullName" TEXT NOT NULL, "fatherFirstName" TEXT,
"birthPlace" TEXT NOT NULL, "fatherLastName" TEXT,
"motherBirthPlace" TEXT,
"motherFirstName" TEXT,
"motherLastName" TEXT,
"fatherFirstNameEN" TEXT,
"fatherLastNameEN" TEXT,
"motherFirstNameEN" TEXT,
"motherLastNameEN" TEXT,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeOtherInfo_pkey" PRIMARY KEY ("id") CONSTRAINT "EmployeeOtherInfo_pkey" PRIMARY KEY ("id")
@ -283,10 +435,12 @@ CREATE TABLE "Service" (
"code" TEXT NOT NULL, "code" TEXT NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"detail" TEXT NOT NULL, "detail" TEXT NOT NULL,
"attributes" JSONB,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Service_pkey" PRIMARY KEY ("id") CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
@ -297,11 +451,13 @@ CREATE TABLE "Work" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"order" INTEGER NOT NULL, "order" INTEGER NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"serviceId" TEXT NOT NULL, "attributes" JSONB,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"serviceId" TEXT,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Work_pkey" PRIMARY KEY ("id") CONSTRAINT "Work_pkey" PRIMARY KEY ("id")
@ -309,14 +465,15 @@ CREATE TABLE "Work" (
-- CreateTable -- CreateTable
CREATE TABLE "WorkProduct" ( CREATE TABLE "WorkProduct" (
"id" TEXT NOT NULL, "order" INTEGER NOT NULL,
"workId" TEXT NOT NULL, "workId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("id") CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("workId","productId")
); );
-- CreateTable -- CreateTable
@ -327,9 +484,10 @@ CREATE TABLE "ProductGroup" (
"detail" TEXT NOT NULL, "detail" TEXT NOT NULL,
"remark" TEXT NOT NULL, "remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductGroup_pkey" PRIMARY KEY ("id") CONSTRAINT "ProductGroup_pkey" PRIMARY KEY ("id")
@ -343,10 +501,12 @@ CREATE TABLE "ProductType" (
"detail" TEXT NOT NULL, "detail" TEXT NOT NULL,
"remark" TEXT NOT NULL, "remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"productGroupId" TEXT NOT NULL,
CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id") CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id")
); );
@ -357,22 +517,49 @@ CREATE TABLE "Product" (
"code" TEXT NOT NULL, "code" TEXT NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"detail" TEXT NOT NULL, "detail" TEXT NOT NULL,
"process" TEXT NOT NULL, "process" INTEGER NOT NULL,
"price" INTEGER NOT NULL, "price" DOUBLE PRECISION NOT NULL,
"agentPrice" INTEGER NOT NULL, "agentPrice" DOUBLE PRECISION NOT NULL,
"serviceCharge" INTEGER NOT NULL, "serviceCharge" DOUBLE PRECISION NOT NULL,
"imageUrl" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED', "status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"remark" TEXT,
"productTypeId" TEXT, "productTypeId" TEXT,
"productGroupId" TEXT,
"createdBy" TEXT, "createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT, "updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id") CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
); );
-- CreateIndex
CREATE UNIQUE INDEX "RunningNo_key_key" ON "RunningNo"("key");
-- 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;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "District" ADD CONSTRAINT "District_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "District" ADD CONSTRAINT "District_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@ -433,6 +620,12 @@ ALTER TABLE "Employee" ADD CONSTRAINT "Employee_subDistrictId_fkey" FOREIGN KEY
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_customerBranchId_fkey" FOREIGN KEY ("customerBranchId") REFERENCES "CustomerBranch"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Employee" ADD CONSTRAINT "Employee_customerBranchId_fkey" FOREIGN KEY ("customerBranchId") REFERENCES "CustomerBranch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeHistory" ADD CONSTRAINT "EmployeeHistory_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeHistory" ADD CONSTRAINT "EmployeeHistory_masterId_fkey" FOREIGN KEY ("masterId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@ -452,7 +645,10 @@ ALTER TABLE "Work" ADD CONSTRAINT "Work_serviceId_fkey" FOREIGN KEY ("serviceId"
ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "Work"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "Work"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_productTypeId_fkey" FOREIGN KEY ("productTypeId") REFERENCES "ProductType"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "ProductType" ADD CONSTRAINT "ProductType_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("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;

View file

@ -2,6 +2,11 @@ generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
generator kysely {
provider = "prisma-kysely"
output = "../src/generated/kysely"
}
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
@ -17,7 +22,7 @@ model Menu {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id]) parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
@ -40,7 +45,7 @@ model RoleMenuPermission {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -57,7 +62,7 @@ model UserMenuPermission {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -72,7 +77,7 @@ model MenuComponent {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
roleMenuComponentPermission RoleMenuComponentPermission[] roleMenuComponentPermission RoleMenuComponentPermission[]
userMennuComponentPermission UserMenuComponentPermission[] userMennuComponentPermission UserMenuComponentPermission[]
@ -89,10 +94,15 @@ model RoleMenuComponentPermission {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model RunningNo {
key String @id @unique
value Int
}
model UserMenuComponentPermission { model UserMenuComponentPermission {
id String @id @default(uuid()) id String @id @default(uuid())
@ -106,7 +116,7 @@ model UserMenuComponentPermission {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -117,7 +127,7 @@ model Province {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
district District[] district District[]
@ -138,7 +148,7 @@ model District {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
subDistrict SubDistrict[] subDistrict SubDistrict[]
@ -159,7 +169,7 @@ model SubDistrict {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
branch Branch[] branch Branch[]
@ -207,11 +217,12 @@ model Branch {
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id]) headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
headOfficeId String? headOfficeId String?
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
branch Branch[] @relation(name: "HeadOfficeRelation") branch Branch[] @relation(name: "HeadOfficeRelation")
@ -228,7 +239,7 @@ model BranchContact {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -243,7 +254,7 @@ model BranchUser {
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -307,16 +318,18 @@ model User {
birthDate DateTime? birthDate DateTime?
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
branch BranchUser[] branch BranchUser[]
userMenuPermission UserMenuPermission[] userMenuPermission UserMenuPermission[]
userMenuComponentPermission UserMenuComponentPermission[] userMenuComponentPermission UserMenuComponentPermission[]
employeeHistory EmployeeHistory[]
} }
enum CustomerType { enum CustomerType {
@ -327,15 +340,19 @@ enum CustomerType {
model Customer { model Customer {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String
personName String
personNameEN String?
customerType CustomerType customerType CustomerType
customerName String customerName String
customerNameEN String customerNameEN String
taxNo String?
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
branch CustomerBranch[] branch CustomerBranch[]
@ -343,7 +360,8 @@ model Customer {
model CustomerBranch { model CustomerBranch {
id String @id @default(uuid()) id String @id @default(uuid())
branchNo String branchNo Int
code String
legalPersonNo String legalPersonNo String
name String name String
@ -352,7 +370,7 @@ model CustomerBranch {
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String customerId String
taxNo String taxNo String?
registerName String registerName String
registerDate DateTime registerDate DateTime
authorizedCapital String authorizedCapital String
@ -374,14 +392,22 @@ model CustomerBranch {
email String email String
telephoneNo String telephoneNo String
latitude String employmentOffice String
longitude String bussinessType String
bussinessTypeEN String
jobPosition String
jobPositionEN String
jobDescription String
saleEmployee String
payDate DateTime
wageRate Int
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
employee Employee[] employee Employee[]
@ -401,8 +427,8 @@ model Employee {
gender String gender String
nationality String nationality String
address String address String?
addressEN String addressEN String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String? provinceId String?
@ -415,25 +441,57 @@ model Employee {
zipCode String zipCode String
email String passportType String
telephoneNo String passportNumber String
passportIssueDate DateTime
passportExpiryDate DateTime
passportIssuingCountry String
passportIssuingPlace String
previousPassportReference String?
arrivalBarricade String visaType String?
arrivalCardNo String visaNumber String?
visaIssueDate DateTime?
visaExpiryDate DateTime?
visaIssuingPlace String?
visaStayUntilDate DateTime?
tm6Number String?
entryDate DateTime?
workerStatus String?
customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull) customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull)
customerBranchId String? customerBranchId String?
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
employeeCheckup EmployeeCheckup[] employeeCheckup EmployeeCheckup[]
employeeWork EmployeeWork[] employeeWork EmployeeWork[]
employeeOtherInfo EmployeeOtherInfo[] employeeOtherInfo EmployeeOtherInfo[]
editHistory EmployeeHistory[]
}
model EmployeeHistory {
id String @id @default(uuid())
field String
valueBefore Json
valueAfter Json
timestamp DateTime @default(now())
updatedByUserId String?
updatedByUser User? @relation(fields: [updatedByUserId], references: [id])
updatedBy String?
updatedAt DateTime @default(now())
masterId String
master Employee @relation(fields: [masterId], references: [id], onDelete: Cascade)
} }
model EmployeeCheckup { model EmployeeCheckup {
@ -442,22 +500,22 @@ model EmployeeCheckup {
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String employeeId String
checkupResult String checkupResult String?
checkupType String checkupType String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String? provinceId String?
hospitalName String hospitalName String?
remark String remark String?
medicalBenefitScheme String medicalBenefitScheme String?
insuranceCompany String insuranceCompany String?
coverageStartDate DateTime coverageStartDate DateTime?
coverageExpireDate DateTime coverageExpireDate DateTime?
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -467,18 +525,19 @@ model EmployeeWork {
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String employeeId String
ownerName String ownerName String?
positionName String positionName String?
jobType String jobType String?
workplace String workplace String?
workPermitNo String workPermitNo String?
workPermitIssuDate DateTime workPermitIssuDate DateTime?
workPermitExpireDate DateTime workPermitExpireDate DateTime?
workEndDate DateTime workEndDate DateTime?
remark String?
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -488,61 +547,78 @@ model EmployeeOtherInfo {
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String employeeId String
citizenId String citizenId String?
fatherFullName String fatherBirthPlace String?
motherFullName String fatherFirstName String?
birthPlace String fatherLastName String?
motherBirthPlace String?
motherFirstName String?
motherLastName String?
fatherFirstNameEN String?
fatherLastNameEN String?
motherFirstNameEN String?
motherLastNameEN String?
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Service { model Service {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String
name String name String
detail String detail String
attributes Json?
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
work Work[]
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
work Work[]
} }
model Work { model Work {
id String @id @default(uuid()) id String @id @default(uuid())
order Int order Int
name String name String
attributes Json?
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) status Status @default(CREATED)
serviceId String statusOrder Int @default(0)
status Status @default(CREATED) service Service? @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId String?
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
WorkProduct WorkProduct[]
}
model WorkProduct {
id String @id @default(uuid())
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
workId String
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
productOnWork WorkProduct[]
}
model WorkProduct {
order Int
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
workId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
productId String
createdBy String?
createdAt DateTime @default(now())
updatedBy String?
updatedAt DateTime @updatedAt
@@id([workId, productId])
} }
model ProductGroup { model ProductGroup {
@ -553,13 +629,15 @@ model ProductGroup {
detail String detail String
remark String remark String
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
Product Product[]
type ProductType[]
} }
model ProductType { model ProductType {
@ -570,13 +648,17 @@ model ProductType {
detail String detail String
remark String remark String
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
productGroupId String
product Product[] product Product[]
} }
@ -586,22 +668,23 @@ model Product {
code String code String
name String name String
detail String detail String
process String process Int
price Int price Float
agentPrice Int agentPrice Float
serviceCharge Int serviceCharge Float
imageUrl String
status Status @default(CREATED) status Status @default(CREATED)
statusOrder Int @default(0)
remark String?
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
productTypeId String? productTypeId String?
productGroup ProductGroup? @relation(fields: [productGroupId], references: [id], onDelete: SetNull) workProduct WorkProduct[]
productGroupId String?
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updatedBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }

View file

@ -1,7 +1,7 @@
import { Controller, Get, Path, Route, Tags } from "tsoa"; import { Controller, Get, Path, Route, Tags } from "tsoa";
import prisma from "../db"; import prisma from "../db";
@Route("api/address") @Route("api/v1/address")
@Tags("Address") @Tags("Address")
export class AddressController extends Controller { export class AddressController extends Controller {
@Get("province") @Get("province")

View file

@ -26,7 +26,7 @@ type BranchContactUpdate = {
telephoneNo?: string; telephoneNo?: string;
}; };
@Route("api/branch/{branchId}/contact") @Route("api/v1/branch/{branchId}/contact")
@Tags("Branch Contact") @Tags("Branch Contact")
@Security("keycloak") @Security("keycloak")
export class BranchContactController extends Controller { export class BranchContactController extends Controller {
@ -62,7 +62,7 @@ export class BranchContactController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Branch contact cannot be found.", "Branch contact cannot be found.",
"data_not_found", "branchContactNotFound",
); );
} }
@ -76,14 +76,10 @@ export class BranchContactController extends Controller {
@Body() body: BranchContactCreate, @Body() body: BranchContactCreate,
) { ) {
if (!(await prisma.branch.findFirst({ where: { id: branchId } }))) { if (!(await prisma.branch.findFirst({ where: { id: branchId } }))) {
throw new HttpError( throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq");
HttpStatus.BAD_REQUEST,
"Branch not found.",
"missing_or_invalid_parameter",
);
} }
const record = await prisma.branchContact.create({ const record = await prisma.branchContact.create({
data: { ...body, branchId, createdBy: req.user.name, updateBy: req.user.name }, data: { ...body, branchId, createdBy: req.user.name, updatedBy: req.user.name },
}); });
this.setStatus(HttpStatus.CREATED); this.setStatus(HttpStatus.CREATED);
@ -106,12 +102,12 @@ export class BranchContactController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Branch contact cannot be found.", "Branch contact cannot be found.",
"data_not_found", "branchContactNotFound",
); );
} }
const record = await prisma.branchContact.update({ const record = await prisma.branchContact.update({
data: { ...body, updateBy: req.user.name }, data: { ...body, updatedBy: req.user.name },
where: { id: contactId, branchId }, where: { id: contactId, branchId },
}); });
@ -125,7 +121,7 @@ export class BranchContactController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Branch contact cannot be found.", "Branch contact cannot be found.",
"data_not_found", "branchContactNotFound",
); );
} }
} }

View file

@ -57,7 +57,7 @@ type BranchUpdate = {
address?: string; address?: string;
zipCode?: string; zipCode?: string;
email?: string; email?: string;
telephoneNo: string; telephoneNo?: string;
contactName?: string; contactName?: string;
contact?: string | string[] | null; contact?: string | string[] | null;
lineId?: string; lineId?: string;
@ -78,7 +78,7 @@ function branchImageLoc(id: string) {
return `branch/branch-img-${id}`; return `branch/branch-img-${id}`;
} }
@Route("api/branch") @Route("api/v1/branch")
@Tags("Branch") @Tags("Branch")
@Security("keycloak") @Security("keycloak")
export class BranchController extends Controller { export class BranchController extends Controller {
@ -109,13 +109,26 @@ export class BranchController extends Controller {
const record = await prisma.branch.findMany({ const record = await prisma.branch.findMany({
select: { select: {
id: true, id: true,
headOfficeId: true,
isHeadOffice: true,
nameEN: true, nameEN: true,
name: true, name: true,
isHeadOffice: true,
}, },
orderBy: [{ isHeadOffice: "desc" }, { createdAt: "asc" }],
}); });
return record.map((a) => const sort = record.reduce<(typeof record)[]>((acc, curr) => {
for (const i of acc) {
if (i[0].id === curr.headOfficeId) {
i.push(curr);
return acc;
}
}
acc.push([curr]);
return acc;
}, []);
return sort.flat().map((a) =>
Object.assign(a, { Object.assign(a, {
count: list.find((b) => b.branchId === a.id)?._count ?? 0, count: list.find((b) => b.branchId === a.id)?._count ?? 0,
}), }),
@ -195,7 +208,7 @@ export class BranchController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
} }
return Object.assign(record, { return Object.assign(record, {
@ -216,58 +229,70 @@ export class BranchController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Province cannot be found.",
"missing_or_invalid_parameter", "relationProvinceNotFound",
); );
if (body.districtId && !district) if (body.districtId && !district)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"District cannot be found.", "District cannot be found.",
"missing_or_invalid_parameter", "relationDistrictNotFound",
); );
if (body.subDistrictId && !subDistrict) if (body.subDistrictId && !subDistrict)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.", "Sub-district cannot be found.",
"missing_or_invalid_parameter", "relationSubDistrictNotFound",
); );
if (body.headOfficeId && !head) if (body.headOfficeId && !head)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Head branch cannot be found.", "Headquaters cannot be found.",
"missing_or_invalid_parameter", "relationHQNotFound",
); );
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body; const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
const year = new Date().getFullYear(); const year = new Date().getFullYear();
const last = await prisma.branch.findFirst({ const record = await prisma.$transaction(
orderBy: { createdAt: "desc" }, async (tx) => {
where: { headOfficeId: headOfficeId ?? null }, const last = await tx.runningNo.upsert({
}); where: {
key: !headOfficeId ? `HQ${year.toString().slice(2)}` : `BR${head?.code.slice(2, 5)}`,
},
create: {
key: !headOfficeId ? `HQ${year.toString().slice(2)}` : `BR${head?.code.slice(2, 5)}`,
value: 1,
},
update: { value: { increment: 1 } },
});
const code = !headOfficeId const code = !headOfficeId
? `HQ${year.toString().slice(2)}${+(last?.code.slice(-1) || 0) + 1}` ? `HQ${year.toString().slice(2)}${last.value}`
: `BR${head?.code.slice(2, 5)}${(+(last?.code.slice(-2) || 0) + 1).toString().padStart(2, "0")}`; : `BR${head?.code.slice(2, 5)}${last.value.toString().padStart(2, "0")}`;
const record = await prisma.branch.create({ return await tx.branch.create({
include: { include: {
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
},
data: {
...rest,
statusOrder: +(rest.status === "INACTIVE"),
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,
updatedBy: req.user.name,
},
});
}, },
data: { { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
...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) { if (headOfficeId) {
await prisma.branch.updateMany({ await prisma.branch.updateMany({
@ -313,8 +338,8 @@ export class BranchController extends Controller {
if (body.headOfficeId === branchId) if (body.headOfficeId === branchId)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Cannot make this as head office and branch at the same time.", "Cannot make this as headquaters and branch at the same time.",
"missing_or_invalid_parameter", "cantMakeHQAndBranchSameTime",
); );
if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) { if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) {
@ -328,38 +353,39 @@ export class BranchController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Province cannot be found.",
"missing_or_invalid_parameter", "relationProvinceNotFound",
); );
if (body.districtId && !district) if (body.districtId && !district)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"District cannot be found.", "District cannot be found.",
"missing_or_invalid_parameter", "relationDistrictNotFound",
); );
if (body.subDistrictId && !subDistrict) if (body.subDistrictId && !subDistrict)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.", "Sub-district cannot be found.",
"missing_or_invalid_parameter", "relationSubDistrictNotFound",
); );
if (body.headOfficeId && !branch) if (body.headOfficeId && !branch)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Head branch cannot be found.", "Headquaters cannot be found.",
"missing_or_invalid_parameter", "relationHQNotFound",
); );
} }
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body; const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) { if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
} }
const record = await prisma.branch.update({ const record = await prisma.branch.update({
include: { province: true, district: true, subDistrict: true }, include: { province: true, district: true, subDistrict: true },
data: { data: {
...rest, ...rest,
statusOrder: +(rest.status === "INACTIVE"),
isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined, isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined,
province: { province: {
connect: provinceId ? { id: provinceId } : undefined, connect: provinceId ? { id: provinceId } : undefined,
@ -377,7 +403,7 @@ export class BranchController extends Controller {
connect: headOfficeId ? { id: headOfficeId } : undefined, connect: headOfficeId ? { id: headOfficeId } : undefined,
disconnect: headOfficeId === null || undefined, disconnect: headOfficeId === null || undefined,
}, },
updateBy: req.user.name, updatedBy: req.user.name,
}, },
where: { id: branchId }, where: { id: branchId },
}); });
@ -421,11 +447,11 @@ export class BranchController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
} }
if (record.status !== Status.CREATED) { if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used"); throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "branchInUsed");
} }
await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), { await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), {

View file

@ -1,4 +1,4 @@
import { Prisma, Status, UserType } from "@prisma/client"; import { Branch, Prisma, Status, User, UserType } from "@prisma/client";
import { import {
Body, Body,
Controller, Controller,
@ -20,7 +20,38 @@ import { RequestWithUser } from "../interfaces/user";
type BranchUserBody = { user: string[] }; type BranchUserBody = { user: string[] };
@Route("api/branch/{branchId}/user") async function userBranchCodeGen(branch: Branch, user: User[]) {
await prisma.$transaction(
async (tx) => {
for (const usr of user) {
if (usr.code !== null) continue;
const typ = usr.userType;
const last = await tx.runningNo.upsert({
where: {
key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`,
},
create: {
key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`,
value: 1,
},
update: { value: { increment: 1 } },
});
await prisma.user.update({
where: { id: usr.id },
data: {
code: `${last.key.slice(7)}${last.value.toString().padStart(4, "0")}`,
},
});
}
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
}
@Route("api/v1/branch/{branchId}/user")
@Tags("Branch User") @Tags("Branch User")
@Security("keycloak") @Security("keycloak")
export class BranchUserController extends Controller { export class BranchUserController extends Controller {
@ -85,7 +116,7 @@ export class BranchUserController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Branch cannot be found.", "Branch cannot be found.",
"missing_or_invalid_parameter", "branchBadReq",
); );
} }
@ -93,7 +124,7 @@ export class BranchUserController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"One or more user cannot be found.", "One or more user cannot be found.",
"missing_or_invalid_parameter", "oneOrMoreUserBadReq",
); );
} }
@ -109,47 +140,12 @@ export class BranchUserController extends Controller {
branchId, branchId,
userId: v.id, userId: v.id,
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
})), })),
}), }),
]); ]);
const group: Record<UserType, string[]> = { await userBranchCodeGen(branch, user);
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() @Delete()
@ -182,7 +178,7 @@ export class BranchUserController extends Controller {
type UserBranchBody = { branch: string[] }; type UserBranchBody = { branch: string[] };
@Route("api/user/{userId}/branch") @Route("api/v1/user/{userId}/branch")
@Tags("Branch User") @Tags("Branch User")
@Security("keycloak") @Security("keycloak")
export class UserBranchController extends Controller { export class UserBranchController extends Controller {
@ -238,7 +234,7 @@ export class UserBranchController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"One or more branch cannot be found.", "One or more branch cannot be found.",
"missing_or_invalid_parameter", "oneOrMoreBranchBadReq",
); );
} }
@ -254,7 +250,7 @@ export class UserBranchController extends Controller {
branchId: v.id, branchId: v.id,
userId, userId,
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
})), })),
}); });

View file

@ -29,14 +29,18 @@ function imageLocation(id: string) {
return `employee/profile-img-${id}`; return `employee/profile-img-${id}`;
} }
type CustomerBranchCreate = { function attachmentLocation(customerId: string, branchId: string) {
return `customer/${customerId}/branch/${branchId}`;
}
export type CustomerBranchCreate = {
customerId: string; customerId: string;
status?: Status; status?: Status;
legalPersonNo: string; legalPersonNo: string;
taxNo: string; taxNo: string | null;
name: string; name: string;
nameEN: string; nameEN: string;
addressEN: string; addressEN: string;
@ -44,26 +48,34 @@ type CustomerBranchCreate = {
zipCode: string; zipCode: string;
email: string; email: string;
telephoneNo: string; telephoneNo: string;
longitude: string;
latitude: string;
registerName: string; registerName: string;
registerDate: Date; registerDate: Date;
authorizedCapital: string; authorizedCapital: string;
employmentOffice: string;
bussinessType: string;
bussinessTypeEN: string;
jobPosition: string;
jobPositionEN: string;
jobDescription: string;
saleEmployee: string;
payDate: Date;
wageRate: number;
subDistrictId?: string | null; subDistrictId?: string | null;
districtId?: string | null; districtId?: string | null;
provinceId?: string | null; provinceId?: string | null;
}; };
type CustomerBranchUpdate = { export type CustomerBranchUpdate = {
customerId?: string; customerId?: string;
status?: "ACTIVE" | "INACTIVE"; status?: "ACTIVE" | "INACTIVE";
legalPersonNo?: string; legalPersonNo?: string;
taxNo?: string; taxNo?: string | null;
name?: string; name?: string;
nameEN?: string; nameEN?: string;
addressEN?: string; addressEN?: string;
@ -71,44 +83,82 @@ type CustomerBranchUpdate = {
zipCode?: string; zipCode?: string;
email?: string; email?: string;
telephoneNo?: string; telephoneNo?: string;
longitude?: string;
latitude?: string;
registerName?: string; registerName?: string;
registerDate?: Date; registerDate?: Date;
authorizedCapital?: string; authorizedCapital?: string;
employmentOffice?: string;
bussinessType?: string;
bussinessTypeEN?: string;
jobPosition?: string;
jobPositionEN?: string;
jobDescription?: string;
saleEmployee?: string;
payDate?: Date;
wageRate?: number;
subDistrictId?: string | null; subDistrictId?: string | null;
districtId?: string | null; districtId?: string | null;
provinceId?: string | null; provinceId?: string | null;
}; };
@Route("api/customer-branch") @Route("api/v1/customer-branch")
@Tags("Customer Branch") @Tags("Customer Branch")
@Security("keycloak") @Security("keycloak")
export class CustomerBranchController extends Controller { export class CustomerBranchController extends Controller {
@Get() @Get()
async list( async list(
@Query() zipCode?: string, @Query() zipCode?: string,
@Query() customerId?: string,
@Query() status?: Status,
@Query() includeCustomer?: boolean,
@Query() query: string = "", @Query() query: string = "",
@Query() page: number = 1, @Query() page: number = 1,
@Query() pageSize: number = 30, @Query() pageSize: number = 30,
) { ) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = { const where = {
OR: [ OR: [
{ nameEN: { contains: query }, zipCode }, { nameEN: { contains: query }, zipCode, ...filterStatus(status) },
{ name: { contains: query }, zipCode }, { name: { contains: query }, zipCode, ...filterStatus(status) },
{ email: { contains: query }, zipCode }, { email: { contains: query }, zipCode, ...filterStatus(status) },
{ code: { contains: query }, zipCode, ...filterStatus(status) },
{ address: { contains: query }, zipCode, ...filterStatus(status) },
{ addressEN: { contains: query }, zipCode, ...filterStatus(status) },
{ province: { name: { contains: query } }, zipCode, ...filterStatus(status) },
{ province: { nameEN: { contains: query } }, zipCode, ...filterStatus(status) },
{ district: { name: { contains: query } }, zipCode, ...filterStatus(status) },
{ district: { nameEN: { contains: query } }, zipCode, ...filterStatus(status) },
{ subDistrict: { name: { contains: query } }, zipCode, ...filterStatus(status) },
{ subDistrict: { nameEN: { contains: query } }, zipCode, ...filterStatus(status) },
{
customer: {
OR: [{ customerName: { contains: query } }, { customerNameEN: { contains: query } }],
},
zipCode,
status,
},
], ],
} satisfies Prisma.BranchWhereInput; AND: { customerId },
} satisfies Prisma.CustomerBranchWhereInput;
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
prisma.customerBranch.findMany({ prisma.customerBranch.findMany({
orderBy: { createdAt: "asc" }, orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: { include: {
customer: includeCustomer,
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
_count: true,
}, },
where, where,
take: pageSize, take: pageSize,
@ -132,7 +182,7 @@ export class CustomerBranchController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
} }
return record; return record;
@ -153,7 +203,6 @@ export class CustomerBranchController extends Controller {
{ firstNameEN: { contains: query }, zipCode }, { firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode }, { lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode }, { lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
], ],
} satisfies Prisma.EmployeeWhereInput; } satisfies Prisma.EmployeeWhereInput;
@ -191,62 +240,67 @@ export class CustomerBranchController extends Controller {
@Post() @Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) { 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([
const [province, district, subDistrict, customer] = await prisma.$transaction([ prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }), ]);
]); if (body.provinceId && !province)
if (body.provinceId && !province) throw new HttpError(
throw new HttpError( HttpStatus.BAD_REQUEST,
HttpStatus.BAD_REQUEST, "Province cannot be found.",
"Province cannot be found.", "relationProvinceNotFound",
"missing_or_invalid_parameter", );
); if (body.districtId && !district)
if (body.districtId && !district) throw new HttpError(
throw new HttpError( HttpStatus.BAD_REQUEST,
HttpStatus.BAD_REQUEST, "District cannot be found.",
"District cannot be found.", "relationDistrictNotFound",
"missing_or_invalid_parameter", );
); if (body.subDistrictId && !subDistrict)
if (body.subDistrictId && !subDistrict) throw new HttpError(
throw new HttpError( HttpStatus.BAD_REQUEST,
HttpStatus.BAD_REQUEST, "Sub-district cannot be found.",
"Sub-district cannot be found.", "relationSubDistrictNotFound",
"missing_or_invalid_parameter", );
); if (!customer)
if (body.customerId && !customer) throw new HttpError(
throw new HttpError( HttpStatus.BAD_REQUEST,
HttpStatus.BAD_REQUEST, "Customer cannot be found.",
"Customer cannot be found.", "relationCustomerNotFound",
"missing_or_invalid_parameter", );
);
}
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
const count = await prisma.customerBranch.count({ const record = await prisma.$transaction(
where: { customerId }, async (tx) => {
}); const count = await tx.customerBranch.count({
where: { customerId },
});
const record = await prisma.customerBranch.create({ return await tx.customerBranch.create({
include: { include: {
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
},
data: {
...rest,
statusOrder: +(rest.status === "INACTIVE"),
branchNo: count + 1,
code: `${customer.code}-${(count + 1).toString().padStart(2, "0")}`,
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,
updatedBy: req.user.name,
},
});
}, },
data: { { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
...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({ await prisma.customer.updateMany({
where: { id: customerId, status: Status.CREATED }, where: { id: customerId, status: Status.CREATED },
@ -275,32 +329,32 @@ export class CustomerBranchController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Province cannot be found.",
"missing_or_invalid_parameter", "relationProvinceNotFound",
); );
if (body.districtId && !district) if (body.districtId && !district)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"District cannot be found.", "District cannot be found.",
"missing_or_invalid_parameter", "relationDistrictNotFound",
); );
if (body.subDistrictId && !subDistrict) if (body.subDistrictId && !subDistrict)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.", "Sub-district cannot be found.",
"missing_or_invalid_parameter", "relationSubDistrictNotFound",
); );
if (body.customerId && !customer) if (body.customerId && !customer)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Customer cannot be found.", "Customer cannot be found.",
"missing_or_invalid_parameter", "relationCustomerNotFound",
); );
} }
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
if (!(await prisma.customerBranch.findUnique({ where: { id: branchId } }))) { if (!(await prisma.customerBranch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
} }
const record = await prisma.customerBranch.update({ const record = await prisma.customerBranch.update({
@ -312,6 +366,7 @@ export class CustomerBranchController extends Controller {
}, },
data: { data: {
...rest, ...rest,
statusOrder: +(rest.status === "INACTIVE"),
customer: { connect: customerId ? { id: customerId } : undefined }, customer: { connect: customerId ? { id: customerId } : undefined },
province: { province: {
connect: provinceId ? { id: provinceId } : undefined, connect: provinceId ? { id: provinceId } : undefined,
@ -326,7 +381,7 @@ export class CustomerBranchController extends Controller {
disconnect: subDistrictId === null || undefined, disconnect: subDistrictId === null || undefined,
}, },
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
}, },
}); });
@ -337,20 +392,143 @@ export class CustomerBranchController extends Controller {
@Delete("{branchId}") @Delete("{branchId}")
async delete(@Path() branchId: string) { async delete(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({ where: { id: branchId } }); const record = await prisma.customerBranch.findFirst({
where: { id: branchId },
});
if (!record) { if (!record) {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Customer branch cannot be found.", "Customer branch cannot be found.",
"data_not_found", "customerBranchNotFound",
); );
} }
if (record.status !== Status.CREATED) { if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Customer branch is in used.", "data_in_used"); throw new HttpError(
HttpStatus.FORBIDDEN,
"Customer branch is in used.",
"customerBranchInUsed",
);
} }
return await prisma.customerBranch.delete({ where: { id: branchId } }); return await prisma.customerBranch.delete({ where: { id: branchId } }).then((v) => {
new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(
MINIO_BUCKET,
`${attachmentLocation(record.customerId, branchId)}/`,
);
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, v, {
forceDelete: true,
});
});
});
return v;
});
}
}
@Route("api/v1/customer-branch/{branchId}/attachment")
@Tags("Customer Branch")
@Security("keycloak")
export class CustomerAttachmentController extends Controller {
@Get()
async listAttachment(@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.",
"customerBranchNotFound",
);
}
const list = await new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(
MINIO_BUCKET,
`${attachmentLocation(record.customerId, branchId)}/`,
);
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() branchId: string, @Body() payload: { file: string[] }) {
const record = await prisma.customerBranch.findFirst({
where: { id: branchId },
});
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Customer branch cannot be found.",
"customerBranchNotFound",
);
}
return await Promise.all(
payload.file.map(async (v) => ({
name: v,
url: await minio.presignedGetObject(
MINIO_BUCKET,
`${attachmentLocation(record.customerId, branchId)}/${v}`,
),
uploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
`${attachmentLocation(record.customerId, branchId)}/${v}`,
12 * 60 * 60,
),
})),
);
}
@Delete()
async deleteAttachment(@Path() branchId: string, @Body() payload: { file: string[] }) {
const record = await prisma.customerBranch.findFirst({
where: { id: branchId },
});
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Customer branch cannot be found.",
"customerBranchNotFound",
);
}
await Promise.all(
payload.file.map(async (v) => {
await minio.removeObject(
MINIO_BUCKET,
`${attachmentLocation(record.customerId, branchId)}/${v}`,
{
forceDelete: true,
},
);
}),
);
} }
} }

View file

@ -15,7 +15,7 @@ import {
} from "tsoa"; } from "tsoa";
import { RequestWithUser } from "../interfaces/user"; import { RequestWithUser } from "../interfaces/user";
import prisma from "../db"; import prisma from "../db";
import minio from "../services/minio"; import minio, { presignedGetObjectIfExist } from "../services/minio";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
@ -27,39 +27,155 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET;
export type CustomerCreate = { export type CustomerCreate = {
status?: Status; status?: Status;
personName: string;
personNameEN?: string;
customerType: CustomerType; customerType: CustomerType;
customerName: string; customerName: string;
customerNameEN: string; customerNameEN: string;
taxNo?: string | null;
customerBranch?: {
status?: Status;
legalPersonNo: string;
taxNo: string | null;
name: string;
nameEN: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
registerName: string;
registerDate: Date;
authorizedCapital: string;
employmentOffice: string;
bussinessType: string;
bussinessTypeEN: string;
jobPosition: string;
jobPositionEN: string;
jobDescription: string;
saleEmployee: string;
payDate: Date;
wageRate: number;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
}[];
}; };
export type CustomerUpdate = { export type CustomerUpdate = {
status?: "ACTIVE" | "INACTIVE"; status?: "ACTIVE" | "INACTIVE";
personName?: string;
personNameEN?: string;
customerType?: CustomerType; customerType?: CustomerType;
customerName?: string; customerName?: string;
customerNameEN?: string; customerNameEN?: string;
taxNo?: string | null;
customerBranch?: {
id?: string;
status?: Status;
legalPersonNo: string;
taxNo: string | null;
name: string;
nameEN: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
registerName: string;
registerDate: Date;
authorizedCapital: string;
employmentOffice: string;
bussinessType: string;
bussinessTypeEN: string;
jobPosition: string;
jobPositionEN: string;
jobDescription: string;
saleEmployee: string;
payDate: Date;
wageRate: number;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
}[];
}; };
function imageLocation(id: string) { function imageLocation(id: string) {
return `customer/img-${id}`; return `customer/${id}/profile-image`;
} }
@Route("api/customer") @Route("api/v1/customer")
@Tags("Customer") @Tags("Customer")
@Security("keycloak") @Security("keycloak")
export class CustomerController extends Controller { export class CustomerController extends Controller {
@Get("type-stats")
async stat() {
const list = await prisma.customer.groupBy({
by: "customerType",
_count: true,
});
return list.reduce<Record<CustomerType, number>>(
(a, c) => {
a[c.customerType] = c._count;
return a;
},
{
CORP: 0,
PERS: 0,
},
);
}
@Get() @Get()
async list( async list(
@Query() customerType?: CustomerType,
@Query() query: string = "", @Query() query: string = "",
@Query() status?: Status,
@Query() page: number = 1, @Query() page: number = 1,
@Query() pageSize: number = 30, @Query() pageSize: number = 30,
@Query() includeBranch: boolean = false,
) { ) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = { const where = {
OR: [{ customerName: { contains: query } }, { customerNameEN: { contains: query } }], OR: [
{ customerName: { contains: query }, customerType, ...filterStatus(status) },
{ customerNameEN: { contains: query }, customerType, ...filterStatus(status) },
],
} satisfies Prisma.CustomerWhereInput; } satisfies Prisma.CustomerWhereInput;
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
prisma.customer.findMany({ prisma.customer.findMany({
orderBy: { createdAt: "asc" }, include: {
_count: true,
branch: includeBranch
? {
include: {
province: true,
district: true,
subDistrict: true,
},
}
: undefined,
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where, where,
take: pageSize, take: pageSize,
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
@ -71,7 +187,11 @@ export class CustomerController extends Controller {
result: await Promise.all( result: await Promise.all(
result.map(async (v) => ({ result.map(async (v) => ({
...v, ...v,
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(v.id), 12 * 60 * 60), imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})), })),
), ),
page, page,
@ -82,11 +202,22 @@ export class CustomerController extends Controller {
@Get("{customerId}") @Get("{customerId}")
async getById(@Path() customerId: string) { async getById(@Path() customerId: string) {
const record = await prisma.customer.findFirst({ where: { id: customerId } }); const record = await prisma.customer.findFirst({
include: {
branch: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where: { id: customerId },
});
if (!record) if (!record)
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound");
return Object.assign(record, { return Object.assign(record, {
imageUrl: await minio.presignedGetObject( imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET, MINIO_BUCKET,
imageLocation(record.id), imageLocation(record.id),
12 * 60 * 60, 12 * 60 * 60,
@ -96,26 +227,101 @@ export class CustomerController extends Controller {
@Post() @Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
const last = await prisma.customer.findFirst({ const { customerBranch, ...payload } = body;
orderBy: { createdAt: "desc" },
where: { customerType: body.customerType },
});
const code = `${body.customerType}${(+(last?.code.slice(-6) || 0) + 1).toString().padStart(6, "0")}`; const provinceId = body.customerBranch?.reduce<string[]>((acc, cur) => {
if (cur.provinceId && !acc.includes(cur.provinceId)) return acc.concat(cur.provinceId);
return acc;
}, []);
const districtId = body.customerBranch?.reduce<string[]>((acc, cur) => {
if (cur.districtId && !acc.includes(cur.districtId)) return acc.concat(cur.districtId);
return acc;
}, []);
const subDistrictId = body.customerBranch?.reduce<string[]>((acc, cur) => {
if (cur.subDistrictId && !acc.includes(cur.subDistrictId))
return acc.concat(cur.subDistrictId);
return acc;
}, []);
const record = await prisma.customer.create({ const [province, district, subDistrict] = await prisma.$transaction([
data: { prisma.province.findMany({ where: { id: { in: provinceId } } }),
...body, prisma.district.findMany({ where: { id: { in: districtId } } }),
code, prisma.subDistrict.findMany({ where: { id: { in: subDistrictId } } }),
createdBy: req.user.name, ]);
updateBy: req.user.name,
if (provinceId && province.length !== provinceId?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some province cannot be found.",
"relationProvinceNotFound",
);
}
if (districtId && district.length !== districtId?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some district cannot be found.",
"relationDistrictNotFound",
);
}
if (subDistrictId && subDistrict.length !== subDistrictId?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some sub district cannot be found.",
"relationSubDistrictNotFound",
);
}
const record = await prisma.$transaction(
async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: `CUSTOMER_${body.customerType}`,
},
create: {
key: `CUSTOMER_${body.customerType}`,
value: 1,
},
update: { value: { increment: 1 } },
});
return await prisma.customer.create({
include: {
branch: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
data: {
...payload,
statusOrder: +(payload.status === "INACTIVE"),
code: `${last.key.slice(9)}${last.value.toString().padStart(6, "0")}`,
branch: {
createMany: {
data:
customerBranch?.map((v, i) => ({
...v,
branchNo: i + 1,
code: `${last.key.slice(9)}${last.value.toString().padStart(6, "0")}-${(i + 1).toString().padStart(2, "0")}`,
createdBy: req.user.name,
updatedBy: req.user.name,
})) || [],
},
},
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
}, },
}); { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
this.setStatus(HttpStatus.CREATED); this.setStatus(HttpStatus.CREATED);
return Object.assign(record, { return Object.assign(record, {
imageUrl: await minio.presignedGetObject( imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET, MINIO_BUCKET,
imageLocation(record.id), imageLocation(record.id),
12 * 60 * 60, 12 * 60 * 60,
@ -134,21 +340,145 @@ export class CustomerController extends Controller {
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Body() body: CustomerUpdate, @Body() body: CustomerUpdate,
) { ) {
if (!(await prisma.customer.findUnique({ where: { id: customerId } }))) { const customer = await prisma.customer.findUnique({ where: { id: customerId } });
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
if (!customer) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound");
} }
const record = await prisma.customer.update({ const provinceId = body.customerBranch?.reduce<string[]>((acc, cur) => {
where: { id: customerId }, if (cur.provinceId && !acc.includes(cur.provinceId)) return acc.concat(cur.provinceId);
data: { return acc;
...body, }, []);
createdBy: req.user.name, const districtId = body.customerBranch?.reduce<string[]>((acc, cur) => {
updateBy: req.user.name, if (cur.districtId && !acc.includes(cur.districtId)) return acc.concat(cur.districtId);
return acc;
}, []);
const subDistrictId = body.customerBranch?.reduce<string[]>((acc, cur) => {
if (cur.subDistrictId && !acc.includes(cur.subDistrictId))
return acc.concat(cur.subDistrictId);
return acc;
}, []);
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findMany({ where: { id: { in: provinceId } } }),
prisma.district.findMany({ where: { id: { in: districtId } } }),
prisma.subDistrict.findMany({ where: { id: { in: subDistrictId } } }),
]);
if (provinceId && province.length !== provinceId?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some province cannot be found.",
"relationProvinceNotFound",
);
}
if (districtId && district.length !== districtId?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some district cannot be found.",
"relationDistrictNotFound",
);
}
if (subDistrictId && subDistrict.length !== subDistrictId?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some sub district cannot be found.",
"relationSubDistrictNotFound",
);
}
const { customerBranch, ...payload } = body;
const relation = await prisma.customerBranch.findMany({
where: {
customerId,
}, },
}); });
if (
customerBranch &&
relation.find((a) => !customerBranch.find((b) => a.id === b.id) && a.status !== "CREATED")
) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more branch cannot be delete and is missing.",
"oneOrMoreBranchMissing",
);
}
const record = await prisma.customer
.update({
include: {
branch: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where: { id: customerId },
data: {
...payload,
statusOrder: +(payload.status === "INACTIVE"),
branch:
(customerBranch && {
deleteMany: {
id: {
notIn: customerBranch.map((v) => v.id).filter((v): v is string => !!v) || [],
},
status: Status.CREATED,
},
upsert: customerBranch.map((v, i) => ({
where: { id: v.id || "" },
create: {
...v,
branchNo: i + 1,
code: `${customer.code}-${(i + 1).toString().padStart(2, "0")}`,
createdBy: req.user.name,
updatedBy: req.user.name,
id: undefined,
},
update: {
...v,
branchNo: i + 1,
code: `${customer.code}-${(i + 1).toString().padStart(2, "0")}`,
updatedBy: req.user.name,
},
})),
}) ||
undefined,
updatedBy: req.user.name,
},
})
.then((v) => {
if (customerBranch) {
relation
.filter((a) => !customerBranch.find((b) => b.id === a.id))
.forEach((deleted) => {
new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `customer/${deleted.id}`);
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, v, {
forceDelete: true,
});
});
});
});
}
return v;
});
return Object.assign(record, { return Object.assign(record, {
imageUrl: await minio.presignedGetObject( imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET, MINIO_BUCKET,
imageLocation(record.id), imageLocation(record.id),
12 * 60 * 60, 12 * 60 * 60,
@ -166,13 +496,30 @@ export class CustomerController extends Controller {
const record = await prisma.customer.findFirst({ where: { id: customerId } }); const record = await prisma.customer.findFirst({ where: { id: customerId } });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound");
} }
if (record.status !== Status.CREATED) { if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Customer is in used.", "data_in_used"); throw new HttpError(HttpStatus.FORBIDDEN, "Customer is in used.", "customerInUsed");
} }
return await prisma.customer.delete({ where: { id: customerId } }); return await prisma.customer.delete({ where: { id: customerId } }).then((v) => {
new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `customer/${customerId}`);
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, v, {
forceDelete: true,
});
});
});
return v;
});
} }
} }

View file

@ -16,35 +16,21 @@ import prisma from "../db";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
type EmployeeCheckupCreate = { type EmployeeCheckupPayload = {
checkupType: string; checkupType?: string | null;
checkupResult: string; checkupResult?: string | null;
provinceId?: string | null; provinceId?: string | null;
hospitalName: string; hospitalName?: string | null;
remark: string; remark?: string | null;
medicalBenefitScheme: string; medicalBenefitScheme?: string | null;
insuranceCompany: string; insuranceCompany?: string | null;
coverageStartDate: Date; coverageStartDate?: Date | null;
coverageExpireDate: Date; coverageExpireDate?: Date | null;
}; };
type EmployeeCheckupEdit = { @Route("api/v1/employee/{employeeId}/checkup")
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") @Tags("Employee Checkup")
@Security("keycloak") @Security("keycloak")
export class EmployeeCheckupController extends Controller { export class EmployeeCheckupController extends Controller {
@ -65,7 +51,7 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.", "Employee checkup cannot be found.",
"data_not_found", "employeeCheckupNotFound",
); );
} }
return record; return record;
@ -75,7 +61,7 @@ export class EmployeeCheckupController extends Controller {
async create( async create(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() employeeId: string, @Path() employeeId: string,
@Body() body: EmployeeCheckupCreate, @Body() body: EmployeeCheckupPayload,
) { ) {
if (body.provinceId || employeeId) { if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([ const [province, employee] = await prisma.$transaction([
@ -86,13 +72,13 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Province cannot be found.",
"missing_or_invalid_parameter", "provinceNotFound",
); );
if (!employee) if (!employee)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Employee cannot be found.", "Employee cannot be found.",
"missing_or_invalid_parameter", "employeeNotFound",
); );
} }
@ -105,7 +91,7 @@ export class EmployeeCheckupController extends Controller {
province: { connect: provinceId ? { id: provinceId } : undefined }, province: { connect: provinceId ? { id: provinceId } : undefined },
employee: { connect: { id: employeeId } }, employee: { connect: { id: employeeId } },
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
}, },
}); });
@ -119,7 +105,7 @@ export class EmployeeCheckupController extends Controller {
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() employeeId: string, @Path() employeeId: string,
@Path() checkupId: string, @Path() checkupId: string,
@Body() body: EmployeeCheckupEdit, @Body() body: EmployeeCheckupPayload,
) { ) {
if (body.provinceId || employeeId) { if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([ const [province, employee] = await prisma.$transaction([
@ -130,13 +116,13 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Province cannot be found.",
"missing_or_invalid_parameter", "provinceNotFound",
); );
if (!employee) if (!employee)
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Employee cannot be found.", "Employee cannot be found.",
"missing_or_invalid_parameter", "employeeNotFound",
); );
} }
@ -146,7 +132,7 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.", "Employee checkup cannot be found.",
"data_not_found", "employeeCheckupNotFound",
); );
} }
@ -157,7 +143,7 @@ export class EmployeeCheckupController extends Controller {
...rest, ...rest,
province: { connect: provinceId ? { id: provinceId } : undefined }, province: { connect: provinceId ? { id: provinceId } : undefined },
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
}, },
}); });
@ -174,7 +160,7 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.", "Employee checkup cannot be found.",
"data_not_found", "employeeCheckupNotFound",
); );
} }

View file

@ -17,7 +17,7 @@ import { RequestWithUser } from "../interfaces/user";
import prisma from "../db"; import prisma from "../db";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import minio from "../services/minio"; import minio, { presignedGetObjectIfExist } from "../services/minio";
if (!process.env.MINIO_BUCKET) { if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket."); throw Error("Require MinIO bucket.");
@ -26,7 +26,7 @@ if (!process.env.MINIO_BUCKET) {
const MINIO_BUCKET = process.env.MINIO_BUCKET; const MINIO_BUCKET = process.env.MINIO_BUCKET;
function imageLocation(id: string) { function imageLocation(id: string) {
return `employee/profile-img-${id}`; return `employee/${id}/profile-image`;
} }
type EmployeeCreate = { type EmployeeCreate = {
@ -34,7 +34,6 @@ type EmployeeCreate = {
status?: Status; status?: Status;
code: string;
nrcNo: string; nrcNo: string;
dateOfBirth: Date; dateOfBirth: Date;
@ -49,22 +48,75 @@ type EmployeeCreate = {
addressEN: string; addressEN: string;
address: string; address: string;
zipCode: string; zipCode: string;
email: string;
telephoneNo: string;
arrivalBarricade: string; passportType: string;
arrivalCardNo: string; passportNumber: string;
passportIssueDate: Date;
passportExpiryDate: Date;
passportIssuingCountry: string;
passportIssuingPlace: string;
previousPassportReference?: string;
visaType?: string | null;
visaNumber?: string | null;
visaIssueDate?: Date | null;
visaExpiryDate?: Date | null;
visaIssuingPlace?: string | null;
visaStayUntilDate?: Date | null;
tm6Number?: string | null;
entryDate?: Date | null;
workerStatus?: string | null;
subDistrictId?: string | null; subDistrictId?: string | null;
districtId?: string | null; districtId?: string | null;
provinceId?: string | null; provinceId?: string | null;
employeeWork?: {
ownerName?: string | null;
positionName?: string | null;
jobType?: string | null;
workplace?: string | null;
workPermitNo?: string | null;
workPermitIssuDate?: Date | null;
workPermitExpireDate?: Date | null;
workEndDate?: Date | null;
remark?: string | null;
}[];
employeeCheckup?: {
checkupType?: string | null;
checkupResult?: string | null;
provinceId?: string | null;
hospitalName?: string | null;
remark?: string | null;
medicalBenefitScheme?: string | null;
insuranceCompany?: string | null;
coverageStartDate?: Date | null;
coverageExpireDate?: Date | null;
}[];
employeeOtherInfo?: {
citizenId?: string | null;
fatherFirstName?: string | null;
fatherLastName?: string | null;
fatherBirthPlace?: string | null;
motherFirstName?: string | null;
motherLastName?: string | null;
motherBirthPlace?: string | null;
fatherFirstNameEN?: string | null;
fatherLastNameEN?: string | null;
motherFirstNameEN?: string | null;
motherLastNameEN?: string | null;
};
}; };
type EmployeeUpdate = { type EmployeeUpdate = {
customerBranchId?: string; customerBranchId?: string;
status?: "ACTIVE" | "INACTIVE"; status?: "ACTIVE" | "INACTIVE";
code?: string;
nrcNo?: string; nrcNo?: string;
dateOfBirth?: Date; dateOfBirth?: Date;
@ -79,41 +131,129 @@ type EmployeeUpdate = {
addressEN?: string; addressEN?: string;
address?: string; address?: string;
zipCode?: string; zipCode?: string;
email?: string;
telephoneNo?: string;
arrivalBarricade?: string; passportType?: string;
arrivalCardNo?: string; passportNumber?: string;
passportIssueDate?: Date;
passportExpiryDate?: Date;
passportIssuingCountry?: string;
passportIssuingPlace?: string;
previousPassportReference?: string;
visaType?: string | null;
visaNumber?: string | null;
visaIssueDate?: Date | null;
visaExpiryDate?: Date | null;
visaIssuingPlace?: string | null;
visaStayUntilDate?: Date | null;
tm6Number?: string | null;
entryDate?: Date | null;
workerStatus?: string | null;
subDistrictId?: string | null; subDistrictId?: string | null;
districtId?: string | null; districtId?: string | null;
provinceId?: string | null; provinceId?: string | null;
employeeWork?: {
id?: string;
ownerName?: string | null;
positionName?: string | null;
jobType?: string | null;
workplace?: string | null;
workPermitNo?: string | null;
workPermitIssuDate?: Date | null;
workPermitExpireDate?: Date | null;
workEndDate?: Date | null;
remark?: string | null;
}[];
employeeCheckup?: {
id?: string;
checkupType?: string | null;
checkupResult?: string | null;
provinceId?: string | null;
hospitalName?: string | null;
remark?: string | null;
medicalBenefitScheme?: string | null;
insuranceCompany?: string | null;
coverageStartDate?: Date | null;
coverageExpireDate?: Date | null;
}[];
employeeOtherInfo?: {
citizenId?: string | null;
fatherFirstName?: string | null;
fatherLastName?: string | null;
fatherBirthPlace?: string | null;
motherFirstName?: string | null;
motherLastName?: string | null;
motherBirthPlace?: string | null;
fatherFirstNameEN?: string | null;
fatherLastNameEN?: string | null;
motherFirstNameEN?: string | null;
motherLastNameEN?: string | null;
};
}; };
@Route("api/employee") @Route("api/v1/employee")
@Tags("Employee") @Tags("Employee")
@Security("keycloak") @Security("keycloak")
export class EmployeeController extends Controller { export class EmployeeController extends Controller {
@Get("stats")
async getEmployeeStats(@Query() customerBranchId?: string) {
return await prisma.employee.count({
where: { customerBranchId },
});
}
@Get("stats/gender")
async getEmployeeStatsGender(@Query() customerBranchId?: string) {
return await prisma.employee
.groupBy({
_count: true,
by: ["gender"],
where: { customerBranchId },
})
.then((res) =>
res.reduce<Record<string, number>>((a, c) => {
a[c.gender] = c._count;
return a;
}, {}),
);
}
@Get() @Get()
async list( async list(
@Query() zipCode?: string, @Query() zipCode?: string,
@Query() gender?: string,
@Query() status?: Status,
@Query() query: string = "", @Query() query: string = "",
@Query() page: number = 1, @Query() page: number = 1,
@Query() pageSize: number = 30, @Query() pageSize: number = 30,
) { ) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = { const where = {
OR: [ OR: [
{ firstName: { contains: query }, zipCode }, { firstName: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ firstNameEN: { contains: query }, zipCode }, { firstNameEN: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ lastName: { contains: query }, zipCode }, { lastName: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ lastNameEN: { contains: query }, zipCode }, { lastNameEN: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ email: { contains: query }, zipCode },
], ],
} satisfies Prisma.EmployeeWhereInput; } satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
prisma.employee.findMany({ prisma.employee.findMany({
orderBy: { createdAt: "asc" }, orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: { include: {
province: true, province: true,
district: true, district: true,
@ -130,7 +270,7 @@ export class EmployeeController extends Controller {
result: await Promise.all( result: await Promise.all(
result.map(async (v) => ({ result.map(async (v) => ({
...v, ...v,
profileImageUrl: await minio.presignedGetObject( profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET, MINIO_BUCKET,
imageLocation(v.id), imageLocation(v.id),
12 * 60 * 60, 12 * 60 * 60,
@ -155,7 +295,7 @@ export class EmployeeController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "employeeNotFound");
} }
return record; return record;
@ -163,67 +303,148 @@ export class EmployeeController extends Controller {
@Post() @Post()
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) { 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([
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([ prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.customerBranch.findFirst({
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }), where: { id: body.customerBranchId },
include: { customer: true },
}),
]);
if (body.provinceId !== province?.id)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"relationProvinceNotFound",
);
if (body.districtId !== district?.id)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"relationDistrictNotFound",
);
if (body.subDistrictId !== subDistrict?.id)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"relationSubDistrictNotFound",
);
if (!customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer Branch cannot be found.",
"relationCustomerBranchNotFound",
);
const {
provinceId,
districtId,
subDistrictId,
customerBranchId,
employeeWork,
employeeCheckup,
employeeOtherInfo,
...rest
} = body;
const listProvinceId = employeeCheckup?.reduce<string[]>((acc, cur) => {
if (cur.provinceId && !acc.includes(cur.provinceId)) return acc.concat(cur.provinceId);
if (!cur.provinceId) cur.provinceId = null;
return acc;
}, []);
if (listProvinceId) {
const [listProvince] = await prisma.$transaction([
prisma.province.findMany({ where: { id: { in: listProvinceId } } }),
]); ]);
if (body.provinceId && !province) if (listProvince.length !== listProvinceId.length) {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Some province cannot be found.",
"missing_or_invalid_parameter", "someProvinceNotFound",
);
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.$transaction(
async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
},
create: {
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
value: 1,
},
update: { value: { increment: 1 } },
});
const record = await prisma.employee.create({ return await prisma.employee.create({
include: { include: {
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
employeeOtherInfo: true,
employeeCheckup: {
include: {
province: true,
},
},
employeeWork: true,
},
data: {
...rest,
statusOrder: +(rest.status === "INACTIVE"),
code: `${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}${last.value.toString().padStart(4, "0")}`,
employeeWork: {
createMany: {
data: employeeWork || [],
},
},
employeeCheckup: {
createMany: {
data:
employeeCheckup?.map((v) => ({
...v,
provinceId: !!v.provinceId ? null : v.provinceId,
})) || [],
},
},
employeeOtherInfo: {
create: employeeOtherInfo,
},
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,
updatedBy: req.user.name,
},
});
}, },
data: { { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
...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({ await prisma.customerBranch.updateMany({
where: { id: customerBranchId, status: Status.CREATED }, where: { id: customerBranchId, status: Status.CREATED },
data: { status: Status.ACTIVE }, data: { status: Status.ACTIVE },
}); });
await prisma.customer.updateMany({
where: {
branch: {
some: { id: customerBranchId },
},
status: Status.CREATED,
},
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED); this.setStatus(HttpStatus.CREATED);
return Object.assign(record, { return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject( profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET, MINIO_BUCKET,
imageLocation(record.id), imageLocation(record.id),
12 * 60 * 60, 12 * 60 * 60,
@ -242,71 +463,223 @@ export class EmployeeController extends Controller {
@Body() body: EmployeeUpdate, @Body() body: EmployeeUpdate,
@Path() employeeId: string, @Path() employeeId: string,
) { ) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) { const [province, district, subDistrict, customerBranch, employee] = await prisma.$transaction([
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([ prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.customerBranch.findFirst({
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }), where: { id: body.customerBranchId || undefined },
]); include: { customer: true },
if (body.provinceId && !province) }),
throw new HttpError( prisma.employee.findFirst({ where: { id: employeeId } }),
HttpStatus.BAD_REQUEST, ]);
"Province cannot be found.", if (body.provinceId && !province)
"missing_or_invalid_parameter", throw new HttpError(
); HttpStatus.BAD_REQUEST,
if (body.districtId && !district) "Province cannot be found.",
throw new HttpError( "relationProvinceNotFound",
HttpStatus.BAD_REQUEST, );
"District cannot be found.", if (body.districtId && !district)
"missing_or_invalid_parameter", throw new HttpError(
); HttpStatus.BAD_REQUEST,
if (body.subDistrictId && !subDistrict) "District cannot be found.",
throw new HttpError( "relationDistrictNotFound",
HttpStatus.BAD_REQUEST, );
"Sub-district cannot be found.", if (body.subDistrictId && !subDistrict)
"missing_or_invalid_parameter", throw new HttpError(
); HttpStatus.BAD_REQUEST,
if (body.customerBranchId && !customerBranch) "Sub-district cannot be found.",
throw new HttpError( "relationSubDistrictNotFound",
HttpStatus.BAD_REQUEST, );
"Customer cannot be found.", if (body.customerBranchId && !customerBranch)
"missing_or_invalid_parameter", throw new HttpError(
); HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"relationCustomerNotFound",
);
if (!employee) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "employeeNotFound");
} }
const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body; const {
provinceId,
districtId,
subDistrictId,
customerBranchId,
employeeWork,
employeeCheckup,
employeeOtherInfo,
...rest
} = body;
const record = await prisma.employee.update({ const listProvinceId = employeeCheckup?.reduce<string[]>((acc, cur) => {
where: { id: employeeId }, if (cur.provinceId && !acc.includes(cur.provinceId)) return acc.concat(cur.provinceId);
include: { if (!cur.provinceId) cur.provinceId = null;
province: true, return acc;
district: true, }, []);
subDistrict: true,
}, if (listProvinceId) {
data: { const [listProvince] = await prisma.$transaction([
...rest, prisma.province.findMany({ where: { id: { in: listProvinceId } } }),
customerBranch: { connect: customerBranchId ? { id: customerBranchId } : undefined }, ]);
province: { if (listProvince.length !== listProvinceId.length) {
connect: provinceId ? { id: provinceId } : undefined, throw new HttpError(
disconnect: provinceId === null || undefined, HttpStatus.BAD_REQUEST,
"Some province cannot be found.",
"someProvinceNotFound",
);
}
}
const record = await prisma.$transaction(async (tx) => {
let code: string | undefined;
if (customerBranch && customerBranch.id !== employee.customerBranchId) {
const last = await tx.runningNo.upsert({
where: {
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
},
create: {
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
value: 1,
},
update: { value: { increment: 1 } },
});
code = `${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}${last.value.toString().padStart(4, "0")}`;
}
return await prisma.employee.update({
where: { id: employeeId },
include: {
province: true,
district: true,
subDistrict: true,
employeeOtherInfo: true,
employeeCheckup: {
include: {
province: true,
},
},
employeeWork: true,
}, },
district: { data: {
connect: districtId ? { id: districtId } : undefined, ...rest,
disconnect: districtId === null || undefined, statusOrder: +(rest.status === "INACTIVE"),
code,
customerBranch: { connect: customerBranchId ? { id: customerBranchId } : undefined },
employeeWork: employeeWork
? {
deleteMany: {
id: {
notIn: employeeWork.map((v) => v.id).filter((v): v is string => !!v) || [],
},
},
upsert: employeeWork.map((v) => ({
where: { id: v.id || "" },
create: {
...v,
createdBy: req.user.name,
updatedBy: req.user.name,
id: undefined,
},
update: {
...v,
updatedBy: req.user.name,
},
})),
}
: undefined,
employeeCheckup: employeeCheckup
? {
deleteMany: {
id: {
notIn: employeeCheckup.map((v) => v.id).filter((v): v is string => !!v) || [],
},
},
upsert: employeeCheckup.map((v) => ({
where: { id: v.id || "" },
create: {
...v,
provinceId: !v.provinceId ? undefined : v.provinceId,
createdBy: req.user.name,
updatedBy: req.user.name,
id: undefined,
},
update: {
...v,
updatedBy: req.user.name,
},
})),
}
: undefined,
employeeOtherInfo: employeeOtherInfo
? {
deleteMany: {},
create: employeeOtherInfo,
}
: 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,
updatedBy: req.user.name,
}, },
subDistrict: { });
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
createdBy: req.user.name,
updateBy: req.user.name,
},
}); });
this.setStatus(HttpStatus.CREATED); const historyEntries: { field: string; valueBefore: string; valueAfter: string }[] = [];
return record; for (const k of Object.keys(body)) {
const field = k as keyof typeof body;
if (field === "employeeCheckup") continue;
if (field === "employeeOtherInfo") continue;
if (field === "employeeWork") continue;
let valueBefore = employee[field];
let valueAfter = body[field];
if (valueBefore === undefined && valueAfter === undefined) continue;
if (valueBefore instanceof Date) valueBefore = valueBefore.toISOString();
if (valueBefore === null || valueBefore === undefined) valueBefore = "";
if (valueAfter instanceof Date) valueAfter = valueAfter.toISOString();
if (valueAfter === null || valueAfter === undefined) valueAfter = "";
if (valueBefore !== valueAfter) historyEntries.push({ field, valueBefore, valueAfter });
}
await prisma.employeeHistory.createMany({
data: historyEntries.map((v) => ({
...v,
updatedByUserId: req.user.sub,
updatedBy: req.user.preferred_username,
masterId: employee.id,
})),
});
return Object.assign(record, {
profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
} }
@Delete("{employeeId}") @Delete("{employeeId}")
@ -314,17 +687,20 @@ export class EmployeeController extends Controller {
const record = await prisma.employee.findFirst({ where: { id: employeeId } }); const record = await prisma.employee.findFirst({ where: { id: employeeId } });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "employeeNotFound");
} }
if (record.status !== Status.CREATED) { if (record.status !== Status.CREATED) {
throw new HttpError( throw new HttpError(HttpStatus.FORBIDDEN, "Employee is in used.", "employeeInUsed");
HttpStatus.FORBIDDEN,
"Emplyee is in used.",
"missing_or_invalid_parameter",
);
} }
return await prisma.employee.delete({ where: { id: employeeId } }); return await prisma.employee.delete({ where: { id: employeeId } });
} }
@Get("{employeeId}/edit-history")
async editHistory(@Path() employeeId: string) {
return await prisma.employeeHistory.findMany({
where: { masterId: employeeId },
});
}
} }

View file

@ -1,4 +1,3 @@
import { Prisma, Status } from "@prisma/client";
import { import {
Body, Body,
Controller, Controller,
@ -7,7 +6,6 @@ import {
Put, Put,
Path, Path,
Post, Post,
Query,
Request, Request,
Route, Route,
Security, Security,
@ -19,54 +17,44 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user"; import { RequestWithUser } from "../interfaces/user";
type EmployeeOtherInfoCreate = { type EmployeeOtherInfoPayload = {
citizenId: string; citizenId?: string | null;
fatherFullName: string; fatherFirstName?: string | null;
motherFullName: string; fatherLastName?: string | null;
birthPlace: string; fatherBirthPlace?: string | null;
motherFirstName?: string | null;
motherLastName?: string | null;
motherBirthPlace?: string | null;
fatherFirstNameEN?: string | null;
fatherLastNameEN?: string | null;
motherFirstNameEN?: string | null;
motherLastNameEN?: string | null;
}; };
type EmployeeOtherInfoUpdate = { @Route("api/v1/employee/{employeeId}/other-info")
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
};
@Route("api/employee/{employeeId}/other-info")
@Tags("Employee Other Info") @Tags("Employee Other Info")
@Security("keycloak") @Security("keycloak")
export class EmployeeOtherInfo extends Controller { export class EmployeeOtherInfo extends Controller {
@Get() @Get()
async list(@Path() employeeId: string) { async list(@Path() employeeId: string) {
return prisma.employeeOtherInfo.findMany({ return prisma.employeeOtherInfo.findFirst({
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "asc" },
where: { employeeId }, 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() @Post()
async create( async create(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() employeeId: string, @Path() employeeId: string,
@Body() body: EmployeeOtherInfoCreate, @Body() body: EmployeeOtherInfoPayload,
) { ) {
if (!(await prisma.employee.findUnique({ where: { id: employeeId } }))) if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Employee cannot be found.", "Employee cannot be found.",
"missing_or_invalid_parameter", "employeeBadReq",
); );
const record = await prisma.employeeOtherInfo.create({ const record = await prisma.employeeOtherInfo.create({
@ -74,7 +62,7 @@ export class EmployeeOtherInfo extends Controller {
...body, ...body,
employee: { connect: { id: employeeId } }, employee: { connect: { id: employeeId } },
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
}, },
}); });
@ -88,19 +76,19 @@ export class EmployeeOtherInfo extends Controller {
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() employeeId: string, @Path() employeeId: string,
@Path() otherInfoId: string, @Path() otherInfoId: string,
@Body() body: EmployeeOtherInfoUpdate, @Body() body: EmployeeOtherInfoPayload,
) { ) {
if (!(await prisma.employeeOtherInfo.findUnique({ where: { id: otherInfoId, employeeId } }))) { if (!(await prisma.employeeOtherInfo.findUnique({ where: { id: otherInfoId, employeeId } }))) {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Employee other info cannot be found.", "Employee other info cannot be found.",
"data_not_found", "employeeOtherNotFound",
); );
} }
const record = await prisma.employeeOtherInfo.update({ const record = await prisma.employeeOtherInfo.update({
where: { id: otherInfoId, employeeId }, where: { id: otherInfoId, employeeId },
data: { ...body, createdBy: req.user.name, updateBy: req.user.name }, data: { ...body, createdBy: req.user.name, updatedBy: req.user.name },
}); });
this.setStatus(HttpStatus.CREATED); this.setStatus(HttpStatus.CREATED);
@ -118,7 +106,7 @@ export class EmployeeOtherInfo extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Employee other info cannot be found.", "Employee other info cannot be found.",
"data_not_found", "employeeOtherNotFound",
); );
} }

View file

@ -0,0 +1,112 @@
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 EmployeeWorkPayload = {
ownerName?: string | null;
positionName?: string | null;
jobType?: string | null;
workplace?: string | null;
workPermitNo?: string | null;
workPermitIssuDate?: Date | null;
workPermitExpireDate?: Date | null;
workEndDate?: Date | null;
remark?: string | null;
};
@Route("api/v1/employee/{employeeId}/work")
@Tags("Employee Work")
@Security("keycloak")
export class EmployeeWorkController extends Controller {
@Get()
async list(@Path() employeeId: string) {
return prisma.employeeWork.findMany({
orderBy: { createdAt: "asc" },
where: { employeeId },
});
}
@Get("{workId}")
async getById(@Path() employeeId: string, @Path() workId: string) {
const record = await prisma.employeeWork.findFirst({
where: { id: workId, employeeId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee work cannot be found.", "employeeWorkNotFound");
}
return record;
}
@Post()
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeWorkPayload,
) {
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"employeeBadReq",
);
const record = await prisma.employeeWork.create({
data: {
...body,
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{workId}")
async editById(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() workId: string,
@Body() body: EmployeeWorkPayload,
) {
if (!(await prisma.employeeWork.findUnique({ where: { id: workId, employeeId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee work cannot be found.", "employeeWorkNotFound");
}
const record = await prisma.employeeWork.update({
where: { id: workId, employeeId },
data: { ...body, createdBy: req.user.name, updatedBy: req.user.name },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{workId}")
async deleteById(@Path() employeeId: string, @Path() workId: string) {
const record = await prisma.employeeWork.findFirst({ where: { id: workId, employeeId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee work cannot be found.", "employeeWorkNotFound");
}
return await prisma.employeeWork.delete({ where: { id: workId, employeeId } });
}
}

View file

@ -8,7 +8,7 @@ import {
removeUserRoles, removeUserRoles,
} from "../services/keycloak"; } from "../services/keycloak";
@Route("api/keycloak") @Route("api/v1/keycloak")
@Tags("Single-Sign On") @Tags("Single-Sign On")
@Security("keycloak") @Security("keycloak")
export class KeycloakController extends Controller { export class KeycloakController extends Controller {

View file

@ -19,7 +19,7 @@ type MenuEdit = {
url: string; url: string;
}; };
@Route("v1/permission/menu") @Route("api/v1/permission/menu")
@Tags("Permission") @Tags("Permission")
@Security("keycloak") @Security("keycloak")
export class MenuController extends Controller { export class MenuController extends Controller {
@ -41,7 +41,7 @@ export class MenuController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Parent menu not found.", "Parent menu not found.",
"missing_or_invalid_parameter", "parentMenuBadReq",
); );
} }
} }
@ -66,7 +66,7 @@ export class MenuController extends Controller {
}) })
.catch((e) => { .catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "menuNotFound");
} }
throw new Error(e); throw new Error(e);
}); });
@ -78,7 +78,83 @@ export class MenuController extends Controller {
async deleteMenu(@Path("menuId") id: string) { async deleteMenu(@Path("menuId") id: string) {
const record = await prisma.menu.deleteMany({ where: { id } }); const record = await prisma.menu.deleteMany({ where: { id } });
if (record.count <= 0) { if (record.count <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "menuNotFound");
}
}
}
type RoleMenuPermissionCreate = {
userRole: string;
permission: string;
};
type RoleMenuPermissionEdit = {
userRole?: string;
permission?: string;
};
@Route("api/v1/permission/menu/{menuId}/role")
@Tags("Permission")
@Security("keycloak")
export class RoleMenuController extends Controller {
@Get()
async listRoleMenu(@Path() menuId: string) {
const record = await prisma.roleMenuPermission.findMany({
where: { menuId },
orderBy: [{ userRole: "asc" }, { createdAt: "asc" }],
});
return record;
}
@Post()
async createRoleMenu(@Path() menuId: string, @Body() body: RoleMenuPermissionCreate) {
const menu = await prisma.menu.findFirst({ where: { id: menuId } });
if (!menu) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"menuBadReq",
);
}
const record = await prisma.roleMenuPermission.create({
data: Object.assign(body, { menuId }),
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{roleMenuId}")
async editRoleMenu(
@Path("roleMenuId") id: string,
@Path() menuId: string,
@Body() body: RoleMenuPermissionEdit,
) {
const record = await prisma.roleMenuPermission
.update({
where: { id, menuId },
data: body,
})
.catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(HttpStatus.NOT_FOUND, "Role menu cannot be found.", "roleMenuNotFound");
}
throw new Error(e);
});
return record;
}
@Delete("{roleMenuId}")
async deleteRoleMenu(@Path("roleMenuId") id: string, @Path() menuId: string) {
const record = await prisma.roleMenuPermission.deleteMany({
where: { id, menuId },
});
if (record.count <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "Role menu cannot be found.", "roleMenuNotFound");
} }
} }
} }
@ -95,7 +171,7 @@ type MenuComponentEdit = {
menuId?: string; menuId?: string;
}; };
@Route("v1/permission/menu-component") @Route("api/v1/permission/menu-component")
@Tags("Permission") @Tags("Permission")
@Security("keycloak") @Security("keycloak")
export class MenuComponentController extends Controller { export class MenuComponentController extends Controller {
@ -117,7 +193,7 @@ export class MenuComponentController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Menu not found.", "Menu not found.",
"missing_or_invalid_parameter", "menuBadReq",
); );
} }
@ -139,7 +215,7 @@ export class MenuComponentController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Menu not found.", "Menu not found.",
"missing_or_invalid_parameter", "menuBadReq",
); );
} }
} }
@ -155,7 +231,7 @@ export class MenuComponentController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Menu component cannot be found.", "Menu component cannot be found.",
"data_not_found", "menuComponentNotFound",
); );
} }
throw new Error(e); throw new Error(e);
@ -171,7 +247,97 @@ export class MenuComponentController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Menu component cannot be found.", "Menu component cannot be found.",
"data_not_found", "menuComponentNotFound",
);
}
}
}
type RoleMenuComponentPermissionCreate = {
userRole: string;
permission: string;
};
type RoleMenuComponentPermissionEdit = {
userRole?: string;
permission?: string;
};
@Route("api/v1/permission/menu-component/{menuComponentId}/role")
@Tags("Permission")
@Security("keycloak")
export class RoleMenuComponentController extends Controller {
@Get()
async listRoleMenuComponent(@Path() menuComponentId: string) {
const record = await prisma.roleMenuComponentPermission.findMany({
where: { menuComponentId },
orderBy: [{ userRole: "asc" }, { createdAt: "asc" }],
});
return record;
}
@Post()
async createRoleMenuComponent(
@Path() menuComponentId: string,
@Body() body: RoleMenuComponentPermissionCreate,
) {
const menu = await prisma.menuComponent.findFirst({ where: { id: menuComponentId } });
if (!menu) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"menuBadReq",
);
}
const record = await prisma.roleMenuComponentPermission.create({
data: Object.assign(body, { menuComponentId }),
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{roleMenuComponentId}")
async editRoleMenuComponent(
@Path("roleMenuComponentId") id: string,
@Path() menuComponentId: string,
@Body() body: RoleMenuComponentPermissionEdit,
) {
const record = await prisma.roleMenuComponentPermission
.update({
where: { id, menuComponentId },
data: body,
})
.catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Role menu component cannot be found.",
"roleMenuNotFound",
);
}
throw new Error(e);
});
return record;
}
@Delete("{roleMenuComponentId}")
async deleteRoleMenuComponent(
@Path("roleMenuComponentId") id: string,
@Path() menuComponentId: string,
) {
const record = await prisma.roleMenuComponentPermission.deleteMany({
where: { id, menuComponentId },
});
if (record.count <= 0) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Role menu component cannot be found.",
"roleMenuNotFound",
); );
} }
} }

View file

@ -0,0 +1,100 @@
import { Controller, Get, Query, Route, Security } from "tsoa";
import prisma from "../db";
import { Prisma, Product, Service } from "@prisma/client";
@Route("/api/v1/product-service")
export class ProductServiceController extends Controller {
@Get()
@Security("keycloak")
async getProductService(
@Query() status?: "ACTIVE" | "INACTIVE",
@Query() query = "",
@Query() productTypeId?: string,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const union = Prisma.sql`
SELECT
"id",
"code",
"name",
"detail",
"price",
"agentPrice",
"serviceCharge",
"process",
"remark",
"status",
"statusOrder",
"productTypeId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
'product' as "type"
FROM "Product"
UNION ALL
SELECT
"id",
"code",
"name",
"detail",
null as "price",
null as "agentPrice",
null as "serviceCharge",
null as "process",
null as "remark",
"status",
"statusOrder",
null as "productTypeId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
'service' as "type"
FROM "Service"
`;
const or: Prisma.Sql[] = [];
const and: Prisma.Sql[] = [];
if (query) or.push(Prisma.sql`"name" LIKE ${`%${query}%`}`);
if (status) and.push(Prisma.sql`"status" = ${status}::"Status"`);
if (productTypeId) {
and.push(Prisma.sql`("productTypeId" = ${productTypeId} OR ("type" = 'service'))`);
}
const where = Prisma.sql`
${or.length > 0 || and.length > 0 ? Prisma.sql`WHERE ` : Prisma.empty}
${or.length > 0 ? Prisma.join(or, " OR ", "(", ")") : Prisma.empty}
${or.length > 0 && and.length > 0 ? Prisma.sql` AND ` : Prisma.empty}
${and.length > 0 ? Prisma.join(and, " AND ", "(", ")") : Prisma.empty}
`;
const [result, [{ total }]] = await prisma.$transaction([
prisma.$queryRaw<((Product & { type: "product" }) | (Service & { type: "service" }))[]>`
SELECT * FROM (${union}) AS "ProductService"
${where}
ORDER BY "ProductService"."statusOrder" ASC, "ProductService"."createdAt" ASC
LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}
`,
prisma.$queryRaw<[{ total: number }]>`
SELECT COUNT(*) AS "total" FROM (${union}) as "ProductService"
${where}
`,
]);
const work = await prisma.work.findMany({
where: { serviceId: { in: result.flatMap((v) => (v.type === "service" ? v.id : [])) } },
});
return {
result: result.map((v) =>
v.type === "service" ? { ...v, work: work.filter((w) => w.serviceId === v.id) || [] } : v,
),
page,
pageSize,
total: +String(total),
};
}
}

View file

@ -0,0 +1,199 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { Prisma, Status } from "@prisma/client";
import prisma from "../../db";
import { RequestWithUser } from "../../interfaces/user";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
type ProductGroupCreate = {
name: string;
detail: string;
remark: string;
status?: Status;
};
type ProductGroupUpdate = {
name?: string;
detail?: string;
remark?: string;
status?: "ACTIVE" | "INACTIVE";
};
@Route("api/v1/product-group")
@Tags("Product Group")
@Security("keycloak")
export class ProductGroup extends Controller {
@Get("stats")
async getProductGroupStats() {
return await prisma.productGroup.count();
}
@Get()
async getProductGroup(
@Query() query: string = "",
@Query() status?: Status,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = {
OR: [
{ name: { contains: query }, ...filterStatus(status) },
{ detail: { contains: query }, ...filterStatus(status) },
],
} satisfies Prisma.ProductGroupWhereInput;
const [result, total] = await prisma.$transaction([
prisma.productGroup.findMany({
include: {
_count: {
select: {
type: true,
},
},
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.productGroup.count({ where }),
]);
const statsProduct = await prisma.productType.findMany({
include: {
_count: { select: { product: true } },
},
where: {
productGroupId: { in: result.map((v) => v.id) },
},
});
return {
result: result.map((v) => ({
...v,
_count: {
...v._count,
product: statsProduct.reduce(
(a, c) => (c.productGroupId === v.id ? a + c._count.product : a),
0,
),
},
})),
page,
pageSize,
total,
};
}
@Get("{groupId}")
async getProductGroupById(@Path() groupId: string) {
const record = await prisma.productGroup.findFirst({
where: { id: groupId },
});
if (!record)
throw new HttpError(
HttpStatus.NOT_FOUND,
"Product group cannot be found.",
"productGroupNotFound",
);
return record;
}
@Post()
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
const record = await prisma.$transaction(
async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: `PRODGRP`,
},
create: {
key: `PRODGRP`,
value: 1,
},
update: { value: { increment: 1 } },
});
return await tx.productGroup.create({
data: {
...body,
statusOrder: +(body.status === "INACTIVE"),
code: `G${last.value.toString().padStart(2, "0")}`,
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{groupId}")
async editProductGroup(
@Request() req: RequestWithUser,
@Body() body: ProductGroupUpdate,
@Path() groupId: string,
) {
if (!(await prisma.productGroup.findUnique({ where: { id: groupId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Product group cannot be found.",
"productGroupNotFound",
);
}
const record = await prisma.productGroup.update({
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedBy: req.user.name },
where: { id: groupId },
});
return record;
}
@Delete("{groupId}")
async deleteProductGroup(@Path() groupId: string) {
const record = await prisma.productGroup.findFirst({ where: { id: groupId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Product group cannot be found.",
"productGroupNotFound",
);
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Product group is in used.", "productGroupInUsed");
}
return await prisma.productGroup.delete({ where: { id: groupId } });
}
}

View file

@ -0,0 +1,273 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Request,
Route,
Security,
Tags,
Query,
} from "tsoa";
import { Prisma, Status } from "@prisma/client";
import prisma from "../../db";
import minio, { presignedGetObjectIfExist } from "../../services/minio";
import { RequestWithUser } from "../../interfaces/user";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type ProductCreate = {
status?: Status;
code: "AC" | "DO" | "ac" | "do";
name: string;
detail: string;
process: number;
price: number;
agentPrice: number;
serviceCharge: number;
productTypeId: string;
remark?: string;
};
type ProductUpdate = {
status?: "ACTIVE" | "INACTIVE";
name?: string;
detail?: string;
process?: number;
price?: number;
agentPrice?: number;
serviceCharge?: number;
remark?: string;
productTypeId?: string;
};
function imageLocation(id: string) {
return `product/${id}/image`;
}
@Route("api/v1/product")
@Tags("Product")
export class ProductController extends Controller {
@Get("stats")
async getProductStats(@Query() productTypeId?: string) {
return await prisma.product.count({ where: { productTypeId } });
}
@Get()
@Security("keycloak")
async getProduct(
@Query() status?: Status,
@Query() productTypeId?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = {
OR: [
{ name: { contains: query }, productTypeId, ...filterStatus(status) },
{ detail: { contains: query }, productTypeId, ...filterStatus(status) },
],
} satisfies Prisma.ProductWhereInput;
const [result, total] = await prisma.$transaction([
prisma.product.findMany({
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.product.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{productId}")
@Security("keycloak")
async getProductById(@Path() productId: string) {
const record = await prisma.product.findFirst({
where: { id: productId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
}
return Object.assign(record, {
imageUrl: await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(record.id), 60 * 60),
});
}
@Get("{productId}/image")
async getProductImageById(@Request() req: RequestWithUser, @Path() productId: string) {
const url = await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(productId), 60 * 60);
if (!url) {
throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound");
}
return req.res?.redirect(url);
}
@Post()
@Security("keycloak")
async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) {
const productType = await prisma.productType.findFirst({
where: { id: body.productTypeId },
});
if (!productType) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Product Type cannot be found.",
"relationProductTypeNotFound",
);
}
const record = await prisma.$transaction(
async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: `PRODUCT_${body.code.toLocaleUpperCase()}`,
},
create: {
key: `PRODUCT_${body.code.toLocaleUpperCase()}`,
value: 1,
},
update: { value: { increment: 1 } },
});
return await prisma.product.create({
data: {
...body,
statusOrder: +(body.status === "INACTIVE"),
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
},
);
if (productType.status === "CREATED") {
await prisma.productType.update({
where: { id: body.productTypeId },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{productId}")
@Security("keycloak")
async editProduct(
@Request() req: RequestWithUser,
@Body() body: ProductUpdate,
@Path() productId: string,
) {
if (!(await prisma.product.findUnique({ where: { id: productId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
}
const productType = await prisma.productType.findFirst({
where: { id: body.productTypeId },
});
if (!productType) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Product Type cannot be found.",
"relationProductTypeNotFound",
);
}
const record = await prisma.product.update({
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedBy: req.user.name },
where: { id: productId },
});
if (productType.status === "CREATED") {
await prisma.productType.updateMany({
where: { id: body.productTypeId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
}
return Object.assign(record, {
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{productId}")
@Security("keycloak")
async deleteProduct(@Path() productId: string) {
const record = await prisma.product.findFirst({ where: { id: productId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Product is in used.", "productInUsed");
}
return await prisma.product.delete({ where: { id: productId } });
}
}

View file

@ -0,0 +1,213 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { Prisma, Status } from "@prisma/client";
import prisma from "../../db";
import { RequestWithUser } from "../../interfaces/user";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
type ProductTypeCreate = {
productGroupId: string;
name: string;
detail: string;
remark: string;
status?: Status;
};
type ProductTypeUpdate = {
productGroupId?: string;
name?: string;
detail?: string;
remark?: string;
status?: "ACTIVE" | "INACTIVE";
};
@Route("api/v1/product-type")
@Tags("Product Type")
@Security("keycloak")
export class ProductType extends Controller {
@Get("stats")
async getProductTypeStats() {
return await prisma.productType.count();
}
@Get()
async getProductType(
@Query() query: string = "",
@Query() productGroupId?: string,
@Query() status?: Status,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = {
AND: { productGroupId },
OR: [
{ name: { contains: query }, ...filterStatus(status) },
{ detail: { contains: query }, ...filterStatus(status) },
],
} satisfies Prisma.ProductTypeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.productType.findMany({
include: {
_count: {
select: { product: true },
},
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.productType.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{typeId}")
async getProductTypeById(@Path() typeId: string) {
const record = await prisma.productType.findFirst({
where: { id: typeId },
});
if (!record)
throw new HttpError(
HttpStatus.NOT_FOUND,
"Product type cannot be found.",
"productTypeNotFound",
);
return record;
}
@Post()
async createProductType(@Request() req: RequestWithUser, @Body() body: ProductTypeCreate) {
const productGroup = await prisma.productGroup.findFirst({
where: { id: body.productGroupId },
});
if (!productGroup) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Product group associated cannot be found.",
"productGroupAssociatedBadReq",
);
}
const record = await prisma.$transaction(
async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: `PRODTYP_T${productGroup.code}`,
},
create: {
key: `PRODTYP_T${productGroup.code}`,
value: 1,
},
update: { value: { increment: 1 } },
});
return await tx.productType.create({
data: {
...body,
statusOrder: +(body.status === "INACTIVE"),
code: `T${productGroup.code}${last.value.toString().padStart(2, "0")}`,
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
if (productGroup.status === "CREATED") {
await prisma.productGroup.update({
where: { id: body.productGroupId },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{typeId}")
async editProductType(
@Request() req: RequestWithUser,
@Body() body: ProductTypeUpdate,
@Path() typeId: string,
) {
const productGroup = await prisma.productGroup.findFirst({
where: { id: body.productGroupId },
});
if (body.productGroupId && !productGroup) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Product group cannot be found.",
"productGroupBadReq",
);
}
if (!(await prisma.productType.findUnique({ where: { id: typeId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Product type cannot be found.",
"productTypeNotFound",
);
}
const record = await prisma.productType.update({
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedBy: req.user.name },
where: { id: typeId },
});
if (productGroup?.status === "CREATED") {
await prisma.productGroup.update({
where: { id: body.productGroupId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
}
return record;
}
@Delete("{typeId}")
async deleteProductType(@Path() typeId: string) {
const record = await prisma.productType.findFirst({ where: { id: typeId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Product type cannot be found.",
"productTypeNotFound",
);
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Product type is in used.", "productTypeInUsed");
}
return await prisma.productType.delete({ where: { id: typeId } });
}
}

View file

@ -0,0 +1,346 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { Prisma, Status } from "@prisma/client";
import prisma from "../../db";
import minio, { presignedGetObjectIfExist } from "../../services/minio";
import { RequestWithUser } from "../../interfaces/user";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type ServiceCreate = {
code: "MOU" | "mou";
name: string;
detail: string;
attributes?: {
[key: string]: any;
};
status?: Status;
work?: {
name: string;
productId: string[];
attributes?: { [key: string]: any };
}[];
};
type ServiceUpdate = {
name?: string;
detail?: string;
attributes?: {
[key: string]: any;
};
status?: "ACTIVE" | "INACTIVE";
work?: {
name: string;
productId: string[];
attributes?: { [key: string]: any };
}[];
};
function imageLocation(id: string) {
return `service/${id}/service-image`;
}
@Route("api/v1/service")
@Tags("Service")
export class ServiceController extends Controller {
@Get("stats")
@Security("keycloak")
async getServiceStats() {
return await prisma.service.count();
}
@Get()
@Security("keycloak")
async getService(
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() status?: Status,
) {
const filterStatus = (val?: Status) => {
if (!val) return {};
return val !== Status.CREATED && val !== Status.ACTIVE
? { status: val }
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
};
const where = {
OR: [
{ name: { contains: query }, ...filterStatus(status) },
{ detail: { contains: query }, ...filterStatus(status) },
],
} satisfies Prisma.ServiceWhereInput;
const [result, total] = await prisma.$transaction([
prisma.service.findMany({
include: {
work: true,
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.service.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{serviceId}")
@Security("keycloak")
async getServiceById(@Path() serviceId: string) {
const record = await prisma.service.findFirst({
include: {
work: {
orderBy: { order: "asc" },
include: {
productOnWork: {
include: { product: true },
orderBy: { order: "asc" },
},
},
},
},
where: { id: serviceId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
}
return Object.assign(record, {
imageUrl: await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(record.id), 60 * 60),
});
}
@Get("{serviceId}/work")
@Security("keycloak")
async getWorkOfService(
@Path() serviceId: string,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
serviceId,
} satisfies Prisma.WorkWhereInput;
const [result, total] = await prisma.$transaction([
prisma.work.findMany({
include: {
productOnWork: {
include: {
product: true,
},
orderBy: { order: "asc" },
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.work.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{serviceId}/image")
async getServiceImageById(@Request() req: RequestWithUser, @Path() serviceId: string) {
const url = await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(serviceId), 60 * 60);
if (!url) {
throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound");
}
return req.res?.redirect(url);
}
@Post()
@Security("keycloak")
async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) {
const { work, ...payload } = body;
const record = await prisma.$transaction(
async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: `SERVICE_${body.code.toLocaleUpperCase()}`,
},
create: {
key: `SERVICE_${body.code.toLocaleUpperCase()}`,
value: 1,
},
update: { value: { increment: 1 } },
});
const workList = await Promise.all(
(work || []).map(async (w, wIdx) =>
tx.work.create({
data: {
name: w.name,
order: wIdx + 1,
attributes: w.attributes,
productOnWork: {
createMany: {
data: w.productId.map((p, pIdx) => ({
productId: p,
order: pIdx + 1,
})),
},
},
},
}),
),
);
return tx.service.create({
include: {
work: {
include: {
productOnWork: {
include: {
product: true,
},
orderBy: { order: "asc" },
},
},
},
},
data: {
...payload,
statusOrder: +(body.status === "INACTIVE"),
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
work: { connect: workList.map((v) => ({ id: v.id })) },
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{serviceId}")
@Security("keycloak")
async editService(
@Request() req: RequestWithUser,
@Body() body: ServiceUpdate,
@Path() serviceId: string,
) {
if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
}
const { work, ...payload } = body;
const record = await prisma.$transaction(async (tx) => {
const workList = await Promise.all(
(work || []).map(async (w, wIdx) =>
tx.work.create({
data: {
name: w.name,
order: wIdx + 1,
attributes: w.attributes,
productOnWork: {
createMany: {
data: w.productId.map((p, pIdx) => ({
productId: p,
order: pIdx + 1,
})),
},
},
},
}),
),
);
return await tx.service.update({
data: {
...payload,
statusOrder: +(payload.status === "INACTIVE"),
work: {
deleteMany: {},
connect: workList.map((v) => ({ id: v.id })),
},
updatedBy: req.user.name,
},
where: { id: serviceId },
});
});
return Object.assign(record, {
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{serviceId}")
@Security("keycloak")
async deleteService(@Path() serviceId: string) {
const record = await prisma.service.findFirst({ where: { id: serviceId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Service is in used.", "serviceInUsed");
}
return await prisma.service.delete({ where: { id: serviceId } });
}
}

View file

@ -15,7 +15,7 @@ import {
import { Prisma, Status, UserType } from "@prisma/client"; import { Prisma, Status, UserType } from "@prisma/client";
import prisma from "../db"; import prisma from "../db";
import minio from "../services/minio"; import minio, { presignedGetObjectIfExist } from "../services/minio";
import { RequestWithUser } from "../interfaces/user"; import { RequestWithUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
@ -119,11 +119,11 @@ function imageLocation(id: string) {
return `user/profile-img-${id}`; return `user/profile-img-${id}`;
} }
@Route("api/user") @Route("api/v1/user")
@Tags("User") @Tags("User")
@Security("keycloak")
export class UserController extends Controller { export class UserController extends Controller {
@Get("type-stats") @Get("type-stats")
@Security("keycloak")
async getUserTypeStats() { async getUserTypeStats() {
const list = await prisma.user.groupBy({ const list = await prisma.user.groupBy({
by: "userType", by: "userType",
@ -145,6 +145,7 @@ export class UserController extends Controller {
} }
@Get() @Get()
@Security("keycloak")
async getUser( async getUser(
@Query() userType?: UserType, @Query() userType?: UserType,
@Query() zipCode?: string, @Query() zipCode?: string,
@ -166,7 +167,7 @@ export class UserController extends Controller {
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
prisma.user.findMany({ prisma.user.findMany({
orderBy: { createdAt: "asc" }, orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: { include: {
province: true, province: true,
district: true, district: true,
@ -185,7 +186,7 @@ export class UserController extends Controller {
result.map(async (v) => ({ result.map(async (v) => ({
...v, ...v,
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined, branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
profileImageUrl: await minio.presignedGetObject( profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET, MINIO_BUCKET,
imageLocation(v.id), imageLocation(v.id),
12 * 60 * 60, 12 * 60 * 60,
@ -199,6 +200,7 @@ export class UserController extends Controller {
} }
@Get("{userId}") @Get("{userId}")
@Security("keycloak")
async getUserById(@Path() userId: string) { async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({ const record = await prisma.user.findFirst({
include: { include: {
@ -209,8 +211,7 @@ export class UserController extends Controller {
where: { id: userId }, where: { id: userId },
}); });
if (!record) if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
return Object.assign(record, { return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject( profileImageUrl: await minio.presignedGetObject(
@ -222,6 +223,7 @@ export class UserController extends Controller {
} }
@Post() @Post()
@Security("keycloak")
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) { async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
if (body.provinceId || body.districtId || body.subDistrictId) { if (body.provinceId || body.districtId || body.subDistrictId) {
const [province, district, subDistrict] = await prisma.$transaction([ const [province, district, subDistrict] = await prisma.$transaction([
@ -233,21 +235,21 @@ export class UserController extends Controller {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Province cannot be found.", "Province cannot be found.",
"missing_or_invalid_parameter", "relationProvinceNotFound",
); );
} }
if (body.districtId && !district) { if (body.districtId && !district) {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"District cannot be found.", "District cannot be found.",
"missing_or_invalid_parameter", "relationDistrictNotFound",
); );
} }
if (body.subDistrictId && !subDistrict) { if (body.subDistrictId && !subDistrict) {
throw new HttpError( throw new HttpError(
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.", "Sub-district cannot be found.",
"missing_or_invalid_parameter", "relationSubDistrictNotFound",
); );
} }
} }
@ -268,6 +270,7 @@ export class UserController extends Controller {
firstName: body.firstName, firstName: body.firstName,
lastName: body.lastName, lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"], requiredActions: ["UPDATE_PASSWORD"],
enabled: rest.status !== "INACTIVE",
}); });
if (!userId || typeof userId !== "string") { if (!userId || typeof userId !== "string") {
@ -288,13 +291,14 @@ export class UserController extends Controller {
data: { data: {
id: userId, id: userId,
...rest, ...rest,
statusOrder: +(rest.status === "INACTIVE"),
username, username,
userRole: role.name, userRole: role.name,
province: { connect: provinceId ? { id: provinceId } : undefined }, province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name, createdBy: req.user.name,
updateBy: req.user.name, updatedBy: req.user.name,
}, },
}); });
@ -315,6 +319,7 @@ export class UserController extends Controller {
} }
@Put("{userId}") @Put("{userId}")
@Security("keycloak")
async editUser( async editUser(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Body() body: UserUpdate, @Body() body: UserUpdate,
@ -370,14 +375,23 @@ export class UserController extends Controller {
if (!resultAddRole) { if (!resultAddRole) {
throw new Error("Failed. Cannot set user's role."); throw new Error("Failed. Cannot set user's role.");
} else { } else {
if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole); if (Array.isArray(currentRole))
await removeUserRoles(
userId,
currentRole.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) =>
a.name.includes(b),
),
),
);
} }
userRole = role.name; userRole = role.name;
} }
if (body.username) { if (body.username) {
await editUser(userId, { username: body.username }); await editUser(userId, { username: body.username, enabled: body.status !== "INACTIVE" });
} }
const { provinceId, districtId, subDistrictId, ...rest } = body; const { provinceId, districtId, subDistrictId, ...rest } = body;
@ -387,7 +401,7 @@ export class UserController extends Controller {
}); });
if (!user) { if (!user) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
} }
const lastUserOfType = const lastUserOfType =
@ -406,6 +420,7 @@ export class UserController extends Controller {
include: { province: true, district: true, subDistrict: true }, include: { province: true, district: true, subDistrict: true },
data: { data: {
...rest, ...rest,
statusOrder: +(rest.status === "INACTIVE"),
userRole, userRole,
code: code:
(lastUserOfType && (lastUserOfType &&
@ -423,7 +438,7 @@ export class UserController extends Controller {
connect: subDistrictId ? { id: subDistrictId } : undefined, connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined, disconnect: subDistrictId === null || undefined,
}, },
updateBy: req.user.name, updatedBy: req.user.name,
}, },
where: { id: userId }, where: { id: userId },
}); });
@ -443,6 +458,7 @@ export class UserController extends Controller {
} }
@Delete("{userId}") @Delete("{userId}")
@Security("keycloak")
async deleteUser(@Path() userId: string) { async deleteUser(@Path() userId: string) {
const record = await prisma.user.findFirst({ const record = await prisma.user.findFirst({
include: { include: {
@ -454,11 +470,11 @@ export class UserController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
} }
if (record.status !== Status.CREATED) { if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used"); throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "userInUsed");
} }
await minio.removeObject(MINIO_BUCKET, imageLocation(userId), { await minio.removeObject(MINIO_BUCKET, imageLocation(userId), {
@ -475,9 +491,7 @@ export class UserController extends Controller {
stream.on("error", () => reject(new Error("MinIO error."))); stream.on("error", () => reject(new Error("MinIO error.")));
}).then((list) => { }).then((list) => {
list.map(async (v) => { list.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, { await minio.removeObject(MINIO_BUCKET, v, { forceDelete: true });
forceDelete: true,
});
}); });
}); });
@ -498,7 +512,7 @@ function attachmentLocation(uid: string) {
return `user-attachment/${uid}`; return `user-attachment/${uid}`;
} }
@Route("api/user/{userId}/attachment") @Route("api/v1/user/{userId}/attachment")
@Tags("User") @Tags("User")
@Security("keycloak") @Security("keycloak")
export class UserAttachmentController extends Controller { export class UserAttachmentController extends Controller {
@ -514,7 +528,7 @@ export class UserAttachmentController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
} }
const list = await new Promise<string[]>((resolve, reject) => { const list = await new Promise<string[]>((resolve, reject) => {
@ -547,7 +561,7 @@ export class UserAttachmentController extends Controller {
}); });
if (!record) { if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
} }
return await Promise.all( return await Promise.all(

View file

@ -0,0 +1,311 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { Prisma, Status } from "@prisma/client";
import prisma from "../../db";
import { RequestWithUser } from "../../interfaces/user";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
type WorkCreate = {
order: number;
name: string;
productId: string[];
attributes?: {
[key: string]: any;
};
};
type WorkUpdate = {
order?: number;
name?: string;
productId?: string[];
attributes?: {
[key: string]: any;
};
};
@Route("api/v1/work")
@Tags("Work")
@Security("keycloak")
export class WorkController extends Controller {
@Get()
async getWork(
@Query() baseOnly?: boolean,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [{ name: { contains: query }, serviceId: baseOnly ? null : undefined }],
} satisfies Prisma.WorkWhereInput;
const [result, total] = await prisma.$transaction([
prisma.work.findMany({
include: {
productOnWork: {
include: {
product: true,
},
orderBy: {
order: "asc",
},
},
},
orderBy: { createdAt: "asc" },
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.work.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{workId}")
async getWorkById(@Path() workId: string) {
const record = await prisma.work.findFirst({
where: { id: workId },
});
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "Work cannot be found.", "workNotFound");
return record;
}
@Get("{workId}/product")
async getProductOfWork(
@Path() workId: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
AND: [
{
workProduct: { some: { workId } },
},
{
OR: [{ name: { contains: query } }],
},
],
} satisfies Prisma.ProductWhereInput;
const [result, total] = await prisma.$transaction([
prisma.product.findMany({
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.product.count({
where,
}),
]);
return { result, page, pageSize, total };
}
@Post()
async createWork(@Request() req: RequestWithUser, @Body() body: WorkCreate) {
const { productId, ...payload } = body;
const exist = await prisma.work.findFirst({
include: {
productOnWork: {
include: {
product: true,
},
},
},
where: {
productOnWork: {
every: {
productId: { in: productId },
},
},
NOT: {
OR: [
{
productOnWork: {
some: {
productId: { notIn: productId },
},
},
},
{
productOnWork: {
none: {},
},
},
],
},
},
});
if (exist) return exist;
const productList = await prisma.product.findMany({
where: { id: { in: productId } },
});
if (productList.length !== productId.length) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Some product not found.", "someProductBadReq");
}
const record = await prisma.work.create({
include: {
productOnWork: {
include: {
product: true,
},
orderBy: {
order: "asc",
},
},
},
data: {
...payload,
productOnWork: {
createMany: {
data: productId.map((v, i) => ({
order: i + 1,
productId: v,
createdBy: req.user.name,
updatedBy: req.user.name,
})),
},
},
createdBy: req.user.name,
updatedBy: req.user.name,
},
});
await prisma.product.updateMany({
where: { id: { in: body.productId }, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{workId}")
async editWork(
@Request() req: RequestWithUser,
@Body() body: WorkUpdate,
@Path() workId: string,
) {
const { productId, ...payload } = body;
if (!(await prisma.work.findUnique({ where: { id: workId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Work cannot be found.", "workNotFound");
}
const exist = await prisma.work.findFirst({
include: {
productOnWork: {
include: {
product: true,
},
},
},
where: {
productOnWork: {
every: {
productId: { in: productId },
},
},
NOT: {
OR: [
{ id: workId },
{
productOnWork: {
some: {
productId: { notIn: productId },
},
},
},
{
productOnWork: {
none: {},
},
},
],
},
},
});
if (exist) return exist;
const record = await prisma.work.update({
include: {
productOnWork: {
include: {
product: true,
},
orderBy: {
order: "asc",
},
},
},
where: { id: workId },
data: {
...payload,
productOnWork: productId
? {
deleteMany: {
productId: { notIn: productId },
},
upsert: productId.map((v, i) => ({
where: {
workId_productId: {
workId,
productId: v,
},
},
update: { order: i + 1 },
create: {
order: i + 1,
productId: v,
createdBy: req.user.name,
updatedBy: req.user.name,
},
})),
}
: undefined,
updatedBy: req.user.name,
},
});
return record;
}
@Delete("{workId}")
async deleteWork(@Path() workId: string) {
const record = await prisma.work.findFirst({ where: { id: workId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Work cannot be found.", "workNotFound");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Work is in used.", "workInUsed");
}
return await prisma.work.delete({ where: { id: workId } });
}
}

View file

@ -1,7 +1,23 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely";
import kyselyExtension from "prisma-extension-kysely";
import type { DB } from "./generated/kysely/types";
const prisma = new PrismaClient({ const prisma = new PrismaClient({
errorFormat: process.env.NODE_ENV === "production" ? "minimal" : "pretty", errorFormat: process.env.NODE_ENV === "production" ? "minimal" : "pretty",
}); }).$extends(
kyselyExtension({
kysely: (driver) =>
new Kysely<DB>({
dialect: {
createDriver: () => driver,
createAdapter: () => new PostgresAdapter(),
createIntrospector: (db: Kysely<DB>) => new PostgresIntrospector(db),
createQueryCompiler: () => new PostgresQueryCompiler(),
},
plugins: [],
}),
}),
);
export default prisma; export default prisma;

View file

@ -1,29 +1,20 @@
import HttpStatus from "./http-status"; 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 { class HttpError extends Error {
/** /**
* HTTP Status Code * HTTP Status Code
*/ */
status: HttpStatus; status: HttpStatus;
message: string; message: string;
devMessage?: DevMessage; code?: string;
constructor(status: HttpStatus, message: string, devMessage?: DevMessage) { constructor(status: HttpStatus, message: string, code?: string) {
super(message); super(message);
this.name = "HttpError"; this.name = "HttpError";
this.status = status; this.status = status;
this.message = message; this.message = message;
this.devMessage = devMessage; this.code = code;
} }
} }

View file

@ -2,6 +2,7 @@ import type { Request } from "express";
export type RequestWithUser = Request & { export type RequestWithUser = Request & {
user: { user: {
sub: string;
name: string; name: string;
given_name: string; given_name: string;
familiy_name: string; familiy_name: string;

View file

@ -10,8 +10,15 @@ export async function expressAuthentication(
) { ) {
switch (securityName) { switch (securityName) {
case "keycloak": case "keycloak":
return keycloakAuth(request, scopes); const authData = await keycloakAuth(request, scopes);
request.app.locals.logData.sessionId = authData.session_state;
request.app.locals.logData.user = authData.preffered_username;
return authData;
default: default:
throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "ไม่ทราบวิธียืนยันตัวตน"); throw new HttpError(
HttpStatus.NOT_IMPLEMENTED,
"Unknown how to verify identity.",
"unknowHowToVerify",
);
} }
} }

View file

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

View file

@ -1,5 +1,6 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import elasticsearch from "../services/elasticsearch"; import elasticsearch from "../services/elasticsearch";
import { randomUUID } from "crypto";
if (!process.env.ELASTICSEARCH_INDEX) { if (!process.env.ELASTICSEARCH_INDEX) {
throw new Error("Require ELASTICSEARCH_INDEX to store log."); throw new Error("Require ELASTICSEARCH_INDEX to store log.");
@ -50,16 +51,11 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
host: req.hostname, host: req.hostname,
sessionId: req.headers["x-session-id"], sessionId: req.headers["x-session-id"],
rtId: req.headers["x-rtid"], rtId: req.headers["x-rtid"],
tId: req.headers["x-tid"], tId: randomUUID(),
method: req.method, method: req.method,
endpoint: req.url, endpoint: req.url,
responseCode: res.statusCode, responseCode: res.statusCode,
responseDescription: responseDescription: data?.code,
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, input: (level === 4 && JSON.stringify(req.body, null, 2)) || undefined,
output: (level === 4 && JSON.stringify(data, null, 2)) || undefined, output: (level === 4 && JSON.stringify(data, null, 2)) || undefined,
...req.app.locals.logData, ...req.app.locals.logData,

View file

@ -5,14 +5,14 @@ import HttpStatus from "../interfaces/http-status";
export function role( export function role(
role: string | string[], role: string | string[],
errorMessage: string = "คุณไม่มีสิทธิในการเข้าถึงทรัพยากรดังกล่าว", errorMessage: string = "You do not have permission to access this resource.",
) { ) {
return (req: RequestWithUser, _res: Response, next: NextFunction) => { return (req: RequestWithUser, _res: Response, next: NextFunction) => {
if (!Array.isArray(role) && !req.user.role.includes(role) && !req.user.role.includes("*")) { if (!Array.isArray(role) && !req.user.role.includes(role) && !req.user.role.includes("*")) {
throw new HttpError(HttpStatus.FORBIDDEN, errorMessage); throw new HttpError(HttpStatus.FORBIDDEN, errorMessage, "noPermissionToAccess");
} }
if (role !== "*" && !req.user.role.some((v) => role.includes(v))) { if (role !== "*" && !req.user.role.some((v) => role.includes(v))) {
throw new HttpError(HttpStatus.FORBIDDEN, errorMessage); throw new HttpError(HttpStatus.FORBIDDEN, errorMessage, "noPermissionToAccess");
} }
return next(); return next();
}; };

View file

@ -2,8 +2,8 @@ import { DecodedJwt, createDecoder } from "fast-jwt";
const KC_URL = process.env.KC_URL; const KC_URL = process.env.KC_URL;
const KC_REALM = process.env.KC_REALM; const KC_REALM = process.env.KC_REALM;
const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID; const KC_ADMIN_USERNAME = process.env.KC_ADMIN_USERNAME;
const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; const KC_ADMIN_PASSWORD = process.env.KC_ADMIN_PASSWORD;
let token: string | null = null; let token: string | null = null;
let decoded: DecodedJwt | null = null; let decoded: DecodedJwt | null = null;
@ -14,7 +14,7 @@ const jwtDecode = createDecoder({ complete: true });
* Check if token is expired or will expire in 30 seconds * Check if token is expired or will expire in 30 seconds
* @returns true if expire or can't get exp, false otherwise * @returns true if expire or can't get exp, false otherwise
*/ */
export function isTokenExpired(token: string, beforeExpire: number = 30) { export function isTokenExpired(token: string, beforeExpire: number = 10) {
decoded = jwtDecode(token); decoded = jwtDecode(token);
if (decoded && decoded.payload.exp) { if (decoded && decoded.payload.exp) {
@ -28,19 +28,20 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) {
* Get token from keycloak if needed * Get token from keycloak if needed
*/ */
export async function getToken() { export async function getToken() {
if (!KC_CLIENT_ID || !KC_SECRET) { if (!KC_ADMIN_PASSWORD || !KC_ADMIN_USERNAME) {
throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature."); throw new Error("KC_ADMIN_USERNAME and KC_ADMIN_PASSWORD are required to used this feature.");
} }
if (token && !isTokenExpired(token)) return token; if (token && !isTokenExpired(token)) return token;
const body = new URLSearchParams(); const body = new URLSearchParams();
body.append("client_id", KC_CLIENT_ID); body.append("client_id", "admin-cli");
body.append("client_secret", KC_SECRET); body.append("grant_type", "password");
body.append("grant_type", "client_credentials"); body.append("username", KC_ADMIN_USERNAME);
body.append("password", KC_ADMIN_PASSWORD);
const res = await fetch(`${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token`, { const res = await fetch(`${KC_URL}/realms/master/protocol/openid-connect/token`, {
method: "POST", method: "POST",
body: body, body: body,
}).catch((e) => console.error(e)); }).catch((e) => console.error(e));
@ -74,7 +75,7 @@ export async function createUser(username: string, password: string, opts?: Reco
}, },
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
enabled: true, enabled: opts?.enabled !== undefined ? opts.enabled : true,
credentials: [{ type: "password", value: password }], credentials: [{ type: "password", value: password }],
username, username,
...opts, ...opts,
@ -109,7 +110,7 @@ export async function editUser(userId: string, opts: Record<string, any>) {
}, },
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
enabled: true, enabled: opts?.enabled !== undefined ? opts.enabled : true,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
...rest, ...rest,
}), }),

View file

@ -9,3 +9,52 @@ const minio = new Client({
}); });
export default minio; export default minio;
// minio typescript does not support include version
type BucketItemWithVersion = {
name: string;
lastModified: string;
etag: string;
size: number;
versionId: string;
isLatest: boolean;
isDeleteMarker: boolean;
};
export async function listObjectVersion(bucket: string, obj: string) {
return await new Promise<BucketItemWithVersion[]>((resolve, reject) => {
const data: BucketItemWithVersion[] = [];
// @ts-ignore
let stream = minio.listObjects(bucket, obj, true, {
IncludeVersion: true, // type error (ts not support) - expected 3 args but got 4
});
stream.on("data", (obj) => data.push(obj as unknown as BucketItemWithVersion));
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve(data));
});
}
export async function deleteObjectAllVersion(bucket: string, obj: string) {
const item = await listObjectVersion(bucket, obj);
return await new Promise((resolve, reject) => {
minio.removeObjects(
bucket,
// @ts-ignore
item.map(({ name, versionId }) => ({ name, versionId })), // type error (ts not support) - expected "string[]"
(e) => (e && reject(e)) || resolve(true),
);
});
}
export async function presignedGetObjectIfExist(bucket: string, obj: string, exp?: number) {
if (
await minio.statObject(bucket, obj).catch((e) => {
if (e.code === "NotFound") return false;
throw new Error("Object storage error.");
})
) {
return await minio.presignedGetObject(bucket, obj, exp);
}
return null;
}

View file

@ -29,10 +29,11 @@
{ "name": "Employee Checkup" }, { "name": "Employee Checkup" },
{ "name": "Employee Work" }, { "name": "Employee Work" },
{ "name": "Employee Other Info" }, { "name": "Employee Other Info" },
{ "name": "Service" }, { "name": "Product Group" },
{ "name": "Work" },
{ "name": "Product Type" }, { "name": "Product Type" },
{ "name": "Product Group" } { "name": "Product" },
{ "name": "Work" },
{ "name": "Service" }
] ]
} }
}, },