import { Prisma, Status } from "@prisma/client"; import { Body, Controller, Delete, Get, Head, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import { RequestWithUser } from "../interfaces/user"; import prisma from "../db"; import HttpStatus from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; import { isSystem } from "../utils/keycloak"; import { filterStatus } from "../services/prisma"; import { branchRelationPermInclude, createPermCheck, createPermCondition, } from "../services/permission"; import { connectOrDisconnect, connectOrNot, queryOrNot, whereAddressQuery, whereDateQuery, } from "../utils/relation"; import { isUsedError, notFoundError, relationError } from "../utils/error"; import { deleteFile, deleteFolder, fileLocation, getFile, getPresigned, listFile, setFile, } from "../utils/minio"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); } const MANAGE_ROLES = [ "system", "head_of_admin", "admin", "executive", "accountant", "branch_admin", "branch_manager", "branch_accountant", "head_of_sale", "sale", ]; function globalAllow(user: RequestWithUser["user"]) { const listAllowed = MANAGE_ROLES; return user.roles?.some((v) => listAllowed.includes(v)) || false; } const permissionCondCompany = createPermCondition((_) => true); const permissionCond = createPermCondition(globalAllow); const permissionCheckCompany = createPermCheck((_) => true); const permissionCheck = createPermCheck(globalAllow); type EmployeeCreate = { customerBranchId: string; status?: Status; nrcNo?: string | null; dateOfBirth?: Date | null; gender: string; nationality: string; otherNationality?: string | null; namePrefix?: string | null; firstName?: string; firstNameEN: string; middleName?: string | null; middleNameEN?: string | null; lastName?: string; lastNameEN: 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 | null; districtId?: string | null; provinceId?: string | null; workerType?: string | null; workerStatus?: string | null; selectedImage?: string | null; }; type EmployeeUpdate = { customerBranchId?: string; status?: "ACTIVE" | "INACTIVE"; nrcNo?: string | null; dateOfBirth?: Date | null; gender?: string; nationality?: string; otherNationality?: string | null; namePrefix?: string | null; firstName?: string; firstNameEN: string; middleName?: string | null; middleNameEN?: string | null; lastName?: string; lastNameEN?: string; addressEN?: string; address?: string; soi?: string | null; soiEN?: string | null; moo?: string | null; mooEN?: string | null; street?: string | null; streetEN?: string | null; workerType?: string | null; workerStatus?: string | null; selectedImage?: string | null; subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; }; @Route("api/v1/employee") @Tags("Employee") export class EmployeeController extends Controller { @Get("stats") @Security("keycloak") async getEmployeeStats(@Request() req: RequestWithUser, @Query() customerBranchId?: string) { return await prisma.employee.count({ where: { customerBranchId, customerBranch: { customer: isSystem(req.user) ? undefined : { registeredBranch: { OR: permissionCond(req.user) }, }, }, }, }); } @Get("stats/gender") @Security("keycloak") async getEmployeeStatsGender( @Request() req: RequestWithUser, @Query() customerBranchId?: string, @Query() status?: Status, @Query() query: string = "", @Query() startDate?: Date, @Query() endDate?: Date, ) { return await prisma.employee .groupBy({ _count: true, by: ["gender"], where: { OR: queryOrNot(query, [ { employeePassport: { some: { number: { contains: query, mode: "insensitive" } }, }, }, { firstName: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } }, { lastNameEN: { contains: query, mode: "insensitive" } }, ...whereAddressQuery(query), ]), AND: { ...filterStatus(status), customerBranchId, customerBranch: { customer: isSystem(req.user) ? undefined : { registeredBranch: { OR: permissionCond(req.user) }, }, }, }, ...whereDateQuery(startDate, endDate), }, }) .then((res) => res.reduce>((a, c) => { a[c.gender] = c._count; return a; }, {}), ); } @Get() @Security("keycloak") async list( @Request() req: RequestWithUser, @Query() zipCode?: string, @Query() gender?: string, @Query() status?: Status, @Query() visa?: boolean, @Query() passport?: boolean, @Query() customerId?: string, @Query() customerBranchId?: string, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, @Query() activeOnly?: boolean, @Query() startDate?: Date, @Query() endDate?: Date, ) { return this.listByCriteria( req, zipCode, gender, status, visa, passport, customerId, customerBranchId, query, page, pageSize, activeOnly, startDate, endDate, ); } @Post("list") @Security("keycloak") async listByCriteria( @Request() req: RequestWithUser, @Query() zipCode?: string, @Query() gender?: string, @Query() status?: Status, @Query() visa?: boolean, @Query() passport?: boolean, @Query() customerId?: string, @Query() customerBranchId?: string, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, @Query() activeOnly?: boolean, @Query() startDate?: Date, @Query() endDate?: Date, @Body() body?: { passport?: string[]; }, ) { const where = { OR: !!query || !!body ? [ ...(queryOrNot(query, [ { employeePassport: { some: { number: { contains: query, mode: "insensitive" } }, }, }, { firstName: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } }, { lastNameEN: { contains: query, mode: "insensitive" } }, ...whereAddressQuery(query), ]) ?? []), ...(queryOrNot(!!body, [ { employeePassport: body?.passport ? { some: { number: { in: body?.passport } } } : undefined, }, ]) ?? []), ] : undefined, AND: { ...filterStatus(activeOnly ? Status.ACTIVE : status), customerBranch: { status: activeOnly ? { not: Status.INACTIVE } : undefined, id: customerBranchId, customerId, customer: isSystem(req.user) ? { status: activeOnly ? { not: Status.INACTIVE } : undefined } : { status: activeOnly ? { not: Status.INACTIVE } : undefined, registeredBranch: { OR: permissionCond(req.user, { activeOnly: activeOnly }), }, }, }, subDistrict: zipCode ? { zipCode } : undefined, gender, }, ...whereDateQuery(startDate, endDate), } satisfies Prisma.EmployeeWhereInput; const [result, total] = await prisma.$transaction([ prisma.employee.findMany({ orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], include: { employeePassport: passport ? { orderBy: { expireDate: "desc" } } : undefined, employeeVisa: visa ? { orderBy: { expireDate: "desc" } } : undefined, province: true, district: true, subDistrict: true, customerBranch: { include: { customer: true }, }, createdBy: true, updatedBy: true, }, where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.employee.count({ where }), ]); return { result, page, pageSize, total, }; } @Get("{employeeId}") @Security("keycloak") async getById(@Path() employeeId: string) { const record = await prisma.employee.findFirst({ include: { employeePassport: true, employeeVisa: true, employeeWork: true, employeeCheckup: true, employeeOtherInfo: true, province: true, district: true, subDistrict: true, createdBy: true, updatedBy: true, }, where: { id: employeeId }, }); if (!record) throw notFoundError("Employee"); return record; } @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 } }), prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.customerBranch.findFirst({ where: { id: body.customerBranchId }, include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, }), ]); 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); const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body; const record = await prisma.$transaction( async (tx) => { const last = await tx.runningNo.upsert({ where: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, }, create: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, value: 1, }, update: { value: { increment: 1 } }, }); return await prisma.employee.create({ include: { province: true, district: true, subDistrict: true, employeeOtherInfo: true, employeeCheckup: { include: { province: true, }, }, employeeWork: true, createdBy: true, updatedBy: true, }, data: { ...rest, statusOrder: +(rest.status === "INACTIVE"), code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${last.value}`.padStart(7, "0")}`, province: connectOrNot(provinceId), district: connectOrNot(districtId), subDistrict: connectOrNot(subDistrictId), customerBranch: { connect: { id: customerBranchId } }, createdBy: { connect: { id: req.user.sub } }, updatedBy: { connect: { id: req.user.sub } }, }, }); }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, ); await prisma.customerBranch.updateMany({ where: { id: customerBranchId, status: Status.CREATED }, data: { status: Status.ACTIVE }, }); await prisma.customer.updateMany({ where: { branch: { some: { id: customerBranchId }, }, status: Status.CREATED, }, data: { status: Status.ACTIVE }, }); this.setStatus(HttpStatus.CREATED); return record; } @Put("{employeeId}") @Security("keycloak", MANAGE_ROLES) async editById( @Request() req: RequestWithUser, @Body() body: EmployeeUpdate, @Path() employeeId: string, ) { const [province, district, subDistrict, customerBranch, employee] = 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.customerBranch.findFirst({ where: { id: body.customerBranchId }, include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, }), prisma.employee.findFirst({ where: { id: employeeId }, include: { customerBranch: { include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, }, }, }), ]); if (body.provinceId && !province) throw relationError("Province"); if (body.districtId && !district) throw relationError("District"); if (body.subDistrictId && !subDistrict) throw relationError("SubDistrict"); if (body.customerBranchId && !customerBranch) throw relationError("Customer"); if (!employee) throw notFoundError("Employee"); await permissionCheck(req.user, employee.customerBranch.customer.registeredBranch); if (body.customerBranchId && customerBranch) { await permissionCheck(req.user, customerBranch.customer.registeredBranch); } const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body; const record = await prisma.$transaction(async (tx) => { let code: string | undefined; if ( customerBranchId !== undefined && customerBranch && customerBranch.id !== employee.customerBranchId ) { const last = await tx.runningNo.upsert({ where: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, }, create: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, value: 1, }, update: { value: { increment: 1 } }, }); code = `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${last.value}`.padStart(7, "0")}`; } return await prisma.employee.update({ where: { id: employeeId }, include: { province: true, district: true, subDistrict: true, employeeOtherInfo: true, employeeCheckup: { include: { province: true, }, }, employeeWork: true, createdBy: true, updatedBy: true, }, data: { ...rest, statusOrder: +(rest.status === "INACTIVE"), code, customerBranch: connectOrNot(customerBranchId), province: connectOrDisconnect(provinceId), district: connectOrDisconnect(districtId), subDistrict: connectOrDisconnect(subDistrictId), createdBy: { connect: { id: req.user.sub } }, updatedBy: { connect: { id: req.user.sub } }, }, }); }); const historyEntries: { field: string; valueBefore: string; valueAfter: string }[] = []; for (const field of Object.keys(body) as (keyof typeof body)[]) { let valueBefore = employee[field]; let valueAfter = body[field]; if (valueBefore === undefined && valueAfter === undefined) continue; if (valueBefore instanceof Date) valueBefore = valueBefore.toISOString(); if (valueBefore === null || valueBefore === undefined) valueBefore = ""; if (valueAfter instanceof Date) valueAfter = valueAfter.toISOString(); if (valueAfter === null || valueAfter === undefined) valueAfter = ""; if (valueBefore !== valueAfter) historyEntries.push({ field, valueBefore, valueAfter }); } await prisma.employeeHistory.createMany({ data: historyEntries.map((v) => ({ ...v, updatedByUserId: req.user.sub, masterId: employee.id, })), }); return record; } @Delete("{employeeId}") @Security("keycloak", MANAGE_ROLES) async delete(@Request() req: RequestWithUser, @Path() employeeId: string) { const record = await prisma.employee.findFirst({ where: { id: employeeId }, include: { customerBranch: { include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, }, }, }); if (!record) throw notFoundError("Employee"); await permissionCheck(req.user, record.customerBranch.customer.registeredBranch); if (record.status !== Status.CREATED) throw isUsedError("Employee"); await Promise.all([ deleteFolder(fileLocation.employee.img(employeeId)), deleteFolder(fileLocation.employee.attachment(employeeId)), deleteFolder(fileLocation.employee.passport(employeeId)), deleteFolder(fileLocation.employee.visa(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 }, }); } } @Route("api/v1/employee/{employeeId}") export class EmployeeFileController extends Controller { private async checkPermission(user: RequestWithUser["user"], id: string) { const data = await prisma.employee.findFirst({ where: { id }, include: { customerBranch: { include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(user), }, }, }, }, }, }, }); if (!data) throw notFoundError("Employee"); await permissionCheckCompany(user, data.customerBranch.customer.registeredBranch); } @Get("image") @Security("keycloak") @Tags("Employee") async listImage(@Request() req: RequestWithUser, @Path() employeeId: string) { await this.checkPermission(req.user, employeeId); return await listFile(fileLocation.employee.img(employeeId)); } @Get("image/{name}") @Tags("Employee") async getImage( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() name: string, ) { return req.res?.redirect(await getFile(fileLocation.employee.img(employeeId, name))); } @Head("image/{name}") @Tags("Employee") async headImage( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() name: string, ) { return req.res?.redirect( await getPresigned("head", fileLocation.employee.img(employeeId, name)), ); } @Put("image/{name}") @Security("keycloak") @Tags("Employee") async putImage( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() name: string, ) { if (!req.headers["content-type"]?.startsWith("image/")) { throw new HttpError(HttpStatus.BAD_REQUEST, "Not a valid image.", "notValidImage"); } await this.checkPermission(req.user, employeeId); return req.res?.redirect(await setFile(fileLocation.employee.img(employeeId, name))); } @Delete("image/{name}") @Security("keycloak") @Tags("Employee") async delImage( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() name: string, ) { await this.checkPermission(req.user, employeeId); return await deleteFile(fileLocation.employee.img(employeeId, name)); } @Get("attachment") @Security("keycloak") @Tags("Employee") async listAttachment(@Request() req: RequestWithUser, @Path() employeeId: string) { await this.checkPermission(req.user, employeeId); return await listFile(fileLocation.employee.attachment(employeeId)); } @Get("attachment/{name}") @Security("keycloak") @Tags("Employee") async getAttachment(@Path() employeeId: string, @Path() name: string) { return await getFile(fileLocation.employee.attachment(employeeId, name)); } @Head("attachment/{name}") @Security("keycloak") @Tags("Employee") async headAttachment(@Path() employeeId: string, @Path() name: string) { return await getPresigned("head", fileLocation.employee.attachment(employeeId, name)); } @Put("attachment/{name}") @Security("keycloak") @Tags("Employee") async putAttachment( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() name: string, ) { await this.checkPermission(req.user, employeeId); return await setFile(fileLocation.employee.attachment(employeeId, name)); } @Delete("attachment/{name}") @Security("keycloak") @Tags("Employee") async delAttachment( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() name: string, ) { await this.checkPermission(req.user, employeeId); return await deleteFile(fileLocation.employee.attachment(employeeId, name)); } @Get("file-passport") @Security("keycloak") @Tags("Employee Passport") async listPassport(@Request() req: RequestWithUser, @Path() employeeId: string) { await this.checkPermission(req.user, employeeId); return await listFile(fileLocation.employee.passport(employeeId)); } @Get("file-passport/{passportId}") @Security("keycloak") @Tags("Employee Passport") async getPassport(@Path() employeeId: string, @Path() passportId: string) { return await getFile(fileLocation.employee.passport(employeeId, passportId)); } @Head("file-passport/{passportId}") @Security("keycloak") @Tags("Employee Passport") async headPassport(@Path() employeeId: string, @Path() passportId: string) { return await getPresigned("head", fileLocation.employee.passport(employeeId, passportId)); } @Put("file-passport/{passportId}") @Security("keycloak") @Tags("Employee Passport") async putPassport( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() passportId: string, ) { await this.checkPermission(req.user, employeeId); return req.res?.redirect(await setFile(fileLocation.employee.passport(employeeId, passportId))); } @Delete("file-passport/{passportId}") @Security("keycloak") @Tags("Employee Passport") async delPassport( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() passportId: string, ) { await this.checkPermission(req.user, employeeId); return await deleteFile(fileLocation.employee.passport(employeeId, passportId)); } @Get("file-visa") @Security("keycloak") @Tags("Employee Visa") async listVisa(@Request() req: RequestWithUser, @Path() employeeId: string) { await this.checkPermission(req.user, employeeId); return await listFile(fileLocation.employee.visa(employeeId)); } @Get("file-visa/{visaId}") @Security("keycloak") @Tags("Employee Visa") async getVisa(@Path() employeeId: string, @Path() visaId: string) { return await getFile(fileLocation.employee.visa(employeeId, visaId)); } @Head("file-visa/{visaId}") @Security("keycloak") @Tags("Employee Visa") async headVisa(@Path() employeeId: string, @Path() visaId: string) { return await getPresigned("head", fileLocation.employee.visa(employeeId, visaId)); } @Put("file-visa/{visaId}") @Security("keycloak") @Tags("Employee Visa") async putVisa( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() visaId: string, ) { await this.checkPermission(req.user, employeeId); return req.res?.redirect(await setFile(fileLocation.employee.visa(employeeId, visaId))); } @Delete("file-visa/{visaId}") @Security("keycloak") @Tags("Employee Visa") async delVisa( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() visaId: string, ) { await this.checkPermission(req.user, employeeId); return await deleteFile(fileLocation.employee.visa(employeeId, visaId)); } @Get("file-in-country-notice") @Security("keycloak") @Tags("Employee In Country Notice") async listNotice(@Request() req: RequestWithUser, @Path() employeeId: string) { await this.checkPermission(req.user, employeeId); return await listFile(fileLocation.employee.inCountryNotice(employeeId)); } @Get("file-in-country-notice/{noticeId}") @Security("keycloak") @Tags("Employee In Country Notice") async getNotice( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() noticeId: string, ) { await this.checkPermission(req.user, employeeId); return await getFile(fileLocation.employee.inCountryNotice(employeeId, noticeId)); } @Head("file-in-country-notice/{noticeId}") @Security("keycloak") @Tags("Employee In Country Notice") async headNotice( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() noticeId: string, ) { await this.checkPermission(req.user, employeeId); return await getPresigned("head", fileLocation.employee.inCountryNotice(employeeId, noticeId)); } @Put("file-in-country-notice/{noticeId}") @Security("keycloak") @Tags("Employee In Country Notice") async putNotice( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() noticeId: string, ) { await this.checkPermission(req.user, employeeId); return req.res?.redirect( await setFile(fileLocation.employee.inCountryNotice(employeeId, noticeId)), ); } @Delete("file-in-country-notice/{noticeId}") @Security("keycloak") @Tags("Employee In Country Notice") async delNotice( @Request() req: RequestWithUser, @Path() employeeId: string, @Path() noticeId: string, ) { await this.checkPermission(req.user, employeeId); return await deleteFile(fileLocation.employee.inCountryNotice(employeeId, noticeId)); } }