diff --git a/prisma/migrations/20250404040202_add/migration.sql b/prisma/migrations/20250404040202_add/migration.sql new file mode 100644 index 0000000..1bc8248 --- /dev/null +++ b/prisma/migrations/20250404040202_add/migration.sql @@ -0,0 +1,29 @@ +-- AlterTable +ALTER TABLE "Employee" ALTER COLUMN "firstName" DROP NOT NULL, +ALTER COLUMN "lastName" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Institution" ADD COLUMN "contactEmail" TEXT, +ADD COLUMN "contactName" TEXT, +ADD COLUMN "contactTel" TEXT; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "agencyStatus" TEXT, +ADD COLUMN "remark" TEXT; + +-- CreateTable +CREATE TABLE "InstitutionBank" ( + "id" TEXT NOT NULL, + "bankName" TEXT NOT NULL, + "bankBranch" TEXT NOT NULL, + "accountName" TEXT NOT NULL, + "accountNumber" TEXT NOT NULL, + "accountType" TEXT NOT NULL, + "currentlyUse" BOOLEAN NOT NULL, + "institutionId" TEXT NOT NULL, + + CONSTRAINT "InstitutionBank_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "InstitutionBank" ADD CONSTRAINT "InstitutionBank_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "Institution"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250404040846_add/migration.sql b/prisma/migrations/20250404040846_add/migration.sql new file mode 100644 index 0000000..babac56 --- /dev/null +++ b/prisma/migrations/20250404040846_add/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Employee" ALTER COLUMN "dateOfBirth" DROP NOT NULL; diff --git a/prisma/migrations/20250404071034_add/migration.sql b/prisma/migrations/20250404071034_add/migration.sql new file mode 100644 index 0000000..08c3ea5 --- /dev/null +++ b/prisma/migrations/20250404071034_add/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "InstitutionBank" DROP CONSTRAINT "InstitutionBank_institutionId_fkey"; + +-- AddForeignKey +ALTER TABLE "InstitutionBank" ADD CONSTRAINT "InstitutionBank_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "Institution"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 666fa32..cb38c88 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -493,7 +493,10 @@ model User { requestWorkStepStatus RequestWorkStepStatus[] userTask UserTask[] - RequestData RequestData[] + requestData RequestData[] + + remark String? + agencyStatus String? } model UserResponsibleArea { @@ -763,14 +766,14 @@ model Employee { nrcNo String? namePrefix String? - firstName String + firstName String? firstNameEN String middleName String? middleNameEN String? - lastName String + lastName String? lastNameEN String - dateOfBirth DateTime @db.Date + dateOfBirth DateTime? @db.Date gender String nationality String @@ -1004,6 +1007,25 @@ model Institution { selectedImage String? taskOrder TaskOrder[] + + contactName String? + contactEmail String? + contactTel String? + + bank InstitutionBank[] +} + +model InstitutionBank { + id String @id @default(cuid()) + bankName String + bankBranch String + accountName String + accountNumber String + accountType String + currentlyUse Boolean + + institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade) + institutionId String } model Property { diff --git a/src/controllers/02-user-controller.ts b/src/controllers/02-user-controller.ts index 0a92740..f1dca32 100644 --- a/src/controllers/02-user-controller.ts +++ b/src/controllers/02-user-controller.ts @@ -120,6 +120,9 @@ type UserCreate = { selectedImage?: string; branchId: string | string[]; + + remark?: string; + agencyStatus?: string; }; type UserUpdate = { @@ -176,6 +179,9 @@ type UserUpdate = { provinceId?: string | null; branchId?: string | string[]; + + remark?: string; + agencyStatus?: string; }; const permissionCondCompany = createPermCondition((_) => true); diff --git a/src/controllers/03-employee-controller.ts b/src/controllers/03-employee-controller.ts index 994e246..eb08db7 100644 --- a/src/controllers/03-employee-controller.ts +++ b/src/controllers/03-employee-controller.ts @@ -70,16 +70,16 @@ type EmployeeCreate = { nrcNo?: string | null; - dateOfBirth: Date; + dateOfBirth?: Date | null; gender: string; nationality: string; namePrefix?: string | null; - firstName: string; + firstName?: string; firstNameEN: string; middleName?: string | null; middleNameEN?: string | null; - lastName: string; + lastName?: string; lastNameEN: string; addressEN: string; @@ -112,11 +112,11 @@ type EmployeeUpdate = { namePrefix?: string | null; firstName?: string; - firstNameEN?: string; + firstNameEN: string; middleName?: string | null; middleNameEN?: string | null; lastName?: string; - lastNameEN?: string; + lastNameEN: string; addressEN?: string; address?: string; @@ -364,9 +364,10 @@ export class EmployeeController extends Controller { }, }), ]); - if (body.provinceId !== province?.id) throw relationError("Province"); - if (body.districtId !== district?.id) throw relationError("District"); - if (body.subDistrictId !== subDistrict?.id) throw relationError("SubDistrict"); + if (!!body.provinceId && body.provinceId !== province?.id) throw relationError("Province"); + if (!!body.districtId && body.districtId !== district?.id) throw relationError("District"); + if (!!body.subDistrictId && body.subDistrictId !== subDistrict?.id) + throw relationError("SubDistrict"); if (!customerBranch) throw relationError("Customer Branch"); await permissionCheck(req.user, customerBranch.customer.registeredBranch); diff --git a/src/controllers/04-institution-controller.ts b/src/controllers/04-institution-controller.ts index 2eef31c..5e21445 100644 --- a/src/controllers/04-institution-controller.ts +++ b/src/controllers/04-institution-controller.ts @@ -44,6 +44,55 @@ type InstitutionPayload = { provinceId: string; selectedImage?: string | null; + + contactName?: string; + contactEmail?: string; + contactTel?: string; + + bank?: { + bankName: string; + bankBranch: string; + accountName: string; + accountNumber: string; + accountType: string; + currentlyUse: boolean; + }[]; +}; + +type InstitutionUpdatePayload = { + name: string; + nameEN: string; + + code: string; + + addressEN: string; + address: string; + soi?: string | null; + soiEN?: string | null; + moo?: string | null; + mooEN?: string | null; + street?: string | null; + streetEN?: string | null; + + subDistrictId: string; + districtId: string; + provinceId: string; + + selectedImage?: string | null; + + contactName?: string; + contactEmail?: string; + contactTel?: string; + + bank?: { + id?: string; + bankName: string; + bankBranch: string; + accountName: string; + accountNumber: string; + accountType: string; + currentlyUse: boolean; + }[]; }; @Route("api/v1/institution") @@ -94,6 +143,7 @@ export class InstitutionController extends Controller { province: true, district: true, subDistrict: true, + bank: true, }, orderBy: [{ statusOrder: "asc" }, { code: "asc" }], take: pageSize, @@ -114,6 +164,7 @@ export class InstitutionController extends Controller { province: true, district: true, subDistrict: true, + bank: true, }, where: { id: institutionId, group }, }); @@ -141,10 +192,18 @@ export class InstitutionController extends Controller { }); return await tx.institution.create({ + include: { + bank: true, + }, data: { ...body, code: `${body.code}${last.value.toString().padStart(5, "0")}`, group: body.code, + bank: { + createMany: { + data: body.bank ?? [], + }, + }, }, }); }); @@ -156,13 +215,46 @@ export class InstitutionController extends Controller { async updateInstitution( @Path() institutionId: string, @Body() - body: InstitutionPayload & { + body: InstitutionUpdatePayload & { status?: "ACTIVE" | "INACTIVE"; }, ) { - return await prisma.institution.update({ - where: { id: institutionId }, - data: { ...body, statusOrder: +(body.status === "INACTIVE") }, + const { bank } = body; + return await prisma.$transaction(async (tx) => { + const listDeleted = bank + ? await tx.institutionBank.findMany({ + where: { + id: { not: { in: bank.flatMap((v) => (!!v.id ? v.id : [])) } }, + institutionId, + }, + }) + : []; + + await Promise.all( + listDeleted.map((v) => deleteFile(fileLocation.institution.bank(v.institutionId, v.id))), + ); + + return await prisma.institution.update({ + include: { + bank: true, + }, + where: { id: institutionId }, + data: { + ...body, + statusOrder: +(body.status === "INACTIVE"), + bank: bank + ? { + deleteMany: + listDeleted.length > 0 ? { id: { in: listDeleted.map((v) => v.id) } } : undefined, + upsert: bank.map((v) => ({ + where: { id: v.id || "" }, + create: { ...v, id: undefined }, + update: v, + })), + } + : undefined, + }, + }); }); } @@ -185,9 +277,18 @@ export class InstitutionController extends Controller { throw isUsedError("Institution"); } - return await tx.institution.delete({ + const data = await tx.institution.delete({ + include: { + bank: true, + }, where: { id: institutionId }, }); + + await Promise.all([ + ...data.bank.map((v) => deleteFile(fileLocation.institution.bank(institutionId, v.id))), + ]); + + return data; }); } } @@ -294,4 +395,49 @@ export class InstitutionFileController extends Controller { await this.checkPermission(req.user, institutionId); return await deleteFile(fileLocation.institution.attachment(institutionId, name)); } + + @Get("bank-qr/{bankId}") + async getBankImage( + @Request() req: RequestWithUser, + @Path() institutionId: string, + @Path() bankId: string, + ) { + return req.res?.redirect(await getFile(fileLocation.institution.bank(institutionId, bankId))); + } + + @Head("bank-qr/{bankId}") + async headBankImage( + @Request() req: RequestWithUser, + @Path() institutionId: string, + @Path() bankId: string, + ) { + return req.res?.redirect( + await getPresigned("head", fileLocation.institution.bank(institutionId, bankId)), + ); + } + + @Put("bank-qr/{bankId}") + @Security("keycloak") + async putBankImage( + @Request() req: RequestWithUser, + @Path() institutionId: string, + @Path() bankId: string, + ) { + if (!req.headers["content-type"]?.startsWith("image/")) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Not a valid image.", "notValidImage"); + } + await this.checkPermission(req.user, institutionId); + return req.res?.redirect(await setFile(fileLocation.institution.bank(institutionId, bankId))); + } + + @Delete("bank-qr/{bankId}") + @Security("keycloak") + async delBankImage( + @Request() req: RequestWithUser, + @Path() institutionId: string, + @Path() bankId: string, + ) { + await this.checkPermission(req.user, institutionId); + return await deleteFile(fileLocation.institution.bank(institutionId, bankId)); + } } diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts index df2252c..4484ce3 100644 --- a/src/controllers/06-request-list-controller.ts +++ b/src/controllers/06-request-list-controller.ts @@ -85,7 +85,7 @@ export class RequestDataController extends Controller { OR: queryOrNot(query, [ { code: { contains: query, mode: "insensitive" } }, { quotation: { code: { contains: query, mode: "insensitive" } } }, - { quotation: { workName: { contains: query } } }, + { quotation: { workName: { contains: query, mode: "insensitive" } } }, { quotation: { customerBranch: { diff --git a/src/utils/minio.ts b/src/utils/minio.ts index e80108d..d4ecda5 100644 --- a/src/utils/minio.ts +++ b/src/utils/minio.ts @@ -127,6 +127,8 @@ export const fileLocation = { `${ROOT}/institution/attachment-${institutionId}/${name || ""}`, img: (institutionId: string, name?: string) => `${ROOT}/institution/img-${institutionId}/${name || ""}`, + bank: (institutionId: string, bankId: string) => + `${ROOT}/institution/bank-qr-${institutionId}-${bankId}`, }, task: { attachment: (taskId: string, name?: string) =>