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_REALM=dev
KC_SERVICE_ACCOUNT_CLIENT_ID=dev-service
KC_SERVICE_ACCOUNT_SECRET=
KC_ADMIN_USERNAME=admin
KC_ADMIN_PASSWORD=
APP_HOST=0.0.0.0
APP_PORT=3000

1
.gitignore vendored
View file

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

View file

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

View file

@ -22,21 +22,24 @@
"@types/express": "^4.17.21",
"@types/node": "^20.12.2",
"@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.1.0",
"nodemon": "^3.1.3",
"prettier": "^3.2.5",
"prisma": "^5.12.1",
"prisma": "^5.16.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.3"
},
"dependencies": {
"@elastic/elasticsearch": "^8.13.0",
"@prisma/client": "5.12.1",
"@prisma/client": "^5.16.0",
"@tsoa/runtime": "^6.2.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"fast-jwt": "^4.0.0",
"kysely": "^0.27.3",
"minio": "^7.1.3",
"prisma-extension-kysely": "^2.1.0",
"promise.any": "^2.0.6",
"swagger-ui-express": "^5.0.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
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
CREATE TABLE "Province" (
"id" TEXT NOT NULL,
@ -11,7 +108,7 @@ CREATE TABLE "Province" (
"nameEN" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Province_pkey" PRIMARY KEY ("id")
@ -25,7 +122,7 @@ CREATE TABLE "District" (
"provinceId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "District_pkey" PRIMARY KEY ("id")
@ -40,7 +137,7 @@ CREATE TABLE "SubDistrict" (
"districtId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id")
@ -55,20 +152,23 @@ CREATE TABLE "Branch" (
"nameEN" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"contactName" TEXT,
"lineId" TEXT,
"latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL,
"isHeadOffice" BOOLEAN NOT NULL DEFAULT false,
"headOfficeId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Branch_pkey" PRIMARY KEY ("id")
@ -78,11 +178,10 @@ CREATE TABLE "Branch" (
CREATE TABLE "BranchContact" (
"id" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"lineId" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchContact_pkey" PRIMARY KEY ("id")
@ -95,7 +194,7 @@ CREATE TABLE "BranchUser" (
"userId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchUser_pkey" PRIMARY KEY ("id")
@ -104,12 +203,12 @@ CREATE TABLE "BranchUser" (
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"keycloakId" TEXT NOT NULL,
"code" TEXT,
"firstName" TEXT NOT NULL,
"firstNameEN" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"lastNameEN" TEXT NOT NULL,
"username" TEXT NOT NULL,
"gender" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
@ -122,6 +221,8 @@ CREATE TABLE "User" (
"registrationNo" TEXT,
"startDate" TIMESTAMP(3),
"retireDate" TIMESTAMP(3),
"checkpoint" TEXT,
"checkpointEN" TEXT,
"userType" "UserType" NOT NULL,
"userRole" TEXT NOT NULL,
"discountCondition" TEXT,
@ -131,10 +232,13 @@ CREATE TABLE "User" (
"sourceNationality" TEXT,
"importNationality" TEXT,
"trainingPlace" TEXT,
"responsibleArea" TEXT,
"birthDate" TIMESTAMP(3),
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
@ -144,14 +248,17 @@ CREATE TABLE "User" (
CREATE TABLE "Customer" (
"id" 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,
"customerNameEN" TEXT NOT NULL,
"imageUrl" TEXT,
"taxNo" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
@ -160,12 +267,13 @@ CREATE TABLE "Customer" (
-- CreateTable
CREATE TABLE "CustomerBranch" (
"id" TEXT NOT NULL,
"branchNo" TEXT NOT NULL,
"branchNo" INTEGER NOT NULL,
"code" TEXT NOT NULL,
"legalPersonNo" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"taxNo" TEXT NOT NULL,
"taxNo" TEXT,
"registerName" TEXT NOT NULL,
"registerDate" TIMESTAMP(3) NOT NULL,
"authorizedCapital" TEXT NOT NULL,
@ -177,12 +285,20 @@ CREATE TABLE "CustomerBranch" (
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL,
"employmentOffice" 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',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerBranch_pkey" PRIMARY KEY ("id")
@ -200,42 +316,70 @@ CREATE TABLE "Employee" (
"dateOfBirth" TIMESTAMP(3) NOT NULL,
"gender" TEXT NOT NULL,
"nationality" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"address" TEXT,
"addressEN" TEXT,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"arrivalBarricade" TEXT NOT NULL,
"arrivalCardNo" TEXT NOT NULL,
"passportType" TEXT NOT NULL,
"passportNumber" TEXT NOT NULL,
"passportIssueDate" TIMESTAMP(3) 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,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
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
CREATE TABLE "EmployeeCheckup" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"checkupResult" TEXT NOT NULL,
"checkupType" TEXT NOT NULL,
"checkupResult" TEXT,
"checkupType" TEXT,
"provinceId" TEXT,
"hospitalName" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"medicalBenefitScheme" TEXT NOT NULL,
"insuranceCompany" TEXT NOT NULL,
"coverageStartDate" TIMESTAMP(3) NOT NULL,
"coverageExpireDate" TIMESTAMP(3) NOT NULL,
"hospitalName" TEXT,
"remark" TEXT,
"medicalBenefitScheme" TEXT,
"insuranceCompany" TEXT,
"coverageStartDate" TIMESTAMP(3),
"coverageExpireDate" TIMESTAMP(3),
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeCheckup_pkey" PRIMARY KEY ("id")
@ -245,17 +389,18 @@ CREATE TABLE "EmployeeCheckup" (
CREATE TABLE "EmployeeWork" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"ownerName" TEXT NOT NULL,
"positionName" TEXT NOT NULL,
"jobType" TEXT NOT NULL,
"workplace" TEXT NOT NULL,
"workPermitNo" TEXT NOT NULL,
"workPermitIssuDate" TIMESTAMP(3) NOT NULL,
"workPermitExpireDate" TIMESTAMP(3) NOT NULL,
"workEndDate" TIMESTAMP(3) NOT NULL,
"ownerName" TEXT,
"positionName" TEXT,
"jobType" TEXT,
"workplace" TEXT,
"workPermitNo" TEXT,
"workPermitIssuDate" TIMESTAMP(3),
"workPermitExpireDate" TIMESTAMP(3),
"workEndDate" TIMESTAMP(3),
"remark" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeWork_pkey" PRIMARY KEY ("id")
@ -265,13 +410,20 @@ CREATE TABLE "EmployeeWork" (
CREATE TABLE "EmployeeOtherInfo" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"citizenId" TEXT NOT NULL,
"fatherFullName" TEXT NOT NULL,
"motherFullName" TEXT NOT NULL,
"birthPlace" TEXT NOT NULL,
"citizenId" TEXT,
"fatherBirthPlace" TEXT,
"fatherFirstName" TEXT,
"fatherLastName" TEXT,
"motherBirthPlace" TEXT,
"motherFirstName" TEXT,
"motherLastName" TEXT,
"fatherFirstNameEN" TEXT,
"fatherLastNameEN" TEXT,
"motherFirstNameEN" TEXT,
"motherLastNameEN" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeOtherInfo_pkey" PRIMARY KEY ("id")
@ -283,10 +435,12 @@ CREATE TABLE "Service" (
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"attributes" JSONB,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
@ -297,11 +451,13 @@ CREATE TABLE "Work" (
"id" TEXT NOT NULL,
"order" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"attributes" JSONB,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"serviceId" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Work_pkey" PRIMARY KEY ("id")
@ -309,14 +465,15 @@ CREATE TABLE "Work" (
-- CreateTable
CREATE TABLE "WorkProduct" (
"id" TEXT NOT NULL,
"order" INTEGER NOT NULL,
"workId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("id")
CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("workId","productId")
);
-- CreateTable
@ -327,9 +484,10 @@ CREATE TABLE "ProductGroup" (
"detail" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductGroup_pkey" PRIMARY KEY ("id")
@ -343,10 +501,12 @@ CREATE TABLE "ProductType" (
"detail" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"productGroupId" TEXT NOT NULL,
CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id")
);
@ -357,22 +517,49 @@ CREATE TABLE "Product" (
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"process" TEXT NOT NULL,
"price" INTEGER NOT NULL,
"agentPrice" INTEGER NOT NULL,
"serviceCharge" INTEGER NOT NULL,
"imageUrl" TEXT NOT NULL,
"process" INTEGER NOT NULL,
"price" DOUBLE PRECISION NOT NULL,
"agentPrice" DOUBLE PRECISION NOT NULL,
"serviceCharge" DOUBLE PRECISION NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"remark" TEXT,
"productTypeId" TEXT,
"productGroupId" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
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
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
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
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;
-- 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
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"
}
generator kysely {
provider = "prisma-kysely"
output = "../src/generated/kysely"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
@ -17,7 +22,7 @@ model Menu {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
@ -40,7 +45,7 @@ model RoleMenuPermission {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -57,7 +62,7 @@ model UserMenuPermission {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -72,7 +77,7 @@ model MenuComponent {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
roleMenuComponentPermission RoleMenuComponentPermission[]
userMennuComponentPermission UserMenuComponentPermission[]
@ -89,10 +94,15 @@ model RoleMenuComponentPermission {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
model RunningNo {
key String @id @unique
value Int
}
model UserMenuComponentPermission {
id String @id @default(uuid())
@ -106,7 +116,7 @@ model UserMenuComponentPermission {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -117,7 +127,7 @@ model Province {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
district District[]
@ -138,7 +148,7 @@ model District {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
subDistrict SubDistrict[]
@ -159,7 +169,7 @@ model SubDistrict {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
branch Branch[]
@ -207,11 +217,12 @@ model Branch {
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
headOfficeId String?
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
branch Branch[] @relation(name: "HeadOfficeRelation")
@ -228,7 +239,7 @@ model BranchContact {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -243,7 +254,7 @@ model BranchUser {
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -307,16 +318,18 @@ model User {
birthDate DateTime?
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
branch BranchUser[]
userMenuPermission UserMenuPermission[]
userMenuComponentPermission UserMenuComponentPermission[]
employeeHistory EmployeeHistory[]
}
enum CustomerType {
@ -327,15 +340,19 @@ enum CustomerType {
model Customer {
id String @id @default(uuid())
code String
personName String
personNameEN String?
customerType CustomerType
customerName String
customerNameEN String
taxNo String?
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
branch CustomerBranch[]
@ -343,7 +360,8 @@ model Customer {
model CustomerBranch {
id String @id @default(uuid())
branchNo String
branchNo Int
code String
legalPersonNo String
name String
@ -352,7 +370,7 @@ model CustomerBranch {
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
taxNo String
taxNo String?
registerName String
registerDate DateTime
authorizedCapital String
@ -374,14 +392,22 @@ model CustomerBranch {
email String
telephoneNo String
latitude String
longitude String
employmentOffice 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?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
employee Employee[]
@ -401,8 +427,8 @@ model Employee {
gender String
nationality String
address String
addressEN String
address String?
addressEN String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
@ -415,25 +441,57 @@ model Employee {
zipCode String
email String
telephoneNo String
passportType String
passportNumber String
passportIssueDate DateTime
passportExpiryDate DateTime
passportIssuingCountry String
passportIssuingPlace String
previousPassportReference String?
arrivalBarricade String
arrivalCardNo String
visaType 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)
customerBranchId String?
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
employeeCheckup EmployeeCheckup[]
employeeWork EmployeeWork[]
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 {
@ -442,22 +500,22 @@ model EmployeeCheckup {
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String
checkupResult String
checkupType String
checkupResult String?
checkupType String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
hospitalName String
remark String
medicalBenefitScheme String
insuranceCompany String
coverageStartDate DateTime
coverageExpireDate DateTime
hospitalName String?
remark String?
medicalBenefitScheme String?
insuranceCompany String?
coverageStartDate DateTime?
coverageExpireDate DateTime?
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -467,18 +525,19 @@ model EmployeeWork {
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String
ownerName String
positionName String
jobType String
workplace String
workPermitNo String
workPermitIssuDate DateTime
workPermitExpireDate DateTime
workEndDate DateTime
ownerName String?
positionName String?
jobType String?
workplace String?
workPermitNo String?
workPermitIssuDate DateTime?
workPermitExpireDate DateTime?
workEndDate DateTime?
remark String?
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
@ -488,61 +547,78 @@ model EmployeeOtherInfo {
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String
citizenId String
fatherFullName String
motherFullName String
birthPlace String
citizenId String?
fatherBirthPlace String?
fatherFirstName String?
fatherLastName String?
motherBirthPlace String?
motherFirstName String?
motherLastName String?
fatherFirstNameEN String?
fatherLastNameEN String?
motherFirstNameEN String?
motherLastNameEN String?
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}
model Service {
id String @id @default(uuid())
code String
name String
detail String
code String
name String
detail String
attributes Json?
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
work Work[]
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
work Work[]
}
model Work {
id String @id @default(uuid())
order Int
name String
order Int
name String
attributes Json?
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId String
status Status @default(CREATED)
statusOrder Int @default(0)
status Status @default(CREATED)
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
service Service? @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId String?
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
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 {
@ -553,13 +629,15 @@ model ProductGroup {
detail String
remark String
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
Product Product[]
createdAt DateTime @default(now())
updatedBy String?
updatedAt DateTime @updatedAt
type ProductType[]
}
model ProductType {
@ -570,13 +648,17 @@ model ProductType {
detail String
remark String
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
productGroupId String
product Product[]
}
@ -586,22 +668,23 @@ model Product {
code String
name String
detail String
process String
price Int
agentPrice Int
serviceCharge Int
imageUrl String
process Int
price Float
agentPrice Float
serviceCharge Float
status Status @default(CREATED)
status Status @default(CREATED)
statusOrder Int @default(0)
remark String?
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
productTypeId String?
productGroup ProductGroup? @relation(fields: [productGroupId], references: [id], onDelete: SetNull)
productGroupId String?
workProduct WorkProduct[]
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedBy String?
updatedAt DateTime @updatedAt
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { Prisma, Status, UserType } from "@prisma/client";
import { Branch, Prisma, Status, User, UserType } from "@prisma/client";
import {
Body,
Controller,
@ -20,7 +20,38 @@ import { RequestWithUser } from "../interfaces/user";
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")
@Security("keycloak")
export class BranchUserController extends Controller {
@ -85,7 +116,7 @@ export class BranchUserController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Branch cannot be found.",
"missing_or_invalid_parameter",
"branchBadReq",
);
}
@ -93,7 +124,7 @@ export class BranchUserController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more user cannot be found.",
"missing_or_invalid_parameter",
"oneOrMoreUserBadReq",
);
}
@ -109,47 +140,12 @@ export class BranchUserController extends Controller {
branchId,
userId: v.id,
createdBy: req.user.name,
updateBy: req.user.name,
updatedBy: req.user.name,
})),
}),
]);
const group: Record<UserType, string[]> = {
USER: [],
AGENCY: [],
DELEGATE: [],
MESSENGER: [],
};
for (const u of user) group[u.userType].push(u.id);
for (const g of Object.values(UserType)) {
if (group[g].length === 0) continue;
const last = await prisma.branchUser.findFirst({
orderBy: { createdAt: "desc" },
include: { user: true },
where: {
branchId,
user: {
userType: g,
code: { startsWith: `${branch.code.slice(4).padEnd(3, "0")}` },
},
},
});
const code = (idx: number) =>
`${branch.code.slice(4).padEnd(3, "0")}${g !== "USER" ? g.charAt(0) : ""}${(+(last?.user.code?.slice(-4) || 0) + idx + 1).toString().padStart(4, "0")}`;
await prisma.$transaction(
group[g].map((v, i) =>
prisma.user.updateMany({
where: { id: v, code: null },
data: { code: code(i) },
}),
),
);
}
await userBranchCodeGen(branch, user);
}
@Delete()
@ -182,7 +178,7 @@ export class BranchUserController extends Controller {
type UserBranchBody = { branch: string[] };
@Route("api/user/{userId}/branch")
@Route("api/v1/user/{userId}/branch")
@Tags("Branch User")
@Security("keycloak")
export class UserBranchController extends Controller {
@ -238,7 +234,7 @@ export class UserBranchController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more branch cannot be found.",
"missing_or_invalid_parameter",
"oneOrMoreBranchBadReq",
);
}
@ -254,7 +250,7 @@ export class UserBranchController extends Controller {
branchId: v.id,
userId,
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}`;
}
type CustomerBranchCreate = {
function attachmentLocation(customerId: string, branchId: string) {
return `customer/${customerId}/branch/${branchId}`;
}
export type CustomerBranchCreate = {
customerId: string;
status?: Status;
legalPersonNo: string;
taxNo: string;
taxNo: string | null;
name: string;
nameEN: string;
addressEN: string;
@ -44,26 +48,34 @@ type CustomerBranchCreate = {
zipCode: string;
email: string;
telephoneNo: string;
longitude: string;
latitude: 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;
};
type CustomerBranchUpdate = {
export type CustomerBranchUpdate = {
customerId?: string;
status?: "ACTIVE" | "INACTIVE";
legalPersonNo?: string;
taxNo?: string;
taxNo?: string | null;
name?: string;
nameEN?: string;
addressEN?: string;
@ -71,44 +83,82 @@ type CustomerBranchUpdate = {
zipCode?: string;
email?: string;
telephoneNo?: string;
longitude?: string;
latitude?: 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;
};
@Route("api/customer-branch")
@Route("api/v1/customer-branch")
@Tags("Customer Branch")
@Security("keycloak")
export class CustomerBranchController extends Controller {
@Get()
async list(
@Query() zipCode?: string,
@Query() customerId?: string,
@Query() status?: Status,
@Query() includeCustomer?: boolean,
@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: [
{ nameEN: { contains: query }, zipCode },
{ name: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
{ nameEN: { contains: query }, zipCode, ...filterStatus(status) },
{ name: { contains: query }, zipCode, ...filterStatus(status) },
{ 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([
prisma.customerBranch.findMany({
orderBy: { createdAt: "asc" },
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: {
customer: includeCustomer,
province: true,
district: true,
subDistrict: true,
_count: true,
},
where,
take: pageSize,
@ -132,7 +182,7 @@ export class CustomerBranchController extends Controller {
});
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;
@ -153,7 +203,6 @@ export class CustomerBranchController extends Controller {
{ firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.EmployeeWhereInput;
@ -191,62 +240,67 @@ export class CustomerBranchController extends Controller {
@Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) {
const [province, district, subDistrict, customer] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerId && !customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const [province, district, subDistrict, customer] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"relationProvinceNotFound",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"relationDistrictNotFound",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"relationSubDistrictNotFound",
);
if (!customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"relationCustomerNotFound",
);
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
const count = await prisma.customerBranch.count({
where: { customerId },
});
const record = await prisma.$transaction(
async (tx) => {
const count = await tx.customerBranch.count({
where: { customerId },
});
const record = await prisma.customerBranch.create({
include: {
province: true,
district: true,
subDistrict: true,
return await tx.customerBranch.create({
include: {
province: true,
district: 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: {
...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,
},
});
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
await prisma.customer.updateMany({
where: { id: customerId, status: Status.CREATED },
@ -275,32 +329,32 @@ export class CustomerBranchController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
"relationProvinceNotFound",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
"relationDistrictNotFound",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
"relationSubDistrictNotFound",
);
if (body.customerId && !customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
"relationCustomerNotFound",
);
}
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
if (!(await prisma.customerBranch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
}
const record = await prisma.customerBranch.update({
@ -312,6 +366,7 @@ export class CustomerBranchController extends Controller {
},
data: {
...rest,
statusOrder: +(rest.status === "INACTIVE"),
customer: { connect: customerId ? { id: customerId } : undefined },
province: {
connect: provinceId ? { id: provinceId } : undefined,
@ -326,7 +381,7 @@ export class CustomerBranchController extends Controller {
disconnect: subDistrictId === null || undefined,
},
createdBy: req.user.name,
updateBy: req.user.name,
updatedBy: req.user.name,
},
});
@ -337,20 +392,143 @@ export class CustomerBranchController extends Controller {
@Delete("{branchId}")
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) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Customer branch cannot be found.",
"data_not_found",
"customerBranchNotFound",
);
}
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";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import minio from "../services/minio";
import minio, { presignedGetObjectIfExist } from "../services/minio";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
@ -27,39 +27,155 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET;
export type CustomerCreate = {
status?: Status;
personName: string;
personNameEN?: string;
customerType: CustomerType;
customerName: 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 = {
status?: "ACTIVE" | "INACTIVE";
personName?: string;
personNameEN?: string;
customerType?: CustomerType;
customerName?: 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) {
return `customer/img-${id}`;
return `customer/${id}/profile-image`;
}
@Route("api/customer")
@Route("api/v1/customer")
@Tags("Customer")
@Security("keycloak")
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()
async list(
@Query() customerType?: CustomerType,
@Query() query: string = "",
@Query() status?: Status,
@Query() page: number = 1,
@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 = {
OR: [{ customerName: { contains: query } }, { customerNameEN: { contains: query } }],
OR: [
{ customerName: { contains: query }, customerType, ...filterStatus(status) },
{ customerNameEN: { contains: query }, customerType, ...filterStatus(status) },
],
} satisfies Prisma.CustomerWhereInput;
const [result, total] = await prisma.$transaction([
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,
take: pageSize,
skip: (page - 1) * pageSize,
@ -71,7 +187,11 @@ export class CustomerController extends Controller {
result: await Promise.all(
result.map(async (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,
@ -82,11 +202,22 @@ export class CustomerController extends Controller {
@Get("{customerId}")
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)
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, {
imageUrl: await minio.presignedGetObject(
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
@ -96,26 +227,101 @@ export class CustomerController extends Controller {
@Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
const last = await prisma.customer.findFirst({
orderBy: { createdAt: "desc" },
where: { customerType: body.customerType },
});
const { customerBranch, ...payload } = body;
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({
data: {
...body,
code,
createdBy: req.user.name,
updateBy: req.user.name,
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 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);
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
@ -134,21 +340,145 @@ export class CustomerController extends Controller {
@Request() req: RequestWithUser,
@Body() body: CustomerUpdate,
) {
if (!(await prisma.customer.findUnique({ where: { id: customerId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
if (!customer) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound");
}
const record = await prisma.customer.update({
where: { id: customerId },
data: {
...body,
createdBy: req.user.name,
updateBy: req.user.name,
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 [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, {
imageUrl: await minio.presignedGetObject(
imageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
@ -166,13 +496,30 @@ export class CustomerController extends Controller {
const record = await prisma.customer.findFirst({ where: { id: customerId } });
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) {
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 HttpError from "../interfaces/http-error";
type EmployeeCheckupCreate = {
checkupType: string;
checkupResult: string;
type EmployeeCheckupPayload = {
checkupType?: string | null;
checkupResult?: string | null;
provinceId?: string | null;
hospitalName: string;
remark: string;
medicalBenefitScheme: string;
insuranceCompany: string;
coverageStartDate: Date;
coverageExpireDate: Date;
hospitalName?: string | null;
remark?: string | null;
medicalBenefitScheme?: string | null;
insuranceCompany?: string | null;
coverageStartDate?: Date | null;
coverageExpireDate?: Date | null;
};
type EmployeeCheckupEdit = {
checkupType?: string;
checkupResult?: string;
provinceId?: string | null;
hospitalName?: string;
remark?: string;
medicalBenefitScheme?: string;
insuranceCompany?: string;
coverageStartDate?: Date;
coverageExpireDate?: Date;
};
@Route("api/employee/{employeeId}/checkup")
@Route("api/v1/employee/{employeeId}/checkup")
@Tags("Employee Checkup")
@Security("keycloak")
export class EmployeeCheckupController extends Controller {
@ -65,7 +51,7 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
"employeeCheckupNotFound",
);
}
return record;
@ -75,7 +61,7 @@ export class EmployeeCheckupController extends Controller {
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeCheckupCreate,
@Body() body: EmployeeCheckupPayload,
) {
if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([
@ -86,13 +72,13 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
"provinceNotFound",
);
if (!employee)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
"employeeNotFound",
);
}
@ -105,7 +91,7 @@ export class EmployeeCheckupController extends Controller {
province: { connect: provinceId ? { id: provinceId } : undefined },
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updateBy: req.user.name,
updatedBy: req.user.name,
},
});
@ -119,7 +105,7 @@ export class EmployeeCheckupController extends Controller {
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() checkupId: string,
@Body() body: EmployeeCheckupEdit,
@Body() body: EmployeeCheckupPayload,
) {
if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([
@ -130,13 +116,13 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
"provinceNotFound",
);
if (!employee)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
"employeeNotFound",
);
}
@ -146,7 +132,7 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
"employeeCheckupNotFound",
);
}
@ -157,7 +143,7 @@ export class EmployeeCheckupController extends Controller {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
updatedBy: req.user.name,
},
});
@ -174,7 +160,7 @@ export class EmployeeCheckupController extends Controller {
throw new HttpError(
HttpStatus.NOT_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 HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import minio from "../services/minio";
import minio, { presignedGetObjectIfExist } from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
@ -26,7 +26,7 @@ if (!process.env.MINIO_BUCKET) {
const MINIO_BUCKET = process.env.MINIO_BUCKET;
function imageLocation(id: string) {
return `employee/profile-img-${id}`;
return `employee/${id}/profile-image`;
}
type EmployeeCreate = {
@ -34,7 +34,6 @@ type EmployeeCreate = {
status?: Status;
code: string;
nrcNo: string;
dateOfBirth: Date;
@ -49,22 +48,75 @@ type EmployeeCreate = {
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
arrivalBarricade: string;
arrivalCardNo: string;
passportType: 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;
districtId?: 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 = {
customerBranchId?: string;
status?: "ACTIVE" | "INACTIVE";
code?: string;
nrcNo?: string;
dateOfBirth?: Date;
@ -79,41 +131,129 @@ type EmployeeUpdate = {
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
arrivalBarricade?: string;
arrivalCardNo?: string;
passportType?: 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;
districtId?: 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")
@Security("keycloak")
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()
async list(
@Query() zipCode?: string,
@Query() gender?: string,
@Query() status?: Status,
@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: [
{ firstName: { contains: query }, zipCode },
{ firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
{ firstName: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ firstNameEN: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ lastName: { contains: query }, zipCode, gender, ...filterStatus(status) },
{ lastNameEN: { contains: query }, zipCode, gender, ...filterStatus(status) },
],
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.employee.findMany({
orderBy: { createdAt: "asc" },
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: {
province: true,
district: true,
@ -130,7 +270,7 @@ export class EmployeeController extends Controller {
result: await Promise.all(
result.map(async (v) => ({
...v,
profileImageUrl: await minio.presignedGetObject(
profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
@ -155,7 +295,7 @@ export class EmployeeController extends Controller {
});
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;
@ -163,67 +303,148 @@ export class EmployeeController extends Controller {
@Post()
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) {
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }),
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({
where: { id: body.customerBranchId },
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(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerBranchId && !customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer Branch cannot be found.",
"missing_or_invalid_parameter",
"Some province cannot be found.",
"someProvinceNotFound",
);
}
}
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({
include: {
province: true,
district: true,
subDistrict: true,
return await prisma.employee.create({
include: {
province: true,
district: 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: {
...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,
},
});
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
await prisma.customerBranch.updateMany({
where: { id: customerBranchId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
await prisma.customer.updateMany({
where: {
branch: {
some: { id: customerBranchId },
},
status: Status.CREATED,
},
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject(
profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
@ -242,71 +463,223 @@ export class EmployeeController extends Controller {
@Body() body: EmployeeUpdate,
@Path() employeeId: string,
) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) {
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerBranchId && !customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
const [province, district, subDistrict, customerBranch, employee] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({
where: { id: body.customerBranchId || undefined },
include: { customer: true },
}),
prisma.employee.findFirst({ where: { id: employeeId } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"relationProvinceNotFound",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"relationDistrictNotFound",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"relationSubDistrictNotFound",
);
if (body.customerBranchId && !customerBranch)
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({
where: { id: employeeId },
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
customerBranch: { connect: customerBranchId ? { id: customerBranchId } : undefined },
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
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 (listProvince.length !== listProvinceId.length) {
throw new HttpError(
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: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
data: {
...rest,
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}")
@ -314,17 +687,20 @@ export class EmployeeController extends Controller {
const record = await prisma.employee.findFirst({ where: { id: employeeId } });
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) {
throw new HttpError(
HttpStatus.FORBIDDEN,
"Emplyee is in used.",
"missing_or_invalid_parameter",
);
throw new HttpError(HttpStatus.FORBIDDEN, "Employee is in used.", "employeeInUsed");
}
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 {
Body,
Controller,
@ -7,7 +6,6 @@ import {
Put,
Path,
Post,
Query,
Request,
Route,
Security,
@ -19,54 +17,44 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type EmployeeOtherInfoCreate = {
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
type EmployeeOtherInfoPayload = {
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 EmployeeOtherInfoUpdate = {
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
};
@Route("api/employee/{employeeId}/other-info")
@Route("api/v1/employee/{employeeId}/other-info")
@Tags("Employee Other Info")
@Security("keycloak")
export class EmployeeOtherInfo extends Controller {
@Get()
async list(@Path() employeeId: string) {
return prisma.employeeOtherInfo.findMany({
return prisma.employeeOtherInfo.findFirst({
orderBy: { createdAt: "asc" },
where: { employeeId },
});
}
@Get("{otherInfoId}")
async getById(@Path() employeeId: string, @Path() otherInfoId: string) {
const record = await prisma.employeeOtherInfo.findFirst({
where: { id: otherInfoId, employeeId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee info cannot be found.", "data_not_found");
}
return record;
}
@Post()
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeOtherInfoCreate,
@Body() body: EmployeeOtherInfoPayload,
) {
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
"employeeBadReq",
);
const record = await prisma.employeeOtherInfo.create({
@ -74,7 +62,7 @@ export class EmployeeOtherInfo extends Controller {
...body,
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updateBy: req.user.name,
updatedBy: req.user.name,
},
});
@ -88,19 +76,19 @@ export class EmployeeOtherInfo extends Controller {
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() otherInfoId: string,
@Body() body: EmployeeOtherInfoUpdate,
@Body() body: EmployeeOtherInfoPayload,
) {
if (!(await prisma.employeeOtherInfo.findUnique({ where: { id: otherInfoId, employeeId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee other info cannot be found.",
"data_not_found",
"employeeOtherNotFound",
);
}
const record = await prisma.employeeOtherInfo.update({
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);
@ -118,7 +106,7 @@ export class EmployeeOtherInfo extends Controller {
throw new HttpError(
HttpStatus.NOT_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,
} from "../services/keycloak";
@Route("api/keycloak")
@Route("api/v1/keycloak")
@Tags("Single-Sign On")
@Security("keycloak")
export class KeycloakController extends Controller {

View file

@ -19,7 +19,7 @@ type MenuEdit = {
url: string;
};
@Route("v1/permission/menu")
@Route("api/v1/permission/menu")
@Tags("Permission")
@Security("keycloak")
export class MenuController extends Controller {
@ -41,7 +41,7 @@ export class MenuController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Parent menu not found.",
"missing_or_invalid_parameter",
"parentMenuBadReq",
);
}
}
@ -66,7 +66,7 @@ export class MenuController extends Controller {
})
.catch((e) => {
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);
});
@ -78,7 +78,83 @@ export class MenuController extends Controller {
async deleteMenu(@Path("menuId") id: string) {
const record = await prisma.menu.deleteMany({ where: { id } });
if (record.count <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found");
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;
};
@Route("v1/permission/menu-component")
@Route("api/v1/permission/menu-component")
@Tags("Permission")
@Security("keycloak")
export class MenuComponentController extends Controller {
@ -117,7 +193,7 @@ export class MenuComponentController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"missing_or_invalid_parameter",
"menuBadReq",
);
}
@ -139,7 +215,7 @@ export class MenuComponentController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"missing_or_invalid_parameter",
"menuBadReq",
);
}
}
@ -155,7 +231,7 @@ export class MenuComponentController extends Controller {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Menu component cannot be found.",
"data_not_found",
"menuComponentNotFound",
);
}
throw new Error(e);
@ -171,7 +247,97 @@ export class MenuComponentController extends Controller {
throw new HttpError(
HttpStatus.NOT_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 from "../db";
import minio from "../services/minio";
import minio, { presignedGetObjectIfExist } from "../services/minio";
import { RequestWithUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
@ -119,11 +119,11 @@ function imageLocation(id: string) {
return `user/profile-img-${id}`;
}
@Route("api/user")
@Route("api/v1/user")
@Tags("User")
@Security("keycloak")
export class UserController extends Controller {
@Get("type-stats")
@Security("keycloak")
async getUserTypeStats() {
const list = await prisma.user.groupBy({
by: "userType",
@ -145,6 +145,7 @@ export class UserController extends Controller {
}
@Get()
@Security("keycloak")
async getUser(
@Query() userType?: UserType,
@Query() zipCode?: string,
@ -166,7 +167,7 @@ export class UserController extends Controller {
const [result, total] = await prisma.$transaction([
prisma.user.findMany({
orderBy: { createdAt: "asc" },
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: {
province: true,
district: true,
@ -185,7 +186,7 @@ export class UserController extends Controller {
result.map(async (v) => ({
...v,
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
profileImageUrl: await minio.presignedGetObject(
profileImageUrl: await presignedGetObjectIfExist(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
@ -199,6 +200,7 @@ export class UserController extends Controller {
}
@Get("{userId}")
@Security("keycloak")
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
@ -209,8 +211,7 @@ export class UserController extends Controller {
where: { id: userId },
});
if (!record)
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
@ -222,6 +223,7 @@ export class UserController extends Controller {
}
@Post()
@Security("keycloak")
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
if (body.provinceId || body.districtId || body.subDistrictId) {
const [province, district, subDistrict] = await prisma.$transaction([
@ -233,21 +235,21 @@ export class UserController extends Controller {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
"relationProvinceNotFound",
);
}
if (body.districtId && !district) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
"relationDistrictNotFound",
);
}
if (body.subDistrictId && !subDistrict) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
"relationSubDistrictNotFound",
);
}
}
@ -268,6 +270,7 @@ export class UserController extends Controller {
firstName: body.firstName,
lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"],
enabled: rest.status !== "INACTIVE",
});
if (!userId || typeof userId !== "string") {
@ -288,13 +291,14 @@ export class UserController extends Controller {
data: {
id: userId,
...rest,
statusOrder: +(rest.status === "INACTIVE"),
username,
userRole: role.name,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
updatedBy: req.user.name,
},
});
@ -315,6 +319,7 @@ export class UserController extends Controller {
}
@Put("{userId}")
@Security("keycloak")
async editUser(
@Request() req: RequestWithUser,
@Body() body: UserUpdate,
@ -370,14 +375,23 @@ export class UserController extends Controller {
if (!resultAddRole) {
throw new Error("Failed. Cannot set user's role.");
} 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;
}
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;
@ -387,7 +401,7 @@ export class UserController extends Controller {
});
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 =
@ -406,6 +420,7 @@ export class UserController extends Controller {
include: { province: true, district: true, subDistrict: true },
data: {
...rest,
statusOrder: +(rest.status === "INACTIVE"),
userRole,
code:
(lastUserOfType &&
@ -423,7 +438,7 @@ export class UserController extends Controller {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
updateBy: req.user.name,
updatedBy: req.user.name,
},
where: { id: userId },
});
@ -443,6 +458,7 @@ export class UserController extends Controller {
}
@Delete("{userId}")
@Security("keycloak")
async deleteUser(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
@ -454,11 +470,11 @@ export class UserController extends Controller {
});
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) {
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), {
@ -475,9 +491,7 @@ export class UserController extends Controller {
stream.on("error", () => reject(new Error("MinIO error.")));
}).then((list) => {
list.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
await minio.removeObject(MINIO_BUCKET, v, { forceDelete: true });
});
});
@ -498,7 +512,7 @@ function attachmentLocation(uid: string) {
return `user-attachment/${uid}`;
}
@Route("api/user/{userId}/attachment")
@Route("api/v1/user/{userId}/attachment")
@Tags("User")
@Security("keycloak")
export class UserAttachmentController extends Controller {
@ -514,7 +528,7 @@ export class UserAttachmentController extends Controller {
});
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) => {
@ -547,7 +561,7 @@ export class UserAttachmentController extends Controller {
});
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(

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 { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely";
import kyselyExtension from "prisma-extension-kysely";
import type { DB } from "./generated/kysely/types";
const prisma = new PrismaClient({
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;

View file

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

View file

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

View file

@ -10,8 +10,15 @@ export async function expressAuthentication(
) {
switch (securityName) {
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:
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({
status: error.status,
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,
message: "Validation error(s).",
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({
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: error.message,
devMessage: "system_error",
code: "system_error",
});
}

View file

@ -1,5 +1,6 @@
import { NextFunction, Request, Response } from "express";
import elasticsearch from "../services/elasticsearch";
import { randomUUID } from "crypto";
if (!process.env.ELASTICSEARCH_INDEX) {
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,
sessionId: req.headers["x-session-id"],
rtId: req.headers["x-rtid"],
tId: req.headers["x-tid"],
tId: randomUUID(),
method: req.method,
endpoint: req.url,
responseCode: res.statusCode,
responseDescription:
data?.devMessage !== undefined
? data.devMessage
: { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[
res.statusCode
],
responseDescription: data?.code,
input: (level === 4 && JSON.stringify(req.body, null, 2)) || undefined,
output: (level === 4 && JSON.stringify(data, null, 2)) || undefined,
...req.app.locals.logData,

View file

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

View file

@ -2,8 +2,8 @@ import { DecodedJwt, createDecoder } from "fast-jwt";
const KC_URL = process.env.KC_URL;
const KC_REALM = process.env.KC_REALM;
const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID;
const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET;
const KC_ADMIN_USERNAME = process.env.KC_ADMIN_USERNAME;
const KC_ADMIN_PASSWORD = process.env.KC_ADMIN_PASSWORD;
let token: string | 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
* @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);
if (decoded && decoded.payload.exp) {
@ -28,19 +28,20 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) {
* Get token from keycloak if needed
*/
export async function getToken() {
if (!KC_CLIENT_ID || !KC_SECRET) {
throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
if (!KC_ADMIN_PASSWORD || !KC_ADMIN_USERNAME) {
throw new Error("KC_ADMIN_USERNAME and KC_ADMIN_PASSWORD are required to used this feature.");
}
if (token && !isTokenExpired(token)) return token;
const body = new URLSearchParams();
body.append("client_id", KC_CLIENT_ID);
body.append("client_secret", KC_SECRET);
body.append("grant_type", "client_credentials");
body.append("client_id", "admin-cli");
body.append("grant_type", "password");
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",
body: body,
}).catch((e) => console.error(e));
@ -74,7 +75,7 @@ export async function createUser(username: string, password: string, opts?: Reco
},
method: "POST",
body: JSON.stringify({
enabled: true,
enabled: opts?.enabled !== undefined ? opts.enabled : true,
credentials: [{ type: "password", value: password }],
username,
...opts,
@ -109,7 +110,7 @@ export async function editUser(userId: string, opts: Record<string, any>) {
},
method: "PUT",
body: JSON.stringify({
enabled: true,
enabled: opts?.enabled !== undefined ? opts.enabled : true,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
...rest,
}),

View file

@ -9,3 +9,52 @@ const minio = new Client({
});
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 Work" },
{ "name": "Employee Other Info" },
{ "name": "Service" },
{ "name": "Work" },
{ "name": "Product Group" },
{ "name": "Product Type" },
{ "name": "Product Group" }
{ "name": "Product" },
{ "name": "Work" },
{ "name": "Service" }
]
}
},