import { Prisma, Status, UserType } from "@prisma/client"; import { Body, Controller, Delete, Get, Put, Path, Post, Query, Request, Route, Security, Tags, } from "tsoa"; 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"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); } const MINIO_BUCKET = process.env.MINIO_BUCKET; type BranchCreate = { status?: Status; taxNo: string; nameEN: string; name: string; addressEN: string; address: string; zipCode: string; email: string; telephoneNo: string; lineId: string; longitude: string; latitude: string; subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; headOfficeId?: string | null; }; type BranchUpdate = { status?: "ACTIVE" | "INACTIVE"; taxNo?: string; nameEN?: string; name?: string; addressEN?: string; address?: string; zipCode?: string; email?: string; telephoneNo?: string; lineId?: string; longitude?: string; latitude?: string; subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; headOfficeId?: string | null; }; function lineImageLoc(id: string) { return `branch/line-qr-${id}`; } @Route("api/branch") @Tags("Branch") @Security("keycloak") export class BranchController extends Controller { @Get("stats") async getStats() { const list = await prisma.branch.groupBy({ _count: true, by: "isHeadOffice", }); return list.reduce>( (a, c) => { a[c.isHeadOffice ? "hq" : "br"] = c._count; return a; }, { hq: 0, br: 0 }, ); } @Get("user-stats") async getUserStat(@Query() userType?: UserType) { const list = await prisma.branchUser.groupBy({ _count: true, where: { user: { userType } }, by: "branchId", }); const record = await prisma.branch.findMany({ select: { id: true, nameEN: true, name: true, isHeadOffice: true, }, }); return record.map((a) => Object.assign(a, { count: list.find((b) => b.branchId === a.id)?._count ?? 0, }), ); } @Get() async getBranch( @Query() zipCode?: string, @Query() filter?: "head" | "sub", @Query() headOfficeId?: string, @Query() tree?: boolean, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, ) { const where = { AND: { headOfficeId: headOfficeId ?? (filter === "head" || tree ? null : undefined), NOT: { headOfficeId: filter === "sub" && !headOfficeId ? null : undefined }, }, OR: [ { nameEN: { contains: query }, zipCode }, { name: { contains: query }, zipCode }, { email: { contains: query }, zipCode }, ], } satisfies Prisma.BranchWhereInput; const [result, total] = await prisma.$transaction([ prisma.branch.findMany({ orderBy: { createdAt: "asc" }, include: { province: true, district: true, subDistrict: true, branch: tree && { include: { province: true, district: true, subDistrict: true, }, }, }, where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.branch.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{branchId}") async getBranchById( @Path() branchId: string, @Query() includeSubBranch?: boolean, @Query() includeContact?: boolean, ) { const record = await prisma.branch.findFirst({ include: { province: true, district: true, subDistrict: true, branch: includeSubBranch && { include: { province: true, district: true, subDistrict: true, }, }, contact: includeContact, }, where: { id: branchId }, }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } return { ...record, contact: record.contact ? await Promise.all( record.contact.map(async (v) => Object.assign(v, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, `branch/contact-${record.id}`, ), }), ), ) : undefined, }; } @Post() async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) { const [province, district, subDistrict, head] = 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.findFirst({ where: { id: body.headOfficeId || undefined } }), ]); if (body.provinceId && !province) throw new HttpError( HttpStatus.BAD_REQUEST, "Province cannot be found.", "missing_or_invalid_parameter", ); if (body.districtId && !district) throw new HttpError( HttpStatus.BAD_REQUEST, "District cannot be found.", "missing_or_invalid_parameter", ); if (body.subDistrictId && !subDistrict) throw new HttpError( HttpStatus.BAD_REQUEST, "Sub-district cannot be found.", "missing_or_invalid_parameter", ); if (body.headOfficeId && !head) throw new HttpError( HttpStatus.BAD_REQUEST, "Head branch cannot be found.", "missing_or_invalid_parameter", ); const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; const year = new Date().getFullYear(); const last = await prisma.branch.findFirst({ orderBy: { createdAt: "desc" }, where: { headOfficeId: headOfficeId ?? null }, }); const code = !headOfficeId ? `HQ${year.toString().slice(2)}${+(last?.code.slice(-1) || 0) + 1}` : `BR${head?.code.slice(2, 5)}${(+(last?.code.slice(-2) || 0) + 1).toString().padStart(2, "0")}`; const record = await prisma.branch.create({ include: { province: true, district: true, subDistrict: true, }, data: { ...rest, code, isHeadOffice: !headOfficeId, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined }, createdBy: req.user.name, updateBy: req.user.name, }, }); if (headOfficeId) { await prisma.branch.updateMany({ where: { id: headOfficeId, status: Status.CREATED }, data: { status: Status.ACTIVE }, }); } this.setStatus(HttpStatus.CREATED); return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, lineImageLoc(record.id), 12 * 60 * 60, ), qrCodeImageUploadUrl: await minio.presignedPutObject( MINIO_BUCKET, lineImageLoc(record.id), 12 * 60 * 60, ), }); } @Put("{branchId}") async editBranch( @Request() req: RequestWithUser, @Body() body: BranchUpdate, @Path() branchId: string, ) { if (body.headOfficeId === branchId) throw new HttpError( HttpStatus.BAD_REQUEST, "Cannot make this as head office and branch at the same time.", "missing_or_invalid_parameter", ); if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) { 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.findFirst({ where: { id: body.headOfficeId || undefined } }), ]); if (body.provinceId && !province) throw new HttpError( HttpStatus.BAD_REQUEST, "Province cannot be found.", "missing_or_invalid_parameter", ); if (body.districtId && !district) throw new HttpError( HttpStatus.BAD_REQUEST, "District cannot be found.", "missing_or_invalid_parameter", ); if (body.subDistrictId && !subDistrict) throw new HttpError( HttpStatus.BAD_REQUEST, "Sub-district cannot be found.", "missing_or_invalid_parameter", ); if (body.headOfficeId && !branch) throw new HttpError( HttpStatus.BAD_REQUEST, "Head branch cannot be found.", "missing_or_invalid_parameter", ); } const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } const record = await prisma.branch.update({ include: { province: true, district: true, subDistrict: true }, data: { ...rest, isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined, province: { connect: provinceId ? { id: provinceId } : undefined, disconnect: provinceId === null || undefined, }, district: { connect: districtId ? { id: districtId } : undefined, disconnect: districtId === null || undefined, }, subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined, disconnect: subDistrictId === null || undefined, }, headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined, disconnect: headOfficeId === null || undefined, }, updateBy: req.user.name, }, where: { id: branchId }, }); return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, lineImageLoc(record.id), 12 * 60 * 60, ), qrCodeImageUploadUrl: await minio.presignedPutObject( MINIO_BUCKET, lineImageLoc(record.id), 12 * 60 * 60, ), }); } @Delete("{branchId}") async deleteBranch(@Path() branchId: string) { const record = await prisma.branch.findFirst({ include: { province: true, district: true, subDistrict: true, }, where: { id: branchId }, }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used"); } await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), { forceDelete: true, }); return await prisma.branch.delete({ include: { province: true, district: true, subDistrict: true, }, where: { id: branchId }, }); } }