Compare commits

..

No commits in common. "develop" and "version-0.11.11" have entirely different histories.

56 changed files with 777 additions and 4339 deletions

View file

@ -1,22 +1,33 @@
FROM node:20-slim FROM node:23-slim AS base
RUN apt-get update -y \ ENV PNPM_HOME="/pnpm"
&& apt-get install -y openssl \ ENV PATH="$PNPM_HOME:$PATH"
&& npm install -g pnpm \
&& apt-get clean \ RUN corepack enable
&& rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y openssl
RUN pnpm i -g prisma prisma-kysely
WORKDIR /app WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . . COPY . .
FROM base AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
RUN pnpm prisma generate
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm prisma generate RUN pnpm prisma generate
RUN pnpm run build RUN pnpm run build
COPY entrypoint.sh /entrypoint.sh FROM base AS prod
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENV NODE_ENV="production"
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
COPY --from=base /app/static /app/static
RUN chmod u+x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

View file

@ -7,7 +7,6 @@
"start": "node ./dist/app.js", "start": "node ./dist/app.js",
"dev": "nodemon", "dev": "nodemon",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"test": "vitest",
"format": "prettier --write .", "format": "prettier --write .",
"debug": "nodemon", "debug": "nodemon",
"build": "tsoa spec-and-routes && tsc", "build": "tsoa spec-and-routes && tsc",
@ -28,26 +27,22 @@
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"@types/node": "^20.17.10", "@types/node": "^20.17.10",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@vitest/ui": "^3.1.4",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prisma": "6.16.2", "prisma": "^6.3.0",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.7.2", "typescript": "^5.7.2"
"vitest": "^3.1.4"
}, },
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "^8.17.0", "@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2", "@fast-csv/parse": "^5.0.2",
"@prisma/client": "6.16.2", "@prisma/client": "^6.3.0",
"@scalar/express-api-reference": "^0.4.182", "@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0", "@tsoa/runtime": "^6.6.0",
"@types/html-to-text": "^9.0.4", "barcode": "^0.1.0",
"canvas": "^3.1.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^3.3.1", "cron": "^3.3.1",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dayjs-plugin-utc": "^0.1.2", "dayjs-plugin-utc": "^0.1.2",
"docx-templates": "^4.13.0", "docx-templates": "^4.13.0",
@ -55,15 +50,12 @@
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^4.21.2", "express": "^4.21.2",
"fast-jwt": "^5.0.5", "fast-jwt": "^5.0.5",
"html-to-text": "^9.0.5",
"jsbarcode": "^3.11.6",
"json-2-csv": "^5.5.8", "json-2-csv": "^5.5.8",
"kysely": "^0.27.5", "kysely": "^0.27.5",
"minio": "^8.0.2", "minio": "^8.0.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.2", "multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"pnpm": "^10.18.3",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"promise.any": "^2.0.6", "promise.any": "^2.0.6",
"thai-baht-text": "^2.0.5", "thai-baht-text": "^2.0.5",

1532
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
-- CreateTable
CREATE TABLE "WorkflowTemplateStepGroup" (
"id" TEXT NOT NULL,
"group" TEXT NOT NULL,
"workflowTemplateStepId" TEXT NOT NULL,
CONSTRAINT "WorkflowTemplateStepGroup_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "WorkflowTemplateStepGroup" ADD CONSTRAINT "WorkflowTemplateStepGroup_workflowTemplateStepId_fkey" FOREIGN KEY ("workflowTemplateStepId") REFERENCES "WorkflowTemplateStep"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,20 +0,0 @@
/*
Warnings:
- You are about to drop the column `importNationality` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "importNationality";
-- CreateTable
CREATE TABLE "UserImportNationality" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "UserImportNationality_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Employee" ADD COLUMN "otherNationality" TEXT;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "EmployeePassport" ADD COLUMN "otherNationality" TEXT;

View file

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "QuotationWorker" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -1,5 +0,0 @@
-- DropForeignKey
ALTER TABLE "UserImportNationality" DROP CONSTRAINT "UserImportNationality_userId_fkey";
-- AddForeignKey
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "Quotation" ADD COLUMN "sellerId" TEXT;
-- AddForeignKey
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,36 +0,0 @@
/*
Warnings:
- You are about to drop the column `businessType` on the `CustomerBranch` table. All the data in the column will be lost.
- You are about to drop the column `customerName` on the `CustomerBranch` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "CustomerBranch" DROP COLUMN "businessType",
DROP COLUMN "customerName",
ADD COLUMN "businessTypeId" TEXT;
-- AlterTable
ALTER TABLE "EmployeeVisa" ADD COLUMN "reportDate" DATE;
-- CreateTable
CREATE TABLE "BusinessType" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdByUserId" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedByUserId" TEXT,
CONSTRAINT "BusinessType_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_businessTypeId_fkey" FOREIGN KEY ("businessTypeId") REFERENCES "BusinessType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BusinessType" ADD CONSTRAINT "BusinessType_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BusinessType" ADD CONSTRAINT "BusinessType_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "addressForeign" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "districtText" TEXT,
ADD COLUMN "provinceText" TEXT,
ADD COLUMN "subDistrictText" TEXT,
ADD COLUMN "zipCodeText" TEXT;

View file

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "districtTextEN" TEXT,
ADD COLUMN "provinceTextEN" TEXT,
ADD COLUMN "subDistrictTextEN" TEXT;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "RequestWorkStepStatus" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "Payment" ADD COLUMN "account" TEXT,
ADD COLUMN "channel" TEXT,
ADD COLUMN "reference" TEXT;

View file

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "public"."Product" ADD COLUMN "flowAccountProductIdAgentPrice" TEXT,
ADD COLUMN "flowAccountProductIdSellPrice" TEXT;

View file

@ -366,14 +366,6 @@ enum UserType {
AGENCY AGENCY
} }
model UserImportNationality {
id String @id @default(cuid())
name String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
}
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
@ -398,24 +390,14 @@ model User {
street String? street String?
streetEN String? streetEN String?
addressForeign Boolean @default(false) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
provinceText String? district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
provinceTextEN String? districtId String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
districtText String? subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
districtTextEN String? subDistrictId String?
district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
districtId String?
subDistrictText String?
subDistrictTextEN String?
subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
subDistrictId String?
zipCodeText String?
email String email String
telephoneNo String telephoneNo String
@ -442,7 +424,7 @@ model User {
licenseExpireDate DateTime? @db.Date licenseExpireDate DateTime? @db.Date
sourceNationality String? sourceNationality String?
importNationality UserImportNationality[] importNationality String?
trainingPlace String? trainingPlace String?
responsibleArea UserResponsibleArea[] responsibleArea UserResponsibleArea[]
@ -511,8 +493,6 @@ model User {
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser") creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
institutionCreated Institution[] @relation("InstitutionCreatedByUser") institutionCreated Institution[] @relation("InstitutionCreatedByUser")
institutionUpdated Institution[] @relation("InstitutionUpdatedByUser") institutionUpdated Institution[] @relation("InstitutionUpdatedByUser")
businessTypeCreated BusinessType[] @relation("BusinessTypeCreatedByUser")
businessTypeUpdated BusinessType[] @relation("BusinessTypeUpdatedByUser")
requestWorkStepStatus RequestWorkStepStatus[] requestWorkStepStatus RequestWorkStepStatus[]
userTask UserTask[] userTask UserTask[]
@ -523,7 +503,6 @@ model User {
contactName String? contactName String?
contactTel String? contactTel String?
quotation Quotation[]
} }
model UserResponsibleArea { model UserResponsibleArea {
@ -562,9 +541,10 @@ model Customer {
} }
model CustomerBranch { model CustomerBranch {
id String @id @default(cuid()) id String @id @default(cuid())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String customerId String
customerName String?
code String code String
codeCustomer String codeCustomer String
@ -626,8 +606,7 @@ model CustomerBranch {
agentUser User? @relation(fields: [agentUserId], references: [id], onDelete: SetNull) agentUser User? @relation(fields: [agentUserId], references: [id], onDelete: SetNull)
// NOTE: Business // NOTE: Business
businessTypeId String? businessType String
businessType BusinessType? @relation(fields: [businessTypeId], references: [id], onDelete: SetNull)
jobPosition String jobPosition String
jobDescription String jobDescription String
payDate String payDate String
@ -786,21 +765,6 @@ model CustomerBranchVatRegis {
customerBranch CustomerBranch @relation(fields: [customerBranchId], references: [id], onDelete: Cascade) customerBranch CustomerBranch @relation(fields: [customerBranchId], references: [id], onDelete: Cascade)
} }
model BusinessType {
id String @id @default(cuid())
name String
nameEN String
createdAt DateTime @default(now())
createdBy User? @relation(name: "BusinessTypeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @updatedAt
updatedBy User? @relation(name: "BusinessTypeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
customerBranch CustomerBranch[]
}
model Employee { model Employee {
id String @id @default(cuid()) id String @id @default(cuid())
@ -815,10 +779,9 @@ model Employee {
lastName String? lastName String?
lastNameEN String? lastNameEN String?
dateOfBirth DateTime? @db.Date dateOfBirth DateTime? @db.Date
gender String gender String
nationality String nationality String
otherNationality String?
address String? address String?
addressEN String? addressEN String?
@ -893,19 +856,18 @@ model EmployeePassport {
issuePlace String issuePlace String
previousPassportRef String? previousPassportRef String?
workerStatus String? workerStatus String?
nationality String? nationality String?
otherNationality String? namePrefix String?
namePrefix String? firstName String?
firstName String? firstNameEN String?
firstNameEN String? middleName String?
middleName String? middleNameEN String?
middleNameEN String? lastName String?
lastName String? lastNameEN String?
lastNameEN String? gender String?
gender String? birthDate String?
birthDate String? birthCountry String?
birthCountry String?
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String employeeId String
@ -922,9 +884,8 @@ model EmployeeVisa {
entryCount Int entryCount Int
issueCountry String issueCountry String
issuePlace String issuePlace String
issueDate DateTime @db.Date issueDate DateTime @db.Date
expireDate DateTime @db.Date expireDate DateTime @db.Date
reportDate DateTime? @db.Date
mrz String? mrz String?
remark String? remark String?
@ -1128,15 +1089,6 @@ model WorkflowTemplateStepInstitution {
workflowTemplateStepId String workflowTemplateStepId String
} }
model WorkflowTemplateStepGroup {
id String @id @default(cuid())
group String
workflowTemplateStep WorkflowTemplateStep @relation(fields: [workflowTemplateStepId], references: [id], onDelete: Cascade)
workflowTemplateStepId String
}
model WorkflowTemplateStep { model WorkflowTemplateStep {
id String @id @default(cuid()) id String @id @default(cuid())
@ -1147,7 +1099,6 @@ model WorkflowTemplateStep {
value WorkflowTemplateStepValue[] // NOTE: For enum or options type value WorkflowTemplateStepValue[] // NOTE: For enum or options type
responsiblePerson WorkflowTemplateStepUser[] responsiblePerson WorkflowTemplateStepUser[]
responsibleInstitution WorkflowTemplateStepInstitution[] responsibleInstitution WorkflowTemplateStepInstitution[]
responsibleGroup WorkflowTemplateStepGroup[]
messengerByArea Boolean @default(false) messengerByArea Boolean @default(false)
attributes Json? attributes Json?
@ -1243,9 +1194,6 @@ model Product {
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade) productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
productGroupId String productGroupId String
flowAccountProductIdSellPrice String?
flowAccountProductIdAgentPrice String?
workProduct WorkProduct[] workProduct WorkProduct[]
quotationProductServiceList QuotationProductServiceList[] quotationProductServiceList QuotationProductServiceList[]
taskProduct TaskProduct[] taskProduct TaskProduct[]
@ -1418,9 +1366,6 @@ model Quotation {
invoice Invoice[] invoice Invoice[]
creditNote CreditNote[] creditNote CreditNote[]
seller User? @relation(fields: [sellerId], references: [id], onDelete: Cascade)
sellerId String?
} }
model QuotationPaySplit { model QuotationPaySplit {
@ -1445,9 +1390,6 @@ model QuotationWorker {
employeeId String employeeId String
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade) quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
quotationId String quotationId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
} }
model QuotationProductServiceList { model QuotationProductServiceList {
@ -1527,11 +1469,8 @@ model Payment {
paymentStatus PaymentStatus paymentStatus PaymentStatus
amount Float amount Float
date DateTime? date DateTime?
channel String?
account String?
reference String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull) createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
@ -1618,7 +1557,6 @@ model RequestWork {
model RequestWorkStepStatus { model RequestWorkStepStatus {
step Int step Int
workStatus RequestWorkStatus @default(Pending) workStatus RequestWorkStatus @default(Pending)
updatedAt DateTime @default(now()) @updatedAt
requestWork RequestWork @relation(fields: [requestWorkId], references: [id], onDelete: Cascade) requestWork RequestWork @relation(fields: [requestWorkId], references: [id], onDelete: Cascade)
requestWorkId String requestWorkId String

View file

@ -1,5 +1,4 @@
import { createCanvas } from "canvas"; import barcode from "barcode";
import JsBarcode from "jsbarcode";
import createReport from "docx-templates"; import createReport from "docx-templates";
import ThaiBahtText from "thai-baht-text"; import ThaiBahtText from "thai-baht-text";
import { District, Province, SubDistrict } from "@prisma/client"; import { District, Province, SubDistrict } from "@prisma/client";
@ -34,14 +33,8 @@ const quotationData = (id: string) =>
}, },
}, },
customerBranch: { customerBranch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: { include: {
customer: true, customer: true,
businessType: true,
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
@ -118,12 +111,12 @@ export class DocTemplateController extends Controller {
) { ) {
const ret = await edmList( const ret = await edmList(
"file", "file",
templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH, templateGroup ? [templateGroup, ...DOCUMENT_PATH] : DOCUMENT_PATH,
); );
if (ret) return ret.map((v) => v.fileName); if (ret) return ret.map((v) => v.fileName);
} }
return await listFile( return await listFile(
(templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH).join("/") + "/", (templateGroup ? [templateGroup, ...DOCUMENT_PATH] : DOCUMENT_PATH).join("/") + "/",
); );
} }
@ -260,23 +253,13 @@ export class DocTemplateController extends Controller {
thaiBahtText: (input: string | number) => { thaiBahtText: (input: string | number) => {
ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input); ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input);
}, },
barcode: async (data: string, width?: number, height?: number) => barcode: async (data: string) =>
new Promise<{ new Promise<string>((resolve, reject) =>
width: number; barcode("code39", { data, width: 400, height: 100 }).getBase64((err, data) => {
height: number; if (!err) return resolve(data);
data: string; return reject(err);
extension: string; }),
}>((resolve) => { ),
const canvas = createCanvas(400, 100);
JsBarcode(canvas, data);
resolve({
width: width ?? 8,
height: height ?? 3,
data: canvas.toDataURL("image/jpeg").slice("data:image/jpeg;base64".length),
extension: ".jpeg",
});
}),
}, },
}).then(Buffer.from); }).then(Buffer.from);
@ -293,7 +276,6 @@ function replaceEmptyField<T>(data: T): T {
} }
type FullAddress = { type FullAddress = {
addressForeign?: boolean;
address: string; address: string;
addressEN: string; addressEN: string;
moo?: string; moo?: string;
@ -302,14 +284,8 @@ type FullAddress = {
soiEN?: string; soiEN?: string;
street?: string; street?: string;
streetEN?: string; streetEN?: string;
provinceText?: string | null;
provinceTextEN?: string | null;
province?: Province | null; province?: Province | null;
districtText?: string | null;
districtTextEN?: string | null;
district?: District | null; district?: District | null;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrict?: SubDistrict | null; subDistrict?: SubDistrict | null;
en?: boolean; en?: boolean;
}; };
@ -343,22 +319,13 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
if (addr.soi) fragments.push(`ซอย ${addr.soi},`); if (addr.soi) fragments.push(`ซอย ${addr.soi},`);
if (addr.street) fragments.push(`ถนน${addr.street},`); if (addr.street) fragments.push(`ถนน${addr.street},`);
if (!addr.addressForeign && addr.subDistrict) { if (addr.subDistrict) {
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name}`); fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name},`);
} }
if (addr.addressForeign && addr.subDistrictText) { if (addr.district) {
fragments.push(`ตำบล${addr.subDistrictText}`); fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name},`);
} }
if (addr.province) fragments.push(`จังหวัด${addr.province.name},`);
if (!addr.addressForeign && addr.district) {
fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name}`);
}
if (addr.addressForeign && addr.districtText) {
fragments.push(`อำเภอ${addr.districtText}`);
}
if (!addr.addressForeign && addr.province) fragments.push(`จังหวัด${addr.province.name}`);
if (addr.addressForeign && addr.provinceText) fragments.push(`จังหวัด${addr.provinceText}`);
break; break;
default: default:
@ -367,31 +334,14 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
if (addr.soiEN) fragments.push(`Soi ${addr.soiEN},`); if (addr.soiEN) fragments.push(`Soi ${addr.soiEN},`);
if (addr.streetEN) fragments.push(`${addr.streetEN} Rd.`); if (addr.streetEN) fragments.push(`${addr.streetEN} Rd.`);
if (!addr.addressForeign && addr.subDistrict) { if (addr.subDistrict) {
fragments.push(`${addr.subDistrict.nameEN} sub-district,`); fragments.push(`${addr.subDistrict.nameEN} sub-district,`);
} }
if (addr.addressForeign && addr.subDistrictTextEN) { if (addr.district) fragments.push(`${addr.district.nameEN} district,`);
fragments.push(`${addr.subDistrictTextEN} sub-district,`); if (addr.province) fragments.push(`${addr.province.nameEN},`);
}
if (!addr.addressForeign && addr.district) {
fragments.push(`${addr.district.nameEN} district,`);
}
if (addr.addressForeign && addr.districtTextEN) {
fragments.push(`${addr.districtTextEN} district,`);
}
if (!addr.addressForeign && addr.province) {
fragments.push(`${addr.province.nameEN},`);
}
if (addr.addressForeign && addr.provinceTextEN) {
fragments.push(`${addr.provinceTextEN} district,`);
}
break; break;
} }
if (addr.subDistrict) fragments.push(addr.subDistrict.zipCode);
return fragments.join(" "); return fragments.join(" ");
} }
@ -404,9 +354,6 @@ function gender(text: string, lang: "th" | "en" = "en") {
} }
} }
/**
* @deprecated
*/
function businessType(text: string, lang: "th" | "en" = "en") { function businessType(text: string, lang: "th" | "en" = "en") {
switch (lang) { switch (lang) {
case "th": case "th":

View file

@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Path, Post, Query, Route, Security, Tags } from "tsoa"; import { Body, Controller, Delete, Get, Path, Post, Route, Security, Tags } from "tsoa";
import { addUserRoles, getGroup, listRole, removeUserRoles } from "../services/keycloak"; import { addUserRoles, getGroup, listRole, removeUserRoles } from "../services/keycloak";
@Route("api/v1/keycloak") @Route("api/v1/keycloak")
@ -46,9 +46,8 @@ export class KeycloakController extends Controller {
} }
@Get("group") @Get("group")
async getGroup(@Query() query: string = "") { async getGroup() {
const querySearch = query === "" ? "q" : `search=${query}`; const group = await getGroup();
const group = await getGroup(querySearch);
if (!Array.isArray(group)) throw new Error("Failed. Cannot get group(s) data from the server."); if (!Array.isArray(group)) throw new Error("Failed. Cannot get group(s) data from the server.");
return group; return group;

View file

@ -618,22 +618,9 @@ export class StatsController extends Controller {
startDate = dayjs(startDate).startOf("month").add(1, "month").toDate(); startDate = dayjs(startDate).startOf("month").add(1, "month").toDate();
} }
const invoices = await tx.invoice.findMany({
select: { id: true },
where: {
quotation: {
quotationStatus: { notIn: [QuotationStatus.Canceled] },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
});
if (invoices.length === 0) return [];
return await Promise.all( return await Promise.all(
months.map(async (v) => { months.map(async (v) => {
const date = dayjs(v); const date = dayjs(v);
return { return {
month: date.format("MM"), month: date.format("MM"),
year: date.format("YYYY"), year: date.format("YYYY"),
@ -642,7 +629,11 @@ export class StatsController extends Controller {
_sum: { amount: true }, _sum: { amount: true },
where: { where: {
createdAt: { gte: v, lte: date.endOf("month").toDate() }, createdAt: { gte: v, lte: date.endOf("month").toDate() },
invoiceId: { in: invoices.map((v) => v.id) }, invoice: {
quotation: {
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
}, },
by: "paymentStatus", by: "paymentStatus",
}) })

View file

@ -47,20 +47,16 @@ if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket."); throw Error("Require MinIO bucket.");
} }
const MANAGE_ROLES = [ const MANAGE_ROLES = ["system", "head_of_admin"];
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; return MANAGE_ROLES.some((v) => user.roles?.includes(v));
return user.roles?.some((v) => listAllowed.includes(v)) || false; }
function globalAllowView(user: RequestWithUser["user"]) {
return MANAGE_ROLES.concat("head_of_accountant", "head_of_sale").some((v) =>
user.roles?.includes(v),
);
} }
type BranchCreate = { type BranchCreate = {
@ -151,7 +147,7 @@ type BranchUpdate = {
}[]; }[];
}; };
const permissionCond = createPermCondition(globalAllow); const permissionCond = createPermCondition(globalAllowView);
const permissionCheck = createPermCheck(globalAllow); const permissionCheck = createPermCheck(globalAllow);
@Route("api/v1/branch") @Route("api/v1/branch")
@ -330,7 +326,7 @@ export class BranchController extends Controller {
district: true, district: true,
subDistrict: true, subDistrict: true,
}, },
orderBy: [{ statusOrder: "asc" }, { code: "asc" }], orderBy: { code: "asc" },
} }
: false, : false,
bank: true, bank: true,
@ -374,7 +370,7 @@ export class BranchController extends Controller {
bank: true, bank: true,
contact: includeContact, contact: includeContact,
}, },
orderBy: [{ statusOrder: "asc" }, { code: "asc" }], orderBy: { code: "asc" },
}, },
bank: true, bank: true,
contact: includeContact, contact: includeContact,
@ -387,14 +383,6 @@ export class BranchController extends Controller {
return record; return record;
} }
@Get("{branchId}/bank")
@Security("keycloak")
async getBranchBankById(@Path() branchId: string) {
return await prisma.branchBank.findMany({
where: { branchId },
});
}
@Post() @Post()
@Security("keycloak", MANAGE_ROLES) @Security("keycloak", MANAGE_ROLES)
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) { async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {

View file

@ -20,19 +20,10 @@ import { RequestWithUser } from "../interfaces/user";
import { branchRelationPermInclude, createPermCheck } from "../services/permission"; import { branchRelationPermInclude, createPermCheck } from "../services/permission";
import { queryOrNot, whereDateQuery } from "../utils/relation"; import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = [ const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const listAllowed = ["system", "head_of_admin", "admin"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return user.roles?.some((v) => listAllowed.includes(v)) || false;
} }

View file

@ -61,17 +61,10 @@ if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket."); throw Error("Require MinIO bucket.");
} }
const MANAGE_ROLES = [ const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
"system",
"head_of_admin",
"admin",
"executive",
"branch_admin",
"branch_manager",
];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive"]; const listAllowed = ["system", "head_of_admin"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return user.roles?.some((v) => listAllowed.includes(v)) || false;
} }
@ -106,12 +99,11 @@ type UserCreate = {
licenseIssueDate?: Date | null; licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null; licenseExpireDate?: Date | null;
sourceNationality?: string | null; sourceNationality?: string | null;
importNationality?: string[] | null; importNationality?: string | null;
trainingPlace?: string | null; trainingPlace?: string | null;
responsibleArea?: string[] | null; responsibleArea?: string[] | null;
birthDate?: Date | null; birthDate?: Date | null;
addressForeign?: boolean;
address: string; address: string;
addressEN: string; addressEN: string;
soi?: string | null; soi?: string | null;
@ -123,16 +115,9 @@ type UserCreate = {
email: string; email: string;
telephoneNo: string; telephoneNo: string;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrictId?: string | null; subDistrictId?: string | null;
districtText?: string | null;
districtTextEN?: string | null;
districtId?: string | null; districtId?: string | null;
provinceText?: string | null;
provinceTextEN?: string | null;
provinceId?: string | null; provinceId?: string | null;
zipCodeText?: string | null;
selectedImage?: string; selectedImage?: string;
@ -141,8 +126,8 @@ type UserCreate = {
remark?: string; remark?: string;
agencyStatus?: string; agencyStatus?: string;
contactName?: string | null; contactName?: string;
contactTel?: string | null; contactTel?: string;
}; };
type UserUpdate = { type UserUpdate = {
@ -159,9 +144,9 @@ type UserUpdate = {
namePrefix?: string | null; namePrefix?: string | null;
firstName?: string; firstName?: string;
firstNameEN?: string; firstNameEN: string;
middleName?: string | null; middleName?: string | null;
middleNameEN?: string | null; middleNameEN: string | null;
lastName?: string; lastName?: string;
lastNameEN?: string; lastNameEN?: string;
gender?: string; gender?: string;
@ -176,12 +161,11 @@ type UserUpdate = {
licenseIssueDate?: Date | null; licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null; licenseExpireDate?: Date | null;
sourceNationality?: string | null; sourceNationality?: string | null;
importNationality?: string[] | null; importNationality?: string | null;
trainingPlace?: string | null; trainingPlace?: string | null;
responsibleArea?: string[] | null; responsibleArea?: string[] | null;
birthDate?: Date | null; birthDate?: Date | null;
addressForeign?: boolean;
address?: string; address?: string;
addressEN?: string; addressEN?: string;
soi?: string | null; soi?: string | null;
@ -195,24 +179,17 @@ type UserUpdate = {
selectedImage?: string; selectedImage?: string;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrictId?: string | null; subDistrictId?: string | null;
districtText?: string | null;
districtTextEN?: string | null;
districtId?: string | null; districtId?: string | null;
provinceText?: string | null;
provinceTextEN?: string | null;
provinceId?: string | null; provinceId?: string | null;
zipCodeText?: string | null;
branchId?: string | string[]; branchId?: string | string[];
remark?: string; remark?: string;
agencyStatus?: string; agencyStatus?: string;
contactName?: string | null; contactName?: string;
contactTel?: string | null; contactTel?: string;
}; };
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);
@ -406,7 +383,6 @@ export class UserController extends Controller {
prisma.user.findMany({ prisma.user.findMany({
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: { include: {
importNationality: true,
responsibleArea: true, responsibleArea: true,
province: true, province: true,
district: true, district: true,
@ -425,7 +401,6 @@ export class UserController extends Controller {
return { return {
result: result.map((v) => ({ result: result.map((v) => ({
...v, ...v,
importNationality: v.importNationality.map((v) => v.name),
responsibleArea: v.responsibleArea.map((v) => v.area), responsibleArea: v.responsibleArea.map((v) => v.area),
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined, branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
})), })),
@ -440,7 +415,6 @@ export class UserController extends Controller {
async getUserById(@Path() userId: string) { async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({ const record = await prisma.user.findFirst({
include: { include: {
importNationality: true,
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
@ -452,11 +426,7 @@ export class UserController extends Controller {
if (!record) throw notFoundError("User"); if (!record) throw notFoundError("User");
const { importNationality, ...rest } = record; return record;
return Object.assign(rest, {
importNationality: importNationality.map((v) => v.name),
});
} }
@Post() @Post()
@ -558,9 +528,6 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })), create: rest.responsibleArea.map((v) => ({ area: v })),
} }
: undefined, : undefined,
importNationality: {
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"), statusOrder: +(rest.status === "INACTIVE"),
username, username,
userRole: role.name, userRole: role.name,
@ -716,7 +683,6 @@ export class UserController extends Controller {
const record = await prisma.user.update({ const record = await prisma.user.update({
include: { include: {
importNationality: true,
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
@ -731,10 +697,6 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })), create: rest.responsibleArea.map((v) => ({ area: v })),
} }
: undefined, : undefined,
importNationality: {
deleteMany: {},
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"), statusOrder: +(rest.status === "INACTIVE"),
userRole, userRole,
province: connectOrDisconnect(provinceId), province: connectOrDisconnect(provinceId),

View file

@ -23,16 +23,15 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager", "sale",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
type CustomerBranchCitizenPayload = { type CustomerBranchCitizenPayload = {

View file

@ -47,18 +47,15 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale", "sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);
@ -87,6 +84,7 @@ export type CustomerBranchCreate = {
authorizedCapital?: string; authorizedCapital?: string;
authorizedName?: string; authorizedName?: string;
authorizedNameEN?: string; authorizedNameEN?: string;
customerName?: string;
telephoneNo: string; telephoneNo: string;
@ -110,7 +108,7 @@ export type CustomerBranchCreate = {
contactName: string; contactName: string;
agentUserId?: string; agentUserId?: string;
businessTypeId?: string; businessType: string;
jobPosition: string; jobPosition: string;
jobDescription: string; jobDescription: string;
payDate: string; payDate: string;
@ -144,6 +142,7 @@ export type CustomerBranchUpdate = {
authorizedCapital?: string; authorizedCapital?: string;
authorizedName?: string; authorizedName?: string;
authorizedNameEN?: string; authorizedNameEN?: string;
customerName?: string;
telephoneNo: string; telephoneNo: string;
@ -167,7 +166,7 @@ export type CustomerBranchUpdate = {
contactName?: string; contactName?: string;
agentUserId?: string; agentUserId?: string;
businessTypeId?: string; businessType?: string;
jobPosition?: string; jobPosition?: string;
jobDescription?: string; jobDescription?: string;
payDate?: string; payDate?: string;
@ -202,6 +201,7 @@ export class CustomerBranchController extends Controller {
) { ) {
const where = { const where = {
OR: queryOrNot<Prisma.CustomerBranchWhereInput[]>(query, [ OR: queryOrNot<Prisma.CustomerBranchWhereInput[]>(query, [
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } }, { registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } }, { registerNameEN: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } }, { email: { contains: query, mode: "insensitive" } },
@ -238,11 +238,6 @@ export class CustomerBranchController extends Controller {
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
prisma.customerBranch.findMany({ prisma.customerBranch.findMany({
orderBy: [{ code: "asc" }, { statusOrder: "asc" }, { createdAt: "asc" }], orderBy: [{ code: "asc" }, { statusOrder: "asc" }, { createdAt: "asc" }],
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: { include: {
customer: includeCustomer, customer: includeCustomer,
province: true, province: true,
@ -251,7 +246,6 @@ export class CustomerBranchController extends Controller {
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
_count: true, _count: true,
businessType: true,
}, },
where, where,
take: pageSize, take: pageSize,
@ -267,11 +261,6 @@ export class CustomerBranchController extends Controller {
@Security("keycloak") @Security("keycloak")
async getById(@Path() branchId: string) { async getById(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({ const record = await prisma.customerBranch.findFirst({
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: { include: {
customer: true, customer: true,
province: true, province: true,
@ -279,7 +268,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true, subDistrict: true,
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
businessType: true,
}, },
where: { id: branchId }, where: { id: branchId },
}); });
@ -362,11 +350,6 @@ export class CustomerBranchController extends Controller {
include: branchRelationPermInclude(req.user), include: branchRelationPermInclude(req.user),
}, },
branch: { branch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
take: 1, take: 1,
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "asc" },
}, },
@ -395,15 +378,7 @@ export class CustomerBranchController extends Controller {
(v) => (v.headOffice || v).code, (v) => (v.headOffice || v).code,
); );
const { const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
provinceId,
districtId,
subDistrictId,
customerId,
agentUserId,
businessTypeId,
...rest
} = body;
const record = await prisma.$transaction( const record = await prisma.$transaction(
async (tx) => { async (tx) => {
@ -446,7 +421,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true, subDistrict: true,
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
businessType: true,
}, },
data: { data: {
...rest, ...rest,
@ -458,7 +432,6 @@ export class CustomerBranchController extends Controller {
province: connectOrNot(provinceId), province: connectOrNot(provinceId),
district: connectOrNot(districtId), district: connectOrNot(districtId),
subDistrict: connectOrNot(subDistrictId), subDistrict: connectOrNot(subDistrictId),
businessType: connectOrNot(businessTypeId),
createdBy: { connect: { id: req.user.sub } }, createdBy: { connect: { id: req.user.sub } },
updatedBy: { connect: { id: req.user.sub } }, updatedBy: { connect: { id: req.user.sub } },
}, },
@ -489,7 +462,6 @@ export class CustomerBranchController extends Controller {
}, },
}, },
}, },
businessType: true,
}, },
}); });
@ -534,15 +506,7 @@ export class CustomerBranchController extends Controller {
await permissionCheck(req.user, customer.registeredBranch); await permissionCheck(req.user, customer.registeredBranch);
} }
const { const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
provinceId,
districtId,
subDistrictId,
customerId,
agentUserId,
businessTypeId,
...rest
} = body;
return await prisma.customerBranch.update({ return await prisma.customerBranch.update({
where: { id: branchId }, where: { id: branchId },
@ -552,7 +516,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true, subDistrict: true,
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
businessType: true,
}, },
data: { data: {
...rest, ...rest,
@ -562,7 +525,6 @@ export class CustomerBranchController extends Controller {
province: connectOrDisconnect(provinceId), province: connectOrDisconnect(provinceId),
district: connectOrDisconnect(districtId), district: connectOrDisconnect(districtId),
subDistrict: connectOrDisconnect(subDistrictId), subDistrict: connectOrDisconnect(subDistrictId),
businessType: connectOrNot(businessTypeId),
updatedBy: { connect: { id: req.user.sub } }, updatedBy: { connect: { id: req.user.sub } },
}, },
}); });
@ -581,7 +543,6 @@ export class CustomerBranchController extends Controller {
}, },
}, },
}, },
businessType: true,
}, },
}); });
@ -634,11 +595,10 @@ export class CustomerBranchFileController extends Controller {
}, },
}, },
}, },
businessType: true,
}, },
}); });
if (!data) throw notFoundError("Customer Branch"); if (!data) throw notFoundError("Customer Branch");
await permissionCheckCompany(user, data.customer.registeredBranch); await permissionCheck(user, data.customer.registeredBranch);
} }
@Get("attachment") @Get("attachment")

View file

@ -37,24 +37,20 @@ import {
} from "../utils/minio"; } from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error"; import { isUsedError, notFoundError, relationError } from "../utils/error";
import { connectOrNot, queryOrNot, whereDateQuery } from "../utils/relation"; import { connectOrNot, queryOrNot, whereDateQuery } from "../utils/relation";
import { json2csv } from "json-2-csv";
const MANAGE_ROLES = [ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale", "sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);
@ -86,6 +82,7 @@ export type CustomerCreate = {
authorizedCapital?: string; authorizedCapital?: string;
authorizedName?: string; authorizedName?: string;
authorizedNameEN?: string; authorizedNameEN?: string;
customerName?: string;
telephoneNo: string; telephoneNo: string;
@ -109,7 +106,7 @@ export type CustomerCreate = {
contactName: string; contactName: string;
agentUserId?: string; agentUserId?: string;
businessTypeId?: string | null; businessType: string;
jobPosition: string; jobPosition: string;
jobDescription: string; jobDescription: string;
payDate: string; payDate: string;
@ -170,14 +167,11 @@ export class CustomerController extends Controller {
@Query() activeBranchOnly?: boolean, @Query() activeBranchOnly?: boolean,
@Query() startDate?: Date, @Query() startDate?: Date,
@Query() endDate?: Date, @Query() endDate?: Date,
@Query() businessTypeId?: string,
@Query() provinceId?: string,
@Query() districtId?: string,
@Query() subDistrictId?: string,
) { ) {
const where = { const where = {
OR: queryOrNot<Prisma.CustomerWhereInput[]>(query, [ OR: queryOrNot<Prisma.CustomerWhereInput[]>(query, [
{ branch: { some: { namePrefix: { contains: query, mode: "insensitive" } } } }, { branch: { some: { namePrefix: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { customerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerName: { contains: query, mode: "insensitive" } } } }, { branch: { some: { registerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerNameEN: { contains: query, mode: "insensitive" } } } }, { branch: { some: { registerNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { firstName: { contains: query, mode: "insensitive" } } } }, { branch: { some: { firstName: { contains: query, mode: "insensitive" } } } },
@ -196,35 +190,6 @@ export class CustomerController extends Controller {
: permissionCond(req.user, { activeOnly: activeBranchOnly }), : permissionCond(req.user, { activeOnly: activeBranchOnly }),
}, },
}, },
branch: {
some: {
AND: [
businessTypeId
? {
OR: [{ businessType: { id: businessTypeId } }],
}
: {},
provinceId
? {
OR: [{ province: { id: provinceId } }],
}
: {},
districtId
? {
OR: [{ district: { id: districtId } }],
}
: {},
subDistrictId
? {
OR: [{ subDistrict: { id: subDistrictId } }],
}
: {},
],
},
},
...whereDateQuery(startDate, endDate), ...whereDateQuery(startDate, endDate),
} satisfies Prisma.CustomerWhereInput; } satisfies Prisma.CustomerWhereInput;
@ -235,16 +200,10 @@ export class CustomerController extends Controller {
branch: includeBranch branch: includeBranch
? { ? {
include: { include: {
businessType: true,
province: true, province: true,
district: true, district: true,
subDistrict: true, subDistrict: true,
}, },
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
} }
: { : {
@ -253,17 +212,11 @@ export class CustomerController extends Controller {
district: true, district: true,
subDistrict: true, subDistrict: true,
}, },
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
take: 1, take: 1,
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "asc" },
}, },
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
// businessType:true
}, },
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where, where,
@ -288,11 +241,6 @@ export class CustomerController extends Controller {
district: true, district: true,
subDistrict: true, subDistrict: true,
}, },
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "asc" },
}, },
createdBy: true, createdBy: true,
@ -364,11 +312,6 @@ export class CustomerController extends Controller {
district: true, district: true,
subDistrict: true, subDistrict: true,
}, },
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
}, },
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
@ -380,8 +323,6 @@ export class CustomerController extends Controller {
...v, ...v,
code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - branch.length + i}`.padStart(2, "0")}`, code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - branch.length + i}`.padStart(2, "0")}`,
codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""), codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""),
businessType: connectOrNot(v.businessTypeId),
businessTypeId: undefined,
agentUser: connectOrNot(v.agentUserId), agentUser: connectOrNot(v.agentUserId),
agentUserId: undefined, agentUserId: undefined,
province: connectOrNot(v.provinceId), province: connectOrNot(v.provinceId),
@ -468,11 +409,6 @@ export class CustomerController extends Controller {
district: true, district: true,
subDistrict: true, subDistrict: true,
}, },
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
}, },
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
@ -511,13 +447,7 @@ export class CustomerController extends Controller {
await deleteFolder(`customer/${customerId}`); await deleteFolder(`customer/${customerId}`);
const data = await tx.customer.delete({ const data = await tx.customer.delete({
include: { include: {
branch: { branch: true,
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
registeredBranch: { registeredBranch: {
include: { include: {
headOffice: true, headOffice: true,
@ -612,52 +542,3 @@ export class CustomerImageController extends Controller {
await deleteFile(fileLocation.customer.img(customerId, name)); await deleteFile(fileLocation.customer.img(customerId, name));
} }
} }
@Route("api/v1/customer-export")
@Tags("Customer")
export class CustomerExportController extends CustomerController {
@Get()
@Security("keycloak")
async exportCustomer(
@Request() req: RequestWithUser,
@Query() customerType?: CustomerType,
@Query() query: string = "",
@Query() status?: Status,
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() includeBranch: boolean = false,
@Query() company: boolean = false,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Query() businessTypeId?: string,
@Query() provinceId?: string,
@Query() districtId?: string,
@Query() subDistrictId?: string,
) {
const ret = await this.list(
req,
customerType,
query,
status,
page,
pageSize,
includeBranch,
company,
activeBranchOnly,
startDate,
endDate,
businessTypeId,
provinceId,
districtId,
subDistrictId,
);
this.setHeader("Content-Type", "text/csv");
return json2csv(
ret.result.map((v) => Object.assign(v, { branch: v.branch.at(0) ?? null })),
{ useDateIso8601Format: true, expandNestedObjects: true },
);
}
}

View file

@ -23,18 +23,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
type EmployeeCheckupPayload = { type EmployeeCheckupPayload = {

View file

@ -42,7 +42,6 @@ import {
listFile, listFile,
setFile, setFile,
} from "../utils/minio"; } from "../utils/minio";
import { json2csv } from "json-2-csv";
if (!process.env.MINIO_BUCKET) { if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket."); throw Error("Require MinIO bucket.");
@ -52,23 +51,17 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true);
const permissionCond = createPermCondition(globalAllow); const permissionCond = createPermCondition(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
const permissionCheck = createPermCheck(globalAllow); const permissionCheck = createPermCheck(globalAllow);
type EmployeeCreate = { type EmployeeCreate = {
@ -81,7 +74,6 @@ type EmployeeCreate = {
dateOfBirth?: Date | null; dateOfBirth?: Date | null;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string | null; namePrefix?: string | null;
firstName?: string; firstName?: string;
@ -115,10 +107,9 @@ type EmployeeUpdate = {
nrcNo?: string | null; nrcNo?: string | null;
dateOfBirth?: Date | null; dateOfBirth?: Date;
gender?: string; gender?: string;
nationality?: string; nationality?: string;
otherNationality?: string | null;
namePrefix?: string | null; namePrefix?: string | null;
firstName?: string; firstName?: string;
@ -151,18 +142,9 @@ type EmployeeUpdate = {
export class EmployeeController extends Controller { export class EmployeeController extends Controller {
@Get("stats") @Get("stats")
@Security("keycloak") @Security("keycloak")
async getEmployeeStats(@Request() req: RequestWithUser, @Query() customerBranchId?: string) { async getEmployeeStats(@Query() customerBranchId?: string) {
return await prisma.employee.count({ return await prisma.employee.count({
where: { where: { customerBranchId },
customerBranchId,
customerBranch: {
customer: isSystem(req.user)
? undefined
: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
},
}); });
} }
@ -250,6 +232,7 @@ export class EmployeeController extends Controller {
endDate, endDate,
); );
} }
@Post("list") @Post("list")
@Security("keycloak") @Security("keycloak")
async listByCriteria( async listByCriteria(
@ -671,7 +654,7 @@ export class EmployeeFileController extends Controller {
}, },
}); });
if (!data) throw notFoundError("Employee"); if (!data) throw notFoundError("Employee");
await permissionCheckCompany(user, data.customerBranch.customer.registeredBranch); await permissionCheck(user, data.customerBranch.customer.registeredBranch);
} }
@Get("image") @Get("image")
@ -927,55 +910,3 @@ export class EmployeeFileController extends Controller {
return await deleteFile(fileLocation.employee.inCountryNotice(employeeId, noticeId)); return await deleteFile(fileLocation.employee.inCountryNotice(employeeId, noticeId));
} }
} }
@Route("api/v1/employee-export")
@Tags("Employee")
export class EmployeeExportController extends EmployeeController {
@Get()
@Security("keycloak")
async exportEmployee(
@Request() req: RequestWithUser,
@Query() zipCode?: string,
@Query() gender?: string,
@Query() status?: Status,
@Query() visa?: boolean,
@Query() passport?: boolean,
@Query() customerId?: string,
@Query() customerBranchId?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const ret = await this.listByCriteria(
req,
zipCode,
gender,
status,
visa,
passport,
customerId,
customerBranchId,
query,
page,
pageSize,
activeOnly,
startDate,
endDate,
);
this.setHeader("Content-Type", "text/csv");
return json2csv(
ret.result.map((v) =>
Object.assign(v, {
employeePassport: v.employeePassport?.at(0) ?? null,
employeeVisa: v.employeeVisa?.at(0) ?? null,
}),
),
{ useDateIso8601Format: true },
);
}
}

View file

@ -23,18 +23,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
type EmployeeOtherInfoPayload = { type EmployeeOtherInfoPayload = {

View file

@ -22,18 +22,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
type EmployeePassportPayload = { type EmployeePassportPayload = {
@ -47,7 +43,6 @@ type EmployeePassportPayload = {
workerStatus: string; workerStatus: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string | null; namePrefix?: string | null;
firstName: string; firstName: string;
firstNameEN: string; firstNameEN: string;

View file

@ -22,18 +22,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
type EmployeeVisaPayload = { type EmployeeVisaPayload = {
@ -44,7 +40,6 @@ type EmployeeVisaPayload = {
issuePlace: string; issuePlace: string;
issueDate: Date; issueDate: Date;
expireDate: Date; expireDate: Date;
reportDate?: Date | null;
mrz?: string | null; mrz?: string | null;
remark?: string | null; remark?: string | null;

View file

@ -22,18 +22,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale", "head_of_sale",
"sale",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
type EmployeeWorkPayload = { type EmployeeWorkPayload = {

View file

@ -37,37 +37,20 @@ type WorkflowPayload = {
attributes?: { [key: string]: any }; attributes?: { [key: string]: any };
responsiblePersonId?: string[]; responsiblePersonId?: string[];
responsibleInstitution?: string[]; responsibleInstitution?: string[];
responsibleGroup?: string[];
messengerByArea?: boolean; messengerByArea?: boolean;
}[]; }[];
registeredBranchId?: string; registeredBranchId?: string;
status?: Status; status?: Status;
}; };
const MANAGE_ROLES = [ const permissionCondCompany = createPermCondition((_) => true);
"system", const permissionCheckCompany = createPermCheck((_) => true);
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
}
const permissionCondCompany = createPermCondition(globalAllow);
const permissionCheckCompany = createPermCheck(globalAllow);
@Route("api/v1/workflow-template") @Route("api/v1/workflow-template")
@Tags("Workflow") @Tags("Workflow")
@Security("keycloak")
export class FlowTemplateController extends Controller { export class FlowTemplateController extends Controller {
@Get() @Get()
@Security("keycloak")
async getFlowTemplate( async getFlowTemplate(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Query() page: number = 1, @Query() page: number = 1,
@ -106,7 +89,6 @@ export class FlowTemplateController extends Controller {
include: { user: true }, include: { user: true },
}, },
responsibleInstitution: true, responsibleInstitution: true,
responsibleGroup: true,
}, },
orderBy: { order: "asc" }, orderBy: { order: "asc" },
}, },
@ -124,7 +106,6 @@ export class FlowTemplateController extends Controller {
step: r.step.map((v) => ({ step: r.step.map((v) => ({
...v, ...v,
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group), responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
responsibleGroup: v.responsibleGroup.map((group) => group.group),
})), })),
})), })),
page, page,
@ -134,7 +115,6 @@ export class FlowTemplateController extends Controller {
} }
@Get("{templateId}") @Get("{templateId}")
@Security("keycloak")
async getFlowTemplateById(@Request() _req: RequestWithUser, @Path() templateId: string) { async getFlowTemplateById(@Request() _req: RequestWithUser, @Path() templateId: string) {
const record = await prisma.workflowTemplate.findFirst({ const record = await prisma.workflowTemplate.findFirst({
include: { include: {
@ -146,7 +126,6 @@ export class FlowTemplateController extends Controller {
include: { user: true }, include: { user: true },
}, },
responsibleInstitution: true, responsibleInstitution: true,
responsibleGroup: true,
}, },
}, },
}, },
@ -161,13 +140,11 @@ export class FlowTemplateController extends Controller {
step: record.step.map((v) => ({ step: record.step.map((v) => ({
...v, ...v,
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group), responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
responsibleGroup: v.responsibleGroup.map((group) => group.group),
})), })),
}; };
} }
@Post() @Post()
@Security("keycloak", MANAGE_ROLES)
async createFlowTemplate(@Request() req: RequestWithUser, @Body() body: WorkflowPayload) { async createFlowTemplate(@Request() req: RequestWithUser, @Body() body: WorkflowPayload) {
const where = { const where = {
OR: [ OR: [
@ -238,9 +215,6 @@ export class FlowTemplateController extends Controller {
responsibleInstitution: { responsibleInstitution: {
create: v.responsibleInstitution?.map((group) => ({ group })), create: v.responsibleInstitution?.map((group) => ({ group })),
}, },
responsibleGroup: {
create: v.responsibleGroup?.map((group) => ({ group })),
},
})), })),
}, },
}, },
@ -248,7 +222,6 @@ export class FlowTemplateController extends Controller {
} }
@Put("{templateId}") @Put("{templateId}")
@Security("keycloak", MANAGE_ROLES)
async updateFlowTemplate( async updateFlowTemplate(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() templateId: string, @Path() templateId: string,
@ -322,10 +295,6 @@ export class FlowTemplateController extends Controller {
deleteMany: {}, deleteMany: {},
create: v.responsibleInstitution?.map((group) => ({ group })), create: v.responsibleInstitution?.map((group) => ({ group })),
}, },
responsibleGroup: {
deleteMany: {},
create: v.responsibleGroup?.map((group) => ({ group })),
},
}, },
})), })),
}, },
@ -334,7 +303,6 @@ export class FlowTemplateController extends Controller {
} }
@Delete("{templateId}") @Delete("{templateId}")
@Security("keycloak", MANAGE_ROLES)
async deleteFlowTemplateById(@Request() req: RequestWithUser, @Path() templateId: string) { async deleteFlowTemplateById(@Request() req: RequestWithUser, @Path() templateId: string) {
const record = await prisma.workflowTemplate.findUnique({ const record = await prisma.workflowTemplate.findUnique({
where: { id: templateId }, where: { id: templateId },

View file

@ -95,17 +95,6 @@ type InstitutionUpdatePayload = {
}[]; }[];
}; };
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
@Route("api/v1/institution") @Route("api/v1/institution")
@Tags("Institution") @Tags("Institution")
export class InstitutionController extends Controller { export class InstitutionController extends Controller {
@ -196,7 +185,7 @@ export class InstitutionController extends Controller {
} }
@Post() @Post()
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
@OperationId("createInstitution") @OperationId("createInstitution")
async createInstitution( async createInstitution(
@Body() @Body()
@ -240,7 +229,7 @@ export class InstitutionController extends Controller {
} }
@Put("{institutionId}") @Put("{institutionId}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
@OperationId("updateInstitution") @OperationId("updateInstitution")
async updateInstitution( async updateInstitution(
@Path() institutionId: string, @Path() institutionId: string,
@ -289,7 +278,7 @@ export class InstitutionController extends Controller {
} }
@Delete("{institutionId}") @Delete("{institutionId}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
@OperationId("deleteInstitution") @OperationId("deleteInstitution")
async deleteInstitution(@Path() institutionId: string) { async deleteInstitution(@Path() institutionId: string) {
return await prisma.$transaction(async (tx) => { return await prisma.$transaction(async (tx) => {
@ -361,7 +350,7 @@ export class InstitutionFileController extends Controller {
} }
@Put("image/{name}") @Put("image/{name}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
async putImage( async putImage(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() institutionId: string, @Path() institutionId: string,
@ -375,7 +364,7 @@ export class InstitutionFileController extends Controller {
} }
@Delete("image/{name}") @Delete("image/{name}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
async delImage( async delImage(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() institutionId: string, @Path() institutionId: string,
@ -405,7 +394,7 @@ export class InstitutionFileController extends Controller {
} }
@Put("attachment/{name}") @Put("attachment/{name}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
async putAttachment( async putAttachment(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() institutionId: string, @Path() institutionId: string,
@ -416,7 +405,7 @@ export class InstitutionFileController extends Controller {
} }
@Delete("attachment/{name}") @Delete("attachment/{name}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
async delAttachment( async delAttachment(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() institutionId: string, @Path() institutionId: string,
@ -447,7 +436,7 @@ export class InstitutionFileController extends Controller {
} }
@Put("bank-qr/{bankId}") @Put("bank-qr/{bankId}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
async putBankImage( async putBankImage(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() institutionId: string, @Path() institutionId: string,
@ -461,7 +450,7 @@ export class InstitutionFileController extends Controller {
} }
@Delete("bank-qr/{bankId}") @Delete("bank-qr/{bankId}")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak")
async delBankImage( async delBankImage(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() institutionId: string, @Path() institutionId: string,

View file

@ -29,23 +29,14 @@ type InvoicePayload = {
installmentNo: number[]; installmentNo: number[];
}; };
const MANAGE_ROLES = [ const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const allowList = ["system", "head_of_admin", "head_of_accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition(globalAllow); const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow); const permissionCheck = createPermCheck(globalAllow);
@Route("/api/v1/invoice") @Route("/api/v1/invoice")
@ -117,6 +108,7 @@ export class InvoiceController extends Controller {
customerBranch: { customerBranch: {
OR: [ OR: [
{ code: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } }, { registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } }, { registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
@ -192,7 +184,7 @@ export class InvoiceController extends Controller {
@Post() @Post()
@OperationId("createInvoice") @OperationId("createInvoice")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"])) @Security("keycloak", MANAGE_ROLES)
async createInvoice(@Request() req: RequestWithUser, @Body() body: InvoicePayload) { async createInvoice(@Request() req: RequestWithUser, @Body() body: InvoicePayload) {
const [quotation] = await prisma.$transaction([ const [quotation] = await prisma.$transaction([
prisma.quotation.findUnique({ prisma.quotation.findUnique({
@ -237,7 +229,7 @@ export class InvoiceController extends Controller {
title: "ใบแจ้งหนี้ใหม่ / New Invoice", title: "ใบแจ้งหนี้ใหม่ / New Invoice",
detail: "รหัส / code : " + record.code, detail: "รหัส / code : " + record.code,
registeredBranchId: record.registeredBranchId, registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "branch_accountant" } }, groupReceiver: { create: { name: "accountant" } },
}, },
}); });

View file

@ -30,23 +30,19 @@ import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } fr
import { isUsedError, notFoundError, relationError } from "../utils/error"; import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation"; import { queryOrNot, whereDateQuery } from "../utils/relation";
import spreadsheet from "../utils/spreadsheet"; import spreadsheet from "../utils/spreadsheet";
import flowAccount from "../services/flowaccount";
import { json2csv } from "json-2-csv";
const MANAGE_ROLES = [ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);
@ -78,7 +74,6 @@ type ProductCreate = {
type ProductUpdate = { type ProductUpdate = {
status?: "ACTIVE" | "INACTIVE"; status?: "ACTIVE" | "INACTIVE";
code?: string;
name?: string; name?: string;
detail?: string; detail?: string;
process?: number; process?: number;
@ -302,21 +297,13 @@ export class ProductController extends Controller {
}, },
update: { value: { increment: 1 } }, update: { value: { increment: 1 } },
}); });
return await prisma.product.create({
const listId = await flowAccount.createProducts(
`${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
body,
);
return await tx.product.create({
include: { include: {
createdBy: true, createdBy: true,
updatedBy: true, updatedBy: true,
}, },
data: { data: {
...body, ...body,
flowAccountProductIdAgentPrice: `${listId.data.productIdAgentPrice}`,
flowAccountProductIdSellPrice: `${listId.data.productIdSellPrice}`,
document: body.document document: body.document
? { ? {
createMany: { data: body.document.map((v) => ({ name: v })) }, createMany: { data: body.document.map((v) => ({ name: v })) },
@ -390,30 +377,6 @@ export class ProductController extends Controller {
await permissionCheck(req.user, productGroup.registeredBranch); await permissionCheck(req.user, productGroup.registeredBranch);
} }
if (
product.flowAccountProductIdSellPrice !== null &&
product.flowAccountProductIdAgentPrice !== null
) {
const mergedBody = {
...body,
code: body.code ?? product.code,
price: body.price ?? product.price,
agentPrice: body.agentPrice ?? product.agentPrice,
serviceCharge: body.serviceCharge ?? product.serviceCharge,
vatIncluded: body.vatIncluded ?? product.vatIncluded,
agentPriceVatIncluded: body.agentPriceVatIncluded ?? product.agentPriceVatIncluded,
serviceChargeVatIncluded: body.serviceChargeVatIncluded ?? product.serviceChargeVatIncluded,
};
await flowAccount.editProducts(
product.flowAccountProductIdSellPrice,
product.flowAccountProductIdAgentPrice,
mergedBody,
);
} else {
throw notFoundError("FlowAccountProductId");
}
const record = await prisma.product.update({ const record = await prisma.product.update({
include: { include: {
productGroup: true, productGroup: true,
@ -476,18 +439,6 @@ export class ProductController extends Controller {
if (record.status !== Status.CREATED) throw isUsedError("Product"); if (record.status !== Status.CREATED) throw isUsedError("Product");
if (
record.flowAccountProductIdSellPrice !== null &&
record.flowAccountProductIdAgentPrice !== null
) {
await Promise.all([
flowAccount.deleteProduct(record.flowAccountProductIdSellPrice),
flowAccount.deleteProduct(record.flowAccountProductIdAgentPrice),
]);
} else {
throw notFoundError("FlowAccountProductId");
}
await deleteFolder(fileLocation.product.img(productId)); await deleteFolder(fileLocation.product.img(productId));
return await prisma.product.delete({ return await prisma.product.delete({
@ -689,43 +640,3 @@ export class ProductFileController extends Controller {
return await deleteFile(fileLocation.product.img(productId, name)); return await deleteFile(fileLocation.product.img(productId, name));
} }
} }
@Route("api/v1/product-export")
@Tags("Product")
export class ProductExportController extends ProductController {
@Get()
@Security("keycloak")
async exportCustomer(
@Request() req: RequestWithUser,
@Query() status?: Status,
@Query() shared?: boolean,
@Query() productGroupId?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() orderField?: keyof Product,
@Query() orderBy?: "asc" | "desc",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const ret = await this.getProduct(
req,
status,
shared,
productGroupId,
query,
page,
pageSize,
orderField,
orderBy,
activeOnly,
startDate,
endDate,
);
this.setHeader("Content-Type", "text/csv");
return json2csv(ret.result, { useDateIso8601Format: true, expandNestedObjects: true });
}
}

View file

@ -35,7 +35,7 @@ type ProductGroupCreate = {
remark: string; remark: string;
status?: Status; status?: Status;
shared?: boolean; shared?: boolean;
registeredBranchId?: string; registeredBranchId: string;
}; };
type ProductGroupUpdate = { type ProductGroupUpdate = {
@ -51,16 +51,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCond = createPermCondition((_) => true); const permissionCond = createPermCondition((_) => true);
@ -159,23 +157,7 @@ export class ProductGroup extends Controller {
@Post() @Post()
@Security("keycloak", MANAGE_ROLES) @Security("keycloak", MANAGE_ROLES)
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) { async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
const userAffiliatedBranch = await prisma.branch.findFirst({ let company = await permissionCheck(req.user, body.registeredBranchId).then(
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",
);
}
let company = await permissionCheck(req.user, userAffiliatedBranch).then(
(v) => (v.headOffice || v).code, (v) => (v.headOffice || v).code,
); );
@ -199,7 +181,6 @@ export class ProductGroup extends Controller {
}, },
data: { data: {
...body, ...body,
registeredBranchId: userAffiliatedBranch.id,
statusOrder: +(body.status === "INACTIVE"), statusOrder: +(body.status === "INACTIVE"),
code: `G${last.value.toString().padStart(2, "0")}`, code: `G${last.value.toString().padStart(2, "0")}`,
createdByUserId: req.user.sub, createdByUserId: req.user.sub,

View file

@ -42,16 +42,14 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);

View file

@ -26,20 +26,11 @@ import flowAccount from "../services/flowaccount";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
const MANAGE_ROLES = [ const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const allowList = ["system", "head_of_admin", "head_of_accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);
@ -110,18 +101,10 @@ export class QuotationPayment extends Controller {
} }
@Put("{paymentId}") @Put("{paymentId}")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"])) @Security("keycloak", MANAGE_ROLES)
async updatePayment( async updatePayment(
@Path() paymentId: string, @Path() paymentId: string,
@Body() @Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus },
body: {
amount?: number;
date?: Date;
paymentStatus?: PaymentStatus;
channel?: string | null;
account?: string | null;
reference?: string | null;
},
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
) { ) {
const record = await prisma.payment.findUnique({ const record = await prisma.payment.findUnique({
@ -152,18 +135,7 @@ export class QuotationPayment extends Controller {
if (!record) throw notFoundError("Payment"); if (!record) throw notFoundError("Payment");
if (record.paymentStatus === "PaymentSuccess") { if (record.paymentStatus === "PaymentSuccess") return record;
const { channel, account, reference } = body;
return await prisma.payment.update({
where: { id: paymentId, invoice: { quotationId: record.invoice.quotationId } },
data: {
channel,
account,
reference,
updatedByUserId: req.user.sub,
},
});
}
return await prisma.$transaction(async (tx) => { return await prisma.$transaction(async (tx) => {
const current = new Date(); const current = new Date();
@ -209,7 +181,6 @@ export class QuotationPayment extends Controller {
await tx.quotation await tx.quotation
.update({ .update({
include: { requestData: true },
where: { id: quotation.id }, where: { id: quotation.id },
data: { data: {
quotationStatus: quotationStatus:
@ -267,17 +238,6 @@ export class QuotationPayment extends Controller {
receiverId: res.createdByUserId, receiverId: res.createdByUserId,
}, },
}); });
if (quotation.quotationStatus === "PaymentInProcess") {
await prisma.notification.create({
data: {
title: "รายการคำขอใหม่ / New Request",
detail: "รหัส / code : " + res.requestData.map((v) => v.code).join(", "),
registeredBranchId: res.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}
}); });
return payment; return payment;

View file

@ -55,7 +55,6 @@ type QuotationCreate = {
dateOfBirth: Date; dateOfBirth: Date;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string; namePrefix?: string;
firstName: string; firstName: string;
firstNameEN: string; firstNameEN: string;
@ -84,8 +83,6 @@ type QuotationCreate = {
installmentNo?: number; installmentNo?: number;
workerIndex?: number[]; workerIndex?: number[];
}[]; }[];
sellerId?: string;
}; };
type QuotationUpdate = { type QuotationUpdate = {
@ -115,7 +112,6 @@ type QuotationUpdate = {
dateOfBirth: Date; dateOfBirth: Date;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string; namePrefix?: string;
firstName?: string; firstName?: string;
@ -144,8 +140,6 @@ type QuotationUpdate = {
installmentNo?: number; installmentNo?: number;
workerIndex?: number[]; workerIndex?: number[];
}[]; }[];
sellerId?: string;
}; };
const VAT_DEFAULT = config.vat; const VAT_DEFAULT = config.vat;
@ -154,16 +148,15 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager", "sale",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCheckCompany = createPermCheck((_) => true); const permissionCheckCompany = createPermCheck((_) => true);
@ -215,7 +208,6 @@ export class QuotationController extends Controller {
@Query() query = "", @Query() query = "",
@Query() startDate?: Date, @Query() startDate?: Date,
@Query() endDate?: Date, @Query() endDate?: Date,
@Query() sellerId?: string,
) { ) {
const where = { const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [ OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
@ -225,6 +217,7 @@ export class QuotationController extends Controller {
customerBranch: { customerBranch: {
OR: [ OR: [
{ code: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } },
@ -263,7 +256,6 @@ export class QuotationController extends Controller {
} }
: undefined, : undefined,
...whereDateQuery(startDate, endDate), ...whereDateQuery(startDate, endDate),
sellerId: sellerId,
} satisfies Prisma.QuotationWhereInput; } satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
@ -421,7 +413,7 @@ export class QuotationController extends Controller {
} }
@Post() @Post()
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"])) @Security("keycloak", MANAGE_ROLES)
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) { async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
const ids = { const ids = {
employee: body.worker.filter((v) => typeof v === "string"), employee: body.worker.filter((v) => typeof v === "string"),
@ -527,15 +519,16 @@ export class QuotationController extends Controller {
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = body.agentPrice ? p.agentPrice : p.price; const originalPrice = body.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound( const finalPriceWithVat = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
); );
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const price = finalPriceWithVat;
const pricePerUnit = price / (1 + VAT_DEFAULT);
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat) const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) / ? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
: 0; : 0;
return { return {
order: i + 1, order: i + 1,
productId: v.productId, productId: v.productId,
@ -556,13 +549,13 @@ export class QuotationController extends Controller {
const price = list.reduce( const price = list.reduce(
(a, c) => { (a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0; a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount); a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat); a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded; a.vatExcluded =
c.vat === 0
? precisionRound(a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)))
: a.vatExcluded;
a.finalPrice = precisionRound( a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0), Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
); );
@ -663,14 +656,7 @@ export class QuotationController extends Controller {
title: "ใบเสนอราคาใหม่ / New Quotation", title: "ใบเสนอราคาใหม่ / New Quotation",
detail: "รหัส / code : " + ret.code, detail: "รหัส / code : " + ret.code,
registeredBranchId: ret.registeredBranchId, registeredBranchId: ret.registeredBranchId,
groupReceiver: { groupReceiver: { create: [{ name: "sale" }, { name: "head_of_sale" }] },
create: [
{ name: "sale" },
{ name: "head_of_sale" },
{ name: "accountant" },
{ name: "branch_accountant" },
],
},
}, },
}); });
@ -678,7 +664,7 @@ export class QuotationController extends Controller {
} }
@Put("{quotationId}") @Put("{quotationId}")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"])) @Security("keycloak", MANAGE_ROLES)
async editQuotation( async editQuotation(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() quotationId: string, @Path() quotationId: string,
@ -814,14 +800,14 @@ export class QuotationController extends Controller {
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = record.agentPrice ? p.agentPrice : p.price; const originalPrice = record.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound( const finalPriceWithVat = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
); );
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const price = finalPriceWithVat;
const pricePerUnit = price / (1 + VAT_DEFAULT);
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat) const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) / ? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
: 0; : 0;
return { return {
@ -844,13 +830,15 @@ export class QuotationController extends Controller {
const price = list?.reduce( const price = list?.reduce(
(a, c) => { (a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0; a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount); a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat); a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded; a.vatExcluded =
c.vat === 0
? precisionRound(
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
)
: a.vatExcluded;
a.finalPrice = precisionRound( a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0), Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
); );
@ -866,7 +854,6 @@ export class QuotationController extends Controller {
finalPrice: 0, finalPrice: 0,
}, },
); );
const changed = list?.some((lhs) => { const changed = list?.some((lhs) => {
const found = record.productServiceList.find((rhs) => { const found = record.productServiceList.find((rhs) => {
return ( return (
@ -900,20 +887,6 @@ export class QuotationController extends Controller {
}), }),
]); ]);
if (customerBranch) {
await tx.customerBranch.update({
where: { id: customerBranch.id },
data: {
customer: {
update: {
status: Status.ACTIVE,
},
},
status: Status.ACTIVE,
},
});
}
return await tx.quotation.update({ return await tx.quotation.update({
include: { include: {
productServiceList: { productServiceList: {
@ -1035,7 +1008,6 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date; dateOfBirth: Date;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string; namePrefix?: string;
firstName: string; firstName: string;
firstNameEN: string; firstNameEN: string;
@ -1058,7 +1030,6 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date; dateOfBirth: Date;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string; namePrefix?: string;
firstName: string; firstName: string;
firstNameEN: string; firstNameEN: string;

View file

@ -32,7 +32,6 @@ import { notFoundError } from "../utils/error";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
import { getGroupUser } from "../services/keycloak";
// User in company can edit. // User in company can edit.
const permissionCheck = createPermCheck((_) => true); const permissionCheck = createPermCheck((_) => true);
@ -95,6 +94,7 @@ export class RequestDataController extends Controller {
customerBranch: { customerBranch: {
OR: [ OR: [
{ code: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } }, { registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } }, { registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
@ -104,8 +104,6 @@ export class RequestDataController extends Controller {
], ],
}, },
}, },
},
{
employee: { employee: {
OR: [ OR: [
{ {
@ -136,24 +134,9 @@ export class RequestDataController extends Controller {
workflow: { workflow: {
step: { step: {
some: { some: {
OR: [ responsiblePerson: {
{ some: { userId: req.user.sub },
responsiblePerson: { },
some: { userId: req.user.sub },
},
},
{
responsibleGroup: {
some: {
group: {
in: await getGroupUser(req.user.sub).then((r) =>
r.map(({ name }: { name: string }) => name),
),
},
},
},
},
],
}, },
}, },
}, },
@ -189,7 +172,6 @@ export class RequestDataController extends Controller {
include: { user: true }, include: { user: true },
}, },
responsibleInstitution: true, responsibleInstitution: true,
responsibleGroup: true,
}, },
}, },
}, },
@ -293,62 +275,34 @@ export class RequestDataController extends Controller {
async updateRequestData( async updateRequestData(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Body() @Body()
body: { boby: {
defaultMessengerId: string; defaultMessengerId: string;
requestDataId: string[]; requestDataId: string[];
}, },
) { ) {
if (body.requestDataId.length === 0) return; const record = await prisma.requestData.updateManyAndReturn({
where: {
return await prisma.$transaction(async (tx) => { id: { in: boby.requestDataId },
const record = await tx.requestData.updateManyAndReturn({ quotation: {
where: { registeredBranch: {
id: { in: body.requestDataId }, OR: permissionCond(req.user),
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
}, },
}, },
data: { },
defaultMessengerId: body.defaultMessengerId, data: {
}, defaultMessengerId: boby.defaultMessengerId,
}); },
if (record.length <= 0) throw notFoundError("Request Data");
await tx.requestWorkStepStatus.updateMany({
where: {
workStatus: {
in: [
RequestWorkStatus.Pending,
RequestWorkStatus.Waiting,
RequestWorkStatus.InProgress,
],
},
requestWork: {
requestDataId: { in: body.requestDataId },
},
},
data: { responsibleUserId: body.defaultMessengerId },
});
return record[0];
}); });
if (record.length <= 0) throw notFoundError("Request Data");
return record[0];
} }
} }
@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 {
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;
}
@Post("reject-request-cancel") @Post("reject-request-cancel")
@Security("keycloak") @Security("keycloak")
async rejectRequestCancel( async rejectRequestCancel(
@ -423,17 +377,6 @@ export class RequestDataActionController extends Controller {
}, },
}, },
}, },
include: {
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
},
},
}); });
if (!result) throw notFoundError("Request Data"); if (!result) throw notFoundError("Request Data");
@ -476,88 +419,23 @@ export class RequestDataActionController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
}) })
.then(async (res) => { .then(async (res) => {
await Promise.all( await tx.notification.createMany({
res.map((v) => data: res.map((v) => ({
tx.notification.create({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
data: { detail: "รหัส / code : " + v.code + " Canceled",
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", receiverId: v.createdByUserId,
detail: "รหัส / code : " + v.code + " Canceled", })),
receiverId: v.createdByUserId, });
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
}), }),
tx.taskOrder tx.taskOrder.updateMany({
.updateManyAndReturn({ where: {
where: { taskList: {
taskList: { every: { taskStatus: TaskStatus.Canceled },
every: { taskStatus: TaskStatus.Canceled },
},
}, },
data: { taskOrderStatus: TaskStatus.Canceled },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
}),
]);
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 dataUserId: string[] = [];
result.quotation.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
finalTextWork = `เลขที่ใบเสนอราคา: ${result.code} ${result.quotation.workName}`;
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
const data = {
to: dataUserId,
messages: [
{
type: "text",
text: textData,
}, },
], data: { taskOrderStatus: TaskStatus.Canceled },
}; }),
]);
await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}); });
} }
@ -689,19 +567,13 @@ export class RequestDataActionController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
}) })
.then(async (res) => { .then(async (res) => {
await Promise.all( await tx.notification.createMany({
res.map((v) => data: res.map((v) => ({
tx.notification.create({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
data: { detail: "รหัส / code : " + v.code + " Canceled",
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", receiverId: v.createdByUserId,
detail: "รหัส / code : " + v.code + " Canceled", })),
receiverId: v.createdByUserId, });
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
}), }),
tx.taskOrder.updateMany({ tx.taskOrder.updateMany({
where: { where: {
@ -784,83 +656,14 @@ export class RequestDataActionController extends Controller {
}, },
}, },
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
},
},
}) })
.then(async (res) => { .then(async (res) => {
await Promise.all( await tx.notification.createMany({
res.map((v) => data: res.map((v) => ({
tx.notification.create({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
data: { detail: "รหัส / code : " + v.code + " Completed",
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", receiverId: v.createdByUserId,
detail: "รหัส / code : " + v.code + " Completed", })),
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
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),
}); });
}); });
// dataRecord.push(record); // dataRecord.push(record);
@ -984,7 +787,6 @@ export class RequestListController extends Controller {
include: { user: true }, include: { user: true },
}, },
responsibleInstitution: true, responsibleInstitution: true,
responsibleGroup: true,
}, },
}, },
}, },
@ -1045,7 +847,6 @@ export class RequestListController extends Controller {
include: { user: true }, include: { user: true },
}, },
responsibleInstitution: true, responsibleInstitution: true,
responsibleGroup: true,
}, },
}, },
}, },
@ -1155,7 +956,7 @@ export class RequestListController extends Controller {
}); });
if (record.responsibleUserId === null) { if (record.responsibleUserId === null) {
await tx.requestWorkStepStatus.update({ await prisma.requestWorkStepStatus.update({
where: { where: {
step_requestWorkId: { step_requestWorkId: {
step: step, step: step,
@ -1217,19 +1018,13 @@ export class RequestListController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
}) })
.then(async (res) => { .then(async (res) => {
await Promise.all( await tx.notification.createMany({
res.map((v) => data: res.map((v) => ({
tx.notification.create({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
data: { detail: "รหัส / code : " + v.code + " Canceled",
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", receiverId: v.createdByUserId,
detail: "รหัส / code : " + v.code + " Canceled", })),
receiverId: v.createdByUserId, });
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
}), }),
tx.taskOrder.updateMany({ tx.taskOrder.updateMany({
where: { where: {
@ -1337,19 +1132,13 @@ export class RequestListController extends Controller {
}, },
}) })
.then(async (res) => { .then(async (res) => {
await Promise.all( await tx.notification.createMany({
res.map((v) => data: res.map((v) => ({
tx.notification.create({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
data: { detail: "รหัส / code : " + v.code + " Completed",
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", receiverId: v.createdByUserId,
detail: "รหัส / code : " + v.code + " Completed", })),
receiverId: v.createdByUserId, });
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
const token = await this.#getLineToken(); const token = await this.#getLineToken();
if (!token) return; if (!token) return;

View file

@ -44,21 +44,11 @@ import {
} from "../utils/minio"; } from "../utils/minio";
import { queryOrNot, whereDateQuery } from "../utils/relation"; import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = [ const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"];
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"data_entry",
];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const allowList = ["system", "head_of_admin"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const permissionCondCompany = createPermCondition((_) => true); const permissionCondCompany = createPermCondition((_) => true);
@ -70,14 +60,11 @@ const permissionCheckCompany = createPermCheck((_) => true);
@Tags("Task Order") @Tags("Task Order")
export class TaskController extends Controller { export class TaskController extends Controller {
@Get("stats") @Get("stats")
@Security("keycloak") async getTaskOrderStats() {
async getTaskOrderStats(@Request() req: RequestWithUser) {
const task = await prisma.taskOrder.groupBy({ const task = await prisma.taskOrder.groupBy({
where: { registeredBranch: { OR: permissionCondCompany(req.user) } },
by: ["taskOrderStatus"], by: ["taskOrderStatus"],
_count: true, _count: true,
}); });
return task.reduce<Record<TaskOrderStatus, number>>( return task.reduce<Record<TaskOrderStatus, number>>(
(a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }), (a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }),
{ {
@ -213,7 +200,6 @@ export class TaskController extends Controller {
step: { step: {
include: { include: {
value: true, value: true,
responsibleGroup: true,
responsiblePerson: { responsiblePerson: {
include: { user: true }, include: { user: true },
}, },
@ -265,12 +251,6 @@ export class TaskController extends Controller {
taskProduct?: { productId: string; discount?: number }[]; taskProduct?: { productId: string; discount?: number }[];
}, },
) { ) {
if (body.taskList.length < 1 || !body.registeredBranchId)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Your created invalid task order",
"taskOrderInvalid",
);
return await prisma.$transaction(async (tx) => { return await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({ const last = await tx.runningNo.upsert({
where: { where: {
@ -320,8 +300,8 @@ export class TaskController extends Controller {
if (updated.count !== taskList.length) { if (updated.count !== taskList.length) {
throw new HttpError( throw new HttpError(
HttpStatus.PRECONDITION_FAILED, HttpStatus.PRECONDITION_FAILED,
"all request work to issue task order must be in ready state.", "All request work to issue task order must be in ready state.",
"requestworkmustready", "requestWorkMustReady",
); );
} }
await tx.institution.updateMany({ await tx.institution.updateMany({
@ -344,51 +324,49 @@ export class TaskController extends Controller {
where: { OR: taskList }, where: { OR: taskList },
}); });
return await tx.taskOrder return await tx.taskOrder.create({
.create({ include: {
include: { taskList: {
taskList: { include: {
include: { requestWorkStep: {
requestWorkStep: { include: {
include: { requestWork: {
requestWork: { include: {
include: { request: {
request: { include: {
include: { employee: true,
employee: true, quotation: {
quotation: { include: {
include: { customerBranch: {
customerBranch: { include: {
include: { customer: true,
customer: true,
},
}, },
}, },
}, },
}, },
}, },
productService: { },
include: { productService: {
service: { include: {
include: { service: {
workflow: { include: {
include: { workflow: {
step: { include: {
include: { step: {
value: true, include: {
responsiblePerson: { value: true,
include: { user: true }, responsiblePerson: {
}, include: { user: true },
responsibleInstitution: true,
}, },
responsibleInstitution: true,
}, },
}, },
}, },
}, },
}, },
work: true,
product: true,
}, },
work: true,
product: true,
}, },
}, },
}, },
@ -396,30 +374,20 @@ export class TaskController extends Controller {
}, },
}, },
}, },
institution: true,
createdBy: true,
}, },
data: { institution: true,
...rest, createdBy: true,
code, },
urgent: work.some((v) => v.requestWork.request.quotation.urgent), data: {
registeredBranchId: userAffiliatedBranch.id, ...rest,
createdByUserId: req.user.sub, code,
taskList: { create: taskList }, urgent: work.some((v) => v.requestWork.request.quotation.urgent),
taskProduct: { create: taskProduct }, registeredBranchId: userAffiliatedBranch.id,
}, createdByUserId: req.user.sub,
}) taskList: { create: taskList },
.then(async (v) => { taskProduct: { create: taskProduct },
await prisma.notification.create({ },
data: { });
title: "ใบสั่งงานใหม่ / New Task Order",
detail: "รหัส / code : " + v.code,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
return v;
});
}); });
} }
@ -562,8 +530,6 @@ export class TaskController extends Controller {
title: "มีการส่งงาน / Task Submitted", title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code, detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId, receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
}, },
}); });
} }
@ -639,28 +605,7 @@ export class TaskActionController extends Controller {
return await prisma.$transaction(async (tx) => { return await prisma.$transaction(async (tx) => {
const promises = body.map(async (v) => { const promises = body.map(async (v) => {
const record = await tx.task.findFirst({ const record = await tx.task.findFirst({
include: { include: { requestWorkStep: true },
requestWorkStep: {
include: {
requestWork: {
include: {
request: {
include: {
quotation: true,
employee: true,
},
},
productService: {
include: {
product: true,
},
},
},
},
},
},
taskOrder: true,
},
where: { where: {
step: v.step, step: v.step,
requestWorkId: v.requestWorkId, requestWorkId: v.requestWorkId,
@ -678,25 +623,6 @@ export class TaskActionController extends Controller {
data: { userTaskStatus: UserTaskStatus.Restart }, data: { userTaskStatus: UserTaskStatus.Restart },
}); });
} }
if (v.taskStatus === TaskStatus.Failed) {
const taskCode = record.taskOrder.code;
const taskName = record.taskOrder.taskName;
const productCode = record.requestWorkStep.requestWork.productService.product.code;
const productName = record.requestWorkStep.requestWork.productService.product.name;
const employeeName = `${record.requestWorkStep.requestWork.request.employee.namePrefix}.${record.requestWorkStep.requestWork.request.employee.firstNameEN} ${record.requestWorkStep.requestWork.request.employee.lastNameEN}`;
await tx.notification.create({
data: {
title: "ใบรายการคำขอที่จัดการเกิดปัญหา / Task Failed",
detail: `ใบรายการคำขอรหัส ${taskCode}: ${taskName} รหัสสินค้า ${productCode}: ${productName} ของลูกจ้าง ${employeeName} เกิดข้อผิดพลาด`,
groupReceiver: { create: { name: "document_checker" } },
receiverId: record.requestWorkStep.requestWork.request.quotation.createdByUserId,
registeredBranchId: record.taskOrder.registeredBranchId,
},
});
}
return await tx.task.update({ return await tx.task.update({
where: { id: record.id }, where: { id: record.id },
data: { data: {
@ -758,8 +684,6 @@ export class TaskActionController extends Controller {
title: "มีการส่งงาน / Task Submitted", title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code, detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId, receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
}, },
}), }),
]); ]);
@ -792,34 +716,22 @@ export class TaskActionController extends Controller {
const code = `RI${year}${month}${last.value.toString().padStart(6, "0")}`; const code = `RI${year}${month}${last.value.toString().padStart(6, "0")}`;
await Promise.all([ await Promise.all([
tx.taskOrder tx.taskOrder.update({
.update({ where: { id: taskOrderId },
where: { id: taskOrderId }, data: {
data: { urgent: false,
urgent: false, taskOrderStatus: TaskOrderStatus.Complete,
taskOrderStatus: TaskOrderStatus.Complete, codeProductReceived: code,
codeProductReceived: code, userTask: {
userTask: { updateMany: {
updateMany: { where: { taskOrderId },
where: { taskOrderId }, data: {
data: { userTaskStatus: UserTaskStatus.Submit,
userTaskStatus: UserTaskStatus.Submit,
},
}, },
}, },
}, },
}) },
.then(async (record) => { }),
await tx.notification.create({
data: {
title: "ใบงานเสร็จสิ้น / Task Complete",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}),
tx.requestWorkStepStatus.updateMany({ tx.requestWorkStepStatus.updateMany({
where: { where: {
task: { task: {
@ -923,34 +835,10 @@ export class TaskActionController extends Controller {
if (completeCheck) completed.push(item.id); if (completeCheck) completed.push(item.id);
}); });
await tx.requestData await tx.requestData.updateMany({
.updateManyAndReturn({ where: { id: { in: completed } },
where: { id: { in: completed } }, data: { requestDataStatus: RequestDataStatus.Completed },
include: { });
quotation: {
select: {
registeredBranchId: true,
createdByUserId: true,
},
},
},
data: { requestDataStatus: RequestDataStatus.Completed },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.quotation.createdByUserId,
registeredBranchId: v.quotation.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
});
await tx.quotation await tx.quotation
.updateManyAndReturn({ .updateManyAndReturn({
where: { where: {
@ -990,19 +878,13 @@ export class TaskActionController extends Controller {
}, },
}) })
.then(async (res) => { .then(async (res) => {
await Promise.all( await tx.notification.createMany({
res.map((v) => data: res.map((v) => ({
tx.notification.create({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
data: { detail: "รหัส / code : " + v.code + " Completed",
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", receiverId: v.createdByUserId,
detail: "รหัส / code : " + v.code + " Completed", })),
receiverId: v.createdByUserId, });
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
const token = await this.#getLineToken(); const token = await this.#getLineToken();
@ -1241,23 +1123,19 @@ export class UserTaskController extends Controller {
}, },
}) })
.then(async (v) => { .then(async (v) => {
await tx.notification.create({ await tx.notification.createMany({
data: { data: [
title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed", {
detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress", title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed",
receiverId: v.createdByUserId, detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress",
registeredBranchId: v.registeredBranchId, receiverId: v.createdByUserId,
groupReceiver: { create: { name: "document_checker" } }, },
}, {
}); title: "มีการรับงาน / Task Accepted",
await tx.notification.create({ detail: "รหัสใบสั่งงาน / Order : " + v.code,
data: { receiverId: v.createdByUserId,
title: "มีการรับงาน / Task Accepted", },
detail: "รหัสใบสั่งงาน / Order : " + v.code, ],
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}); });
}), }),
tx.task.updateMany({ tx.task.updateMany({

View file

@ -13,7 +13,6 @@ import {
Security, Security,
Tags, Tags,
} from "tsoa"; } from "tsoa";
import config from "../config.json";
import prisma from "../db"; import prisma from "../db";
@ -43,21 +42,22 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager", "sale",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
const VAT_DEFAULT = config.vat;
// NOTE: permission condition/check in requestWork -> requestData -> quotation -> registeredBranch
const permissionCond = createPermCondition(globalAllow); const permissionCond = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow); const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
type CreditNoteCreate = { type CreditNoteCreate = {
requestWorkId: string[]; requestWorkId: string[];
@ -85,14 +85,6 @@ type CreditNoteUpdate = {
@Route("api/v1/credit-note") @Route("api/v1/credit-note")
@Tags("Credit Note") @Tags("Credit Note")
export class CreditNoteController extends Controller { export class CreditNoteController 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("stats") @Get("stats")
@Security("keycloak") @Security("keycloak")
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) { async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) {
@ -102,7 +94,7 @@ export class CreditNoteController extends Controller {
request: { request: {
quotationId, quotationId,
quotation: { quotation: {
registeredBranch: { OR: permissionCond(req.user) }, registeredBranch: { OR: permissionCondCompany(req.user) },
}, },
}, },
}, },
@ -156,6 +148,7 @@ export class CreditNoteController extends Controller {
@Query() creditNoteStatus?: CreditNoteStatus, @Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date, @Query() startDate?: Date,
@Query() endDate?: Date, @Query() endDate?: Date,
@Body() body?: {},
) { ) {
const where = { const where = {
OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [ OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [
@ -172,6 +165,7 @@ export class CreditNoteController extends Controller {
customerBranch: { customerBranch: {
OR: [ OR: [
{ code: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } },
@ -206,7 +200,7 @@ export class CreditNoteController extends Controller {
request: { request: {
quotationId, quotationId,
quotation: { quotation: {
registeredBranch: { OR: permissionCond(req.user) }, registeredBranch: { OR: permissionCondCompany(req.user) },
}, },
}, },
}, },
@ -217,8 +211,6 @@ export class CreditNoteController extends Controller {
const [result, total] = await prisma.$transaction([ const [result, total] = await prisma.$transaction([
prisma.creditNote.findMany({ prisma.creditNote.findMany({
where, where,
take: pageSize,
skip: (page - 1) * pageSize,
include: { include: {
quotation: { quotation: {
include: { include: {
@ -251,7 +243,7 @@ export class CreditNoteController extends Controller {
some: { some: {
request: { request: {
quotation: { quotation: {
registeredBranch: { OR: permissionCond(req.user) }, registeredBranch: { OR: permissionCondCompany(req.user) },
}, },
}, },
}, },
@ -349,8 +341,9 @@ export class CreditNoteController extends Controller {
).length; ).length;
const price = const price =
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) - c.productService.pricePerUnit -
c.productService.discount; c.productService.discount / c.productService.amount +
c.productService.vat / c.productService.amount;
if (serviceChargeStepCount && successCount) { if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount; return a + price - c.productService.product.serviceCharge * successCount;
@ -376,98 +369,40 @@ export class CreditNoteController extends Controller {
update: { value: { increment: 1 } }, update: { value: { increment: 1 } },
}); });
return await prisma.creditNote return await prisma.creditNote.create({
.create({ include: {
include: { requestWork: {
requestWork: { include: {
include: { request: true,
request: true,
},
},
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
}, },
}, },
data: { quotation: true,
reason: body.reason, },
detail: body.detail, data: {
remark: body.remark, reason: body.reason,
paybackType: body.paybackType, detail: body.detail,
paybackBank: body.paybackBank, remark: body.remark,
paybackAccount: body.paybackAccount, paybackType: body.paybackType,
paybackAccountName: body.paybackAccountName, paybackBank: body.paybackBank,
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`, paybackAccount: body.paybackAccount,
value, paybackAccountName: body.paybackAccountName,
requestWork: { code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
connect: body.requestWorkId.map((v) => ({ value,
id: v, requestWork: {
})), connect: body.requestWorkId.map((v) => ({
}, id: v,
quotationId: body.quotationId, })),
}, },
}) quotationId: body.quotationId,
.then(async (res) => { },
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 dataUserId: string[] = [];
if (res) {
res.quotation.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
finalTextWork = `จำนวนเงิน ${res.value.toFixed(2)} บาท `;
}
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}${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 res;
});
}, },
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
); );
} }
@Put("{creditNoteId}") @Put("{creditNoteId}")
@Security("keycloak") @Security("keycloak", MANAGE_ROLES)
async updateCreditNote( async updateCreditNote(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() creditNoteId: string, @Path() creditNoteId: string,
@ -542,8 +477,9 @@ export class CreditNoteController extends Controller {
).length; ).length;
const price = const price =
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) - c.productService.pricePerUnit -
c.productService.discount; c.productService.discount / c.productService.amount +
c.productService.vat / c.productService.amount;
if (serviceChargeStepCount && successCount) { if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount; return a + price - c.productService.product.serviceCharge * successCount;
@ -640,14 +576,6 @@ export class CreditNoteActionController extends Controller {
return creditNoteData; return creditNoteData;
} }
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;
}
@Post("accept") @Post("accept")
@Security("keycloak", MANAGE_ROLES) @Security("keycloak", MANAGE_ROLES)
async acceptCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) { async acceptCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) {
@ -666,81 +594,23 @@ export class CreditNoteActionController extends Controller {
@Body() body: { paybackStatus: PaybackStatus }, @Body() body: { paybackStatus: PaybackStatus },
) { ) {
await this.#checkPermission(req.user, creditNoteId); await this.#checkPermission(req.user, creditNoteId);
return await prisma.creditNote return await prisma.creditNote.update({
.update({ where: { id: creditNoteId },
where: { id: creditNoteId }, include: {
include: { requestWork: {
requestWork: { include: {
include: { request: true,
request: true,
},
},
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
}, },
}, },
data: { quotation: true,
creditNoteStatus: },
body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined, data: {
paybackStatus: body.paybackStatus, creditNoteStatus:
paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined, body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
}, paybackStatus: body.paybackStatus,
}) paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined,
.then(async (res) => { },
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.quotation.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
finalTextWork = `จำนวนเงิน ${res.value.toFixed(2)} บาท `;
}
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}${textAlert2}\n\n${textAlert3}`;
const data = {
to: dataUserId,
messages: [
{
type: "text",
text: textData,
},
],
};
body.paybackStatus === PaybackStatus.Done
? await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
: undefined;
});
} }
} }

View file

@ -44,20 +44,22 @@ const MANAGE_ROLES = [
"system", "system",
"head_of_admin", "head_of_admin",
"admin", "admin",
"executive", "head_of_accountant",
"accountant", "accountant",
"branch_admin", "head_of_sale",
"branch_manager", "sale",
"branch_accountant",
]; ];
function globalAllow(user: RequestWithUser["user"]) { function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"]; const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return user.roles?.some((v) => listAllowed.includes(v)) || false; return allowList.some((v) => user.roles?.includes(v));
} }
// NOTE: permission condition/check in registeredBranch
const permissionCond = createPermCondition(globalAllow); const permissionCond = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow); const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
type DebitNoteCreate = { type DebitNoteCreate = {
quotationId: string; quotationId: string;
@ -74,7 +76,6 @@ type DebitNoteCreate = {
dateOfBirth: Date; dateOfBirth: Date;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string; namePrefix?: string;
firstName: string; firstName: string;
firstNameEN: string; firstNameEN: string;
@ -110,7 +111,6 @@ type DebitNoteUpdate = {
dateOfBirth: Date; dateOfBirth: Date;
gender: string; gender: string;
nationality: string; nationality: string;
otherNationality?: string | null;
namePrefix?: string; namePrefix?: string;
firstName?: string; firstName?: string;
firstNameEN: string; firstNameEN: string;
@ -211,6 +211,7 @@ export class DebitNoteController extends Controller {
customerBranch: { customerBranch: {
OR: [ OR: [
{ code: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } },
@ -430,18 +431,12 @@ export class DebitNoteController extends Controller {
const list = body.productServiceList.map((v, i) => { const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!; const p = product.find((p) => p.id === v.productId)!;
const price = body.agentPrice ? p.agentPrice : p.price;
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
const originalPrice = body.agentPrice ? p.agentPrice : p.price; ? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
const finalPrice = precisionRound( VAT_DEFAULT *
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), (!v.discount ? v.amount : 1)
);
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
: 0; : 0;
return { return {
@ -464,13 +459,15 @@ export class DebitNoteController extends Controller {
const price = list.reduce( const price = list.reduce(
(a, c) => { (a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0; a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount); a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat); a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded; a.vatExcluded =
c.vat === 0
? precisionRound(
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
)
: a.vatExcluded;
a.finalPrice = precisionRound( a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0), Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
); );
@ -582,7 +579,7 @@ export class DebitNoteController extends Controller {
} }
@Put("{debitNoteId}") @Put("{debitNoteId}")
@Security("keycloak") @Security("keycloak", MANAGE_ROLES)
async updateDebitNote( async updateDebitNote(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Path() debitNoteId: string, @Path() debitNoteId: string,
@ -606,7 +603,7 @@ export class DebitNoteController extends Controller {
if (!record) throw notFoundError("Debit Note"); if (!record) throw notFoundError("Debit Note");
await permissionCheck(req.user, record.registeredBranch); await permissionCheckCompany(req.user, record.registeredBranch);
const { productServiceList: _productServiceList, ...rest } = body; const { productServiceList: _productServiceList, ...rest } = body;
const ids = { const ids = {
@ -677,18 +674,12 @@ export class DebitNoteController extends Controller {
} }
const list = body.productServiceList.map((v, i) => { const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!; const p = product.find((p) => p.id === v.productId)!;
const price = body.agentPrice ? p.agentPrice : p.price;
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
const originalPrice = record.agentPrice ? p.agentPrice : p.price; ? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
const finalPrice = precisionRound( VAT_DEFAULT *
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), (!v.discount ? v.amount : 1)
);
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
: 0; : 0;
return { return {
@ -711,13 +702,15 @@ export class DebitNoteController extends Controller {
const price = list.reduce( const price = list.reduce(
(a, c) => { (a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0; a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount); a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat); a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded; a.vatExcluded =
c.vat === 0
? precisionRound(
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
)
: a.vatExcluded;
a.finalPrice = precisionRound( a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0), Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
); );

View file

@ -189,6 +189,7 @@ export class LineController extends Controller {
customerBranch: { customerBranch: {
OR: [ OR: [
{ code: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } }, { registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } }, { registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
@ -613,22 +614,39 @@ export class LineController extends Controller {
@Query() endDate?: Date, @Query() endDate?: Date,
) { ) {
const where = { const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [ OR:
{ code: { contains: query, mode: "insensitive" } }, query || pendingOnly
{ workName: { contains: query, mode: "insensitive" } }, ? [
{ ...(queryOrNot<Prisma.QuotationWhereInput[]>(query, [
customerBranch: { { code: { contains: query, mode: "insensitive" } },
OR: [ { workName: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } }, {
{ registerName: { contains: query, mode: "insensitive" } }, customerBranch: {
{ firstName: { contains: query, mode: "insensitive" } }, OR: [
{ firstNameEN: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } }, { customerName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } },
], { firstNameEN: { contains: query, mode: "insensitive" } },
}, { lastName: { contains: query, mode: "insensitive" } },
}, { lastNameEN: { contains: query, mode: "insensitive" } },
]), ],
},
},
]) || []),
...(queryOrNot<Prisma.QuotationWhereInput[]>(!!pendingOnly, [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
]) || []),
]
: undefined,
isDebitNote: false, isDebitNote: false,
code, code,
payCondition, payCondition,
@ -650,22 +668,6 @@ export class LineController extends Controller {
}, },
} }
: undefined, : undefined,
AND: pendingOnly
? {
OR: [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
],
}
: undefined,
...whereDateQuery(startDate, endDate), ...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput; } satisfies Prisma.QuotationWhereInput;

View file

@ -90,7 +90,7 @@ export class WebHookController extends Controller {
firstNameEN: true, firstNameEN: true,
lastName: true, lastName: true,
lastNameEN: true, lastNameEN: true,
registerName: true, customerName: true,
customer: { customer: {
select: { select: {
customerType: true, customerType: true,
@ -133,13 +133,13 @@ export class WebHookController extends Controller {
let textData = ""; let textData = "";
if (dataEmployee.length > 0) { if (dataEmployee.length > 0) {
const registerName = const customerName =
dataEmployee[0]?.employee?.customerBranch?.registerName ?? "ไม่ระบุ"; dataEmployee[0]?.employee?.customerBranch?.customerName ?? "ไม่ระบุ";
const telephoneNo = const telephoneNo =
dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ?? dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ??
"ไม่ระบุ"; "ไม่ระบุ";
const textEmployer = `เรียน คุณ${registerName}`; const textEmployer = `เรียน คุณ${customerName}`;
const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง"; const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง";
const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้"; const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้";
const textExpDate = const textExpDate =

View file

@ -1,113 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import { Prisma } from "@prisma/client";
import { queryOrNot } from "../utils/relation";
import { notFoundError } from "../utils/error";
type BusinessTypePayload = {
name: string;
nameEN: string;
};
@Route("api/v1/business-type")
@Tags("Business Type")
export class businessTypeController extends Controller {
@Get()
@Security("keycloak")
async getList(
@Request() req: RequestWithUser,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: queryOrNot<Prisma.BusinessTypeWhereInput[]>(query, [
{ name: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
]),
} satisfies Prisma.BusinessTypeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.businessType.findMany({
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.businessType.count({ where }),
]);
return { result, page, pageSize, total };
}
@Post()
@Security("keycloak")
async createBusinessType(@Request() req: RequestWithUser, @Body() body: BusinessTypePayload) {
return await prisma.businessType.create({
data: {
...body,
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
},
});
}
@Get(":businessTypeId")
@Security("keycloak")
async getBusinessTypeById(@Path() businessTypeId: string) {
return await prisma.businessType.findUnique({
where: { id: businessTypeId },
});
}
@Put(":businessTypeId")
@Security("keycloak")
async updateBusinessType(
@Request() req: RequestWithUser,
@Path() businessTypeId: string,
@Body() body: BusinessTypePayload,
) {
return await prisma.$transaction(async (tx) => {
const record = await tx.businessType.findUnique({
where: { id: businessTypeId },
});
if (!record) throw notFoundError("BusinessType");
return await tx.businessType.update({
where: { id: businessTypeId },
data: {
...body,
updatedByUserId: req.user.sub,
},
});
});
}
@Delete(":businessTypeId")
@Security("keycloak")
async deleteBusinessType(@Path() businessTypeId: string) {
return await prisma.$transaction(async (tx) => {
const record = await tx.businessType.findUnique({
where: { id: businessTypeId },
});
if (!record) throw notFoundError("BusinessType");
return await tx.businessType.delete({
where: { id: businessTypeId },
});
});
}
}

View file

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

View file

@ -1,10 +1,6 @@
import prisma from "../db"; import prisma from "../db";
import config from "../config.json"; import config from "../config.json";
import { CustomerType, PayCondition } from "@prisma/client"; import { CustomerType, PayCondition } from "@prisma/client";
import { convertTemplate } from "../utils/string-template";
import { htmlToText } from "html-to-text";
import { JsonObject } from "@prisma/client/runtime/library";
import { precisionRound } from "../utils/arithmetic";
if (!process.env.FLOW_ACCOUNT_URL) throw new Error("Require FLOW_ACCOUNT_URL"); if (!process.env.FLOW_ACCOUNT_URL) throw new Error("Require FLOW_ACCOUNT_URL");
if (!process.env.FLOW_ACCOUNT_CLIENT_ID) throw new Error("Require FLOW_ACCOUNT_CLIENT_ID"); if (!process.env.FLOW_ACCOUNT_CLIENT_ID) throw new Error("Require FLOW_ACCOUNT_CLIENT_ID");
@ -236,29 +232,6 @@ const flowAccount = {
installments: true, installments: true,
quotation: { quotation: {
include: { include: {
paySplit: true,
worker: {
select: {
employee: {
select: {
employeePassport: {
select: {
number: true,
},
orderBy: {
expireDate: "desc",
},
take: 1,
},
namePrefix: true,
firstName: true,
lastName: true,
firstNameEN: true,
lastNameEN: true,
},
},
},
},
registeredBranch: { registeredBranch: {
include: { include: {
province: true, province: true,
@ -289,58 +262,19 @@ const flowAccount = {
const quotation = data.quotation; const quotation = data.quotation;
const customer = quotation.customerBranch; const customer = quotation.customerBranch;
const product =
const summary = {
subTotal: 0,
discountAmount: 0,
vatableAmount: 0,
exemptAmount: 0,
vatAmount: 0,
grandTotal: 0,
};
const products = (
quotation.payCondition === PayCondition.BillFull || quotation.payCondition === PayCondition.BillFull ||
quotation.payCondition === PayCondition.Full quotation.payCondition === PayCondition.Full
? quotation.productServiceList ? quotation.productServiceList
: quotation.productServiceList.filter((lhs) => : quotation.productServiceList.filter((lhs) =>
data.installments.some((rhs) => rhs.no === lhs.installmentNo), data.installments.some((rhs) => rhs.no === lhs.installmentNo),
) );
).map((v) => {
// TODO: Use product's VAT field (not implemented) instead.
const VAT_RATE = VAT_DEFAULT;
summary.subTotal +=
precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_RATE : 0))) * v.amount;
summary.discountAmount += v.discount;
const total =
precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_RATE : 0))) * v.amount -
(v.discount ?? 0);
if (v.vat > 0) {
summary.vatableAmount += precisionRound(total / (1 + VAT_RATE));
summary.vatAmount += v.vat;
} else {
summary.exemptAmount += total;
}
summary.grandTotal += total;
return {
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
pricePerUnit: precisionRound(v.pricePerUnit),
quantity: v.amount,
discountAmount: v.discount,
vatRate: v.vat === 0 ? 0 : Math.round(VAT_RATE * 100),
total,
};
});
const payload = { const payload = {
contactCode: customer.code, contactCode: customer.code,
contactName: customer.contactName || "-", contactName:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
: customer.registerName) || "-",
contactAddress: [ contactAddress: [
customer.address, customer.address,
!!customer.moo ? "หมู่ " + customer.moo : null, !!customer.moo ? "หมู่ " + customer.moo : null,
@ -349,10 +283,11 @@ const flowAccount = {
(customer.province?.id === "10" ? "แขวง" : "อำเภอ") + customer.subDistrict?.name, (customer.province?.id === "10" ? "แขวง" : "อำเภอ") + customer.subDistrict?.name,
(customer.province?.id === "10" ? "เขต" : "ตำบล") + customer.district?.name, (customer.province?.id === "10" ? "เขต" : "ตำบล") + customer.district?.name,
"จังหวัด" + customer.province?.name, "จังหวัด" + customer.province?.name,
customer.subDistrict?.zipCode,
] ]
.filter(Boolean) .filter(Boolean)
.join(" "), .join(" "),
contactTaxId: customer.citizenId || customer.legalPersonNo || "-", contactTaxId: customer.citizenId || customer.code,
contactBranch: contactBranch:
(customer.customer.customerType === CustomerType.PERS (customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim() ? [customer.firstName, customer.lastName].join(" ").trim()
@ -370,35 +305,36 @@ const flowAccount = {
isVat: true, isVat: true,
useReceiptDeduction: false, useReceiptDeduction: false,
useInlineVat: true,
discounPercentage: 0, discounPercentage: 0,
discountAmount: quotation.totalDiscount, discountAmount: quotation.totalDiscount,
subTotal: summary.subTotal, subTotal:
totalAfterDiscount: summary.subTotal - summary.discountAmount, quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
vatableAmount: summary.vatableAmount, ? 0
exemptAmount: summary.exemptAmount, : quotation.totalPrice,
vatAmount: summary.vatAmount, totalAfterDiscount:
grandTotal: summary.grandTotal, quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.finalPrice,
vatAmount:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.vat,
grandTotal:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? data.installments.reduce((a, c) => a + c.amount, 0)
: quotation.finalPrice,
remarks: htmlToText( items: product.map((v) => ({
convertTemplate(quotation.remark ?? "", { type: ProductAndServiceType.ProductNonInv,
"quotation-payment": { name: v.product.name,
paymentType: quotation?.payCondition || "Full", pricePerUnit: v.pricePerUnit,
amount: quotation.finalPrice, quantity: v.amount,
installments: quotation?.paySplit, discountAmount: v.discount,
}, total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
"quotation-labor": { vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
name: quotation.worker.map( })),
(v, i) =>
`${i + 1}. ` +
`${v.employee.employeePassport.length !== 0 ? v.employee.employeePassport[0].number + "_" : ""}${v.employee.namePrefix}. ${v.employee.firstNameEN ? `${v.employee.firstNameEN} ${v.employee.lastNameEN}` : `${v.employee.firstName} ${v.employee.lastName}`} `.toUpperCase(),
),
},
}),
),
items: products,
}; };
return await flowAccountAPI.createReceipt(payload, false); return await flowAccountAPI.createReceipt(payload, false);
@ -411,219 +347,6 @@ const flowAccount = {
} }
return null; return null;
}, },
// flowAccount GET Product list
async getProducts() {
const { token } = await flowAccountAPI.auth();
const res = await fetch(api + "/products", {
method: "GET",
headers: {
["Content-Type"]: `application/json`,
["Authorization"]: `Bearer ${token}`,
},
});
return {
ok: res.ok,
status: res.status,
body: await res.json(),
};
},
// flowAccount GET Product by id
async getProductsById(recordId: string) {
const { token } = await flowAccountAPI.auth();
const res = await fetch(api + `/products/${recordId}`, {
method: "GET",
headers: {
["Content-Type"]: `application/json`,
["Authorization"]: `Bearer ${token}`,
},
});
const data = await res.json();
return {
ok: res.ok,
status: res.status,
list: data.data.list,
total: data.data.total,
};
},
// flowAccount POST create Product
async createProducts(code: string, body: JsonObject) {
const { token } = await flowAccountAPI.auth();
const commonBody = {
productStructureType: null,
type: 3,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
buyPrice: body.serviceCharge,
buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
buyDescription: body.detail,
};
const createProduct = async (name: string, price: any, vatIncluded: boolean) => {
try {
const res = await fetch(`${api}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...commonBody,
name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: Failed to create product`);
const json = await res.json().catch(() => {
throw new Error("Invalid JSON response from FlowAccount API");
});
return json?.data?.list?.[0]?.id ?? null;
} catch (err) {
console.error("createProduct error:", err);
return null;
}
};
const deleteProduct = async (id: string) => {
try {
await fetch(`${api}/products/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
} catch (err) {
console.error("Rollback delete failed:", err);
}
};
const [sellResult, agentResult] = await Promise.allSettled([
createProduct(`${code} ${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
createProduct(
`${code} ${body.name} (ราคาตัวแทน)`,
body.agentPrice,
/true/.test(`${body.agentPriceVatIncluded}`),
),
]);
const sellId = sellResult.status === "fulfilled" ? sellResult.value : null;
const agentId = agentResult.status === "fulfilled" ? agentResult.value : null;
// --- validation ---
if (!sellId && !agentId) {
throw new Error("FlowAccountProductError.BOTH_CREATION_FAILED");
}
if (!sellId && agentId) {
await deleteProduct(agentId);
throw new Error("FlowAccountProductError.SELL_PRICE_CREATION_FAILED");
}
if (sellId && !agentId) {
await deleteProduct(sellId);
throw new Error("FlowAccountProductError.AGENT_PRICE_CREATION_FAILED");
}
return {
ok: true,
status: 200,
data: {
productIdSellPrice: sellId,
productIdAgentPrice: agentId,
},
};
},
// flowAccount PUT edit Product
async editProducts(sellPriceId: String, agentPriceId: String, body: JsonObject) {
const { token } = await flowAccountAPI.auth();
const commonBody = {
productStructureType: null,
type: 3,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
buyPrice: body.serviceCharge,
buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
buyDescription: body.detail,
};
const editProduct = async (id: String, name: String, price: any, vatIncluded: boolean) => {
try {
const res = await fetch(api + `/products/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...commonBody,
name: name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
if (!res.ok) {
throw new Error(`Request failed with status ${res.status} ${res}`);
}
let json: any = null;
try {
json = await res.json();
} catch {
throw new Error("Response is not valid JSON");
}
return json?.data?.list?.[0]?.id ?? null;
} catch (err) {
console.error("createProduct error:", err);
return null;
}
};
await Promise.all([
editProduct(
sellPriceId,
`${body.code} ${body.name}`,
body.price,
/true/.test(`${body.vatIncluded}`),
),
editProduct(
agentPriceId,
`${body.code} ${body.name} (ราคาตัวแทน)`,
body.agentPrice,
/true/.test(`${body.agentPriceVatIncluded}`),
),
]);
},
// flowAccount DELETE Product
async deleteProduct(recordId: string) {
const { token } = await flowAccountAPI.auth();
const res = await fetch(api + `/products/${recordId}`, {
method: "DELETE",
headers: {
["Authorization"]: `Bearer ${token}`,
},
});
return {
ok: res.ok,
status: res.status,
};
},
}; };
export default flowAccount; export default flowAccount;

View file

@ -346,8 +346,8 @@ export async function removeUserRoles(userId: string, roles: { id: string; name:
return true; return true;
} }
export async function getGroup(query: string) { export async function getGroup() {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/groups?${query}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/groups?q`, {
headers: { headers: {
authorization: `Bearer ${await getToken()}`, authorization: `Bearer ${await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
@ -358,9 +358,9 @@ export async function getGroup(query: string) {
const dataMainGroup = await res.json(); const dataMainGroup = await res.json();
const fetchSubGroups = async (group: any) => { const fetchSubGroups = async (group: any) => {
let fullSubGroup = await Promise.all( let fullSubGroup = await Promise.all(
group.subGroups.map((subGroupsData: any) => { group.subGroups.map(async (subGroupsData: any) => {
if (group.subGroupCount > 0) { if (group.subGroupCount > 0) {
return fetchSubGroups(subGroupsData); return await fetchSubGroups(subGroupsData);
} else { } else {
return { return {
id: subGroupsData.id, id: subGroupsData.id,

View file

@ -2,7 +2,6 @@ import dayjs from "dayjs";
import { CronJob } from "cron"; import { CronJob } from "cron";
import prisma from "../db"; import prisma from "../db";
import { Prisma } from "@prisma/client";
const jobs = [ const jobs = [
CronJob.from({ CronJob.from({
@ -39,162 +38,6 @@ const jobs = [
.catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e)); .catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e));
}, },
}), }),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
const employeeExpireData = await prisma.employee.findMany({
include: {
employeePassport: {
orderBy: {
expireDate: "desc",
},
take: 1,
},
customerBranch: {
include: {
customer: true,
},
},
quotationWorker: {
include: {
quotation: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
where: {
employeePassport: {
some: {
expireDate: dayjs().add(90, "day").toDate(),
},
},
},
});
await Promise.all(
employeeExpireData.map(async (record) => {
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
const expireDate = `${dayjs(record.employeePassport[0].expireDate).format("DD/MM")}/${dayjs(record.employeePassport[0].expireDate).year() + 543}`;
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} หนังสือเดินทางจะหมดอายุในวันที่ ${expireDate}`;
const duplicateText = await prisma.notification.findFirst({
where: {
detail: textDetail,
},
});
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
title: "หนังสือเดินทางลูกจ้างหมดอายุ / Employee Passport Expire",
detail: textDetail,
};
if (record.quotationWorker && record.quotationWorker.length > 0) {
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
dataNotification.registeredBranchId =
record.quotationWorker[0].quotation.registeredBranchId;
} else {
(dataNotification.groupReceiver = {
create: [{ name: "sale" }, { name: "head_of_sale" }],
}),
(dataNotification.registeredBranchId =
record.customerBranch.customer.registeredBranchId);
}
if (!duplicateText) {
await prisma.notification
.create({
data: dataNotification,
})
.then(() => console.log("[INFO]: Create notification employee passport expired, OK."))
.catch((e) =>
console.error("[ERR]: Create notification employee passport expired, FAILED.", e),
);
}
}),
);
},
}),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
const employeeVisaData = await prisma.employee.findMany({
include: {
employeeVisa: {
orderBy: {
expireDate: "desc",
},
take: 1,
},
customerBranch: {
include: {
customer: true,
},
},
quotationWorker: {
include: {
quotation: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
where: {
employeeVisa: {
some: {
expireDate: dayjs().add(90, "day").toDate(),
},
},
},
});
await Promise.all(
employeeVisaData.map(async (record) => {
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
const expireDate = `${dayjs(record.employeeVisa[0].expireDate).format("DD/MM")}/${dayjs(record.employeeVisa[0].expireDate).year() + 543}`;
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} ข้อมูลการตรวจลงตราจะหมดอายุในวันที่ ${expireDate}`;
const duplicateText = await prisma.notification.findFirst({
where: {
detail: textDetail,
},
});
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
title: "ข้อมูลการตรวจลงตราลูกจ้างหมดอายุ / Employee Visa Expire",
detail: textDetail,
};
if (record.quotationWorker && record.quotationWorker.length > 0) {
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
dataNotification.registeredBranchId =
record.quotationWorker[0].quotation.registeredBranchId;
} else {
(dataNotification.groupReceiver = {
create: [{ name: "sale" }, { name: "head_of_sale" }],
}),
(dataNotification.registeredBranchId =
record.customerBranch.customer.registeredBranchId);
}
if (!duplicateText) {
await prisma.notification
.create({
data: dataNotification,
})
.then(() => console.log("[INFO]: Create notification employee visa expired, OK."))
.catch((e) =>
console.error("[ERR]: Create notification employee visa expired, FAILED.", e),
);
}
}),
);
},
}),
]; ];
export function initSchedule() { export function initSchedule() {

View file

@ -1,67 +0,0 @@
export function formatNumberDecimal(num: number, point: number = 2): string {
return (num || 0).toLocaleString("eng", {
minimumFractionDigits: point,
maximumFractionDigits: point,
});
}
const templates = {
"quotation-labor": {
converter: (context?: { name: string[] }) => {
return context?.name.join("<br />") || "";
},
},
"quotation-payment": {
converter: (context?: {
paymentType: "Full" | "Split" | "SplitCustom" | "BillFull" | "BillSplit" | "BillSplitCustom";
amount?: number;
installments?: {
no: number;
amount: number;
}[];
}) => {
if (context?.paymentType === "Full") {
return [
"**** เงื่อนไขเพิ่มเติม",
"- เงื่อนไขการชำระเงิน แบบเต็มจำนวน",
`&nbsp; จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
].join("<br/>");
} else {
return [
"**** เงื่อนไขเพิ่มเติม",
`- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === "SplitCustom" ? " กำหนดเอง " : " "}${context?.installments?.length} งวด`,
...(context?.installments?.map(
(v) => `&nbsp; งวดที่ ${v.no} จำนวน ${formatNumberDecimal(v.amount, 2)}`,
) || []),
].join("<br />");
}
},
},
} as const;
type Template = typeof templates;
type TemplateName = keyof Template;
type TemplateContext = {
[key in TemplateName]?: Parameters<Template[key]["converter"]>[0];
};
export function convertTemplate(
text: string,
context?: TemplateContext,
templateUse?: TemplateName[],
) {
let ret = text;
for (const [name, template] of Object.entries(templates)) {
if (templateUse && !templateUse.includes(name as TemplateName)) continue;
ret = ret.replace(
new RegExp("\\#\\[" + name.replaceAll("-", "\\-") + "\\]", "g"),
typeof template.converter === "function"
? template.converter(context?.[name as TemplateName] as any)
: template.converter,
);
}
return ret;
}

View file

@ -62,90 +62,85 @@ export async function initThailandAreaDatabase() {
return result; return result;
} }
await prisma.$transaction( await prisma.$transaction(async (tx) => {
async (tx) => { const meta = {
const meta = { createdBy: null,
createdBy: null, createdAt: new Date(),
createdAt: new Date(), updatedBy: null,
updatedBy: null, updatedAt: new Date(),
updatedAt: new Date(), };
};
await Promise.all( await Promise.all(
splitChunk(province, 1000, async (r) => { splitChunk(province, 1000, async (r) => {
return await tx.$kysely return await tx.$kysely
.insertInto("Province") .insertInto("Province")
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"]) .columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
.values(r.map((v) => ({ ...v, ...meta }))) .values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) => .onConflict((oc) =>
oc.column("id").doUpdateSet({ oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"), name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"), nameEN: (eb) => eb.ref("excluded.nameEN"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"), updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}), }),
) )
.execute(); .execute();
}), }),
); );
await Promise.all( await Promise.all(
splitChunk(district, 2000, async (r) => { splitChunk(district, 2000, async (r) => {
return await tx.$kysely return await tx.$kysely
.insertInto("District") .insertInto("District")
.columns([ .columns([
"id", "id",
"name", "name",
"nameEN", "nameEN",
"provinceId", "provinceId",
"createdBy", "createdBy",
"createdAt", "createdAt",
"updatedBy", "updatedBy",
"updatedAt", "updatedAt",
]) ])
.values(r.map((v) => ({ ...v, ...meta }))) .values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) => .onConflict((oc) =>
oc.column("id").doUpdateSet({ oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"), name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"), nameEN: (eb) => eb.ref("excluded.nameEN"),
provinceId: (eb) => eb.ref("excluded.provinceId"), provinceId: (eb) => eb.ref("excluded.provinceId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"), updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}), }),
) )
.execute(); .execute();
}), }),
); );
await Promise.all( await Promise.all(
splitChunk(subDistrict, 1000, async (r) => { splitChunk(subDistrict, 1000, async (r) => {
return await tx.$kysely return await tx.$kysely
.insertInto("SubDistrict") .insertInto("SubDistrict")
.columns([ .columns([
"id", "id",
"name", "name",
"nameEN", "nameEN",
"districtId", "districtId",
"createdBy", "createdBy",
"createdAt", "createdAt",
"updatedBy", "updatedBy",
"updatedAt", "updatedAt",
]) ])
.values(r.map((v) => ({ ...v, ...meta }))) .values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) => .onConflict((oc) =>
oc.column("id").doUpdateSet({ oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"), name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"), nameEN: (eb) => eb.ref("excluded.nameEN"),
districtId: (eb) => eb.ref("excluded.districtId"), districtId: (eb) => eb.ref("excluded.districtId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"), updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}), }),
) )
.execute(); .execute();
}), }),
); );
}, });
{
timeout: 15_000,
},
);
console.log("[INFO]: Sync thailand province, district and subdistrict, OK."); console.log("[INFO]: Sync thailand province, district and subdistrict, OK.");
} }
@ -175,72 +170,67 @@ export async function initEmploymentOffice() {
const list = await prisma.province.findMany(); const list = await prisma.province.findMany();
await prisma.$transaction( await prisma.$transaction(async (tx) => {
async (tx) => { await Promise.all(
await Promise.all( list
list .map(async (province) => {
.map(async (province) => { if (special[province.id]) {
if (special[province.id]) { await tx.employmentOffice.deleteMany({
await tx.employmentOffice.deleteMany({ where: { provinceId: province.id, district: { none: {} } },
where: { provinceId: province.id, district: { none: {} } },
});
return await Promise.all(
Object.entries(special[province.id]).map(async ([key, val]) => {
const id = province.id + "-" + key.padStart(2, "0");
return tx.employmentOffice.upsert({
where: { id },
create: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
update: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
deleteMany: { districtId: { notIn: val } },
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
});
}),
);
}
return tx.employmentOffice.upsert({
where: { id: province.id },
create: {
id: province.id,
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
update: {
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
}); });
}) return await Promise.all(
.flat(), Object.entries(special[province.id]).map(async ([key, val]) => {
); const id = province.id + "-" + key.padStart(2, "0");
}, return tx.employmentOffice.upsert({
{ where: { id },
timeout: 15_000, create: {
}, id,
); name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
update: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
deleteMany: { districtId: { notIn: val } },
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
});
}),
);
}
return tx.employmentOffice.upsert({
where: { id: province.id },
create: {
id: province.id,
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
update: {
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
});
})
.flat(),
);
});
console.log("[INFO]: Sync employment office, OK."); console.log("[INFO]: Sync employment office, OK.");
} }

View file

@ -1,323 +0,0 @@
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from "vitest";
import { PrismaClient } from "@prisma/client";
import { isDateString } from "./lib";
const prisma = new PrismaClient({
datasourceUrl: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL,
});
const baseUrl = process.env.TEST_BASE_URL || "http://localhost";
const record: Record<string, any> = {
code: "CMT",
taxNo: "1052299402851",
name: "Chamomind",
nameEN: "Chamomind",
email: "contact@chamomind.com",
lineId: "@chamomind",
telephoneNo: "0988929248",
contactName: "John",
webUrl: "https://chamomind.com",
latitude: "",
longitude: "",
virtual: false,
permitNo: "1135182804792",
permitIssueDate: "2025-01-01T00:00:00.000Z",
permitExpireDate: "2030-01-01T00:00:00.000Z",
address: "11/3",
addressEN: "11/3",
soi: "1",
soiEN: "1",
moo: "2",
mooEN: "2",
street: "Straight",
streetEN: "Straight",
provinceId: "50",
districtId: "5001",
subDistrictId: "500107",
};
const recordList: Record<string, any>[] = [];
let token: string;
beforeAll(async () => {
const body = new URLSearchParams();
body.append("grant_type", "password");
body.append("client_id", "app");
body.append("username", process.env.TEST_USERNAME || "");
body.append("password", process.env.TEST_PASSWORD || "");
body.append("scope", "openid");
const res = await fetch(
process.env.KC_URL + "/realms/" + process.env.KC_REALM + "/protocol/openid-connect/token",
{
method: "POST",
body: body,
},
);
expect(res.ok).toBe(true);
await res.json().then((data) => {
token = data["access_token"];
});
});
afterAll(async () => {
if (!record["id"]) return;
await prisma.branch.deleteMany({
where: { id: { in: [record, ...recordList].map((v) => v["id"]) } },
});
await prisma.runningNo.deleteMany({
where: {
key: { in: [record, ...recordList].map((v) => `MAIN_BRANCH_${v["code"].slice(0, -5)}`) },
},
});
});
describe("branch management", () => {
it("create branch without required fields", async () => {
const requiredFields = [
"taxNo",
"name",
"nameEN",
"permitNo",
"telephoneNo",
"address",
"addressEN",
"email",
];
onTestFailed(() => console.log("Field:", requiredFields, "is required."));
for await (const field of requiredFields) {
const res = await fetch(baseUrl + "/api/v1/branch", {
method: "POST",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
body: JSON.stringify({ ...record, [field]: undefined }),
});
if (res.ok) recordList.push(await res.json());
expect(res.ok).toBe(false);
expect(res.status).toBe(400);
}
});
it("create branch", async () => {
const res = await fetch(baseUrl + "/api/v1/branch", {
method: "POST",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
body: JSON.stringify(record),
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
record["id"] = data["id"]; // This field is auto generated
record["code"] = data["code"]; // This field is auto generated
recordList.push(data);
expect(data).toMatchObject(record);
});
it("get branch list", async () => {
const res = await fetch(baseUrl + "/api/v1/branch", {
method: "GET",
headers: {
["Authorization"]: "Bearer " + token,
},
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toHaveProperty("result");
expect(data).toHaveProperty("total");
expect(data).toHaveProperty("page");
expect(data).toHaveProperty("pageSize");
expect(data.result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
code: expect.any(String),
virtual: expect.any(Boolean),
name: expect.any(String),
nameEN: expect.any(String),
email: expect.any(String),
taxNo: expect.any(String),
telephoneNo: expect.any(String),
latitude: expect.any(String),
longitude: expect.any(String),
contactName: expect.toBeOneOf([expect.any(String), null]),
lineId: expect.toBeOneOf([expect.any(String), null]),
webUrl: expect.toBeOneOf([expect.any(String), null]),
remark: expect.toBeOneOf([expect.any(String), null]),
selectedImage: expect.toBeOneOf([expect.any(String), null]),
isHeadOffice: expect.any(Boolean),
permitNo: expect.any(String),
permitIssueDate: expect.toSatisfy(isDateString(true)),
permitExpireDate: expect.toSatisfy(isDateString(true)),
address: expect.any(String),
addressEN: expect.any(String),
moo: expect.toBeOneOf([expect.any(String), null]),
mooEN: expect.toBeOneOf([expect.any(String), null]),
street: expect.toBeOneOf([expect.any(String), null]),
streetEN: expect.toBeOneOf([expect.any(String), null]),
provinceId: expect.any(String),
province: expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
nameEN: expect.any(String),
}),
districtId: expect.any(String),
district: expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
nameEN: expect.any(String),
}),
subDistrictId: expect.any(String),
subDistrict: expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
nameEN: expect.any(String),
zipCode: expect.any(String),
}),
status: expect.toBeOneOf(["CREATED", "ACTIVE", "INACTIVE"]),
statusOrder: expect.toBeOneOf([1, 0]),
createdAt: expect.toSatisfy(isDateString()),
createdByUserId: expect.toBeOneOf([expect.any(String), null]),
createdBy: expect.objectContaining({
id: expect.any(String),
username: expect.any(String),
firstName: expect.any(String),
lastName: expect.any(String),
firstNameEN: expect.any(String),
lastNameEN: expect.any(String),
}),
updatedAt: expect.toSatisfy(isDateString()),
updatedByUserId: expect.toBeOneOf([expect.any(String), null]),
updatedBy: expect.objectContaining({
id: expect.any(String),
username: expect.any(String),
firstName: expect.any(String),
lastName: expect.any(String),
firstNameEN: expect.any(String),
lastNameEN: expect.any(String),
}),
_count: expect.objectContaining({
branch: expect.any(Number),
}),
}),
]),
);
});
it("get branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "GET",
headers: {
["Authorization"]: "Bearer " + token,
},
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toMatchObject(record);
});
it("update branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "PUT",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
body: JSON.stringify({ name: "Chamomind Intl.", nameEN: "Chamomind Intl." }),
});
record["name"] = "Chamomind Intl.";
record["nameEN"] = "Chamomind Intl.";
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toMatchObject(record);
});
it("delete branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "DELETE",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toMatchObject(record);
});
it("get deleted branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "GET",
headers: {
["Authorization"]: "Bearer " + token,
},
});
expect(res.ok).toBe(false);
});
});

View file

@ -1,10 +0,0 @@
export function isDateString(nullable: boolean = false): (val: any) => boolean {
return (value: any) => {
try {
if (value) return !!new Date(value);
return nullable;
} catch (_) {
return false;
}
};
}

View file

@ -1,5 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {},
});