1296 lines
37 KiB
TypeScript
1296 lines
37 KiB
TypeScript
import {
|
|
Prisma,
|
|
QuotationStatus,
|
|
RequestDataStatus,
|
|
RequestWorkStatus,
|
|
TaskStatus,
|
|
} 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 {
|
|
branchRelationPermInclude,
|
|
createPermCheck,
|
|
createPermCondition,
|
|
} from "../services/permission";
|
|
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
|
import { notFoundError } from "../utils/error";
|
|
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
|
import HttpError from "../interfaces/http-error";
|
|
import HttpStatus from "../interfaces/http-status";
|
|
import { getGroupUser } from "../services/keycloak";
|
|
|
|
// User in company can edit.
|
|
const permissionCheck = createPermCheck((_) => true);
|
|
|
|
// User in company can see.
|
|
const permissionCond = createPermCondition((_) => true);
|
|
|
|
@Route("/api/v1/request-data")
|
|
@Tags("Request List")
|
|
export class RequestDataController extends Controller {
|
|
@Get("stats")
|
|
@Security("keycloak")
|
|
async getRequestDataStats(@Request() req: RequestWithUser) {
|
|
const where = {
|
|
quotation: {
|
|
registeredBranch: { OR: permissionCond(req.user) },
|
|
},
|
|
} satisfies Prisma.RequestDataWhereInput;
|
|
|
|
const list = await prisma.requestData.groupBy({
|
|
_count: true,
|
|
by: "requestDataStatus",
|
|
where: where,
|
|
});
|
|
|
|
return list.reduce<Record<RequestDataStatus, number>>(
|
|
(a, c) => Object.assign(a, { [c.requestDataStatus]: c._count }),
|
|
{
|
|
[RequestDataStatus.Pending]: 0,
|
|
[RequestDataStatus.Ready]: 0,
|
|
[RequestDataStatus.InProgress]: 0,
|
|
[RequestDataStatus.Completed]: 0,
|
|
[RequestDataStatus.Canceled]: 0,
|
|
},
|
|
);
|
|
}
|
|
|
|
@Get()
|
|
@Security("keycloak")
|
|
async getRequestDataList(
|
|
@Request() req: RequestWithUser,
|
|
@Query() page: number = 1,
|
|
@Query() pageSize: number = 30,
|
|
@Query() query: string = "",
|
|
@Query() responsibleOnly?: boolean,
|
|
@Query() requestDataStatus?: RequestDataStatus,
|
|
@Query() quotationId?: string,
|
|
@Query() code?: string,
|
|
@Query() incomplete?: boolean,
|
|
@Query() startDate?: Date,
|
|
@Query() endDate?: Date,
|
|
) {
|
|
const where = {
|
|
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
|
|
{ code: { contains: query, mode: "insensitive" } },
|
|
{ quotation: { code: { contains: query, mode: "insensitive" } } },
|
|
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
|
|
{
|
|
quotation: {
|
|
customerBranch: {
|
|
OR: [
|
|
{ code: { contains: query, mode: "insensitive" } },
|
|
{ registerName: { contains: query, mode: "insensitive" } },
|
|
{ registerNameEN: { contains: query, mode: "insensitive" } },
|
|
{ firstName: { contains: query, mode: "insensitive" } },
|
|
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
|
{ lastName: { contains: query, mode: "insensitive" } },
|
|
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
employee: {
|
|
OR: [
|
|
{
|
|
employeePassport: {
|
|
some: { number: { contains: query, mode: "insensitive" } },
|
|
},
|
|
},
|
|
{ code: { contains: query, mode: "insensitive" } },
|
|
{ firstName: { contains: query, mode: "insensitive" } },
|
|
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
|
{ lastName: { contains: query, mode: "insensitive" } },
|
|
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
|
],
|
|
},
|
|
},
|
|
]),
|
|
code,
|
|
requestDataStatus: incomplete
|
|
? {
|
|
notIn: [RequestDataStatus.Completed, RequestDataStatus.Canceled],
|
|
}
|
|
: requestDataStatus,
|
|
requestWork: responsibleOnly
|
|
? {
|
|
some: {
|
|
productService: {
|
|
service: {
|
|
workflow: {
|
|
step: {
|
|
some: {
|
|
OR: [
|
|
{
|
|
responsiblePerson: {
|
|
some: { userId: req.user.sub },
|
|
},
|
|
},
|
|
{
|
|
responsibleGroup: {
|
|
some: {
|
|
group: {
|
|
in: await getGroupUser(req.user.sub).then((r) =>
|
|
r.map(({ name }: { name: string }) => name),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
: undefined,
|
|
quotation: {
|
|
id: quotationId,
|
|
registeredBranch: { OR: permissionCond(req.user) },
|
|
},
|
|
...whereDateQuery(startDate, endDate),
|
|
} satisfies Prisma.RequestDataWhereInput;
|
|
|
|
const [result, total] = await prisma.$transaction([
|
|
prisma.requestData.findMany({
|
|
where,
|
|
include: {
|
|
quotation: {
|
|
include: {
|
|
productServiceList: {
|
|
include: {
|
|
service: {
|
|
include: {
|
|
workflow: {
|
|
include: {
|
|
step: {
|
|
orderBy: { order: "asc" },
|
|
include: {
|
|
value: true,
|
|
responsiblePerson: {
|
|
include: { user: true },
|
|
},
|
|
responsibleInstitution: true,
|
|
responsibleGroup: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
customerBranch: {
|
|
include: { customer: true },
|
|
},
|
|
},
|
|
},
|
|
employee: {
|
|
include: {
|
|
employeePassport: {
|
|
orderBy: { expireDate: "desc" },
|
|
},
|
|
customerBranch: {
|
|
include: {
|
|
province: {
|
|
include: {
|
|
employmentOffice: true,
|
|
},
|
|
},
|
|
district: {
|
|
include: {
|
|
employmentOffice: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
take: pageSize,
|
|
skip: (page - 1) * pageSize,
|
|
orderBy: [{ quotation: { urgent: "desc" } }, { createdAt: "desc" }],
|
|
}),
|
|
prisma.requestData.count({ where }),
|
|
]);
|
|
|
|
const dataRequestData = result.map((item) => {
|
|
const employee = item.employee;
|
|
const dataOffice =
|
|
employee.customerBranch.district?.employmentOffice.at(0) ??
|
|
employee.customerBranch.province?.employmentOffice.at(0);
|
|
|
|
return {
|
|
...item,
|
|
dataOffice,
|
|
};
|
|
});
|
|
|
|
return {
|
|
result: dataRequestData,
|
|
page,
|
|
pageSize,
|
|
total,
|
|
};
|
|
}
|
|
|
|
@Get("{requestDataId}")
|
|
@Security("keycloak")
|
|
async getRequestData(@Path() requestDataId: string) {
|
|
const record = await prisma.requestData.findFirst({
|
|
where: { id: requestDataId },
|
|
include: {
|
|
quotation: {
|
|
include: {
|
|
customerBranch: { include: { customer: true } },
|
|
debitNoteQuotation: {
|
|
select: { code: true },
|
|
},
|
|
invoice: {
|
|
include: {
|
|
installments: true,
|
|
payment: true,
|
|
},
|
|
},
|
|
createdBy: true,
|
|
},
|
|
},
|
|
employee: {
|
|
include: {
|
|
employeePassport: {
|
|
orderBy: { expireDate: "desc" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!record) throw notFoundError("Request Data");
|
|
|
|
return record;
|
|
}
|
|
|
|
@Post("update-messenger")
|
|
@Security("keycloak")
|
|
async updateRequestData(
|
|
@Request() req: RequestWithUser,
|
|
@Body()
|
|
boby: {
|
|
defaultMessengerId: string;
|
|
requestDataId: string[];
|
|
},
|
|
) {
|
|
const record = await prisma.requestData.updateManyAndReturn({
|
|
where: {
|
|
id: { in: boby.requestDataId },
|
|
quotation: {
|
|
registeredBranch: {
|
|
OR: permissionCond(req.user),
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
defaultMessengerId: boby.defaultMessengerId,
|
|
},
|
|
});
|
|
|
|
if (record.length <= 0) throw notFoundError("Request Data");
|
|
|
|
return record[0];
|
|
}
|
|
}
|
|
|
|
@Route("/api/v1/request-data/{requestDataId}")
|
|
@Tags("Request List")
|
|
export class RequestDataActionController extends Controller {
|
|
@Post("reject-request-cancel")
|
|
@Security("keycloak")
|
|
async rejectRequestCancel(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestDataId: string,
|
|
@Body()
|
|
body: {
|
|
reason?: string;
|
|
},
|
|
) {
|
|
const result = await prisma.requestData.updateManyAndReturn({
|
|
where: {
|
|
id: requestDataId,
|
|
quotation: {
|
|
registeredBranch: {
|
|
OR: permissionCond(req.user),
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
rejectRequestCancel: true,
|
|
rejectRequestCancelReason: body.reason || "",
|
|
},
|
|
});
|
|
|
|
if (result.length <= 0) throw notFoundError("Request Data");
|
|
|
|
return result[0];
|
|
}
|
|
|
|
@Post("request-work/{requestWorkId}/reject-request-cancel")
|
|
@Security("keycloak")
|
|
async rejectWorkRequestCancel(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestWorkId: string,
|
|
@Body()
|
|
body: {
|
|
reason?: string;
|
|
},
|
|
) {
|
|
const result = await prisma.requestWork.updateManyAndReturn({
|
|
where: {
|
|
id: requestWorkId,
|
|
request: {
|
|
quotation: {
|
|
registeredBranch: {
|
|
OR: permissionCond(req.user),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
rejectRequestCancel: true,
|
|
rejectRequestCancelReason: body.reason || "",
|
|
},
|
|
});
|
|
|
|
if (result.length <= 0) throw notFoundError("Request Data");
|
|
|
|
return result[0];
|
|
}
|
|
|
|
@Post("cancel")
|
|
@Security("keycloak")
|
|
async cancelRequestData(@Request() req: RequestWithUser, @Path() requestDataId: string) {
|
|
const result = await prisma.requestData.findFirst({
|
|
where: {
|
|
id: requestDataId,
|
|
quotation: {
|
|
registeredBranch: {
|
|
OR: permissionCond(req.user),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!result) throw notFoundError("Request Data");
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
const workStepCondition = {
|
|
requestWork: { requestDataId },
|
|
workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] },
|
|
};
|
|
|
|
await Promise.all([
|
|
tx.requestData.update({
|
|
where: { id: requestDataId },
|
|
data: {
|
|
requestDataStatus: RequestDataStatus.Canceled,
|
|
},
|
|
}),
|
|
tx.requestWorkStepStatus.updateMany({
|
|
where: workStepCondition,
|
|
data: {
|
|
workStatus: RequestWorkStatus.Canceled,
|
|
},
|
|
}),
|
|
tx.task.updateMany({
|
|
where: {
|
|
taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] },
|
|
requestWorkStep: workStepCondition,
|
|
},
|
|
data: { taskStatus: TaskStatus.Canceled },
|
|
}),
|
|
]);
|
|
await Promise.all([
|
|
tx.quotation
|
|
.updateManyAndReturn({
|
|
where: {
|
|
requestData: {
|
|
every: { requestDataStatus: RequestDataStatus.Canceled },
|
|
},
|
|
},
|
|
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
|
|
})
|
|
.then(async (res) => {
|
|
await tx.notification.createMany({
|
|
data: res.map((v) => ({
|
|
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
|
detail: "รหัส / code : " + v.code + " Canceled",
|
|
receiverId: v.createdByUserId,
|
|
})),
|
|
});
|
|
}),
|
|
tx.taskOrder.updateMany({
|
|
where: {
|
|
taskList: {
|
|
every: { taskStatus: TaskStatus.Canceled },
|
|
},
|
|
},
|
|
data: { taskOrderStatus: TaskStatus.Canceled },
|
|
}),
|
|
]);
|
|
});
|
|
}
|
|
|
|
@Put("request-work/step-status/{step}")
|
|
@Security("keycloak")
|
|
async updateRequestWorkDataStepStatus(
|
|
@Path() requestDataId: string,
|
|
@Path() step: number,
|
|
@Body()
|
|
payload: {
|
|
workStatus?: RequestWorkStatus;
|
|
requestWorkId: string;
|
|
attributes?: Record<string, any>;
|
|
customerDuty?: boolean | null;
|
|
customerDutyCost?: number | null;
|
|
companyDuty?: boolean | null;
|
|
companyDutyCost?: number | null;
|
|
individualDuty?: boolean | null;
|
|
individualDutyCost?: number | null;
|
|
responsibleUserLocal?: boolean | null;
|
|
responsibleUserId?: string | null;
|
|
}[],
|
|
) {
|
|
payload.forEach((item) => {
|
|
if (!item.responsibleUserId) item.responsibleUserId = undefined;
|
|
});
|
|
return await prisma.$transaction(async (tx) => {
|
|
const workStepCondition = await tx.requestData.findFirst({
|
|
where: {
|
|
id: requestDataId,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!workStepCondition) {
|
|
throw new Error("RequestWork not found requestDataId");
|
|
}
|
|
|
|
const data = await Promise.all(
|
|
payload.map(async (item) => {
|
|
return await tx.requestWorkStepStatus.upsert({
|
|
include: {
|
|
requestWork: {
|
|
include: {
|
|
request: true,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
step_requestWorkId: {
|
|
step: step,
|
|
requestWorkId: item.requestWorkId,
|
|
},
|
|
requestWork: {
|
|
request: { id: requestDataId },
|
|
},
|
|
},
|
|
create: {
|
|
...item,
|
|
step: step,
|
|
requestWorkId: item.requestWorkId,
|
|
},
|
|
update: item,
|
|
});
|
|
}),
|
|
);
|
|
|
|
if (
|
|
data.some((item) => {
|
|
return (
|
|
item.workStatus === "Ready" && item.requestWork.request.requestDataStatus === "Pending"
|
|
);
|
|
})
|
|
) {
|
|
await tx.requestData.updateMany({
|
|
where: {
|
|
id: requestDataId,
|
|
requestDataStatus: "Pending",
|
|
},
|
|
data: { requestDataStatus: "Ready" },
|
|
});
|
|
}
|
|
|
|
if (
|
|
data.some((item) => {
|
|
return (
|
|
item.workStatus === "InProgress" ||
|
|
item.workStatus === "Waiting" ||
|
|
item.workStatus === "Validate" ||
|
|
item.workStatus === "Completed" ||
|
|
item.workStatus === "Ended"
|
|
);
|
|
})
|
|
) {
|
|
await tx.requestData.update({
|
|
where: {
|
|
id: requestDataId,
|
|
},
|
|
data: { requestDataStatus: "InProgress" },
|
|
});
|
|
}
|
|
|
|
if (
|
|
data.some((item) => {
|
|
return item.workStatus === "Canceled";
|
|
})
|
|
) {
|
|
const dataId = data.map((itemId) => itemId.requestWork.id);
|
|
await tx.task.updateMany({
|
|
where: {
|
|
taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] },
|
|
requestWorkStep: {
|
|
step: step,
|
|
requestWorkId: { in: dataId },
|
|
workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] },
|
|
},
|
|
},
|
|
data: { taskStatus: TaskStatus.Canceled },
|
|
});
|
|
await Promise.all([
|
|
tx.quotation
|
|
.updateManyAndReturn({
|
|
where: {
|
|
quotationStatus: { not: QuotationStatus.Canceled },
|
|
requestData: {
|
|
every: { requestDataStatus: RequestDataStatus.Canceled },
|
|
},
|
|
},
|
|
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
|
|
})
|
|
.then(async (res) => {
|
|
await tx.notification.createMany({
|
|
data: res.map((v) => ({
|
|
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
|
detail: "รหัส / code : " + v.code + " Canceled",
|
|
receiverId: v.createdByUserId,
|
|
})),
|
|
});
|
|
}),
|
|
tx.taskOrder.updateMany({
|
|
where: {
|
|
taskList: {
|
|
every: { taskStatus: TaskStatus.Canceled },
|
|
},
|
|
},
|
|
data: { taskOrderStatus: TaskStatus.Canceled },
|
|
}),
|
|
]);
|
|
}
|
|
|
|
const requestList = await tx.requestData.findMany({
|
|
include: {
|
|
requestWork: {
|
|
include: {
|
|
productService: {
|
|
include: {
|
|
product: true,
|
|
service: true,
|
|
work: {
|
|
include: { productOnWork: true },
|
|
},
|
|
},
|
|
},
|
|
stepStatus: true,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
requestWork: {
|
|
some: {
|
|
requestDataId: requestDataId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
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
|
|
.updateManyAndReturn({
|
|
where: {
|
|
quotationStatus: {
|
|
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
|
},
|
|
requestData: {
|
|
every: {
|
|
requestDataStatus: {
|
|
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
|
})
|
|
.then(async (res) => {
|
|
await tx.notification.createMany({
|
|
data: res.map((v) => ({
|
|
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
|
detail: "รหัส / code : " + v.code + " Completed",
|
|
receiverId: v.createdByUserId,
|
|
})),
|
|
});
|
|
});
|
|
// dataRecord.push(record);
|
|
return data;
|
|
});
|
|
}
|
|
}
|
|
|
|
@Route("/api/v1/request-work")
|
|
@Tags("Request List")
|
|
export class RequestListController extends Controller {
|
|
async #getLineToken() {
|
|
if (!process.env.LINE_MESSAGING_API_TOKEN) {
|
|
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
|
|
}
|
|
|
|
return process.env.LINE_MESSAGING_API_TOKEN;
|
|
}
|
|
|
|
@Get()
|
|
@Security("keycloak")
|
|
async getRequestWork(
|
|
@Request() req: RequestWithUser,
|
|
@Query() page: number = 1,
|
|
@Query() pageSize: number = 30,
|
|
@Query() requestDataId?: string,
|
|
@Query() workStatus?: RequestWorkStatus,
|
|
@Query() readyToTask?: boolean,
|
|
@Query() cancelOnly?: boolean,
|
|
@Query() quotationId?: string,
|
|
) {
|
|
let statusCondition: Prisma.RequestWorkWhereInput["stepStatus"] = {};
|
|
|
|
if (readyToTask) {
|
|
statusCondition = {
|
|
some: {
|
|
OR: [
|
|
{ workStatus: RequestWorkStatus.Ready },
|
|
{
|
|
workStatus: { in: [RequestWorkStatus.Ready, RequestWorkStatus.InProgress] },
|
|
task: {
|
|
some: {
|
|
taskStatus: TaskStatus.Redo,
|
|
requestWorkStep: {
|
|
workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
if (workStatus && !readyToTask && !cancelOnly) {
|
|
statusCondition = {
|
|
some: { workStatus },
|
|
};
|
|
}
|
|
|
|
const where = {
|
|
OR: cancelOnly
|
|
? [
|
|
{
|
|
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
|
|
},
|
|
{
|
|
request: { requestDataStatus: RequestDataStatus.Canceled },
|
|
},
|
|
]
|
|
: undefined,
|
|
stepStatus: readyToTask || workStatus ? statusCondition : undefined,
|
|
creditNoteId: cancelOnly ? null : undefined,
|
|
request: {
|
|
id: requestDataId,
|
|
requestDataStatus: readyToTask
|
|
? { notIn: [RequestDataStatus.Canceled, RequestDataStatus.Completed] }
|
|
: undefined,
|
|
quotationId,
|
|
quotation: {
|
|
registeredBranch: { OR: permissionCond(req.user) },
|
|
},
|
|
},
|
|
} satisfies Prisma.RequestWorkWhereInput;
|
|
|
|
const [result, total] = await prisma.$transaction([
|
|
prisma.requestWork.findMany({
|
|
where,
|
|
include: {
|
|
request: {
|
|
include: {
|
|
quotation: readyToTask
|
|
? {
|
|
include: {
|
|
customerBranch: {
|
|
include: { customer: true },
|
|
},
|
|
},
|
|
}
|
|
: true,
|
|
employee: true,
|
|
},
|
|
},
|
|
stepStatus: cancelOnly
|
|
? true
|
|
: {
|
|
include: {
|
|
task: { where: { taskStatus: TaskStatus.Complete } },
|
|
},
|
|
},
|
|
productService: {
|
|
include: {
|
|
service: {
|
|
include: {
|
|
workflow: {
|
|
include: {
|
|
step: {
|
|
include: {
|
|
value: true,
|
|
responsiblePerson: {
|
|
include: { user: true },
|
|
},
|
|
responsibleInstitution: true,
|
|
responsibleGroup: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
work: true,
|
|
product: {
|
|
include: { document: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
take: pageSize,
|
|
skip: (page - 1) * pageSize,
|
|
}),
|
|
prisma.requestWork.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
result: result.map((v) => {
|
|
return Object.assign(v, {
|
|
productService: Object.assign(v.productService, {
|
|
product: Object.assign(v.productService.product, {
|
|
document: v.productService.product.document.map((doc) => doc.name),
|
|
}),
|
|
}),
|
|
});
|
|
}),
|
|
page,
|
|
pageSize,
|
|
total,
|
|
};
|
|
}
|
|
|
|
@Get("{requestWorkId}")
|
|
@Security("keycloak")
|
|
async getRequestWorkById(@Path() requestWorkId: string) {
|
|
const record = await prisma.requestWork.findFirst({
|
|
include: {
|
|
request: {
|
|
include: {
|
|
quotation: true,
|
|
employee: true,
|
|
},
|
|
},
|
|
stepStatus: true,
|
|
productService: {
|
|
include: {
|
|
service: {
|
|
include: {
|
|
workflow: {
|
|
include: {
|
|
step: {
|
|
include: {
|
|
value: true,
|
|
responsiblePerson: {
|
|
include: { user: true },
|
|
},
|
|
responsibleInstitution: true,
|
|
responsibleGroup: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
work: true,
|
|
product: {
|
|
include: {
|
|
document: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
where: { id: requestWorkId },
|
|
});
|
|
|
|
if (!record) throw notFoundError("Request Work");
|
|
|
|
return Object.assign(record, {
|
|
productService: Object.assign(record.productService, {
|
|
product: Object.assign(record.productService.product, {
|
|
document: record.productService.product.document.map((doc) => doc.name),
|
|
}),
|
|
}),
|
|
});
|
|
}
|
|
|
|
@Put("{requestWorkId}")
|
|
@Security("keycloak")
|
|
async updateRequestWorkById(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestWorkId: string,
|
|
@Body() payload: { attributes: Record<string, any> },
|
|
) {
|
|
const record = await prisma.requestWork.update({
|
|
include: {
|
|
request: {
|
|
include: {
|
|
quotation: true,
|
|
employee: true,
|
|
},
|
|
},
|
|
stepStatus: true,
|
|
productService: {
|
|
include: {
|
|
service: true,
|
|
work: true,
|
|
product: {
|
|
include: {
|
|
document: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
where: { id: requestWorkId },
|
|
data: { attributes: payload.attributes },
|
|
});
|
|
|
|
return record;
|
|
}
|
|
|
|
@Put("{requestWorkId}/step-status/{step}")
|
|
@Security("keycloak")
|
|
async updateRequestWorkStepStatus(
|
|
@Path() requestWorkId: string,
|
|
@Path() step: number,
|
|
@Body()
|
|
payload: {
|
|
workStatus?: RequestWorkStatus;
|
|
attributes?: Record<string, any>;
|
|
customerDuty?: boolean | null;
|
|
customerDutyCost?: number | null;
|
|
companyDuty?: boolean | null;
|
|
companyDutyCost?: number | null;
|
|
individualDuty?: boolean | null;
|
|
individualDutyCost?: number | null;
|
|
responsibleUserLocal?: boolean | null;
|
|
responsibleUserId?: string | null;
|
|
},
|
|
) {
|
|
if (!payload.responsibleUserId) payload.responsibleUserId = undefined;
|
|
|
|
return await prisma.$transaction(async (tx) => {
|
|
const record = await tx.requestWorkStepStatus.upsert({
|
|
include: {
|
|
requestWork: {
|
|
include: {
|
|
request: true,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
step_requestWorkId: {
|
|
step: step,
|
|
requestWorkId,
|
|
},
|
|
},
|
|
create: {
|
|
...payload,
|
|
step: step,
|
|
requestWorkId,
|
|
},
|
|
update: payload,
|
|
});
|
|
|
|
if (record.responsibleUserId === null) {
|
|
await tx.requestWorkStepStatus.update({
|
|
where: {
|
|
step_requestWorkId: {
|
|
step: step,
|
|
requestWorkId,
|
|
},
|
|
responsibleUserId: null,
|
|
},
|
|
data: {
|
|
responsibleUserId: record.requestWork?.request.defaultMessengerId,
|
|
},
|
|
});
|
|
}
|
|
|
|
switch (payload.workStatus) {
|
|
case "Ready":
|
|
if (record.requestWork.request.requestDataStatus === "Pending") {
|
|
await tx.requestData.updateMany({
|
|
where: {
|
|
id: record.requestWork.requestDataId,
|
|
requestDataStatus: "Pending",
|
|
},
|
|
data: { requestDataStatus: "Ready" },
|
|
});
|
|
}
|
|
break;
|
|
case "InProgress":
|
|
case "Waiting":
|
|
case "Validate":
|
|
case "Completed":
|
|
case "Ended":
|
|
await tx.requestData.update({
|
|
where: {
|
|
id: record.requestWork.requestDataId,
|
|
},
|
|
data: { requestDataStatus: "InProgress" },
|
|
});
|
|
break;
|
|
case "Canceled":
|
|
await tx.task.updateMany({
|
|
where: {
|
|
taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] },
|
|
requestWorkStep: {
|
|
step: step,
|
|
requestWorkId,
|
|
workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] },
|
|
},
|
|
},
|
|
data: { taskStatus: TaskStatus.Canceled },
|
|
});
|
|
await Promise.all([
|
|
tx.quotation
|
|
.updateManyAndReturn({
|
|
where: {
|
|
quotationStatus: { not: QuotationStatus.Canceled },
|
|
requestData: {
|
|
every: { requestDataStatus: RequestDataStatus.Canceled },
|
|
},
|
|
},
|
|
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
|
|
})
|
|
.then(async (res) => {
|
|
await tx.notification.createMany({
|
|
data: res.map((v) => ({
|
|
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
|
detail: "รหัส / code : " + v.code + " Canceled",
|
|
receiverId: v.createdByUserId,
|
|
})),
|
|
});
|
|
}),
|
|
tx.taskOrder.updateMany({
|
|
where: {
|
|
taskList: {
|
|
every: { taskStatus: TaskStatus.Canceled },
|
|
},
|
|
},
|
|
data: { taskOrderStatus: TaskStatus.Canceled },
|
|
}),
|
|
]);
|
|
break;
|
|
}
|
|
|
|
const requestList = await tx.requestData.findMany({
|
|
include: {
|
|
requestWork: {
|
|
include: {
|
|
productService: {
|
|
include: {
|
|
product: true,
|
|
service: true,
|
|
work: {
|
|
include: { productOnWork: true },
|
|
},
|
|
},
|
|
},
|
|
stepStatus: true,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
requestWork: {
|
|
some: {
|
|
requestDataId: record.requestWork.requestDataId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
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
|
|
.updateManyAndReturn({
|
|
where: {
|
|
quotationStatus: {
|
|
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
|
},
|
|
AND: [
|
|
{
|
|
requestData: {
|
|
every: {
|
|
requestDataStatus: {
|
|
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
requestData: {
|
|
some: {},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
|
include: {
|
|
customerBranch: {
|
|
include: {
|
|
customer: {
|
|
include: {
|
|
branch: {
|
|
where: { userId: { not: null } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
.then(async (res) => {
|
|
await tx.notification.createMany({
|
|
data: res.map((v) => ({
|
|
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
|
detail: "รหัส / code : " + v.code + " Completed",
|
|
receiverId: v.createdByUserId,
|
|
})),
|
|
});
|
|
const token = await this.#getLineToken();
|
|
if (!token) return;
|
|
|
|
const textHead = "JWS ALERT:";
|
|
|
|
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
|
|
const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว";
|
|
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
|
|
let finalTextWork = "";
|
|
let textData = "";
|
|
|
|
let dataCustomerId: string[] = [];
|
|
let textWorkList: string[] = [];
|
|
let dataUserId: string[] = [];
|
|
|
|
if (res) {
|
|
res.forEach((data, index) => {
|
|
data.customerBranch.customer.branch.forEach((item) => {
|
|
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
|
dataCustomerId.push(item.id);
|
|
dataUserId.push(item.userId);
|
|
}
|
|
});
|
|
textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`);
|
|
});
|
|
|
|
finalTextWork = textWorkList.join("\n");
|
|
}
|
|
|
|
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
|
|
|
|
const data = {
|
|
to: dataUserId,
|
|
messages: [
|
|
{
|
|
type: "text",
|
|
text: textData,
|
|
},
|
|
],
|
|
};
|
|
|
|
await fetch("https://api.line.me/v2/bot/message/multicast", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
});
|
|
|
|
return record;
|
|
});
|
|
}
|
|
}
|
|
|
|
@Route("api/v1/request-work/{requestId}/step-status/{step}")
|
|
@Tags("Request List")
|
|
export class RequestListFileController extends Controller {
|
|
private async checkPermission(user: RequestWithUser["user"], id: string) {
|
|
const data = await prisma.requestWork.findUnique({
|
|
where: { id },
|
|
include: {
|
|
request: {
|
|
include: {
|
|
quotation: {
|
|
include: { registeredBranch: { include: branchRelationPermInclude(user) } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!data) throw notFoundError("Request Work");
|
|
await permissionCheck(user, data.request.quotation.registeredBranch);
|
|
}
|
|
|
|
@Get("attachment")
|
|
@Security("keycloak")
|
|
async listAttachment(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestId: string,
|
|
@Path() step: number,
|
|
) {
|
|
await this.checkPermission(req.user, requestId);
|
|
return await listFile(fileLocation.request.attachment(requestId, step));
|
|
}
|
|
|
|
@Get("attachment/{name}")
|
|
@Security("keycloak")
|
|
async getAttachment(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestId: string,
|
|
@Path() step: number,
|
|
@Path() name: string,
|
|
) {
|
|
await this.checkPermission(req.user, requestId);
|
|
return await getFile(fileLocation.request.attachment(requestId, step, name));
|
|
}
|
|
|
|
@Head("attachment/{name}")
|
|
@Security("keycloak")
|
|
async headAttachment(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestId: string,
|
|
@Path() step: number,
|
|
@Path() name: string,
|
|
) {
|
|
await this.checkPermission(req.user, requestId);
|
|
return await getPresigned("head", fileLocation.request.attachment(requestId, step, name));
|
|
}
|
|
|
|
@Put("attachment/{name}")
|
|
@Security("keycloak")
|
|
async putAttachment(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestId: string,
|
|
@Path() step: number,
|
|
@Path() name: string,
|
|
) {
|
|
await this.checkPermission(req.user, requestId);
|
|
return await setFile(fileLocation.request.attachment(requestId, step, name));
|
|
}
|
|
|
|
@Delete("attachment/{name}")
|
|
@Security("keycloak")
|
|
async delAttachment(
|
|
@Request() req: RequestWithUser,
|
|
@Path() requestId: string,
|
|
@Path() step: number,
|
|
@Path() name: string,
|
|
) {
|
|
await this.checkPermission(req.user, requestId);
|
|
return await deleteFile(fileLocation.request.attachment(requestId, step, name));
|
|
}
|
|
}
|