jws-backend/src/controllers/04-service-controller.ts

540 lines
14 KiB
TypeScript
Raw Normal View History

2024-06-10 17:47:40 +07:00
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
2024-10-22 10:43:20 +07:00
Head,
2024-06-10 17:47:40 +07:00
} from "tsoa";
import { Prisma, Status } from "@prisma/client";
2024-09-04 16:56:10 +07:00
import prisma from "../db";
import { RequestWithUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { isSystem } from "../utils/keycloak";
import {
branchRelationPermInclude,
createPermCheck,
createPermCondition,
} from "../services/permission";
import { filterStatus } from "../services/prisma";
import { isUsedError, notFoundError, relationError } from "../utils/error";
2024-10-22 10:43:20 +07:00
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import { queryOrNot } from "../utils/relation";
2024-06-10 17:47:40 +07:00
2024-07-03 17:28:00 +07:00
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"head_of_accountant",
"accountant",
"head_of_sale",
2024-07-03 17:28:00 +07:00
];
2024-06-10 17:47:40 +07:00
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));
}
2024-09-10 17:00:21 +07:00
const permissionCondShared = createPermCondition((_) => true);
const permissionCond = createPermCondition(globalAllow);
const permissionCheck = createPermCheck(globalAllow);
2024-06-10 17:47:40 +07:00
type ServiceCreate = {
code: string;
2024-06-10 17:47:40 +07:00
name: string;
detail: string;
2024-06-13 15:47:11 +07:00
attributes?: {
[key: string]: any;
};
2024-06-24 14:22:14 +07:00
status?: Status;
/**
* @isInt
*/
installments?: number;
2024-10-10 12:03:40 +07:00
workflowId?: string;
2024-06-14 15:49:05 +07:00
work?: {
name: string;
product: {
id: string;
/**
* @isInt
*/
installmentNo?: number;
attributes?: { [key: string]: any };
}[];
2024-06-14 15:49:05 +07:00
attributes?: { [key: string]: any };
}[];
2024-09-10 15:51:22 +07:00
shared?: boolean;
selectedImage?: string;
2024-09-03 14:06:02 +07:00
productGroupId: string;
2024-06-10 17:47:40 +07:00
};
type ServiceUpdate = {
2024-06-24 14:17:17 +07:00
name?: string;
detail?: string;
2024-06-13 15:47:11 +07:00
attributes?: {
[key: string]: any;
};
/**
* @isInt
*/
installments?: number;
2024-06-24 16:19:49 +07:00
status?: "ACTIVE" | "INACTIVE";
2024-10-10 12:03:40 +07:00
workflowId?: string;
2024-06-14 15:49:05 +07:00
work?: {
2024-11-25 15:03:49 +07:00
id?: string;
2024-06-14 15:49:05 +07:00
name: string;
product: {
id: string;
/**
* @isInt
*/
installmentNo?: number;
attributes?: { [key: string]: any };
}[];
2024-06-14 15:49:05 +07:00
attributes?: { [key: string]: any };
}[];
2024-09-10 15:51:22 +07:00
shared?: boolean;
selectedImage?: string;
2024-09-03 14:06:02 +07:00
productGroupId?: string;
2024-06-10 17:47:40 +07:00
};
2024-06-11 13:35:54 +07:00
@Route("api/v1/service")
2024-06-10 17:47:40 +07:00
@Tags("Service")
export class ServiceController extends Controller {
2024-06-11 09:31:10 +07:00
@Get("stats")
2024-06-21 14:39:00 +07:00
@Security("keycloak")
async getServiceStats(@Request() req: RequestWithUser, @Query() productGroupId?: string) {
return await prisma.service.count({
where: {
productGroupId,
2024-09-10 17:00:21 +07:00
OR: isSystem(req.user)
? undefined
2024-09-10 17:00:21 +07:00
: [
{
productGroup: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
{
shared: true,
productGroup: {
registeredBranch: { OR: permissionCondShared(req.user) },
},
},
2024-09-10 17:00:21 +07:00
],
},
});
2024-06-11 09:27:37 +07:00
}
2024-06-10 17:47:40 +07:00
@Get()
2024-06-21 14:39:00 +07:00
@Security("keycloak")
2024-06-10 17:47:40 +07:00
async getService(
@Request() req: RequestWithUser,
2024-06-10 17:47:40 +07:00
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
2024-06-25 14:09:54 +07:00
@Query() status?: Status,
2024-09-03 14:06:02 +07:00
@Query() productGroupId?: string,
@Query() fullDetail?: boolean,
2024-06-10 17:47:40 +07:00
) {
const where = {
OR: queryOrNot<Prisma.ServiceWhereInput[]>(query, [
{ name: { contains: query } },
{ detail: { contains: query } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: {
...filterStatus(status),
productGroupId,
2024-09-10 17:00:21 +07:00
OR: isSystem(req.user)
? undefined
2024-09-10 17:00:21 +07:00
: [
{
productGroup: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
{
shared: true,
productGroup: {
registeredBranch: { OR: permissionCondShared(req.user) },
},
},
2024-09-10 17:00:21 +07:00
],
},
2024-06-10 17:47:40 +07:00
} satisfies Prisma.ServiceWhereInput;
const [result, total] = await prisma.$transaction([
prisma.service.findMany({
include: {
work: fullDetail
? {
orderBy: { order: "asc" },
include: {
productOnWork: {
include: { product: true },
orderBy: { order: "asc" },
},
},
}
: true,
2024-07-01 14:38:07 +07:00
createdBy: true,
updatedBy: true,
},
2024-06-24 13:20:59 +07:00
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
2024-06-10 17:47:40 +07:00
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.service.count({ where }),
]);
return {
result,
2024-06-10 17:47:40 +07:00
page,
pageSize,
total,
};
}
@Get("{serviceId}")
2024-06-21 14:39:00 +07:00
@Security("keycloak")
2024-06-10 17:47:40 +07:00
async getServiceById(@Path() serviceId: string) {
const record = await prisma.service.findFirst({
include: {
2024-06-14 15:49:05 +07:00
work: {
orderBy: { order: "asc" },
include: {
2024-06-14 15:49:05 +07:00
productOnWork: {
include: { product: true },
orderBy: { order: "asc" },
},
},
},
2024-07-01 14:38:07 +07:00
createdBy: true,
updatedBy: true,
},
2024-06-10 17:47:40 +07:00
where: { id: serviceId },
});
if (!record) throw notFoundError("Service");
2024-06-10 17:47:40 +07:00
return record;
2024-06-10 17:47:40 +07:00
}
@Get("{serviceId}/work")
2024-06-21 14:39:00 +07:00
@Security("keycloak")
2024-06-10 17:47:40 +07:00
async getWorkOfService(
@Path() serviceId: string,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
2024-06-14 15:49:05 +07:00
serviceId,
} satisfies Prisma.WorkWhereInput;
2024-06-10 17:47:40 +07:00
const [result, total] = await prisma.$transaction([
prisma.work.findMany({
include: {
productOnWork: {
include: {
product: true,
},
2024-06-14 15:49:05 +07:00
orderBy: { order: "asc" },
},
2024-07-01 14:38:07 +07:00
createdBy: true,
updatedBy: true,
},
where,
2024-06-10 17:47:40 +07:00
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.work.count({ where }),
2024-06-10 17:47:40 +07:00
]);
return { result, page, pageSize, total };
}
@Post()
2024-07-03 17:28:00 +07:00
@Security("keycloak", MANAGE_ROLES)
2024-06-10 17:47:40 +07:00
async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) {
2024-09-03 14:06:02 +07:00
const { work, productGroupId, ...payload } = body;
2024-07-03 11:33:12 +07:00
const [productGroup] = await prisma.$transaction([
2024-09-03 14:06:02 +07:00
prisma.productGroup.findFirst({
2024-07-03 17:28:00 +07:00
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
2024-07-03 17:28:00 +07:00
createdBy: true,
updatedBy: true,
},
2024-09-03 14:06:02 +07:00
where: { id: body.productGroupId },
2024-07-03 17:28:00 +07:00
}),
]);
if (!productGroup) throw relationError("Product Type");
await permissionCheck(req.user, productGroup.registeredBranch);
2024-07-03 17:28:00 +07:00
const record = await prisma.$transaction(
async (tx) => {
2024-09-10 14:37:33 +07:00
const branch = productGroup.registeredBranch;
const company = (branch.headOffice || branch).code;
const last = await tx.runningNo.upsert({
where: {
2024-09-10 14:37:33 +07:00
key: `SERVICE_${company}_${body.code.toLocaleUpperCase()}`,
},
create: {
2024-09-10 14:37:33 +07:00
key: `SERVICE_${company}_${body.code.toLocaleUpperCase()}`,
value: 1,
},
update: { value: { increment: 1 } },
});
return tx.service.create({
include: {
2024-06-14 15:49:05 +07:00
work: {
include: {
2024-06-14 15:49:05 +07:00
productOnWork: {
include: { product: true },
2024-06-14 15:49:05 +07:00
orderBy: { order: "asc" },
},
},
},
2024-07-01 14:38:07 +07:00
createdBy: true,
updatedBy: true,
},
data: {
...payload,
2024-09-03 14:06:02 +07:00
productGroupId,
2024-06-24 14:23:10 +07:00
statusOrder: +(body.status === "INACTIVE"),
code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
work: {
create: (work || []).map((w, wIdx) => ({
name: w.name,
order: wIdx + 1,
attributes: w.attributes,
productOnWork: {
create: w.product.map((p, pIdx) => ({
productId: p.id,
installmentNo: p.installmentNo,
order: pIdx + 1,
})),
},
})),
},
2024-07-01 13:24:02 +07:00
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
2024-06-10 17:47:40 +07:00
this.setStatus(HttpStatus.CREATED);
return record;
2024-06-10 17:47:40 +07:00
}
@Put("{serviceId}")
2024-07-03 17:28:00 +07:00
@Security("keycloak", MANAGE_ROLES)
2024-06-10 17:47:40 +07:00
async editService(
@Request() req: RequestWithUser,
@Body() body: ServiceUpdate,
@Path() serviceId: string,
) {
const service = await prisma.service.findUnique({
where: { id: serviceId },
include: {
productGroup: {
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
2024-07-03 17:28:00 +07:00
},
},
},
},
});
if (!service) throw notFoundError("Service");
const { work, productGroupId, ...payload } = body;
const [productGroup] = await prisma.$transaction([
2024-09-03 14:06:02 +07:00
prisma.productGroup.findFirst({
2024-07-03 17:28:00 +07:00
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
2024-07-03 17:28:00 +07:00
createdBy: true,
updatedBy: true,
},
2024-09-03 14:06:02 +07:00
where: { id: body.productGroupId },
2024-07-03 17:28:00 +07:00
}),
]);
if (!service) throw notFoundError("Service");
2024-07-03 17:28:00 +07:00
if (!!body.productGroupId && !productGroup) throw relationError("Product Group");
2024-07-03 17:28:00 +07:00
await permissionCheck(req.user, service.productGroup.registeredBranch);
if (!!body.productGroupId && productGroup) {
await permissionCheck(req.user, productGroup.registeredBranch);
2024-07-03 17:28:00 +07:00
}
2024-06-21 14:14:10 +07:00
const record = await prisma.$transaction(async (tx) => {
2024-06-21 14:23:03 +07:00
return await tx.service.update({
2024-07-01 14:38:07 +07:00
include: {
createdBy: true,
updatedBy: true,
},
2024-06-21 14:14:10 +07:00
data: {
...payload,
2024-06-24 14:23:10 +07:00
statusOrder: +(payload.status === "INACTIVE"),
2024-06-21 14:14:10 +07:00
work: {
2024-11-25 15:14:42 +07:00
deleteMany: work?.some((v) => !!v.id)
? { id: { notIn: work.flatMap((v) => (!!v.id ? v.id : [])) } }
: {},
2024-11-25 15:03:49 +07:00
upsert: (work || []).map((w, wIdx) => ({
where: { id: w.id },
create: {
name: w.name,
order: wIdx + 1,
attributes: w.attributes,
productOnWork: {
createMany: {
data: w.product.map((p, pIdx) => ({
productId: p.id,
installmentNo: p.installmentNo,
order: pIdx + 1,
})),
skipDuplicates: true,
},
2024-11-25 15:03:49 +07:00
},
},
update: {
name: w.name,
order: wIdx + 1,
attributes: w.attributes,
productOnWork: {
deleteMany: {},
2024-11-25 15:03:49 +07:00
create: w.product.map((p, pIdx) => ({
productId: p.id,
installmentNo: p.installmentNo,
order: pIdx + 1,
})),
},
},
})),
2024-06-14 15:49:05 +07:00
},
2024-07-01 13:24:02 +07:00
updatedByUserId: req.user.sub,
2024-06-14 15:49:05 +07:00
},
2024-06-21 14:14:10 +07:00
where: { id: serviceId },
});
2024-06-10 17:47:40 +07:00
});
2024-06-21 14:14:10 +07:00
return record;
2024-06-10 17:47:40 +07:00
}
@Delete("{serviceId}")
2024-07-03 17:28:00 +07:00
@Security("keycloak", MANAGE_ROLES)
async deleteService(@Request() req: RequestWithUser, @Path() serviceId: string) {
const record = await prisma.service.findFirst({
include: {
productGroup: {
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
2024-07-03 17:28:00 +07:00
},
},
},
where: { id: serviceId },
});
2024-06-10 17:47:40 +07:00
if (!record) throw notFoundError("Service");
2024-06-10 17:47:40 +07:00
await permissionCheck(req.user, record.productGroup.registeredBranch);
2024-07-03 17:28:00 +07:00
if (record.status !== Status.CREATED) throw isUsedError("Service");
2024-06-10 17:47:40 +07:00
2024-07-01 14:38:07 +07:00
return await prisma.service.delete({
include: {
createdBy: true,
updatedBy: true,
},
where: { id: serviceId },
});
2024-06-10 17:47:40 +07:00
}
}
2024-09-10 15:19:31 +07:00
@Route("api/v1/service/{serviceId}")
@Tags("Service")
export class ServiceFileController extends Controller {
async checkPermission(user: RequestWithUser["user"], id: string) {
const data = await prisma.service.findUnique({
include: {
productGroup: {
include: {
registeredBranch: {
include: branchRelationPermInclude(user),
},
},
},
},
where: { id },
});
if (!data) throw notFoundError("Service");
2024-09-10 15:19:31 +07:00
await permissionCheck(user, data.productGroup.registeredBranch);
}
@Get("image")
@Security("keycloak")
async listImage(@Request() req: RequestWithUser, @Path() serviceId: string) {
await this.checkPermission(req.user, serviceId);
return await listFile(fileLocation.service.img(serviceId));
}
@Get("image/{name}")
async getImage(@Request() req: RequestWithUser, @Path() serviceId: string, @Path() name: string) {
return req.res?.redirect(await getFile(fileLocation.service.img(serviceId, name)));
}
2024-10-22 10:43:20 +07:00
@Head("image/{name}")
async headImage(
@Request() req: RequestWithUser,
@Path() serviceId: string,
@Path() name: string,
) {
return req.res?.redirect(await getPresigned("head", fileLocation.service.img(serviceId, name)));
}
2024-09-10 15:19:31 +07:00
@Put("image/{name}")
@Security("keycloak")
async putImage(@Request() req: RequestWithUser, @Path() serviceId: 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, serviceId);
return req.res?.redirect(await setFile(fileLocation.service.img(serviceId, name)));
}
@Delete("image/{name}")
@Security("keycloak")
async delImage(@Request() req: RequestWithUser, @Path() serviceId: string, @Path() name: string) {
await this.checkPermission(req.user, serviceId);
return await deleteFile(fileLocation.service.img(serviceId, name));
}
}