Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s

This commit is contained in:
Methapon2001 2025-03-19 09:20:18 +07:00
commit 5ce356648d
11 changed files with 224 additions and 57 deletions

View file

@ -8,6 +8,7 @@
"dev": "nodemon", "dev": "nodemon",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"format": "prettier --write .", "format": "prettier --write .",
"debug": "nodemon",
"build": "tsoa spec-and-routes && tsc", "build": "tsoa spec-and-routes && tsc",
"changelog:generate": "git-cliff -o CHANGELOG.md && git add CHANGELOG.md && git commit -m 'Update CHANGELOG.md'", "changelog:generate": "git-cliff -o CHANGELOG.md && git add CHANGELOG.md && git commit -m 'Update CHANGELOG.md'",
"db:generate": "prisma generate", "db:generate": "prisma generate",

View file

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "RequestData" ADD COLUMN "rejectRequestCancel" BOOLEAN,
ADD COLUMN "rejectRequestCancelReason" TEXT;
-- AlterTable
ALTER TABLE "RequestWork" ADD COLUMN "rejectRequestCancel" BOOLEAN,
ADD COLUMN "rejectRequestCancelReason" TEXT;

View file

@ -1464,6 +1464,8 @@ model RequestData {
customerRequestCancel Boolean? customerRequestCancel Boolean?
customerRequestCancelReason String? customerRequestCancelReason String?
rejectRequestCancel Boolean?
rejectRequestCancelReason String?
flow Json? flow Json?
@ -1502,6 +1504,8 @@ model RequestWork {
customerRequestCancel Boolean? customerRequestCancel Boolean?
customerRequestCancelReason String? customerRequestCancelReason String?
rejectRequestCancel Boolean?
rejectRequestCancelReason String?
creditNote CreditNote? @relation(fields: [creditNoteId], references: [id], onDelete: SetNull) creditNote CreditNote? @relation(fields: [creditNoteId], references: [id], onDelete: SetNull)
creditNoteId String? creditNoteId String?

View file

@ -61,6 +61,13 @@ const quotationData = (id: string) =>
service: true, service: true,
}, },
}, },
createdBy: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
}, },
}); });

View file

@ -2,13 +2,9 @@ import config from "../config.json";
import { import {
Customer, Customer,
CustomerBranch, CustomerBranch,
ProductGroup,
QuotationStatus, QuotationStatus,
RequestWorkStatus, RequestWorkStatus,
PaymentStatus, PaymentStatus,
User,
Invoice,
CustomerType,
} from "@prisma/client"; } from "@prisma/client";
import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa"; import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa";
import prisma from "../db"; import prisma from "../db";
@ -64,6 +60,7 @@ export class StatsController extends Controller {
code: true, code: true,
quotationStatus: true, quotationStatus: true,
customerBranch: { customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true }, include: { customer: true },
}, },
finalPrice: true, finalPrice: true,
@ -127,7 +124,10 @@ export class StatsController extends Controller {
code: true, code: true,
quotation: { quotation: {
select: { select: {
customerBranch: { include: { customer: true } }, customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true },
},
}, },
}, },
payment: { payment: {
@ -198,7 +198,12 @@ export class StatsController extends Controller {
invoice: { invoice: {
select: { select: {
quotation: { quotation: {
select: { customerBranch: { include: { customer: true } } }, select: {
customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true },
},
},
}, },
}, },
}, },
@ -402,6 +407,7 @@ export class StatsController extends Controller {
include: { include: {
createdBy: true, createdBy: true,
customerBranch: { customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true }, include: { customer: true },
}, },
}, },
@ -430,9 +436,9 @@ export class StatsController extends Controller {
}); });
return list.reduce<{ return list.reduce<{
byProductGroup: (ProductGroup & { _count: number })[]; byProductGroup: ((typeof list)[number]["product"]["productGroup"] & { _count: number })[];
bySale: (User & { _count: number })[]; bySale: ((typeof list)[number]["quotation"]["createdBy"] & { _count: number })[];
byCustomer: ((CustomerBranch & { customer: Customer }) & { _count: number })[]; byCustomer: ((typeof list)[number]["quotation"]["customerBranch"] & { _count: number })[];
}>( }>(
(a, c) => { (a, c) => {
{ {
@ -683,7 +689,9 @@ export class StatsController extends Controller {
{ {
paid: number; paid: number;
unpaid: number; unpaid: number;
customerBranch: CustomerBranch & { customer: Customer }; customerBranch: CustomerBranch & {
customer: Customer;
};
}[] }[]
>((acc, item) => { >((acc, item) => {
const exists = acc.find((v) => v.customerBranch.id === item.customerBranch!.id); const exists = acc.find((v) => v.customerBranch.id === item.customerBranch!.id);
@ -702,6 +710,15 @@ export class StatsController extends Controller {
return acc; return acc;
}, []) }, [])
.map((v) => ({ ...v, _quotation: undefined })); .map((v) => ({
...v,
customerBranch: {
...v.customerBranch,
userId: undefined,
otpCode: undefined,
otpExpires: undefined,
},
_quotation: undefined,
}));
} }
} }

View file

@ -239,9 +239,84 @@ export class RequestDataController extends Controller {
@Route("/api/v1/request-data/{requestDataId}") @Route("/api/v1/request-data/{requestDataId}")
@Tags("Request List") @Tags("Request List")
export class RequestDataActionController extends Controller { 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") @Post("cancel")
@Security("keycloak") @Security("keycloak")
async cancelRequestData(@Path() requestDataId: string) { 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) => { await prisma.$transaction(async (tx) => {
const workStepCondition = { const workStepCondition = {
requestWork: { requestDataId }, requestWork: { requestDataId },
@ -945,13 +1020,22 @@ export class RequestListController extends Controller {
quotationStatus: { quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
}, },
requestData: { AND: [
every: { {
requestDataStatus: { requestData: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], every: {
requestDataStatus: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
},
},
}, },
}, },
}, {
requestData: {
some: {},
},
},
],
}, },
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: { include: {

View file

@ -819,13 +819,22 @@ export class TaskActionController extends Controller {
quotationStatus: { quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
}, },
requestData: { AND: [
every: { {
requestDataStatus: { requestData: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], every: {
requestDataStatus: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
},
},
}, },
}, },
}, {
requestData: {
some: {},
},
},
],
}, },
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: { include: {

View file

@ -146,8 +146,8 @@ export class CreditNoteController extends Controller {
) { ) {
const where = { const where = {
OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [ OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ {
code: { contains: query, mode: "insensitive" },
requestWork: { requestWork: {
some: { some: {
request: { request: {

View file

@ -784,7 +784,7 @@ export class LineController extends Controller {
async customerRequestCancel( async customerRequestCancel(
@Path() requestDataId: string, @Path() requestDataId: string,
@Request() req: RequestWithLineUser, @Request() req: RequestWithLineUser,
@Body() body: { reason: string }, @Body() body: { cancel: boolean; reason?: string },
) { ) {
const result = await prisma.requestData.updateMany({ const result = await prisma.requestData.updateMany({
where: { where: {
@ -803,8 +803,10 @@ export class LineController extends Controller {
}, },
}, },
data: { data: {
customerRequestCancel: true, customerRequestCancel: body.cancel,
customerRequestCancelReason: body.reason, customerRequestCancelReason: body.reason || null,
rejectRequestCancel: false,
rejectRequestCancelReason: null,
}, },
}); });
if (result.count <= 0) throw notFoundError("Request Data"); if (result.count <= 0) throw notFoundError("Request Data");
@ -815,7 +817,7 @@ export class LineController extends Controller {
async customerRequestCancelWork( async customerRequestCancelWork(
@Path() requestWorkId: string, @Path() requestWorkId: string,
@Request() req: RequestWithLineUser, @Request() req: RequestWithLineUser,
@Body() body: { reason: string }, @Body() body: { cancel: boolean; reason?: string },
) { ) {
const result = await prisma.requestWork.updateMany({ const result = await prisma.requestWork.updateMany({
where: { where: {
@ -836,8 +838,10 @@ export class LineController extends Controller {
}, },
}, },
data: { data: {
customerRequestCancel: true, customerRequestCancel: body.cancel,
customerRequestCancelReason: body.reason, customerRequestCancelReason: body.reason || null,
rejectRequestCancel: false,
rejectRequestCancelReason: null,
}, },
}); });
if (result.count <= 0) throw notFoundError("Request Data"); if (result.count <= 0) throw notFoundError("Request Data");

View file

@ -0,0 +1,25 @@
import express from "express";
import { Controller, Get, Path, Request, Route } from "tsoa";
import { getFile } from "../utils/minio";
@Route("api/v1/manual")
export class ManualController extends Controller {
@Get()
async get(@Request() req: express.Request) {
return req.res?.redirect(await getFile(".manual/toc.json"));
}
@Get("{category}/assets/{name}")
async getAsset(@Request() req: express.Request, @Path() category: string, @Path() name: string) {
return req.res?.redirect(await getFile(`.manual/${category}/assets/${name}`));
}
@Get("{category}/page/{page}")
async getContent(
@Request() req: express.Request,
@Path() category: string,
@Path() page: string,
) {
return req.res?.redirect(await getFile(`.manual/${category}/${page}.md`));
}
}

View file

@ -55,71 +55,80 @@ export async function deleteFolder(path: string) {
}); });
} }
const ROOT = ".system";
export const fileLocation = { export const fileLocation = {
branch: { branch: {
line: (branchId: string) => `branch/line-qr-${branchId}`, line: (branchId: string) => `${ROOT}/branch/line-qr-${branchId}`,
bank: (branchId: string, bankId: string) => `branch/bank-qr-${branchId}-${bankId}`, bank: (branchId: string, bankId: string) => `${ROOT}/branch/bank-qr-${branchId}-${bankId}`,
img: (branchId: string, name?: string) => `branch/img-${branchId}/${name || ""}`, img: (branchId: string, name?: string) => `${ROOT}/branch/img-${branchId}/${name || ""}`,
attachment: (branchId: string, name?: string) => `branch/attachment-${branchId}/${name || ""}`, attachment: (branchId: string, name?: string) =>
`${ROOT}/branch/attachment-${branchId}/${name || ""}`,
}, },
user: { user: {
profile: (userId: string, name?: string) => `user/profile-image-${userId}/${name || ""}`, profile: (userId: string, name?: string) =>
attachment: (userId: string, name?: string) => `user/attachment-${userId}/${name || ""}`, `${ROOT}/user/profile-image-${userId}/${name || ""}`,
attachment: (userId: string, name?: string) =>
`${ROOT}/user/attachment-${userId}/${name || ""}`,
}, },
customer: { customer: {
img: (customerId: string, name?: string) => `customer/img-${customerId}/${name || ""}`, img: (customerId: string, name?: string) => `${ROOT}/customer/img-${customerId}/${name || ""}`,
}, },
customerBranch: { customerBranch: {
attachment: (customerBranchId: string, name?: string) => attachment: (customerBranchId: string, name?: string) =>
`customer-branch/attachment-${customerBranchId}/${name || ""}`, `${ROOT}/customer-branch/attachment-${customerBranchId}/${name || ""}`,
citizen: (customerBranchId: string, citizenId?: string) => citizen: (customerBranchId: string, citizenId?: string) =>
`customer-branch/citizen-${customerBranchId}/${citizenId || ""}`, `${ROOT}/customer-branch/citizen-${customerBranchId}/${citizenId || ""}`,
houseRegistration: (customerBranchId: string, houseRegistrationId?: string) => houseRegistration: (customerBranchId: string, houseRegistrationId?: string) =>
`customer-branch/house-registration-${customerBranchId}/${houseRegistrationId || ""}`, `${ROOT}/customer-branch/house-registration-${customerBranchId}/${houseRegistrationId || ""}`,
commercialRegistration: (customerBranchId: string, commercialRegistrationId?: string) => commercialRegistration: (customerBranchId: string, commercialRegistrationId?: string) =>
`customer-branch/commercial-registration-${customerBranchId}/${commercialRegistrationId || ""}`, `${ROOT}/customer-branch/commercial-registration-${customerBranchId}/${commercialRegistrationId || ""}`,
vatRegistration: (customerBranchId: string, vatRegistrationId?: string) => vatRegistration: (customerBranchId: string, vatRegistrationId?: string) =>
`customer-branch/vat-registration-${customerBranchId}/${vatRegistrationId || ""}`, `${ROOT}/customer-branch/vat-registration-${customerBranchId}/${vatRegistrationId || ""}`,
powerOfAttorney: (customerBranchId: string, powerOfAttorneyId?: string) => powerOfAttorney: (customerBranchId: string, powerOfAttorneyId?: string) =>
`customer-branch/power-of-attorney-${customerBranchId}/${powerOfAttorneyId || ""}`, `${ROOT}/customer-branch/power-of-attorney-${customerBranchId}/${powerOfAttorneyId || ""}`,
}, },
employee: { employee: {
img: (employeeId: string, name?: string) => `employee/img-${employeeId}/${name || ""}`, img: (employeeId: string, name?: string) => `${ROOT}/employee/img-${employeeId}/${name || ""}`,
attachment: (employeeId: string, name?: string) => attachment: (employeeId: string, name?: string) =>
`employee/attachment-${employeeId}/${name || ""}`, `${ROOT}/employee/attachment-${employeeId}/${name || ""}`,
visa: (employeeId: string, visaId?: string) => `employee/visa-${employeeId}/${visaId || ""}`, visa: (employeeId: string, visaId?: string) =>
`${ROOT}/employee/visa-${employeeId}/${visaId || ""}`,
passport: (employeeId: string, passportId?: string) => passport: (employeeId: string, passportId?: string) =>
`employee/passport-${employeeId}/${passportId || ""}`, `${ROOT}/employee/passport-${employeeId}/${passportId || ""}`,
inCountryNotice: (employeeId: string, noticeId?: string) => inCountryNotice: (employeeId: string, noticeId?: string) =>
`employee/in-country-notice-${employeeId}/${noticeId || ""}`, `${ROOT}/employee/in-country-notice-${employeeId}/${noticeId || ""}`,
}, },
product: { product: {
img: (productId: string, name?: string) => `product/img-${productId}/${name || ""}`, img: (productId: string, name?: string) => `${ROOT}/product/img-${productId}/${name || ""}`,
}, },
service: { service: {
img: (serviceId: string, name?: string) => `service/img-${serviceId}/${name || ""}`, img: (serviceId: string, name?: string) => `${ROOT}/service/img-${serviceId}/${name || ""}`,
}, },
quotation: { quotation: {
attachment: (quotationId: string, name?: string) => attachment: (quotationId: string, name?: string) =>
`quotation/attachment-${quotationId}/${name || ""}`, `${ROOT}/quotation/attachment-${quotationId}/${name || ""}`,
payment: (quotationId: string, paymentId: string, name?: string) => payment: (quotationId: string, paymentId: string, name?: string) =>
`quotation/payment-${quotationId}/${paymentId}/${name || ""}`, `${ROOT}/quotation/payment-${quotationId}/${paymentId}/${name || ""}`,
}, },
request: { request: {
attachment: (requestId: string, step: number, name?: string) => attachment: (requestId: string, step: number, name?: string) =>
`request/attachment-${requestId}-${step}/${name || ""}`, `${ROOT}/request/attachment-${requestId}-${step}/${name || ""}`,
}, },
institution: { institution: {
attachment: (institutionId: string, name?: string) => attachment: (institutionId: string, name?: string) =>
`institution/attachment-${institutionId}/${name || ""}`, `${ROOT}/institution/attachment-${institutionId}/${name || ""}`,
img: (institutionId: string, name?: string) => `institution/img-${institutionId}/${name || ""}`, img: (institutionId: string, name?: string) =>
`${ROOT}/institution/img-${institutionId}/${name || ""}`,
}, },
task: { task: {
attachment: (taskId: string, name?: string) => `task/attachment-${taskId}/${name || ""}`, attachment: (taskId: string, name?: string) =>
`${ROOT}/task/attachment-${taskId}/${name || ""}`,
}, },
creditNote: { creditNote: {
slip: (creditNoteId: string, name?: string) => `credit-note/slip-${creditNoteId}/${name || ""}`, slip: (creditNoteId: string, name?: string) =>
`${ROOT}/credit-note/slip-${creditNoteId}/${name || ""}`,
attachment: (creditNoteId: string, name?: string) => attachment: (creditNoteId: string, name?: string) =>
`credit-note/attachment-${creditNoteId}/${name || ""}`, `${ROOT}/credit-note/attachment-${creditNoteId}/${name || ""}`,
}, },
}; };