diff --git a/prisma/migrations/20240703043154_update_structure/migration.sql b/prisma/migrations/20240703043154_update_structure/migration.sql new file mode 100644 index 0000000..a6692a4 --- /dev/null +++ b/prisma/migrations/20240703043154_update_structure/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a12981..bd0bc3b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -230,6 +230,10 @@ model Branch { branch Branch[] @relation(name: "HeadOfficeRelation") contact BranchContact[] user BranchUser[] + + productRegistration Product[] + serviceRegistration Service[] + customerRegistration Customer[] } model BranchContact { @@ -391,6 +395,9 @@ model Customer { status Status @default(CREATED) statusOrder Int @default(0) + registeredBranchId String? + registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id]) + createdAt DateTime @default(now()) createdBy User? @relation(name: "CustomerCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull) createdByUserId String? @@ -616,6 +623,84 @@ model EmployeeOtherInfo { updatedByUserId String? } +model ProductGroup { + id String @id @default(uuid()) + + code String + name String + detail String + remark String + + status Status @default(CREATED) + statusOrder Int @default(0) + + 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[] +} + +model ProductType { + id String @id @default(uuid()) + + code String + name String + detail String + remark String + + status Status @default(CREATED) + statusOrder Int @default(0) + + 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 { + id String @id @default(uuid()) + + code String + name String + detail String + process Int + price Float + agentPrice Float + serviceCharge Float + + status Status @default(CREATED) + statusOrder Int @default(0) + + remark String? + + productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) + productTypeId String? + + registeredBranchId String? + registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id]) + + workProduct WorkProduct[] + + 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()) @@ -629,6 +714,12 @@ model Service { work Work[] + 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? @@ -676,77 +767,3 @@ model WorkProduct { @@id([workId, productId]) } - -model ProductGroup { - id String @id @default(uuid()) - - code String - name String - detail String - remark String - - status Status @default(CREATED) - statusOrder Int @default(0) - - 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[] -} - -model ProductType { - id String @id @default(uuid()) - - code String - name String - detail String - remark String - - status Status @default(CREATED) - statusOrder Int @default(0) - - 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[] -} - -model Product { - id String @id @default(uuid()) - - code String - name String - detail String - process Int - price Float - agentPrice Float - serviceCharge Float - - status Status @default(CREATED) - statusOrder Int @default(0) - - remark String? - - productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) - productTypeId String? - - workProduct WorkProduct[] - - 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? -} diff --git a/src/controllers/branch-user-controller.ts b/src/controllers/branch-user-controller.ts index 4be9ac3..7fa62f4 100644 --- a/src/controllers/branch-user-controller.ts +++ b/src/controllers/branch-user-controller.ts @@ -53,9 +53,9 @@ async function userBranchCodeGen(branch: Branch, user: User[]) { @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 +97,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 +105,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,6 +118,18 @@ export class BranchUserController extends Controller { }), ]); + 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.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + if (!branch) { throw new HttpError(HttpStatus.BAD_REQUEST, "Branch cannot be found.", "branchBadReq"); } diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index af6e112..5a1fa45 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -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"; @@ -73,6 +73,8 @@ type UserCreate = { subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; + + branchId: string | string[]; }; type UserUpdate = { @@ -113,8 +115,37 @@ 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 last = await tx.runningNo.upsert({ + where: { + key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`, + }, + create: { + key: `BR_USR_${branch.code.slice(4).padEnd(3, "0")}${typ !== "USER" ? typ.charAt(0).toLocaleUpperCase() : ""}`, + value: 1, + }, + update: { value: { increment: 1 } }, + }); + + return await tx.user.update({ + where: { id: user.id }, + data: { + code: `${last.key.slice(7)}${last.value.toString().padStart(4, "0")}`, + }, + }); + }, + { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, + ); +} + function imageLocation(id: string) { return `user/profile-img-${id}`; } @@ -227,38 +258,58 @@ 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)) + ) { + console.log(req.user.roles); + 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 listRole(); @@ -312,6 +363,26 @@ export class UserController extends Controller { }, }); + 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, { @@ -329,37 +400,66 @@ 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; @@ -404,15 +504,7 @@ 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 { provinceId, districtId, subDistrictId, branchId, ...rest } = body; const lastUserOfType = body.userType && @@ -459,6 +551,30 @@ export class UserController extends Controller { 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, + })), + }), + ]); + + if (branch[0]?.id !== user.branch[0]?.id) { + const updated = await userBranchCodeGen(user, branch[0]); + record.code = updated.code; + } + } + return Object.assign(record, { profileImageUrl: await minio.presignedGetObject( MINIO_BUCKET, @@ -474,8 +590,8 @@ 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, @@ -483,10 +599,26 @@ export class UserController extends Controller { 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"); }