jws-backend/src/controllers/07-task-controller.ts

859 lines
24 KiB
TypeScript
Raw Normal View History

import {
Body,
Controller,
Delete,
Get,
2024-12-06 17:36:45 +07:00
Head,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import { notFoundError } from "../utils/error";
import {
Prisma,
QuotationStatus,
RequestDataStatus,
RequestWorkStatus,
TaskOrderStatus,
TaskStatus,
UserTaskStatus,
} from "@prisma/client";
import { RequestWithUser } from "../interfaces/user";
2024-12-03 17:11:44 +07:00
import {
branchRelationPermInclude,
createPermCheck,
createPermCondition,
} from "../services/permission";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
2024-12-06 17:36:45 +07:00
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import { queryOrNot } from "../utils/relation";
2024-12-03 17:11:44 +07:00
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"];
function globalAllow(user: RequestWithUser["user"]) {
const allowList = ["system", "head_of_admin"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
@Route("/api/v1/task-order")
@Tags("Task Order")
export class TaskController extends Controller {
2024-12-03 09:37:35 +07:00
@Get("stats")
async getTaskOrderStats() {
const task = await prisma.taskOrder.groupBy({
2024-12-10 10:03:51 +07:00
by: ["taskOrderStatus"],
2024-12-03 09:37:35 +07:00
_count: true,
});
2024-12-10 10:03:51 +07:00
return task.reduce<Record<TaskOrderStatus, number>>(
(a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }),
2024-12-03 10:20:45 +07:00
{
2024-12-10 10:03:51 +07:00
[TaskOrderStatus.Pending]: 0,
[TaskOrderStatus.InProgress]: 0,
[TaskOrderStatus.Validate]: 0,
[TaskOrderStatus.Complete]: 0,
[TaskOrderStatus.Canceled]: 0,
2024-12-03 10:20:45 +07:00
},
2024-12-03 09:37:35 +07:00
);
}
@Get()
@Security("keycloak")
async getTaskOrderList(
2024-12-03 17:11:44 +07:00
@Request() req: RequestWithUser,
@Query() query: string = "",
@Query() page = 1,
@Query() pageSize = 30,
@Query() assignedByUserId?: string,
2024-12-10 10:03:51 +07:00
@Query() taskOrderStatus?: TaskOrderStatus,
2024-12-06 13:21:57 +07:00
) {
return this.getTaskOrderListByCriteria(
req,
query,
page,
pageSize,
assignedByUserId,
taskOrderStatus,
);
2024-12-06 13:21:57 +07:00
}
@Post("list")
@Security("keycloak")
async getTaskOrderListByCriteria(
@Request() req: RequestWithUser,
@Query() query: string = "",
@Query() page = 1,
@Query() pageSize = 30,
@Query() assignedUserId?: string,
2024-12-10 10:03:51 +07:00
@Query() taskOrderStatus?: TaskOrderStatus,
2024-12-06 13:21:57 +07:00
@Body() body?: { code?: string[] },
) {
const where = {
taskOrderStatus,
registeredBranch: { OR: permissionCondCompany(req.user) },
taskList: assignedUserId
? {
some: {
requestWorkStep: { responsibleUserId: assignedUserId },
},
}
: undefined,
code: body?.code ? { in: body.code } : undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query } },
{ contactName: { contains: query } },
{ contactTel: { contains: query } },
]),
} satisfies Prisma.TaskOrderWhereInput;
const [result, total] = await prisma.$transaction([
prisma.taskOrder.findMany({
where,
include: {
2024-12-20 11:30:34 +07:00
userTask: true,
taskList: true,
institution: true,
registeredBranch: true,
createdBy: true,
},
}),
prisma.taskOrder.count({ where }),
]);
return { result, total, page, pageSize };
}
@Get("{taskOrderId}")
@Security("keycloak")
async getTaskOrder(
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
@Query() taskAssignedUserId?: string,
) {
const record = await prisma.taskOrder.findFirst({
where: { id: taskOrderId },
include: {
userTask: true,
2024-12-24 18:11:40 +07:00
taskProduct: true,
taskList: {
where: {
requestWorkStep: { responsibleUserId: taskAssignedUserId },
},
include: {
2024-12-10 10:03:51 +07:00
requestWorkStep: {
include: {
2024-12-11 16:41:57 +07:00
responsibleUser: true,
2024-12-10 10:03:51 +07:00
requestWork: {
include: {
2024-12-10 10:03:51 +07:00
request: {
include: {
employee: true,
quotation: true,
},
},
productService: {
include: {
service: {
include: {
workflow: {
include: {
step: {
include: {
value: true,
responsiblePerson: {
include: { user: true },
},
responsibleInstitution: true,
},
},
},
},
},
},
2024-12-10 10:03:51 +07:00
work: true,
product: true,
},
},
2024-12-06 14:37:23 +07:00
},
},
},
},
},
},
institution: true,
registeredBranch: true,
createdBy: true,
},
});
if (!record) throw notFoundError("Task Order");
return record;
}
@Post()
2024-12-03 17:11:44 +07:00
@Security("keycloak", MANAGE_ROLES)
async createTaskOrderList(
@Request() req: RequestWithUser,
@Body()
body: {
taskName: string;
contactName: string;
contactTel: string;
institutionId: string;
2024-12-03 17:11:44 +07:00
registeredBranchId?: string;
taskList: { requestWorkId: string; step: number }[];
2024-12-24 18:11:40 +07:00
taskProduct: { productId: string; discount?: number }[];
},
) {
return await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: "TASK",
},
create: {
key: "TASK",
value: 1,
},
update: {
value: { increment: 1 },
},
});
const current = new Date();
const year = `${current.getFullYear()}`.slice(-2).padStart(2, "0");
const month = `${current.getMonth() + 1}`.padStart(2, "0");
const code = `PO${year}${month}${last.value.toString().padStart(6, "0")}`;
2024-12-24 18:11:40 +07:00
const { taskList, taskProduct, ...rest } = body;
2024-12-03 17:11:44 +07:00
const userAffiliatedBranch = await tx.branch.findFirst({
include: branchRelationPermInclude(req.user),
where: body.registeredBranchId
? { id: body.registeredBranchId }
: {
user: { some: { userId: req.user.sub } },
},
});
if (!userAffiliatedBranch) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
"reqMinAffilatedBranch",
);
}
await permissionCheckCompany(req.user, userAffiliatedBranch);
const updated = await tx.requestWorkStepStatus.updateMany({
where: {
OR: taskList,
workStatus: RequestWorkStatus.Ready,
},
data: { workStatus: RequestWorkStatus.InProgress },
});
if (updated.count !== taskList.length) {
throw new HttpError(
HttpStatus.PRECONDITION_FAILED,
"All request work to issue task order must be in ready state.",
"requestWorkMustReady",
);
}
2024-12-06 11:25:38 +07:00
return await tx.taskOrder.create({
include: {
taskList: {
include: {
2024-12-10 10:03:51 +07:00
requestWorkStep: {
include: {
2024-12-10 10:03:51 +07:00
requestWork: {
include: {
2024-12-10 10:03:51 +07:00
request: {
include: {
employee: true,
quotation: true,
},
},
productService: {
include: {
service: {
include: {
workflow: {
include: {
step: {
include: {
value: true,
responsiblePerson: {
include: { user: true },
},
responsibleInstitution: true,
},
},
},
},
},
},
2024-12-10 10:03:51 +07:00
work: true,
product: true,
},
},
},
},
},
},
},
},
institution: true,
createdBy: true,
},
data: {
...rest,
code,
2024-12-03 17:11:44 +07:00
registeredBranchId: userAffiliatedBranch.id,
createdByUserId: req.user.sub,
2024-12-10 10:03:51 +07:00
taskList: { create: taskList },
2024-12-24 18:11:40 +07:00
taskProduct: { create: taskProduct },
},
});
});
}
@Put("{taskOrderId}")
@Security("keycloak")
async editTaskById(
2024-12-03 17:11:44 +07:00
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
@Body()
body: {
taskName: string;
taskOrderStatus?: TaskOrderStatus;
contactName: string;
contactTel: string;
institutionId: string;
taskList: { requestWorkId: string; step: number }[];
2024-12-24 18:11:40 +07:00
taskProduct: { productId: string; discount?: number }[];
},
) {
const record = await prisma.taskOrder.findFirst({
where: { id: taskOrderId },
include: {
2024-12-03 17:11:44 +07:00
registeredBranch: { include: branchRelationPermInclude(req.user) },
taskList: {
2024-12-10 10:03:51 +07:00
include: {
requestWorkStep: {
include: { requestWork: true },
},
},
},
institution: true,
createdBy: true,
},
});
2024-12-03 17:11:44 +07:00
if (!record) throw notFoundError("Task Order");
await permissionCheckCompany(req.user, record.registeredBranch);
await prisma.taskOrder.update({
where: { id: taskOrderId },
include: {
taskList: {
include: {
2024-12-10 10:03:51 +07:00
requestWorkStep: {
include: {
requestWork: true,
},
},
},
},
institution: true,
2024-12-03 17:11:44 +07:00
registeredBranch: true,
createdBy: true,
},
data: {
...body,
taskList: {
2024-12-10 10:03:51 +07:00
deleteMany: record?.taskList.filter(
(lhs) =>
!body.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
),
createMany: {
2024-12-16 10:50:14 +07:00
data: body.taskList.filter(
(lhs) =>
!record?.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
),
2024-12-10 10:03:51 +07:00
skipDuplicates: true,
},
},
2024-12-24 18:11:40 +07:00
taskProduct: { deleteMany: {}, create: body.taskProduct },
},
});
}
@Delete("{taskOrderId}")
2024-12-03 17:11:44 +07:00
@Security("keycloak", MANAGE_ROLES)
async deleteTask(@Request() req: RequestWithUser, @Path() taskOrderId: string) {
await prisma.$transaction(async (tx) => {
2024-12-03 17:11:44 +07:00
let record = await tx.taskOrder.findFirst({
where: { id: taskOrderId },
2024-12-03 17:11:44 +07:00
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
},
});
if (!record) throw notFoundError("Task Order");
2024-12-03 17:11:44 +07:00
await permissionCheck(req.user, record.registeredBranch);
});
}
}
@Route("/api/v1/task-order/{taskOrderId}")
@Tags("Task Order")
export class TaskActionController extends Controller {
2024-12-13 11:04:50 +07:00
@Post("set-task-status")
@Security("keycloak")
async changeTaskOrderTaskListStatus(
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
2024-12-13 11:04:50 +07:00
@Body()
body: {
step: number;
requestWorkId: string;
taskStatus: TaskStatus;
failedType?: string;
failedComment?: string;
2024-12-13 11:04:50 +07:00
}[],
) {
return await prisma.$transaction(async (tx) => {
const promises = body.map(async (v) => {
const record = await tx.task.findFirst({
include: { requestWorkStep: true },
2024-12-13 11:04:50 +07:00
where: {
step: v.step,
requestWorkId: v.requestWorkId,
taskOrderId: taskOrderId,
2024-12-13 11:04:50 +07:00
},
});
if (!record) throw notFoundError("Task List");
if (v.taskStatus === TaskStatus.Restart && record.requestWorkStep.responsibleUserId) {
await tx.userTask.updateMany({
where: {
taskOrderId: record.taskOrderId,
userId: record.requestWorkStep.responsibleUserId,
},
data: { userTaskStatus: UserTaskStatus.Restart },
});
}
2024-12-13 11:04:50 +07:00
return await tx.task.update({
where: { id: record.id },
data: {
taskStatus: v.taskStatus,
failedType: v.failedType,
failedComment: v.failedComment,
},
2024-12-13 11:04:50 +07:00
});
});
return await Promise.all(promises);
});
}
@Post("submit")
@Security("keycloak")
2024-12-11 12:06:29 +07:00
async submitTaskOrder(
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
2024-12-11 12:06:29 +07:00
@Query() submitUserId?: string, // for explicit
) {
submitUserId = submitUserId ?? req.user.sub;
const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId } });
2024-12-11 12:06:29 +07:00
if (!record) throw notFoundError("Task Order");
await prisma.$transaction([
2024-12-25 16:58:42 +07:00
prisma.requestWorkStepStatus.updateMany({
where: {
task: {
some: {
taskOrderId: taskOrderId,
taskStatus: TaskStatus.Success,
requestWorkStep: { responsibleUserId: submitUserId },
},
},
},
data: { workStatus: RequestWorkStatus.Validate },
}),
prisma.task.updateMany({
where: {
taskOrderId: taskOrderId,
taskStatus: TaskStatus.Success,
requestWorkStep: { responsibleUserId: submitUserId },
},
data: {
taskStatus: TaskStatus.Validate,
},
}),
prisma.userTask.updateMany({
where: {
taskOrderId: taskOrderId,
userId: submitUserId,
},
data: { userTaskStatus: UserTaskStatus.Submit },
}),
]);
2024-12-11 12:06:29 +07:00
}
@Post("complete")
@Security("keycloak")
async completeTaskOrder(@Request() req: RequestWithUser, @Path() taskOrderId: string) {
2024-12-16 11:27:10 +07:00
const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId } });
if (!record) throw notFoundError("Task Order");
2024-12-24 16:04:54 +07:00
await prisma.$transaction(async (tx) => {
await Promise.all([
tx.taskOrder.update({
where: { id: taskOrderId },
data: { taskOrderStatus: TaskOrderStatus.Complete },
}),
tx.requestWorkStepStatus.updateMany({
where: {
task: {
some: {
taskOrderId,
taskStatus: { notIn: [TaskStatus.Canceled, TaskStatus.Success] },
},
2024-12-24 16:04:54 +07:00
},
},
data: { workStatus: RequestWorkStatus.Ready },
}),
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,
taskStatus: { notIn: [TaskStatus.Canceled, TaskStatus.Success] },
},
data: { taskStatus: TaskStatus.Redo },
}),
2024-12-24 16:04:54 +07:00
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,
taskStatus: TaskStatus.Validate,
},
data: { taskStatus: TaskStatus.Complete },
}),
]);
await tx.requestWorkStepStatus.updateMany({
where: {
task: {
2024-12-24 16:04:54 +07:00
some: { taskOrderId, taskStatus: TaskStatus.Complete },
},
},
2024-12-24 16:04:54 +07:00
data: { workStatus: RequestWorkStatus.Completed },
});
const requestList = await tx.requestData.findMany({
include: {
requestWork: {
include: {
productService: {
include: {
product: true,
service: true,
work: {
include: { productOnWork: true },
},
},
},
stepStatus: true,
},
},
},
where: {
requestWork: {
some: {
stepStatus: {
some: {
task: { some: { taskOrderId } },
},
},
},
},
},
});
const completed: string[] = [];
requestList.forEach((item) => {
const completeCheck = item.requestWork.every((work) => {
const stepCount =
work.productService.work?.productOnWork.find(
(v) => v.productId === work.productService.productId,
)?.stepCount || 0;
const completeCount = work.stepStatus.filter(
(v) =>
v.workStatus === RequestWorkStatus.Completed ||
v.workStatus === RequestWorkStatus.Ended ||
v.workStatus === RequestWorkStatus.Canceled,
).length;
// NOTE: step found then check if complete count equals step count
if (stepCount === completeCount && completeCount > 0) return true;
// NOTE: likely no step found and completed at least one
if (stepCount === 0 && completeCount > 0) return true;
});
if (completeCheck) completed.push(item.id);
});
await tx.requestData.updateMany({
where: { id: { in: completed } },
data: { requestDataStatus: RequestDataStatus.Completed },
});
await tx.quotation.updateMany({
where: {
quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
},
requestData: {
every: {
requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed] },
},
},
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
});
});
2024-12-16 11:27:10 +07:00
}
}
2024-12-04 10:49:22 +07:00
@Route("api/v1/task-order/{taskOrderId}")
2024-12-04 10:49:22 +07:00
@Tags("Task Order")
2024-12-06 17:36:45 +07:00
export class TaskOrderAttachmentController extends Controller {
private async checkPermission(user: RequestWithUser["user"], id: string) {
const data = await prisma.taskOrder.findUnique({
include: { registeredBranch: { include: branchRelationPermInclude(user) } },
where: { id },
});
2024-12-06 17:42:27 +07:00
if (!data) throw notFoundError("Task Order");
2024-12-06 17:36:45 +07:00
await permissionCheck(user, data.registeredBranch);
}
2024-12-11 12:06:29 +07:00
2024-12-06 17:36:45 +07:00
@Get("attachment")
@Security("keycloak")
async listAttachment(@Request() req: RequestWithUser, @Path() taskOrderId: string) {
await this.checkPermission(req.user, taskOrderId);
return await listFile(fileLocation.task.attachment(taskOrderId));
2024-12-06 17:36:45 +07:00
}
@Get("attachment/{name}")
@Security("keycloak")
async getAttachment(@Path() taskOrderId: string, @Path() name: string) {
return await getFile(fileLocation.task.attachment(taskOrderId, name));
2024-12-06 17:36:45 +07:00
}
@Head("attachment/{name}")
async headAttachment(@Path() taskOrderId: string, @Path() name: string) {
return await getPresigned("head", fileLocation.task.attachment(taskOrderId, name));
2024-12-06 17:36:45 +07:00
}
@Put("attachment/{name}")
@Security("keycloak")
async putAttachment(
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
2024-12-06 17:36:45 +07:00
@Path() name: string,
) {
await this.checkPermission(req.user, taskOrderId);
return await setFile(fileLocation.task.attachment(taskOrderId, name));
2024-12-06 17:36:45 +07:00
}
@Delete("attachment/{name}")
@Security("keycloak")
async delAttachment(
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
2024-12-06 17:36:45 +07:00
@Path() name: string,
) {
await this.checkPermission(req.user, taskOrderId);
return await deleteFile(fileLocation.task.attachment(taskOrderId, name));
2024-12-06 17:36:45 +07:00
}
}
@Route("api/v1/user-task-order")
2024-12-16 10:50:03 +07:00
@Tags("User Task Order")
export class UserTaskController extends Controller {
@Get()
@Security("keycloak")
async getUserTask(
@Request() req: RequestWithUser,
@Query() query: string = "",
@Query() page = 1,
@Query() pageSize = 30,
@Query() userTaskStatus?: UserTaskStatus,
) {
const where = {
taskList: {
some: {
requestWorkStep: { responsibleUserId: req.user.sub },
},
},
2025-01-08 14:23:21 +07:00
AND: userTaskStatus
? [
{
OR:
2025-01-08 13:42:24 +07:00
userTaskStatus === UserTaskStatus.Pending
2025-01-08 14:23:21 +07:00
? [
{
userTask: {
some: {
userTaskStatus: {
in: [UserTaskStatus.Pending, UserTaskStatus.Restart],
},
userId: req.user.sub,
},
},
},
{
userTask: { none: { userId: req.user.sub } },
},
]
: undefined,
userTask:
userTaskStatus !== UserTaskStatus.Pending
? {
some: {
userTaskStatus,
userId: req.user.sub,
},
}
: undefined,
2025-01-08 13:42:24 +07:00
},
2025-01-08 14:23:21 +07:00
]
: undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query } },
{ contactName: { contains: query } },
{ contactTel: { contains: query } },
]),
} satisfies Prisma.TaskOrderWhereInput;
const [result, total] = await prisma.$transaction([
prisma.taskOrder.findMany({
where,
include: {
userTask: {
where: { userId: req.user.sub },
},
registeredBranch: true,
institution: true,
createdBy: true,
},
}),
prisma.taskOrder.count({ where }),
]);
return {
result: result.map((lhs) => ({
...lhs,
taskOrderStatus:
lhs.userTask.find((rhs) => rhs.taskOrderId === lhs.id)?.userTaskStatus ??
UserTaskStatus.Pending,
userTask: undefined,
})),
page,
pageSize,
total,
};
}
2024-12-16 10:50:03 +07:00
@Post("accept")
@Security("keycloak")
async acceptTaskOrder(
@Request() req: RequestWithUser,
@Body()
body: {
taskOrderId: string[];
},
) {
const record = await prisma.taskOrder.findMany({
include: {
taskList: {
orderBy: { step: "asc" },
},
},
where: { id: { in: body.taskOrderId } },
});
if (!record) throw notFoundError("Task Order");
await prisma.$transaction(async (tx) => {
const promises = body.taskOrderId.flatMap((taskOrderId) => [
tx.taskOrder.update({
where: { id: taskOrderId },
data: {
taskOrderStatus: TaskOrderStatus.InProgress,
userTask: {
2025-01-08 14:41:26 +07:00
deleteMany: { userId: req.user.sub },
2024-12-16 10:50:03 +07:00
create: {
userId: req.user.sub,
userTaskStatus: UserTaskStatus.Accept,
},
},
},
}),
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,
2025-01-08 14:41:26 +07:00
taskStatus: { in: [TaskStatus.Pending, TaskStatus.Restart] },
2024-12-16 10:50:03 +07:00
requestWorkStep: { responsibleUserId: req.user.sub },
},
data: {
taskStatus: TaskStatus.InProgress,
},
}),
tx.requestData.updateMany({
where: {
requestWork: {
some: {
stepStatus: {
some: { task: { some: { taskOrderId: taskOrderId } } },
},
},
},
},
data: { requestDataStatus: RequestDataStatus.InProgress },
}),
]);
await Promise.all(promises);
});
}
}