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": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.12.2",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/swagger-ui-express": "^4.1.6",
|
||||
"nodemon": "^3.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.16.0",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.2",
|
||||
"prisma": "^5.16.1",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.3"
|
||||
"typescript": "^5.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.13.0",
|
||||
"@prisma/client": "^5.16.0",
|
||||
"@tsoa/runtime": "^6.2.0",
|
||||
"@elastic/elasticsearch": "^8.14.0",
|
||||
"@prisma/client": "^5.16.1",
|
||||
"@tsoa/runtime": "^6.3.0",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"fast-jwt": "^4.0.0",
|
||||
"fast-jwt": "^4.0.1",
|
||||
"kysely": "^0.27.3",
|
||||
"minio": "^7.1.3",
|
||||
"minio": "^8.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"promise.any": "^2.0.6",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"tsoa": "^6.2.0"
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"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?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
|
||||
|
|
@ -45,7 +45,7 @@ model RoleMenuPermission {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ model UserMenuPermission {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ model MenuComponent {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
roleMenuComponentPermission RoleMenuComponentPermission[]
|
||||
userMennuComponentPermission UserMenuComponentPermission[]
|
||||
|
|
@ -94,7 +94,7 @@ model RoleMenuComponentPermission {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ model UserMenuComponentPermission {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ model Province {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
district District[]
|
||||
|
|
@ -148,7 +148,7 @@ model District {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
subDistrict SubDistrict[]
|
||||
|
|
@ -169,7 +169,7 @@ model SubDistrict {
|
|||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
branch Branch[]
|
||||
|
|
@ -217,17 +217,38 @@ model Branch {
|
|||
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
|
||||
headOfficeId String?
|
||||
|
||||
bank BranchBank[]
|
||||
|
||||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "BranchCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "BranchUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
branch Branch[] @relation(name: "HeadOfficeRelation")
|
||||
contact BranchContact[]
|
||||
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 {
|
||||
|
|
@ -237,10 +258,12 @@ model BranchContact {
|
|||
branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
|
||||
branchId String
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "BranchContactCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "BranchContactUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
}
|
||||
|
||||
model BranchUser {
|
||||
|
|
@ -252,10 +275,12 @@ model BranchUser {
|
|||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "BranchUserCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "BranchUserUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
}
|
||||
|
||||
enum UserType {
|
||||
|
|
@ -268,13 +293,16 @@ enum UserType {
|
|||
model User {
|
||||
id String @id @default(uuid())
|
||||
|
||||
code String?
|
||||
firstName String
|
||||
firstNameEN String
|
||||
lastName String
|
||||
lastNameEN String
|
||||
username String
|
||||
gender String
|
||||
code String?
|
||||
namePrefix String?
|
||||
firstName String
|
||||
firstNameEN String
|
||||
middleName String?
|
||||
middleNameEN String?
|
||||
lastName String
|
||||
lastNameEN String
|
||||
username String
|
||||
gender String
|
||||
|
||||
address String
|
||||
addressEN String
|
||||
|
|
@ -321,15 +349,52 @@ model User {
|
|||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "UserCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "UserUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
branch BranchUser[]
|
||||
userMenuPermission UserMenuPermission[]
|
||||
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 {
|
||||
|
|
@ -350,12 +415,18 @@ model Customer {
|
|||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
registeredBranchId String?
|
||||
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
|
||||
|
||||
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 {
|
||||
|
|
@ -405,19 +476,22 @@ model CustomerBranch {
|
|||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "CustomerBranchCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "CustomerBranchUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
employee Employee[]
|
||||
employee Employee[]
|
||||
quotation Quotation[]
|
||||
}
|
||||
|
||||
model Employee {
|
||||
id String @id @default(uuid())
|
||||
|
||||
code String
|
||||
nrcNo String
|
||||
nrcNo String?
|
||||
firstName String
|
||||
firstNameEN String
|
||||
lastName String
|
||||
|
|
@ -465,16 +539,19 @@ model Employee {
|
|||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "EmployeeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "EmployeeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
employeeCheckup EmployeeCheckup[]
|
||||
employeeWork EmployeeWork[]
|
||||
employeeOtherInfo EmployeeOtherInfo[]
|
||||
employeeOtherInfo EmployeeOtherInfo?
|
||||
|
||||
editHistory EmployeeHistory[]
|
||||
editHistory EmployeeHistory[]
|
||||
quotationWorker QuotationWorker[]
|
||||
}
|
||||
|
||||
model EmployeeHistory {
|
||||
|
|
@ -483,12 +560,9 @@ model EmployeeHistory {
|
|||
valueBefore Json
|
||||
valueAfter Json
|
||||
|
||||
timestamp DateTime @default(now())
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "EmployeeHistoryUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
updatedByUser User? @relation(fields: [updatedByUserId], references: [id])
|
||||
updatedBy String?
|
||||
updatedAt DateTime @default(now())
|
||||
|
||||
masterId String
|
||||
master Employee @relation(fields: [masterId], references: [id], onDelete: Cascade)
|
||||
|
|
@ -513,10 +587,12 @@ model EmployeeCheckup {
|
|||
coverageStartDate DateTime?
|
||||
coverageExpireDate DateTime?
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "EmployeeCheckupCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "EmployeeCheckupUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
}
|
||||
|
||||
model EmployeeWork {
|
||||
|
|
@ -535,17 +611,19 @@ model EmployeeWork {
|
|||
workEndDate DateTime?
|
||||
remark String?
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "EmployeeWorkCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "EmployeeWorkUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
}
|
||||
|
||||
model EmployeeOtherInfo {
|
||||
id String @id @default(uuid())
|
||||
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employeeId String @unique
|
||||
|
||||
citizenId String?
|
||||
fatherBirthPlace String?
|
||||
|
|
@ -560,65 +638,12 @@ model EmployeeOtherInfo {
|
|||
motherFirstNameEN String?
|
||||
motherLastNameEN String?
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
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])
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "EmployeeOtherInfoCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "EmployeeOtherInfoUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
}
|
||||
|
||||
model ProductGroup {
|
||||
|
|
@ -632,10 +657,12 @@ model ProductGroup {
|
|||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "ProductGroupCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "ProductGroupUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
type ProductType[]
|
||||
}
|
||||
|
|
@ -651,15 +678,18 @@ model ProductType {
|
|||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "ProductTypeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
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)
|
||||
productGroupId String
|
||||
|
||||
product Product[]
|
||||
service Service[]
|
||||
}
|
||||
|
||||
model Product {
|
||||
|
|
@ -681,10 +711,199 @@ model Product {
|
|||
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
|
||||
productTypeId String?
|
||||
|
||||
workProduct WorkProduct[]
|
||||
registeredBranchId String?
|
||||
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedBy String?
|
||||
updatedAt DateTime @updatedAt
|
||||
workProduct WorkProduct[]
|
||||
quotationServiceWorkProduct QuotationServiceWorkProduct[]
|
||||
|
||||
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 swaggerDocument from "./swagger.json";
|
||||
import error from "./middlewares/error";
|
||||
import morgan from "./middlewares/morgan";
|
||||
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_PORT = +(process.env.APP_PORT || 3000);
|
||||
|
|
@ -13,11 +15,64 @@ const APP_PORT = +(process.env.APP_PORT || 3000);
|
|||
(async () => {
|
||||
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(json());
|
||||
app.use(urlencoded({ extended: true }));
|
||||
|
||||
app.use(logMiddleware);
|
||||
app.use(morgan);
|
||||
|
||||
app.use("/", express.static("static"));
|
||||
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([
|
||||
prisma.branchContact.findMany({
|
||||
include: { createdBy: true, updatedBy: true },
|
||||
orderBy: { createdAt: "asc" },
|
||||
where: { branchId },
|
||||
take: pageSize,
|
||||
|
|
@ -79,7 +80,11 @@ export class BranchContactController extends Controller {
|
|||
throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq");
|
||||
}
|
||||
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);
|
||||
|
|
@ -107,7 +112,8 @@ export class BranchContactController extends Controller {
|
|||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import prisma from "../db";
|
|||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import minio from "../services/minio";
|
||||
import minio, { presignedGetObjectIfExist } from "../services/minio";
|
||||
|
||||
if (!process.env.MINIO_BUCKET) {
|
||||
throw Error("Require MinIO bucket.");
|
||||
|
|
@ -28,6 +28,7 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET;
|
|||
|
||||
type BranchCreate = {
|
||||
status?: Status;
|
||||
code: string;
|
||||
taxNo: string;
|
||||
nameEN: string;
|
||||
name: string;
|
||||
|
|
@ -42,6 +43,15 @@ type BranchCreate = {
|
|||
longitude: string;
|
||||
latitude: string;
|
||||
|
||||
bank?: {
|
||||
bankName: string;
|
||||
bankBranch: string;
|
||||
accountName: string;
|
||||
accountNumber: string;
|
||||
accountType: string;
|
||||
currentlyUse: boolean;
|
||||
}[];
|
||||
|
||||
subDistrictId?: string | null;
|
||||
districtId?: string | null;
|
||||
provinceId?: string | null;
|
||||
|
|
@ -68,6 +78,15 @@ type BranchUpdate = {
|
|||
districtId?: string | null;
|
||||
provinceId?: string | null;
|
||||
headOfficeId?: string | null;
|
||||
|
||||
bank?: {
|
||||
bankName: string;
|
||||
bankBranch: string;
|
||||
accountName: string;
|
||||
accountNumber: string;
|
||||
accountType: string;
|
||||
currentlyUse: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
function lineImageLoc(id: string) {
|
||||
|
|
@ -80,9 +99,9 @@ function branchImageLoc(id: string) {
|
|||
|
||||
@Route("api/v1/branch")
|
||||
@Tags("Branch")
|
||||
@Security("keycloak")
|
||||
export class BranchController extends Controller {
|
||||
@Get("stats")
|
||||
@Security("keycloak")
|
||||
async getStats() {
|
||||
const list = await prisma.branch.groupBy({
|
||||
_count: true,
|
||||
|
|
@ -99,14 +118,27 @@ export class BranchController extends Controller {
|
|||
}
|
||||
|
||||
@Get("user-stats")
|
||||
async getUserStat(@Query() userType?: UserType) {
|
||||
@Security("keycloak")
|
||||
async getUserStat(@Request() req: RequestWithUser, @Query() userType?: UserType) {
|
||||
const list = await prisma.branchUser.groupBy({
|
||||
_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",
|
||||
});
|
||||
|
||||
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: {
|
||||
id: true,
|
||||
headOfficeId: true,
|
||||
|
|
@ -136,6 +168,7 @@ export class BranchController extends Controller {
|
|||
}
|
||||
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async getBranch(
|
||||
@Query() zipCode?: string,
|
||||
@Query() filter?: "head" | "sub",
|
||||
|
|
@ -173,6 +206,12 @@ export class BranchController extends Controller {
|
|||
subDistrict: true,
|
||||
},
|
||||
},
|
||||
bank: true,
|
||||
_count: {
|
||||
select: { branch: true },
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -185,6 +224,7 @@ export class BranchController extends Controller {
|
|||
}
|
||||
|
||||
@Get("{branchId}")
|
||||
@Security("keycloak")
|
||||
async getBranchById(
|
||||
@Path() branchId: string,
|
||||
@Query() includeSubBranch?: boolean,
|
||||
|
|
@ -195,6 +235,8 @@ export class BranchController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
branch: includeSubBranch && {
|
||||
include: {
|
||||
province: true,
|
||||
|
|
@ -202,6 +244,7 @@ export class BranchController extends Controller {
|
|||
subDistrict: true,
|
||||
},
|
||||
},
|
||||
bank: true,
|
||||
contact: includeContact,
|
||||
},
|
||||
where: { id: branchId },
|
||||
|
|
@ -218,6 +261,7 @@ export class BranchController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin"])
|
||||
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {
|
||||
const [province, district, subDistrict, head] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||
|
|
@ -250,44 +294,70 @@ export class BranchController extends Controller {
|
|||
"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(
|
||||
async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: !headOfficeId ? `HQ${year.toString().slice(2)}` : `BR${head?.code.slice(2, 5)}`,
|
||||
key: `MAIN_BRANCH_${code}`,
|
||||
},
|
||||
create: {
|
||||
key: !headOfficeId ? `HQ${year.toString().slice(2)}` : `BR${head?.code.slice(2, 5)}`,
|
||||
key: `MAIN_BRANCH_${code}`,
|
||||
value: 1,
|
||||
},
|
||||
update: { value: { increment: 1 } },
|
||||
});
|
||||
|
||||
const code = !headOfficeId
|
||||
? `HQ${year.toString().slice(2)}${last.value}`
|
||||
: `BR${head?.code.slice(2, 5)}${last.value.toString().padStart(2, "0")}`;
|
||||
if (last.value === 1) {
|
||||
const exist = await tx.branch.findFirst({
|
||||
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({
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
code,
|
||||
code: `${code?.toLocaleUpperCase()}${`${last.value - 1}`.padStart(6, "0")}`,
|
||||
bank: bank ? { createMany: { data: bank } } : undefined,
|
||||
isHeadOffice: !headOfficeId,
|
||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||
district: { connect: districtId ? { id: districtId } : undefined },
|
||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||
headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -315,12 +385,12 @@ export class BranchController extends Controller {
|
|||
return Object.assign(record, {
|
||||
contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }),
|
||||
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
||||
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
||||
qrCodeImageUrl: await minio.presignedGetObject(
|
||||
imageUploadUrl: await minio.presignedPutObject(
|
||||
MINIO_BUCKET,
|
||||
lineImageLoc(record.id),
|
||||
branchImageLoc(record.id),
|
||||
12 * 60 * 60,
|
||||
),
|
||||
qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)),
|
||||
qrCodeImageUploadUrl: await minio.presignedPutObject(
|
||||
MINIO_BUCKET,
|
||||
lineImageLoc(record.id),
|
||||
|
|
@ -330,6 +400,7 @@ export class BranchController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{branchId}")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||
async editBranch(
|
||||
@Request() req: RequestWithUser,
|
||||
@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");
|
||||
}
|
||||
|
||||
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({
|
||||
include: { province: true, district: true, subDistrict: true },
|
||||
data: {
|
||||
...rest,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined,
|
||||
bank: bank ? { deleteMany: {}, createMany: { data: bank } } : undefined,
|
||||
province: {
|
||||
connect: provinceId ? { id: provinceId } : undefined,
|
||||
disconnect: provinceId === null || undefined,
|
||||
|
|
@ -403,7 +493,7 @@ export class BranchController extends Controller {
|
|||
connect: headOfficeId ? { id: headOfficeId } : undefined,
|
||||
disconnect: headOfficeId === null || undefined,
|
||||
},
|
||||
updatedBy: req.user.name,
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
where: { id: branchId },
|
||||
});
|
||||
|
|
@ -421,12 +511,12 @@ export class BranchController extends Controller {
|
|||
|
||||
return Object.assign(record, {
|
||||
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
||||
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
|
||||
qrCodeImageUrl: await minio.presignedGetObject(
|
||||
imageUploadUrl: await minio.presignedPutObject(
|
||||
MINIO_BUCKET,
|
||||
lineImageLoc(record.id),
|
||||
branchImageLoc(record.id),
|
||||
12 * 60 * 60,
|
||||
),
|
||||
qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)),
|
||||
qrCodeImageUploadUrl: await minio.presignedPutObject(
|
||||
MINIO_BUCKET,
|
||||
lineImageLoc(record.id),
|
||||
|
|
@ -436,16 +526,33 @@ export class BranchController extends Controller {
|
|||
}
|
||||
|
||||
@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({
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
user: { where: { userId: req.user.sub } },
|
||||
},
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
|
||||
await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), {
|
||||
forceDelete: true,
|
||||
});
|
||||
await minio.removeObject(MINIO_BUCKET, branchImageLoc(branchId), {
|
||||
forceDelete: true,
|
||||
});
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const data = await tx.branch.delete({
|
||||
include: {
|
||||
province: 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: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
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, 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 {
|
||||
Body,
|
||||
Controller,
|
||||
|
|
@ -28,21 +28,28 @@ async function userBranchCodeGen(branch: Branch, user: User[]) {
|
|||
|
||||
const typ = usr.userType;
|
||||
|
||||
const mapTypeNo = {
|
||||
USER: 1,
|
||||
MESSENGER: 2,
|
||||
DELEGATE: 3,
|
||||
AGENCY: 4,
|
||||
}[typ];
|
||||
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`,
|
||||
key: `BR_USR_${branch.code}_${mapTypeNo}`,
|
||||
},
|
||||
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,
|
||||
},
|
||||
update: { value: { increment: 1 } },
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
await tx.user.update({
|
||||
where: { id: usr.id },
|
||||
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")
|
||||
@Tags("Branch User")
|
||||
@Security("keycloak")
|
||||
export class BranchUserController extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async getBranchUser(
|
||||
@Path() branchId: string,
|
||||
@Query() zipCode?: string,
|
||||
|
|
@ -97,6 +121,7 @@ export class BranchUserController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||
async createBranchUser(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() branchId: string,
|
||||
|
|
@ -104,6 +129,11 @@ export class BranchUserController extends Controller {
|
|||
) {
|
||||
const [branch, user] = await prisma.$transaction([
|
||||
prisma.branch.findUnique({
|
||||
include: {
|
||||
user: {
|
||||
where: { userId: req.user.sub },
|
||||
},
|
||||
},
|
||||
where: { id: branchId },
|
||||
}),
|
||||
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(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Branch cannot be found.",
|
||||
"branchBadReq",
|
||||
HttpStatus.FORBIDDEN,
|
||||
"You do not have permission to perform this action.",
|
||||
"noPermission",
|
||||
);
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq");
|
||||
}
|
||||
|
||||
if (user.length !== body.user.length) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
|
|
@ -139,8 +177,8 @@ export class BranchUserController extends Controller {
|
|||
.map((v) => ({
|
||||
branchId,
|
||||
userId: v.id,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
|
|
@ -249,8 +287,8 @@ export class UserBranchController extends Controller {
|
|||
.map((v) => ({
|
||||
branchId: v.id,
|
||||
userId,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
})),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@ if (!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) {
|
||||
return `employee/profile-img-${id}`;
|
||||
|
|
@ -40,6 +49,7 @@ export type CustomerBranchCreate = {
|
|||
|
||||
legalPersonNo: string;
|
||||
|
||||
branchNo: number;
|
||||
taxNo: string | null;
|
||||
name: string;
|
||||
nameEN: string;
|
||||
|
|
@ -105,9 +115,9 @@ export type CustomerBranchUpdate = {
|
|||
|
||||
@Route("api/v1/customer-branch")
|
||||
@Tags("Customer Branch")
|
||||
@Security("keycloak")
|
||||
export class CustomerBranchController extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async list(
|
||||
@Query() zipCode?: string,
|
||||
@Query() customerId?: string,
|
||||
|
|
@ -158,6 +168,8 @@ export class CustomerBranchController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
_count: true,
|
||||
},
|
||||
where,
|
||||
|
|
@ -171,12 +183,15 @@ export class CustomerBranchController extends Controller {
|
|||
}
|
||||
|
||||
@Get("{branchId}")
|
||||
@Security("keycloak")
|
||||
async getById(@Path() branchId: string) {
|
||||
const record = await prisma.customerBranch.findFirst({
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: branchId },
|
||||
});
|
||||
|
|
@ -189,6 +204,7 @@ export class CustomerBranchController extends Controller {
|
|||
}
|
||||
|
||||
@Get("{branchId}/employee")
|
||||
@Security("keycloak")
|
||||
async listEmployee(
|
||||
@Path() branchId: string,
|
||||
@Query() zipCode?: string,
|
||||
|
|
@ -213,6 +229,8 @@ export class CustomerBranchController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -239,6 +257,7 @@ export class CustomerBranchController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) {
|
||||
const [province, district, subDistrict, customer] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||
|
|
@ -275,8 +294,26 @@ export class CustomerBranchController extends Controller {
|
|||
|
||||
const record = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const count = await tx.customerBranch.count({
|
||||
where: { customerId },
|
||||
const conflict = await tx.customerBranch.findFirst({
|
||||
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({
|
||||
|
|
@ -284,35 +321,32 @@ export class CustomerBranchController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
branchNo: count + 1,
|
||||
code: `${customer.code}-${(count + 1).toString().padStart(2, "0")}`,
|
||||
code: `${customer.code.slice(0, -6)}${`${last.value - 1}`.padStart(6, "0")}`,
|
||||
customer: { connect: { id: customerId } },
|
||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||
district: { connect: districtId ? { id: districtId } : undefined },
|
||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
},
|
||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||
);
|
||||
|
||||
await prisma.customer.updateMany({
|
||||
where: { id: customerId, status: Status.CREATED },
|
||||
data: { status: Status.ACTIVE },
|
||||
});
|
||||
|
||||
this.setStatus(HttpStatus.CREATED);
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
@Put("{branchId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: CustomerBranchUpdate,
|
||||
|
|
@ -363,6 +397,8 @@ export class CustomerBranchController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
|
|
@ -380,8 +416,7 @@ export class CustomerBranchController extends Controller {
|
|||
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
||||
disconnect: subDistrictId === null || undefined,
|
||||
},
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -391,6 +426,7 @@ export class CustomerBranchController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{branchId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async delete(@Path() branchId: string) {
|
||||
const record = await prisma.customerBranch.findFirst({
|
||||
where: { id: branchId },
|
||||
|
|
@ -412,27 +448,30 @@ export class CustomerBranchController extends Controller {
|
|||
);
|
||||
}
|
||||
|
||||
return await prisma.customerBranch.delete({ where: { id: branchId } }).then((v) => {
|
||||
new Promise<string[]>((resolve, reject) => {
|
||||
const item: string[] = [];
|
||||
return await prisma.customerBranch
|
||||
.delete({
|
||||
include: { createdBy: true, updatedBy: true },
|
||||
where: { id: branchId },
|
||||
})
|
||||
.then((v) => {
|
||||
new Promise<string[]>((resolve, reject) => {
|
||||
const item: string[] = [];
|
||||
|
||||
const stream = minio.listObjectsV2(
|
||||
MINIO_BUCKET,
|
||||
`${attachmentLocation(record.customerId, branchId)}/`,
|
||||
);
|
||||
const stream = minio.listObjectsV2(
|
||||
MINIO_BUCKET,
|
||||
`${attachmentLocation(record.customerId, branchId)}/`,
|
||||
);
|
||||
|
||||
stream.on("data", (v) => v && v.name && item.push(v.name));
|
||||
stream.on("end", () => resolve(item));
|
||||
stream.on("error", () => reject(new Error("MinIO error.")));
|
||||
}).then((list) => {
|
||||
list.map(async (v) => {
|
||||
await minio.removeObject(MINIO_BUCKET, v, {
|
||||
forceDelete: true,
|
||||
stream.on("data", (v) => v && v.name && item.push(v.name));
|
||||
stream.on("end", () => resolve(item));
|
||||
stream.on("error", () => reject(new Error("MinIO error.")));
|
||||
}).then((list) => {
|
||||
list.map(async (v) => {
|
||||
await minio.removeObject(MINIO_BUCKET, v, { forceDelete: true });
|
||||
});
|
||||
});
|
||||
return v;
|
||||
});
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,21 @@ if (!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 = {
|
||||
registeredBranchId?: string;
|
||||
|
||||
code: string;
|
||||
|
||||
status?: Status;
|
||||
personName: string;
|
||||
personNameEN?: string;
|
||||
|
|
@ -38,6 +51,7 @@ export type CustomerCreate = {
|
|||
|
||||
legalPersonNo: string;
|
||||
|
||||
branchNo: number;
|
||||
taxNo: string | null;
|
||||
name: string;
|
||||
nameEN: string;
|
||||
|
|
@ -68,6 +82,8 @@ export type CustomerCreate = {
|
|||
};
|
||||
|
||||
export type CustomerUpdate = {
|
||||
registeredBranchId?: string;
|
||||
|
||||
status?: "ACTIVE" | "INACTIVE";
|
||||
personName?: string;
|
||||
personNameEN?: string;
|
||||
|
|
@ -82,6 +98,7 @@ export type CustomerUpdate = {
|
|||
|
||||
legalPersonNo: string;
|
||||
|
||||
branchNo: number;
|
||||
taxNo: string | null;
|
||||
name: string;
|
||||
nameEN: string;
|
||||
|
|
@ -117,9 +134,9 @@ function imageLocation(id: string) {
|
|||
|
||||
@Route("api/v1/customer")
|
||||
@Tags("Customer")
|
||||
@Security("keycloak")
|
||||
export class CustomerController extends Controller {
|
||||
@Get("type-stats")
|
||||
@Security("keycloak")
|
||||
async stat() {
|
||||
const list = await prisma.customer.groupBy({
|
||||
by: "customerType",
|
||||
|
|
@ -139,6 +156,7 @@ export class CustomerController extends Controller {
|
|||
}
|
||||
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async list(
|
||||
@Query() customerType?: CustomerType,
|
||||
@Query() query: string = "",
|
||||
|
|
@ -172,8 +190,13 @@ export class CustomerController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
orderBy: {
|
||||
branchNo: "asc",
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
where,
|
||||
|
|
@ -201,6 +224,7 @@ export class CustomerController extends Controller {
|
|||
}
|
||||
|
||||
@Get("{customerId}")
|
||||
@Security("keycloak")
|
||||
async getById(@Path() customerId: string) {
|
||||
const record = await prisma.customer.findFirst({
|
||||
include: {
|
||||
|
|
@ -210,7 +234,10 @@ export class CustomerController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
orderBy: { branchNo: "asc" },
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: customerId },
|
||||
});
|
||||
|
|
@ -226,6 +253,7 @@ export class CustomerController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
|
||||
const { customerBranch, ...payload } = body;
|
||||
|
||||
|
|
@ -243,10 +271,11 @@ export class CustomerController extends Controller {
|
|||
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.district.findMany({ where: { id: { in: districtId } } }),
|
||||
prisma.subDistrict.findMany({ where: { id: { in: subDistrictId } } }),
|
||||
prisma.branch.findFirst({ where: { id: body.registeredBranchId } }),
|
||||
]);
|
||||
|
||||
if (provinceId && province.length !== provinceId?.length) {
|
||||
|
|
@ -270,20 +299,44 @@ export class CustomerController extends Controller {
|
|||
"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(
|
||||
async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `CUSTOMER_${body.customerType}`,
|
||||
key: `CUSTOMER_${body.code.toLocaleUpperCase()}`,
|
||||
},
|
||||
create: {
|
||||
key: `CUSTOMER_${body.customerType}`,
|
||||
value: 1,
|
||||
key: `CUSTOMER_${body.code.toLocaleUpperCase()}`,
|
||||
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({
|
||||
include: {
|
||||
branch: {
|
||||
|
|
@ -293,25 +346,26 @@ export class CustomerController extends Controller {
|
|||
subDistrict: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...payload,
|
||||
statusOrder: +(payload.status === "INACTIVE"),
|
||||
code: `${last.key.slice(9)}${last.value.toString().padStart(6, "0")}`,
|
||||
code: `${body.code}000000`,
|
||||
branch: {
|
||||
createMany: {
|
||||
data:
|
||||
customerBranch?.map((v, i) => ({
|
||||
...v,
|
||||
branchNo: i + 1,
|
||||
code: `${last.key.slice(9)}${last.value.toString().padStart(6, "0")}-${(i + 1).toString().padStart(2, "0")}`,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
code: `${body.code}${`${last.value - customerBranch?.length + i + 1}`.padStart(6, "0")}`,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -335,6 +389,7 @@ export class CustomerController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{customerId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editById(
|
||||
@Path() customerId: string,
|
||||
@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
|
||||
.update({
|
||||
include: {
|
||||
|
|
@ -417,6 +483,8 @@ export class CustomerController extends Controller {
|
|||
subDistrict: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: customerId },
|
||||
data: {
|
||||
|
|
@ -430,26 +498,25 @@ export class CustomerController extends Controller {
|
|||
},
|
||||
status: Status.CREATED,
|
||||
},
|
||||
upsert: customerBranch.map((v, i) => ({
|
||||
upsert: customerBranch.map((v) => ({
|
||||
where: { id: v.id || "" },
|
||||
create: {
|
||||
...v,
|
||||
branchNo: i + 1,
|
||||
code: `${customer.code}-${(i + 1).toString().padStart(2, "0")}`,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
code: `${customer.code}-${v.branchNo.toString().padStart(2, "0")}`,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
id: undefined,
|
||||
},
|
||||
update: {
|
||||
...v,
|
||||
branchNo: i + 1,
|
||||
code: `${customer.code}-${(i + 1).toString().padStart(2, "0")}`,
|
||||
updatedBy: req.user.name,
|
||||
code: undefined,
|
||||
branchNo: undefined,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
})),
|
||||
}) ||
|
||||
undefined,
|
||||
updatedBy: req.user.name,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
})
|
||||
.then((v) => {
|
||||
|
|
@ -492,6 +559,7 @@ export class CustomerController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{customerId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async deleteById(@Path() customerId: string) {
|
||||
const record = await prisma.customer.findFirst({ where: { id: customerId } });
|
||||
|
||||
|
|
@ -522,4 +590,33 @@ export class CustomerController extends Controller {
|
|||
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 HttpError from "../interfaces/http-error";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
type EmployeeCheckupPayload = {
|
||||
checkupType?: string | null;
|
||||
checkupResult?: string | null;
|
||||
|
|
@ -32,19 +42,28 @@ type EmployeeCheckupPayload = {
|
|||
|
||||
@Route("api/v1/employee/{employeeId}/checkup")
|
||||
@Tags("Employee Checkup")
|
||||
@Security("keycloak")
|
||||
export class EmployeeCheckupController extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async list(@Path() employeeId: string) {
|
||||
return prisma.employeeCheckup.findMany({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
where: { employeeId },
|
||||
});
|
||||
}
|
||||
|
||||
@Get("{checkupId}")
|
||||
@Security("keycloak")
|
||||
async getById(@Path() employeeId: string, @Path() checkupId: string) {
|
||||
const record = await prisma.employeeCheckup.findFirst({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: checkupId, employeeId },
|
||||
});
|
||||
if (!record) {
|
||||
|
|
@ -58,6 +77,7 @@ export class EmployeeCheckupController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async create(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() employeeId: string,
|
||||
|
|
@ -85,13 +105,13 @@ export class EmployeeCheckupController extends Controller {
|
|||
const { provinceId, ...rest } = body;
|
||||
|
||||
const record = await prisma.employeeCheckup.create({
|
||||
include: { province: true },
|
||||
include: { province: true, createdBy: true, updatedBy: true },
|
||||
data: {
|
||||
...rest,
|
||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||
employee: { connect: { id: employeeId } },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -101,6 +121,7 @@ export class EmployeeCheckupController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{checkupId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() employeeId: string,
|
||||
|
|
@ -128,7 +149,12 @@ export class EmployeeCheckupController extends Controller {
|
|||
|
||||
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(
|
||||
HttpStatus.NOT_FOUND,
|
||||
"Employee checkup cannot be found.",
|
||||
|
|
@ -137,13 +163,12 @@ export class EmployeeCheckupController extends Controller {
|
|||
}
|
||||
|
||||
const record = await prisma.employeeCheckup.update({
|
||||
include: { province: true },
|
||||
include: { province: true, createdBy: true, updatedBy: true },
|
||||
where: { id: checkupId, employeeId },
|
||||
data: {
|
||||
...rest,
|
||||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -153,6 +178,7 @@ export class EmployeeCheckupController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{checkupId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async deleteById(@Path() employeeId: string, @Path() checkupId: string) {
|
||||
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 MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function imageLocation(id: string) {
|
||||
return `employee/${id}/profile-image`;
|
||||
|
|
@ -34,7 +43,7 @@ type EmployeeCreate = {
|
|||
|
||||
status?: Status;
|
||||
|
||||
nrcNo: string;
|
||||
nrcNo?: string;
|
||||
|
||||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
|
|
@ -200,9 +209,9 @@ type EmployeeUpdate = {
|
|||
|
||||
@Route("api/v1/employee")
|
||||
@Tags("Employee")
|
||||
@Security("keycloak")
|
||||
export class EmployeeController extends Controller {
|
||||
@Get("stats")
|
||||
@Security("keycloak")
|
||||
async getEmployeeStats(@Query() customerBranchId?: string) {
|
||||
return await prisma.employee.count({
|
||||
where: { customerBranchId },
|
||||
|
|
@ -210,12 +219,32 @@ export class EmployeeController extends Controller {
|
|||
}
|
||||
|
||||
@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
|
||||
.groupBy({
|
||||
_count: true,
|
||||
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) =>
|
||||
res.reduce<Record<string, number>>((a, c) => {
|
||||
|
|
@ -226,6 +255,7 @@ export class EmployeeController extends Controller {
|
|||
}
|
||||
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async list(
|
||||
@Query() zipCode?: string,
|
||||
@Query() gender?: string,
|
||||
|
|
@ -258,6 +288,11 @@ export class EmployeeController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
customerBranch: {
|
||||
include: { customer: true },
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -284,12 +319,18 @@ export class EmployeeController extends Controller {
|
|||
}
|
||||
|
||||
@Get("{employeeId}")
|
||||
@Security("keycloak")
|
||||
async getById(@Path() employeeId: string) {
|
||||
const record = await prisma.employee.findFirst({
|
||||
include: {
|
||||
employeeWork: true,
|
||||
employeeCheckup: true,
|
||||
employeeOtherInfo: true,
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: employeeId },
|
||||
});
|
||||
|
|
@ -298,10 +339,17 @@ export class EmployeeController extends Controller {
|
|||
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()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) {
|
||||
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||
|
|
@ -371,10 +419,10 @@ export class EmployeeController extends Controller {
|
|||
async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
|
||||
key: `EMPLOYEE_${customerBranch.customer.code.slice(0, -6)}${`${new Date().getFullYear()}`.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")}`,
|
||||
key: `EMPLOYEE_${customerBranch.customer.code.slice(0, -6)}${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
|
||||
value: 1,
|
||||
},
|
||||
update: { value: { increment: 1 } },
|
||||
|
|
@ -392,11 +440,13 @@ export class EmployeeController extends Controller {
|
|||
},
|
||||
},
|
||||
employeeWork: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
code: `${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}${last.value.toString().padStart(4, "0")}`,
|
||||
code: `${customerBranch.customer.code.slice(0, -6)}${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${last.value}`.padStart(7, "0")}`,
|
||||
employeeWork: {
|
||||
createMany: {
|
||||
data: employeeWork || [],
|
||||
|
|
@ -418,8 +468,8 @@ export class EmployeeController extends Controller {
|
|||
district: { connect: districtId ? { id: districtId } : undefined },
|
||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||
customerBranch: { connect: { id: customerBranchId } },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -458,6 +508,7 @@ export class EmployeeController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{employeeId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: EmployeeUpdate,
|
||||
|
|
@ -534,7 +585,11 @@ export class EmployeeController extends Controller {
|
|||
const record = await prisma.$transaction(async (tx) => {
|
||||
let code: string | undefined;
|
||||
|
||||
if (customerBranch && customerBranch.id !== employee.customerBranchId) {
|
||||
if (
|
||||
customerBranchId !== undefined &&
|
||||
customerBranch &&
|
||||
customerBranch.id !== employee.customerBranchId
|
||||
) {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`,
|
||||
|
|
@ -561,6 +616,8 @@ export class EmployeeController extends Controller {
|
|||
},
|
||||
},
|
||||
employeeWork: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
|
|
@ -578,13 +635,13 @@ export class EmployeeController extends Controller {
|
|||
where: { id: v.id || "" },
|
||||
create: {
|
||||
...v,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
id: undefined,
|
||||
},
|
||||
update: {
|
||||
...v,
|
||||
updatedBy: req.user.name,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
})),
|
||||
}
|
||||
|
|
@ -602,21 +659,20 @@ export class EmployeeController extends Controller {
|
|||
create: {
|
||||
...v,
|
||||
provinceId: !v.provinceId ? undefined : v.provinceId,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
id: undefined,
|
||||
},
|
||||
update: {
|
||||
...v,
|
||||
updatedBy: req.user.name,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
employeeOtherInfo: employeeOtherInfo
|
||||
? {
|
||||
deleteMany: {},
|
||||
create: employeeOtherInfo,
|
||||
update: employeeOtherInfo,
|
||||
}
|
||||
: undefined,
|
||||
province: {
|
||||
|
|
@ -631,8 +687,8 @@ export class EmployeeController extends Controller {
|
|||
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
||||
disconnect: subDistrictId === null || undefined,
|
||||
},
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -663,7 +719,6 @@ export class EmployeeController extends Controller {
|
|||
data: historyEntries.map((v) => ({
|
||||
...v,
|
||||
updatedByUserId: req.user.sub,
|
||||
updatedBy: req.user.preferred_username,
|
||||
masterId: employee.id,
|
||||
})),
|
||||
});
|
||||
|
|
@ -683,6 +738,7 @@ export class EmployeeController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{employeeId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async delete(@Path() employeeId: string) {
|
||||
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");
|
||||
}
|
||||
|
||||
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")
|
||||
async editHistory(@Path() employeeId: string) {
|
||||
return await prisma.employeeHistory.findMany({
|
||||
include: {
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { masterId: employeeId },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ import HttpError from "../interfaces/http-error";
|
|||
import HttpStatus from "../interfaces/http-status";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
type EmployeeOtherInfoPayload = {
|
||||
citizenId?: string | null;
|
||||
fatherFirstName?: string | null;
|
||||
|
|
@ -34,35 +44,40 @@ type EmployeeOtherInfoPayload = {
|
|||
|
||||
@Route("api/v1/employee/{employeeId}/other-info")
|
||||
@Tags("Employee Other Info")
|
||||
@Security("keycloak")
|
||||
export class EmployeeOtherInfo extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async list(@Path() employeeId: string) {
|
||||
return prisma.employeeOtherInfo.findFirst({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
where: { employeeId },
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async create(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() employeeId: string,
|
||||
@Body() body: EmployeeOtherInfoPayload,
|
||||
) {
|
||||
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Employee cannot be found.",
|
||||
"employeeBadReq",
|
||||
);
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Employee cannot be found.", "employeeBadReq");
|
||||
|
||||
const record = await prisma.employeeOtherInfo.create({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
employee: { connect: { id: employeeId } },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -72,6 +87,7 @@ export class EmployeeOtherInfo extends Controller {
|
|||
}
|
||||
|
||||
@Put("{otherInfoId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() employeeId: string,
|
||||
|
|
@ -87,8 +103,12 @@ export class EmployeeOtherInfo extends Controller {
|
|||
}
|
||||
|
||||
const record = await prisma.employeeOtherInfo.update({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: otherInfoId, employeeId },
|
||||
data: { ...body, createdBy: req.user.name, updatedBy: req.user.name },
|
||||
data: { ...body, updatedByUserId: req.user.sub },
|
||||
});
|
||||
|
||||
this.setStatus(HttpStatus.CREATED);
|
||||
|
|
@ -97,6 +117,7 @@ export class EmployeeOtherInfo extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{otherInfoId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async deleteById(@Path() employeeId: string, @Path() otherInfoId: string) {
|
||||
const record = await prisma.employeeOtherInfo.findFirst({
|
||||
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 HttpError from "../interfaces/http-error";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
type EmployeeWorkPayload = {
|
||||
ownerName?: string | null;
|
||||
positionName?: string | null;
|
||||
|
|
@ -30,46 +40,60 @@ type EmployeeWorkPayload = {
|
|||
|
||||
@Route("api/v1/employee/{employeeId}/work")
|
||||
@Tags("Employee Work")
|
||||
@Security("keycloak")
|
||||
export class EmployeeWorkController extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async list(@Path() employeeId: string) {
|
||||
return prisma.employeeWork.findMany({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
where: { employeeId },
|
||||
});
|
||||
}
|
||||
|
||||
@Get("{workId}")
|
||||
@Security("keycloak")
|
||||
async getById(@Path() employeeId: string, @Path() workId: string) {
|
||||
const record = await prisma.employeeWork.findFirst({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: workId, employeeId },
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async create(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() employeeId: string,
|
||||
@Body() body: EmployeeWorkPayload,
|
||||
) {
|
||||
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Employee cannot be found.",
|
||||
"employeeBadReq",
|
||||
);
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Employee cannot be found.", "employeeBadReq");
|
||||
|
||||
const record = await prisma.employeeWork.create({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
employee: { connect: { id: employeeId } },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -79,6 +103,7 @@ export class EmployeeWorkController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{workId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() employeeId: string,
|
||||
|
|
@ -86,12 +111,20 @@ export class EmployeeWorkController extends Controller {
|
|||
@Body() body: EmployeeWorkPayload,
|
||||
) {
|
||||
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({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: workId, employeeId },
|
||||
data: { ...body, createdBy: req.user.name, updatedBy: req.user.name },
|
||||
data: { ...body, updatedByUserId: req.user.sub },
|
||||
});
|
||||
|
||||
this.setStatus(HttpStatus.CREATED);
|
||||
|
|
@ -100,11 +133,22 @@ export class EmployeeWorkController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{workId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
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) {
|
||||
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 } });
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
createUser,
|
||||
deleteUser,
|
||||
editUser,
|
||||
getRoles,
|
||||
listRole,
|
||||
removeUserRoles,
|
||||
} from "../services/keycloak";
|
||||
|
||||
|
|
@ -38,18 +38,20 @@ export class KeycloakController extends Controller {
|
|||
|
||||
@Get("role")
|
||||
async getRole() {
|
||||
const role = await getRoles();
|
||||
const role = await listRole();
|
||||
if (Array.isArray(role))
|
||||
return role.filter(
|
||||
(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.");
|
||||
}
|
||||
|
||||
@Post("{userId}/role")
|
||||
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.");
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ export class KeycloakController extends Controller {
|
|||
|
||||
@Delete("{userId}/role/{roleId}")
|
||||
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.");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export class ProductServiceController extends Controller {
|
|||
@Query() productTypeId?: string,
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() registeredBranchId?: string,
|
||||
) {
|
||||
const union = Prisma.sql`
|
||||
SELECT
|
||||
|
|
@ -27,9 +28,10 @@ export class ProductServiceController extends Controller {
|
|||
"status",
|
||||
"statusOrder",
|
||||
"productTypeId",
|
||||
"createdBy",
|
||||
"registeredBranchId",
|
||||
"createdByUserId",
|
||||
"createdAt",
|
||||
"updatedBy",
|
||||
"updatedByUserId",
|
||||
"updatedAt",
|
||||
'product' as "type"
|
||||
FROM "Product"
|
||||
|
|
@ -46,10 +48,11 @@ export class ProductServiceController extends Controller {
|
|||
null as "remark",
|
||||
"status",
|
||||
"statusOrder",
|
||||
null as "productTypeId",
|
||||
"createdBy",
|
||||
"productTypeId",
|
||||
"registeredBranchId",
|
||||
"createdByUserId",
|
||||
"createdAt",
|
||||
"updatedBy",
|
||||
"updatedByUserId",
|
||||
"updatedAt",
|
||||
'service' as "type"
|
||||
FROM "Service"
|
||||
|
|
@ -61,7 +64,12 @@ export class ProductServiceController extends Controller {
|
|||
if (query) or.push(Prisma.sql`"name" LIKE ${`%${query}%`}`);
|
||||
if (status) and.push(Prisma.sql`"status" = ${status}::"Status"`);
|
||||
if (productTypeId) {
|
||||
and.push(Prisma.sql`("productTypeId" = ${productTypeId} OR ("type" = 'service'))`);
|
||||
and.push(Prisma.sql`"productTypeId" = ${productTypeId}`);
|
||||
}
|
||||
if (registeredBranchId) {
|
||||
and.push(
|
||||
Prisma.sql`("registeredBranchId" = ${registeredBranchId} OR "registeredBranchId" IS NULL)`,
|
||||
);
|
||||
}
|
||||
|
||||
const where = Prisma.sql`
|
||||
|
|
@ -70,6 +78,7 @@ export class ProductServiceController extends Controller {
|
|||
${or.length > 0 && and.length > 0 ? Prisma.sql` AND ` : Prisma.empty}
|
||||
${and.length > 0 ? Prisma.join(and, " AND ", "(", ")") : Prisma.empty}
|
||||
`;
|
||||
console.log(where.sql);
|
||||
|
||||
const [result, [{ total }]] = await prisma.$transaction([
|
||||
prisma.$queryRaw<((Product & { type: "product" }) | (Service & { type: "service" }))[]>`
|
||||
|
|
|
|||
|
|
@ -35,14 +35,15 @@ type ProductGroupUpdate = {
|
|||
|
||||
@Route("api/v1/product-group")
|
||||
@Tags("Product Group")
|
||||
@Security("keycloak")
|
||||
export class ProductGroup extends Controller {
|
||||
@Get("stats")
|
||||
@Security("keycloak")
|
||||
async getProductGroupStats() {
|
||||
return await prisma.productGroup.count();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async getProductGroup(
|
||||
@Query() query: string = "",
|
||||
@Query() status?: Status,
|
||||
|
|
@ -72,6 +73,8 @@ export class ProductGroup extends Controller {
|
|||
type: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
where,
|
||||
|
|
@ -81,9 +84,9 @@ export class ProductGroup extends Controller {
|
|||
prisma.productGroup.count({ where }),
|
||||
]);
|
||||
|
||||
const statsProduct = await prisma.productType.findMany({
|
||||
const statsDeep = await prisma.productType.findMany({
|
||||
include: {
|
||||
_count: { select: { product: true } },
|
||||
_count: { select: { product: true, service: true } },
|
||||
},
|
||||
where: {
|
||||
productGroupId: { in: result.map((v) => v.id) },
|
||||
|
|
@ -95,10 +98,14 @@ export class ProductGroup extends Controller {
|
|||
...v,
|
||||
_count: {
|
||||
...v._count,
|
||||
product: statsProduct.reduce(
|
||||
product: statsDeep.reduce(
|
||||
(a, c) => (c.productGroupId === v.id ? a + c._count.product : a),
|
||||
0,
|
||||
),
|
||||
service: statsDeep.reduce(
|
||||
(a, c) => (c.productGroupId === v.id ? a + c._count.service : a),
|
||||
0,
|
||||
),
|
||||
},
|
||||
})),
|
||||
page,
|
||||
|
|
@ -108,6 +115,7 @@ export class ProductGroup extends Controller {
|
|||
}
|
||||
|
||||
@Get("{groupId}")
|
||||
@Security("keycloak")
|
||||
async getProductGroupById(@Path() groupId: string) {
|
||||
const record = await prisma.productGroup.findFirst({
|
||||
where: { id: groupId },
|
||||
|
|
@ -124,6 +132,7 @@ export class ProductGroup extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
|
||||
const record = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
|
|
@ -139,12 +148,16 @@ export class ProductGroup extends Controller {
|
|||
});
|
||||
|
||||
return await tx.productGroup.create({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
code: `G${last.value.toString().padStart(2, "0")}`,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -157,6 +170,7 @@ export class ProductGroup extends Controller {
|
|||
}
|
||||
|
||||
@Put("{groupId}")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async editProductGroup(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: ProductGroupUpdate,
|
||||
|
|
@ -171,7 +185,11 @@ export class ProductGroup extends Controller {
|
|||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
|
|
@ -179,6 +197,7 @@ export class ProductGroup extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{groupId}")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async deleteProductGroup(@Path() groupId: string) {
|
||||
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");
|
||||
}
|
||||
|
||||
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 MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"accountant",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
type ProductCreate = {
|
||||
status?: Status;
|
||||
code: "AC" | "DO" | "ac" | "do";
|
||||
code:
|
||||
| "DOE"
|
||||
| "IMM"
|
||||
| "TM"
|
||||
| "HP"
|
||||
| "MOUC"
|
||||
| "MOUL"
|
||||
| "AC"
|
||||
| "doe"
|
||||
| "imm"
|
||||
| "tm"
|
||||
| "hp"
|
||||
| "mouc"
|
||||
| "moul"
|
||||
| "ac";
|
||||
name: string;
|
||||
detail: string;
|
||||
process: number;
|
||||
|
|
@ -37,6 +60,8 @@ type ProductCreate = {
|
|||
serviceCharge: number;
|
||||
productTypeId: string;
|
||||
remark?: string;
|
||||
|
||||
registeredBranchId?: string;
|
||||
};
|
||||
|
||||
type ProductUpdate = {
|
||||
|
|
@ -49,12 +74,20 @@ type ProductUpdate = {
|
|||
serviceCharge?: number;
|
||||
remark?: string;
|
||||
productTypeId?: string;
|
||||
|
||||
registeredBranchId?: string;
|
||||
};
|
||||
|
||||
function imageLocation(id: string) {
|
||||
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")
|
||||
@Tags("Product")
|
||||
export class ProductController extends Controller {
|
||||
|
|
@ -71,6 +104,7 @@ export class ProductController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() registeredBranchId?: string,
|
||||
) {
|
||||
const filterStatus = (val?: Status) => {
|
||||
if (!val) return {};
|
||||
|
|
@ -85,10 +119,19 @@ export class ProductController extends Controller {
|
|||
{ name: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||
{ detail: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||
],
|
||||
AND: registeredBranchId
|
||||
? {
|
||||
OR: [{ registeredBranchId: registeredBranchId }, { registeredBranchId: null }],
|
||||
}
|
||||
: undefined,
|
||||
} satisfies Prisma.ProductWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.product.findMany({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -118,6 +161,10 @@ export class ProductController extends Controller {
|
|||
@Security("keycloak")
|
||||
async getProductById(@Path() productId: string) {
|
||||
const record = await prisma.product.findFirst({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: productId },
|
||||
});
|
||||
|
||||
|
|
@ -142,11 +189,29 @@ export class ProductController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) {
|
||||
const productType = await prisma.productType.findFirst({
|
||||
where: { id: body.productTypeId },
|
||||
});
|
||||
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(
|
||||
|
|
@ -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(
|
||||
async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
|
|
@ -169,12 +242,16 @@ export class ProductController extends Controller {
|
|||
update: { value: { increment: 1 } },
|
||||
});
|
||||
return await prisma.product.create({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -185,6 +262,10 @@ export class ProductController extends Controller {
|
|||
|
||||
if (productType.status === "CREATED") {
|
||||
await prisma.productType.update({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: body.productTypeId },
|
||||
data: { status: Status.ACTIVE },
|
||||
});
|
||||
|
|
@ -207,19 +288,44 @@ export class ProductController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{productId}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editProduct(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: ProductUpdate,
|
||||
@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");
|
||||
}
|
||||
|
||||
const productType = await prisma.productType.findFirst({
|
||||
where: { id: body.productTypeId },
|
||||
});
|
||||
if (!globalAllow(req.user.roles) && !product.registeredBranch) {
|
||||
throw new HttpError(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"You do not have permission to perform this action.",
|
||||
"noPermission",
|
||||
);
|
||||
}
|
||||
|
||||
if (!productType) {
|
||||
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({
|
||||
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 },
|
||||
});
|
||||
|
||||
|
|
@ -256,18 +374,42 @@ export class ProductController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{productId}")
|
||||
@Security("keycloak")
|
||||
async deleteProduct(@Path() productId: string) {
|
||||
const record = await prisma.product.findFirst({ where: { id: productId } });
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async deleteProduct(@Request() req: RequestWithUser, @Path() productId: string) {
|
||||
const record = await prisma.product.findFirst({
|
||||
include: {
|
||||
registeredBranch: {
|
||||
where: {
|
||||
user: { some: { userId: req.user.sub } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
where: { id: productId },
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
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) {
|
||||
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")
|
||||
@Tags("Product Type")
|
||||
@Security("keycloak")
|
||||
export class ProductType extends Controller {
|
||||
@Get("stats")
|
||||
async getProductTypeStats() {
|
||||
|
|
@ -45,6 +44,7 @@ export class ProductType extends Controller {
|
|||
}
|
||||
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async getProductType(
|
||||
@Query() query: string = "",
|
||||
@Query() productGroupId?: string,
|
||||
|
|
@ -70,8 +70,13 @@ export class ProductType extends Controller {
|
|||
prisma.productType.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: { product: true },
|
||||
select: {
|
||||
product: true,
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
where,
|
||||
|
|
@ -85,6 +90,7 @@ export class ProductType extends Controller {
|
|||
}
|
||||
|
||||
@Get("{typeId}")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async getProductTypeById(@Path() typeId: string) {
|
||||
const record = await prisma.productType.findFirst({
|
||||
where: { id: typeId },
|
||||
|
|
@ -101,6 +107,7 @@ export class ProductType extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async createProductType(@Request() req: RequestWithUser, @Body() body: ProductTypeCreate) {
|
||||
const productGroup = await prisma.productGroup.findFirst({
|
||||
where: { id: body.productGroupId },
|
||||
|
|
@ -128,12 +135,16 @@ export class ProductType extends Controller {
|
|||
});
|
||||
|
||||
return await tx.productType.create({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
code: `T${productGroup.code}${last.value.toString().padStart(2, "0")}`,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -153,6 +164,7 @@ export class ProductType extends Controller {
|
|||
}
|
||||
|
||||
@Put("{typeId}")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async editProductType(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: ProductTypeUpdate,
|
||||
|
|
@ -178,12 +190,20 @@ export class ProductType extends Controller {
|
|||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
if (productGroup?.status === "CREATED") {
|
||||
await prisma.productGroup.update({
|
||||
if (body.productGroupId && productGroup?.status === "CREATED") {
|
||||
await prisma.productGroup.updateMany({
|
||||
where: { id: body.productGroupId, status: Status.CREATED },
|
||||
data: { status: Status.ACTIVE },
|
||||
});
|
||||
|
|
@ -193,6 +213,7 @@ export class ProductType extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{typeId}")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"])
|
||||
async deleteProductType(@Path() typeId: string) {
|
||||
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");
|
||||
}
|
||||
|
||||
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 MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"accountant",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
type ServiceCreate = {
|
||||
code: "MOU" | "mou";
|
||||
|
|
@ -39,6 +48,8 @@ type ServiceCreate = {
|
|||
productId: string[];
|
||||
attributes?: { [key: string]: any };
|
||||
}[];
|
||||
productTypeId: string;
|
||||
registeredBranchId?: string;
|
||||
};
|
||||
|
||||
type ServiceUpdate = {
|
||||
|
|
@ -53,12 +64,18 @@ type ServiceUpdate = {
|
|||
productId: string[];
|
||||
attributes?: { [key: string]: any };
|
||||
}[];
|
||||
productTypeId?: string;
|
||||
registeredBranchId?: string;
|
||||
};
|
||||
|
||||
function imageLocation(id: string) {
|
||||
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")
|
||||
@Tags("Service")
|
||||
export class ServiceController extends Controller {
|
||||
|
|
@ -75,6 +92,9 @@ export class ServiceController extends Controller {
|
|||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() status?: Status,
|
||||
@Query() productTypeId?: string,
|
||||
@Query() registeredBranchId?: string,
|
||||
@Query() fullDetail?: boolean,
|
||||
) {
|
||||
const filterStatus = (val?: Status) => {
|
||||
if (!val) return {};
|
||||
|
|
@ -86,15 +106,32 @@ export class ServiceController extends Controller {
|
|||
|
||||
const where = {
|
||||
OR: [
|
||||
{ name: { contains: query }, ...filterStatus(status) },
|
||||
{ detail: { contains: query }, ...filterStatus(status) },
|
||||
{ name: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||
{ detail: { contains: query }, productTypeId, ...filterStatus(status) },
|
||||
],
|
||||
AND: registeredBranchId
|
||||
? {
|
||||
OR: [{ registeredBranchId: registeredBranchId }, { registeredBranchId: null }],
|
||||
}
|
||||
: undefined,
|
||||
} satisfies Prisma.ServiceWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.service.findMany({
|
||||
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" }],
|
||||
where,
|
||||
|
|
@ -135,6 +172,8 @@ export class ServiceController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: serviceId },
|
||||
});
|
||||
|
|
@ -168,6 +207,8 @@ export class ServiceController extends Controller {
|
|||
},
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -190,9 +231,47 @@ export class ServiceController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
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(
|
||||
async (tx) => {
|
||||
|
|
@ -239,14 +318,17 @@ export class ServiceController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...payload,
|
||||
productTypeId,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
||||
work: { connect: workList.map((v) => ({ id: v.id })) },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -269,7 +351,7 @@ export class ServiceController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{serviceId}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async editService(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: ServiceUpdate,
|
||||
|
|
@ -278,7 +360,57 @@ export class ServiceController extends Controller {
|
|||
if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) {
|
||||
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 workList = await Promise.all(
|
||||
(work || []).map(async (w, wIdx) =>
|
||||
|
|
@ -301,6 +433,10 @@ export class ServiceController extends Controller {
|
|||
);
|
||||
|
||||
return await tx.service.update({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...payload,
|
||||
statusOrder: +(payload.status === "INACTIVE"),
|
||||
|
|
@ -308,7 +444,7 @@ export class ServiceController extends Controller {
|
|||
deleteMany: {},
|
||||
connect: workList.map((v) => ({ id: v.id })),
|
||||
},
|
||||
updatedBy: req.user.name,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
where: { id: serviceId },
|
||||
});
|
||||
|
|
@ -329,18 +465,41 @@ export class ServiceController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{serviceId}")
|
||||
@Security("keycloak")
|
||||
async deleteService(@Path() serviceId: string) {
|
||||
const record = await prisma.service.findFirst({ where: { id: serviceId } });
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async deleteService(@Request() req: RequestWithUser, @Path() serviceId: string) {
|
||||
const record = await prisma.service.findFirst({
|
||||
include: {
|
||||
registeredBranch: {
|
||||
where: {
|
||||
user: { some: { userId: req.user.sub } },
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { id: serviceId },
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
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) {
|
||||
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,
|
||||
Tags,
|
||||
} from "tsoa";
|
||||
import { Prisma, Status, UserType } from "@prisma/client";
|
||||
import { Branch, Prisma, Status, User, UserType } from "@prisma/client";
|
||||
|
||||
import prisma from "../db";
|
||||
import minio, { presignedGetObjectIfExist } from "../services/minio";
|
||||
|
|
@ -24,7 +24,7 @@ import {
|
|||
createUser,
|
||||
deleteUser,
|
||||
editUser,
|
||||
getRoles,
|
||||
listRole,
|
||||
getUserRoles,
|
||||
removeUserRoles,
|
||||
} from "../services/keycloak";
|
||||
|
|
@ -43,8 +43,11 @@ type UserCreate = {
|
|||
|
||||
username: string;
|
||||
|
||||
namePrefix?: string | null;
|
||||
firstName: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string | null;
|
||||
middleNameEN?: string | null;
|
||||
lastName: string;
|
||||
lastNameEN: string;
|
||||
gender: string;
|
||||
|
|
@ -73,6 +76,8 @@ type UserCreate = {
|
|||
subDistrictId?: string | null;
|
||||
districtId?: string | null;
|
||||
provinceId?: string | null;
|
||||
|
||||
branchId: string | string[];
|
||||
};
|
||||
|
||||
type UserUpdate = {
|
||||
|
|
@ -83,8 +88,11 @@ type UserUpdate = {
|
|||
userType?: UserType;
|
||||
userRole?: string;
|
||||
|
||||
namePrefix?: string | null;
|
||||
firstName?: string;
|
||||
firstNameEN?: string;
|
||||
middleName?: string | null;
|
||||
middleNameEN?: string | null;
|
||||
lastName?: string;
|
||||
lastNameEN?: string;
|
||||
gender?: string;
|
||||
|
|
@ -113,8 +121,44 @@ type UserUpdate = {
|
|||
subDistrictId?: string | null;
|
||||
districtId?: 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) {
|
||||
return `user/profile-img-${id}`;
|
||||
}
|
||||
|
|
@ -153,16 +197,28 @@ export class UserController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() status?: Status,
|
||||
) {
|
||||
const filterStatus = (val?: Status) => {
|
||||
if (!val) return {};
|
||||
|
||||
return val !== Status.CREATED && val !== Status.ACTIVE
|
||||
? { status: val }
|
||||
: { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] };
|
||||
};
|
||||
|
||||
const where = {
|
||||
OR: [
|
||||
{ firstName: { contains: query }, zipCode, userType },
|
||||
{ firstNameEN: { contains: query }, zipCode, userType },
|
||||
{ lastName: { contains: query }, zipCode, userType },
|
||||
{ lastNameEN: { contains: query }, zipCode, userType },
|
||||
{ email: { contains: query }, zipCode, userType },
|
||||
{ telephoneNo: { contains: query }, zipCode, userType },
|
||||
{ firstName: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||
{ firstNameEN: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||
{ lastName: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||
{ lastNameEN: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||
{ email: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||
{ telephoneNo: { contains: query }, zipCode, userType, ...filterStatus(status) },
|
||||
],
|
||||
AND: {
|
||||
userRole: { not: "system" },
|
||||
},
|
||||
} satisfies Prisma.UserWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -173,6 +229,8 @@ export class UserController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
branch: { include: { branch: includeBranch } },
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -207,6 +265,8 @@ export class UserController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: userId },
|
||||
});
|
||||
|
|
@ -223,40 +283,59 @@ export class UserController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin"])
|
||||
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
|
||||
if (body.provinceId || body.districtId || body.subDistrictId) {
|
||||
const [province, district, subDistrict] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
|
||||
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
|
||||
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
|
||||
]);
|
||||
if (body.provinceId && !province) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Province cannot be found.",
|
||||
"relationProvinceNotFound",
|
||||
);
|
||||
}
|
||||
if (body.districtId && !district) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"District cannot be found.",
|
||||
"relationDistrictNotFound",
|
||||
);
|
||||
}
|
||||
if (body.subDistrictId && !subDistrict) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Sub-district cannot be found.",
|
||||
"relationSubDistrictNotFound",
|
||||
);
|
||||
}
|
||||
const [province, district, subDistrict, branch] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
|
||||
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
|
||||
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
|
||||
prisma.branch.findMany({
|
||||
include: { user: { where: { userId: req.user.sub } } },
|
||||
where: { id: { in: Array.isArray(body.branchId) ? body.branchId : [body.branchId] } },
|
||||
}),
|
||||
]);
|
||||
if (body.provinceId && !province) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Province cannot be found.",
|
||||
"relationProvinceNotFound",
|
||||
);
|
||||
}
|
||||
if (body.districtId && !district) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"District cannot be found.",
|
||||
"relationDistrictNotFound",
|
||||
);
|
||||
}
|
||||
if (body.subDistrictId && !subDistrict) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Sub-district cannot be found.",
|
||||
"relationSubDistrictNotFound",
|
||||
);
|
||||
}
|
||||
if (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)) {
|
||||
|
|
@ -287,7 +366,13 @@ export class UserController extends Controller {
|
|||
}
|
||||
|
||||
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: {
|
||||
id: userId,
|
||||
...rest,
|
||||
|
|
@ -297,11 +382,31 @@ export class UserController extends Controller {
|
|||
province: { connect: provinceId ? { id: provinceId } : undefined },
|
||||
district: { connect: districtId ? { id: districtId } : undefined },
|
||||
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
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);
|
||||
|
||||
return Object.assign(record, {
|
||||
|
|
@ -319,43 +424,72 @@ export class UserController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{userId}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||
async editUser(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body() body: UserUpdate,
|
||||
@Path() userId: string,
|
||||
) {
|
||||
if (body.subDistrictId || body.districtId || body.provinceId) {
|
||||
const [province, district, subDistrict] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
|
||||
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
|
||||
]);
|
||||
|
||||
if (body.provinceId && !province)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Province cannot be found.",
|
||||
"missing_or_invalid_parameter",
|
||||
);
|
||||
if (body.districtId && !district)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"District cannot be found.",
|
||||
"missing_or_invalid_parameter",
|
||||
);
|
||||
if (body.subDistrictId && !subDistrict)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Sub-district cannot be found.",
|
||||
"missing_or_invalid_parameter",
|
||||
);
|
||||
const [province, district, subDistrict, user, branch] = await prisma.$transaction([
|
||||
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
|
||||
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
|
||||
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
|
||||
prisma.user.findFirst({
|
||||
include: { branch: true },
|
||||
where: { id: userId },
|
||||
}),
|
||||
prisma.branch.findMany({
|
||||
include: { user: { where: { id: req.user.sub } } },
|
||||
where: {
|
||||
id: {
|
||||
in: Array.isArray(body.branchId) ? body.branchId : body.branchId ? [body.branchId] : [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
if (!user) {
|
||||
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
|
||||
}
|
||||
if (body.provinceId && !province)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Province cannot be found.",
|
||||
"missing_or_invalid_parameter",
|
||||
);
|
||||
if (body.districtId && !district)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"District cannot be found.",
|
||||
"missing_or_invalid_parameter",
|
||||
);
|
||||
if (body.subDistrictId && !subDistrict)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Sub-district cannot be found.",
|
||||
"missing_or_invalid_parameter",
|
||||
);
|
||||
if (body.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;
|
||||
|
||||
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)) {
|
||||
|
|
@ -394,38 +528,20 @@ export class UserController extends Controller {
|
|||
await editUser(userId, { username: body.username, enabled: body.status !== "INACTIVE" });
|
||||
}
|
||||
|
||||
const { provinceId, districtId, subDistrictId, ...rest } = body;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "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 { provinceId, districtId, subDistrictId, branchId, ...rest } = body;
|
||||
|
||||
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: {
|
||||
...rest,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
userRole,
|
||||
code:
|
||||
(lastUserOfType &&
|
||||
`${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) ||
|
||||
undefined,
|
||||
province: {
|
||||
connect: provinceId ? { id: provinceId } : undefined,
|
||||
disconnect: provinceId === null || undefined,
|
||||
|
|
@ -438,11 +554,42 @@ export class UserController extends Controller {
|
|||
connect: subDistrictId ? { id: subDistrictId } : undefined,
|
||||
disconnect: subDistrictId === null || undefined,
|
||||
},
|
||||
updatedBy: req.user.name,
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
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, {
|
||||
profileImageUrl: await minio.presignedGetObject(
|
||||
MINIO_BUCKET,
|
||||
|
|
@ -458,17 +605,35 @@ export class UserController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{userId}")
|
||||
@Security("keycloak")
|
||||
async deleteUser(@Path() userId: string) {
|
||||
@Security("keycloak", ["system", "head_of_admin", "admin", "branch_admin", "branch_manager"])
|
||||
async deleteUser(@Request() req: RequestWithUser, @Path() userId: string) {
|
||||
const record = await prisma.user.findFirst({
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
branch: {
|
||||
where: {
|
||||
userId: req.user.sub,
|
||||
},
|
||||
},
|
||||
},
|
||||
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) {
|
||||
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound");
|
||||
}
|
||||
|
|
@ -502,10 +667,55 @@ export class UserController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
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) {
|
||||
|
|
@ -519,11 +729,6 @@ export class UserAttachmentController extends Controller {
|
|||
@Get()
|
||||
async listAttachment(@Path() userId: string) {
|
||||
const record = await prisma.user.findFirst({
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
|
|
@ -552,11 +757,6 @@ export class UserAttachmentController extends Controller {
|
|||
@Post()
|
||||
async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
|
||||
const record = await prisma.user.findFirst({
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export class WorkController extends Controller {
|
|||
order: "asc",
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
where,
|
||||
|
|
@ -106,6 +108,10 @@ export class WorkController extends Controller {
|
|||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.product.findMany({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
|
|
@ -129,6 +135,8 @@ export class WorkController extends Controller {
|
|||
product: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: {
|
||||
productOnWork: {
|
||||
|
|
@ -175,6 +183,8 @@ export class WorkController extends Controller {
|
|||
order: "asc",
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...payload,
|
||||
|
|
@ -183,13 +193,13 @@ export class WorkController extends Controller {
|
|||
data: productId.map((v, i) => ({
|
||||
order: i + 1,
|
||||
productId: v,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
})),
|
||||
},
|
||||
},
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -261,6 +271,8 @@ export class WorkController extends Controller {
|
|||
order: "asc",
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: workId },
|
||||
data: {
|
||||
|
|
@ -281,13 +293,13 @@ export class WorkController extends Controller {
|
|||
create: {
|
||||
order: i + 1,
|
||||
productId: v,
|
||||
createdBy: req.user.name,
|
||||
updatedBy: req.user.name,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
updatedBy: req.user.name,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -296,7 +308,10 @@ export class WorkController extends Controller {
|
|||
|
||||
@Delete("{workId}")
|
||||
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) {
|
||||
throw new HttpError(HttpStatus.NOT_FOUND, "Work cannot be found.", "workNotFound");
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ export type RequestWithUser = Request & {
|
|||
familiy_name: string;
|
||||
preferred_username: string;
|
||||
email: string;
|
||||
role: string[];
|
||||
roles: string[];
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,10 +19,14 @@ const jwtDecode = createDecoder();
|
|||
export async function keycloakAuth(request: Express.Request, roles?: string[]) {
|
||||
const token = request.headers["authorization"]?.includes("Bearer ")
|
||||
? request.headers["authorization"].split(" ")[1]
|
||||
: request.headers["authorization"];
|
||||
: "";
|
||||
|
||||
if (!token) {
|
||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบข้อมูลสำหรับยืนยันตัวตน");
|
||||
throw new HttpError(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"authorization data not found.",
|
||||
"authDataNotFound",
|
||||
);
|
||||
}
|
||||
|
||||
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 (!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) {
|
||||
const payload = await jwtVerify(token).catch((_) => null);
|
||||
if (!payload) {
|
||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่สามารถยืนยันตัวตนได้");
|
||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "Unauthorized.", "authFailed");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
|
@ -70,9 +78,14 @@ async function verifyOnline(token: string) {
|
|||
},
|
||||
).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) {
|
||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่สามารถยืนยันตัวตนได้");
|
||||
throw new HttpError(HttpStatus.UNAUTHORIZED, "Unauthorized.", "authFailed");
|
||||
}
|
||||
|
||||
return await jwtDecode(token);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ export async function expressAuthentication(
|
|||
switch (securityName) {
|
||||
case "keycloak":
|
||||
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.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;
|
||||
default:
|
||||
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.",
|
||||
) {
|
||||
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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
return next();
|
||||
|
|
|
|||
|
|
@ -59,6 +59,61 @@ export async function getToken() {
|
|||
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
|
||||
*
|
||||
|
|
@ -151,36 +206,42 @@ export async function deleteUser(userId: string) {
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
const res = await fetch(
|
||||
`${KC_URL}/admin/realms/${KC_REALM}/roles`.concat((name && `/${name}`) || ""),
|
||||
{
|
||||
// prettier-ignore
|
||||
headers: {
|
||||
"authorization": `Bearer ${await getToken()}`,
|
||||
export async function listRole() {
|
||||
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/roles`, {
|
||||
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) {
|
||||
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
|
||||
return console.error("Keycloak Error Response: ", await res.json());
|
||||
}
|
||||
|
||||
if (res.status === 404) {
|
||||
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 {
|
||||
id: data.id,
|
||||
|
|
@ -285,7 +346,7 @@ export async function removeUserRoles(userId: string, roles: { id: string; name:
|
|||
|
||||
export default {
|
||||
createUser,
|
||||
getRoles,
|
||||
listRole,
|
||||
addUserRoles,
|
||||
removeUserRoles,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,13 +37,15 @@ export async function listObjectVersion(bucket: string, obj: string) {
|
|||
export async function deleteObjectAllVersion(bucket: string, obj: string) {
|
||||
const item = await listObjectVersion(bucket, obj);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
minio.removeObjects(
|
||||
bucket,
|
||||
// @ts-ignore
|
||||
item.map(({ name, versionId }) => ({ name, versionId })), // type error (ts not support) - expected "string[]"
|
||||
(e) => (e && reject(e)) || resolve(true),
|
||||
);
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
await minio
|
||||
.removeObjects(
|
||||
bucket,
|
||||
item.map(({ name, versionId }) => ({ name, versionId })),
|
||||
)
|
||||
.catch((e) => reject(e));
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
"specVersion": 3,
|
||||
"securityDefinitions": {
|
||||
"keycloak": {
|
||||
"type": "apiKey",
|
||||
"type": "http",
|
||||
"name": "Authorization",
|
||||
"description": "Keycloak Bearer Token",
|
||||
"in": "header"
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
|
|
@ -33,7 +33,8 @@
|
|||
{ "name": "Product Type" },
|
||||
{ "name": "Product" },
|
||||
{ "name": "Work" },
|
||||
{ "name": "Service" }
|
||||
{ "name": "Service" },
|
||||
{ "name": "Quotation" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue