jws-backend/src/controllers/03-employee-controller.ts
Methapon Metanipat d5ab07bacb refactor: upload now return presigned url instead of redirect
This will prevent from payload upload to API Server first and then
upload to MinIO which cause upload to be happens twice.
2024-10-24 09:56:37 +07:00

830 lines
24 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, 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_account",
"account",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const allowList = ["system", "head_of_admin", "head_of_account", "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;
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;
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: [
{ 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() customerId?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
],
AND: {
...filterStatus(status),
customerBranch: {
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: {
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,
employeeInCountryNotice: 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.id}-${`${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)),
deleteFolder(fileLocation.employee.inCountryNotice(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));
}
}