Merge branch 'dev'
This commit is contained in:
commit
3d9f0881c3
50 changed files with 6949 additions and 2203 deletions
|
|
@ -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
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
.DS_S
|
||||
node_modules
|
||||
/src/generated
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
4664
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "birtDate" TIMESTAMP(3),
|
||||
ADD COLUMN "responsibleArea" TEXT;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Branch" ADD COLUMN "contactName" TEXT;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "checkpoint" TEXT,
|
||||
ADD COLUMN "checkpointEN" TEXT;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Branch" ADD COLUMN "telephoneHq" TEXT;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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), {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
112
src/controllers/employee-work-controller.ts
Normal file
112
src/controllers/employee-work-controller.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
100
src/controllers/product-service-controller.ts
Normal file
100
src/controllers/product-service-controller.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
199
src/controllers/product/group-controller.ts
Normal file
199
src/controllers/product/group-controller.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
273
src/controllers/product/product-controller.ts
Normal file
273
src/controllers/product/product-controller.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
213
src/controllers/product/type-controller.ts
Normal file
213
src/controllers/product/type-controller.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
346
src/controllers/service/service-controller.ts
Normal file
346
src/controllers/service/service-controller.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
311
src/controllers/work/work-controller.ts
Normal file
311
src/controllers/work/work-controller.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
18
src/db.ts
18
src/db.ts
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Request } from "express";
|
|||
|
||||
export type RequestWithUser = Request & {
|
||||
user: {
|
||||
sub: string;
|
||||
name: string;
|
||||
given_name: string;
|
||||
familiy_name: string;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue