jws-backend/src/controllers/03-employee-controller.ts
2024-11-13 14:39:42 +07:00

895 lines
26 KiB
TypeScript

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,
} 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",
"head_of_accountant",
"accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCond = createPermCondition(globalAllow);
const permissionCheck = createPermCheck(globalAllow);
type EmployeeCreate = {
customerBranchId: string;
status?: Status;
nrcNo?: string | null;
dateOfBirth: Date;
gender: string;
nationality: string;
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;
gender?: string;
nationality?: string;
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(@Query() customerBranchId?: string) {
return await prisma.employee.count({
where: { customerBranchId },
});
}
@Get("stats/gender")
@Security("keycloak")
async getEmployeeStatsGender(
@Request() req: RequestWithUser,
@Query() customerBranchId?: string,
@Query() status?: Status,
@Query() query: string = "",
) {
return await prisma.employee
.groupBy({
_count: true,
by: ["gender"],
where: {
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query } },
},
},
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]),
AND: {
...filterStatus(status),
customerBranchId,
customerBranch: {
customer: isSystem(req.user)
? undefined
: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
},
},
})
.then((res) =>
res.reduce<Record<string, number>>((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,
) {
return this.listByCriteria(
req,
zipCode,
gender,
status,
visa,
passport,
customerId,
customerBranchId,
query,
page,
pageSize,
);
}
@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,
@Body()
body?: {
passport?: string[];
},
) {
const where = {
OR:
!!query || !!body
? [
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query } },
},
},
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]) ?? []),
...(queryOrNot<Prisma.EmployeeWhereInput[]>(!!body, [
{
employeePassport: body?.passport
? { some: { number: { in: body?.passport } } }
: undefined,
},
]) ?? []),
]
: undefined,
AND: {
...filterStatus(status),
customerBranch: {
id: customerBranchId,
customerId,
customer: isSystem(req.user)
? undefined
: {
registeredBranch: {
OR: permissionCond(req.user),
},
},
},
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
} 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 !== province?.id) throw relationError("Province");
if (body.districtId !== district?.id) throw relationError("District");
if (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 permissionCheck(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));
}
}