Merge branch 'dev'
This commit is contained in:
commit
a5653962fc
44 changed files with 4403 additions and 738 deletions
28
package.json
28
package.json
|
|
@ -20,28 +20,32 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.12.2",
|
"@types/node": "^20.14.9",
|
||||||
"@types/swagger-ui-express": "^4.1.6",
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"nodemon": "^3.1.3",
|
"nodemon": "^3.1.4",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.3.2",
|
||||||
"prisma": "^5.16.0",
|
"prisma": "^5.16.1",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.5.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "^8.13.0",
|
"@elastic/elasticsearch": "^8.14.0",
|
||||||
"@prisma/client": "^5.16.0",
|
"@prisma/client": "^5.16.1",
|
||||||
"@tsoa/runtime": "^6.2.0",
|
"@tsoa/runtime": "^6.3.0",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"fast-jwt": "^4.0.0",
|
"fast-jwt": "^4.0.1",
|
||||||
"kysely": "^0.27.3",
|
"kysely": "^0.27.3",
|
||||||
"minio": "^7.1.3",
|
"minio": "^8.0.1",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
"prisma-extension-kysely": "^2.1.0",
|
"prisma-extension-kysely": "^2.1.0",
|
||||||
"promise.any": "^2.0.6",
|
"promise.any": "^2.0.6",
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tsoa": "^6.2.0"
|
"tsoa": "^6.3.1",
|
||||||
|
"winston": "^3.13.1",
|
||||||
|
"winston-elasticsearch": "^0.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1122
pnpm-lock.yaml
generated
1122
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
238
prisma/migrations/20240701062318_user_relation/migration.sql
Normal file
238
prisma/migrations/20240701062318_user_relation/migration.sql
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `createdBy` on the `Branch` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `Branch` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `BranchContact` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `BranchContact` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `BranchUser` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `BranchUser` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `Customer` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `Customer` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `CustomerBranch` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `CustomerBranch` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `Employee` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `Employee` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `EmployeeCheckup` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `EmployeeCheckup` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `EmployeeHistory` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `EmployeeOtherInfo` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `EmployeeOtherInfo` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `EmployeeWork` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `EmployeeWork` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `Product` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `Product` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `ProductGroup` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `ProductGroup` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `ProductType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `ProductType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `Service` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `Service` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `Work` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `Work` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdBy` on the `WorkProduct` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedBy` on the `WorkProduct` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Branch" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BranchContact" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BranchUser" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Customer" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CustomerBranch" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Employee" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EmployeeCheckup" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EmployeeHistory" DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EmployeeOtherInfo" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EmployeeWork" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Product" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ProductGroup" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ProductType" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Work" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "WorkProduct" DROP COLUMN "createdBy",
|
||||||
|
DROP COLUMN "updatedBy",
|
||||||
|
ADD COLUMN "createdByUserId" TEXT,
|
||||||
|
ADD COLUMN "updatedByUserId" TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BranchContact" ADD CONSTRAINT "BranchContact_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BranchContact" ADD CONSTRAINT "BranchContact_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "User" ADD CONSTRAINT "User_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "User" ADD CONSTRAINT "User_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeHistory" ADD CONSTRAINT "EmployeeHistory_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeWork" ADD CONSTRAINT "EmployeeWork_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeWork" ADD CONSTRAINT "EmployeeWork_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeOtherInfo" ADD CONSTRAINT "EmployeeOtherInfo_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EmployeeOtherInfo" ADD CONSTRAINT "EmployeeOtherInfo_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Service" ADD CONSTRAINT "Service_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Service" ADD CONSTRAINT "Service_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Work" ADD CONSTRAINT "Work_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Work" ADD CONSTRAINT "Work_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProductGroup" ADD CONSTRAINT "ProductGroup_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProductGroup" ADD CONSTRAINT "ProductGroup_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProductType" ADD CONSTRAINT "ProductType_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProductType" ADD CONSTRAINT "ProductType_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Product" ADD CONSTRAINT "Product_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Product" ADD CONSTRAINT "Product_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `createdAt` on the `EmployeeHistory` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdByUserId` on the `EmployeeHistory` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `timestamp` on the `EmployeeHistory` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "EmployeeHistory" DROP CONSTRAINT "EmployeeHistory_createdByUserId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EmployeeHistory" DROP COLUMN "createdAt",
|
||||||
|
DROP COLUMN "createdByUserId",
|
||||||
|
DROP COLUMN "timestamp";
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Customer" ADD COLUMN "registeredBranchId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Product" ADD COLUMN "registeredBranchId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "productTypeId" TEXT,
|
||||||
|
ADD COLUMN "registeredBranchId" TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Product" ADD CONSTRAINT "Product_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Service" ADD CONSTRAINT "Service_productTypeId_fkey" FOREIGN KEY ("productTypeId") REFERENCES "ProductType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Service" ADD CONSTRAINT "Service_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Employee" ALTER COLUMN "nrcNo" DROP NOT NULL;
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "PayCondition" AS ENUM ('Full', 'Split', 'BillFull', 'BillSplit');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Quotation" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"customerId" TEXT NOT NULL,
|
||||||
|
"customerBranchId" TEXT NOT NULL,
|
||||||
|
"status" "Status" NOT NULL DEFAULT 'CREATED',
|
||||||
|
"statusOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"payCondition" "PayCondition" NOT NULL,
|
||||||
|
"paySplitCount" INTEGER,
|
||||||
|
"payBillDate" TIMESTAMP(3),
|
||||||
|
"workerCount" INTEGER NOT NULL,
|
||||||
|
"urgent" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"totalPrice" DOUBLE PRECISION NOT NULL,
|
||||||
|
"totalDiscount" DOUBLE PRECISION NOT NULL,
|
||||||
|
"vat" DOUBLE PRECISION NOT NULL,
|
||||||
|
"vatExcluded" DOUBLE PRECISION NOT NULL,
|
||||||
|
"finalPrice" DOUBLE PRECISION NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdByUserId" TEXT,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"updatedByUserId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Quotation_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "QuotationPaySplit" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"no" INTEGER NOT NULL,
|
||||||
|
"date" TIMESTAMP(3) NOT NULL,
|
||||||
|
"quotationId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "QuotationPaySplit_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "QuotationWorker" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"no" INTEGER NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"employeeId" TEXT NOT NULL,
|
||||||
|
"quotationId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "QuotationWorker_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "QuotationService" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"detail" TEXT NOT NULL,
|
||||||
|
"attributes" JSONB,
|
||||||
|
"quotationId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "QuotationService_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "QuotationServiceWork" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"order" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"attributes" JSONB,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "QuotationServiceWork_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "QuotationServiceWorkProduct" (
|
||||||
|
"order" INTEGER NOT NULL,
|
||||||
|
"workId" TEXT NOT NULL,
|
||||||
|
"productId" TEXT NOT NULL,
|
||||||
|
"vat" DOUBLE PRECISION NOT NULL,
|
||||||
|
"amount" INTEGER NOT NULL,
|
||||||
|
"discount" DOUBLE PRECISION NOT NULL,
|
||||||
|
"pricePerUnit" DOUBLE PRECISION NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "QuotationServiceWorkProduct_pkey" PRIMARY KEY ("workId","productId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_customerBranchId_fkey" FOREIGN KEY ("customerBranchId") REFERENCES "CustomerBranch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationPaySplit" ADD CONSTRAINT "QuotationPaySplit_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationWorker" ADD CONSTRAINT "QuotationWorker_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationWorker" ADD CONSTRAINT "QuotationWorker_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationService" ADD CONSTRAINT "QuotationService_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWork" ADD CONSTRAINT "QuotationServiceWork_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "QuotationService"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWorkProduct" ADD CONSTRAINT "QuotationServiceWorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "QuotationServiceWork"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWorkProduct" ADD CONSTRAINT "QuotationServiceWorkProduct_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `refServiceId` to the `QuotationService` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "QuotationService" ADD COLUMN "refServiceId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationService" ADD CONSTRAINT "QuotationService_refServiceId_fkey" FOREIGN KEY ("refServiceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "QuotationService" DROP CONSTRAINT "QuotationService_quotationId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWork" DROP CONSTRAINT "QuotationServiceWork_serviceId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWorkProduct" DROP CONSTRAINT "QuotationServiceWorkProduct_workId_fkey";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationService" ADD CONSTRAINT "QuotationService_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWork" ADD CONSTRAINT "QuotationServiceWork_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "QuotationService"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuotationServiceWorkProduct" ADD CONSTRAINT "QuotationServiceWorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "QuotationServiceWork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BranchBank" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"bankName" TEXT NOT NULL,
|
||||||
|
"accountName" TEXT NOT NULL,
|
||||||
|
"accountNumber" TEXT NOT NULL,
|
||||||
|
"branchId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "BranchBank_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BranchBank" ADD CONSTRAINT "BranchBank_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `accountType` to the `BranchBank` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `bankBranch` to the `BranchBank` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `currentlyUse` to the `BranchBank` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Made the column `branchId` on table `BranchBank` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "BranchBank" DROP CONSTRAINT "BranchBank_branchId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BranchBank" ADD COLUMN "accountType" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "bankBranch" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "currentlyUse" BOOLEAN NOT NULL,
|
||||||
|
ALTER COLUMN "branchId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BranchBank" ADD CONSTRAINT "BranchBank_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[employeeId]` on the table `EmployeeOtherInfo` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "EmployeeOtherInfo_employeeId_key" ON "EmployeeOtherInfo"("employeeId");
|
||||||
4
prisma/migrations/20240815021817_add_field/migration.sql
Normal file
4
prisma/migrations/20240815021817_add_field/migration.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "middleName" TEXT,
|
||||||
|
ADD COLUMN "middleNameEN" TEXT,
|
||||||
|
ADD COLUMN "namePrefix" TEXT;
|
||||||
|
|
@ -22,7 +22,7 @@ model Menu {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
|
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
|
||||||
|
|
@ -45,7 +45,7 @@ model RoleMenuPermission {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ model UserMenuPermission {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ model MenuComponent {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
roleMenuComponentPermission RoleMenuComponentPermission[]
|
roleMenuComponentPermission RoleMenuComponentPermission[]
|
||||||
userMennuComponentPermission UserMenuComponentPermission[]
|
userMennuComponentPermission UserMenuComponentPermission[]
|
||||||
|
|
@ -94,7 +94,7 @@ model RoleMenuComponentPermission {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ model UserMenuComponentPermission {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,7 +127,7 @@ model Province {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
district District[]
|
district District[]
|
||||||
|
|
@ -148,7 +148,7 @@ model District {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
subDistrict SubDistrict[]
|
subDistrict SubDistrict[]
|
||||||
|
|
@ -169,7 +169,7 @@ model SubDistrict {
|
||||||
|
|
||||||
createdBy String?
|
createdBy String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedBy String?
|
updatedBy String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
branch Branch[]
|
branch Branch[]
|
||||||
|
|
@ -217,17 +217,38 @@ model Branch {
|
||||||
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
|
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
|
||||||
headOfficeId String?
|
headOfficeId String?
|
||||||
|
|
||||||
|
bank BranchBank[]
|
||||||
|
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "BranchCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "BranchUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
branch Branch[] @relation(name: "HeadOfficeRelation")
|
branch Branch[] @relation(name: "HeadOfficeRelation")
|
||||||
contact BranchContact[]
|
contact BranchContact[]
|
||||||
user BranchUser[]
|
user BranchUser[]
|
||||||
|
|
||||||
|
productRegistration Product[]
|
||||||
|
serviceRegistration Service[]
|
||||||
|
customerRegistration Customer[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model BranchBank {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
bankName String
|
||||||
|
bankBranch String
|
||||||
|
accountName String
|
||||||
|
accountNumber String
|
||||||
|
accountType String
|
||||||
|
currentlyUse Boolean
|
||||||
|
|
||||||
|
branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
|
||||||
|
branchId String
|
||||||
}
|
}
|
||||||
|
|
||||||
model BranchContact {
|
model BranchContact {
|
||||||
|
|
@ -237,10 +258,12 @@ model BranchContact {
|
||||||
branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
|
branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
|
||||||
branchId String
|
branchId String
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "BranchContactCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "BranchContactUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model BranchUser {
|
model BranchUser {
|
||||||
|
|
@ -252,10 +275,12 @@ model BranchUser {
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "BranchUserCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "BranchUserUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserType {
|
enum UserType {
|
||||||
|
|
@ -268,13 +293,16 @@ enum UserType {
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
code String?
|
code String?
|
||||||
firstName String
|
namePrefix String?
|
||||||
firstNameEN String
|
firstName String
|
||||||
lastName String
|
firstNameEN String
|
||||||
lastNameEN String
|
middleName String?
|
||||||
username String
|
middleNameEN String?
|
||||||
gender String
|
lastName String
|
||||||
|
lastNameEN String
|
||||||
|
username String
|
||||||
|
gender String
|
||||||
|
|
||||||
address String
|
address String
|
||||||
addressEN String
|
addressEN String
|
||||||
|
|
@ -321,15 +349,52 @@ model User {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "UserCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "UserUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
branch BranchUser[]
|
branch BranchUser[]
|
||||||
userMenuPermission UserMenuPermission[]
|
userMenuPermission UserMenuPermission[]
|
||||||
userMenuComponentPermission UserMenuComponentPermission[]
|
userMenuComponentPermission UserMenuComponentPermission[]
|
||||||
employeeHistory EmployeeHistory[]
|
|
||||||
|
userCreated User[] @relation("UserCreatedByUser")
|
||||||
|
userUpdated User[] @relation("UserUpdatedByUser")
|
||||||
|
branchCreated Branch[] @relation("BranchCreatedByUser")
|
||||||
|
branchUpdated Branch[] @relation("BranchUpdatedByUser")
|
||||||
|
branchContactCreated BranchContact[] @relation("BranchContactCreatedByUser")
|
||||||
|
branchContactUpdated BranchContact[] @relation("BranchContactUpdatedByUser")
|
||||||
|
branchUserCreated BranchUser[] @relation("BranchUserCreatedByUser")
|
||||||
|
branchUserUpdated BranchUser[] @relation("BranchUserUpdatedByUser")
|
||||||
|
customerCreated Customer[] @relation("CustomerCreatedByUser")
|
||||||
|
customerUpdated Customer[] @relation("CustomerUpdatedByUser")
|
||||||
|
customerBranchCreated CustomerBranch[] @relation("CustomerBranchCreatedByUser")
|
||||||
|
customerBranchUpdated CustomerBranch[] @relation("CustomerBranchUpdatedByUser")
|
||||||
|
emplyeeCreated Employee[] @relation("EmployeeCreatedByUser")
|
||||||
|
employeUpdated Employee[] @relation("EmployeeUpdatedByUser")
|
||||||
|
employeeHistoryUpdated EmployeeHistory[] @relation("EmployeeHistoryUpdatedByUser")
|
||||||
|
employeeCheckupCreated EmployeeCheckup[] @relation("EmployeeCheckupCreatedByUser")
|
||||||
|
employeeCheckupUpdated EmployeeCheckup[] @relation("EmployeeCheckupUpdatedByUser")
|
||||||
|
employeeWorkCreated EmployeeWork[] @relation("EmployeeWorkCreatedByUser")
|
||||||
|
employeeWorkUpdated EmployeeWork[] @relation("EmployeeWorkUpdatedByUser")
|
||||||
|
employeeOtherInfoCreated EmployeeOtherInfo[] @relation("EmployeeOtherInfoCreatedByUser")
|
||||||
|
employeeOtherInfoUpdated EmployeeOtherInfo[] @relation("EmployeeOtherInfoUpdatedByUser")
|
||||||
|
serviceCreated Service[] @relation("ServiceCreatedByUser")
|
||||||
|
serviceUpdated Service[] @relation("ServiceUpdatedByUser")
|
||||||
|
workCreated Work[] @relation("WorkCreatedByUser")
|
||||||
|
workUpdated Work[] @relation("WorkUpdatedByUser")
|
||||||
|
workProductCreated WorkProduct[] @relation("WorkProductCreatedByUser")
|
||||||
|
workProductUpdated WorkProduct[] @relation("WorkProductUpdatedByUser")
|
||||||
|
productGroupCreated ProductGroup[] @relation("ProductGroupCreatedByUser")
|
||||||
|
productGroupUpdated ProductGroup[] @relation("ProductGroupUpdatedByUser")
|
||||||
|
productTypeCreated ProductType[] @relation("ProductTypeCreatedByUser")
|
||||||
|
productTypeUpdated ProductType[] @relation("ProductTypeUpdatedByUser")
|
||||||
|
productCreated Product[] @relation("ProductCreatedByUser")
|
||||||
|
productUpdated Product[] @relation("ProductUpdatedByUser")
|
||||||
|
quotationCreated Quotation[] @relation("QuotationCreatedByUser")
|
||||||
|
quotationUpdated Quotation[] @relation("QuotationUpdatedByUser")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CustomerType {
|
enum CustomerType {
|
||||||
|
|
@ -350,12 +415,18 @@ model Customer {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
registeredBranchId String?
|
||||||
createdAt DateTime @default(now())
|
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
|
||||||
updatedBy String?
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
branch CustomerBranch[]
|
createdAt DateTime @default(now())
|
||||||
|
createdBy User? @relation(name: "CustomerCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
|
createdByUserId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "CustomerUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
|
branch CustomerBranch[]
|
||||||
|
quotation Quotation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model CustomerBranch {
|
model CustomerBranch {
|
||||||
|
|
@ -405,19 +476,22 @@ model CustomerBranch {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "CustomerBranchCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "CustomerBranchUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
employee Employee[]
|
employee Employee[]
|
||||||
|
quotation Quotation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Employee {
|
model Employee {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
code String
|
code String
|
||||||
nrcNo String
|
nrcNo String?
|
||||||
firstName String
|
firstName String
|
||||||
firstNameEN String
|
firstNameEN String
|
||||||
lastName String
|
lastName String
|
||||||
|
|
@ -465,16 +539,19 @@ model Employee {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "EmployeeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "EmployeeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
employeeCheckup EmployeeCheckup[]
|
employeeCheckup EmployeeCheckup[]
|
||||||
employeeWork EmployeeWork[]
|
employeeWork EmployeeWork[]
|
||||||
employeeOtherInfo EmployeeOtherInfo[]
|
employeeOtherInfo EmployeeOtherInfo?
|
||||||
|
|
||||||
editHistory EmployeeHistory[]
|
editHistory EmployeeHistory[]
|
||||||
|
quotationWorker QuotationWorker[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmployeeHistory {
|
model EmployeeHistory {
|
||||||
|
|
@ -483,12 +560,9 @@ model EmployeeHistory {
|
||||||
valueBefore Json
|
valueBefore Json
|
||||||
valueAfter Json
|
valueAfter Json
|
||||||
|
|
||||||
timestamp DateTime @default(now())
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "EmployeeHistoryUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedByUserId String?
|
updatedByUserId String?
|
||||||
updatedByUser User? @relation(fields: [updatedByUserId], references: [id])
|
|
||||||
updatedBy String?
|
|
||||||
updatedAt DateTime @default(now())
|
|
||||||
|
|
||||||
masterId String
|
masterId String
|
||||||
master Employee @relation(fields: [masterId], references: [id], onDelete: Cascade)
|
master Employee @relation(fields: [masterId], references: [id], onDelete: Cascade)
|
||||||
|
|
@ -513,10 +587,12 @@ model EmployeeCheckup {
|
||||||
coverageStartDate DateTime?
|
coverageStartDate DateTime?
|
||||||
coverageExpireDate DateTime?
|
coverageExpireDate DateTime?
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "EmployeeCheckupCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "EmployeeCheckupUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmployeeWork {
|
model EmployeeWork {
|
||||||
|
|
@ -535,17 +611,19 @@ model EmployeeWork {
|
||||||
workEndDate DateTime?
|
workEndDate DateTime?
|
||||||
remark String?
|
remark String?
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "EmployeeWorkCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "EmployeeWorkUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmployeeOtherInfo {
|
model EmployeeOtherInfo {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||||
employeeId String
|
employeeId String @unique
|
||||||
|
|
||||||
citizenId String?
|
citizenId String?
|
||||||
fatherBirthPlace String?
|
fatherBirthPlace String?
|
||||||
|
|
@ -560,65 +638,12 @@ model EmployeeOtherInfo {
|
||||||
motherFirstNameEN String?
|
motherFirstNameEN String?
|
||||||
motherLastNameEN String?
|
motherLastNameEN String?
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "EmployeeOtherInfoCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
updatedBy User? @relation(name: "EmployeeOtherInfoUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
model Service {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
code String
|
|
||||||
name String
|
|
||||||
detail String
|
|
||||||
attributes Json?
|
|
||||||
|
|
||||||
status Status @default(CREATED)
|
|
||||||
statusOrder Int @default(0)
|
|
||||||
|
|
||||||
work Work[]
|
|
||||||
|
|
||||||
createdBy String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedBy String?
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model Work {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
order Int
|
|
||||||
name String
|
|
||||||
attributes Json?
|
|
||||||
|
|
||||||
status Status @default(CREATED)
|
|
||||||
statusOrder Int @default(0)
|
|
||||||
|
|
||||||
service Service? @relation(fields: [serviceId], references: [id], onDelete: Cascade)
|
|
||||||
serviceId String?
|
|
||||||
|
|
||||||
createdBy String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
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 {
|
model ProductGroup {
|
||||||
|
|
@ -632,10 +657,12 @@ model ProductGroup {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "ProductGroupCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "ProductGroupUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
type ProductType[]
|
type ProductType[]
|
||||||
}
|
}
|
||||||
|
|
@ -651,15 +678,18 @@ model ProductType {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
createdBy String?
|
createdAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdBy User? @relation(name: "ProductTypeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedBy String?
|
createdByUserId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "ProductTypeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
|
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
|
||||||
productGroupId String
|
productGroupId String
|
||||||
|
|
||||||
product Product[]
|
product Product[]
|
||||||
|
service Service[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Product {
|
model Product {
|
||||||
|
|
@ -681,10 +711,199 @@ model Product {
|
||||||
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
|
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
|
||||||
productTypeId String?
|
productTypeId String?
|
||||||
|
|
||||||
workProduct WorkProduct[]
|
registeredBranchId String?
|
||||||
|
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
|
||||||
|
|
||||||
createdBy String?
|
workProduct WorkProduct[]
|
||||||
createdAt DateTime @default(now())
|
quotationServiceWorkProduct QuotationServiceWorkProduct[]
|
||||||
updatedBy String?
|
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
createdBy User? @relation(name: "ProductCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
|
createdByUserId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "ProductUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model Service {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
code String
|
||||||
|
name String
|
||||||
|
detail String
|
||||||
|
attributes Json?
|
||||||
|
|
||||||
|
status Status @default(CREATED)
|
||||||
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
|
work Work[]
|
||||||
|
quotationService QuotationService[]
|
||||||
|
|
||||||
|
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
|
||||||
|
productTypeId String?
|
||||||
|
|
||||||
|
registeredBranchId String?
|
||||||
|
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
createdBy User? @relation(name: "ServiceCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
|
createdByUserId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "ServiceUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model Work {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
order Int
|
||||||
|
name String
|
||||||
|
attributes Json?
|
||||||
|
|
||||||
|
status Status @default(CREATED)
|
||||||
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
|
service Service? @relation(fields: [serviceId], references: [id], onDelete: Cascade)
|
||||||
|
serviceId String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
createdBy User? @relation(name: "WorkCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
|
createdByUserId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "WorkUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
createdBy User? @relation(name: "WorkProductCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
|
createdByUserId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "WorkProductUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
|
||||||
|
@@id([workId, productId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PayCondition {
|
||||||
|
Full
|
||||||
|
Split
|
||||||
|
BillFull
|
||||||
|
BillSplit
|
||||||
|
}
|
||||||
|
|
||||||
|
model Quotation {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
customerId String
|
||||||
|
customer Customer @relation(fields: [customerId], references: [id])
|
||||||
|
|
||||||
|
customerBranchId String
|
||||||
|
customerBranch CustomerBranch @relation(fields: [customerBranchId], references: [id])
|
||||||
|
|
||||||
|
status Status @default(CREATED)
|
||||||
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
|
code String
|
||||||
|
date DateTime @default(now())
|
||||||
|
payCondition PayCondition
|
||||||
|
|
||||||
|
paySplitCount Int?
|
||||||
|
paySplit QuotationPaySplit[]
|
||||||
|
|
||||||
|
payBillDate DateTime?
|
||||||
|
|
||||||
|
workerCount Int
|
||||||
|
worker QuotationWorker[]
|
||||||
|
|
||||||
|
service QuotationService[]
|
||||||
|
|
||||||
|
urgent Boolean @default(false)
|
||||||
|
|
||||||
|
totalPrice Float
|
||||||
|
totalDiscount Float
|
||||||
|
vat Float
|
||||||
|
vatExcluded Float
|
||||||
|
finalPrice Float
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
createdBy User? @relation(name: "QuotationCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||||
|
createdByUserId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
updatedBy User? @relation(name: "QuotationUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUserId String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuotationPaySplit {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
no Int
|
||||||
|
date DateTime
|
||||||
|
|
||||||
|
quotation Quotation? @relation(fields: [quotationId], references: [id])
|
||||||
|
quotationId String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuotationWorker {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
no Int
|
||||||
|
code String
|
||||||
|
employee Employee @relation(fields: [employeeId], references: [id])
|
||||||
|
employeeId String
|
||||||
|
quotation Quotation @relation(fields: [quotationId], references: [id])
|
||||||
|
quotationId String
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuotationService {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
code String
|
||||||
|
name String
|
||||||
|
detail String
|
||||||
|
attributes Json?
|
||||||
|
|
||||||
|
work QuotationServiceWork[]
|
||||||
|
|
||||||
|
refServiceId String
|
||||||
|
refService Service @relation(fields: [refServiceId], references: [id])
|
||||||
|
|
||||||
|
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
|
||||||
|
quotationId String
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuotationServiceWork {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
order Int
|
||||||
|
name String
|
||||||
|
attributes Json?
|
||||||
|
|
||||||
|
service QuotationService @relation(fields: [serviceId], references: [id], onDelete: Cascade)
|
||||||
|
serviceId String
|
||||||
|
|
||||||
|
productOnWork QuotationServiceWorkProduct[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuotationServiceWorkProduct {
|
||||||
|
order Int
|
||||||
|
work QuotationServiceWork @relation(fields: [workId], references: [id], onDelete: Cascade)
|
||||||
|
workId String
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
productId String
|
||||||
|
|
||||||
|
vat Float
|
||||||
|
amount Int
|
||||||
|
discount Float
|
||||||
|
pricePerUnit Float
|
||||||
|
|
||||||
|
@@id([workId, productId])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
src/app.ts
61
src/app.ts
|
|
@ -4,8 +4,10 @@ import express, { json, urlencoded } from "express";
|
||||||
import swaggerUi from "swagger-ui-express";
|
import swaggerUi from "swagger-ui-express";
|
||||||
import swaggerDocument from "./swagger.json";
|
import swaggerDocument from "./swagger.json";
|
||||||
import error from "./middlewares/error";
|
import error from "./middlewares/error";
|
||||||
|
import morgan from "./middlewares/morgan";
|
||||||
import { RegisterRoutes } from "./routes";
|
import { RegisterRoutes } from "./routes";
|
||||||
import logMiddleware from "./middlewares/log";
|
import { addUserRoles, createUser, getRoleByName, listUser } from "./services/keycloak";
|
||||||
|
import prisma from "./db";
|
||||||
|
|
||||||
const APP_HOST = process.env.APP_HOST || "0.0.0.0";
|
const APP_HOST = process.env.APP_HOST || "0.0.0.0";
|
||||||
const APP_PORT = +(process.env.APP_PORT || 3000);
|
const APP_PORT = +(process.env.APP_PORT || 3000);
|
||||||
|
|
@ -13,11 +15,64 @@ const APP_PORT = +(process.env.APP_PORT || 3000);
|
||||||
(async () => {
|
(async () => {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
let users = await (async () => {
|
||||||
|
let list = await listUser();
|
||||||
|
while (!list) {
|
||||||
|
list = await listUser();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
const role = await getRoleByName("system");
|
||||||
|
const userId = await createUser("admin", "1234", {
|
||||||
|
firstName: "Admin",
|
||||||
|
lastName: "System",
|
||||||
|
email: "admin@jws.local",
|
||||||
|
requiredActions: ["UPDATE_PASSWORD"],
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userId || typeof userId !== "string") {
|
||||||
|
throw new Error("Error create user with keycloak service.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role) await addUserRoles(userId, [role]);
|
||||||
|
|
||||||
|
await prisma.user.create({
|
||||||
|
include: { province: true, district: true, subDistrict: true },
|
||||||
|
data: {
|
||||||
|
id: userId,
|
||||||
|
email: "admin@jws.local",
|
||||||
|
gender: "",
|
||||||
|
address: "",
|
||||||
|
addressEN: "",
|
||||||
|
zipCode: "",
|
||||||
|
userType: "USER",
|
||||||
|
userRole: "system",
|
||||||
|
telephoneNo: "",
|
||||||
|
firstName: "Admin",
|
||||||
|
firstNameEN: "Admin",
|
||||||
|
lastName: "System",
|
||||||
|
lastNameEN: "System",
|
||||||
|
statusOrder: 0,
|
||||||
|
username: "admin",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSend = app.response.json;
|
||||||
|
|
||||||
|
app.response.json = function (body: unknown) {
|
||||||
|
this.app.locals.response = body;
|
||||||
|
return originalSend.call(this, body);
|
||||||
|
};
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(json());
|
app.use(json());
|
||||||
app.use(urlencoded({ extended: true }));
|
app.use(urlencoded({ extended: true }));
|
||||||
|
app.use(morgan);
|
||||||
app.use(logMiddleware);
|
|
||||||
|
|
||||||
app.use("/", express.static("static"));
|
app.use("/", express.static("static"));
|
||||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||||
|
|
|
||||||
8
src/config.json
Normal file
8
src/config.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"branch": {
|
||||||
|
"maxHeadOfficeBranch": 10
|
||||||
|
},
|
||||||
|
"personnel": {
|
||||||
|
"type": ["USER", "MESSENGER", "DELEGATE", "AGENCY"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ export class BranchContactController extends Controller {
|
||||||
) {
|
) {
|
||||||
const [result, total] = await prisma.$transaction([
|
const [result, total] = await prisma.$transaction([
|
||||||
prisma.branchContact.findMany({
|
prisma.branchContact.findMany({
|
||||||
|
include: { createdBy: true, updatedBy: true },
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
where: { branchId },
|
where: { branchId },
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -79,7 +80,11 @@ export class BranchContactController extends Controller {
|
||||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq");
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq");
|
||||||
}
|
}
|
||||||
const record = await prisma.branchContact.create({
|
const record = await prisma.branchContact.create({
|
||||||
data: { ...body, branchId, createdBy: req.user.name, updatedBy: req.user.name },
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
data: { ...body, branchId, createdByUserId: req.user.sub, updatedByUserId: req.user.sub },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setStatus(HttpStatus.CREATED);
|
this.setStatus(HttpStatus.CREATED);
|
||||||
|
|
@ -107,7 +112,8 @@ export class BranchContactController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.branchContact.update({
|
const record = await prisma.branchContact.update({
|
||||||
data: { ...body, updatedBy: req.user.name },
|
include: { createdBy: true, updatedBy: true },
|
||||||
|
data: { ...body, updatedByUserId: req.user.sub },
|
||||||
where: { id: contactId, branchId },
|
where: { id: contactId, branchId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import prisma from "../db";
|
||||||
import HttpError from "../interfaces/http-error";
|
import HttpError from "../interfaces/http-error";
|
||||||
import HttpStatus from "../interfaces/http-status";
|
import HttpStatus from "../interfaces/http-status";
|
||||||
import { RequestWithUser } from "../interfaces/user";
|
import { RequestWithUser } from "../interfaces/user";
|
||||||
import minio from "../services/minio";
|
import minio, { presignedGetObjectIfExist } from "../services/minio";
|
||||||
|
|
||||||
if (!process.env.MINIO_BUCKET) {
|
if (!process.env.MINIO_BUCKET) {
|
||||||
throw Error("Require MinIO bucket.");
|
throw Error("Require MinIO bucket.");
|
||||||
|
|
@ -28,6 +28,7 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
||||||
|
|
||||||
type BranchCreate = {
|
type BranchCreate = {
|
||||||
status?: Status;
|
status?: Status;
|
||||||
|
code: string;
|
||||||
taxNo: string;
|
taxNo: string;
|
||||||
nameEN: string;
|
nameEN: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -42,6 +43,15 @@ type BranchCreate = {
|
||||||
longitude: string;
|
longitude: string;
|
||||||
latitude: string;
|
latitude: string;
|
||||||
|
|
||||||
|
bank?: {
|
||||||
|
bankName: string;
|
||||||
|
bankBranch: string;
|
||||||
|
accountName: string;
|
||||||
|
accountNumber: string;
|
||||||
|
accountType: string;
|
||||||
|
currentlyUse: boolean;
|
||||||
|
}[];
|
||||||
|
|
||||||
subDistrictId?: string | null;
|
subDistrictId?: string | null;
|
||||||
districtId?: string | null;
|
districtId?: string | null;
|
||||||
provinceId?: string | null;
|
provinceId?: string | null;
|
||||||
|
|
@ -68,6 +78,15 @@ type BranchUpdate = {
|
||||||
districtId?: string | null;
|
districtId?: string | null;
|
||||||
provinceId?: string | null;
|
provinceId?: string | null;
|
||||||
headOfficeId?: string | null;
|
headOfficeId?: string | null;
|
||||||
|
|
||||||
|
bank?: {
|
||||||
|
bankName: string;
|
||||||
|
bankBranch: string;
|
||||||
|
accountName: string;
|
||||||
|
accountNumber: string;
|
||||||
|
accountType: string;
|
||||||
|
currentlyUse: boolean;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function lineImageLoc(id: string) {
|
function lineImageLoc(id: string) {
|
||||||
|
|
@ -80,9 +99,9 @@ function branchImageLoc(id: string) {
|
||||||
|
|
||||||
@Route("api/v1/branch")
|
@Route("api/v1/branch")
|
||||||
@Tags("Branch")
|
@Tags("Branch")
|
||||||
@Security("keycloak")
|
|
||||||
export class BranchController extends Controller {
|
export class BranchController extends Controller {
|
||||||
@Get("stats")
|
@Get("stats")
|
||||||
|
@Security("keycloak")
|
||||||
async getStats() {
|
async getStats() {
|
||||||
const list = await prisma.branch.groupBy({
|
const list = await prisma.branch.groupBy({
|
||||||
_count: true,
|
_count: true,
|
||||||
|
|
@ -99,14 +118,27 @@ export class BranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("user-stats")
|
@Get("user-stats")
|
||||||
async getUserStat(@Query() userType?: UserType) {
|
@Security("keycloak")
|
||||||
|
async getUserStat(@Request() req: RequestWithUser, @Query() userType?: UserType) {
|
||||||
const list = await prisma.branchUser.groupBy({
|
const list = await prisma.branchUser.groupBy({
|
||||||
_count: true,
|
_count: true,
|
||||||
where: { user: { userType } },
|
where: {
|
||||||
|
userId: !["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v))
|
||||||
|
? req.user.sub
|
||||||
|
: undefined,
|
||||||
|
user: {
|
||||||
|
userType,
|
||||||
|
},
|
||||||
|
},
|
||||||
by: "branchId",
|
by: "branchId",
|
||||||
});
|
});
|
||||||
|
|
||||||
const record = await prisma.branch.findMany({
|
const record = await prisma.branch.findMany({
|
||||||
|
where: {
|
||||||
|
user: !["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v))
|
||||||
|
? { some: { userId: req.user.sub } }
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
headOfficeId: true,
|
headOfficeId: true,
|
||||||
|
|
@ -136,6 +168,7 @@ export class BranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async getBranch(
|
async getBranch(
|
||||||
@Query() zipCode?: string,
|
@Query() zipCode?: string,
|
||||||
@Query() filter?: "head" | "sub",
|
@Query() filter?: "head" | "sub",
|
||||||
|
|
@ -173,6 +206,12 @@ export class BranchController extends Controller {
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bank: true,
|
||||||
|
_count: {
|
||||||
|
select: { branch: true },
|
||||||
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -185,6 +224,7 @@ export class BranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{branchId}")
|
@Get("{branchId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getBranchById(
|
async getBranchById(
|
||||||
@Path() branchId: string,
|
@Path() branchId: string,
|
||||||
@Query() includeSubBranch?: boolean,
|
@Query() includeSubBranch?: boolean,
|
||||||
|
|
@ -195,6 +235,8 @@ export class BranchController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
branch: includeSubBranch && {
|
branch: includeSubBranch && {
|
||||||
include: {
|
include: {
|
||||||
province: true,
|
province: true,
|
||||||
|
|
@ -202,6 +244,7 @@ export class BranchController extends Controller {
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bank: true,
|
||||||
contact: includeContact,
|
contact: includeContact,
|
||||||
},
|
},
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
|
|
@ -218,6 +261,7 @@ export class BranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin"])
|
||||||
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {
|
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {
|
||||||
const [province, district, subDistrict, head] = await prisma.$transaction([
|
const [province, district, subDistrict, head] = await prisma.$transaction([
|
||||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||||
|
|
@ -250,44 +294,70 @@ export class BranchController extends Controller {
|
||||||
"relationHQNotFound",
|
"relationHQNotFound",
|
||||||
);
|
);
|
||||||
|
|
||||||
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
|
const { provinceId, districtId, subDistrictId, headOfficeId, bank, contact, code, ...rest } =
|
||||||
|
body;
|
||||||
|
|
||||||
const year = new Date().getFullYear();
|
if (headOfficeId && head && head.code.slice(0, -6) !== code) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Headoffice code not match with branch code",
|
||||||
|
"codeMismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.$transaction(
|
const record = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const last = await tx.runningNo.upsert({
|
const last = await tx.runningNo.upsert({
|
||||||
where: {
|
where: {
|
||||||
key: !headOfficeId ? `HQ${year.toString().slice(2)}` : `BR${head?.code.slice(2, 5)}`,
|
key: `MAIN_BRANCH_${code}`,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
key: !headOfficeId ? `HQ${year.toString().slice(2)}` : `BR${head?.code.slice(2, 5)}`,
|
key: `MAIN_BRANCH_${code}`,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
update: { value: { increment: 1 } },
|
update: { value: { increment: 1 } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const code = !headOfficeId
|
if (last.value === 1) {
|
||||||
? `HQ${year.toString().slice(2)}${last.value}`
|
const exist = await tx.branch.findFirst({
|
||||||
: `BR${head?.code.slice(2, 5)}${last.value.toString().padStart(2, "0")}`;
|
where: { code: `${code?.toLocaleUpperCase()}${`${last.value - 1}`.padStart(6, "0")}` },
|
||||||
|
});
|
||||||
|
if (exist)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch with same code already exists.",
|
||||||
|
"sameBranchCodeExists",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.value !== 1 && !headOfficeId) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch with same code already exists.",
|
||||||
|
"sameBranchCodeExists",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return await tx.branch.create({
|
return await tx.branch.create({
|
||||||
include: {
|
include: {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
statusOrder: +(rest.status === "INACTIVE"),
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
code,
|
code: `${code?.toLocaleUpperCase()}${`${last.value - 1}`.padStart(6, "0")}`,
|
||||||
|
bank: bank ? { createMany: { data: bank } } : undefined,
|
||||||
isHeadOffice: !headOfficeId,
|
isHeadOffice: !headOfficeId,
|
||||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||||
district: { connect: districtId ? { id: districtId } : undefined },
|
district: { connect: districtId ? { id: districtId } : undefined },
|
||||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||||
headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined },
|
headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -315,12 +385,12 @@ export class BranchController extends Controller {
|
||||||
return Object.assign(record, {
|
return Object.assign(record, {
|
||||||
contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }),
|
contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }),
|
||||||
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
||||||
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
imageUploadUrl: await minio.presignedPutObject(
|
||||||
qrCodeImageUrl: await minio.presignedGetObject(
|
|
||||||
MINIO_BUCKET,
|
MINIO_BUCKET,
|
||||||
lineImageLoc(record.id),
|
branchImageLoc(record.id),
|
||||||
12 * 60 * 60,
|
12 * 60 * 60,
|
||||||
),
|
),
|
||||||
|
qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)),
|
||||||
qrCodeImageUploadUrl: await minio.presignedPutObject(
|
qrCodeImageUploadUrl: await minio.presignedPutObject(
|
||||||
MINIO_BUCKET,
|
MINIO_BUCKET,
|
||||||
lineImageLoc(record.id),
|
lineImageLoc(record.id),
|
||||||
|
|
@ -330,6 +400,7 @@ export class BranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{branchId}")
|
@Put("{branchId}")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
async editBranch(
|
async editBranch(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: BranchUpdate,
|
@Body() body: BranchUpdate,
|
||||||
|
|
@ -375,18 +446,37 @@ export class BranchController extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
|
const { provinceId, districtId, subDistrictId, headOfficeId, bank, contact, ...rest } = body;
|
||||||
|
|
||||||
if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) {
|
const branch = await prisma.branch.findUnique({
|
||||||
|
include: {
|
||||||
|
user: { where: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
where: { id: branchId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!branch) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
!branch?.user.find((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.branch.update({
|
const record = await prisma.branch.update({
|
||||||
include: { province: true, district: true, subDistrict: true },
|
include: { province: true, district: true, subDistrict: true },
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
statusOrder: +(rest.status === "INACTIVE"),
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined,
|
isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined,
|
||||||
|
bank: bank ? { deleteMany: {}, createMany: { data: bank } } : undefined,
|
||||||
province: {
|
province: {
|
||||||
connect: provinceId ? { id: provinceId } : undefined,
|
connect: provinceId ? { id: provinceId } : undefined,
|
||||||
disconnect: provinceId === null || undefined,
|
disconnect: provinceId === null || undefined,
|
||||||
|
|
@ -403,7 +493,7 @@ export class BranchController extends Controller {
|
||||||
connect: headOfficeId ? { id: headOfficeId } : undefined,
|
connect: headOfficeId ? { id: headOfficeId } : undefined,
|
||||||
disconnect: headOfficeId === null || undefined,
|
disconnect: headOfficeId === null || undefined,
|
||||||
},
|
},
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
});
|
});
|
||||||
|
|
@ -421,12 +511,12 @@ export class BranchController extends Controller {
|
||||||
|
|
||||||
return Object.assign(record, {
|
return Object.assign(record, {
|
||||||
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
||||||
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
imageUploadUrl: await minio.presignedPutObject(
|
||||||
qrCodeImageUrl: await minio.presignedGetObject(
|
|
||||||
MINIO_BUCKET,
|
MINIO_BUCKET,
|
||||||
lineImageLoc(record.id),
|
branchImageLoc(record.id),
|
||||||
12 * 60 * 60,
|
12 * 60 * 60,
|
||||||
),
|
),
|
||||||
|
qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)),
|
||||||
qrCodeImageUploadUrl: await minio.presignedPutObject(
|
qrCodeImageUploadUrl: await minio.presignedPutObject(
|
||||||
MINIO_BUCKET,
|
MINIO_BUCKET,
|
||||||
lineImageLoc(record.id),
|
lineImageLoc(record.id),
|
||||||
|
|
@ -436,16 +526,33 @@ export class BranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{branchId}")
|
@Delete("{branchId}")
|
||||||
async deleteBranch(@Path() branchId: string) {
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_manager"])
|
||||||
|
async deleteBranch(@Request() req: RequestWithUser, @Path() branchId: string) {
|
||||||
const record = await prisma.branch.findFirst({
|
const record = await prisma.branch.findFirst({
|
||||||
include: {
|
include: {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
user: { where: { userId: req.user.sub } },
|
||||||
},
|
},
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v))) {
|
||||||
|
if (
|
||||||
|
record?.createdByUserId !== req.user.sub &&
|
||||||
|
!record?.user.find((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
||||||
}
|
}
|
||||||
|
|
@ -454,20 +561,116 @@ export class BranchController extends Controller {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "branchInUsed");
|
throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "branchInUsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), {
|
return await prisma.$transaction(async (tx) => {
|
||||||
forceDelete: true,
|
const data = await tx.branch.delete({
|
||||||
});
|
include: {
|
||||||
await minio.removeObject(MINIO_BUCKET, branchImageLoc(branchId), {
|
province: true,
|
||||||
forceDelete: true,
|
district: true,
|
||||||
});
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: branchId },
|
||||||
|
});
|
||||||
|
|
||||||
return await prisma.branch.delete({
|
if (record.isHeadOffice) {
|
||||||
|
await tx.runningNo.delete({
|
||||||
|
where: {
|
||||||
|
key: `MAIN_BRANCH_${record.code.slice(0, -6)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), {
|
||||||
|
forceDelete: true,
|
||||||
|
});
|
||||||
|
await minio.removeObject(MINIO_BUCKET, branchImageLoc(branchId), {
|
||||||
|
forceDelete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("{branchId}/line-image")
|
||||||
|
async getLineImageByBranchId(@Request() req: RequestWithUser, @Path() branchId: string) {
|
||||||
|
const url = await presignedGetObjectIfExist(MINIO_BUCKET, lineImageLoc(branchId), 60 * 60);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("{branchId}/line-image")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
|
async setLineImageByBranchId(@Request() req: RequestWithUser, @Path() branchId: string) {
|
||||||
|
const record = await prisma.branch.findUnique({
|
||||||
include: {
|
include: {
|
||||||
province: true,
|
user: { where: { userId: req.user.sub } },
|
||||||
district: true,
|
|
||||||
subDistrict: true,
|
|
||||||
},
|
},
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
!record?.user.find((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(
|
||||||
|
await minio.presignedPutObject(MINIO_BUCKET, lineImageLoc(record.id), 12 * 60 * 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("{branchId}/branch-image")
|
||||||
|
async getBranchImageByBranchId(@Request() req: RequestWithUser, @Path() branchId: string) {
|
||||||
|
const url = await presignedGetObjectIfExist(MINIO_BUCKET, branchImageLoc(branchId), 60 * 60);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("{branchId}/branch-image")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
|
async setBranchImageByBranchId(@Request() req: RequestWithUser, @Path() branchId: string) {
|
||||||
|
const record = await prisma.branch.findUnique({
|
||||||
|
include: {
|
||||||
|
user: { where: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
where: { id: branchId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
!record?.user.find((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(
|
||||||
|
await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id), 12 * 60 * 60),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Branch, Prisma, Status, User, UserType } from "@prisma/client";
|
import { Branch, Prisma, Status, User } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
|
@ -28,21 +28,28 @@ async function userBranchCodeGen(branch: Branch, user: User[]) {
|
||||||
|
|
||||||
const typ = usr.userType;
|
const typ = usr.userType;
|
||||||
|
|
||||||
|
const mapTypeNo = {
|
||||||
|
USER: 1,
|
||||||
|
MESSENGER: 2,
|
||||||
|
DELEGATE: 3,
|
||||||
|
AGENCY: 4,
|
||||||
|
}[typ];
|
||||||
|
|
||||||
const last = await tx.runningNo.upsert({
|
const last = await tx.runningNo.upsert({
|
||||||
where: {
|
where: {
|
||||||
key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`,
|
key: `BR_USR_${branch.code}_${mapTypeNo}`,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`,
|
key: `BR_USR_${branch.code}_${mapTypeNo}`,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
update: { value: { increment: 1 } },
|
update: { value: { increment: 1 } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.user.update({
|
await tx.user.update({
|
||||||
where: { id: usr.id },
|
where: { id: usr.id },
|
||||||
data: {
|
data: {
|
||||||
code: `${last.key.slice(7)}${last.value.toString().padStart(4, "0")}`,
|
code: mapTypeNo + `${last.value}`.padStart(6, "0"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -51,11 +58,28 @@ async function userBranchCodeGen(branch: Branch, user: User[]) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Route("api/v1/branch/{branchId}/admin")
|
||||||
|
@Tags("Branch User")
|
||||||
|
export class BranchAdminUserController extends Controller {
|
||||||
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
|
async getBranchAdmin(@Path() branchId: string) {
|
||||||
|
return await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
branch: {
|
||||||
|
some: { branchId },
|
||||||
|
},
|
||||||
|
userRole: "branch_admin",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Route("api/v1/branch/{branchId}/user")
|
@Route("api/v1/branch/{branchId}/user")
|
||||||
@Tags("Branch User")
|
@Tags("Branch User")
|
||||||
@Security("keycloak")
|
|
||||||
export class BranchUserController extends Controller {
|
export class BranchUserController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async getBranchUser(
|
async getBranchUser(
|
||||||
@Path() branchId: string,
|
@Path() branchId: string,
|
||||||
@Query() zipCode?: string,
|
@Query() zipCode?: string,
|
||||||
|
|
@ -97,6 +121,7 @@ export class BranchUserController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
async createBranchUser(
|
async createBranchUser(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() branchId: string,
|
@Path() branchId: string,
|
||||||
|
|
@ -104,6 +129,11 @@ export class BranchUserController extends Controller {
|
||||||
) {
|
) {
|
||||||
const [branch, user] = await prisma.$transaction([
|
const [branch, user] = await prisma.$transaction([
|
||||||
prisma.branch.findUnique({
|
prisma.branch.findUnique({
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
where: { userId: req.user.sub },
|
||||||
|
},
|
||||||
|
},
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
}),
|
}),
|
||||||
prisma.user.findMany({
|
prisma.user.findMany({
|
||||||
|
|
@ -112,14 +142,22 @@ export class BranchUserController extends Controller {
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!branch) {
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
branch?.createdByUserId !== req.user.sub &&
|
||||||
|
!branch?.user.find((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.FORBIDDEN,
|
||||||
"Branch cannot be found.",
|
"You do not have permission to perform this action.",
|
||||||
"branchBadReq",
|
"noPermission",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!branch) {
|
||||||
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq");
|
||||||
|
}
|
||||||
|
|
||||||
if (user.length !== body.user.length) {
|
if (user.length !== body.user.length) {
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
|
|
@ -139,8 +177,8 @@ export class BranchUserController extends Controller {
|
||||||
.map((v) => ({
|
.map((v) => ({
|
||||||
branchId,
|
branchId,
|
||||||
userId: v.id,
|
userId: v.id,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
@ -249,8 +287,8 @@ export class UserBranchController extends Controller {
|
||||||
.map((v) => ({
|
.map((v) => ({
|
||||||
branchId: v.id,
|
branchId: v.id,
|
||||||
userId,
|
userId,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ if (!process.env.MINIO_BUCKET) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"head_of_sale",
|
||||||
|
"sale",
|
||||||
|
];
|
||||||
|
|
||||||
function imageLocation(id: string) {
|
function imageLocation(id: string) {
|
||||||
return `employee/profile-img-${id}`;
|
return `employee/profile-img-${id}`;
|
||||||
|
|
@ -40,6 +49,7 @@ export type CustomerBranchCreate = {
|
||||||
|
|
||||||
legalPersonNo: string;
|
legalPersonNo: string;
|
||||||
|
|
||||||
|
branchNo: number;
|
||||||
taxNo: string | null;
|
taxNo: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
nameEN: string;
|
nameEN: string;
|
||||||
|
|
@ -105,9 +115,9 @@ export type CustomerBranchUpdate = {
|
||||||
|
|
||||||
@Route("api/v1/customer-branch")
|
@Route("api/v1/customer-branch")
|
||||||
@Tags("Customer Branch")
|
@Tags("Customer Branch")
|
||||||
@Security("keycloak")
|
|
||||||
export class CustomerBranchController extends Controller {
|
export class CustomerBranchController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async list(
|
async list(
|
||||||
@Query() zipCode?: string,
|
@Query() zipCode?: string,
|
||||||
@Query() customerId?: string,
|
@Query() customerId?: string,
|
||||||
|
|
@ -158,6 +168,8 @@ export class CustomerBranchController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
_count: true,
|
_count: true,
|
||||||
},
|
},
|
||||||
where,
|
where,
|
||||||
|
|
@ -171,12 +183,15 @@ export class CustomerBranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{branchId}")
|
@Get("{branchId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getById(@Path() branchId: string) {
|
async getById(@Path() branchId: string) {
|
||||||
const record = await prisma.customerBranch.findFirst({
|
const record = await prisma.customerBranch.findFirst({
|
||||||
include: {
|
include: {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
});
|
});
|
||||||
|
|
@ -189,6 +204,7 @@ export class CustomerBranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{branchId}/employee")
|
@Get("{branchId}/employee")
|
||||||
|
@Security("keycloak")
|
||||||
async listEmployee(
|
async listEmployee(
|
||||||
@Path() branchId: string,
|
@Path() branchId: string,
|
||||||
@Query() zipCode?: string,
|
@Query() zipCode?: string,
|
||||||
|
|
@ -213,6 +229,8 @@ export class CustomerBranchController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -239,6 +257,7 @@ export class CustomerBranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) {
|
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) {
|
||||||
const [province, district, subDistrict, customer] = await prisma.$transaction([
|
const [province, district, subDistrict, customer] = await prisma.$transaction([
|
||||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||||
|
|
@ -275,8 +294,26 @@ export class CustomerBranchController extends Controller {
|
||||||
|
|
||||||
const record = await prisma.$transaction(
|
const record = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const count = await tx.customerBranch.count({
|
const conflict = await tx.customerBranch.findFirst({
|
||||||
where: { customerId },
|
where: { customerId, branchNo: rest.branchNo },
|
||||||
|
});
|
||||||
|
if (conflict) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch with current no already exists.",
|
||||||
|
"branchSameNoExist",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = await tx.runningNo.upsert({
|
||||||
|
where: {
|
||||||
|
key: `CUSTOMER_${customer.code.slice(0, -6)}`,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
key: `CUSTOMER_${customer.code.slice(0, -6)}`,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
update: { value: { increment: 1 } },
|
||||||
});
|
});
|
||||||
|
|
||||||
return await tx.customerBranch.create({
|
return await tx.customerBranch.create({
|
||||||
|
|
@ -284,35 +321,32 @@ export class CustomerBranchController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
statusOrder: +(rest.status === "INACTIVE"),
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
branchNo: count + 1,
|
code: `${customer.code.slice(0, -6)}${`${last.value - 1}`.padStart(6, "0")}`,
|
||||||
code: `${customer.code}-${(count + 1).toString().padStart(2, "0")}`,
|
|
||||||
customer: { connect: { id: customerId } },
|
customer: { connect: { id: customerId } },
|
||||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||||
district: { connect: districtId ? { id: districtId } : undefined },
|
district: { connect: districtId ? { id: districtId } : undefined },
|
||||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||||
);
|
);
|
||||||
|
|
||||||
await prisma.customer.updateMany({
|
|
||||||
where: { id: customerId, status: Status.CREATED },
|
|
||||||
data: { status: Status.ACTIVE },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setStatus(HttpStatus.CREATED);
|
this.setStatus(HttpStatus.CREATED);
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{branchId}")
|
@Put("{branchId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editById(
|
async editById(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: CustomerBranchUpdate,
|
@Body() body: CustomerBranchUpdate,
|
||||||
|
|
@ -363,6 +397,8 @@ export class CustomerBranchController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
|
|
@ -380,8 +416,7 @@ export class CustomerBranchController extends Controller {
|
||||||
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
||||||
disconnect: subDistrictId === null || undefined,
|
disconnect: subDistrictId === null || undefined,
|
||||||
},
|
},
|
||||||
createdBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -391,6 +426,7 @@ export class CustomerBranchController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{branchId}")
|
@Delete("{branchId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async delete(@Path() branchId: string) {
|
async delete(@Path() branchId: string) {
|
||||||
const record = await prisma.customerBranch.findFirst({
|
const record = await prisma.customerBranch.findFirst({
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
|
|
@ -412,27 +448,30 @@ export class CustomerBranchController extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.customerBranch.delete({ where: { id: branchId } }).then((v) => {
|
return await prisma.customerBranch
|
||||||
new Promise<string[]>((resolve, reject) => {
|
.delete({
|
||||||
const item: string[] = [];
|
include: { createdBy: true, updatedBy: true },
|
||||||
|
where: { id: branchId },
|
||||||
|
})
|
||||||
|
.then((v) => {
|
||||||
|
new Promise<string[]>((resolve, reject) => {
|
||||||
|
const item: string[] = [];
|
||||||
|
|
||||||
const stream = minio.listObjectsV2(
|
const stream = minio.listObjectsV2(
|
||||||
MINIO_BUCKET,
|
MINIO_BUCKET,
|
||||||
`${attachmentLocation(record.customerId, branchId)}/`,
|
`${attachmentLocation(record.customerId, branchId)}/`,
|
||||||
);
|
);
|
||||||
|
|
||||||
stream.on("data", (v) => v && v.name && item.push(v.name));
|
stream.on("data", (v) => v && v.name && item.push(v.name));
|
||||||
stream.on("end", () => resolve(item));
|
stream.on("end", () => resolve(item));
|
||||||
stream.on("error", () => reject(new Error("MinIO error.")));
|
stream.on("error", () => reject(new Error("MinIO error.")));
|
||||||
}).then((list) => {
|
}).then((list) => {
|
||||||
list.map(async (v) => {
|
list.map(async (v) => {
|
||||||
await minio.removeObject(MINIO_BUCKET, v, {
|
await minio.removeObject(MINIO_BUCKET, v, { forceDelete: true });
|
||||||
forceDelete: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return v;
|
||||||
});
|
});
|
||||||
return v;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,21 @@ if (!process.env.MINIO_BUCKET) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"head_of_sale",
|
||||||
|
"sale",
|
||||||
|
];
|
||||||
|
|
||||||
export type CustomerCreate = {
|
export type CustomerCreate = {
|
||||||
|
registeredBranchId?: string;
|
||||||
|
|
||||||
|
code: string;
|
||||||
|
|
||||||
status?: Status;
|
status?: Status;
|
||||||
personName: string;
|
personName: string;
|
||||||
personNameEN?: string;
|
personNameEN?: string;
|
||||||
|
|
@ -38,6 +51,7 @@ export type CustomerCreate = {
|
||||||
|
|
||||||
legalPersonNo: string;
|
legalPersonNo: string;
|
||||||
|
|
||||||
|
branchNo: number;
|
||||||
taxNo: string | null;
|
taxNo: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
nameEN: string;
|
nameEN: string;
|
||||||
|
|
@ -68,6 +82,8 @@ export type CustomerCreate = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomerUpdate = {
|
export type CustomerUpdate = {
|
||||||
|
registeredBranchId?: string;
|
||||||
|
|
||||||
status?: "ACTIVE" | "INACTIVE";
|
status?: "ACTIVE" | "INACTIVE";
|
||||||
personName?: string;
|
personName?: string;
|
||||||
personNameEN?: string;
|
personNameEN?: string;
|
||||||
|
|
@ -82,6 +98,7 @@ export type CustomerUpdate = {
|
||||||
|
|
||||||
legalPersonNo: string;
|
legalPersonNo: string;
|
||||||
|
|
||||||
|
branchNo: number;
|
||||||
taxNo: string | null;
|
taxNo: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
nameEN: string;
|
nameEN: string;
|
||||||
|
|
@ -117,9 +134,9 @@ function imageLocation(id: string) {
|
||||||
|
|
||||||
@Route("api/v1/customer")
|
@Route("api/v1/customer")
|
||||||
@Tags("Customer")
|
@Tags("Customer")
|
||||||
@Security("keycloak")
|
|
||||||
export class CustomerController extends Controller {
|
export class CustomerController extends Controller {
|
||||||
@Get("type-stats")
|
@Get("type-stats")
|
||||||
|
@Security("keycloak")
|
||||||
async stat() {
|
async stat() {
|
||||||
const list = await prisma.customer.groupBy({
|
const list = await prisma.customer.groupBy({
|
||||||
by: "customerType",
|
by: "customerType",
|
||||||
|
|
@ -139,6 +156,7 @@ export class CustomerController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async list(
|
async list(
|
||||||
@Query() customerType?: CustomerType,
|
@Query() customerType?: CustomerType,
|
||||||
@Query() query: string = "",
|
@Query() query: string = "",
|
||||||
|
|
@ -172,8 +190,13 @@ export class CustomerController extends Controller {
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
branchNo: "asc",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||||
where,
|
where,
|
||||||
|
|
@ -201,6 +224,7 @@ export class CustomerController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{customerId}")
|
@Get("{customerId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getById(@Path() customerId: string) {
|
async getById(@Path() customerId: string) {
|
||||||
const record = await prisma.customer.findFirst({
|
const record = await prisma.customer.findFirst({
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -210,7 +234,10 @@ export class CustomerController extends Controller {
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
},
|
},
|
||||||
|
orderBy: { branchNo: "asc" },
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: customerId },
|
where: { id: customerId },
|
||||||
});
|
});
|
||||||
|
|
@ -226,6 +253,7 @@ export class CustomerController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
|
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
|
||||||
const { customerBranch, ...payload } = body;
|
const { customerBranch, ...payload } = body;
|
||||||
|
|
||||||
|
|
@ -243,10 +271,11 @@ export class CustomerController extends Controller {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [province, district, subDistrict] = await prisma.$transaction([
|
const [province, district, subDistrict, branch] = await prisma.$transaction([
|
||||||
prisma.province.findMany({ where: { id: { in: provinceId } } }),
|
prisma.province.findMany({ where: { id: { in: provinceId } } }),
|
||||||
prisma.district.findMany({ where: { id: { in: districtId } } }),
|
prisma.district.findMany({ where: { id: { in: districtId } } }),
|
||||||
prisma.subDistrict.findMany({ where: { id: { in: subDistrictId } } }),
|
prisma.subDistrict.findMany({ where: { id: { in: subDistrictId } } }),
|
||||||
|
prisma.branch.findFirst({ where: { id: body.registeredBranchId } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (provinceId && province.length !== provinceId?.length) {
|
if (provinceId && province.length !== provinceId?.length) {
|
||||||
|
|
@ -270,20 +299,44 @@ export class CustomerController extends Controller {
|
||||||
"relationSubDistrictNotFound",
|
"relationSubDistrictNotFound",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!!body.registeredBranchId && !branch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch cannot be found.",
|
||||||
|
"relationBranchNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!body.registeredBranchId) {
|
||||||
|
body.registeredBranchId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.$transaction(
|
const record = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const last = await tx.runningNo.upsert({
|
const last = await tx.runningNo.upsert({
|
||||||
where: {
|
where: {
|
||||||
key: `CUSTOMER_${body.customerType}`,
|
key: `CUSTOMER_${body.code.toLocaleUpperCase()}`,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
key: `CUSTOMER_${body.customerType}`,
|
key: `CUSTOMER_${body.code.toLocaleUpperCase()}`,
|
||||||
value: 1,
|
value: body.customerBranch?.length || 0,
|
||||||
},
|
},
|
||||||
update: { value: { increment: 1 } },
|
update: { value: { increment: body.customerBranch?.length || 0 } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
body.code = body.code.toLocaleUpperCase();
|
||||||
|
|
||||||
|
const exist = await tx.customer.findFirst({
|
||||||
|
where: { code: `${body.code}000000` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer with same code already exists.",
|
||||||
|
"sameCustomerCodeExists",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return await prisma.customer.create({
|
return await prisma.customer.create({
|
||||||
include: {
|
include: {
|
||||||
branch: {
|
branch: {
|
||||||
|
|
@ -293,25 +346,26 @@ export class CustomerController extends Controller {
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...payload,
|
...payload,
|
||||||
statusOrder: +(payload.status === "INACTIVE"),
|
statusOrder: +(payload.status === "INACTIVE"),
|
||||||
code: `${last.key.slice(9)}${last.value.toString().padStart(6, "0")}`,
|
code: `${body.code}000000`,
|
||||||
branch: {
|
branch: {
|
||||||
createMany: {
|
createMany: {
|
||||||
data:
|
data:
|
||||||
customerBranch?.map((v, i) => ({
|
customerBranch?.map((v, i) => ({
|
||||||
...v,
|
...v,
|
||||||
branchNo: i + 1,
|
code: `${body.code}${`${last.value - customerBranch?.length + i + 1}`.padStart(6, "0")}`,
|
||||||
code: `${last.key.slice(9)}${last.value.toString().padStart(6, "0")}-${(i + 1).toString().padStart(2, "0")}`,
|
createdByUserId: req.user.sub,
|
||||||
createdBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
|
||||||
})) || [],
|
})) || [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -335,6 +389,7 @@ export class CustomerController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{customerId}")
|
@Put("{customerId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editById(
|
async editById(
|
||||||
@Path() customerId: string,
|
@Path() customerId: string,
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
|
|
@ -407,6 +462,17 @@ export class CustomerController extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
customerBranch &&
|
||||||
|
relation.find((a) => customerBranch.find((b) => a.id !== b.id && a.branchNo === b.branchNo))
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch cannot have same number.",
|
||||||
|
"oneOrMoreBranchNoExist",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.customer
|
const record = await prisma.customer
|
||||||
.update({
|
.update({
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -417,6 +483,8 @@ export class CustomerController extends Controller {
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: customerId },
|
where: { id: customerId },
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -430,26 +498,25 @@ export class CustomerController extends Controller {
|
||||||
},
|
},
|
||||||
status: Status.CREATED,
|
status: Status.CREATED,
|
||||||
},
|
},
|
||||||
upsert: customerBranch.map((v, i) => ({
|
upsert: customerBranch.map((v) => ({
|
||||||
where: { id: v.id || "" },
|
where: { id: v.id || "" },
|
||||||
create: {
|
create: {
|
||||||
...v,
|
...v,
|
||||||
branchNo: i + 1,
|
code: `${customer.code}-${v.branchNo.toString().padStart(2, "0")}`,
|
||||||
code: `${customer.code}-${(i + 1).toString().padStart(2, "0")}`,
|
createdByUserId: req.user.sub,
|
||||||
createdBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
|
||||||
id: undefined,
|
id: undefined,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
...v,
|
...v,
|
||||||
branchNo: i + 1,
|
code: undefined,
|
||||||
code: `${customer.code}-${(i + 1).toString().padStart(2, "0")}`,
|
branchNo: undefined,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}) ||
|
}) ||
|
||||||
undefined,
|
undefined,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((v) => {
|
.then((v) => {
|
||||||
|
|
@ -492,6 +559,7 @@ export class CustomerController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{customerId}")
|
@Delete("{customerId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async deleteById(@Path() customerId: string) {
|
async deleteById(@Path() customerId: string) {
|
||||||
const record = await prisma.customer.findFirst({ where: { id: customerId } });
|
const record = await prisma.customer.findFirst({ where: { id: customerId } });
|
||||||
|
|
||||||
|
|
@ -522,4 +590,33 @@ export class CustomerController extends Controller {
|
||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("{customerId}/image")
|
||||||
|
async getCustomerImageById(@Request() req: RequestWithUser, @Path() customerId: string) {
|
||||||
|
const url = await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(customerId), 60 * 60);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("{customerId}/image")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
|
async setCustomerImageById(@Request() req: RequestWithUser, @Path() customerId: string) {
|
||||||
|
const record = await prisma.customer.findFirst({
|
||||||
|
where: {
|
||||||
|
id: customerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(
|
||||||
|
await minio.presignedPutObject(MINIO_BUCKET, imageLocation(customerId), 12 * 60 * 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,16 @@ import prisma from "../db";
|
||||||
import HttpStatus from "../interfaces/http-status";
|
import HttpStatus from "../interfaces/http-status";
|
||||||
import HttpError from "../interfaces/http-error";
|
import HttpError from "../interfaces/http-error";
|
||||||
|
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"head_of_sale",
|
||||||
|
"sale",
|
||||||
|
];
|
||||||
|
|
||||||
type EmployeeCheckupPayload = {
|
type EmployeeCheckupPayload = {
|
||||||
checkupType?: string | null;
|
checkupType?: string | null;
|
||||||
checkupResult?: string | null;
|
checkupResult?: string | null;
|
||||||
|
|
@ -32,19 +42,28 @@ type EmployeeCheckupPayload = {
|
||||||
|
|
||||||
@Route("api/v1/employee/{employeeId}/checkup")
|
@Route("api/v1/employee/{employeeId}/checkup")
|
||||||
@Tags("Employee Checkup")
|
@Tags("Employee Checkup")
|
||||||
@Security("keycloak")
|
|
||||||
export class EmployeeCheckupController extends Controller {
|
export class EmployeeCheckupController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async list(@Path() employeeId: string) {
|
async list(@Path() employeeId: string) {
|
||||||
return prisma.employeeCheckup.findMany({
|
return prisma.employeeCheckup.findMany({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
where: { employeeId },
|
where: { employeeId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{checkupId}")
|
@Get("{checkupId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getById(@Path() employeeId: string, @Path() checkupId: string) {
|
async getById(@Path() employeeId: string, @Path() checkupId: string) {
|
||||||
const record = await prisma.employeeCheckup.findFirst({
|
const record = await prisma.employeeCheckup.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { id: checkupId, employeeId },
|
where: { id: checkupId, employeeId },
|
||||||
});
|
});
|
||||||
if (!record) {
|
if (!record) {
|
||||||
|
|
@ -58,6 +77,7 @@ export class EmployeeCheckupController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async create(
|
async create(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() employeeId: string,
|
@Path() employeeId: string,
|
||||||
|
|
@ -85,13 +105,13 @@ export class EmployeeCheckupController extends Controller {
|
||||||
const { provinceId, ...rest } = body;
|
const { provinceId, ...rest } = body;
|
||||||
|
|
||||||
const record = await prisma.employeeCheckup.create({
|
const record = await prisma.employeeCheckup.create({
|
||||||
include: { province: true },
|
include: { province: true, createdBy: true, updatedBy: true },
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||||
employee: { connect: { id: employeeId } },
|
employee: { connect: { id: employeeId } },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,6 +121,7 @@ export class EmployeeCheckupController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{checkupId}")
|
@Put("{checkupId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editById(
|
async editById(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() employeeId: string,
|
@Path() employeeId: string,
|
||||||
|
|
@ -128,7 +149,12 @@ export class EmployeeCheckupController extends Controller {
|
||||||
|
|
||||||
const { provinceId, ...rest } = body;
|
const { provinceId, ...rest } = body;
|
||||||
|
|
||||||
if (!(await prisma.employeeCheckup.findUnique({ where: { id: checkupId, employeeId } }))) {
|
if (
|
||||||
|
!(await prisma.employeeCheckup.findUnique({
|
||||||
|
include: { createdBy: true, updatedBy: true },
|
||||||
|
where: { id: checkupId, employeeId },
|
||||||
|
}))
|
||||||
|
) {
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
HttpStatus.NOT_FOUND,
|
HttpStatus.NOT_FOUND,
|
||||||
"Employee checkup cannot be found.",
|
"Employee checkup cannot be found.",
|
||||||
|
|
@ -137,13 +163,12 @@ export class EmployeeCheckupController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.employeeCheckup.update({
|
const record = await prisma.employeeCheckup.update({
|
||||||
include: { province: true },
|
include: { province: true, createdBy: true, updatedBy: true },
|
||||||
where: { id: checkupId, employeeId },
|
where: { id: checkupId, employeeId },
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||||
createdBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -153,6 +178,7 @@ export class EmployeeCheckupController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{checkupId}")
|
@Delete("{checkupId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async deleteById(@Path() employeeId: string, @Path() checkupId: string) {
|
async deleteById(@Path() employeeId: string, @Path() checkupId: string) {
|
||||||
const record = await prisma.employeeCheckup.findFirst({ where: { id: checkupId, employeeId } });
|
const record = await prisma.employeeCheckup.findFirst({ where: { id: checkupId, employeeId } });
|
||||||
|
|
||||||
|
|
@ -164,6 +190,9 @@ export class EmployeeCheckupController extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.employeeCheckup.delete({ where: { id: checkupId, employeeId } });
|
return await prisma.employeeCheckup.delete({
|
||||||
|
include: { createdBy: true, updatedBy: true },
|
||||||
|
where: { id: checkupId, employeeId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ if (!process.env.MINIO_BUCKET) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"head_of_sale",
|
||||||
|
"sale",
|
||||||
|
];
|
||||||
|
|
||||||
function imageLocation(id: string) {
|
function imageLocation(id: string) {
|
||||||
return `employee/${id}/profile-image`;
|
return `employee/${id}/profile-image`;
|
||||||
|
|
@ -34,7 +43,7 @@ type EmployeeCreate = {
|
||||||
|
|
||||||
status?: Status;
|
status?: Status;
|
||||||
|
|
||||||
nrcNo: string;
|
nrcNo?: string;
|
||||||
|
|
||||||
dateOfBirth: Date;
|
dateOfBirth: Date;
|
||||||
gender: string;
|
gender: string;
|
||||||
|
|
@ -200,9 +209,9 @@ type EmployeeUpdate = {
|
||||||
|
|
||||||
@Route("api/v1/employee")
|
@Route("api/v1/employee")
|
||||||
@Tags("Employee")
|
@Tags("Employee")
|
||||||
@Security("keycloak")
|
|
||||||
export class EmployeeController extends Controller {
|
export class EmployeeController extends Controller {
|
||||||
@Get("stats")
|
@Get("stats")
|
||||||
|
@Security("keycloak")
|
||||||
async getEmployeeStats(@Query() customerBranchId?: string) {
|
async getEmployeeStats(@Query() customerBranchId?: string) {
|
||||||
return await prisma.employee.count({
|
return await prisma.employee.count({
|
||||||
where: { customerBranchId },
|
where: { customerBranchId },
|
||||||
|
|
@ -210,12 +219,32 @@ export class EmployeeController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("stats/gender")
|
@Get("stats/gender")
|
||||||
async getEmployeeStatsGender(@Query() customerBranchId?: string) {
|
@Security("keycloak")
|
||||||
|
async getEmployeeStatsGender(
|
||||||
|
@Query() customerBranchId?: string,
|
||||||
|
@Query() status?: Status,
|
||||||
|
@Query() query: string = "",
|
||||||
|
) {
|
||||||
|
const filterStatus = (val?: Status) => {
|
||||||
|
if (!val) return {};
|
||||||
|
|
||||||
|
return val !== Status.CREATED && val !== Status.ACTIVE
|
||||||
|
? { status: val }
|
||||||
|
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
|
||||||
|
};
|
||||||
|
|
||||||
return await prisma.employee
|
return await prisma.employee
|
||||||
.groupBy({
|
.groupBy({
|
||||||
_count: true,
|
_count: true,
|
||||||
by: ["gender"],
|
by: ["gender"],
|
||||||
where: { customerBranchId },
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ firstName: { contains: query }, customerBranchId, ...filterStatus(status) },
|
||||||
|
{ firstNameEN: { contains: query }, customerBranchId, ...filterStatus(status) },
|
||||||
|
{ lastName: { contains: query }, customerBranchId, ...filterStatus(status) },
|
||||||
|
{ lastNameEN: { contains: query }, customerBranchId, ...filterStatus(status) },
|
||||||
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then((res) =>
|
.then((res) =>
|
||||||
res.reduce<Record<string, number>>((a, c) => {
|
res.reduce<Record<string, number>>((a, c) => {
|
||||||
|
|
@ -226,6 +255,7 @@ export class EmployeeController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async list(
|
async list(
|
||||||
@Query() zipCode?: string,
|
@Query() zipCode?: string,
|
||||||
@Query() gender?: string,
|
@Query() gender?: string,
|
||||||
|
|
@ -258,6 +288,11 @@ export class EmployeeController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
customerBranch: {
|
||||||
|
include: { customer: true },
|
||||||
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -284,12 +319,18 @@ export class EmployeeController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{employeeId}")
|
@Get("{employeeId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getById(@Path() employeeId: string) {
|
async getById(@Path() employeeId: string) {
|
||||||
const record = await prisma.employee.findFirst({
|
const record = await prisma.employee.findFirst({
|
||||||
include: {
|
include: {
|
||||||
|
employeeWork: true,
|
||||||
|
employeeCheckup: true,
|
||||||
|
employeeOtherInfo: true,
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: employeeId },
|
where: { id: employeeId },
|
||||||
});
|
});
|
||||||
|
|
@ -298,10 +339,17 @@ export class EmployeeController extends Controller {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "employeeNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "employeeNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
return record;
|
return Object.assign(record, {
|
||||||
|
profileImageUrl: await presignedGetObjectIfExist(
|
||||||
|
MINIO_BUCKET,
|
||||||
|
imageLocation(employeeId),
|
||||||
|
12 * 60 * 60,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) {
|
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) {
|
||||||
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
|
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
|
||||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||||
|
|
@ -371,10 +419,10 @@ export class EmployeeController extends Controller {
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const last = await tx.runningNo.upsert({
|
const last = await tx.runningNo.upsert({
|
||||||
where: {
|
where: {
|
||||||
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
|
key: `EMPLOYEE_${customerBranch.customer.code.slice(0, -6)}${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
|
key: `EMPLOYEE_${customerBranch.customer.code.slice(0, -6)}${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
update: { value: { increment: 1 } },
|
update: { value: { increment: 1 } },
|
||||||
|
|
@ -392,11 +440,13 @@ export class EmployeeController extends Controller {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
employeeWork: true,
|
employeeWork: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
statusOrder: +(rest.status === "INACTIVE"),
|
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")}`,
|
code: `${customerBranch.customer.code.slice(0, -6)}${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${last.value}`.padStart(7, "0")}`,
|
||||||
employeeWork: {
|
employeeWork: {
|
||||||
createMany: {
|
createMany: {
|
||||||
data: employeeWork || [],
|
data: employeeWork || [],
|
||||||
|
|
@ -418,8 +468,8 @@ export class EmployeeController extends Controller {
|
||||||
district: { connect: districtId ? { id: districtId } : undefined },
|
district: { connect: districtId ? { id: districtId } : undefined },
|
||||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||||
customerBranch: { connect: { id: customerBranchId } },
|
customerBranch: { connect: { id: customerBranchId } },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -458,6 +508,7 @@ export class EmployeeController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{employeeId}")
|
@Put("{employeeId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editById(
|
async editById(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: EmployeeUpdate,
|
@Body() body: EmployeeUpdate,
|
||||||
|
|
@ -534,7 +585,11 @@ export class EmployeeController extends Controller {
|
||||||
const record = await prisma.$transaction(async (tx) => {
|
const record = await prisma.$transaction(async (tx) => {
|
||||||
let code: string | undefined;
|
let code: string | undefined;
|
||||||
|
|
||||||
if (customerBranch && customerBranch.id !== employee.customerBranchId) {
|
if (
|
||||||
|
customerBranchId !== undefined &&
|
||||||
|
customerBranch &&
|
||||||
|
customerBranch.id !== employee.customerBranchId
|
||||||
|
) {
|
||||||
const last = await tx.runningNo.upsert({
|
const last = await tx.runningNo.upsert({
|
||||||
where: {
|
where: {
|
||||||
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
|
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
|
||||||
|
|
@ -561,6 +616,8 @@ export class EmployeeController extends Controller {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
employeeWork: true,
|
employeeWork: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
|
|
@ -578,13 +635,13 @@ export class EmployeeController extends Controller {
|
||||||
where: { id: v.id || "" },
|
where: { id: v.id || "" },
|
||||||
create: {
|
create: {
|
||||||
...v,
|
...v,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
...v,
|
...v,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
@ -602,21 +659,20 @@ export class EmployeeController extends Controller {
|
||||||
create: {
|
create: {
|
||||||
...v,
|
...v,
|
||||||
provinceId: !v.provinceId ? undefined : v.provinceId,
|
provinceId: !v.provinceId ? undefined : v.provinceId,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
...v,
|
...v,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
employeeOtherInfo: employeeOtherInfo
|
employeeOtherInfo: employeeOtherInfo
|
||||||
? {
|
? {
|
||||||
deleteMany: {},
|
update: employeeOtherInfo,
|
||||||
create: employeeOtherInfo,
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
province: {
|
province: {
|
||||||
|
|
@ -631,8 +687,8 @@ export class EmployeeController extends Controller {
|
||||||
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
||||||
disconnect: subDistrictId === null || undefined,
|
disconnect: subDistrictId === null || undefined,
|
||||||
},
|
},
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -663,7 +719,6 @@ export class EmployeeController extends Controller {
|
||||||
data: historyEntries.map((v) => ({
|
data: historyEntries.map((v) => ({
|
||||||
...v,
|
...v,
|
||||||
updatedByUserId: req.user.sub,
|
updatedByUserId: req.user.sub,
|
||||||
updatedBy: req.user.preferred_username,
|
|
||||||
masterId: employee.id,
|
masterId: employee.id,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
@ -683,6 +738,7 @@ export class EmployeeController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{employeeId}")
|
@Delete("{employeeId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async delete(@Path() employeeId: string) {
|
async delete(@Path() employeeId: string) {
|
||||||
const record = await prisma.employee.findFirst({ where: { id: employeeId } });
|
const record = await prisma.employee.findFirst({ where: { id: employeeId } });
|
||||||
|
|
||||||
|
|
@ -694,12 +750,21 @@ export class EmployeeController extends Controller {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "Employee is in used.", "employeeInUsed");
|
throw new HttpError(HttpStatus.FORBIDDEN, "Employee is in used.", "employeeInUsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.employee.delete({ where: { id: employeeId } });
|
return await prisma.employee.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: employeeId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{employeeId}/edit-history")
|
@Get("{employeeId}/edit-history")
|
||||||
async editHistory(@Path() employeeId: string) {
|
async editHistory(@Path() employeeId: string) {
|
||||||
return await prisma.employeeHistory.findMany({
|
return await prisma.employeeHistory.findMany({
|
||||||
|
include: {
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { masterId: employeeId },
|
where: { masterId: employeeId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@ import HttpError from "../interfaces/http-error";
|
||||||
import HttpStatus from "../interfaces/http-status";
|
import HttpStatus from "../interfaces/http-status";
|
||||||
import { RequestWithUser } from "../interfaces/user";
|
import { RequestWithUser } from "../interfaces/user";
|
||||||
|
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"head_of_sale",
|
||||||
|
"sale",
|
||||||
|
];
|
||||||
|
|
||||||
type EmployeeOtherInfoPayload = {
|
type EmployeeOtherInfoPayload = {
|
||||||
citizenId?: string | null;
|
citizenId?: string | null;
|
||||||
fatherFirstName?: string | null;
|
fatherFirstName?: string | null;
|
||||||
|
|
@ -34,35 +44,40 @@ type EmployeeOtherInfoPayload = {
|
||||||
|
|
||||||
@Route("api/v1/employee/{employeeId}/other-info")
|
@Route("api/v1/employee/{employeeId}/other-info")
|
||||||
@Tags("Employee Other Info")
|
@Tags("Employee Other Info")
|
||||||
@Security("keycloak")
|
|
||||||
export class EmployeeOtherInfo extends Controller {
|
export class EmployeeOtherInfo extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async list(@Path() employeeId: string) {
|
async list(@Path() employeeId: string) {
|
||||||
return prisma.employeeOtherInfo.findFirst({
|
return prisma.employeeOtherInfo.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
where: { employeeId },
|
where: { employeeId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async create(
|
async create(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() employeeId: string,
|
@Path() employeeId: string,
|
||||||
@Body() body: EmployeeOtherInfoPayload,
|
@Body() body: EmployeeOtherInfoPayload,
|
||||||
) {
|
) {
|
||||||
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
|
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
|
||||||
throw new HttpError(
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Employee cannot be found.", "employeeBadReq");
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
"Employee cannot be found.",
|
|
||||||
"employeeBadReq",
|
|
||||||
);
|
|
||||||
|
|
||||||
const record = await prisma.employeeOtherInfo.create({
|
const record = await prisma.employeeOtherInfo.create({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
employee: { connect: { id: employeeId } },
|
employee: { connect: { id: employeeId } },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,6 +87,7 @@ export class EmployeeOtherInfo extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{otherInfoId}")
|
@Put("{otherInfoId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editById(
|
async editById(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() employeeId: string,
|
@Path() employeeId: string,
|
||||||
|
|
@ -87,8 +103,12 @@ export class EmployeeOtherInfo extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.employeeOtherInfo.update({
|
const record = await prisma.employeeOtherInfo.update({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { id: otherInfoId, employeeId },
|
where: { id: otherInfoId, employeeId },
|
||||||
data: { ...body, createdBy: req.user.name, updatedBy: req.user.name },
|
data: { ...body, updatedByUserId: req.user.sub },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setStatus(HttpStatus.CREATED);
|
this.setStatus(HttpStatus.CREATED);
|
||||||
|
|
@ -97,6 +117,7 @@ export class EmployeeOtherInfo extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{otherInfoId}")
|
@Delete("{otherInfoId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async deleteById(@Path() employeeId: string, @Path() otherInfoId: string) {
|
async deleteById(@Path() employeeId: string, @Path() otherInfoId: string) {
|
||||||
const record = await prisma.employeeOtherInfo.findFirst({
|
const record = await prisma.employeeOtherInfo.findFirst({
|
||||||
where: { id: otherInfoId, employeeId },
|
where: { id: otherInfoId, employeeId },
|
||||||
|
|
@ -110,6 +131,12 @@ export class EmployeeOtherInfo extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.employeeOtherInfo.delete({ where: { id: otherInfoId, employeeId } });
|
return await prisma.employeeOtherInfo.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: otherInfoId, employeeId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,16 @@ import prisma from "../db";
|
||||||
import HttpStatus from "../interfaces/http-status";
|
import HttpStatus from "../interfaces/http-status";
|
||||||
import HttpError from "../interfaces/http-error";
|
import HttpError from "../interfaces/http-error";
|
||||||
|
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"head_of_sale",
|
||||||
|
"sale",
|
||||||
|
];
|
||||||
|
|
||||||
type EmployeeWorkPayload = {
|
type EmployeeWorkPayload = {
|
||||||
ownerName?: string | null;
|
ownerName?: string | null;
|
||||||
positionName?: string | null;
|
positionName?: string | null;
|
||||||
|
|
@ -30,46 +40,60 @@ type EmployeeWorkPayload = {
|
||||||
|
|
||||||
@Route("api/v1/employee/{employeeId}/work")
|
@Route("api/v1/employee/{employeeId}/work")
|
||||||
@Tags("Employee Work")
|
@Tags("Employee Work")
|
||||||
@Security("keycloak")
|
|
||||||
export class EmployeeWorkController extends Controller {
|
export class EmployeeWorkController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async list(@Path() employeeId: string) {
|
async list(@Path() employeeId: string) {
|
||||||
return prisma.employeeWork.findMany({
|
return prisma.employeeWork.findMany({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
where: { employeeId },
|
where: { employeeId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{workId}")
|
@Get("{workId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getById(@Path() employeeId: string, @Path() workId: string) {
|
async getById(@Path() employeeId: string, @Path() workId: string) {
|
||||||
const record = await prisma.employeeWork.findFirst({
|
const record = await prisma.employeeWork.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { id: workId, employeeId },
|
where: { id: workId, employeeId },
|
||||||
});
|
});
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Employee work cannot be found.", "employeeWorkNotFound");
|
throw new HttpError(
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
"Employee work cannot be found.",
|
||||||
|
"employeeWorkNotFound",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async create(
|
async create(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() employeeId: string,
|
@Path() employeeId: string,
|
||||||
@Body() body: EmployeeWorkPayload,
|
@Body() body: EmployeeWorkPayload,
|
||||||
) {
|
) {
|
||||||
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
|
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
|
||||||
throw new HttpError(
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Employee cannot be found.", "employeeBadReq");
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
"Employee cannot be found.",
|
|
||||||
"employeeBadReq",
|
|
||||||
);
|
|
||||||
|
|
||||||
const record = await prisma.employeeWork.create({
|
const record = await prisma.employeeWork.create({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
employee: { connect: { id: employeeId } },
|
employee: { connect: { id: employeeId } },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -79,6 +103,7 @@ export class EmployeeWorkController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{workId}")
|
@Put("{workId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editById(
|
async editById(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Path() employeeId: string,
|
@Path() employeeId: string,
|
||||||
|
|
@ -86,12 +111,20 @@ export class EmployeeWorkController extends Controller {
|
||||||
@Body() body: EmployeeWorkPayload,
|
@Body() body: EmployeeWorkPayload,
|
||||||
) {
|
) {
|
||||||
if (!(await prisma.employeeWork.findUnique({ where: { id: workId, employeeId } }))) {
|
if (!(await prisma.employeeWork.findUnique({ where: { id: workId, employeeId } }))) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Employee work cannot be found.", "employeeWorkNotFound");
|
throw new HttpError(
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
"Employee work cannot be found.",
|
||||||
|
"employeeWorkNotFound",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.employeeWork.update({
|
const record = await prisma.employeeWork.update({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { id: workId, employeeId },
|
where: { id: workId, employeeId },
|
||||||
data: { ...body, createdBy: req.user.name, updatedBy: req.user.name },
|
data: { ...body, updatedByUserId: req.user.sub },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setStatus(HttpStatus.CREATED);
|
this.setStatus(HttpStatus.CREATED);
|
||||||
|
|
@ -100,11 +133,22 @@ export class EmployeeWorkController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{workId}")
|
@Delete("{workId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async deleteById(@Path() employeeId: string, @Path() workId: string) {
|
async deleteById(@Path() employeeId: string, @Path() workId: string) {
|
||||||
const record = await prisma.employeeWork.findFirst({ where: { id: workId, employeeId } });
|
const record = await prisma.employeeWork.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: workId, employeeId },
|
||||||
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Employee work cannot be found.", "employeeWorkNotFound");
|
throw new HttpError(
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
"Employee work cannot be found.",
|
||||||
|
"employeeWorkNotFound",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.employeeWork.delete({ where: { id: workId, employeeId } });
|
return await prisma.employeeWork.delete({ where: { id: workId, employeeId } });
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
createUser,
|
createUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
editUser,
|
editUser,
|
||||||
getRoles,
|
listRole,
|
||||||
removeUserRoles,
|
removeUserRoles,
|
||||||
} from "../services/keycloak";
|
} from "../services/keycloak";
|
||||||
|
|
||||||
|
|
@ -38,18 +38,20 @@ export class KeycloakController extends Controller {
|
||||||
|
|
||||||
@Get("role")
|
@Get("role")
|
||||||
async getRole() {
|
async getRole() {
|
||||||
const role = await getRoles();
|
const role = await listRole();
|
||||||
if (Array.isArray(role))
|
if (Array.isArray(role))
|
||||||
return role.filter(
|
return role.filter(
|
||||||
(a) =>
|
(a) =>
|
||||||
!["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)),
|
!["uma_authorization", "offline_access", "default-roles", "system"].some((b) =>
|
||||||
|
a.name.includes(b),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
throw new Error("Failed. Cannot get role.");
|
throw new Error("Failed. Cannot get role.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("{userId}/role")
|
@Post("{userId}/role")
|
||||||
async addRole(@Path() userId: string, @Body() body: { role: string[] }) {
|
async addRole(@Path() userId: string, @Body() body: { role: string[] }) {
|
||||||
const list = await getRoles();
|
const list = await listRole();
|
||||||
|
|
||||||
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
||||||
|
|
||||||
|
|
@ -63,7 +65,7 @@ export class KeycloakController extends Controller {
|
||||||
|
|
||||||
@Delete("{userId}/role/{roleId}")
|
@Delete("{userId}/role/{roleId}")
|
||||||
async deleteRole(@Path() userId: string, @Path() roleId: string) {
|
async deleteRole(@Path() userId: string, @Path() roleId: string) {
|
||||||
const list = await getRoles();
|
const list = await listRole();
|
||||||
|
|
||||||
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export class ProductServiceController extends Controller {
|
||||||
@Query() productTypeId?: string,
|
@Query() productTypeId?: string,
|
||||||
@Query() page: number = 1,
|
@Query() page: number = 1,
|
||||||
@Query() pageSize: number = 30,
|
@Query() pageSize: number = 30,
|
||||||
|
@Query() registeredBranchId?: string,
|
||||||
) {
|
) {
|
||||||
const union = Prisma.sql`
|
const union = Prisma.sql`
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -27,9 +28,10 @@ export class ProductServiceController extends Controller {
|
||||||
"status",
|
"status",
|
||||||
"statusOrder",
|
"statusOrder",
|
||||||
"productTypeId",
|
"productTypeId",
|
||||||
"createdBy",
|
"registeredBranchId",
|
||||||
|
"createdByUserId",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedBy",
|
"updatedByUserId",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
'product' as "type"
|
'product' as "type"
|
||||||
FROM "Product"
|
FROM "Product"
|
||||||
|
|
@ -46,10 +48,11 @@ export class ProductServiceController extends Controller {
|
||||||
null as "remark",
|
null as "remark",
|
||||||
"status",
|
"status",
|
||||||
"statusOrder",
|
"statusOrder",
|
||||||
null as "productTypeId",
|
"productTypeId",
|
||||||
"createdBy",
|
"registeredBranchId",
|
||||||
|
"createdByUserId",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedBy",
|
"updatedByUserId",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
'service' as "type"
|
'service' as "type"
|
||||||
FROM "Service"
|
FROM "Service"
|
||||||
|
|
@ -61,7 +64,12 @@ export class ProductServiceController extends Controller {
|
||||||
if (query) or.push(Prisma.sql`"name" LIKE ${`%${query}%`}`);
|
if (query) or.push(Prisma.sql`"name" LIKE ${`%${query}%`}`);
|
||||||
if (status) and.push(Prisma.sql`"status" = ${status}::"Status"`);
|
if (status) and.push(Prisma.sql`"status" = ${status}::"Status"`);
|
||||||
if (productTypeId) {
|
if (productTypeId) {
|
||||||
and.push(Prisma.sql`("productTypeId" = ${productTypeId} OR ("type" = 'service'))`);
|
and.push(Prisma.sql`"productTypeId" = ${productTypeId}`);
|
||||||
|
}
|
||||||
|
if (registeredBranchId) {
|
||||||
|
and.push(
|
||||||
|
Prisma.sql`("registeredBranchId" = ${registeredBranchId} OR "registeredBranchId" IS NULL)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = Prisma.sql`
|
const where = Prisma.sql`
|
||||||
|
|
@ -70,6 +78,7 @@ export class ProductServiceController extends Controller {
|
||||||
${or.length > 0 && and.length > 0 ? Prisma.sql` AND ` : Prisma.empty}
|
${or.length > 0 && and.length > 0 ? Prisma.sql` AND ` : Prisma.empty}
|
||||||
${and.length > 0 ? Prisma.join(and, " AND ", "(", ")") : Prisma.empty}
|
${and.length > 0 ? Prisma.join(and, " AND ", "(", ")") : Prisma.empty}
|
||||||
`;
|
`;
|
||||||
|
console.log(where.sql);
|
||||||
|
|
||||||
const [result, [{ total }]] = await prisma.$transaction([
|
const [result, [{ total }]] = await prisma.$transaction([
|
||||||
prisma.$queryRaw<((Product & { type: "product" }) | (Service & { type: "service" }))[]>`
|
prisma.$queryRaw<((Product & { type: "product" }) | (Service & { type: "service" }))[]>`
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,15 @@ type ProductGroupUpdate = {
|
||||||
|
|
||||||
@Route("api/v1/product-group")
|
@Route("api/v1/product-group")
|
||||||
@Tags("Product Group")
|
@Tags("Product Group")
|
||||||
@Security("keycloak")
|
|
||||||
export class ProductGroup extends Controller {
|
export class ProductGroup extends Controller {
|
||||||
@Get("stats")
|
@Get("stats")
|
||||||
|
@Security("keycloak")
|
||||||
async getProductGroupStats() {
|
async getProductGroupStats() {
|
||||||
return await prisma.productGroup.count();
|
return await prisma.productGroup.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async getProductGroup(
|
async getProductGroup(
|
||||||
@Query() query: string = "",
|
@Query() query: string = "",
|
||||||
@Query() status?: Status,
|
@Query() status?: Status,
|
||||||
|
|
@ -72,6 +73,8 @@ export class ProductGroup extends Controller {
|
||||||
type: true,
|
type: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||||
where,
|
where,
|
||||||
|
|
@ -81,9 +84,9 @@ export class ProductGroup extends Controller {
|
||||||
prisma.productGroup.count({ where }),
|
prisma.productGroup.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const statsProduct = await prisma.productType.findMany({
|
const statsDeep = await prisma.productType.findMany({
|
||||||
include: {
|
include: {
|
||||||
_count: { select: { product: true } },
|
_count: { select: { product: true, service: true } },
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
productGroupId: { in: result.map((v) => v.id) },
|
productGroupId: { in: result.map((v) => v.id) },
|
||||||
|
|
@ -95,10 +98,14 @@ export class ProductGroup extends Controller {
|
||||||
...v,
|
...v,
|
||||||
_count: {
|
_count: {
|
||||||
...v._count,
|
...v._count,
|
||||||
product: statsProduct.reduce(
|
product: statsDeep.reduce(
|
||||||
(a, c) => (c.productGroupId === v.id ? a + c._count.product : a),
|
(a, c) => (c.productGroupId === v.id ? a + c._count.product : a),
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
|
service: statsDeep.reduce(
|
||||||
|
(a, c) => (c.productGroupId === v.id ? a + c._count.service : a),
|
||||||
|
0,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
page,
|
page,
|
||||||
|
|
@ -108,6 +115,7 @@ export class ProductGroup extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{groupId}")
|
@Get("{groupId}")
|
||||||
|
@Security("keycloak")
|
||||||
async getProductGroupById(@Path() groupId: string) {
|
async getProductGroupById(@Path() groupId: string) {
|
||||||
const record = await prisma.productGroup.findFirst({
|
const record = await prisma.productGroup.findFirst({
|
||||||
where: { id: groupId },
|
where: { id: groupId },
|
||||||
|
|
@ -124,6 +132,7 @@ export class ProductGroup extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
|
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
|
||||||
const record = await prisma.$transaction(
|
const record = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
|
|
@ -139,12 +148,16 @@ export class ProductGroup extends Controller {
|
||||||
});
|
});
|
||||||
|
|
||||||
return await tx.productGroup.create({
|
return await tx.productGroup.create({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
statusOrder: +(body.status === "INACTIVE"),
|
statusOrder: +(body.status === "INACTIVE"),
|
||||||
code: `G${last.value.toString().padStart(2, "0")}`,
|
code: `G${last.value.toString().padStart(2, "0")}`,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -157,6 +170,7 @@ export class ProductGroup extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{groupId}")
|
@Put("{groupId}")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async editProductGroup(
|
async editProductGroup(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: ProductGroupUpdate,
|
@Body() body: ProductGroupUpdate,
|
||||||
|
|
@ -171,7 +185,11 @@ export class ProductGroup extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.productGroup.update({
|
const record = await prisma.productGroup.update({
|
||||||
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedBy: req.user.name },
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedByUserId: req.user.sub },
|
||||||
where: { id: groupId },
|
where: { id: groupId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -179,6 +197,7 @@ export class ProductGroup extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{groupId}")
|
@Delete("{groupId}")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async deleteProductGroup(@Path() groupId: string) {
|
async deleteProductGroup(@Path() groupId: string) {
|
||||||
const record = await prisma.productGroup.findFirst({ where: { id: groupId } });
|
const record = await prisma.productGroup.findFirst({ where: { id: groupId } });
|
||||||
|
|
||||||
|
|
@ -194,6 +213,12 @@ export class ProductGroup extends Controller {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "Product group is in used.", "productGroupInUsed");
|
throw new HttpError(HttpStatus.FORBIDDEN, "Product group is in used.", "productGroupInUsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.productGroup.delete({ where: { id: groupId } });
|
return await prisma.productGroup.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: groupId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,33 @@ if (!process.env.MINIO_BUCKET) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"accountant",
|
||||||
|
"branch_accountant",
|
||||||
|
];
|
||||||
|
|
||||||
type ProductCreate = {
|
type ProductCreate = {
|
||||||
status?: Status;
|
status?: Status;
|
||||||
code: "AC" | "DO" | "ac" | "do";
|
code:
|
||||||
|
| "DOE"
|
||||||
|
| "IMM"
|
||||||
|
| "TM"
|
||||||
|
| "HP"
|
||||||
|
| "MOUC"
|
||||||
|
| "MOUL"
|
||||||
|
| "AC"
|
||||||
|
| "doe"
|
||||||
|
| "imm"
|
||||||
|
| "tm"
|
||||||
|
| "hp"
|
||||||
|
| "mouc"
|
||||||
|
| "moul"
|
||||||
|
| "ac";
|
||||||
name: string;
|
name: string;
|
||||||
detail: string;
|
detail: string;
|
||||||
process: number;
|
process: number;
|
||||||
|
|
@ -37,6 +60,8 @@ type ProductCreate = {
|
||||||
serviceCharge: number;
|
serviceCharge: number;
|
||||||
productTypeId: string;
|
productTypeId: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
|
||||||
|
registeredBranchId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProductUpdate = {
|
type ProductUpdate = {
|
||||||
|
|
@ -49,12 +74,20 @@ type ProductUpdate = {
|
||||||
serviceCharge?: number;
|
serviceCharge?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
productTypeId?: string;
|
productTypeId?: string;
|
||||||
|
|
||||||
|
registeredBranchId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function imageLocation(id: string) {
|
function imageLocation(id: string) {
|
||||||
return `product/${id}/image`;
|
return `product/${id}/image`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function globalAllow(roles?: string[]) {
|
||||||
|
return ["system", "head_of_admin", "admin", "branch_admin", "branch_manager", "accountant"].some(
|
||||||
|
(v) => roles?.includes(v),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Route("api/v1/product")
|
@Route("api/v1/product")
|
||||||
@Tags("Product")
|
@Tags("Product")
|
||||||
export class ProductController extends Controller {
|
export class ProductController extends Controller {
|
||||||
|
|
@ -71,6 +104,7 @@ export class ProductController extends Controller {
|
||||||
@Query() query: string = "",
|
@Query() query: string = "",
|
||||||
@Query() page: number = 1,
|
@Query() page: number = 1,
|
||||||
@Query() pageSize: number = 30,
|
@Query() pageSize: number = 30,
|
||||||
|
@Query() registeredBranchId?: string,
|
||||||
) {
|
) {
|
||||||
const filterStatus = (val?: Status) => {
|
const filterStatus = (val?: Status) => {
|
||||||
if (!val) return {};
|
if (!val) return {};
|
||||||
|
|
@ -85,10 +119,19 @@ export class ProductController extends Controller {
|
||||||
{ name: { contains: query }, productTypeId, ...filterStatus(status) },
|
{ name: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||||
{ detail: { contains: query }, productTypeId, ...filterStatus(status) },
|
{ detail: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||||
],
|
],
|
||||||
|
AND: registeredBranchId
|
||||||
|
? {
|
||||||
|
OR: [{ registeredBranchId: registeredBranchId }, { registeredBranchId: null }],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
} satisfies Prisma.ProductWhereInput;
|
} satisfies Prisma.ProductWhereInput;
|
||||||
|
|
||||||
const [result, total] = await prisma.$transaction([
|
const [result, total] = await prisma.$transaction([
|
||||||
prisma.product.findMany({
|
prisma.product.findMany({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -118,6 +161,10 @@ export class ProductController extends Controller {
|
||||||
@Security("keycloak")
|
@Security("keycloak")
|
||||||
async getProductById(@Path() productId: string) {
|
async getProductById(@Path() productId: string) {
|
||||||
const record = await prisma.product.findFirst({
|
const record = await prisma.product.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { id: productId },
|
where: { id: productId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -142,11 +189,29 @@ export class ProductController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Security("keycloak")
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) {
|
async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) {
|
||||||
const productType = await prisma.productType.findFirst({
|
const [productType, branch] = await prisma.$transaction([
|
||||||
where: { id: body.productTypeId },
|
prisma.productType.findFirst({
|
||||||
});
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: body.productTypeId },
|
||||||
|
}),
|
||||||
|
prisma.branch.findFirst({
|
||||||
|
include: { user: { where: { userId: req.user.sub } } },
|
||||||
|
where: { id: body.registeredBranchId },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!globalAllow(req.user.roles) && !branch?.user.find((v) => v.userId === req.user.sub)) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!productType) {
|
if (!productType) {
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
|
|
@ -156,6 +221,14 @@ export class ProductController extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.registeredBranchId && !branch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch cannot be found.",
|
||||||
|
"relationBranchNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.$transaction(
|
const record = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const last = await tx.runningNo.upsert({
|
const last = await tx.runningNo.upsert({
|
||||||
|
|
@ -169,12 +242,16 @@ export class ProductController extends Controller {
|
||||||
update: { value: { increment: 1 } },
|
update: { value: { increment: 1 } },
|
||||||
});
|
});
|
||||||
return await prisma.product.create({
|
return await prisma.product.create({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
statusOrder: +(body.status === "INACTIVE"),
|
statusOrder: +(body.status === "INACTIVE"),
|
||||||
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -185,6 +262,10 @@ export class ProductController extends Controller {
|
||||||
|
|
||||||
if (productType.status === "CREATED") {
|
if (productType.status === "CREATED") {
|
||||||
await prisma.productType.update({
|
await prisma.productType.update({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where: { id: body.productTypeId },
|
where: { id: body.productTypeId },
|
||||||
data: { status: Status.ACTIVE },
|
data: { status: Status.ACTIVE },
|
||||||
});
|
});
|
||||||
|
|
@ -207,19 +288,44 @@ export class ProductController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{productId}")
|
@Put("{productId}")
|
||||||
@Security("keycloak")
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editProduct(
|
async editProduct(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: ProductUpdate,
|
@Body() body: ProductUpdate,
|
||||||
@Path() productId: string,
|
@Path() productId: string,
|
||||||
) {
|
) {
|
||||||
if (!(await prisma.product.findUnique({ where: { id: productId } }))) {
|
const [product, productType, branch] = await prisma.$transaction([
|
||||||
|
prisma.product.findUnique({
|
||||||
|
include: {
|
||||||
|
registeredBranch: {
|
||||||
|
where: {
|
||||||
|
user: { some: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { id: productId },
|
||||||
|
}),
|
||||||
|
prisma.productType.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: body.productTypeId },
|
||||||
|
}),
|
||||||
|
prisma.branch.findFirst({ where: { id: body.registeredBranchId } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
const productType = await prisma.productType.findFirst({
|
if (!globalAllow(req.user.roles) && !product.registeredBranch) {
|
||||||
where: { id: body.productTypeId },
|
throw new HttpError(
|
||||||
});
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!productType) {
|
if (!productType) {
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
|
|
@ -229,8 +335,20 @@ export class ProductController extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.registeredBranchId && !branch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch cannot be found.",
|
||||||
|
"relationBranchNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.product.update({
|
const record = await prisma.product.update({
|
||||||
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedBy: req.user.name },
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedByUserId: req.user.sub },
|
||||||
where: { id: productId },
|
where: { id: productId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -256,18 +374,42 @@ export class ProductController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{productId}")
|
@Delete("{productId}")
|
||||||
@Security("keycloak")
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async deleteProduct(@Path() productId: string) {
|
async deleteProduct(@Request() req: RequestWithUser, @Path() productId: string) {
|
||||||
const record = await prisma.product.findFirst({ where: { id: productId } });
|
const record = await prisma.product.findFirst({
|
||||||
|
include: {
|
||||||
|
registeredBranch: {
|
||||||
|
where: {
|
||||||
|
user: { some: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
where: { id: productId },
|
||||||
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globalAllow(req.user.roles) && !record.registeredBranch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (record.status !== Status.CREATED) {
|
if (record.status !== Status.CREATED) {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "Product is in used.", "productInUsed");
|
throw new HttpError(HttpStatus.FORBIDDEN, "Product is in used.", "productInUsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.product.delete({ where: { id: productId } });
|
return await prisma.product.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: productId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ type ProductTypeUpdate = {
|
||||||
|
|
||||||
@Route("api/v1/product-type")
|
@Route("api/v1/product-type")
|
||||||
@Tags("Product Type")
|
@Tags("Product Type")
|
||||||
@Security("keycloak")
|
|
||||||
export class ProductType extends Controller {
|
export class ProductType extends Controller {
|
||||||
@Get("stats")
|
@Get("stats")
|
||||||
async getProductTypeStats() {
|
async getProductTypeStats() {
|
||||||
|
|
@ -45,6 +44,7 @@ export class ProductType extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
async getProductType(
|
async getProductType(
|
||||||
@Query() query: string = "",
|
@Query() query: string = "",
|
||||||
@Query() productGroupId?: string,
|
@Query() productGroupId?: string,
|
||||||
|
|
@ -70,8 +70,13 @@ export class ProductType extends Controller {
|
||||||
prisma.productType.findMany({
|
prisma.productType.findMany({
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { product: true },
|
select: {
|
||||||
|
product: true,
|
||||||
|
service: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||||
where,
|
where,
|
||||||
|
|
@ -85,6 +90,7 @@ export class ProductType extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("{typeId}")
|
@Get("{typeId}")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async getProductTypeById(@Path() typeId: string) {
|
async getProductTypeById(@Path() typeId: string) {
|
||||||
const record = await prisma.productType.findFirst({
|
const record = await prisma.productType.findFirst({
|
||||||
where: { id: typeId },
|
where: { id: typeId },
|
||||||
|
|
@ -101,6 +107,7 @@ export class ProductType extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async createProductType(@Request() req: RequestWithUser, @Body() body: ProductTypeCreate) {
|
async createProductType(@Request() req: RequestWithUser, @Body() body: ProductTypeCreate) {
|
||||||
const productGroup = await prisma.productGroup.findFirst({
|
const productGroup = await prisma.productGroup.findFirst({
|
||||||
where: { id: body.productGroupId },
|
where: { id: body.productGroupId },
|
||||||
|
|
@ -128,12 +135,16 @@ export class ProductType extends Controller {
|
||||||
});
|
});
|
||||||
|
|
||||||
return await tx.productType.create({
|
return await tx.productType.create({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
statusOrder: +(body.status === "INACTIVE"),
|
statusOrder: +(body.status === "INACTIVE"),
|
||||||
code: `T${productGroup.code}${last.value.toString().padStart(2, "0")}`,
|
code: `T${productGroup.code}${last.value.toString().padStart(2, "0")}`,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -153,6 +164,7 @@ export class ProductType extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{typeId}")
|
@Put("{typeId}")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async editProductType(
|
async editProductType(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: ProductTypeUpdate,
|
@Body() body: ProductTypeUpdate,
|
||||||
|
|
@ -178,12 +190,20 @@ export class ProductType extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.productType.update({
|
const record = await prisma.productType.update({
|
||||||
data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedBy: req.user.name },
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...body,
|
||||||
|
statusOrder: +(body.status === "INACTIVE"),
|
||||||
|
updatedByUserId: req.user.sub,
|
||||||
|
},
|
||||||
where: { id: typeId },
|
where: { id: typeId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (productGroup?.status === "CREATED") {
|
if (body.productGroupId && productGroup?.status === "CREATED") {
|
||||||
await prisma.productGroup.update({
|
await prisma.productGroup.updateMany({
|
||||||
where: { id: body.productGroupId, status: Status.CREATED },
|
where: { id: body.productGroupId, status: Status.CREATED },
|
||||||
data: { status: Status.ACTIVE },
|
data: { status: Status.ACTIVE },
|
||||||
});
|
});
|
||||||
|
|
@ -193,6 +213,7 @@ export class ProductType extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{typeId}")
|
@Delete("{typeId}")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||||
async deleteProductType(@Path() typeId: string) {
|
async deleteProductType(@Path() typeId: string) {
|
||||||
const record = await prisma.productType.findFirst({ where: { id: typeId } });
|
const record = await prisma.productType.findFirst({ where: { id: typeId } });
|
||||||
|
|
||||||
|
|
@ -208,6 +229,12 @@ export class ProductType extends Controller {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "Product type is in used.", "productTypeInUsed");
|
throw new HttpError(HttpStatus.FORBIDDEN, "Product type is in used.", "productTypeInUsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.productType.delete({ where: { id: typeId } });
|
return await prisma.productType.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: typeId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
832
src/controllers/quotation-controller.ts
Normal file
832
src/controllers/quotation-controller.ts
Normal file
|
|
@ -0,0 +1,832 @@
|
||||||
|
import { PayCondition, Prisma, Status } from "@prisma/client";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Path,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Query,
|
||||||
|
Request,
|
||||||
|
Route,
|
||||||
|
Security,
|
||||||
|
Tags,
|
||||||
|
} from "tsoa";
|
||||||
|
import { RequestWithUser } from "../interfaces/user";
|
||||||
|
import prisma from "../db";
|
||||||
|
import HttpError from "../interfaces/http-error";
|
||||||
|
import HttpStatus from "../interfaces/http-status";
|
||||||
|
|
||||||
|
type QuotationCreate = {
|
||||||
|
status?: Status;
|
||||||
|
|
||||||
|
payCondition: PayCondition;
|
||||||
|
|
||||||
|
paySplitCount?: number;
|
||||||
|
paySplit?: Date[];
|
||||||
|
|
||||||
|
payBillDate?: Date;
|
||||||
|
|
||||||
|
workerCount: number;
|
||||||
|
// EmployeeId or Create new employee
|
||||||
|
worker: (
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
dateOfBirth: Date;
|
||||||
|
gender: string;
|
||||||
|
nationality: string;
|
||||||
|
|
||||||
|
firstName: string;
|
||||||
|
firstNameEN: string;
|
||||||
|
lastName: string;
|
||||||
|
lastNameEN: string;
|
||||||
|
|
||||||
|
addressEN: string;
|
||||||
|
address: string;
|
||||||
|
zipCode: string;
|
||||||
|
|
||||||
|
passportType: string;
|
||||||
|
passportNumber: string;
|
||||||
|
passportIssueDate: Date;
|
||||||
|
passportExpiryDate: Date;
|
||||||
|
passportIssuingCountry: string;
|
||||||
|
passportIssuingPlace: string;
|
||||||
|
previousPassportReference?: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
|
||||||
|
customerBranchId: string;
|
||||||
|
customerId: string;
|
||||||
|
|
||||||
|
urgent?: boolean;
|
||||||
|
|
||||||
|
service: {
|
||||||
|
id: string;
|
||||||
|
// Other fields will come from original data
|
||||||
|
work: {
|
||||||
|
id: string;
|
||||||
|
// Name field will come from original data
|
||||||
|
excluded?: boolean;
|
||||||
|
product: {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
/**
|
||||||
|
* @maximum 1
|
||||||
|
* @minimum 0
|
||||||
|
*/
|
||||||
|
discount: number;
|
||||||
|
/**
|
||||||
|
* @maximum 1
|
||||||
|
* @minimum 0
|
||||||
|
*/
|
||||||
|
vat?: number;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type QuotationUpdate = {
|
||||||
|
status?: "ACTIVE" | "INACTIVE";
|
||||||
|
|
||||||
|
payCondition?: PayCondition;
|
||||||
|
|
||||||
|
paySplitCount?: number;
|
||||||
|
paySplit?: Date[];
|
||||||
|
|
||||||
|
payBillDate?: Date;
|
||||||
|
|
||||||
|
workerCount?: number;
|
||||||
|
// EmployeeId or Create new employee
|
||||||
|
worker?: (
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
dateOfBirth: Date;
|
||||||
|
gender: string;
|
||||||
|
nationality: string;
|
||||||
|
|
||||||
|
firstName: string;
|
||||||
|
firstNameEN: string;
|
||||||
|
lastName: string;
|
||||||
|
lastNameEN: string;
|
||||||
|
|
||||||
|
addressEN: string;
|
||||||
|
address: string;
|
||||||
|
zipCode: string;
|
||||||
|
|
||||||
|
passportType: string;
|
||||||
|
passportNumber: string;
|
||||||
|
passportIssueDate: Date;
|
||||||
|
passportExpiryDate: Date;
|
||||||
|
passportIssuingCountry: string;
|
||||||
|
passportIssuingPlace: string;
|
||||||
|
previousPassportReference?: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
|
||||||
|
customerBranchId?: string;
|
||||||
|
customerId?: string;
|
||||||
|
|
||||||
|
urgent?: boolean;
|
||||||
|
|
||||||
|
service?: {
|
||||||
|
id: string;
|
||||||
|
// Other fields will come from original data
|
||||||
|
work: {
|
||||||
|
id: string;
|
||||||
|
excluded?: boolean;
|
||||||
|
// Name field will come from original data
|
||||||
|
product: {
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* @isInt
|
||||||
|
*/
|
||||||
|
amount: number;
|
||||||
|
/**
|
||||||
|
* @maximum 1
|
||||||
|
* @minimum 0
|
||||||
|
*/
|
||||||
|
discount: number;
|
||||||
|
/**
|
||||||
|
* @maximum 1
|
||||||
|
* @minimum 0
|
||||||
|
*/
|
||||||
|
vat: number;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"accountant",
|
||||||
|
"branch_accountant",
|
||||||
|
];
|
||||||
|
|
||||||
|
function globalAllow(roles?: string[]) {
|
||||||
|
return ["system", "head_of_admin", "admin", "branch_admin", "branch_manager", "accountant"].some(
|
||||||
|
(v) => roles?.includes(v),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route("/api/v1/quotation")
|
||||||
|
@Tags("Quotation")
|
||||||
|
export class QuotationController extends Controller {
|
||||||
|
@Get()
|
||||||
|
@Security("keycloak")
|
||||||
|
async getQuotationList(@Query() page: number = 1, @Query() pageSize: number = 30) {
|
||||||
|
const [result, total] = await prisma.$transaction([
|
||||||
|
prisma.quotation.findMany({
|
||||||
|
include: {
|
||||||
|
worker: true,
|
||||||
|
service: {
|
||||||
|
include: {
|
||||||
|
_count: { select: { work: true } },
|
||||||
|
work: {
|
||||||
|
include: {
|
||||||
|
_count: { select: { productOnWork: true } },
|
||||||
|
productOnWork: {
|
||||||
|
include: { product: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.quotation.count(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { result: result, page, pageSize, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("{quotationId}")
|
||||||
|
@Security("keycloak")
|
||||||
|
async getQuotationById(@Path() quotationId: string) {
|
||||||
|
const record = await prisma.quotation.findUnique({
|
||||||
|
include: {
|
||||||
|
worker: true,
|
||||||
|
service: {
|
||||||
|
include: {
|
||||||
|
_count: { select: { work: true } },
|
||||||
|
work: {
|
||||||
|
include: {
|
||||||
|
_count: { select: { productOnWork: true } },
|
||||||
|
productOnWork: {
|
||||||
|
include: { product: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { id: quotationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
|
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
|
||||||
|
const existingEmployee = body.worker.filter((v) => typeof v === "string");
|
||||||
|
const serviceIdList = body.service.map((v) => v.id);
|
||||||
|
const productIdList = body.service.flatMap((a) =>
|
||||||
|
a.work.flatMap((b) => b.product.map((c) => c.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [customer, customerBranch, employee, service, product] = await prisma.$transaction([
|
||||||
|
prisma.customer.findUnique({
|
||||||
|
where: { id: body.customerId },
|
||||||
|
}),
|
||||||
|
prisma.customerBranch.findUnique({
|
||||||
|
include: { customer: true },
|
||||||
|
where: { id: body.customerBranchId },
|
||||||
|
}),
|
||||||
|
prisma.employee.findMany({
|
||||||
|
where: { id: { in: existingEmployee } },
|
||||||
|
}),
|
||||||
|
prisma.service.findMany({
|
||||||
|
include: { work: true },
|
||||||
|
where: { id: { in: serviceIdList } },
|
||||||
|
}),
|
||||||
|
prisma.product.findMany({
|
||||||
|
where: { id: { in: productIdList } },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (serviceIdList.length !== service.length) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Some service cannot be found.",
|
||||||
|
"relationServiceNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (productIdList.length !== product.length) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Some product cannot be found.",
|
||||||
|
"relationProductNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (existingEmployee.length !== employee.length) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Some worker(employee) cannot be found.",
|
||||||
|
"relationWorkerNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!customer)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer cannot be found.",
|
||||||
|
"relationCustomerNotFound",
|
||||||
|
);
|
||||||
|
if (!customerBranch)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer Branch cannot be found.",
|
||||||
|
"relationCustomerBranchNotFound",
|
||||||
|
);
|
||||||
|
if (customerBranch.customerId !== customer.id)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer conflict with customer branch.",
|
||||||
|
"customerConflictCustomerBranch",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { service: _service, worker: _worker, ...rest } = body;
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const nonExistEmployee = body.worker.filter((v) => typeof v !== "string");
|
||||||
|
const lastEmployee = 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: nonExistEmployee.length } },
|
||||||
|
});
|
||||||
|
const newEmployee = await Promise.all(
|
||||||
|
nonExistEmployee.map(async (v, i) =>
|
||||||
|
tx.employee.create({
|
||||||
|
data: {
|
||||||
|
...v,
|
||||||
|
code: `${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}${(lastEmployee.value + i).toString().padStart(4, "0")}`,
|
||||||
|
customerBranchId: customerBranch.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const sortedEmployeeId: string[] = [];
|
||||||
|
|
||||||
|
while (body.worker.length > 0) {
|
||||||
|
const popExist = body.worker.shift();
|
||||||
|
if (typeof popExist === "string") sortedEmployeeId.push(popExist);
|
||||||
|
else {
|
||||||
|
const popNew = newEmployee.shift();
|
||||||
|
popNew && sortedEmployeeId.push(popNew.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = { totalPrice: 0, totalDiscount: 0, totalVat: 0 };
|
||||||
|
|
||||||
|
const restructureService = body.service.flatMap((a) => {
|
||||||
|
const currentService = service.find((b) => b.id === a.id);
|
||||||
|
|
||||||
|
if (!currentService) return []; // should not possible
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: currentService.id,
|
||||||
|
name: currentService.name,
|
||||||
|
code: currentService.code,
|
||||||
|
detail: currentService.detail,
|
||||||
|
attributes: currentService.attributes as Prisma.JsonObject,
|
||||||
|
work: a.work.flatMap((c) => {
|
||||||
|
if (c.excluded) return [];
|
||||||
|
|
||||||
|
const currentWork = currentService.work.find((d) => d.id === c.id);
|
||||||
|
|
||||||
|
if (!currentWork) return []; // additional will get stripped
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: currentWork.id,
|
||||||
|
order: currentWork.order,
|
||||||
|
name: currentWork.name,
|
||||||
|
attributes: currentWork.attributes as Prisma.JsonObject,
|
||||||
|
product: c.product.flatMap((e) => {
|
||||||
|
const currentProduct = product.find((f) => f.id === e.id);
|
||||||
|
|
||||||
|
if (!currentProduct) return []; // should not possible
|
||||||
|
|
||||||
|
price.totalPrice += currentProduct.price * e.amount;
|
||||||
|
price.totalDiscount +=
|
||||||
|
Math.round(currentProduct.price * e.amount * e.discount * 100) / 100;
|
||||||
|
price.totalVat +=
|
||||||
|
Math.round(
|
||||||
|
(currentProduct.price * e.amount -
|
||||||
|
currentProduct.price * e.amount * e.discount) *
|
||||||
|
(e.vat === undefined ? 0.07 : e.vat) *
|
||||||
|
100,
|
||||||
|
) / 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
vat: e.vat === undefined ? 0.07 : e.vat,
|
||||||
|
pricePerUnit: currentProduct.price,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const currentMonth = new Date().getMonth() + 1;
|
||||||
|
const currentDate = new Date().getDate();
|
||||||
|
|
||||||
|
const lastQuotation = await tx.runningNo.upsert({
|
||||||
|
where: {
|
||||||
|
key: `QUOTATION_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}`,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
key: `QUOTATION_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}`,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
update: { value: { increment: 1 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const quotation = await tx.quotation.create({
|
||||||
|
data: {
|
||||||
|
...rest,
|
||||||
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
|
code: `${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(4, "0")}`,
|
||||||
|
worker: {
|
||||||
|
createMany: {
|
||||||
|
data: sortedEmployeeId.map((v, i) => ({
|
||||||
|
no: i,
|
||||||
|
code: "",
|
||||||
|
employeeId: v,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPrice: price.totalPrice,
|
||||||
|
totalDiscount: price.totalDiscount,
|
||||||
|
|
||||||
|
vat: price.totalVat,
|
||||||
|
vatExcluded: 0,
|
||||||
|
|
||||||
|
finalPrice: price.totalPrice - price.totalDiscount,
|
||||||
|
|
||||||
|
paySplit: {
|
||||||
|
createMany: {
|
||||||
|
data: (rest.paySplit || []).map((v, i) => ({
|
||||||
|
no: i + 1,
|
||||||
|
date: v,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
createdByUserId: req.user.sub,
|
||||||
|
updatedByUserId: req.user.sub,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
restructureService.map(async (a) => {
|
||||||
|
const { id: _currentServiceId } = await tx.quotationService.create({
|
||||||
|
data: {
|
||||||
|
code: a.code,
|
||||||
|
name: a.name,
|
||||||
|
detail: a.detail,
|
||||||
|
attributes: a.attributes,
|
||||||
|
quotationId: quotation.id,
|
||||||
|
refServiceId: a.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
a.work.map(async (b) => {
|
||||||
|
await tx.quotationServiceWork.create({
|
||||||
|
data: {
|
||||||
|
order: b.order,
|
||||||
|
name: b.name,
|
||||||
|
attributes: b.attributes,
|
||||||
|
serviceId: _currentServiceId,
|
||||||
|
productOnWork: {
|
||||||
|
createMany: {
|
||||||
|
data: b.product.map((v, i) => ({
|
||||||
|
productId: v.id,
|
||||||
|
order: i + 1,
|
||||||
|
vat: v.vat,
|
||||||
|
amount: v.amount,
|
||||||
|
discount: v.discount,
|
||||||
|
pricePerUnit: v.pricePerUnit,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return await tx.quotation.findUnique({
|
||||||
|
include: {
|
||||||
|
service: {
|
||||||
|
include: {
|
||||||
|
work: {
|
||||||
|
include: {
|
||||||
|
productOnWork: {
|
||||||
|
include: { product: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paySplit: true,
|
||||||
|
worker: true,
|
||||||
|
customerBranch: {
|
||||||
|
include: { customer: true },
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: { service: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { id: quotation.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("{quotationId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
|
async editQuotation(
|
||||||
|
@Request() req: RequestWithUser,
|
||||||
|
@Path() quotationId: string,
|
||||||
|
@Body() body: QuotationUpdate,
|
||||||
|
) {
|
||||||
|
const record = await prisma.quotation.findUnique({
|
||||||
|
include: { customer: true },
|
||||||
|
where: { id: quotationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingEmployee = body.worker?.filter((v) => typeof v === "string");
|
||||||
|
const serviceIdList = body.service?.map((v) => v.id);
|
||||||
|
const productIdList = body.service?.flatMap((a) =>
|
||||||
|
a.work.flatMap((b) => b.product.map((c) => c.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [customer, customerBranch, employee, service, product] = await prisma.$transaction(
|
||||||
|
async (tx) =>
|
||||||
|
await Promise.all([
|
||||||
|
tx.customer.findFirst({
|
||||||
|
where: { id: body.customerId },
|
||||||
|
}),
|
||||||
|
tx.customerBranch.findFirst({
|
||||||
|
include: { customer: true },
|
||||||
|
where: { id: body.customerBranchId },
|
||||||
|
}),
|
||||||
|
body.worker
|
||||||
|
? tx.employee.findMany({
|
||||||
|
where: { id: { in: existingEmployee } },
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
body.service
|
||||||
|
? tx.service.findMany({
|
||||||
|
include: { work: true },
|
||||||
|
where: { id: { in: serviceIdList } },
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
body.service
|
||||||
|
? tx.product.findMany({
|
||||||
|
where: { id: { in: productIdList } },
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serviceIdList?.length !== service?.length) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Some service cannot be found.",
|
||||||
|
"relationServiceNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (productIdList?.length !== product?.length) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Some product cannot be found.",
|
||||||
|
"relationProductNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (existingEmployee?.length !== employee?.length) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Some worker(employee) cannot be found.",
|
||||||
|
"relationWorkerNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!customer)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer cannot be found.",
|
||||||
|
"relationCustomerNotFound",
|
||||||
|
);
|
||||||
|
if (!customerBranch)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer Branch cannot be found.",
|
||||||
|
"relationCustomerBranchNotFound",
|
||||||
|
);
|
||||||
|
if (customerBranch.customerId !== customer.id)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Customer conflict with customer branch.",
|
||||||
|
"customerConflictCustomerBranch",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { service: _service, worker: _worker, ...rest } = body;
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const sortedEmployeeId: string[] = [];
|
||||||
|
|
||||||
|
if (body.worker) {
|
||||||
|
const nonExistEmployee = body.worker.filter((v) => typeof v !== "string");
|
||||||
|
const lastEmployee = 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: nonExistEmployee.length } },
|
||||||
|
});
|
||||||
|
const newEmployee = await Promise.all(
|
||||||
|
nonExistEmployee.map(async (v, i) =>
|
||||||
|
tx.employee.create({
|
||||||
|
data: {
|
||||||
|
...v,
|
||||||
|
code: `${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}${(lastEmployee.value + i).toString().padStart(4, "0")}`,
|
||||||
|
customerBranchId: customerBranch.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
while (body.worker.length > 0) {
|
||||||
|
const popExist = body.worker.shift();
|
||||||
|
if (typeof popExist === "string") sortedEmployeeId.push(popExist);
|
||||||
|
else {
|
||||||
|
const popNew = newEmployee.shift();
|
||||||
|
popNew && sortedEmployeeId.push(popNew.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = { totalPrice: 0, totalDiscount: 0, totalVat: 0 };
|
||||||
|
|
||||||
|
const restructureService = body.service?.flatMap((a) => {
|
||||||
|
const currentService = service?.find((b) => b.id === a.id);
|
||||||
|
|
||||||
|
if (!currentService) return []; // should not possible
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: currentService.id,
|
||||||
|
name: currentService.name,
|
||||||
|
code: currentService.code,
|
||||||
|
detail: currentService.detail,
|
||||||
|
attributes: currentService.attributes as Prisma.JsonObject,
|
||||||
|
work: a.work.flatMap((c) => {
|
||||||
|
if (c.excluded) return [];
|
||||||
|
|
||||||
|
const currentWork = currentService.work.find((d) => d.id === c.id);
|
||||||
|
|
||||||
|
if (!currentWork) return []; // additional will get stripped
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: currentWork.id,
|
||||||
|
order: currentWork.order,
|
||||||
|
name: currentWork.name,
|
||||||
|
attributes: currentWork.attributes as Prisma.JsonObject,
|
||||||
|
product: c.product.flatMap((e) => {
|
||||||
|
const currentProduct = product?.find((f) => f.id === e.id);
|
||||||
|
|
||||||
|
if (!currentProduct) return []; // should not possible
|
||||||
|
|
||||||
|
price.totalPrice += currentProduct.price * e.amount;
|
||||||
|
price.totalDiscount +=
|
||||||
|
Math.round(currentProduct.price * e.amount * e.discount * 100) / 100;
|
||||||
|
price.totalVat +=
|
||||||
|
Math.round(
|
||||||
|
(currentProduct.price * e.amount -
|
||||||
|
currentProduct.price * e.amount * e.discount) *
|
||||||
|
(e.vat === undefined ? 0.07 : e.vat) *
|
||||||
|
100,
|
||||||
|
) / 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
vat: e.vat === undefined ? 0.07 : e.vat,
|
||||||
|
pricePerUnit: currentProduct.price,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const quotation = await tx.quotation.update({
|
||||||
|
where: { id: quotationId },
|
||||||
|
data: {
|
||||||
|
...rest,
|
||||||
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
|
code: "",
|
||||||
|
worker:
|
||||||
|
sortedEmployeeId.length > 0
|
||||||
|
? {
|
||||||
|
deleteMany: { id: { notIn: sortedEmployeeId } },
|
||||||
|
createMany: {
|
||||||
|
skipDuplicates: true,
|
||||||
|
data: sortedEmployeeId.map((v, i) => ({
|
||||||
|
no: i,
|
||||||
|
code: "",
|
||||||
|
employeeId: v,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
totalPrice: body.service ? price.totalPrice : undefined,
|
||||||
|
totalDiscount: body.service ? price.totalDiscount : undefined,
|
||||||
|
|
||||||
|
vat: body.service ? price.totalVat : undefined,
|
||||||
|
vatExcluded: body.service ? 0 : undefined,
|
||||||
|
|
||||||
|
finalPrice: body.service ? price.totalPrice - price.totalDiscount : undefined,
|
||||||
|
|
||||||
|
paySplit: rest.paySplit
|
||||||
|
? {
|
||||||
|
deleteMany: {},
|
||||||
|
createMany: {
|
||||||
|
data: (rest.paySplit || []).map((v, i) => ({
|
||||||
|
no: i + 1,
|
||||||
|
date: v,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
service: body.service ? { deleteMany: {} } : undefined,
|
||||||
|
|
||||||
|
updatedByUserId: req.user.sub,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (restructureService) {
|
||||||
|
await Promise.all(
|
||||||
|
restructureService.map(async (a) => {
|
||||||
|
const { id: _currentServiceId } = await tx.quotationService.create({
|
||||||
|
data: {
|
||||||
|
code: a.code,
|
||||||
|
name: a.name,
|
||||||
|
detail: a.detail,
|
||||||
|
attributes: a.attributes,
|
||||||
|
quotationId: quotation.id,
|
||||||
|
refServiceId: a.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
a.work.map(async (b) => {
|
||||||
|
await tx.quotationServiceWork.create({
|
||||||
|
data: {
|
||||||
|
order: b.order,
|
||||||
|
name: b.name,
|
||||||
|
attributes: b.attributes,
|
||||||
|
serviceId: _currentServiceId,
|
||||||
|
productOnWork: {
|
||||||
|
createMany: {
|
||||||
|
data: b.product.map((v, i) => ({
|
||||||
|
productId: v.id,
|
||||||
|
order: i + 1,
|
||||||
|
vat: v.vat,
|
||||||
|
amount: v.amount,
|
||||||
|
discount: v.discount,
|
||||||
|
pricePerUnit: v.pricePerUnit,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await tx.quotation.findUnique({
|
||||||
|
include: {
|
||||||
|
service: {
|
||||||
|
include: {
|
||||||
|
work: {
|
||||||
|
include: {
|
||||||
|
productOnWork: {
|
||||||
|
include: { product: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paySplit: true,
|
||||||
|
worker: true,
|
||||||
|
customerBranch: {
|
||||||
|
include: { customer: true },
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: { service: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { id: quotation.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("{quotationId}")
|
||||||
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
|
async deleteQuotationById(@Path() quotationId: string) {
|
||||||
|
const record = await prisma.quotation.findUnique({
|
||||||
|
where: { id: quotationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.status !== Status.CREATED) {
|
||||||
|
throw new HttpError(HttpStatus.FORBIDDEN, "Quotation is in used.", "quotationInUsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.quotation.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: quotationId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,15 @@ if (!process.env.MINIO_BUCKET) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
||||||
|
const MANAGE_ROLES = [
|
||||||
|
"system",
|
||||||
|
"head_of_admin",
|
||||||
|
"admin",
|
||||||
|
"branch_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"accountant",
|
||||||
|
"branch_accountant",
|
||||||
|
];
|
||||||
|
|
||||||
type ServiceCreate = {
|
type ServiceCreate = {
|
||||||
code: "MOU" | "mou";
|
code: "MOU" | "mou";
|
||||||
|
|
@ -39,6 +48,8 @@ type ServiceCreate = {
|
||||||
productId: string[];
|
productId: string[];
|
||||||
attributes?: { [key: string]: any };
|
attributes?: { [key: string]: any };
|
||||||
}[];
|
}[];
|
||||||
|
productTypeId: string;
|
||||||
|
registeredBranchId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ServiceUpdate = {
|
type ServiceUpdate = {
|
||||||
|
|
@ -53,12 +64,18 @@ type ServiceUpdate = {
|
||||||
productId: string[];
|
productId: string[];
|
||||||
attributes?: { [key: string]: any };
|
attributes?: { [key: string]: any };
|
||||||
}[];
|
}[];
|
||||||
|
productTypeId?: string;
|
||||||
|
registeredBranchId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function imageLocation(id: string) {
|
function imageLocation(id: string) {
|
||||||
return `service/${id}/service-image`;
|
return `service/${id}/service-image`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function globalAllow(roles?: string[]) {
|
||||||
|
return ["system", "head_of_admin", "admin", "accountant"].some((v) => roles?.includes(v));
|
||||||
|
}
|
||||||
|
|
||||||
@Route("api/v1/service")
|
@Route("api/v1/service")
|
||||||
@Tags("Service")
|
@Tags("Service")
|
||||||
export class ServiceController extends Controller {
|
export class ServiceController extends Controller {
|
||||||
|
|
@ -75,6 +92,9 @@ export class ServiceController extends Controller {
|
||||||
@Query() page: number = 1,
|
@Query() page: number = 1,
|
||||||
@Query() pageSize: number = 30,
|
@Query() pageSize: number = 30,
|
||||||
@Query() status?: Status,
|
@Query() status?: Status,
|
||||||
|
@Query() productTypeId?: string,
|
||||||
|
@Query() registeredBranchId?: string,
|
||||||
|
@Query() fullDetail?: boolean,
|
||||||
) {
|
) {
|
||||||
const filterStatus = (val?: Status) => {
|
const filterStatus = (val?: Status) => {
|
||||||
if (!val) return {};
|
if (!val) return {};
|
||||||
|
|
@ -86,15 +106,32 @@ export class ServiceController extends Controller {
|
||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
OR: [
|
OR: [
|
||||||
{ name: { contains: query }, ...filterStatus(status) },
|
{ name: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||||
{ detail: { contains: query }, ...filterStatus(status) },
|
{ detail: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||||
],
|
],
|
||||||
|
AND: registeredBranchId
|
||||||
|
? {
|
||||||
|
OR: [{ registeredBranchId: registeredBranchId }, { registeredBranchId: null }],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
} satisfies Prisma.ServiceWhereInput;
|
} satisfies Prisma.ServiceWhereInput;
|
||||||
|
|
||||||
const [result, total] = await prisma.$transaction([
|
const [result, total] = await prisma.$transaction([
|
||||||
prisma.service.findMany({
|
prisma.service.findMany({
|
||||||
include: {
|
include: {
|
||||||
work: true,
|
work: fullDetail
|
||||||
|
? {
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
include: {
|
||||||
|
productOnWork: {
|
||||||
|
include: { product: true },
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||||
where,
|
where,
|
||||||
|
|
@ -135,6 +172,8 @@ export class ServiceController extends Controller {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: serviceId },
|
where: { id: serviceId },
|
||||||
});
|
});
|
||||||
|
|
@ -168,6 +207,8 @@ export class ServiceController extends Controller {
|
||||||
},
|
},
|
||||||
orderBy: { order: "asc" },
|
orderBy: { order: "asc" },
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -190,9 +231,47 @@ export class ServiceController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Security("keycloak")
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) {
|
async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) {
|
||||||
const { work, ...payload } = body;
|
const { work, productTypeId, ...payload } = body;
|
||||||
|
|
||||||
|
const [productType, branch] = await prisma.$transaction([
|
||||||
|
prisma.productType.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: body.productTypeId },
|
||||||
|
}),
|
||||||
|
prisma.branch.findFirst({
|
||||||
|
include: { user: { where: { userId: req.user.sub } } },
|
||||||
|
where: { id: body.registeredBranchId },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!globalAllow(req.user.roles) && !branch?.user.find((v) => v.userId === req.user.sub)) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!productType) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Product Type cannot be found.",
|
||||||
|
"relationProductTypeNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.registeredBranchId && !branch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch cannot be found.",
|
||||||
|
"relationBranchNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.$transaction(
|
const record = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
|
|
@ -239,14 +318,17 @@ export class ServiceController extends Controller {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...payload,
|
...payload,
|
||||||
|
productTypeId,
|
||||||
statusOrder: +(body.status === "INACTIVE"),
|
statusOrder: +(body.status === "INACTIVE"),
|
||||||
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
||||||
work: { connect: workList.map((v) => ({ id: v.id })) },
|
work: { connect: workList.map((v) => ({ id: v.id })) },
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -269,7 +351,7 @@ export class ServiceController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{serviceId}")
|
@Put("{serviceId}")
|
||||||
@Security("keycloak")
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async editService(
|
async editService(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: ServiceUpdate,
|
@Body() body: ServiceUpdate,
|
||||||
|
|
@ -278,7 +360,57 @@ export class ServiceController extends Controller {
|
||||||
if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) {
|
if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
|
||||||
}
|
}
|
||||||
const { work, ...payload } = body;
|
const { work, productTypeId, ...payload } = body;
|
||||||
|
|
||||||
|
const [service, productType, branch] = await prisma.$transaction([
|
||||||
|
prisma.service.findUnique({
|
||||||
|
include: {
|
||||||
|
registeredBranch: {
|
||||||
|
where: {
|
||||||
|
user: { some: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { id: serviceId },
|
||||||
|
}),
|
||||||
|
prisma.productType.findFirst({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: body.productTypeId },
|
||||||
|
}),
|
||||||
|
prisma.branch.findFirst({ where: { id: body.registeredBranchId } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalAllow(req.user.roles) && !service.registeredBranch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!productType) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Product Type cannot be found.",
|
||||||
|
"relationProductTypeNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.registeredBranchId && !branch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Branch cannot be found.",
|
||||||
|
"relationBranchNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const record = await prisma.$transaction(async (tx) => {
|
const record = await prisma.$transaction(async (tx) => {
|
||||||
const workList = await Promise.all(
|
const workList = await Promise.all(
|
||||||
(work || []).map(async (w, wIdx) =>
|
(work || []).map(async (w, wIdx) =>
|
||||||
|
|
@ -301,6 +433,10 @@ export class ServiceController extends Controller {
|
||||||
);
|
);
|
||||||
|
|
||||||
return await tx.service.update({
|
return await tx.service.update({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...payload,
|
...payload,
|
||||||
statusOrder: +(payload.status === "INACTIVE"),
|
statusOrder: +(payload.status === "INACTIVE"),
|
||||||
|
|
@ -308,7 +444,7 @@ export class ServiceController extends Controller {
|
||||||
deleteMany: {},
|
deleteMany: {},
|
||||||
connect: workList.map((v) => ({ id: v.id })),
|
connect: workList.map((v) => ({ id: v.id })),
|
||||||
},
|
},
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
where: { id: serviceId },
|
where: { id: serviceId },
|
||||||
});
|
});
|
||||||
|
|
@ -329,18 +465,41 @@ export class ServiceController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{serviceId}")
|
@Delete("{serviceId}")
|
||||||
@Security("keycloak")
|
@Security("keycloak", MANAGE_ROLES)
|
||||||
async deleteService(@Path() serviceId: string) {
|
async deleteService(@Request() req: RequestWithUser, @Path() serviceId: string) {
|
||||||
const record = await prisma.service.findFirst({ where: { id: serviceId } });
|
const record = await prisma.service.findFirst({
|
||||||
|
include: {
|
||||||
|
registeredBranch: {
|
||||||
|
where: {
|
||||||
|
user: { some: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { id: serviceId },
|
||||||
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globalAllow(req.user.roles) && !record.registeredBranch) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (record.status !== Status.CREATED) {
|
if (record.status !== Status.CREATED) {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "Service is in used.", "serviceInUsed");
|
throw new HttpError(HttpStatus.FORBIDDEN, "Service is in used.", "serviceInUsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.service.delete({ where: { id: serviceId } });
|
return await prisma.service.delete({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
|
where: { id: serviceId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
Security,
|
Security,
|
||||||
Tags,
|
Tags,
|
||||||
} from "tsoa";
|
} from "tsoa";
|
||||||
import { Prisma, Status, UserType } from "@prisma/client";
|
import { Branch, Prisma, Status, User, UserType } from "@prisma/client";
|
||||||
|
|
||||||
import prisma from "../db";
|
import prisma from "../db";
|
||||||
import minio, { presignedGetObjectIfExist } from "../services/minio";
|
import minio, { presignedGetObjectIfExist } from "../services/minio";
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
createUser,
|
createUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
editUser,
|
editUser,
|
||||||
getRoles,
|
listRole,
|
||||||
getUserRoles,
|
getUserRoles,
|
||||||
removeUserRoles,
|
removeUserRoles,
|
||||||
} from "../services/keycloak";
|
} from "../services/keycloak";
|
||||||
|
|
@ -43,8 +43,11 @@ type UserCreate = {
|
||||||
|
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
namePrefix?: string | null;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
firstNameEN: string;
|
firstNameEN: string;
|
||||||
|
middleName?: string | null;
|
||||||
|
middleNameEN?: string | null;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
lastNameEN: string;
|
lastNameEN: string;
|
||||||
gender: string;
|
gender: string;
|
||||||
|
|
@ -73,6 +76,8 @@ type UserCreate = {
|
||||||
subDistrictId?: string | null;
|
subDistrictId?: string | null;
|
||||||
districtId?: string | null;
|
districtId?: string | null;
|
||||||
provinceId?: string | null;
|
provinceId?: string | null;
|
||||||
|
|
||||||
|
branchId: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserUpdate = {
|
type UserUpdate = {
|
||||||
|
|
@ -83,8 +88,11 @@ type UserUpdate = {
|
||||||
userType?: UserType;
|
userType?: UserType;
|
||||||
userRole?: string;
|
userRole?: string;
|
||||||
|
|
||||||
|
namePrefix?: string | null;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
firstNameEN?: string;
|
firstNameEN?: string;
|
||||||
|
middleName?: string | null;
|
||||||
|
middleNameEN?: string | null;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
lastNameEN?: string;
|
lastNameEN?: string;
|
||||||
gender?: string;
|
gender?: string;
|
||||||
|
|
@ -113,8 +121,44 @@ type UserUpdate = {
|
||||||
subDistrictId?: string | null;
|
subDistrictId?: string | null;
|
||||||
districtId?: string | null;
|
districtId?: string | null;
|
||||||
provinceId?: string | null;
|
provinceId?: string | null;
|
||||||
|
|
||||||
|
branchId?: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function userBranchCodeGen(user: User, branch: Branch) {
|
||||||
|
return await prisma.$transaction(
|
||||||
|
async (tx) => {
|
||||||
|
const typ = user.userType;
|
||||||
|
|
||||||
|
const mapTypeNo = {
|
||||||
|
USER: 1,
|
||||||
|
MESSENGER: 2,
|
||||||
|
DELEGATE: 3,
|
||||||
|
AGENCY: 4,
|
||||||
|
}[typ];
|
||||||
|
|
||||||
|
const last = await tx.runningNo.upsert({
|
||||||
|
where: {
|
||||||
|
key: `BR_USR_${branch.code}_${mapTypeNo}`,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
key: `BR_USR_${branch.code}_${mapTypeNo}`,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
update: { value: { increment: 1 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return await tx.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
code: mapTypeNo + `${last.value}`.padStart(6, "0"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function imageLocation(id: string) {
|
function imageLocation(id: string) {
|
||||||
return `user/profile-img-${id}`;
|
return `user/profile-img-${id}`;
|
||||||
}
|
}
|
||||||
|
|
@ -153,16 +197,28 @@ export class UserController extends Controller {
|
||||||
@Query() query: string = "",
|
@Query() query: string = "",
|
||||||
@Query() page: number = 1,
|
@Query() page: number = 1,
|
||||||
@Query() pageSize: number = 30,
|
@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 = {
|
const where = {
|
||||||
OR: [
|
OR: [
|
||||||
{ firstName: { contains: query }, zipCode, userType },
|
{ firstName: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||||
{ firstNameEN: { contains: query }, zipCode, userType },
|
{ firstNameEN: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||||
{ lastName: { contains: query }, zipCode, userType },
|
{ lastName: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||||
{ lastNameEN: { contains: query }, zipCode, userType },
|
{ lastNameEN: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||||
{ email: { contains: query }, zipCode, userType },
|
{ email: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||||
{ telephoneNo: { contains: query }, zipCode, userType },
|
{ telephoneNo: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||||
],
|
],
|
||||||
|
AND: {
|
||||||
|
userRole: { not: "system" },
|
||||||
|
},
|
||||||
} satisfies Prisma.UserWhereInput;
|
} satisfies Prisma.UserWhereInput;
|
||||||
|
|
||||||
const [result, total] = await prisma.$transaction([
|
const [result, total] = await prisma.$transaction([
|
||||||
|
|
@ -173,6 +229,8 @@ export class UserController extends Controller {
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
branch: { include: { branch: includeBranch } },
|
branch: { include: { branch: includeBranch } },
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|
@ -207,6 +265,8 @@ export class UserController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|
@ -223,40 +283,59 @@ export class UserController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Security("keycloak")
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin"])
|
||||||
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
|
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
|
||||||
if (body.provinceId || body.districtId || body.subDistrictId) {
|
const [province, district, subDistrict, branch] = await prisma.$transaction([
|
||||||
const [province, district, subDistrict] = await prisma.$transaction([
|
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
|
||||||
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
|
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
|
||||||
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
|
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
|
||||||
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
|
prisma.branch.findMany({
|
||||||
]);
|
include: { user: { where: { userId: req.user.sub } } },
|
||||||
if (body.provinceId && !province) {
|
where: { id: { in: Array.isArray(body.branchId) ? body.branchId : [body.branchId] } },
|
||||||
throw new HttpError(
|
}),
|
||||||
HttpStatus.BAD_REQUEST,
|
]);
|
||||||
"Province cannot be found.",
|
if (body.provinceId && !province) {
|
||||||
"relationProvinceNotFound",
|
throw new HttpError(
|
||||||
);
|
HttpStatus.BAD_REQUEST,
|
||||||
}
|
"Province cannot be found.",
|
||||||
if (body.districtId && !district) {
|
"relationProvinceNotFound",
|
||||||
throw new HttpError(
|
);
|
||||||
HttpStatus.BAD_REQUEST,
|
}
|
||||||
"District cannot be found.",
|
if (body.districtId && !district) {
|
||||||
"relationDistrictNotFound",
|
throw new HttpError(
|
||||||
);
|
HttpStatus.BAD_REQUEST,
|
||||||
}
|
"District cannot be found.",
|
||||||
if (body.subDistrictId && !subDistrict) {
|
"relationDistrictNotFound",
|
||||||
throw new HttpError(
|
);
|
||||||
HttpStatus.BAD_REQUEST,
|
}
|
||||||
"Sub-district cannot be found.",
|
if (body.subDistrictId && !subDistrict) {
|
||||||
"relationSubDistrictNotFound",
|
throw new HttpError(
|
||||||
);
|
HttpStatus.BAD_REQUEST,
|
||||||
}
|
"Sub-district cannot be found.",
|
||||||
|
"relationSubDistrictNotFound",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (branch.length === 0) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Require at least one branch for a user.",
|
||||||
|
"minimumBranchNotMet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
branch?.some((v) => !v.user.find((v) => v.userId === req.user.sub))
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { provinceId, districtId, subDistrictId, username, ...rest } = body;
|
const { branchId, provinceId, districtId, subDistrictId, username, ...rest } = body;
|
||||||
|
|
||||||
let list = await getRoles();
|
let list = await listRole();
|
||||||
|
|
||||||
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
|
|
@ -287,7 +366,13 @@ export class UserController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await prisma.user.create({
|
const record = await prisma.user.create({
|
||||||
include: { province: true, district: true, subDistrict: true },
|
include: {
|
||||||
|
province: true,
|
||||||
|
district: true,
|
||||||
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
id: userId,
|
id: userId,
|
||||||
...rest,
|
...rest,
|
||||||
|
|
@ -297,11 +382,31 @@ export class UserController extends Controller {
|
||||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||||
district: { connect: districtId ? { id: districtId } : undefined },
|
district: { connect: districtId ? { id: districtId } : undefined },
|
||||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||||
createdBy: req.user.name,
|
createdBy: { connect: { id: req.user.sub } },
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prisma.branchUser.createMany({
|
||||||
|
data: Array.isArray(branchId)
|
||||||
|
? branchId.map((v) => ({
|
||||||
|
branchId: v,
|
||||||
|
userId: record.id,
|
||||||
|
createdByUserId: req.user.sub,
|
||||||
|
updatedByUserId: req.user.sub,
|
||||||
|
}))
|
||||||
|
: {
|
||||||
|
branchId,
|
||||||
|
userId: record.id,
|
||||||
|
createdByUserId: req.user.sub,
|
||||||
|
updatedByUserId: req.user.sub,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await userBranchCodeGen(record, branch[0]); // only generate code by using first branch only
|
||||||
|
|
||||||
|
record.code = updated.code;
|
||||||
|
|
||||||
this.setStatus(HttpStatus.CREATED);
|
this.setStatus(HttpStatus.CREATED);
|
||||||
|
|
||||||
return Object.assign(record, {
|
return Object.assign(record, {
|
||||||
|
|
@ -319,43 +424,72 @@ export class UserController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("{userId}")
|
@Put("{userId}")
|
||||||
@Security("keycloak")
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
async editUser(
|
async editUser(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Body() body: UserUpdate,
|
@Body() body: UserUpdate,
|
||||||
@Path() userId: string,
|
@Path() userId: string,
|
||||||
) {
|
) {
|
||||||
if (body.subDistrictId || body.districtId || body.provinceId) {
|
const [province, district, subDistrict, user, branch] = await prisma.$transaction([
|
||||||
const [province, district, subDistrict] = await prisma.$transaction([
|
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
|
||||||
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
|
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
|
||||||
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
|
prisma.user.findFirst({
|
||||||
]);
|
include: { branch: true },
|
||||||
|
where: { id: userId },
|
||||||
if (body.provinceId && !province)
|
}),
|
||||||
throw new HttpError(
|
prisma.branch.findMany({
|
||||||
HttpStatus.BAD_REQUEST,
|
include: { user: { where: { id: req.user.sub } } },
|
||||||
"Province cannot be found.",
|
where: {
|
||||||
"missing_or_invalid_parameter",
|
id: {
|
||||||
);
|
in: Array.isArray(body.branchId) ? body.branchId : body.branchId ? [body.branchId] : [],
|
||||||
if (body.districtId && !district)
|
},
|
||||||
throw new HttpError(
|
},
|
||||||
HttpStatus.BAD_REQUEST,
|
}),
|
||||||
"District cannot be found.",
|
]);
|
||||||
"missing_or_invalid_parameter",
|
if (!user) {
|
||||||
);
|
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
|
||||||
if (body.subDistrictId && !subDistrict)
|
}
|
||||||
throw new HttpError(
|
if (body.provinceId && !province)
|
||||||
HttpStatus.BAD_REQUEST,
|
throw new HttpError(
|
||||||
"Sub-district cannot be found.",
|
HttpStatus.BAD_REQUEST,
|
||||||
"missing_or_invalid_parameter",
|
"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.branchId && branch.length === 0) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Require at least one branch for a user.",
|
||||||
|
"minimumBranchNotMet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
branch?.some((v) => !v.user.find((v) => v.userId === req.user.sub))
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let userRole: string | undefined;
|
let userRole: string | undefined;
|
||||||
|
|
||||||
if (body.userRole) {
|
if (body.userRole) {
|
||||||
let list = await getRoles();
|
let list = await listRole();
|
||||||
|
|
||||||
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
|
||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
|
|
@ -394,38 +528,20 @@ export class UserController extends Controller {
|
||||||
await editUser(userId, { username: body.username, enabled: body.status !== "INACTIVE" });
|
await editUser(userId, { username: body.username, enabled: body.status !== "INACTIVE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { provinceId, districtId, subDistrictId, ...rest } = body;
|
const { provinceId, districtId, subDistrictId, branchId, ...rest } = body;
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: { id: userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "branchNotFound");
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastUserOfType =
|
|
||||||
body.userType &&
|
|
||||||
body.userType !== user.userType &&
|
|
||||||
user.code &&
|
|
||||||
(await prisma.user.findFirst({
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
where: {
|
|
||||||
userType: body.userType,
|
|
||||||
code: { startsWith: `${user.code?.slice(0, 3)}` },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const record = await prisma.user.update({
|
const record = await prisma.user.update({
|
||||||
include: { province: true, district: true, subDistrict: true },
|
include: {
|
||||||
|
province: true,
|
||||||
|
district: true,
|
||||||
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
statusOrder: +(rest.status === "INACTIVE"),
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
userRole,
|
userRole,
|
||||||
code:
|
|
||||||
(lastUserOfType &&
|
|
||||||
`${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) ||
|
|
||||||
undefined,
|
|
||||||
province: {
|
province: {
|
||||||
connect: provinceId ? { id: provinceId } : undefined,
|
connect: provinceId ? { id: provinceId } : undefined,
|
||||||
disconnect: provinceId === null || undefined,
|
disconnect: provinceId === null || undefined,
|
||||||
|
|
@ -438,11 +554,42 @@ export class UserController extends Controller {
|
||||||
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
||||||
disconnect: subDistrictId === null || undefined,
|
disconnect: subDistrictId === null || undefined,
|
||||||
},
|
},
|
||||||
updatedBy: req.user.name,
|
updatedBy: { connect: { id: req.user.sub } },
|
||||||
},
|
},
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (branchId) {
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.branchUser.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
branchId: { not: { in: Array.isArray(branchId) ? branchId : [branchId] } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.branchUser.createMany({
|
||||||
|
data: (Array.isArray(branchId) ? branchId : [branchId])
|
||||||
|
.filter((a) => !user.branch.some((b) => a === b.branchId))
|
||||||
|
.map((v) => ({
|
||||||
|
userId,
|
||||||
|
branchId: v,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
prisma.branch.updateMany({
|
||||||
|
where: {
|
||||||
|
id: { in: Array.isArray(branchId) ? branchId : [branchId] },
|
||||||
|
status: "CREATED",
|
||||||
|
},
|
||||||
|
data: { status: "ACTIVE" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (branch[0]?.id !== user.branch[0]?.branchId) {
|
||||||
|
const updated = await userBranchCodeGen(user, branch[0]);
|
||||||
|
record.code = updated.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Object.assign(record, {
|
return Object.assign(record, {
|
||||||
profileImageUrl: await minio.presignedGetObject(
|
profileImageUrl: await minio.presignedGetObject(
|
||||||
MINIO_BUCKET,
|
MINIO_BUCKET,
|
||||||
|
|
@ -458,17 +605,35 @@ export class UserController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("{userId}")
|
@Delete("{userId}")
|
||||||
@Security("keycloak")
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
async deleteUser(@Path() userId: string) {
|
async deleteUser(@Request() req: RequestWithUser, @Path() userId: string) {
|
||||||
const record = await prisma.user.findFirst({
|
const record = await prisma.user.findFirst({
|
||||||
include: {
|
include: {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
branch: {
|
||||||
|
where: {
|
||||||
|
userId: req.user.sub,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
!record?.branch.some((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
|
||||||
}
|
}
|
||||||
|
|
@ -502,10 +667,55 @@ export class UserController extends Controller {
|
||||||
province: true,
|
province: true,
|
||||||
district: true,
|
district: true,
|
||||||
subDistrict: true,
|
subDistrict: true,
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("{userId}/image")
|
||||||
|
async getUserImageByUserId(@Request() req: RequestWithUser, @Path() userId: string) {
|
||||||
|
const url = await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(userId), 60 * 60);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("{userId}/image")
|
||||||
|
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||||
|
async setUserImageByUserId(@Request() req: RequestWithUser, @Path() userId: string) {
|
||||||
|
const record = await prisma.user.findFirst({
|
||||||
|
include: {
|
||||||
|
branch: { where: { userId: req.user.sub } },
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) &&
|
||||||
|
!record.branch.some((v) => v.userId === req.user.sub)
|
||||||
|
) {
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to perform this action.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.res?.redirect(
|
||||||
|
await minio.presignedPutObject(MINIO_BUCKET, imageLocation(userId), 12 * 60 * 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachmentLocation(uid: string) {
|
function attachmentLocation(uid: string) {
|
||||||
|
|
@ -519,11 +729,6 @@ export class UserAttachmentController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
async listAttachment(@Path() userId: string) {
|
async listAttachment(@Path() userId: string) {
|
||||||
const record = await prisma.user.findFirst({
|
const record = await prisma.user.findFirst({
|
||||||
include: {
|
|
||||||
province: true,
|
|
||||||
district: true,
|
|
||||||
subDistrict: true,
|
|
||||||
},
|
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -552,11 +757,6 @@ export class UserAttachmentController extends Controller {
|
||||||
@Post()
|
@Post()
|
||||||
async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
|
async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
|
||||||
const record = await prisma.user.findFirst({
|
const record = await prisma.user.findFirst({
|
||||||
include: {
|
|
||||||
province: true,
|
|
||||||
district: true,
|
|
||||||
subDistrict: true,
|
|
||||||
},
|
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ export class WorkController extends Controller {
|
||||||
order: "asc",
|
order: "asc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
where,
|
where,
|
||||||
|
|
@ -106,6 +108,10 @@ export class WorkController extends Controller {
|
||||||
|
|
||||||
const [result, total] = await prisma.$transaction([
|
const [result, total] = await prisma.$transaction([
|
||||||
prisma.product.findMany({
|
prisma.product.findMany({
|
||||||
|
include: {
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
},
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
|
|
@ -129,6 +135,8 @@ export class WorkController extends Controller {
|
||||||
product: true,
|
product: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
productOnWork: {
|
productOnWork: {
|
||||||
|
|
@ -175,6 +183,8 @@ export class WorkController extends Controller {
|
||||||
order: "asc",
|
order: "asc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...payload,
|
...payload,
|
||||||
|
|
@ -183,13 +193,13 @@ export class WorkController extends Controller {
|
||||||
data: productId.map((v, i) => ({
|
data: productId.map((v, i) => ({
|
||||||
order: i + 1,
|
order: i + 1,
|
||||||
productId: v,
|
productId: v,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -261,6 +271,8 @@ export class WorkController extends Controller {
|
||||||
order: "asc",
|
order: "asc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
},
|
},
|
||||||
where: { id: workId },
|
where: { id: workId },
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -281,13 +293,13 @@ export class WorkController extends Controller {
|
||||||
create: {
|
create: {
|
||||||
order: i + 1,
|
order: i + 1,
|
||||||
productId: v,
|
productId: v,
|
||||||
createdBy: req.user.name,
|
createdByUserId: req.user.sub,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
updatedBy: req.user.name,
|
updatedByUserId: req.user.sub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -296,7 +308,10 @@ export class WorkController extends Controller {
|
||||||
|
|
||||||
@Delete("{workId}")
|
@Delete("{workId}")
|
||||||
async deleteWork(@Path() workId: string) {
|
async deleteWork(@Path() workId: string) {
|
||||||
const record = await prisma.work.findFirst({ where: { id: workId } });
|
const record = await prisma.work.findFirst({
|
||||||
|
include: { createdBy: true, updatedBy: true },
|
||||||
|
where: { id: workId },
|
||||||
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new HttpError(HttpStatus.NOT_FOUND, "Work cannot be found.", "workNotFound");
|
throw new HttpError(HttpStatus.NOT_FOUND, "Work cannot be found.", "workNotFound");
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ export type RequestWithUser = Request & {
|
||||||
familiy_name: string;
|
familiy_name: string;
|
||||||
preferred_username: string;
|
preferred_username: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string[];
|
roles: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,14 @@ const jwtDecode = createDecoder();
|
||||||
export async function keycloakAuth(request: Express.Request, roles?: string[]) {
|
export async function keycloakAuth(request: Express.Request, roles?: string[]) {
|
||||||
const token = request.headers["authorization"]?.includes("Bearer ")
|
const token = request.headers["authorization"]?.includes("Bearer ")
|
||||||
? request.headers["authorization"].split(" ")[1]
|
? request.headers["authorization"].split(" ")[1]
|
||||||
: request.headers["authorization"];
|
: "";
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบข้อมูลสำหรับยืนยันตัวตน");
|
throw new HttpError(
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
"authorization data not found.",
|
||||||
|
"authDataNotFound",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload: Record<string, any> = {};
|
let payload: Record<string, any> = {};
|
||||||
|
|
@ -47,7 +51,11 @@ export async function keycloakAuth(request: Express.Request, roles?: string[]) {
|
||||||
|
|
||||||
if (Array.isArray(payload.roles) && Array.isArray(roles) && roles.length > 0) {
|
if (Array.isArray(payload.roles) && Array.isArray(roles) && roles.length > 0) {
|
||||||
if (!roles.some((a: string) => payload.roles.includes(a))) {
|
if (!roles.some((a: string) => payload.roles.includes(a))) {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, "คุณไม่มีสิทธิในการเข้าถึงข้อมูลดังกล่าว");
|
throw new HttpError(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"You do not have permission to access this resource.",
|
||||||
|
"noPermission",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +65,7 @@ export async function keycloakAuth(request: Express.Request, roles?: string[]) {
|
||||||
async function verifyOffline(token: string) {
|
async function verifyOffline(token: string) {
|
||||||
const payload = await jwtVerify(token).catch((_) => null);
|
const payload = await jwtVerify(token).catch((_) => null);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่สามารถยืนยันตัวตนได้");
|
throw new HttpError(HttpStatus.UNAUTHORIZED, "Unauthorized.", "authFailed");
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +78,14 @@ async function verifyOnline(token: string) {
|
||||||
},
|
},
|
||||||
).catch((e) => console.error(e));
|
).catch((e) => console.error(e));
|
||||||
|
|
||||||
if (!res) throw new Error("ไม่สามารถเข้าถึงระบบยืนยันตัวตน");
|
if (!res)
|
||||||
|
throw new HttpError(
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error authentication service.",
|
||||||
|
"authFailedFatal",
|
||||||
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่สามารถยืนยันตัวตนได้");
|
throw new HttpError(HttpStatus.UNAUTHORIZED, "Unauthorized.", "authFailed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await jwtDecode(token);
|
return await jwtDecode(token);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@ export async function expressAuthentication(
|
||||||
switch (securityName) {
|
switch (securityName) {
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
const authData = await keycloakAuth(request, scopes);
|
const authData = await keycloakAuth(request, scopes);
|
||||||
|
if (!request.app.locals.logData) request.app.locals.logData = {};
|
||||||
request.app.locals.logData.sessionId = authData.session_state;
|
request.app.locals.logData.sessionId = authData.session_state;
|
||||||
request.app.locals.logData.user = authData.preffered_username;
|
request.app.locals.logData.user = authData.preferred_username;
|
||||||
|
request.app.locals.logData.userName = authData.name;
|
||||||
|
request.app.locals.logData.userId = authData.sub;
|
||||||
return authData;
|
return authData;
|
||||||
default:
|
default:
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const ELASTICSEARCH_INDEX = process.env.ELASTICSEARCH_INDEX;
|
|
||||||
|
|
||||||
const LOG_LEVEL_MAP: Record<string, number> = {
|
|
||||||
debug: 4,
|
|
||||||
info: 3,
|
|
||||||
warning: 2,
|
|
||||||
error: 1,
|
|
||||||
none: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
||||||
if (!req.url.startsWith("/api/")) return next();
|
|
||||||
|
|
||||||
let data: any;
|
|
||||||
|
|
||||||
const originalJson = res.json;
|
|
||||||
|
|
||||||
res.json = function (v: any) {
|
|
||||||
data = v;
|
|
||||||
return originalJson.call(this, v);
|
|
||||||
};
|
|
||||||
|
|
||||||
const timestamp = new Date().toString();
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
req.app.locals.logData = {};
|
|
||||||
|
|
||||||
res.on("finish", () => {
|
|
||||||
if (!req.url.startsWith("/api/")) return;
|
|
||||||
|
|
||||||
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "info"] || 1;
|
|
||||||
|
|
||||||
if (level === 1 && res.statusCode < 500) return;
|
|
||||||
if (level === 2 && res.statusCode < 400) return;
|
|
||||||
if (level === 3 && res.statusCode < 200) return;
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
|
|
||||||
systemName: "JWS-SOS",
|
|
||||||
startTimeStamp: timestamp,
|
|
||||||
endTimeStamp: new Date().toString(),
|
|
||||||
processTime: performance.now() - start,
|
|
||||||
host: req.hostname,
|
|
||||||
sessionId: req.headers["x-session-id"],
|
|
||||||
rtId: req.headers["x-rtid"],
|
|
||||||
tId: randomUUID(),
|
|
||||||
method: req.method,
|
|
||||||
endpoint: req.url,
|
|
||||||
responseCode: 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(obj);
|
|
||||||
|
|
||||||
elasticsearch.index({
|
|
||||||
index: ELASTICSEARCH_INDEX,
|
|
||||||
document: obj,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default logMiddleware;
|
|
||||||
40
src/middlewares/logger.ts
Normal file
40
src/middlewares/logger.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import winston from "winston";
|
||||||
|
import { ElasticsearchTransport } from "winston-elasticsearch";
|
||||||
|
import elasticsearch from "../services/elasticsearch";
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
levels: winston.config.syslog.levels,
|
||||||
|
defaultMeta: { serviceName: "jws-sos" },
|
||||||
|
transports: [
|
||||||
|
new ElasticsearchTransport({
|
||||||
|
level: "info",
|
||||||
|
index: "app-log-test-winston-index",
|
||||||
|
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||||
|
client: elasticsearch,
|
||||||
|
transformer: (payload) => {
|
||||||
|
const { logData: additional, ...rest } = payload.meta;
|
||||||
|
return {
|
||||||
|
level: payload.level,
|
||||||
|
...rest,
|
||||||
|
...additional,
|
||||||
|
requestBody:
|
||||||
|
process.env.LOG_LEVEL === "debug" ? JSON.stringify(rest.requestBody) : undefined,
|
||||||
|
responseBody:
|
||||||
|
process.env.LOG_LEVEL === "debug" ? JSON.stringify(rest.responseBody) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.printf(
|
||||||
|
({ level, timestamp, logData, responseBody, requestBody, ...payload }) =>
|
||||||
|
`${level} ${timestamp} ${JSON.stringify(Object.assign(payload, logData), null, 4)}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default logger;
|
||||||
71
src/middlewares/morgan.ts
Normal file
71
src/middlewares/morgan.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import express from "express";
|
||||||
|
import morgan from "morgan";
|
||||||
|
import logger from "./logger";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
const LOG_LEVEL_MAP: Record<string, number> = {
|
||||||
|
debug: 4,
|
||||||
|
info: 3,
|
||||||
|
warning: 2,
|
||||||
|
error: 1,
|
||||||
|
none: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// log the HTTP method, request URL, response status, and response time.
|
||||||
|
const logFormat = `{
|
||||||
|
"requestMethod": ":method",
|
||||||
|
"requestUrl": ":url",
|
||||||
|
"responseStatus": ":status",
|
||||||
|
"responseTime": ":response-time ms",
|
||||||
|
"transactionId": ":transaction-id",
|
||||||
|
"refTransactionId": ":ref-transaction-id",
|
||||||
|
"sessionId": ":session-id",
|
||||||
|
"requestBody": :request-body,
|
||||||
|
"responseBody": :response-body,
|
||||||
|
"logData": :log-data
|
||||||
|
}`;
|
||||||
|
|
||||||
|
function logMessageHandler(message: string) {
|
||||||
|
const data = JSON.parse(message.trim());
|
||||||
|
|
||||||
|
if (!(data.requestUrl as string).startsWith("/api/")) return;
|
||||||
|
|
||||||
|
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "info"] || 1;
|
||||||
|
const status = +data.responseStatus;
|
||||||
|
|
||||||
|
if (level === 1 && status < 500) return;
|
||||||
|
if (level === 2 && status < 400) return;
|
||||||
|
if (level === 3 && status < 200) return;
|
||||||
|
|
||||||
|
if (status >= 500) return logger.error("HTTP request received", data);
|
||||||
|
if (status >= 400) return logger.warning("HTTP request received", data);
|
||||||
|
return logger.info("HTTP request received", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
morgan.token("log-data", (req: express.Request) => {
|
||||||
|
return JSON.stringify(req.app.locals.logData || {});
|
||||||
|
});
|
||||||
|
morgan.token("request-body", (req: express.Request) => {
|
||||||
|
return JSON.stringify(req.body);
|
||||||
|
});
|
||||||
|
morgan.token("response-body", (req: express.Request) => {
|
||||||
|
return JSON.stringify(req.app.locals.response || {});
|
||||||
|
});
|
||||||
|
morgan.token("identity-field", (req: express.Request) => {
|
||||||
|
return req.app.locals.identityField;
|
||||||
|
});
|
||||||
|
morgan.token("session-id", (req: express.Request) => {
|
||||||
|
return req.headers["x-session-id"] as string | undefined;
|
||||||
|
});
|
||||||
|
morgan.token("ref-transaction-id", (req: express.Request) => {
|
||||||
|
return req.headers["x-rtid"] as string | undefined;
|
||||||
|
});
|
||||||
|
morgan.token("transaction-id", () => {
|
||||||
|
return randomUUID();
|
||||||
|
});
|
||||||
|
|
||||||
|
const loggingMiddleware = morgan(logFormat, {
|
||||||
|
stream: { write: logMessageHandler },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default loggingMiddleware;
|
||||||
|
|
@ -8,10 +8,10 @@ export function role(
|
||||||
errorMessage: string = "You do not have permission to access this resource.",
|
errorMessage: string = "You do not have permission to access this resource.",
|
||||||
) {
|
) {
|
||||||
return (req: RequestWithUser, _res: Response, next: NextFunction) => {
|
return (req: RequestWithUser, _res: Response, next: NextFunction) => {
|
||||||
if (!Array.isArray(role) && !req.user.role.includes(role) && !req.user.role.includes("*")) {
|
if (!Array.isArray(role) && !req.user.roles.includes(role) && !req.user.roles.includes("*")) {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, errorMessage, "noPermissionToAccess");
|
throw new HttpError(HttpStatus.FORBIDDEN, errorMessage, "noPermissionToAccess");
|
||||||
}
|
}
|
||||||
if (role !== "*" && !req.user.role.some((v) => role.includes(v))) {
|
if (role !== "*" && !req.user.roles.some((v) => role.includes(v))) {
|
||||||
throw new HttpError(HttpStatus.FORBIDDEN, errorMessage, "noPermissionToAccess");
|
throw new HttpError(HttpStatus.FORBIDDEN, errorMessage, "noPermissionToAccess");
|
||||||
}
|
}
|
||||||
return next();
|
return next();
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,61 @@ export async function getToken() {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get keycloak user list
|
||||||
|
*
|
||||||
|
* @returns user list if success, false otherwise.
|
||||||
|
*/
|
||||||
|
export async function listUser(search = "", page = 1, pageSize = 30) {
|
||||||
|
const res = await fetch(
|
||||||
|
`${KC_URL}/admin/realms/${KC_REALM}/users?first=${(page - 1) * pageSize}&max=${pageSize}`.concat(
|
||||||
|
!!search ? `&search=${search}` : "",
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${await getToken()}`,
|
||||||
|
"content-type": `application/json`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).catch((e) => console.log("Keycloak Error: ", e));
|
||||||
|
|
||||||
|
if (!res) return;
|
||||||
|
if (!res.ok) {
|
||||||
|
return console.error("Keycloak Error Response: ", await res.json());
|
||||||
|
}
|
||||||
|
return ((await res.json()) as any[]).map((v: Record<string, string>) => ({
|
||||||
|
id: v.id,
|
||||||
|
username: v.username,
|
||||||
|
firstName: v.firstName,
|
||||||
|
lastName: v.lastName,
|
||||||
|
email: v.email,
|
||||||
|
attributes: v.attributes,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count user in the system. Can be use for pagination purpose.
|
||||||
|
*
|
||||||
|
* @returns numer of user on success.
|
||||||
|
*/
|
||||||
|
export async function countUser(search = "") {
|
||||||
|
const res = await fetch(
|
||||||
|
`${KC_URL}/admin/realms/${KC_REALM}/users/count`.concat(!!search ? `?search=${search}` : ""),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${await getToken()}`,
|
||||||
|
"content-type": `application/json`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).catch((e) => console.log("Keycloak Error: ", e));
|
||||||
|
|
||||||
|
if (!res) return;
|
||||||
|
if (!res.ok) {
|
||||||
|
return console.error("Keycloak Error Response: ", await res.json());
|
||||||
|
}
|
||||||
|
return (await res.json()) as number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create keycloak user by given username and password with roles
|
* Create keycloak user by given username and password with roles
|
||||||
*
|
*
|
||||||
|
|
@ -151,36 +206,42 @@ export async function deleteUser(userId: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get roles list or specific role data
|
* Get roles list or specific role data
|
||||||
*
|
|
||||||
* Client must have permission to get realms roles
|
|
||||||
*
|
|
||||||
* @returns role's info (array if not specify name) if success, null if not found, false otherwise.
|
|
||||||
*/
|
*/
|
||||||
export async function getRoles(name?: string) {
|
export async function listRole() {
|
||||||
const res = await fetch(
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/roles`, {
|
||||||
`${KC_URL}/admin/realms/${KC_REALM}/roles`.concat((name && `/${name}`) || ""),
|
headers: {
|
||||||
{
|
authorization: `Bearer ${await getToken()}`,
|
||||||
// prettier-ignore
|
|
||||||
headers: {
|
|
||||||
"authorization": `Bearer ${await getToken()}`,
|
|
||||||
},
|
},
|
||||||
},
|
}).catch((e) => console.log(e));
|
||||||
).catch((e) => console.log(e));
|
|
||||||
|
|
||||||
if (!res) return false;
|
if (!res) return;
|
||||||
if (!res.ok && res.status !== 404) {
|
if (!res.ok && res.status !== 404) {
|
||||||
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
|
return console.error("Keycloak Error Response: ", await res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = (await res.json()) as any[];
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
return data.map((v: Record<string, string>) => ({ id: v.id, name: v.name }));
|
||||||
return data.map((v: Record<string, string>) => ({ id: v.id, name: v.name }));
|
}
|
||||||
|
|
||||||
|
export async function getRoleByName(name: string) {
|
||||||
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/roles`.concat(`/${name}`), {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${await getToken()}`,
|
||||||
|
},
|
||||||
|
}).catch((e) => console.log(e));
|
||||||
|
|
||||||
|
if (!res) return;
|
||||||
|
if (!res.ok && res.status !== 404) {
|
||||||
|
return console.error("Keycloak Error Response: ", await res.json());
|
||||||
}
|
}
|
||||||
|
if (res.status === 404) return null;
|
||||||
|
|
||||||
|
const data = (await res.json()) as any;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
|
|
@ -285,7 +346,7 @@ export async function removeUserRoles(userId: string, roles: { id: string; name:
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createUser,
|
createUser,
|
||||||
getRoles,
|
listRole,
|
||||||
addUserRoles,
|
addUserRoles,
|
||||||
removeUserRoles,
|
removeUserRoles,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,15 @@ export async function listObjectVersion(bucket: string, obj: string) {
|
||||||
export async function deleteObjectAllVersion(bucket: string, obj: string) {
|
export async function deleteObjectAllVersion(bucket: string, obj: string) {
|
||||||
const item = await listObjectVersion(bucket, obj);
|
const item = await listObjectVersion(bucket, obj);
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise(async (resolve, reject) => {
|
||||||
minio.removeObjects(
|
await minio
|
||||||
bucket,
|
.removeObjects(
|
||||||
// @ts-ignore
|
bucket,
|
||||||
item.map(({ name, versionId }) => ({ name, versionId })), // type error (ts not support) - expected "string[]"
|
item.map(({ name, versionId }) => ({ name, versionId })),
|
||||||
(e) => (e && reject(e)) || resolve(true),
|
)
|
||||||
);
|
.catch((e) => reject(e));
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
"specVersion": 3,
|
"specVersion": 3,
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
"keycloak": {
|
"keycloak": {
|
||||||
"type": "apiKey",
|
"type": "http",
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
"description": "Keycloak Bearer Token",
|
"description": "Keycloak Bearer Token",
|
||||||
"in": "header"
|
"scheme": "bearer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
|
|
@ -33,7 +33,8 @@
|
||||||
{ "name": "Product Type" },
|
{ "name": "Product Type" },
|
||||||
{ "name": "Product" },
|
{ "name": "Product" },
|
||||||
{ "name": "Work" },
|
{ "name": "Work" },
|
||||||
{ "name": "Service" }
|
{ "name": "Service" },
|
||||||
|
{ "name": "Quotation" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue