Compare commits

..

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

95 changed files with 1269 additions and 8621 deletions

View file

@ -1,77 +0,0 @@
name: Deploy Local
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
ca=["/etc/ssl/certs/ca-certificates.crt"]
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
tags: ${{ env.CONTAINER_IMAGE_NAME }}
push: true
- name: Remote Deploy
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ vars.SSH_DEPLOY_HOST }}
port: ${{ vars.SSH_DEPLOY_PORT }}
username: ${{ secrets.SSH_DEPLOY_USER }}
password: ${{ secrets.SSH_DEPLOY_PASSWORD }}
script: eval "${{ secrets.SSH_DEPLOY_CMD }}"
- name: Notify Discord Success
if: success()
run: |
curl -H "Content-Type: application/json" -X POST \
-d '{
"embeds": [{
"title": "✅ Gitea Local Deployment Success!",
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Deployed by: `${{ github.actor }}`",
"color": 3066993,
"footer": {
"text": "Local Release Notification",
"icon_url": "https://example.com/success-icon.png"
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}]
}' \
${{ secrets.DISCORD_WEBHOOK }}
- name: Notify Discord Failure
if: failure()
run: |
curl -H "Content-Type: application/json" -X POST \
-d '{
"embeds": [{
"title": "❌ Gitea Local Deployment Failed!",
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Attempted by: `${{ github.actor }}`",
"color": 15158332,
"footer": {
"text": "Local Release Notification",
"icon_url": "https://example.com/failure-icon.png"
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}]
}' \
${{ secrets.DISCORD_WEBHOOK }}

View file

@ -1,21 +0,0 @@
name: Spell Check
permissions:
contents: read
on: [push, pull_request]
env:
CLICOLOR: 1
jobs:
spelling:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
- name: Spell Check Repo
uses: crate-ci/typos@v1.29.9
with:
files: ./src

View file

@ -1,20 +0,0 @@
---
name: "✨ Feature Request"
about: Suggest an idea for this project
title: "✨ Feature: "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,38 +0,0 @@
---
name: "\U0001F41E Bug report"
about: Create a report to help us improve
title: "\U0001F41E Bug: "
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

31
.github/workflows/local-build-dev.yaml vendored Normal file
View file

@ -0,0 +1,31 @@
name: local-build-dev
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
jobs:
local-build-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
http = true
insecure = true
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/jws/jws-backend:dev
allow: security.insecure

View file

@ -0,0 +1,31 @@
name: local-build-release
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
jobs:
local-build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
http = true
insecure = true
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/jws/jws-backend:latest
allow: security.insecure

View file

@ -0,0 +1,24 @@
name: local-release-demo
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
jobs:
local-release-demo:
runs-on: ubuntu-latest
steps:
- name: Remote deploy internal chamomind server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
script: |
cd ~/repositories/jws-backend
git pull
docker compose up -d --build
sleep 1
docker compose logs -n 100

View file

@ -0,0 +1,24 @@
name: local-release-dev
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
jobs:
local-release-dev:
runs-on: ubuntu-latest
steps:
- name: Remote deploy internal chamomind server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
script: |
cd ~/repositories/jws-backend
git pull
docker compose up -d --build
sleep 1
docker compose logs -n 100

View file

@ -1,2 +0,0 @@
[default]
extend-ignore-re = ["(?Rm)^.*(#|//)\\s*spellchecker:disable-line$"]

View file

@ -1,22 +1,33 @@
FROM node:20-slim
FROM node:23-slim AS base
RUN apt-get update -y \
&& apt-get install -y openssl \
&& npm install -g pnpm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apt-get update && apt-get install -y openssl
RUN pnpm i -g prisma prisma-kysely
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
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 run build
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
FROM base AS prod
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,9 +7,7 @@
"start": "node ./dist/app.js",
"dev": "nodemon",
"check": "tsc --noEmit",
"test": "vitest",
"format": "prettier --write .",
"debug": "nodemon",
"build": "tsoa spec-and-routes && tsc",
"changelog:generate": "git-cliff -o CHANGELOG.md && git add CHANGELOG.md && git commit -m 'Update CHANGELOG.md'",
"db:generate": "prisma generate",
@ -21,49 +19,36 @@
"author": "Frappe'T",
"license": "ISC",
"devDependencies": {
"@types/barcode": "^0.0.33",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/multer": "^1.4.12",
"@types/node": "^20.17.10",
"@types/nodemailer": "^6.4.17",
"@vitest/ui": "^3.1.4",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
"prisma": "6.16.2",
"prisma": "^6.3.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2",
"vitest": "^3.1.4"
"typescript": "^5.7.2"
},
"dependencies": {
"@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2",
"@prisma/client": "6.16.2",
"@prisma/client": "^6.3.0",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
"@types/html-to-text": "^9.0.4",
"canvas": "^3.1.0",
"cors": "^2.8.5",
"cron": "^3.3.1",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
"dayjs-plugin-utc": "^0.1.2",
"docx-templates": "^4.13.0",
"dotenv": "^16.4.7",
"exceljs": "^4.4.0",
"express": "^4.21.2",
"fast-jwt": "^5.0.5",
"html-to-text": "^9.0.5",
"jsbarcode": "^3.11.6",
"json-2-csv": "^5.5.8",
"kysely": "^0.27.5",
"minio": "^8.0.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0",
"pnpm": "^10.18.3",
"prisma-extension-kysely": "^3.0.0",
"promise.any": "^2.0.6",
"thai-baht-text": "^2.0.5",

1766
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
-- AlterEnum
ALTER TYPE "CreditNoteStatus" ADD VALUE 'Waiting';

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "CreditNote" ALTER COLUMN "creditNoteStatus" SET DEFAULT 'Waiting';

View file

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "Notification" ADD COLUMN "registeredBranchId" TEXT;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

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

View file

@ -1,48 +0,0 @@
/*
Warnings:
- You are about to drop the `_NotificationToUser` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_NotificationToUser" DROP CONSTRAINT "_NotificationToUser_A_fkey";
-- DropForeignKey
ALTER TABLE "_NotificationToUser" DROP CONSTRAINT "_NotificationToUser_B_fkey";
-- DropTable
DROP TABLE "_NotificationToUser";
-- CreateTable
CREATE TABLE "_NotificationRead" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_NotificationRead_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_NotificationDelete" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_NotificationDelete_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_NotificationRead_B_index" ON "_NotificationRead"("B");
-- CreateIndex
CREATE INDEX "_NotificationDelete_B_index" ON "_NotificationDelete"("B");
-- AddForeignKey
ALTER TABLE "_NotificationRead" ADD CONSTRAINT "_NotificationRead_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_NotificationRead" ADD CONSTRAINT "_NotificationRead_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_NotificationDelete" ADD CONSTRAINT "_NotificationDelete_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_NotificationDelete" ADD CONSTRAINT "_NotificationDelete_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "Property" (
"id" TEXT NOT NULL,
"registeredBranchId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"type" JSONB NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"statusOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Property_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Property" ADD CONSTRAINT "Property_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

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

View file

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "RequestData" ADD COLUMN "defaultMessengerId" TEXT;
-- AddForeignKey
ALTER TABLE "RequestData" ADD CONSTRAINT "RequestData_defaultMessengerId_fkey" FOREIGN KEY ("defaultMessengerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -1,29 +0,0 @@
-- AlterTable
ALTER TABLE "Employee" ALTER COLUMN "firstName" DROP NOT NULL,
ALTER COLUMN "lastName" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Institution" ADD COLUMN "contactEmail" TEXT,
ADD COLUMN "contactName" TEXT,
ADD COLUMN "contactTel" TEXT;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "agencyStatus" TEXT,
ADD COLUMN "remark" TEXT;
-- CreateTable
CREATE TABLE "InstitutionBank" (
"id" TEXT NOT NULL,
"bankName" TEXT NOT NULL,
"bankBranch" TEXT NOT NULL,
"accountName" TEXT NOT NULL,
"accountNumber" TEXT NOT NULL,
"accountType" TEXT NOT NULL,
"currentlyUse" BOOLEAN NOT NULL,
"institutionId" TEXT NOT NULL,
CONSTRAINT "InstitutionBank_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "InstitutionBank" ADD CONSTRAINT "InstitutionBank_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "Institution"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Employee" ALTER COLUMN "dateOfBirth" DROP NOT NULL;

View file

@ -1,5 +0,0 @@
-- DropForeignKey
ALTER TABLE "InstitutionBank" DROP CONSTRAINT "InstitutionBank_institutionId_fkey";
-- AddForeignKey
ALTER TABLE "InstitutionBank" ADD CONSTRAINT "InstitutionBank_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "Institution"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "contactName" TEXT,
ADD COLUMN "contactTel" TEXT;

View file

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "firstName" DROP NOT NULL,
ALTER COLUMN "lastName" DROP NOT NULL;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "TaskOrder" ADD COLUMN "codeProductReceived" TEXT;

View file

@ -1,18 +0,0 @@
-- AlterTable
ALTER TABLE "Institution" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "createdByUserId" TEXT,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedByUserId" TEXT;
-- AlterTable
ALTER TABLE "Payment" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedByUserId" TEXT;
-- AddForeignKey
ALTER TABLE "Institution" ADD CONSTRAINT "Institution_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Institution" ADD CONSTRAINT "Institution_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Employee" ALTER COLUMN "lastNameEN" DROP NOT NULL;

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

@ -21,16 +21,12 @@ model Notification {
groupReceiver NotificationGroup[]
registeredBranchId String?
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
receiver User? @relation(name: "NotificationReceiver", fields: [receiverId], references: [id], onDelete: Cascade)
receiverId String?
createdAt DateTime @default(now())
readByUser User[] @relation(name: "NotificationRead")
deleteByUser User[] @relation(name: "NotificationDelete")
readByUser User[]
}
model NotificationGroup {
@ -317,8 +313,6 @@ model Branch {
quotation Quotation[]
workflowTemplate WorkflowTemplate[]
taskOrder TaskOrder[]
notification Notification[]
property Property[]
}
model BranchBank {
@ -366,24 +360,16 @@ enum UserType {
AGENCY
}
model UserImportNationality {
id String @id @default(cuid())
name String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
}
model User {
id String @id @default(cuid())
code String?
namePrefix String?
firstName String?
firstName String
firstNameEN String
middleName String?
middleNameEN String?
lastName String?
lastName String
lastNameEN String
username String
gender String
@ -398,24 +384,14 @@ model User {
street String?
streetEN String?
addressForeign Boolean @default(false)
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
provinceText String?
provinceTextEN String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
districtId String?
districtText String?
districtTextEN 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?
subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
subDistrictId String?
email String
telephoneNo String
@ -442,7 +418,7 @@ model User {
licenseExpireDate DateTime? @db.Date
sourceNationality String?
importNationality UserImportNationality[]
importNationality String?
trainingPlace String?
responsibleArea UserResponsibleArea[]
@ -502,28 +478,14 @@ model User {
flowCreated WorkflowTemplate[] @relation("FlowCreatedByUser")
flowUpdated WorkflowTemplate[] @relation("FlowUpdatedByUser")
invoiceCreated Invoice[]
paymentCreated Payment[] @relation("PaymentCreatedByUser")
paymentUpdated Payment[] @relation("PaymentUpdatedByUser")
paymentCreated Payment[]
notificationReceive Notification[] @relation("NotificationReceiver")
notificationRead Notification[] @relation("NotificationRead")
notificationDelete Notification[] @relation("NotificationDelete")
notificationRead Notification[]
taskOrderCreated TaskOrder[] @relation("TaskOrderCreatedByUser")
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
institutionCreated Institution[] @relation("InstitutionCreatedByUser")
institutionUpdated Institution[] @relation("InstitutionUpdatedByUser")
businessTypeCreated BusinessType[] @relation("BusinessTypeCreatedByUser")
businessTypeUpdated BusinessType[] @relation("BusinessTypeUpdatedByUser")
requestWorkStepStatus RequestWorkStepStatus[]
userTask UserTask[]
requestData RequestData[]
remark String?
agencyStatus String?
contactName String?
contactTel String?
quotation Quotation[]
}
model UserResponsibleArea {
@ -562,9 +524,10 @@ model Customer {
}
model CustomerBranch {
id String @id @default(cuid())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
id String @id @default(cuid())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
customerName String?
code String
codeCustomer String
@ -626,8 +589,7 @@ model CustomerBranch {
agentUser User? @relation(fields: [agentUserId], references: [id], onDelete: SetNull)
// NOTE: Business
businessTypeId String?
businessType BusinessType? @relation(fields: [businessTypeId], references: [id], onDelete: SetNull)
businessType String
jobPosition String
jobDescription String
payDate String
@ -786,21 +748,6 @@ model CustomerBranchVatRegis {
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 {
id String @id @default(cuid())
@ -808,17 +755,16 @@ model Employee {
nrcNo String?
namePrefix String?
firstName String?
firstName String
firstNameEN String
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
lastName String
lastNameEN String
dateOfBirth DateTime? @db.Date
gender String
nationality String
otherNationality String?
dateOfBirth DateTime @db.Date
gender String
nationality String
address String?
addressEN String?
@ -893,19 +839,18 @@ model EmployeePassport {
issuePlace String
previousPassportRef String?
workerStatus String?
nationality String?
otherNationality String?
namePrefix String?
firstName String?
firstNameEN String?
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
gender String?
birthDate String?
birthCountry String?
workerStatus String?
nationality String?
namePrefix String?
firstName String?
firstNameEN String?
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
gender String?
birthDate String?
birthCountry String?
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String
@ -922,9 +867,8 @@ model EmployeeVisa {
entryCount Int
issueCountry String
issuePlace String
issueDate DateTime @db.Date
expireDate DateTime @db.Date
reportDate DateTime? @db.Date
issueDate DateTime @db.Date
expireDate DateTime @db.Date
mrz String?
remark String?
@ -1052,49 +996,6 @@ model Institution {
selectedImage String?
taskOrder TaskOrder[]
contactName String?
contactEmail String?
contactTel String?
createdAt DateTime @default(now())
createdBy User? @relation(name: "InstitutionCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @default(now()) @updatedAt
updatedBy User? @relation(name: "InstitutionUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
bank InstitutionBank[]
}
model InstitutionBank {
id String @id @default(cuid())
bankName String
bankBranch String
accountName String
accountNumber String
accountType String
currentlyUse Boolean
institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade)
institutionId String
}
model Property {
id String @id @default(cuid())
registeredBranch Branch @relation(fields: [registeredBranchId], references: [id])
registeredBranchId String
name String
nameEN String
type Json
status Status @default(CREATED)
statusOrder Int @default(0)
createdAt DateTime @default(now())
}
model WorkflowTemplate {
@ -1128,15 +1029,6 @@ model WorkflowTemplateStepInstitution {
workflowTemplateStepId String
}
model WorkflowTemplateStepGroup {
id String @id @default(cuid())
group String
workflowTemplateStep WorkflowTemplateStep @relation(fields: [workflowTemplateStepId], references: [id], onDelete: Cascade)
workflowTemplateStepId String
}
model WorkflowTemplateStep {
id String @id @default(cuid())
@ -1147,7 +1039,6 @@ model WorkflowTemplateStep {
value WorkflowTemplateStepValue[] // NOTE: For enum or options type
responsiblePerson WorkflowTemplateStepUser[]
responsibleInstitution WorkflowTemplateStepInstitution[]
responsibleGroup WorkflowTemplateStepGroup[]
messengerByArea Boolean @default(false)
attributes Json?
@ -1243,9 +1134,6 @@ model Product {
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
productGroupId String
flowAccountProductIdSellPrice String?
flowAccountProductIdAgentPrice String?
workProduct WorkProduct[]
quotationProductServiceList QuotationProductServiceList[]
taskProduct TaskProduct[]
@ -1418,9 +1306,6 @@ model Quotation {
invoice Invoice[]
creditNote CreditNote[]
seller User? @relation(fields: [sellerId], references: [id], onDelete: Cascade)
sellerId String?
}
model QuotationPaySplit {
@ -1445,9 +1330,6 @@ model QuotationWorker {
employeeId String
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
quotationId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model QuotationProductServiceList {
@ -1527,19 +1409,12 @@ model Payment {
paymentStatus PaymentStatus
amount Float
date DateTime?
channel String?
account String?
reference String?
amount Float
date DateTime?
createdAt DateTime @default(now())
createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdBy User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @default(now()) @updatedAt
updatedBy User? @relation(name: "PaymentUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
}
enum RequestDataStatus {
@ -1563,16 +1438,8 @@ model RequestData {
requestDataStatus RequestDataStatus @default(Pending)
customerRequestCancel Boolean?
customerRequestCancelReason String?
rejectRequestCancel Boolean?
rejectRequestCancelReason String?
flow Json?
defaultMessenger User? @relation(fields: [defaultMessengerId], references: [id])
defaultMessengerId String?
requestWork RequestWork[]
createdAt DateTime @default(now())
@ -1606,11 +1473,6 @@ model RequestWork {
stepStatus RequestWorkStepStatus[]
customerRequestCancel Boolean?
customerRequestCancelReason String?
rejectRequestCancel Boolean?
rejectRequestCancelReason String?
creditNote CreditNote? @relation(fields: [creditNoteId], references: [id], onDelete: SetNull)
creditNoteId String?
}
@ -1618,7 +1480,6 @@ model RequestWork {
model RequestWorkStepStatus {
step Int
workStatus RequestWorkStatus @default(Pending)
updatedAt DateTime @default(now()) @updatedAt
requestWork RequestWork @relation(fields: [requestWorkId], references: [id], onDelete: Cascade)
requestWorkId String
@ -1693,8 +1554,7 @@ model TaskProduct {
model TaskOrder {
id String @id @default(cuid())
code String
codeProductReceived String?
code String
taskName String
taskOrderStatus TaskOrderStatus @default(Pending)
@ -1743,7 +1603,6 @@ model UserTask {
}
enum CreditNoteStatus {
Waiting
Pending
Success
}
@ -1764,7 +1623,7 @@ model CreditNote {
code String
creditNoteStatus CreditNoteStatus @default(Waiting)
creditNoteStatus CreditNoteStatus @default(Pending)
value Float @default(0)
reason String?

View file

@ -1,19 +1,14 @@
import { createCanvas } from "canvas";
import JsBarcode from "jsbarcode";
import createReport from "docx-templates";
import ThaiBahtText from "thai-baht-text";
import { District, Province, SubDistrict } from "@prisma/client";
import { Readable } from "node:stream";
import { Controller, Get, Path, Query, Route, Tags } from "tsoa";
import { Controller, Get, Path, Query, Route } from "tsoa";
import prisma from "../db";
import { notFoundError } from "../utils/error";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { getFileBuffer, listFile } from "../utils/minio";
import { dateFormat } from "../utils/datetime";
import { downloadFile as edmDownloadFile, list as edmList } from "../services/edm/edm-api";
const DOCUMENT_PATH = process.env.DOCUMENT_TEMPLATE_LOCATION?.split("/").filter(Boolean) || [];
const quotationData = (id: string) =>
prisma.quotation.findFirst({
@ -34,14 +29,8 @@ const quotationData = (id: string) =>
},
},
customerBranch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: {
customer: true,
businessType: true,
province: true,
district: true,
subDistrict: true,
@ -69,62 +58,14 @@ const quotationData = (id: string) =>
service: true,
},
},
createdBy: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
});
const requestWorkData = (id: string, step?: number) =>
prisma.requestWork.findFirst({
where: { id },
include: {
processByUser: true,
productService: {
include: {
product: true,
},
},
request: {
include: {
employee: {
include: {
subDistrict: true,
district: true,
province: true,
},
},
quotation: true,
},
},
stepStatus: {
where: { step },
},
},
});
@Route("api/v1/doc-template")
@Tags("Document Template")
export class DocTemplateController extends Controller {
@Get()
async getTemplate(@Query() templateGroup?: string) {
if (
process.env.DOCUMENT_TEMPLATE_PROVIDER &&
process.env.DOCUMENT_TEMPLATE_PROVIDER === "edm-api"
) {
const ret = await edmList(
"file",
templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH,
);
if (ret) return ret.map((v) => v.fileName);
}
return await listFile(
(templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH).join("/") + "/",
);
async getTemplate() {
return await listFile(`doc-template/`);
}
@Get("{documentTemplate}")
@ -133,7 +74,6 @@ export class DocTemplateController extends Controller {
@Query() data: string,
@Query() dataId: string,
@Query() dataOnly?: boolean,
@Query() templateGroup?: string,
): Promise<Readable | Record<string, any>> {
let record: Record<string, any>;
@ -174,44 +114,14 @@ export class DocTemplateController extends Controller {
}),
);
break;
case "request-work":
record = await requestWorkData(dataId).then((requestWork) => ({
request: replaceEmptyField(requestWork?.request),
requestWork: replaceEmptyField(requestWork),
employee: requestWork?.request.employee,
}));
break;
default:
throw new HttpError(HttpStatus.BAD_REQUEST, "No data for template", "noDataTemplate");
}
if (!data) throw notFoundError("Data");
if (dataOnly) return record;
if (templateGroup) documentTemplate = templateGroup + "/" + documentTemplate;
let template: Buffer<ArrayBufferLike> | null = null;
switch (process.env.DOCUMENT_TEMPLATE_PROVIDER) {
case "edm-api":
await edmDownloadFile(DOCUMENT_PATH, documentTemplate).then(async (payload) => {
if (!payload) return;
const res = await fetch(payload.downloadUrl);
if (!res.ok) return;
template = Buffer.from(await res.arrayBuffer());
});
break;
case "local":
default:
template = await getFileBuffer(`${DOCUMENT_PATH.join("/")}/${documentTemplate}`);
}
if (!template) {
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to get template file",
"templateGetFailed",
);
}
const template = await getFileBuffer(`doc-template/${documentTemplate}`);
if (!data) Readable.from(template);
@ -260,23 +170,6 @@ export class DocTemplateController extends Controller {
thaiBahtText: (input: string | number) => {
ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input);
},
barcode: async (data: string, width?: number, height?: number) =>
new Promise<{
width: number;
height: number;
data: string;
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);
@ -285,15 +178,10 @@ export class DocTemplateController extends Controller {
}
function replaceEmptyField<T>(data: T): T {
try {
return JSON.parse(JSON.stringify(data).replace(/null|\"\"/g, '"\-"'));
} catch (e) {
return data;
}
return JSON.parse(JSON.stringify(data).replace(/null|\"\"/g, '"\-"'));
}
type FullAddress = {
addressForeign?: boolean;
address: string;
addressEN: string;
moo?: string;
@ -302,14 +190,8 @@ type FullAddress = {
soiEN?: string;
street?: string;
streetEN?: string;
provinceText?: string | null;
provinceTextEN?: string | null;
province?: Province | null;
districtText?: string | null;
districtTextEN?: string | null;
district?: District | null;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrict?: SubDistrict | null;
en?: boolean;
};
@ -343,22 +225,13 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
if (addr.soi) fragments.push(`ซอย ${addr.soi},`);
if (addr.street) fragments.push(`ถนน${addr.street},`);
if (!addr.addressForeign && addr.subDistrict) {
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name}`);
if (addr.subDistrict) {
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name},`);
}
if (addr.addressForeign && addr.subDistrictText) {
fragments.push(`ตำบล${addr.subDistrictText}`);
if (addr.district) {
fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.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}`);
if (addr.province) fragments.push(`จังหวัด${addr.province.name},`);
break;
default:
@ -367,31 +240,14 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
if (addr.soiEN) fragments.push(`Soi ${addr.soiEN},`);
if (addr.streetEN) fragments.push(`${addr.streetEN} Rd.`);
if (!addr.addressForeign && addr.subDistrict) {
if (addr.subDistrict) {
fragments.push(`${addr.subDistrict.nameEN} sub-district,`);
}
if (addr.addressForeign && addr.subDistrictTextEN) {
fragments.push(`${addr.subDistrictTextEN} sub-district,`);
}
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,`);
}
if (addr.district) fragments.push(`${addr.district.nameEN} district,`);
if (addr.province) fragments.push(`${addr.province.nameEN},`);
break;
}
if (addr.subDistrict) fragments.push(addr.subDistrict.zipCode);
return fragments.join(" ");
}
@ -404,9 +260,6 @@ function gender(text: string, lang: "th" | "en" = "en") {
}
}
/**
* @deprecated
*/
function businessType(text: string, lang: "th" | "en" = "en") {
switch (lang) {
case "th":
@ -488,7 +341,7 @@ function nationality(text: string, lang: "th" | "en" = "en") {
case "th":
return (
{
["THA"]: "ไทย", // spellchecker:disable-line
["THA"]: "ไทย",
["MMR"]: "เมียนมา",
["LAO"]: "ลาว",
["KHM"]: "กัมพูชา",
@ -500,7 +353,7 @@ function nationality(text: string, lang: "th" | "en" = "en") {
default:
return (
{
["THA"]: "Thai", // spellchecker:disable-line
["THA"]: "Thai",
["MMR"]: "Myanmar",
["LAO"]: "Laos",
["KHM"]: "Khmer",

View file

@ -2,7 +2,6 @@ import { Body, Controller, Get, Path, Post, Query, Route, Tags } from "tsoa";
import prisma from "../db";
import { queryOrNot } from "../utils/relation";
import { notFoundError } from "../utils/error";
import { Prisma } from "@prisma/client";
@Route("/api/v1/employment-office")
@Tags("Employment Office")
@ -12,39 +11,6 @@ export class EmploymentOfficeController extends Controller {
return this.getEmploymentOfficeListByCriteria(districtId, query);
}
@Post("list-same-office-area")
async getSameOfficeArea(@Body() body: { districtId: string }) {
const office = await prisma.employmentOffice.findFirst({
include: {
province: {
include: {
district: true,
},
},
district: true,
},
where: {
OR: [
{
province: { district: { some: { id: body.districtId } } },
district: { none: {} },
},
{
district: {
some: { districtId: body.districtId },
},
},
],
},
});
if (!office) return [];
return [
...office.district.map((v) => v.districtId),
...office.province.district.map((v) => v.id),
];
}
@Post("list")
async getEmploymentOfficeListByCriteria(
@Query() districtId?: string,
@ -74,14 +40,11 @@ export class EmploymentOfficeController extends Controller {
],
[],
),
...(queryOrNot(
...queryOrNot(
query,
[
{ name: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
],
[{ name: { contains: query } }, { nameEN: { contains: query } }],
[],
) satisfies Prisma.EmploymentOfficeWhereInput["OR"]),
),
...queryOrNot(!!body?.id, [{ id: { in: body?.id } }], []),
]
: undefined,

View file

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

View file

@ -5,6 +5,7 @@ import {
Get,
Path,
Post,
Put,
Query,
Request,
Route,
@ -12,14 +13,10 @@ import {
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";
import dayjs from "dayjs";
import { createPermCondition } from "../services/permission";
import HttpStatus from "../interfaces/http-status";
const permissionCondCompany = createPermCondition((_) => true);
type NotificationCreate = {};
type NotificationUpdate = {};
@Route("/api/v1/notification")
@Tags("Notification")
@ -32,53 +29,12 @@ export class NotificationController extends Controller {
@Query() pageSize: number = 30,
@Query() query = "",
) {
const where: Prisma.NotificationWhereInput = {
AND: [
{
OR: queryOrNot<(typeof where)[]>(query, [
{ title: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
]),
},
{
OR: [
{ receiverId: req.user.sub },
req.user.roles.length > 0
? {
groupReceiver: { some: { name: { in: req.user.roles } } },
registeredBranch: { OR: permissionCondCompany(req.user) },
}
: {},
],
},
],
NOT: {
OR: [
{
readByUser: { some: { id: req.user.sub } },
createdAt: { lte: dayjs().subtract(7, "days").toDate() },
},
{ deleteByUser: { some: { id: req.user.sub } } },
],
},
};
const [result, total] = await prisma.$transaction([
prisma.notification.findMany({
where,
include: { readByUser: true },
orderBy: { createdAt: "desc" },
}),
prisma.notification.count({ where }),
]);
const total = 0;
// TODO: implement
return {
result: result.map((v) => ({
id: v.id,
title: v.title,
detail: v.detail,
createdAt: v.createdAt,
read: v.readByUser.some((v) => v.id === req.user.sub),
})),
result: [],
page,
pageSize,
total,
@ -88,85 +44,37 @@ export class NotificationController extends Controller {
@Get("{notificationId}")
@Security("keycloak")
async getNotification(@Request() req: RequestWithUser, @Path() notificationId: string) {
const record = await prisma.notification.update({
where: { id: notificationId },
data: {
readByUser: {
connect: { id: req.user.sub },
},
},
});
// TODO: implement
if (!record) throw notFoundError("Notification");
return record;
return {};
}
@Post("mark-read")
@Post()
@Security("keycloak")
async markRead(@Request() req: RequestWithUser, @Body() body?: { id: string[] }) {
const record = await prisma.notification.findMany({
where: {
id: body ? { in: body.id } : undefined,
OR: !body
? [
{ receiverId: req.user.sub },
req.user.roles.length > 0
? {
groupReceiver: { some: { name: { in: req.user.roles } } },
registeredBranch: { OR: permissionCondCompany(req.user) },
}
: {},
]
: undefined,
},
});
async createNotification(@Request() req: RequestWithUser, @Body() body: NotificationCreate) {
// TODO: implement
await prisma.$transaction(
record.map((v) =>
prisma.notification.update({
where: { id: v.id },
data: {
readByUser: { connect: { id: req.user.sub } },
},
}),
),
);
this.setStatus(HttpStatus.CREATED);
return {};
}
@Delete()
@Put("{notificationId}")
@Security("keycloak")
async deleteNotificationMany(@Request() req: RequestWithUser, @Body() notificationId: string[]) {
if (!notificationId.length) return;
async updateNotification(
@Request() req: RequestWithUser,
@Path() notificationId: string,
@Body() body: NotificationUpdate,
) {
// TODO: implement
return await prisma.notification
.findMany({ where: { id: { in: notificationId } } })
.then(async (v) => {
await prisma.$transaction(
v.map((v) =>
prisma.notification.update({
where: { id: v.id },
data: {
deleteByUser: { connect: { id: req.user.sub } },
},
}),
),
);
});
return {};
}
@Delete("{notificationId}")
@Security("keycloak")
async deleteNotification(@Request() req: RequestWithUser, @Path() notificationId: string) {
const record = await prisma.notification.findFirst({ where: { id: notificationId } });
if (!record) throw notFoundError("Notification");
return await prisma.notification.update({
where: { id: notificationId },
data: {
deleteByUser: {
connect: { id: req.user.sub },
},
},
});
// TODO: implement
return {};
}
}

View file

@ -1,733 +0,0 @@
import config from "../config.json";
import {
Customer,
CustomerBranch,
QuotationStatus,
RequestWorkStatus,
PaymentStatus,
} from "@prisma/client";
import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa";
import prisma from "../db";
import { createPermCondition, createQueryPermissionCondition } from "../services/permission";
import { RequestWithUser } from "../interfaces/user";
import { precisionRound } from "../utils/arithmetic";
import dayjs from "dayjs";
import { json2csv } from "json-2-csv";
import { isSystem } from "../utils/keycloak";
import { jsonObjectFrom } from "kysely/helpers/postgres";
const permissionCondCompany = createPermCondition((_) => true);
const permissionQueryCondCompany = createQueryPermissionCondition((_) => true);
const VAT_DEFAULT = config.vat;
@Route("/api/v1/report")
@Security("keycloak")
@Tags("Report")
export class StatsController extends Controller {
@Get("quotation/download")
async downloadQuotationReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.quotationReport(req, limit, startDate, endDate).then((v) =>
v.map((v) => ({
...v,
customerBranch: {
...v.customerBranch,
customerType: v.customerBranch.customer.customerType,
customer: undefined,
},
})),
),
{ useDateIso8601Format: true },
);
}
@Get("quotation")
async quotationReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.quotation.findMany({
select: {
code: true,
quotationStatus: true,
customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true },
},
finalPrice: true,
createdAt: true,
updatedAt: true,
},
where: {
registeredBranch: { OR: permissionCondCompany(req.user) },
createdAt: { gte: startDate, lte: endDate },
},
orderBy: { createdAt: "desc" },
take: limit,
});
return record.map((v) => ({
document: "quotation",
code: v.code,
status: v.quotationStatus,
amount: v.finalPrice,
createdAt: v.createdAt,
updatedAt: v.updatedAt,
customerBranch: v.customerBranch,
}));
}
@Get("invoice/download")
async downloadInvoiceReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.invoiceReport(req, limit, startDate, endDate).then((v) =>
v.map((v) => ({
...v,
customerBranch: {
...v.customerBranch,
customerType: v.customerBranch.customer.customerType,
customer: undefined,
},
})),
),
{
useDateIso8601Format: true,
},
);
}
@Get("invoice")
async invoiceReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.invoice.findMany({
select: {
code: true,
quotation: {
select: {
customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true },
},
},
},
payment: {
select: {
paymentStatus: true,
},
},
amount: true,
createdAt: true,
},
where: {
quotation: {
isDebitNote: false,
registeredBranch: { OR: permissionCondCompany(req.user) },
},
createdAt: { gte: startDate, lte: endDate },
},
orderBy: { createdAt: "desc" },
take: limit,
});
return record.map((v) => ({
document: "invoice",
code: v.code,
status: v.payment?.paymentStatus,
amount: v.amount,
createdAt: v.createdAt,
customerBranch: v.quotation.customerBranch,
}));
}
@Get("receipt/download")
async downloadReceiptReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.receiptReport(req, limit, startDate, endDate).then((v) =>
v.map((v) => ({
...v,
customerBranch: {
...v.customerBranch,
customerType: v.customerBranch.customer.customerType,
customer: undefined,
},
})),
),
{
useDateIso8601Format: true,
},
);
}
@Get("receipt")
async receiptReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.payment.findMany({
select: {
code: true,
invoice: {
select: {
quotation: {
select: {
customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true },
},
},
},
},
},
amount: true,
paymentStatus: true,
createdAt: true,
},
where: {
paymentStatus: PaymentStatus.PaymentSuccess,
invoice: {
quotation: {
isDebitNote: false,
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
createdAt: { gte: startDate, lte: endDate },
},
orderBy: { createdAt: "desc" },
take: limit,
});
return record.map((v) => ({
document: "receipt",
code: v.code,
amount: v.amount,
status: v.paymentStatus,
createdAt: v.createdAt,
customerBranch: v.invoice.quotation.customerBranch,
}));
}
@Get("product/download")
async downloadProductReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(await this.productReport(req, limit, startDate, endDate), {
useDateIso8601Format: true,
});
}
@Get("product")
async productReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await prisma.$transaction(async (tx) => {
const record = await tx.product.findMany({
select: {
id: true,
code: true,
name: true,
createdAt: true,
updatedAt: true,
quotationProductServiceList: {
include: { quotation: true },
},
_count: {
select: {
quotationProductServiceList: {
where: {
quotation: {
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
},
},
},
},
},
},
where: {
quotationProductServiceList: {
some: {
quotation: { createdAt: { gte: startDate, lte: endDate } },
},
},
productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } },
},
orderBy: {
quotationProductServiceList: { _count: "desc" },
},
take: limit,
});
const doing = await tx.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
requestWork: {
some: {
stepStatus: {
some: {
workStatus: {
in: [
RequestWorkStatus.Pending,
RequestWorkStatus.InProgress,
RequestWorkStatus.Validate,
RequestWorkStatus.Completed,
RequestWorkStatus.Ended,
],
},
},
},
},
},
},
});
const order = await tx.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
},
});
return record.map((v) => ({
document: "product",
code: v.code,
name: v.name,
sale: v._count.quotationProductServiceList,
did: doing.find((item) => item.productId === v.id)?._count || 0,
order: order.find((item) => item.productId === v.id)?._count || 0,
createdAt: v.createdAt,
updatedAt: v.updatedAt,
}));
});
}
@Get("sale/by-product-group/download")
async downloadSaleByProductGroupReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.saleReport(req, limit, startDate, endDate).then((v) => v.byProductGroup),
{ useDateIso8601Format: true },
);
}
@Get("sale/by-sale/download")
async downloadSaleBySaleReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(await this.saleReport(req, limit, startDate, endDate).then((v) => v.bySale), {
useDateIso8601Format: true,
});
}
@Get("sale/by-customer/download")
async downloadSaleByCustomerReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.saleReport(req, limit, startDate, endDate).then((v) => v.byCustomer),
{ useDateIso8601Format: true },
);
}
@Get("sale")
async saleReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const list = await prisma.quotationProductServiceList.findMany({
include: {
quotation: {
include: {
createdBy: true,
customerBranch: {
omit: { otpCode: true, otpExpires: true, userId: true },
include: { customer: true },
},
},
},
product: {
include: {
productGroup: true,
},
},
},
where: {
quotation: {
isDebitNote: false,
registeredBranch: { OR: permissionCondCompany(req.user) },
createdAt: { gte: startDate, lte: endDate },
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
},
},
take: limit,
});
return list.reduce<{
byProductGroup: ((typeof list)[number]["product"]["productGroup"] & { _count: number })[];
bySale: ((typeof list)[number]["quotation"]["createdBy"] & { _count: number })[];
byCustomer: ((typeof list)[number]["quotation"]["customerBranch"] & { _count: number })[];
}>(
(a, c) => {
{
const found = a.byProductGroup.find((v) => v.id === c.product.productGroupId);
if (found) {
found._count++;
} else {
a.byProductGroup.push({ ...c.product.productGroup, _count: 1 });
}
}
{
const found = a.bySale.find((v) => v.id === c.quotation.createdByUserId);
if (found) {
found._count++;
} else {
if (c.quotation.createdBy) {
a.bySale.push({ ...c.quotation.createdBy, _count: 1 });
}
}
}
{
const found = a.byCustomer.find((v) => v.id === c.quotation.customerBranchId);
if (found) {
found._count++;
} else {
a.byCustomer.push({ ...c.quotation.customerBranch, _count: 1 });
}
}
return a;
},
{ byProductGroup: [], bySale: [], byCustomer: [] },
);
}
@Get("profit")
async profit(
@Request() req: RequestWithUser,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.quotationProductServiceList.findMany({
include: {
work: {
include: {
productOnWork: {
select: { stepCount: true, productId: true },
},
},
},
product: {
select: {
agentPrice: true,
agentPriceCalcVat: true,
agentPriceVatIncluded: true,
serviceCharge: true,
serviceChargeCalcVat: true,
serviceChargeVatIncluded: true,
price: true,
calcVat: true,
vatIncluded: true,
},
},
requestWork: {
include: {
stepStatus: true,
creditNote: true,
},
},
quotation: {
select: {
agentPrice: true,
creditNote: true,
createdAt: true,
},
},
},
where: {
quotation: {
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
registeredBranch: {
OR: permissionCondCompany(req.user),
},
createdAt: { gte: startDate, lte: endDate },
},
},
});
const data = record.map((v) => {
const originalPrice = v.product.serviceCharge;
const productExpenses = precisionRound(
originalPrice + (v.product.serviceChargeVatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const finalPrice = v.pricePerUnit * v.amount * (1 + config.vat);
return v.requestWork.map((w) => {
const creditNote = w.creditNote;
const roundCount = v.work?.productOnWork.find((p) => p.productId)?.stepCount || 1;
const successCount = w.stepStatus.filter(
(s) => s.workStatus !== RequestWorkStatus.Canceled,
).length;
const income = creditNote
? precisionRound(productExpenses * successCount)
: precisionRound(finalPrice);
const expenses = creditNote
? precisionRound(productExpenses * successCount)
: precisionRound(productExpenses * roundCount);
const netProfit = creditNote ? 0 : precisionRound(finalPrice - expenses);
return {
month: v.quotation.createdAt.getMonth() + 1,
year: v.quotation.createdAt.getFullYear(),
income,
expenses,
netProfit,
};
});
});
return data
.flat()
.reduce<{ income: number; expenses: 0; netProfit: 0; dataset: (typeof data)[number] }>(
(a, c) => {
const current = a.dataset.find((v) => v.month === c.month && v.year === c.year);
if (current) {
current.income += c.income;
current.expenses += c.expenses;
current.netProfit += c.netProfit;
} else {
a.dataset.push(c);
}
a.income += c.income;
a.expenses += c.expenses;
a.netProfit += c.netProfit;
return a;
},
{ income: 0, expenses: 0, netProfit: 0, dataset: [] },
);
}
@Get("payment")
async invoice(
@Request() req: RequestWithUser,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
if (!startDate && !endDate) {
startDate = dayjs(new Date()).subtract(12, "months").startOf("month").toDate();
endDate = dayjs(new Date()).endOf("months").toDate();
}
if (!startDate && endDate) {
startDate = dayjs(endDate).subtract(12, "months").startOf("month").toDate();
}
if (startDate && !endDate) {
endDate = dayjs(new Date()).endOf("month").toDate();
}
const data = await prisma.$transaction(async (tx) => {
const months: Date[] = [];
while (startDate! < endDate!) {
months.push(startDate!);
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(
months.map(async (v) => {
const date = dayjs(v);
return {
month: date.format("MM"),
year: date.format("YYYY"),
data: await tx.payment
.groupBy({
_sum: { amount: true },
where: {
createdAt: { gte: v, lte: date.endOf("month").toDate() },
invoiceId: { in: invoices.map((v) => v.id) },
},
by: "paymentStatus",
})
.then((v) =>
v.reduce<Partial<Record<(typeof v)[number]["paymentStatus"], number>>>((a, c) => {
a[c.paymentStatus] = c._sum.amount || 0;
return a;
}, {}),
),
};
}),
);
});
return data;
}
@Get("customer-dept")
async reportCustomerDept(@Request() req: RequestWithUser) {
let query = prisma.$kysely
.selectFrom("Invoice")
.leftJoin("Quotation", "Quotation.id", "Invoice.quotationId")
.leftJoin("Payment", "Invoice.id", "Payment.invoiceId")
.leftJoin("CustomerBranch", "CustomerBranch.id", "Quotation.customerBranchId")
.leftJoin("Customer", "Customer.id", "CustomerBranch.customerId")
.select((eb) => [
jsonObjectFrom(
eb
.selectFrom("CustomerBranch")
.select((eb) => [
jsonObjectFrom(
eb
.selectFrom("Customer")
.selectAll("Customer")
.whereRef("Customer.id", "=", "CustomerBranch.customerId"),
).as("customer"),
])
.selectAll("CustomerBranch")
.whereRef("CustomerBranch.id", "=", "Quotation.customerBranchId"),
).as("customerBranch"),
])
.select(["Payment.paymentStatus"])
.selectAll(["Invoice"])
.distinctOn("Invoice.id");
if (!isSystem(req.user)) {
query = query.where(permissionQueryCondCompany(req.user));
}
const ret = await query.execute();
return ret
.reduce<
{
paid: number;
unpaid: number;
customerBranch: CustomerBranch & {
customer: Customer;
};
}[]
>((acc, item) => {
const exists = acc.find((v) => v.customerBranch.id === item.customerBranch!.id);
if (!item.amount) return acc;
if (!exists) {
return acc.concat({
customerBranch: item.customerBranch as CustomerBranch & { customer: Customer },
paid: item.paymentStatus === "PaymentSuccess" ? item.amount : 0,
unpaid: item.paymentStatus !== "PaymentSuccess" ? item.amount : 0,
});
} else {
exists[item.paymentStatus === "PaymentSuccess" ? "paid" : "unpaid"] += item.amount;
}
return acc;
}, [])
.map((v) => ({
...v,
customerBranch: {
...v.customerBranch,
userId: undefined,
otpCode: undefined,
otpExpires: undefined,
},
_quotation: undefined,
}));
}
}

View file

@ -39,7 +39,6 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
@ -47,20 +46,16 @@ if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
return MANAGE_ROLES.some((v) => user.roles?.includes(v));
}
function globalAllowView(user: RequestWithUser["user"]) {
return MANAGE_ROLES.concat("head_of_accountant", "head_of_sale").some((v) =>
user.roles?.includes(v),
);
}
type BranchCreate = {
@ -151,7 +146,7 @@ type BranchUpdate = {
}[];
};
const permissionCond = createPermCondition(globalAllow);
const permissionCond = createPermCondition(globalAllowView);
const permissionCheck = createPermCheck(globalAllow);
@Route("api/v1/branch")
@ -255,8 +250,6 @@ export class BranchController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
AND: {
@ -272,27 +265,26 @@ export class BranchController extends Controller {
},
OR: queryOrNot<Prisma.BranchWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query } },
{ name: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
...whereAddressQuery(query),
{
branch: {
some: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query } },
{ name: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
...whereAddressQuery(query),
],
},
},
},
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.BranchWhereInput;
const [result, total] = await prisma.$transaction([
@ -317,20 +309,19 @@ export class BranchController extends Controller {
where: {
AND: { OR: permissionCond(req.user) },
OR: [
{ nameEN: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query } },
{ name: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
...whereAddressQuery(query),
],
...whereDateQuery(startDate, endDate),
},
include: {
province: true,
district: true,
subDistrict: true,
},
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
orderBy: { code: "asc" },
}
: false,
bank: true,
@ -374,7 +365,7 @@ export class BranchController extends Controller {
bank: true,
contact: includeContact,
},
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
orderBy: { code: "asc" },
},
bank: true,
contact: includeContact,
@ -387,14 +378,6 @@ export class BranchController extends Controller {
return record;
}
@Get("{branchId}/bank")
@Security("keycloak")
async getBranchBankById(@Path() branchId: string) {
return await prisma.branchBank.findMany({
where: { branchId },
});
}
@Post()
@Security("keycloak", MANAGE_ROLES)
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {

View file

@ -18,21 +18,12 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
import { branchRelationPermInclude, createPermCheck } from "../services/permission";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
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;
}
@ -106,8 +97,6 @@ export class UserBranchController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
AND: {
@ -115,10 +104,9 @@ export class UserBranchController extends Controller {
userId,
},
OR: queryOrNot<Prisma.BranchUserWhereInput[]>(query, [
{ branch: { name: { contains: query, mode: "insensitive" } } },
{ branch: { nameEN: { contains: query, mode: "insensitive" } } },
{ branch: { name: { contains: query } } },
{ branch: { nameEN: { contains: query } } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([
@ -162,8 +150,6 @@ export class BranchUserController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
AND: {
@ -171,14 +157,13 @@ export class BranchUserController extends Controller {
branchId,
},
OR: [
{ user: { firstName: { contains: query, mode: "insensitive" } } },
{ user: { firstNameEN: { contains: query, mode: "insensitive" } } },
{ user: { lastName: { contains: query, mode: "insensitive" } } },
{ user: { lastNameEN: { contains: query, mode: "insensitive" } } },
{ user: { email: { contains: query, mode: "insensitive" } } },
{ user: { telephoneNo: { contains: query, mode: "insensitive" } } },
{ user: { firstName: { contains: query } } },
{ user: { firstNameEN: { contains: query } } },
{ user: { lastName: { contains: query } } },
{ user: { lastNameEN: { contains: query } } },
{ user: { email: { contains: query } } },
{ user: { telephoneNo: { contains: query } } },
],
...whereDateQuery(startDate, endDate),
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -27,7 +27,6 @@ import {
listRole,
getUserRoles,
removeUserRoles,
getGroupUser,
} from "../services/keycloak";
import { isSystem } from "../utils/keycloak";
import {
@ -38,7 +37,6 @@ import {
getPresigned,
listFile,
setFile,
uploadFile,
} from "../utils/minio";
import { filterStatus } from "../services/prisma";
import {
@ -52,7 +50,6 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { retry } from "../utils/func";
@ -61,17 +58,10 @@ if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"branch_admin",
"branch_manager",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
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;
}
@ -88,11 +78,11 @@ type UserCreate = {
citizenExpire?: Date | null;
namePrefix?: string | null;
firstName?: string;
firstName: string;
firstNameEN: string;
middleName?: string | null;
middleNameEN?: string | null;
lastName?: string;
lastName: string;
lastNameEN: string;
gender: string;
@ -106,12 +96,11 @@ type UserCreate = {
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string[] | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string[] | null;
birthDate?: Date | null;
addressForeign?: boolean;
address: string;
addressEN: string;
soi?: string | null;
@ -123,26 +112,13 @@ type UserCreate = {
email: string;
telephoneNo: string;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrictId?: string | null;
districtText?: string | null;
districtTextEN?: string | null;
districtId?: string | null;
provinceText?: string | null;
provinceTextEN?: string | null;
provinceId?: string | null;
zipCodeText?: string | null;
selectedImage?: string;
branchId: string | string[];
remark?: string;
agencyStatus?: string;
contactName?: string | null;
contactTel?: string | null;
};
type UserUpdate = {
@ -176,12 +152,11 @@ type UserUpdate = {
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string[] | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string[] | null;
birthDate?: Date | null;
addressForeign?: boolean;
address?: string;
addressEN?: string;
soi?: string | null;
@ -195,27 +170,13 @@ type UserUpdate = {
selectedImage?: string;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrictId?: string | null;
districtText?: string | null;
districtTextEN?: string | null;
districtId?: string | null;
provinceText?: string | null;
provinceTextEN?: string | null;
provinceId?: string | null;
zipCodeText?: string | null;
branchId?: string | string[];
remark?: string;
agencyStatus?: string;
contactName?: string | null;
contactTel?: string | null;
};
const permissionCondCompany = createPermCondition((_) => true);
const permissionCond = createPermCondition(globalAllow);
const permissionCheck = createPermCheck(globalAllow);
@ -304,8 +265,6 @@ export class UserController extends Controller {
@Query() status?: Status,
@Query() responsibleDistrictId?: string,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.getUserByCriteria(
req,
@ -317,8 +276,6 @@ export class UserController extends Controller {
status,
responsibleDistrictId,
activeBranchOnly,
startDate,
endDate,
);
}
@ -334,8 +291,6 @@ export class UserController extends Controller {
@Query() status?: Status,
@Query() responsibleDistrictId?: string,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body()
body?: {
userId?: string[];
@ -361,12 +316,12 @@ export class UserController extends Controller {
const where = {
OR: queryOrNot<Prisma.UserWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
...whereAddressQuery(query),
]),
AND: {
@ -392,21 +347,17 @@ export class UserController extends Controller {
: {
some: {
branch: {
OR: responsibleDistrictId
? permissionCondCompany(req.user, { activeOnly: activeBranchOnly }) // NOTE: when pass responsibleDistrictId should see all user not only to current branch
: permissionCond(req.user, { activeOnly: activeBranchOnly }),
OR: permissionCond(req.user, { activeOnly: activeBranchOnly }),
},
},
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.UserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.user.findMany({
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: {
importNationality: true,
responsibleArea: true,
province: true,
district: true,
@ -425,7 +376,6 @@ export class UserController extends Controller {
return {
result: result.map((v) => ({
...v,
importNationality: v.importNationality.map((v) => v.name),
responsibleArea: v.responsibleArea.map((v) => v.area),
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
})),
@ -440,7 +390,6 @@ export class UserController extends Controller {
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -452,11 +401,7 @@ export class UserController extends Controller {
if (!record) throw notFoundError("User");
const { importNationality, ...rest } = record;
return Object.assign(rest, {
importNationality: importNationality.map((v) => v.name),
});
return record;
}
@Post()
@ -522,8 +467,8 @@ export class UserController extends Controller {
}
const userId = await createUser(username, username, {
firstName: body.firstNameEN,
lastName: body.lastNameEN,
firstName: body.firstName,
lastName: body.lastName,
email: body.email,
requiredActions: ["UPDATE_PASSWORD"],
enabled: rest.status !== "INACTIVE",
@ -558,9 +503,6 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })),
}
: undefined,
importNationality: {
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"),
username,
userRole: role.name,
@ -716,7 +658,6 @@ export class UserController extends Controller {
const record = await prisma.user.update({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -731,10 +672,6 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })),
}
: undefined,
importNationality: {
deleteMany: {},
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"),
userRole,
province: connectOrDisconnect(provinceId),
@ -941,62 +878,3 @@ export class UserAttachmentController extends Controller {
);
}
}
@Route("api/v1/user/{userId}/signature")
@Security("keycloak")
export class UserSignatureController extends Controller {
#checkPermission(req: RequestWithUser, userId: string) {
if (req.user.sub !== userId) {
throw new HttpError(
HttpStatus.FORBIDDEN,
"You do not have permission to perform this action.",
"noPermission",
);
}
}
@Get()
async getSignature(@Request() req: RequestWithUser, @Path() userId: string) {
this.#checkPermission(req, userId);
return await getFile(fileLocation.user.signature(userId));
}
@Put()
async setSignature(
@Request() req: RequestWithUser,
@Path() userId: string,
@Body() signature?: { data: string },
) {
this.#checkPermission(req, userId);
const base64 = signature?.data;
if (base64) {
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ""), "base64");
const mime = "image/" + base64.split(";")[0].split("/")[1];
await uploadFile(fileLocation.user.signature(userId), buffer, mime);
} else {
return await setFile(fileLocation.user.signature(userId));
}
}
@Delete()
async deleteSignature(@Request() req: RequestWithUser, @Path() userId: string) {
this.#checkPermission(req, userId);
await deleteFile(fileLocation.user.signature(userId));
}
}
@Route("api/v1/user/{userId}/group")
@Tags("User")
@Security("keycloak")
export class UserGroupController extends Controller {
@Get()
async getUserGroup(@Path() userId: string) {
const groupUser = await getGroupUser(userId);
if (!Array.isArray(groupUser))
throw new Error("Failed. Cannot get user group(s) data from the server.");
return groupUser;
}
}

View file

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

View file

@ -30,7 +30,6 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import {
@ -47,18 +46,15 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -87,6 +83,7 @@ export type CustomerBranchCreate = {
authorizedCapital?: string;
authorizedName?: string;
authorizedNameEN?: string;
customerName?: string;
telephoneNo: string;
@ -110,7 +107,7 @@ export type CustomerBranchCreate = {
contactName: string;
agentUserId?: string;
businessTypeId?: string;
businessType: string;
jobPosition: string;
jobDescription: string;
payDate: string;
@ -144,6 +141,7 @@ export type CustomerBranchUpdate = {
authorizedCapital?: string;
authorizedName?: string;
authorizedNameEN?: string;
customerName?: string;
telephoneNo: string;
@ -167,7 +165,7 @@ export type CustomerBranchUpdate = {
contactName?: string;
agentUserId?: string;
businessTypeId?: string;
businessType?: string;
jobPosition?: string;
jobDescription?: string;
payDate?: string;
@ -197,19 +195,18 @@ export class CustomerBranchController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeRegisBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.CustomerBranchWhereInput[]>(query, [
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ email: { contains: query } },
{ code: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]),
AND: {
@ -232,17 +229,11 @@ export class CustomerBranchController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
...filterStatus(activeRegisBranchOnly ? Status.ACTIVE : status),
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CustomerBranchWhereInput;
const [result, total] = await prisma.$transaction([
prisma.customerBranch.findMany({
orderBy: [{ code: "asc" }, { statusOrder: "asc" }, { createdAt: "asc" }],
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: {
customer: includeCustomer,
province: true,
@ -251,7 +242,6 @@ export class CustomerBranchController extends Controller {
createdBy: true,
updatedBy: true,
_count: true,
businessType: true,
},
where,
take: pageSize,
@ -267,11 +257,6 @@ export class CustomerBranchController extends Controller {
@Security("keycloak")
async getById(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: {
customer: true,
province: true,
@ -279,7 +264,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true,
createdBy: true,
updatedBy: true,
businessType: true,
},
where: { id: branchId },
});
@ -301,15 +285,13 @@ export class CustomerBranchController extends Controller {
@Query() visa?: boolean,
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]),
AND: {
@ -318,7 +300,6 @@ export class CustomerBranchController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
@ -362,11 +343,6 @@ export class CustomerBranchController extends Controller {
include: branchRelationPermInclude(req.user),
},
branch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
take: 1,
orderBy: { createdAt: "asc" },
},
@ -395,15 +371,7 @@ export class CustomerBranchController extends Controller {
(v) => (v.headOffice || v).code,
);
const {
provinceId,
districtId,
subDistrictId,
customerId,
agentUserId,
businessTypeId,
...rest
} = body;
const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
const record = await prisma.$transaction(
async (tx) => {
@ -446,7 +414,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true,
createdBy: true,
updatedBy: true,
businessType: true,
},
data: {
...rest,
@ -458,7 +425,6 @@ export class CustomerBranchController extends Controller {
province: connectOrNot(provinceId),
district: connectOrNot(districtId),
subDistrict: connectOrNot(subDistrictId),
businessType: connectOrNot(businessTypeId),
createdBy: { connect: { id: req.user.sub } },
updatedBy: { connect: { id: req.user.sub } },
},
@ -489,7 +455,6 @@ export class CustomerBranchController extends Controller {
},
},
},
businessType: true,
},
});
@ -534,15 +499,7 @@ export class CustomerBranchController extends Controller {
await permissionCheck(req.user, customer.registeredBranch);
}
const {
provinceId,
districtId,
subDistrictId,
customerId,
agentUserId,
businessTypeId,
...rest
} = body;
const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
return await prisma.customerBranch.update({
where: { id: branchId },
@ -552,7 +509,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true,
createdBy: true,
updatedBy: true,
businessType: true,
},
data: {
...rest,
@ -562,7 +518,6 @@ export class CustomerBranchController extends Controller {
province: connectOrDisconnect(provinceId),
district: connectOrDisconnect(districtId),
subDistrict: connectOrDisconnect(subDistrictId),
businessType: connectOrNot(businessTypeId),
updatedBy: { connect: { id: req.user.sub } },
},
});
@ -581,7 +536,6 @@ export class CustomerBranchController extends Controller {
},
},
},
businessType: true,
},
});
@ -634,11 +588,10 @@ export class CustomerBranchFileController extends Controller {
},
},
},
businessType: true,
},
});
if (!data) throw notFoundError("Customer Branch");
await permissionCheckCompany(user, data.customer.registeredBranch);
await permissionCheck(user, data.customer.registeredBranch);
}
@Get("attachment")

View file

@ -36,25 +36,21 @@ import {
setFile,
} from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { connectOrNot, queryOrNot, whereDateQuery } from "../utils/relation";
import { json2csv } from "json-2-csv";
import { connectOrNot, queryOrNot } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -86,6 +82,7 @@ export type CustomerCreate = {
authorizedCapital?: string;
authorizedName?: string;
authorizedNameEN?: string;
customerName?: string;
telephoneNo: string;
@ -109,7 +106,7 @@ export type CustomerCreate = {
contactName: string;
agentUserId?: string;
businessTypeId?: string | null;
businessType: string;
jobPosition: string;
jobDescription: string;
payDate: string;
@ -168,22 +165,17 @@ export class CustomerController extends Controller {
@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 where = {
OR: queryOrNot<Prisma.CustomerWhereInput[]>(query, [
{ branch: { some: { namePrefix: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { firstName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { firstNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { lastName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { lastNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { namePrefix: { contains: query } } } },
{ branch: { some: { customerName: { contains: query } } } },
{ branch: { some: { registerName: { contains: query } } } },
{ branch: { some: { registerNameEN: { contains: query } } } },
{ branch: { some: { firstName: { contains: query } } } },
{ branch: { some: { firstNameEN: { contains: query } } } },
{ branch: { some: { lastName: { contains: query } } } },
{ branch: { some: { lastNameEN: { contains: query } } } },
]),
AND: {
customerType,
@ -196,36 +188,6 @@ export class CustomerController extends Controller {
: 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),
} satisfies Prisma.CustomerWhereInput;
const [result, total] = await prisma.$transaction([
@ -235,16 +197,10 @@ export class CustomerController extends Controller {
branch: includeBranch
? {
include: {
businessType: true,
province: true,
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
}
: {
@ -253,17 +209,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
take: 1,
orderBy: { createdAt: "asc" },
},
createdBy: true,
updatedBy: true,
// businessType:true
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where,
@ -288,11 +238,6 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
orderBy: { createdAt: "asc" },
},
createdBy: true,
@ -364,11 +309,6 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
createdBy: true,
updatedBy: true,
@ -380,8 +320,6 @@ export class CustomerController extends Controller {
...v,
code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - branch.length + i}`.padStart(2, "0")}`,
codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""),
businessType: connectOrNot(v.businessTypeId),
businessTypeId: undefined,
agentUser: connectOrNot(v.agentUserId),
agentUserId: undefined,
province: connectOrNot(v.provinceId),
@ -468,11 +406,6 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
createdBy: true,
updatedBy: true,
@ -511,13 +444,7 @@ export class CustomerController extends Controller {
await deleteFolder(`customer/${customerId}`);
const data = await tx.customer.delete({
include: {
branch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
branch: true,
registeredBranch: {
include: {
headOffice: true,
@ -612,52 +539,3 @@ export class CustomerImageController extends Controller {
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",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeeCheckupPayload = {

View file

@ -30,7 +30,6 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import {
@ -42,7 +41,6 @@ import {
listFile,
setFile,
} from "../utils/minio";
import { json2csv } from "json-2-csv";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
@ -52,23 +50,17 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
const permissionCond = createPermCondition(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
const permissionCheck = createPermCheck(globalAllow);
type EmployeeCreate = {
@ -78,17 +70,16 @@ type EmployeeCreate = {
nrcNo?: string | null;
dateOfBirth?: Date | null;
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName?: string;
firstName: string;
firstNameEN: string;
middleName?: string | null;
middleNameEN?: string | null;
lastName?: string;
lastName: string;
lastNameEN: string;
addressEN: string;
@ -115,14 +106,13 @@ type EmployeeUpdate = {
nrcNo?: string | null;
dateOfBirth?: Date | null;
dateOfBirth?: Date;
gender?: string;
nationality?: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName?: string;
firstNameEN: string;
firstNameEN?: string;
middleName?: string | null;
middleNameEN?: string | null;
lastName?: string;
@ -151,18 +141,9 @@ type EmployeeUpdate = {
export class EmployeeController extends Controller {
@Get("stats")
@Security("keycloak")
async getEmployeeStats(@Request() req: RequestWithUser, @Query() customerBranchId?: string) {
async getEmployeeStats(@Query() customerBranchId?: string) {
return await prisma.employee.count({
where: {
customerBranchId,
customerBranch: {
customer: isSystem(req.user)
? undefined
: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
},
where: { customerBranchId },
});
}
@ -173,8 +154,6 @@ export class EmployeeController extends Controller {
@Query() customerBranchId?: string,
@Query() status?: Status,
@Query() query: string = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await prisma.employee
.groupBy({
@ -184,13 +163,13 @@ export class EmployeeController extends Controller {
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query, mode: "insensitive" } },
some: { number: { contains: query } },
},
},
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]),
AND: {
@ -204,7 +183,6 @@ export class EmployeeController extends Controller {
},
},
},
...whereDateQuery(startDate, endDate),
},
})
.then((res) =>
@ -230,8 +208,6 @@ export class EmployeeController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.listByCriteria(
req,
@ -246,10 +222,9 @@ export class EmployeeController extends Controller {
page,
pageSize,
activeOnly,
startDate,
endDate,
);
}
@Post("list")
@Security("keycloak")
async listByCriteria(
@ -265,8 +240,6 @@ export class EmployeeController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body()
body?: {
passport?: string[];
@ -279,13 +252,13 @@ export class EmployeeController extends Controller {
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query, mode: "insensitive" } },
some: { number: { contains: query } },
},
},
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]) ?? []),
...(queryOrNot<Prisma.EmployeeWhereInput[]>(!!body, [
@ -315,7 +288,6 @@ export class EmployeeController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
@ -392,10 +364,9 @@ export class EmployeeController extends Controller {
},
}),
]);
if (!!body.provinceId && body.provinceId !== province?.id) throw relationError("Province");
if (!!body.districtId && body.districtId !== district?.id) throw relationError("District");
if (!!body.subDistrictId && body.subDistrictId !== subDistrict?.id)
throw relationError("SubDistrict");
if (body.provinceId !== province?.id) throw relationError("Province");
if (body.districtId !== district?.id) throw relationError("District");
if (body.subDistrictId !== subDistrict?.id) throw relationError("SubDistrict");
if (!customerBranch) throw relationError("Customer Branch");
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
@ -671,7 +642,7 @@ export class EmployeeFileController extends Controller {
},
});
if (!data) throw notFoundError("Employee");
await permissionCheckCompany(user, data.customerBranch.customer.registeredBranch);
await permissionCheck(user, data.customerBranch.customer.registeredBranch);
}
@Get("image")
@ -927,55 +898,3 @@ export class EmployeeFileController extends Controller {
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",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeeOtherInfoPayload = {

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
import { filterStatus } from "../services/prisma";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
type WorkflowPayload = {
name: string;
@ -37,37 +37,20 @@ type WorkflowPayload = {
attributes?: { [key: string]: any };
responsiblePersonId?: string[];
responsibleInstitution?: string[];
responsibleGroup?: string[];
messengerByArea?: boolean;
}[];
registeredBranchId?: string;
status?: Status;
};
const MANAGE_ROLES = [
"system",
"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);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheckCompany = createPermCheck((_) => true);
@Route("api/v1/workflow-template")
@Tags("Workflow")
@Security("keycloak")
export class FlowTemplateController extends Controller {
@Get()
@Security("keycloak")
async getFlowTemplate(
@Request() req: RequestWithUser,
@Query() page: number = 1,
@ -75,15 +58,13 @@ export class FlowTemplateController extends Controller {
@Query() status?: Status,
@Query() query = "",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot(query, [
{ name: { contains: query, mode: "insensitive" } },
{ name: { contains: query } },
{
step: {
some: { name: { contains: query, mode: "insensitive" } },
some: { name: { contains: query } },
},
},
]),
@ -93,7 +74,6 @@ export class FlowTemplateController extends Controller {
OR: permissionCondCompany(req.user, { activeOnly: true }),
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.WorkflowTemplateWhereInput;
const [result, total] = await prisma.$transaction([
prisma.workflowTemplate.findMany({
@ -106,7 +86,6 @@ export class FlowTemplateController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
orderBy: { order: "asc" },
},
@ -124,7 +103,6 @@ export class FlowTemplateController extends Controller {
step: r.step.map((v) => ({
...v,
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
responsibleGroup: v.responsibleGroup.map((group) => group.group),
})),
})),
page,
@ -134,7 +112,6 @@ export class FlowTemplateController extends Controller {
}
@Get("{templateId}")
@Security("keycloak")
async getFlowTemplateById(@Request() _req: RequestWithUser, @Path() templateId: string) {
const record = await prisma.workflowTemplate.findFirst({
include: {
@ -146,7 +123,6 @@ export class FlowTemplateController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -161,13 +137,11 @@ export class FlowTemplateController extends Controller {
step: record.step.map((v) => ({
...v,
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
responsibleGroup: v.responsibleGroup.map((group) => group.group),
})),
};
}
@Post()
@Security("keycloak", MANAGE_ROLES)
async createFlowTemplate(@Request() req: RequestWithUser, @Body() body: WorkflowPayload) {
const where = {
OR: [
@ -238,9 +212,6 @@ export class FlowTemplateController extends Controller {
responsibleInstitution: {
create: v.responsibleInstitution?.map((group) => ({ group })),
},
responsibleGroup: {
create: v.responsibleGroup?.map((group) => ({ group })),
},
})),
},
},
@ -248,7 +219,6 @@ export class FlowTemplateController extends Controller {
}
@Put("{templateId}")
@Security("keycloak", MANAGE_ROLES)
async updateFlowTemplate(
@Request() req: RequestWithUser,
@Path() templateId: string,
@ -322,10 +292,6 @@ export class FlowTemplateController extends Controller {
deleteMany: {},
create: v.responsibleInstitution?.map((group) => ({ group })),
},
responsibleGroup: {
deleteMany: {},
create: v.responsibleGroup?.map((group) => ({ group })),
},
},
})),
},
@ -334,7 +300,6 @@ export class FlowTemplateController extends Controller {
}
@Delete("{templateId}")
@Security("keycloak", MANAGE_ROLES)
async deleteFlowTemplateById(@Request() req: RequestWithUser, @Path() templateId: string) {
const record = await prisma.workflowTemplate.findUnique({
where: { id: templateId },

View file

@ -17,7 +17,7 @@ import {
} from "tsoa";
import prisma from "../db";
import { isUsedError, notFoundError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
import { RequestWithUser } from "../interfaces/user";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
@ -44,68 +44,8 @@ type InstitutionPayload = {
provinceId: string;
selectedImage?: string | null;
contactName?: string;
contactEmail?: string;
contactTel?: string;
bank?: {
bankName: string;
bankBranch: string;
accountName: string;
accountNumber: string;
accountType: string;
currentlyUse: boolean;
}[];
};
type InstitutionUpdatePayload = {
name: string;
nameEN: string;
code: string;
addressEN: string;
address: string;
soi?: string | null;
soiEN?: string | null;
moo?: string | null;
mooEN?: string | null;
street?: string | null;
streetEN?: string | null;
subDistrictId: string;
districtId: string;
provinceId: string;
selectedImage?: string | null;
contactName?: string;
contactEmail?: string;
contactTel?: string;
bank?: {
id?: string;
bankName: string;
bankBranch: string;
accountName: string;
accountNumber: string;
accountType: string;
currentlyUse: boolean;
}[];
};
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
@Route("api/v1/institution")
@Tags("Institution")
export class InstitutionController extends Controller {
@ -119,19 +59,8 @@ export class InstitutionController extends Controller {
@Query() status?: Status,
@Query() activeOnly?: boolean,
@Query() group?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.getInstitutionListByCriteria(
query,
page,
pageSize,
status,
activeOnly,
group,
startDate,
endDate,
);
return this.getInstitutionListByCriteria(query, page, pageSize, status, activeOnly, group);
}
@Post("list")
@ -144,8 +73,6 @@ export class InstitutionController extends Controller {
@Query() status?: Status,
@Query() activeOnly?: boolean,
@Query() group?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body()
body?: {
group?: string[];
@ -155,10 +82,9 @@ export class InstitutionController extends Controller {
...filterStatus(activeOnly ? Status.ACTIVE : status),
group: body?.group ? { in: body.group } : group,
OR: queryOrNot<Prisma.InstitutionWhereInput[]>(query, [
{ name: { contains: query, mode: "insensitive" } },
{ name: { contains: query } },
{ code: { contains: query, mode: "insensitive" } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.InstitutionWhereInput;
const [result, total] = await prisma.$transaction([
@ -168,7 +94,6 @@ export class InstitutionController extends Controller {
province: true,
district: true,
subDistrict: true,
bank: true,
},
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
take: pageSize,
@ -189,21 +114,19 @@ export class InstitutionController extends Controller {
province: true,
district: true,
subDistrict: true,
bank: true,
},
where: { id: institutionId, group },
});
}
@Post()
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
@OperationId("createInstitution")
async createInstitution(
@Body()
body: InstitutionPayload & {
status?: Status;
},
@Request() req: RequestWithUser,
) {
return await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
@ -218,78 +141,33 @@ export class InstitutionController extends Controller {
});
return await tx.institution.create({
include: {
bank: true,
createdBy: true,
updatedBy: true,
},
data: {
...body,
code: `${body.code}${last.value.toString().padStart(5, "0")}`,
group: body.code,
bank: {
createMany: {
data: body.bank ?? [],
},
},
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
},
});
});
}
@Put("{institutionId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
@OperationId("updateInstitution")
async updateInstitution(
@Path() institutionId: string,
@Body()
body: InstitutionUpdatePayload & {
body: InstitutionPayload & {
status?: "ACTIVE" | "INACTIVE";
},
) {
const { bank } = body;
return await prisma.$transaction(async (tx) => {
const listDeleted = bank
? await tx.institutionBank.findMany({
where: {
id: { not: { in: bank.flatMap((v) => (!!v.id ? v.id : [])) } },
institutionId,
},
})
: [];
await Promise.all(
listDeleted.map((v) => deleteFile(fileLocation.institution.bank(v.institutionId, v.id))),
);
return await prisma.institution.update({
include: {
bank: true,
},
where: { id: institutionId },
data: {
...body,
statusOrder: +(body.status === "INACTIVE"),
bank: bank
? {
deleteMany:
listDeleted.length > 0 ? { id: { in: listDeleted.map((v) => v.id) } } : undefined,
upsert: bank.map((v) => ({
where: { id: v.id || "" },
create: { ...v, id: undefined },
update: v,
})),
}
: undefined,
},
});
return await prisma.institution.update({
where: { id: institutionId },
data: { ...body, statusOrder: +(body.status === "INACTIVE") },
});
}
@Delete("{institutionId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
@OperationId("deleteInstitution")
async deleteInstitution(@Path() institutionId: string) {
return await prisma.$transaction(async (tx) => {
@ -307,18 +185,9 @@ export class InstitutionController extends Controller {
throw isUsedError("Institution");
}
const data = await tx.institution.delete({
include: {
bank: true,
},
return await tx.institution.delete({
where: { id: institutionId },
});
await Promise.all([
...data.bank.map((v) => deleteFile(fileLocation.institution.bank(institutionId, v.id))),
]);
return data;
});
}
}
@ -361,7 +230,7 @@ export class InstitutionFileController extends Controller {
}
@Put("image/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async putImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -375,7 +244,7 @@ export class InstitutionFileController extends Controller {
}
@Delete("image/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async delImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -405,7 +274,7 @@ export class InstitutionFileController extends Controller {
}
@Put("attachment/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async putAttachment(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -416,7 +285,7 @@ export class InstitutionFileController extends Controller {
}
@Delete("attachment/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async delAttachment(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -425,49 +294,4 @@ export class InstitutionFileController extends Controller {
await this.checkPermission(req.user, institutionId);
return await deleteFile(fileLocation.institution.attachment(institutionId, name));
}
@Get("bank-qr/{bankId}")
async getBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@Path() bankId: string,
) {
return req.res?.redirect(await getFile(fileLocation.institution.bank(institutionId, bankId)));
}
@Head("bank-qr/{bankId}")
async headBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@Path() bankId: string,
) {
return req.res?.redirect(
await getPresigned("head", fileLocation.institution.bank(institutionId, bankId)),
);
}
@Put("bank-qr/{bankId}")
@Security("keycloak", MANAGE_ROLES)
async putBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@Path() bankId: string,
) {
if (!req.headers["content-type"]?.startsWith("image/")) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Not a valid image.", "notValidImage");
}
await this.checkPermission(req.user, institutionId);
return req.res?.redirect(await setFile(fileLocation.institution.bank(institutionId, bankId)));
}
@Delete("bank-qr/{bankId}")
@Security("keycloak", MANAGE_ROLES)
async delBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@Path() bankId: string,
) {
await this.checkPermission(req.user, institutionId);
return await deleteFile(fileLocation.institution.bank(institutionId, bankId));
}
}

View file

@ -21,7 +21,6 @@ import {
createPermCondition,
} from "../services/permission";
import { PaymentStatus } from "../generated/kysely/types";
import { whereDateQuery } from "../utils/relation";
type InvoicePayload = {
quotationId: string;
@ -29,23 +28,14 @@ type InvoicePayload = {
installmentNo: number[];
};
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
@Route("/api/v1/invoice")
@ -105,24 +95,23 @@ export class InvoiceController extends Controller {
@Query() quotationId?: string,
@Query() debitNoteId?: string,
@Query() pay?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where: Prisma.InvoiceWhereInput = {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
@ -143,7 +132,6 @@ export class InvoiceController extends Controller {
OR: permissionCondCompany(req.user),
},
},
...whereDateQuery(startDate, endDate),
};
const [result, total] = await prisma.$transaction([
@ -192,7 +180,7 @@ export class InvoiceController extends Controller {
@Post()
@OperationId("createInvoice")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async createInvoice(@Request() req: RequestWithUser, @Body() body: InvoicePayload) {
const [quotation] = await prisma.$transaction([
prisma.quotation.findUnique({
@ -231,16 +219,6 @@ export class InvoiceController extends Controller {
quotationStatus: "PaymentInProcess",
},
});
await tx.notification.create({
data: {
title: "ใบแจ้งหนี้ใหม่ / New Invoice",
detail: "รหัส / code : " + record.code,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "branch_accountant" } },
},
});
return await tx.invoice.create({
data: {
quotationId: body.quotationId,

View file

@ -11,7 +11,6 @@ import {
Security,
Tags,
Query,
UploadedFile,
} from "tsoa";
import { Prisma, Product, Status } from "@prisma/client";
@ -26,27 +25,22 @@ import {
} from "../services/permission";
import { isSystem } from "../utils/keycloak";
import { filterStatus } from "../services/prisma";
import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } from "../utils/minio";
import { deleteFile, fileLocation, getFile, listFile, setFile } from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import spreadsheet from "../utils/spreadsheet";
import flowAccount from "../services/flowaccount";
import { json2csv } from "json-2-csv";
import { queryOrNot } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -78,7 +72,6 @@ type ProductCreate = {
type ProductUpdate = {
status?: "ACTIVE" | "INACTIVE";
code?: string;
name?: string;
detail?: string;
process?: number;
@ -146,8 +139,6 @@ export class ProductController extends Controller {
@Query() orderField?: keyof Product,
@Query() orderBy?: "asc" | "desc",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
// NOTE: will be used to scope product within product group that is shared between branch but not company when select shared product if user is system
const targetGroup =
@ -163,8 +154,8 @@ export class ProductController extends Controller {
const where = {
OR: queryOrNot<Prisma.ProductWhereInput[]>(query, [
{ name: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
{ name: { contains: query } },
{ detail: { contains: query } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: {
@ -203,7 +194,6 @@ export class ProductController extends Controller {
: []),
],
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ProductWhereInput;
const [result, total] = await prisma.$transaction([
@ -302,21 +292,13 @@ export class ProductController extends Controller {
},
update: { value: { increment: 1 } },
});
const listId = await flowAccount.createProducts(
`${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
body,
);
return await tx.product.create({
return await prisma.product.create({
include: {
createdBy: true,
updatedBy: true,
},
data: {
...body,
flowAccountProductIdAgentPrice: `${listId.data.productIdAgentPrice}`,
flowAccountProductIdSellPrice: `${listId.data.productIdSellPrice}`,
document: body.document
? {
createMany: { data: body.document.map((v) => ({ name: v })) },
@ -390,33 +372,8 @@ export class ProductController extends Controller {
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({
include: {
productGroup: true,
createdBy: true,
updatedBy: true,
},
@ -441,17 +398,6 @@ export class ProductController extends Controller {
});
}
await prisma.notification.create({
data: {
title: "สินค้ามีการเปลี่ยนแปลง / Product Updated",
detail: "รหัส / code : " + record.code,
groupReceiver: {
create: [{ name: "sale" }, { name: "head_of_sale" }],
},
registeredBranchId: record.productGroup.registeredBranchId,
},
});
return record;
}
@ -476,20 +422,6 @@ export class ProductController extends Controller {
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));
return await prisma.product.delete({
include: {
createdBy: true,
@ -498,146 +430,6 @@ export class ProductController extends Controller {
where: { id: productId },
});
}
@Post("import-product")
@Security("keycloak", MANAGE_ROLES)
async importProduct(
@Request() req: RequestWithUser,
@UploadedFile() file: Express.Multer.File,
@Query() productGroupId: string,
) {
if (!file?.buffer) throw notFoundError("File");
const buffer = new Uint8Array(file.buffer).buffer;
const dataFile = await spreadsheet.readExcel(buffer, {
header: true,
worksheet: "Sheet1",
});
let dataName: string[] = [];
const data = dataFile.map((item: any) => {
dataName.push(item.name);
return {
...item,
expenseType:
item.expenseType === "ค่าธรรมเนียม"
? "fee"
: item.expenseType === "ค่าบริการ"
? "serviceFee"
: "processingFee",
shared: item.shared === "ใช่" ? true : false,
price:
typeof item.price === "number"
? item.price
: +parseFloat(item.price?.replace(",", "") || "0").toFixed(6),
calcVat: item.calcVat === "ใช่" ? true : false,
vatIncluded: item.vatIncluded === "รวม" ? true : false,
agentPrice:
typeof item.agentPrice === "number"
? item.agentPrice
: +parseFloat(item.agentPrice?.replace(",", "") || "0").toFixed(6),
agentPriceCalcVat: item.agentPriceCalcVat === "ใช่" ? true : false,
agentPriceVatIncluded: item.agentPriceVatIncluded === "รวม" ? true : false,
serviceCharge:
typeof item.serviceCharge === "number"
? item.serviceCharge
: +parseFloat(item.serviceCharge?.replace(",", "") || "0").toFixed(6),
serviceChargeCalcVat: item.serviceChargeCalcVat === "ใช่" ? true : false,
serviceChargeVatIncluded: item.serviceChargeVatIncluded === "รวม" ? true : false,
};
});
const [productGroup, productSameName] = await prisma.$transaction([
prisma.productGroup.findFirst({
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
createdBy: true,
updatedBy: true,
},
where: { id: productGroupId },
}),
prisma.product.findMany({
where: {
productGroup: {
id: productGroupId,
registeredBranch: {
OR: permissionCondCompany(req.user),
},
},
name: { in: dataName },
},
}),
]);
if (!productGroup) throw relationError("Product Group");
await permissionCheck(req.user, productGroup.registeredBranch);
let dataProduct: ProductCreate[] = [];
const record = await prisma.$transaction(
async (tx) => {
const branch = productGroup.registeredBranch;
const company = (branch.headOffice || branch).code;
await Promise.all(
data.map(async (item) => {
const dataDuplicate = productSameName.some(
(v) => v.code.slice(0, -3) === item.code.toUpperCase() && v.name === item.name,
);
if (!dataDuplicate) {
const last = await tx.runningNo.upsert({
where: {
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
},
create: {
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
value: 1,
},
update: { value: { increment: 1 } },
});
dataProduct.push({
...item,
code: `${item.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
productGroupId: productGroupId,
});
}
}),
);
return await prisma.product.createManyAndReturn({
data: dataProduct,
include: {
createdBy: true,
updatedBy: true,
},
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
},
);
if (productGroup.status === "CREATED") {
await prisma.productGroup.update({
include: {
createdBy: true,
updatedBy: true,
},
where: { id: productGroupId },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
return record;
}
}
@Route("api/v1/product/{productId}")
@ -689,43 +481,3 @@ export class ProductFileController extends Controller {
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

@ -27,7 +27,7 @@ import {
} from "../services/permission";
import { filterStatus } from "../services/prisma";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
type ProductGroupCreate = {
name: string;
@ -35,7 +35,7 @@ type ProductGroupCreate = {
remark: string;
status?: Status;
shared?: boolean;
registeredBranchId?: string;
registeredBranchId: string;
};
type ProductGroupUpdate = {
@ -51,16 +51,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCond = createPermCondition((_) => true);
@ -92,13 +90,11 @@ export class ProductGroup extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.ProductGroupWhereInput[]>(query, [
{ name: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
{ name: { contains: query } },
{ detail: { contains: query } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: [
@ -109,7 +105,6 @@ export class ProductGroup extends Controller {
: { OR: permissionCond(req.user, { activeOnly }) },
},
],
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ProductGroupWhereInput;
const [result, total] = await prisma.$transaction([
@ -159,23 +154,7 @@ export class ProductGroup extends Controller {
@Post()
@Security("keycloak", MANAGE_ROLES)
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
const userAffiliatedBranch = await prisma.branch.findFirst({
include: branchRelationPermInclude(req.user),
where: body.registeredBranchId
? { id: body.registeredBranchId }
: {
user: { some: { userId: req.user.sub } },
},
});
if (!userAffiliatedBranch) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
"reqMinAffilatedBranch",
);
}
let company = await permissionCheck(req.user, userAffiliatedBranch).then(
let company = await permissionCheck(req.user, body.registeredBranchId).then(
(v) => (v.headOffice || v).code,
);
@ -199,7 +178,6 @@ export class ProductGroup extends Controller {
},
data: {
...body,
registeredBranchId: userAffiliatedBranch.id,
statusOrder: +(body.status === "INACTIVE"),
code: `G${last.value.toString().padStart(2, "0")}`,
createdByUserId: req.user.sub,

View file

@ -1,193 +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, Status } from "@prisma/client";
import {
branchRelationPermInclude,
createPermCheck,
createPermCondition,
} from "../services/permission";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
import { filterStatus } from "../services/prisma";
import { queryOrNot, whereDateQuery } from "../utils/relation";
type PropertyPayload = {
name: string;
nameEN: string;
type: Record<string, any>;
registeredBranchId?: string;
status?: Status;
};
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheckCompany = createPermCheck((_) => true);
@Route("api/v1/property")
@Tags("Property")
@Security("keycloak")
export class PropertiesController extends Controller {
@Get()
async getProperties(
@Request() req: RequestWithUser,
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() status?: Status,
@Query() query = "",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot(query, [
{ name: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
]),
AND: {
...filterStatus(activeOnly ? Status.ACTIVE : status),
registeredBranch: {
OR: permissionCondCompany(req.user, { activeOnly: true }),
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.PropertyWhereInput;
const [result, total] = await prisma.$transaction([
prisma.property.findMany({
where,
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.property.count({ where }),
]);
return {
result,
page,
pageSize,
total,
};
}
@Get("{propertyId}")
async getPropertyById(@Request() _req: RequestWithUser, @Path() propertyId: string) {
const record = await prisma.property.findFirst({
where: { id: propertyId },
orderBy: { createdAt: "asc" },
});
if (!record) throw notFoundError("Property");
return record;
}
@Post()
async createProperty(@Request() req: RequestWithUser, @Body() body: PropertyPayload) {
const where = {
OR: [{ name: { contains: body.name } }, { nameEN: { contains: body.nameEN } }],
AND: {
registeredBranch: {
OR: permissionCondCompany(req.user),
},
},
} satisfies Prisma.PropertyWhereInput;
const exists = await prisma.property.findFirst({ where });
if (exists) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Property with this name already exists",
"samePropertyNameExists",
);
}
const userAffiliatedBranch = await prisma.branch.findFirst({
include: branchRelationPermInclude(req.user),
where: body.registeredBranchId
? { id: body.registeredBranchId }
: {
user: { some: { userId: req.user.sub } },
},
});
if (!userAffiliatedBranch) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
"reqMinAffilatedBranch",
);
}
await permissionCheckCompany(req.user, userAffiliatedBranch);
return await prisma.property.create({
data: {
...body,
statusOrder: +(body.status === "INACTIVE"),
registeredBranchId: userAffiliatedBranch.id,
},
});
}
@Put("{propertyId}")
async updatePropertyById(
@Request() req: RequestWithUser,
@Path() propertyId: string,
@Body() body: PropertyPayload,
) {
const record = await prisma.property.findUnique({
where: { id: propertyId },
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
},
});
if (!record) throw notFoundError("Property");
await permissionCheckCompany(req.user, record.registeredBranch);
return await prisma.property.update({
where: { id: propertyId },
data: {
...body,
statusOrder: +(body.status === "INACTIVE"),
},
});
}
@Delete("{propertyId}")
async deletePropertyById(@Request() req: RequestWithUser, @Path() propertyId: string) {
const record = await prisma.property.findUnique({
where: { id: propertyId },
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
},
});
if (!record) throw notFoundError("Property");
await permissionCheckCompany(req.user, record.registeredBranch);
return await prisma.property.delete({
where: { id: propertyId },
});
}
}

View file

@ -4,7 +4,6 @@ import { Prisma } from "@prisma/client";
import { notFoundError } from "../utils/error";
import { RequestWithUser } from "../interfaces/user";
import { createPermCondition } from "../services/permission";
import { whereDateQuery } from "../utils/relation";
const permissionCondCompany = createPermCondition((_) => true);
@ -22,8 +21,6 @@ export class ReceiptController extends Controller {
@Query() quotationId?: string,
@Query() debitNoteId?: string,
@Query() debitNoteOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where: Prisma.PaymentWhereInput = {
paymentStatus: "PaymentSuccess",
@ -36,7 +33,6 @@ export class ReceiptController extends Controller {
},
},
},
...whereDateQuery(startDate, endDate),
};
const [result, total] = await prisma.$transaction([

View file

@ -27,31 +27,21 @@ import {
} from "../services/permission";
import { filterStatus } from "../services/prisma";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import {
deleteFile,
deleteFolder,
fileLocation,
getFile,
getPresigned,
listFile,
setFile,
} from "../utils/minio";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import { queryOrNot } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -166,8 +156,6 @@ export class ServiceController extends Controller {
@Query() fullDetail?: boolean,
@Query() activeOnly?: boolean,
@Query() shared?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
// NOTE: will be used to scope product within product group that is shared between branch but not company when select shared product if user is system
const targetGroup =
@ -183,8 +171,8 @@ export class ServiceController extends Controller {
const where = {
OR: queryOrNot<Prisma.ServiceWhereInput[]>(query, [
{ name: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
{ name: { contains: query } },
{ detail: { contains: query } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: {
@ -223,7 +211,6 @@ export class ServiceController extends Controller {
: []),
],
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ServiceWhereInput;
const [result, total] = await prisma.$transaction([
@ -486,7 +473,6 @@ export class ServiceController extends Controller {
return await tx.service.update({
include: {
productGroup: true,
createdBy: true,
updatedBy: true,
},
@ -537,17 +523,6 @@ export class ServiceController extends Controller {
});
});
await prisma.notification.create({
data: {
title: "แพคเกจมีการเปลี่ยนแปลง / Package Updated",
detail: "รหัส / code : " + record.code,
groupReceiver: {
create: [{ name: "sale" }, { name: "head_of_sale" }],
},
registeredBranchId: record.productGroup.registeredBranchId,
},
});
return record;
}
@ -573,8 +548,6 @@ export class ServiceController extends Controller {
if (record.status !== Status.CREATED) throw isUsedError("Service");
await deleteFolder(fileLocation.service.img(serviceId));
return await prisma.service.delete({
include: {
createdBy: true,

View file

@ -18,7 +18,6 @@ import prisma from "../db";
import { RequestWithUser } from "../interfaces/user";
import HttpStatus from "../interfaces/http-status";
import { isUsedError, notFoundError } from "../utils/error";
import { whereDateQuery } from "../utils/relation";
type WorkCreate = {
order: number;
@ -46,12 +45,9 @@ export class WorkController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: [{ name: { contains: query }, serviceId: baseOnly ? null : undefined }],
...whereDateQuery(startDate, endDate),
} satisfies Prisma.WorkWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -26,20 +26,11 @@ import flowAccount from "../services/flowaccount";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -110,19 +101,10 @@ export class QuotationPayment extends Controller {
}
@Put("{paymentId}")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async updatePayment(
@Path() paymentId: string,
@Body()
body: {
amount?: number;
date?: Date;
paymentStatus?: PaymentStatus;
channel?: string | null;
account?: string | null;
reference?: string | null;
},
@Request() req: RequestWithUser,
@Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus },
) {
const record = await prisma.payment.findUnique({
where: { id: paymentId },
@ -152,18 +134,7 @@ export class QuotationPayment extends Controller {
if (!record) throw notFoundError("Payment");
if (record.paymentStatus === "PaymentSuccess") {
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,
},
});
}
if (record.paymentStatus === "PaymentSuccess") return record;
return await prisma.$transaction(async (tx) => {
const current = new Date();
@ -193,7 +164,6 @@ export class QuotationPayment extends Controller {
code: lastReceipt
? `RE${year}${month}${lastReceipt.value.toString().padStart(6, "0")}`
: undefined,
updatedByUserId: req.user.sub,
},
});
@ -207,78 +177,55 @@ export class QuotationPayment extends Controller {
},
});
await tx.quotation
.update({
include: { requestData: true },
where: { id: quotation.id },
data: {
quotationStatus:
(paymentSum._sum.amount || 0) >= quotation.finalPrice
? "PaymentSuccess"
: "PaymentInProcess",
requestData: await (async () => {
if (
body.paymentStatus === "PaymentSuccess" &&
(paymentSum._sum.amount || 0) - payment.amount <= 0
) {
const lastRequest = await tx.runningNo.upsert({
where: {
key: `REQUEST_${year}${month}`,
},
create: {
key: `REQUEST_${year}${month}`,
value: quotation.worker.length,
},
update: { value: { increment: quotation.worker.length } },
});
return {
create: quotation.worker.flatMap((v, i) => {
const productEmployee = quotation.productServiceList.flatMap((item) =>
item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1
? { productServiceId: item.id }
: [],
);
await tx.quotation.update({
where: { id: quotation.id },
data: {
quotationStatus:
(paymentSum._sum.amount || 0) >= quotation.finalPrice
? "PaymentSuccess"
: "PaymentInProcess",
requestData: await (async () => {
if (
body.paymentStatus === "PaymentSuccess" &&
(paymentSum._sum.amount || 0) - payment.amount <= 0
) {
const lastRequest = await tx.runningNo.upsert({
where: {
key: `REQUEST_${year}${month}`,
},
create: {
key: `REQUEST_${year}${month}`,
value: quotation.worker.length,
},
update: { value: { increment: quotation.worker.length } },
});
return {
create: quotation.worker.flatMap((v, i) => {
const productEmployee = quotation.productServiceList.flatMap((item) =>
item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1
? { productServiceId: item.id }
: [],
);
if (productEmployee.length <= 0) return [];
if (productEmployee.length <= 0) return [];
return {
code: `TR${year}${month}${(lastRequest.value - quotation.worker.length + i + 1).toString().padStart(6, "0")}`,
employeeId: v.employeeId,
requestWork: {
create: quotation.productServiceList.flatMap((item) =>
item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1
? { productServiceId: item.id }
: [],
),
},
};
}),
};
}
})(),
},
})
.then(async (res) => {
if (quotation.quotationStatus !== res.quotationStatus)
await tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + res.code + " " + res.quotationStatus,
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 {
code: `TR${year}${month}${(lastRequest.value - quotation.worker.length + i + 1).toString().padStart(6, "0")}`,
employeeId: v.employeeId,
requestWork: {
create: quotation.productServiceList.flatMap((item) =>
item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1
? { productServiceId: item.id }
: [],
),
},
};
}),
};
}
})(),
},
});
return payment;
});

View file

@ -25,7 +25,7 @@ import {
import { isSystem } from "../utils/keycloak";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { precisionRound } from "../utils/arithmetic";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
@ -55,14 +55,13 @@ type QuotationCreate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
middleName?: string;
middleNameEN?: string;
lastName: string;
lastNameEN?: string;
lastNameEN: string;
}
)[];
@ -84,8 +83,6 @@ type QuotationCreate = {
installmentNo?: number;
workerIndex?: number[];
}[];
sellerId?: string;
};
type QuotationUpdate = {
@ -115,15 +112,14 @@ type QuotationUpdate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName?: string;
firstName: string;
firstNameEN: string;
middleName?: string;
middleNameEN?: string;
lastName?: string;
lastNameEN?: string;
lastName: string;
lastNameEN: string;
}
)[];
@ -144,8 +140,6 @@ type QuotationUpdate = {
installmentNo?: number;
workerIndex?: number[];
}[];
sellerId?: string;
};
const VAT_DEFAULT = config.vat;
@ -154,16 +148,15 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCheckCompany = createPermCheck((_) => true);
@ -175,21 +168,13 @@ const permissionCond = createPermCondition(globalAllow);
export class QuotationController extends Controller {
@Get("stats")
@Security("keycloak")
async getQuotationStats(
@Request() req: RequestWithUser,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
async getProductStats(@Request() req: RequestWithUser) {
const result = await prisma.quotation.groupBy({
_count: true,
by: "quotationStatus",
where: {
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
isDebitNote: false,
createdAt: {
gte: startDate,
lte: endDate,
},
},
});
@ -209,31 +194,28 @@ export class QuotationController extends Controller {
@Query() urgentFirst?: boolean,
@Query() includeRegisteredBranch?: boolean,
@Query() hasCancel?: boolean,
@Query() cancelIncludeDebitNote?: boolean,
@Query() forDebitNote?: boolean,
@Query() code?: string,
@Query() query = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
@Query() sellerId?: string,
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
]),
isDebitNote: hasCancel && cancelIncludeDebitNote ? undefined : false,
isDebitNote: false,
code,
payCondition,
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
@ -262,8 +244,6 @@ export class QuotationController extends Controller {
},
}
: undefined,
...whereDateQuery(startDate, endDate),
sellerId: sellerId,
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -421,7 +401,7 @@ export class QuotationController extends Controller {
}
@Post()
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
const ids = {
employee: body.worker.filter((v) => typeof v === "string"),
@ -473,7 +453,7 @@ export class QuotationController extends Controller {
const { productServiceList: _productServiceList, worker: _worker, ...rest } = body;
const ret = await prisma.$transaction(async (tx) => {
return await prisma.$transaction(async (tx) => {
const nonExistEmployee = body.worker.filter((v) => typeof v !== "string");
const lastEmployee = await tx.runningNo.upsert({
where: {
@ -527,15 +507,16 @@ export class QuotationController extends Controller {
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = body.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound(
const finalPriceWithVat = precisionRound(
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)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
: 0;
return {
order: i + 1,
productId: v.productId,
@ -556,13 +537,13 @@ export class QuotationController extends Controller {
const price = list.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
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(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -657,28 +638,10 @@ export class QuotationController extends Controller {
},
});
});
await prisma.notification.create({
data: {
title: "ใบเสนอราคาใหม่ / New Quotation",
detail: "รหัส / code : " + ret.code,
registeredBranchId: ret.registeredBranchId,
groupReceiver: {
create: [
{ name: "sale" },
{ name: "head_of_sale" },
{ name: "accountant" },
{ name: "branch_accountant" },
],
},
},
});
return ret;
}
@Put("{quotationId}")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async editQuotation(
@Request() req: RequestWithUser,
@Path() quotationId: string,
@ -696,7 +659,6 @@ export class QuotationController extends Controller {
},
},
},
productServiceList: true,
},
where: { id: quotationId, isDebitNote: false },
});
@ -814,14 +776,14 @@ export class QuotationController extends Controller {
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = record.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound(
const finalPriceWithVat = precisionRound(
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)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
: 0;
return {
@ -844,13 +806,15 @@ export class QuotationController extends Controller {
const price = list?.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
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(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -867,18 +831,6 @@ export class QuotationController extends Controller {
},
);
const changed = list?.some((lhs) => {
const found = record.productServiceList.find((rhs) => {
return (
lhs.serviceId === rhs.serviceId &&
lhs.workId === rhs.workId &&
lhs.productId === rhs.productId &&
lhs.amount === rhs.amount &&
precisionRound(lhs.pricePerUnit, 6) === precisionRound(rhs.pricePerUnit, 6)
);
});
return !found;
});
await Promise.all([
tx.service.updateMany({
where: { id: { in: ids.service }, status: Status.CREATED },
@ -888,32 +840,8 @@ export class QuotationController extends Controller {
where: { id: { in: ids.product }, status: Status.CREATED },
data: { status: Status.ACTIVE },
}),
changed &&
tx.notification.create({
data: {
title: "ใบเสนอราคามีการเปลี่ยนแปลง / Quotation Detail Changes",
detail:
"รหัส / code : " + record.code + " มีการเปลี่ยนแปลงของสินค้า / Product Updated",
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: [{ name: "sale" }, { name: "head_of_sale" }] },
},
}),
]);
if (customerBranch) {
await tx.customerBranch.update({
where: { id: customerBranch.id },
data: {
customer: {
update: {
status: Status.ACTIVE,
},
},
status: Status.ACTIVE,
},
});
}
return await tx.quotation.update({
include: {
productServiceList: {
@ -1035,7 +963,6 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -1058,7 +985,6 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -1198,53 +1124,41 @@ export class QuotationActionController extends Controller {
},
update: { value: { increment: quotation.worker.length } },
});
await tx.quotation
.update({
include: { requestData: true },
where: { id: quotationId, isDebitNote: false },
data: {
quotationStatus: QuotationStatus.PaymentSuccess, // NOTE: change back if already complete or canceled
worker: {
createMany: {
data: rearrange
.filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId))
.map((v, i) => ({
no: quotation._count.worker + i + 1,
employeeId: v.workerId,
})),
},
await tx.quotation.update({
where: { id: quotationId, isDebitNote: false },
data: {
quotationStatus: QuotationStatus.PaymentSuccess, // NOTE: change back if already complete or canceled
worker: {
createMany: {
data: rearrange
.filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId))
.map((v, i) => ({
no: quotation._count.worker + i + 1,
employeeId: v.workerId,
})),
},
requestData:
quotation.quotationStatus === "PaymentInProcess" ||
quotation.quotationStatus === "PaymentSuccess"
? {
create: rearrange
.filter(
(lhs) =>
!quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId) &&
lhs.productServiceId.length > 0,
)
.map((v, i) => ({
code: `TR${year}${month}${(lastRequest.value - quotation._count.worker + i + 1).toString().padStart(6, "0")}`,
employeeId: v.workerId,
requestWork: {
create: v.productServiceId.map((v) => ({ productServiceId: v })),
},
})),
}
: undefined,
},
})
.then(async (ret) => {
await prisma.notification.create({
data: {
title: "รายการคำขอใหม่ / New Request",
detail: "รหัส / code : " + ret.requestData.map((v) => v.code).join(", "),
registeredBranchId: ret.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
});
requestData:
quotation.quotationStatus === "PaymentInProcess" ||
quotation.quotationStatus === "PaymentSuccess"
? {
create: rearrange
.filter(
(lhs) =>
!quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId) &&
lhs.productServiceId.length > 0,
)
.map((v, i) => ({
code: `TR${year}${month}${(lastRequest.value - quotation._count.worker + i + 1).toString().padStart(6, "0")}`,
employeeId: v.workerId,
requestWork: {
create: v.productServiceId.map((v) => ({ productServiceId: v })),
},
})),
}
: undefined,
},
});
});
}
}

View file

@ -27,12 +27,9 @@ import {
createPermCheck,
createPermCondition,
} from "../services/permission";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
import { notFoundError } from "../utils/error";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { getGroupUser } from "../services/keycloak";
// User in company can edit.
const permissionCheck = createPermCheck((_) => true);
@ -48,7 +45,11 @@ export class RequestDataController extends Controller {
async getRequestDataStats(@Request() req: RequestWithUser) {
const where = {
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
customerBranch: {
customer: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
},
} satisfies Prisma.RequestDataWhereInput;
@ -81,53 +82,45 @@ export class RequestDataController extends Controller {
@Query() requestDataStatus?: RequestDataStatus,
@Query() quotationId?: string,
@Query() code?: string,
@Query() incomplete?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ quotation: { code: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
},
{
employee: {
OR: [
{
employeePassport: {
some: { number: { contains: query, mode: "insensitive" } },
some: { number: { contains: query } },
},
},
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
]),
code,
requestDataStatus: incomplete
? {
notIn: [RequestDataStatus.Completed, RequestDataStatus.Canceled],
}
: requestDataStatus,
requestDataStatus,
requestWork: responsibleOnly
? {
some: {
@ -136,24 +129,9 @@ export class RequestDataController extends Controller {
workflow: {
step: {
some: {
OR: [
{
responsiblePerson: {
some: { userId: req.user.sub },
},
},
{
responsibleGroup: {
some: {
group: {
in: await getGroupUser(req.user.sub).then((r) =>
r.map(({ name }: { name: string }) => name),
),
},
},
},
},
],
responsiblePerson: {
some: { userId: req.user.sub },
},
},
},
},
@ -166,7 +144,6 @@ export class RequestDataController extends Controller {
id: quotationId,
registeredBranch: { OR: permissionCond(req.user) },
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.RequestDataWhereInput;
const [result, total] = await prisma.$transaction([
@ -189,7 +166,6 @@ export class RequestDataController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -208,20 +184,6 @@ export class RequestDataController extends Controller {
employeePassport: {
orderBy: { expireDate: "desc" },
},
customerBranch: {
include: {
province: {
include: {
employmentOffice: true,
},
},
district: {
include: {
employmentOffice: true,
},
},
},
},
},
},
},
@ -232,24 +194,7 @@ export class RequestDataController extends Controller {
prisma.requestData.count({ where }),
]);
const dataRequestData = result.map((item) => {
const employee = item.employee;
const dataOffice =
employee.customerBranch.district?.employmentOffice.at(0) ??
employee.customerBranch.province?.employmentOffice.at(0);
return {
...item,
dataOffice,
};
});
return {
result: dataRequestData,
page,
pageSize,
total,
};
return { result, page, pageSize, total };
}
@Get("{requestDataId}")
@ -261,9 +206,6 @@ export class RequestDataController extends Controller {
quotation: {
include: {
customerBranch: { include: { customer: true } },
debitNoteQuotation: {
select: { code: true },
},
invoice: {
include: {
installments: true,
@ -287,157 +229,14 @@ export class RequestDataController extends Controller {
return record;
}
@Post("update-messenger")
@Security("keycloak")
async updateRequestData(
@Request() req: RequestWithUser,
@Body()
body: {
defaultMessengerId: string;
requestDataId: string[];
},
) {
if (body.requestDataId.length === 0) return;
return await prisma.$transaction(async (tx) => {
const record = await tx.requestData.updateManyAndReturn({
where: {
id: { in: body.requestDataId },
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
},
},
data: {
defaultMessengerId: body.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];
});
}
}
@Route("/api/v1/request-data/{requestDataId}")
@Tags("Request List")
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")
@Security("keycloak")
async rejectRequestCancel(
@Request() req: RequestWithUser,
@Path() requestDataId: string,
@Body()
body: {
reason?: string;
},
) {
const result = await prisma.requestData.updateManyAndReturn({
where: {
id: requestDataId,
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
},
},
data: {
rejectRequestCancel: true,
rejectRequestCancelReason: body.reason || "",
},
});
if (result.length <= 0) throw notFoundError("Request Data");
return result[0];
}
@Post("request-work/{requestWorkId}/reject-request-cancel")
@Security("keycloak")
async rejectWorkRequestCancel(
@Request() req: RequestWithUser,
@Path() requestWorkId: string,
@Body()
body: {
reason?: string;
},
) {
const result = await prisma.requestWork.updateManyAndReturn({
where: {
id: requestWorkId,
request: {
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
},
},
},
data: {
rejectRequestCancel: true,
rejectRequestCancelReason: body.reason || "",
},
});
if (result.length <= 0) throw notFoundError("Request Data");
return result[0];
}
@Post("cancel")
@Security("keycloak")
async cancelRequestData(@Request() req: RequestWithUser, @Path() requestDataId: string) {
const result = await prisma.requestData.findFirst({
where: {
id: requestDataId,
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
},
},
include: {
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
},
},
});
if (!result) throw notFoundError("Request Data");
async cancelRequestData(@Path() requestDataId: string) {
await prisma.$transaction(async (tx) => {
const workStepCondition = {
requestWork: { requestDataId },
@ -466,405 +265,23 @@ export class RequestDataActionController extends Controller {
}),
]);
await Promise.all([
tx.quotation
.updateManyAndReturn({
where: {
requestData: {
every: { requestDataStatus: RequestDataStatus.Canceled },
},
},
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.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" } },
},
}),
),
);
}),
tx.taskOrder
.updateManyAndReturn({
where: {
taskList: {
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,
},
],
};
await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
});
}
@Put("request-work/step-status/{step}")
@Security("keycloak")
async updateRequestWorkDataStepStatus(
@Path() requestDataId: string,
@Path() step: number,
@Body()
payload: {
workStatus?: RequestWorkStatus;
requestWorkId: string;
attributes?: Record<string, any>;
customerDuty?: boolean | null;
customerDutyCost?: number | null;
companyDuty?: boolean | null;
companyDutyCost?: number | null;
individualDuty?: boolean | null;
individualDutyCost?: number | null;
responsibleUserLocal?: boolean | null;
responsibleUserId?: string | null;
}[],
) {
payload.forEach((item) => {
if (!item.responsibleUserId) item.responsibleUserId = undefined;
});
return await prisma.$transaction(async (tx) => {
const workStepCondition = await tx.requestData.findFirst({
where: {
id: requestDataId,
},
select: { id: true },
});
if (!workStepCondition) {
throw new Error("RequestWork not found requestDataId");
}
const data = await Promise.all(
payload.map(async (item) => {
return await tx.requestWorkStepStatus.upsert({
include: {
requestWork: {
include: {
request: true,
},
},
},
where: {
step_requestWorkId: {
step: step,
requestWorkId: item.requestWorkId,
},
requestWork: {
request: { id: requestDataId },
},
},
create: {
...item,
step: step,
requestWorkId: item.requestWorkId,
},
update: item,
});
}),
);
if (
data.some((item) => {
return (
item.workStatus === "Ready" && item.requestWork.request.requestDataStatus === "Pending"
);
})
) {
await tx.requestData.updateMany({
tx.quotation.updateMany({
where: {
id: requestDataId,
requestDataStatus: "Pending",
},
data: { requestDataStatus: "Ready" },
});
}
if (
data.some((item) => {
return (
item.workStatus === "InProgress" ||
item.workStatus === "Waiting" ||
item.workStatus === "Validate" ||
item.workStatus === "Completed" ||
item.workStatus === "Ended"
);
})
) {
await tx.requestData.update({
where: {
id: requestDataId,
},
data: { requestDataStatus: "InProgress" },
});
}
if (
data.some((item) => {
return item.workStatus === "Canceled";
})
) {
const dataId = data.map((itemId) => itemId.requestWork.id);
await tx.task.updateMany({
where: {
taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] },
requestWorkStep: {
step: step,
requestWorkId: { in: dataId },
workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] },
},
},
data: { taskStatus: TaskStatus.Canceled },
});
await Promise.all([
tx.quotation
.updateManyAndReturn({
where: {
quotationStatus: { not: QuotationStatus.Canceled },
requestData: {
every: { requestDataStatus: RequestDataStatus.Canceled },
},
},
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await 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" } },
},
}),
),
);
}),
tx.taskOrder.updateMany({
where: {
taskList: {
every: { taskStatus: TaskStatus.Canceled },
},
},
data: { taskOrderStatus: TaskStatus.Canceled },
}),
]);
}
const requestList = await tx.requestData.findMany({
include: {
requestWork: {
include: {
productService: {
include: {
product: true,
service: true,
work: {
include: { productOnWork: true },
},
},
},
stepStatus: true,
},
},
},
where: {
requestWork: {
some: {
requestDataId: requestDataId,
},
},
},
});
const completed: string[] = [];
requestList.forEach((item) => {
const completeCheck = item.requestWork.every((work) => {
const stepCount =
work.productService.work?.productOnWork.find(
(v) => v.productId === work.productService.productId,
)?.stepCount || 0;
const completeCount = work.stepStatus.filter(
(v) =>
v.workStatus === RequestWorkStatus.Completed ||
v.workStatus === RequestWorkStatus.Ended ||
v.workStatus === RequestWorkStatus.Canceled,
).length;
// NOTE: step found then check if complete count equals step count
if (stepCount === completeCount && completeCount > 0) return true;
// NOTE: likely no step found and completed at least one
if (stepCount === 0 && completeCount > 0) return true;
});
if (completeCheck) completed.push(item.id);
});
await tx.requestData.updateMany({
where: { id: { in: completed } },
data: { requestDataStatus: RequestDataStatus.Completed },
});
await tx.quotation
.updateManyAndReturn({
where: {
quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
},
requestData: {
every: {
requestDataStatus: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
},
},
every: { requestDataStatus: RequestDataStatus.Canceled },
},
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
}),
tx.taskOrder.updateMany({
where: {
taskList: {
every: { taskStatus: TaskStatus.Canceled },
},
},
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
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);
return data;
data: { taskOrderStatus: TaskStatus.Canceled },
}),
]);
});
}
}
@ -872,14 +289,6 @@ export class RequestDataActionController extends Controller {
@Route("/api/v1/request-work")
@Tags("Request List")
export class RequestListController extends Controller {
async #getLineToken() {
if (!process.env.LINE_MESSAGING_API_TOKEN) {
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
}
return process.env.LINE_MESSAGING_API_TOKEN;
}
@Get()
@Security("keycloak")
async getRequestWork(
@ -984,7 +393,6 @@ export class RequestListController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -1045,7 +453,6 @@ export class RequestListController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -1154,21 +561,6 @@ export class RequestListController extends Controller {
update: payload,
});
if (record.responsibleUserId === null) {
await tx.requestWorkStepStatus.update({
where: {
step_requestWorkId: {
step: step,
requestWorkId,
},
responsibleUserId: null,
},
data: {
responsibleUserId: record.requestWork?.request.defaultMessengerId,
},
});
}
switch (payload.workStatus) {
case "Ready":
if (record.requestWork.request.requestDataStatus === "Pending") {
@ -1206,31 +598,14 @@ export class RequestListController extends Controller {
data: { taskStatus: TaskStatus.Canceled },
});
await Promise.all([
tx.quotation
.updateManyAndReturn({
where: {
quotationStatus: { not: QuotationStatus.Canceled },
requestData: {
every: { requestDataStatus: RequestDataStatus.Canceled },
},
tx.quotation.updateMany({
where: {
requestData: {
every: { requestDataStatus: RequestDataStatus.Canceled },
},
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.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" } },
},
}),
),
);
}),
},
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
}),
tx.taskOrder.updateMany({
where: {
taskList: {
@ -1298,109 +673,19 @@ export class RequestListController extends Controller {
where: { id: { in: completed } },
data: { requestDataStatus: RequestDataStatus.Completed },
});
await tx.quotation
.updateManyAndReturn({
where: {
quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
},
AND: [
{
requestData: {
every: {
requestDataStatus: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
},
},
},
},
{
requestData: {
some: {},
},
},
],
await tx.quotation.updateMany({
where: {
quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
requestData: {
every: {
requestDataStatus: { in: [RequestDataStatus.Canceled, 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.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),
});
});
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
});
return record;
});
}

View file

@ -42,23 +42,13 @@ import {
listFile,
setFile,
} from "../utils/minio";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"data_entry",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -70,14 +60,11 @@ const permissionCheckCompany = createPermCheck((_) => true);
@Tags("Task Order")
export class TaskController extends Controller {
@Get("stats")
@Security("keycloak")
async getTaskOrderStats(@Request() req: RequestWithUser) {
async getTaskOrderStats() {
const task = await prisma.taskOrder.groupBy({
where: { registeredBranch: { OR: permissionCondCompany(req.user) } },
by: ["taskOrderStatus"],
_count: true,
});
return task.reduce<Record<TaskOrderStatus, number>>(
(a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }),
{
@ -99,8 +86,6 @@ export class TaskController extends Controller {
@Query() pageSize = 30,
@Query() assignedByUserId?: string,
@Query() taskOrderStatus?: TaskOrderStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.getTaskOrderListByCriteria(
req,
@ -109,8 +94,6 @@ export class TaskController extends Controller {
pageSize,
assignedByUserId,
taskOrderStatus,
startDate,
endDate,
);
}
@ -123,8 +106,6 @@ export class TaskController extends Controller {
@Query() pageSize = 30,
@Query() assignedUserId?: string,
@Query() taskOrderStatus?: TaskOrderStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: { code?: string[] },
) {
const where = {
@ -140,11 +121,10 @@ export class TaskController extends Controller {
code: body?.code ? { in: body.code } : undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query, mode: "insensitive" } },
{ contactName: { contains: query, mode: "insensitive" } },
{ contactTel: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query } },
{ contactName: { contains: query } },
{ contactTel: { contains: query } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.TaskOrderWhereInput;
const [result, total] = await prisma.$transaction([
@ -213,7 +193,6 @@ export class TaskController extends Controller {
step: {
include: {
value: true,
responsibleGroup: true,
responsiblePerson: {
include: { user: true },
},
@ -265,12 +244,6 @@ export class TaskController extends Controller {
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) => {
const last = await tx.runningNo.upsert({
where: {
@ -320,8 +293,8 @@ export class TaskController extends Controller {
if (updated.count !== taskList.length) {
throw new HttpError(
HttpStatus.PRECONDITION_FAILED,
"all request work to issue task order must be in ready state.",
"requestworkmustready",
"All request work to issue task order must be in ready state.",
"requestWorkMustReady",
);
}
await tx.institution.updateMany({
@ -344,51 +317,49 @@ export class TaskController extends Controller {
where: { OR: taskList },
});
return await tx.taskOrder
.create({
include: {
taskList: {
include: {
requestWorkStep: {
include: {
requestWork: {
include: {
request: {
include: {
employee: true,
quotation: {
include: {
customerBranch: {
include: {
customer: true,
},
return await tx.taskOrder.create({
include: {
taskList: {
include: {
requestWorkStep: {
include: {
requestWork: {
include: {
request: {
include: {
employee: true,
quotation: {
include: {
customerBranch: {
include: {
customer: true,
},
},
},
},
},
productService: {
include: {
service: {
include: {
workflow: {
include: {
step: {
include: {
value: true,
responsiblePerson: {
include: { user: true },
},
responsibleInstitution: true,
},
productService: {
include: {
service: {
include: {
workflow: {
include: {
step: {
include: {
value: true,
responsiblePerson: {
include: { user: true },
},
responsibleInstitution: true,
},
},
},
},
},
work: true,
product: true,
},
work: true,
product: true,
},
},
},
@ -396,30 +367,20 @@ export class TaskController extends Controller {
},
},
},
institution: true,
createdBy: true,
},
data: {
...rest,
code,
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
registeredBranchId: userAffiliatedBranch.id,
createdByUserId: req.user.sub,
taskList: { create: taskList },
taskProduct: { create: taskProduct },
},
})
.then(async (v) => {
await prisma.notification.create({
data: {
title: "ใบสั่งงานใหม่ / New Task Order",
detail: "รหัส / code : " + v.code,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
return v;
});
institution: true,
createdBy: true,
},
data: {
...rest,
code,
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
registeredBranchId: userAffiliatedBranch.id,
createdByUserId: req.user.sub,
taskList: { create: taskList },
taskProduct: { create: taskProduct },
},
});
});
}
@ -472,103 +433,88 @@ export class TaskController extends Controller {
);
}
return await prisma
.$transaction(async (tx) => {
await Promise.all(
record.taskList
.filter(
(lhs) =>
!body.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
)
.map((v) =>
tx.task.update({
where: { id: v.id },
data: {
requestWorkStep: { update: { workStatus: "Ready" } },
},
}),
),
);
await tx.requestWorkStepStatus.updateMany({
where: {
OR: body.taskList,
workStatus: RequestWorkStatus.Ready,
},
data: { workStatus: RequestWorkStatus.InProgress },
});
const work = await tx.requestWorkStepStatus.findMany({
include: {
requestWork: {
include: {
request: {
include: { quotation: true },
},
return await prisma.$transaction(async (tx) => {
await Promise.all(
record.taskList
.filter(
(lhs) =>
!body.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
)
.map((v) =>
tx.task.update({
where: { id: v.id },
data: {
requestWorkStep: { update: { workStatus: "Ready" } },
},
},
},
where: { OR: body.taskList },
});
}),
),
);
return await tx.taskOrder.update({
where: { id: taskOrderId },
include: {
taskList: {
include: {
requestWorkStep: {
include: {
requestWork: true,
},
},
},
},
institution: true,
registeredBranch: true,
createdBy: true,
},
data: {
...body,
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
taskList: {
deleteMany: record?.taskList
.filter(
(lhs) =>
!body.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
)
.map((v) => ({ id: v.id })),
createMany: {
data: body.taskList.filter(
(lhs) =>
!record?.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
),
skipDuplicates: true,
},
},
taskProduct: { deleteMany: {}, create: body.taskProduct },
},
});
})
.then(async (ret) => {
if (body.taskOrderStatus && record.taskOrderStatus !== body.taskOrderStatus) {
await prisma.notification.create({
data: {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}
return ret;
await tx.requestWorkStepStatus.updateMany({
where: {
OR: body.taskList,
workStatus: RequestWorkStatus.Ready,
},
data: { workStatus: RequestWorkStatus.InProgress },
});
const work = await tx.requestWorkStepStatus.findMany({
include: {
requestWork: {
include: {
request: {
include: { quotation: true },
},
},
},
},
where: { OR: body.taskList },
});
return await tx.taskOrder.update({
where: { id: taskOrderId },
include: {
taskList: {
include: {
requestWorkStep: {
include: {
requestWork: true,
},
},
},
},
institution: true,
registeredBranch: true,
createdBy: true,
},
data: {
...body,
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
taskList: {
deleteMany: record?.taskList
.filter(
(lhs) =>
!body.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
)
.map((v) => ({ id: v.id })),
createMany: {
data: body.taskList.filter(
(lhs) =>
!record?.taskList.find(
(rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step,
),
),
skipDuplicates: true,
},
},
taskProduct: { deleteMany: {}, create: body.taskProduct },
},
});
});
}
@Delete("{taskOrderId}")
@ -614,14 +560,6 @@ export class TaskController extends Controller {
@Route("/api/v1/task-order/{taskOrderId}")
@Tags("Task Order")
export class TaskActionController 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("set-task-status")
@Security("keycloak")
async changeTaskOrderTaskListStatus(
@ -639,28 +577,7 @@ export class TaskActionController extends Controller {
return await prisma.$transaction(async (tx) => {
const promises = body.map(async (v) => {
const record = await tx.task.findFirst({
include: {
requestWorkStep: {
include: {
requestWork: {
include: {
request: {
include: {
quotation: true,
employee: true,
},
},
productService: {
include: {
product: true,
},
},
},
},
},
},
taskOrder: true,
},
include: { requestWorkStep: true },
where: {
step: v.step,
requestWorkId: v.requestWorkId,
@ -678,25 +595,6 @@ export class TaskActionController extends Controller {
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({
where: { id: record.id },
data: {
@ -753,15 +651,6 @@ export class TaskActionController extends Controller {
},
data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() },
}),
prisma.notification.create({
data: {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
]);
}
@ -773,53 +662,22 @@ export class TaskActionController extends Controller {
if (!record) throw notFoundError("Task Order");
await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: "TASK_RI",
},
create: {
key: "TASK_RI",
value: 1,
},
update: {
value: { increment: 1 },
},
});
const current = new Date();
const year = `${current.getFullYear()}`.padStart(2, "0");
const month = `${current.getMonth() + 1}`.padStart(2, "0");
const code = `RI${year}${month}${last.value.toString().padStart(6, "0")}`;
await Promise.all([
tx.taskOrder
.update({
where: { id: taskOrderId },
data: {
urgent: false,
taskOrderStatus: TaskOrderStatus.Complete,
codeProductReceived: code,
userTask: {
updateMany: {
where: { taskOrderId },
data: {
userTaskStatus: UserTaskStatus.Submit,
},
tx.taskOrder.update({
where: { id: taskOrderId },
data: {
urgent: false,
taskOrderStatus: TaskOrderStatus.Complete,
userTask: {
updateMany: {
where: { taskOrderId },
data: {
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({
where: {
task: {
@ -923,138 +781,23 @@ export class TaskActionController extends Controller {
if (completeCheck) completed.push(item.id);
});
await tx.requestData
.updateManyAndReturn({
where: { id: { in: completed } },
include: {
quotation: {
select: {
registeredBranchId: true,
createdByUserId: true,
},
await tx.requestData.updateMany({
where: { id: { in: completed } },
data: { requestDataStatus: RequestDataStatus.Completed },
});
await tx.quotation.updateMany({
where: {
quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
},
requestData: {
every: {
requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed] },
},
},
data: { 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
.updateManyAndReturn({
where: {
quotationStatus: {
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
},
AND: [
{
requestData: {
every: {
requestDataStatus: {
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
},
},
},
},
{
requestData: {
some: {},
},
},
],
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
},
},
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
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),
});
});
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
});
});
}
}
@ -1123,8 +866,6 @@ export class UserTaskController extends Controller {
@Query() page = 1,
@Query() pageSize = 30,
@Query() userTaskStatus?: UserTaskStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
taskList: {
@ -1167,11 +908,10 @@ export class UserTaskController extends Controller {
: undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query, mode: "insensitive" } },
{ contactName: { contains: query, mode: "insensitive" } },
{ contactTel: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query } },
{ contactName: { contains: query } },
{ contactTel: { contains: query } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.TaskOrderWhereInput;
const [result, total] = await prisma.$transaction([
@ -1225,41 +965,20 @@ export class UserTaskController extends Controller {
await prisma.$transaction(async (tx) => {
const promises = body.taskOrderId.flatMap((taskOrderId) => [
tx.taskOrder
.update({
where: { id: taskOrderId },
data: {
taskOrderStatus: TaskOrderStatus.InProgress,
userTask: {
deleteMany: { userId: req.user.sub },
create: {
userId: req.user.sub,
userTaskStatus: UserTaskStatus.Accept,
acceptedAt: new Date(),
},
tx.taskOrder.update({
where: { id: taskOrderId },
data: {
taskOrderStatus: TaskOrderStatus.InProgress,
userTask: {
deleteMany: { userId: req.user.sub },
create: {
userId: req.user.sub,
userTaskStatus: UserTaskStatus.Accept,
acceptedAt: new Date(),
},
},
})
.then(async (v) => {
await tx.notification.create({
data: {
title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed",
detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
await tx.notification.create({
data: {
title: "มีการรับงาน / Task Accepted",
detail: "รหัสใบสั่งงาน / Order : " + v.code,
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}),
},
}),
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,

View file

@ -13,7 +13,6 @@ import {
Security,
Tags,
} from "tsoa";
import config from "../config.json";
import prisma from "../db";
@ -36,28 +35,29 @@ import {
} from "../utils/minio";
import { notFoundError } from "../utils/error";
import { CreditNotePaybackType, CreditNoteStatus, Prisma, RequestDataStatus } from "@prisma/client";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
import { PaybackStatus, RequestWorkStatus } from "../generated/kysely/types";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
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 permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
type CreditNoteCreate = {
requestWorkId: string[];
@ -85,14 +85,6 @@ type CreditNoteUpdate = {
@Route("api/v1/credit-note")
@Tags("Credit Note")
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")
@Security("keycloak")
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) {
@ -102,7 +94,7 @@ export class CreditNoteController extends Controller {
request: {
quotationId,
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
@ -129,8 +121,6 @@ export class CreditNoteController extends Controller {
@Query() query: string = "",
@Query() quotationId?: string,
@Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await this.getCreditNoteListByCriteria(
req,
@ -139,8 +129,6 @@ export class CreditNoteController extends Controller {
query,
quotationId,
creditNoteStatus,
startDate,
endDate,
);
}
@ -154,28 +142,28 @@ export class CreditNoteController extends Controller {
@Query() query: string = "",
@Query() quotationId?: string,
@Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: {},
) {
const where = {
OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{
code: { contains: query, mode: "insensitive" },
requestWork: {
some: {
request: {
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
{ quotation: { code: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
@ -183,14 +171,14 @@ export class CreditNoteController extends Controller {
OR: [
{
employeePassport: {
some: { number: { contains: query, mode: "insensitive" } },
some: { number: { contains: query } },
},
},
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
@ -206,19 +194,16 @@ export class CreditNoteController extends Controller {
request: {
quotationId,
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CreditNoteWhereInput;
const [result, total] = await prisma.$transaction([
prisma.creditNote.findMany({
where,
take: pageSize,
skip: (page - 1) * pageSize,
include: {
quotation: {
include: {
@ -251,7 +236,7 @@ export class CreditNoteController extends Controller {
some: {
request: {
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
@ -349,8 +334,9 @@ export class CreditNoteController extends Controller {
).length;
const price =
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
c.productService.discount;
c.productService.pricePerUnit -
c.productService.discount / c.productService.amount +
c.productService.vat / c.productService.amount;
if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount;
@ -376,98 +362,40 @@ export class CreditNoteController extends Controller {
update: { value: { increment: 1 } },
});
return await prisma.creditNote
.create({
include: {
requestWork: {
include: {
request: true,
},
},
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
return await prisma.creditNote.create({
include: {
requestWork: {
include: {
request: true,
},
},
data: {
reason: body.reason,
detail: body.detail,
remark: body.remark,
paybackType: body.paybackType,
paybackBank: body.paybackBank,
paybackAccount: body.paybackAccount,
paybackAccountName: body.paybackAccountName,
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
value,
requestWork: {
connect: body.requestWorkId.map((v) => ({
id: v,
})),
},
quotationId: body.quotationId,
quotation: true,
},
data: {
reason: body.reason,
detail: body.detail,
remark: body.remark,
paybackType: body.paybackType,
paybackBank: body.paybackBank,
paybackAccount: body.paybackAccount,
paybackAccountName: body.paybackAccountName,
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
value,
requestWork: {
connect: body.requestWorkId.map((v) => ({
id: v,
})),
},
})
.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;
});
quotationId: body.quotationId,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
}
@Put("{creditNoteId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
async updateCreditNote(
@Request() req: RequestWithUser,
@Path() creditNoteId: string,
@ -490,20 +418,17 @@ export class CreditNoteController extends Controller {
const requestWork = await prisma.requestWork.findMany({
where: {
OR: [{ creditNote: null }, { creditNoteId }],
request: {
quotation: { id: body.quotationId },
quotation: {
id: body.quotationId,
},
},
AND: [
{
OR: [{ creditNote: null }, { creditNoteId }],
stepStatus: {
some: {
workStatus: RequestWorkStatus.Canceled,
},
{
OR: [
{ request: { requestDataStatus: RequestDataStatus.Canceled } },
{ stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } } },
],
},
],
},
id: { in: body.requestWorkId },
},
include: {
@ -542,8 +467,9 @@ export class CreditNoteController extends Controller {
).length;
const price =
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
c.productService.discount;
c.productService.pricePerUnit -
c.productService.discount / c.productService.amount +
c.productService.vat / c.productService.amount;
if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount;
@ -572,8 +498,10 @@ export class CreditNoteController extends Controller {
value,
requestWork: {
disconnect: creditNoteData.requestWork
.map((item) => ({ id: item.id }))
.filter((data) => !body.requestWorkId.find((item) => item === data.id)),
.map((item) => ({
id: item.id,
}))
.filter((data) => !body.requestWorkId.find((item) => (item = data.id))),
connect: body.requestWorkId.map((v) => ({
id: v,
})),
@ -604,14 +532,6 @@ export class CreditNoteController extends Controller {
if (!record) throw notFoundError("Credit Note");
await permissionCheck(req.user, record.quotation.registeredBranch);
if (record.creditNoteStatus !== CreditNoteStatus.Waiting) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Accepted credit note cannot be deleted",
"creditNoteAcceptedNoDelete",
);
}
await Promise.all([
deleteFolder(fileLocation.creditNote.slip(creditNoteId)),
deleteFolder(fileLocation.creditNote.attachment(creditNoteId)),
@ -640,24 +560,6 @@ export class CreditNoteActionController extends Controller {
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")
@Security("keycloak", MANAGE_ROLES)
async acceptCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) {
await this.#checkPermission(req.user, creditNoteId);
return await prisma.creditNote.update({
where: { id: creditNoteId },
data: { creditNoteStatus: CreditNoteStatus.Pending },
});
}
@Post("payback-status")
@Security("keycloak", MANAGE_ROLES)
async updateStatus(
@ -666,81 +568,23 @@ export class CreditNoteActionController extends Controller {
@Body() body: { paybackStatus: PaybackStatus },
) {
await this.#checkPermission(req.user, creditNoteId);
return await prisma.creditNote
.update({
where: { id: creditNoteId },
include: {
requestWork: {
include: {
request: true,
},
},
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
return await prisma.creditNote.update({
where: { id: creditNoteId },
include: {
requestWork: {
include: {
request: true,
},
},
data: {
creditNoteStatus:
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;
});
quotation: true,
},
data: {
creditNoteStatus:
body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
paybackStatus: body.paybackStatus,
paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined,
},
});
}
}

View file

@ -36,7 +36,7 @@ import {
setFile,
} from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { queryOrNot } from "../utils/relation";
import { isSystem } from "../utils/keycloak";
import { precisionRound } from "../utils/arithmetic";
@ -44,20 +44,22 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
// NOTE: permission condition/check in registeredBranch
const permissionCond = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
type DebitNoteCreate = {
quotationId: string;
@ -74,7 +76,6 @@ type DebitNoteCreate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -110,14 +111,13 @@ type DebitNoteUpdate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName?: string;
firstName: string;
firstNameEN: string;
middleName?: string;
middleNameEN?: string;
lastName?: string;
lastNameEN?: string;
lastName: string;
lastNameEN: string;
}
)[];
@ -168,8 +168,6 @@ export class DebitNoteController extends Controller {
@Query() payCondition?: PayCondition,
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await this.getDebitNoteListByCriteria(
req,
@ -181,8 +179,6 @@ export class DebitNoteController extends Controller {
payCondition,
includeRegisteredBranch,
code,
startDate,
endDate,
);
}
@ -199,22 +195,21 @@ export class DebitNoteController extends Controller {
@Query() payCondition?: PayCondition,
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: {},
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
@ -225,7 +220,6 @@ export class DebitNoteController extends Controller {
debitNoteQuotationId: quotationId,
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
quotationStatus: status,
...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -430,18 +424,12 @@ export class DebitNoteController extends Controller {
const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!;
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = body.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
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
const price = body.agentPrice ? p.agentPrice : p.price;
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
VAT_DEFAULT *
(!v.discount ? v.amount : 1)
: 0;
return {
@ -464,13 +452,15 @@ export class DebitNoteController extends Controller {
const price = list.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
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(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -541,6 +531,7 @@ export class DebitNoteController extends Controller {
...price,
isDebitNote: true,
debitNoteQuotationId: quotationId,
quotationStatus: QuotationStatus.PaymentPending,
statusOrder: +(rest.status === "INACTIVE"),
code: `DN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(6, "0")}`,
contactName: master?.contactName ?? "",
@ -582,7 +573,7 @@ export class DebitNoteController extends Controller {
}
@Put("{debitNoteId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
async updateDebitNote(
@Request() req: RequestWithUser,
@Path() debitNoteId: string,
@ -606,7 +597,7 @@ export class DebitNoteController extends Controller {
if (!record) throw notFoundError("Debit Note");
await permissionCheck(req.user, record.registeredBranch);
await permissionCheckCompany(req.user, record.registeredBranch);
const { productServiceList: _productServiceList, ...rest } = body;
const ids = {
@ -677,18 +668,12 @@ export class DebitNoteController extends Controller {
}
const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!;
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = record.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
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
const price = body.agentPrice ? p.agentPrice : p.price;
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
VAT_DEFAULT *
(!v.discount ? v.amount : 1)
: 0;
return {
@ -711,13 +696,15 @@ export class DebitNoteController extends Controller {
const price = list.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
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(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -837,38 +824,6 @@ export class DebitNoteController extends Controller {
}
}
@Route("api/v1/debit-note/{debitNoteId}")
@Tags("Debit Note")
export class DebitNoteActionController extends Controller {
async #checkPermission(user: RequestWithUser["user"], id: string) {
const data = await prisma.quotation.findUnique({
include: {
registeredBranch: {
include: branchRelationPermInclude(user),
},
},
where: { id, isDebitNote: true },
});
if (!data) throw notFoundError("Debit Note");
await permissionCheck(user, data.registeredBranch);
return data;
}
@Post("accept")
@Security("keycloak", MANAGE_ROLES)
async acceptDebitNote(@Request() req: RequestWithUser, @Path() debitNoteId: string) {
const record = await this.#checkPermission(req.user, debitNoteId);
if (record.quotationStatus !== QuotationStatus.Issued) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Already Accepted", "debitNoteAlreadyAccept");
}
return await prisma.quotation.update({
where: { id: debitNoteId },
data: { quotationStatus: QuotationStatus.PaymentPending },
});
}
}
@Route("api/v1/debit-note/{debitNoteId}")
@Tags("Debit Note")
export class DebitNoteFileController extends Controller {

View file

@ -1,11 +1,9 @@
import {
Body,
Controller,
Delete,
Get,
Head,
Path,
Post,
Put,
Query,
Request,
@ -25,7 +23,7 @@ import {
TaskStatus,
RequestWorkStatus,
} from "@prisma/client";
import { queryOrNot, whereAddressQuery, whereDateQuery } from "../utils/relation";
import { queryOrNot, whereAddressQuery } from "../utils/relation";
import { filterStatus } from "../services/prisma";
// import { RequestWorkStatus } from "../generated/kysely/types";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
@ -51,8 +49,6 @@ export class LineController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: !!query
@ -60,13 +56,13 @@ export class LineController extends Controller {
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query, mode: "insensitive" } },
some: { number: { contains: query } },
},
},
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
...whereAddressQuery(query),
]) ?? []),
]
@ -77,19 +73,11 @@ export class LineController extends Controller {
status: activeOnly ? { not: Status.INACTIVE } : undefined,
id: customerBranchId,
customerId,
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
userId: line.user.sub,
},
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
@ -147,14 +135,7 @@ export class LineController extends Controller {
where: {
id: employeeId,
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
userId: line.user.sub,
},
},
});
@ -176,25 +157,24 @@ export class LineController extends Controller {
@Query() requestDataStatus?: RequestDataStatus,
@Query() quotationId?: string,
@Query() code?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ quotation: { code: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
@ -202,14 +182,14 @@ export class LineController extends Controller {
OR: [
{
employeePassport: {
some: { number: { contains: query, mode: "insensitive" } },
some: { number: { contains: query } },
},
},
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
@ -240,18 +220,8 @@ export class LineController extends Controller {
// registeredBranch: { OR: permissionCond(req.user) },
},
employee: {
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
},
customerBranch: { userId: line.user.sub },
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.RequestDataWhereInput;
const [result, total] = await prisma.$transaction([
@ -312,16 +282,7 @@ export class LineController extends Controller {
where: {
id: requestDataId,
employee: {
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
},
customerBranch: { userId: line.user.sub },
},
},
include: {
@ -438,16 +399,7 @@ export class LineController extends Controller {
: undefined,
quotationId,
employee: {
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
},
customerBranch: { userId: line.user.sub },
},
},
} satisfies Prisma.RequestWorkWhereInput;
@ -567,16 +519,7 @@ export class LineController extends Controller {
id: requestWorkId,
request: {
employee: {
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
},
customerBranch: { userId: line.user.sub },
},
},
},
@ -603,45 +546,49 @@ export class LineController extends Controller {
@Query() status?: QuotationStatus,
@Query() pendingOnly?: boolean,
@Query() inProgressOnly?: boolean,
@Query() successOnly?: boolean,
@Query() canceledOnly?: boolean,
@Query() historyOnly?: boolean,
@Query() urgentFirst?: boolean,
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() query = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query, mode: "insensitive" } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
OR: [
...(queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
},
]),
]) || []),
...(queryOrNot<Prisma.QuotationWhereInput[]>(!!pendingOnly, [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
]) || []),
],
isDebitNote: false,
code,
payCondition,
quotationStatus: successOnly ? "ProcessComplete" : canceledOnly ? "Canceled" : status,
quotationStatus: historyOnly ? { in: ["ProcessComplete", "Canceled"] } : status,
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
userId: line.user.sub,
},
requestData: inProgressOnly
? {
@ -650,23 +597,6 @@ export class LineController extends Controller {
},
}
: undefined,
AND: pendingOnly
? {
OR: [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
],
}
: undefined,
...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -768,16 +698,7 @@ export class LineController extends Controller {
where: {
id: quotationId,
isDebitNote: false,
customerBranch: {
OR: [
{ userId: line.user.sub },
{
customer: {
branch: { some: { userId: line.user.sub } },
},
},
],
},
customerBranch: { userId: line.user.sub },
},
});
@ -785,74 +706,6 @@ export class LineController extends Controller {
return record;
}
@Post("request/{requestDataId}/request-cancel")
@Security("line")
async customerRequestCancel(
@Path() requestDataId: string,
@Request() req: RequestWithLineUser,
@Body() body: { cancel: boolean; reason?: string },
) {
const result = await prisma.requestData.updateMany({
where: {
id: requestDataId,
quotation: {
customerBranch: {
OR: [
{ userId: req.user.sub },
{
customer: {
branch: { some: { userId: req.user.sub } },
},
},
],
},
},
},
data: {
customerRequestCancel: body.cancel,
customerRequestCancelReason: body.reason || null,
rejectRequestCancel: false,
rejectRequestCancelReason: null,
},
});
if (result.count <= 0) throw notFoundError("Request Data");
}
@Post("request-work/{requestWorkId}/request-cancel")
@Security("line")
async customerRequestCancelWork(
@Path() requestWorkId: string,
@Request() req: RequestWithLineUser,
@Body() body: { cancel: boolean; reason?: string },
) {
const result = await prisma.requestWork.updateMany({
where: {
id: requestWorkId,
request: {
quotation: {
customerBranch: {
OR: [
{ userId: req.user.sub },
{
customer: {
branch: { some: { userId: req.user.sub } },
},
},
],
},
},
},
},
data: {
customerRequestCancel: body.cancel,
customerRequestCancelReason: body.reason || null,
rejectRequestCancel: false,
rejectRequestCancelReason: null,
},
});
if (result.count <= 0) throw notFoundError("Request Data");
}
}
@Route("api/v1/line/customer-branch/{branchId}")
@ -1375,65 +1228,3 @@ export class LineQuotationFileController extends Controller {
return await deleteFile(fileLocation.quotation.attachment(quotationId, name));
}
}
@Route("api/v1/line/payment/{paymentId}/attachment")
@Tags("Line")
export class PaymentFileLineController extends Controller {
private async checkPermission(_user: RequestWithUser["user"], id: string) {
const data = await prisma.payment.findUnique({
include: {
invoice: {
include: {
quotation: true,
},
},
},
where: { id },
});
if (!data) throw notFoundError("Payment");
return { paymentId: id, quotationId: data.invoice.quotationId };
}
@Get()
@Security("line")
async listAttachment(@Request() req: RequestWithUser, @Path() paymentId: string) {
const { quotationId } = await this.checkPermission(req.user, paymentId);
return await listFile(fileLocation.quotation.payment(quotationId, paymentId));
}
@Head("{name}")
async headAttachment(
@Request() req: RequestWithUser,
@Path() paymentId: string,
@Path() name: string,
) {
const data = await prisma.payment.findUnique({
where: { id: paymentId },
include: { invoice: true },
});
if (!data) throw notFoundError("Payment");
return req.res?.redirect(
await getPresigned(
"head",
fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name),
),
);
}
@Get("{name}")
async getAttachment(
@Request() req: RequestWithUser,
@Path() paymentId: string,
@Path() name: string,
) {
const data = await prisma.payment.findUnique({
where: { id: paymentId },
include: { invoice: true },
});
if (!data) throw notFoundError("Payment");
return req.res?.redirect(
await getFile(fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name)),
);
}
}

View file

@ -6,7 +6,7 @@ import { RequestWithLineUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
type SendOTP = {
type SendEmail = {
identityNumber: string;
email: string;
};
@ -25,22 +25,11 @@ export class verificationController extends Controller {
@Get()
@Security("line")
async isRegistered(@Request() req: RequestWithLineUser) {
return !!(await prisma.customerBranch.findFirst({
where: {
OR: [
{ userId: req.user.sub },
{
customer: {
branch: { some: { userId: req.user.sub } },
},
},
],
},
}));
return !!(await prisma.customerBranch.findFirst({ where: { userId: req.user.sub } }));
}
@Post("/send-otp")
public async sendOTP(@Body() body: SendOTP) {
public async sendOTP(@Body() body: SendEmail) {
if (
![
process.env.SMTP_HOST,
@ -144,11 +133,13 @@ export class verificationController extends Controller {
customerBranch.otpExpires &&
customerBranch.otpExpires >= new Date()
) {
const dataCustomer = await prisma.customerBranch.updateMany({
const dataCustomer = await prisma.customerBranch.update({
where: {
customerId: customerBranch.customerId,
id: customerBranch.id,
},
data: {
userId: req.user.sub,
},
data: { userId: req.user.sub },
});
return dataCustomer;

View file

@ -68,37 +68,37 @@ export class WebHookController extends Controller {
const userIdLine = payload.events[0]?.source?.userId;
const dataNow = dayjs().tz("Asia/Bangkok").startOf("day");
if (payload?.events[0]?.message) {
const message = payload.events[0].message.text;
// const dataUser = await prisma.customerBranch.findFirst({
// where:{
// userId:userIdLine
// }
// })
if (message === "เมนูหลัก > ข้อความ") {
const dataEmployee = await prisma.employeePassport.findMany({
const dataEmployee = await prisma.employeePassport.findMany({
select: {
firstName: true,
firstNameEN: true,
lastName: true,
lastNameEN: true,
employeeId: true,
expireDate: true,
employee: {
select: {
firstName: true,
firstNameEN: true,
lastName: true,
lastNameEN: true,
employeeId: true,
expireDate: true,
employee: {
customerBranch: {
select: {
firstName: true,
firstNameEN: true,
lastName: true,
customerBranch: {
lastNameEN: true,
customerName: true,
customer: {
select: {
firstName: true,
firstNameEN: true,
lastName: true,
lastNameEN: true,
registerName: true,
customer: {
customerType: true,
registeredBranch: {
select: {
customerType: true,
registeredBranch: {
select: {
telephoneNo: true,
},
},
telephoneNo: true,
},
},
},
@ -106,40 +106,34 @@ export class WebHookController extends Controller {
},
},
},
where: {
employee: {
customerBranch: {
OR: [
{ userId: userIdLine },
{
customer: {
branch: { some: { userId: userIdLine } },
},
},
],
},
},
expireDate: {
lt: dataNow.add(30, "day").toDate(),
},
},
orderBy: {
expireDate: "asc",
},
});
},
},
where: {
expireDate: {
lt: dataNow.add(30, "day").toDate(),
},
},
orderBy: {
expireDate: "asc",
},
});
if (payload?.events[0]?.message) {
const message = payload.events[0].message.text;
if (message === "เมนูหลัก > ข้อความ") {
const dataUser = userIdLine;
const textHead = "JWS ALERT:";
let textData = "";
if (dataEmployee.length > 0) {
const registerName =
dataEmployee[0]?.employee?.customerBranch?.registerName ?? "ไม่ระบุ";
const customerName =
dataEmployee[0]?.employee?.customerBranch?.customerName ?? "ไม่ระบุ";
const telephoneNo =
dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ??
"ไม่ระบุ";
const textEmployer = `เรียน คุณ${registerName}`;
const textEmployer = `เรียน คุณ${customerName}`;
const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง";
const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้";
const textExpDate =
@ -153,10 +147,7 @@ export class WebHookController extends Controller {
dayjs(item.expireDate).format("DD/MM/") + (dayjs(item.expireDate).year() + 543);
const diffDate = dayjs(item.expireDate).diff(dayjs(), "day");
if (diffDate > 0) {
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n ${process.env.LINE_LIFF_URL}/${item.employeeId}`;
}
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} หมดอายุไปแล้ว ${Math.abs(diffDate)} วัน \n ${process.env.LINE_LIFF_URL}/${item.employeeId}`;
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n https://taii-cmm.case-collection.com/api/v1/line/employee/${item.employeeId}`;
})
.join("\n");

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/manual")
export class ManualController extends Controller {
@Get()
async get(@Request() req: express.Request) {
return req.res?.redirect(await getFile(".manual/toc.json"));
}
@Get("{category}/assets/{name}")
async getAsset(@Request() req: express.Request, @Path() category: string, @Path() name: string) {
return req.res?.redirect(await getFile(`.manual/${category}/assets/${name}`));
}
@Get("{category}/page/{page}")
async getContent(
@Request() req: express.Request,
@Path() category: string,
@Path() page: string,
) {
return req.res?.redirect(await getFile(`.manual/${category}/${page}.md`));
}
}

View file

@ -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,39 +0,0 @@
export interface StorageFolder {
/**
* @prop Full path to this folder. It is used as key as there are no files or directories at the same location.
*/
pathname: string;
/**
* @prop Directory / Folder name.
*/
name: string;
createdAt: string | Date;
createdBy: string | Date;
}
export interface StorageFile {
/**
* @prop Full path to this folder. It is used as key as there are no files or directories at the same location.
*/
pathname: string;
fileName: string;
fileSize: number;
fileType: string;
title: string;
description: string;
author: string;
category: string[];
keyword: string[];
metadata: Record<string, unknown>;
path: string;
upload: boolean;
updatedAt: string | Date;
updatedBy: string;
createdAt: string | Date;
createdBy: string;
}

View file

@ -1,170 +0,0 @@
import { DecodedJwt, createDecoder } from "fast-jwt";
import HttpError from "../../interfaces/http-error";
import HttpStatus from "../../interfaces/http-status";
import { StorageFile, StorageFolder } from "../../interfaces/edm";
const jwtDecode = createDecoder({ complete: true });
export type FileProps = Partial<
Pick<StorageFile, "title" | "description" | "author" | "keyword" | "category">
> & {
metadata?: { [key: string]: unknown };
};
const STORAGE_KEYCLOAK = process.env.EDM_KEYCLOAK!;
const STORAGE_KEYCLOAK_CLIENT = process.env.EDM_KEYCLOAK_CLIENT!;
const STORAGE_REALM = process.env.EDM_REALM!;
const STORAGE_URL = process.env.EDM_URL!;
const STORAGE_USER = process.env.EDM_ADMIN_USER!;
const STORAGE_PASSWORD = process.env.EDM_ADMIN_PASSWORD!;
let token: string | null = null;
let decoded: DecodedJwt | null = null;
/**
* Check if token is expired or will expire in 30 seconds
* @returns true if expire or can't get exp, false otherwise
*/
export function expireCheck(token: string, beforeExpire: number = 30) {
decoded = jwtDecode(token);
if (decoded && decoded.payload.exp) {
return Date.now() / 1000 >= decoded.payload.exp - beforeExpire;
}
return true;
}
/**
* Get token from id service if needed
*/
export async function getToken() {
if (!token || expireCheck(token)) {
const body = new URLSearchParams();
body.append("scope", "openid");
body.append("grant_type", "password");
body.append("client_id", STORAGE_KEYCLOAK_CLIENT || "edm");
body.append("username", STORAGE_USER);
body.append("password", STORAGE_PASSWORD);
const res = await fetch(
`${STORAGE_KEYCLOAK}/realms/${STORAGE_REALM}/protocol/openid-connect/token`,
{
method: "POST",
body: body,
},
).catch((e) => console.error(e));
if (!res) return;
const data = await res.json();
if (data && data.access_token) {
token = data.access_token;
}
}
return token;
}
/**
* @param path - Path that new folder will live
* @param name - Name of the folder to create
* @param recursive - Will create parent automatically
*/
export async function createFolder(path: string[], name: string, recursive: boolean = false) {
if (recursive && path.length > 0) {
await createFolder(path.slice(0, -1), path[path.length - 1], true);
}
const res = await fetch(`${STORAGE_URL}/storage/folder`, {
method: "POST",
headers: {
Authorization: `Bearer ${await getToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path, name }),
}).catch((e) => console.error(e));
if (!res || !res.ok) {
return Boolean(console.error(res ? await res.json() : res));
}
return true;
}
/**
* @param path - Path that new file will live
* @param file - Name of the file to create
*/
export async function createFile(path: string[], file: string, props?: FileProps) {
const res = await fetch(`${STORAGE_URL}/storage/file`, {
method: "POST",
headers: {
Authorization: `Bearer ${await getToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...props,
path,
file,
hidden: path.some((v) => v.startsWith(".")),
}),
}).catch((e) => console.error(e));
if (!res || !res.ok) {
return Boolean(console.error(res ? await res.json() : res));
}
return (await res.json()) as StorageFile & { uploadUrl: string };
}
export async function list(
operation: "file" | "folder",
path: string[],
): Promise<false | (StorageFile & { uploadUrl: string })[]> {
const res = await fetch(`${STORAGE_URL}/storage/list`, {
method: "POST",
headers: {
Authorization: `Bearer ${await getToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ operation, path, hidden: true }),
}).catch((e) => console.error(e));
if (!res || !res.ok) {
if (res && res.status === HttpStatus.NOT_FOUND) {
return [];
}
return Boolean(console.error(res ? await res.json() : res)) as false;
}
return await res.json();
}
export async function listFolder(path: string[]) {
return (await list("folder", path)) as StorageFolder[] | boolean;
}
export async function listFile(path: string[]) {
return (await list("file", path)) as StorageFile[] | boolean;
}
export async function downloadFile(
path: string[],
file: string,
): Promise<false | (StorageFile & { downloadUrl: string })> {
const res = await fetch(`${STORAGE_URL}/storage/file/download`, {
method: "POST",
headers: {
Authorization: `Bearer ${await getToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path, file }),
}).catch((e) => console.error(e));
if (!res || !res.ok) {
if (res && res.status === HttpStatus.NOT_FOUND) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบไฟล์ในระบบ");
}
console.error(res ? await res.json() : res);
return false;
}
return await res.json();
}

View file

@ -1,10 +1,6 @@
import prisma from "../db";
import config from "../config.json";
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_CLIENT_ID) throw new Error("Require FLOW_ACCOUNT_CLIENT_ID");
@ -236,29 +232,6 @@ const flowAccount = {
installments: true,
quotation: {
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: {
include: {
province: true,
@ -289,58 +262,19 @@ const flowAccount = {
const quotation = data.quotation;
const customer = quotation.customerBranch;
const summary = {
subTotal: 0,
discountAmount: 0,
vatableAmount: 0,
exemptAmount: 0,
vatAmount: 0,
grandTotal: 0,
};
const products = (
const product =
quotation.payCondition === PayCondition.BillFull ||
quotation.payCondition === PayCondition.Full
? quotation.productServiceList
: quotation.productServiceList.filter((lhs) =>
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 = {
contactCode: customer.code,
contactName: customer.contactName || "-",
contactName:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
: customer.registerName) || "-",
contactAddress: [
customer.address,
!!customer.moo ? "หมู่ " + customer.moo : null,
@ -349,10 +283,11 @@ const flowAccount = {
(customer.province?.id === "10" ? "แขวง" : "อำเภอ") + customer.subDistrict?.name,
(customer.province?.id === "10" ? "เขต" : "ตำบล") + customer.district?.name,
"จังหวัด" + customer.province?.name,
customer.subDistrict?.zipCode,
]
.filter(Boolean)
.join(" "),
contactTaxId: customer.citizenId || customer.legalPersonNo || "-",
contactTaxId: customer.citizenId || customer.code,
contactBranch:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
@ -370,35 +305,36 @@ const flowAccount = {
isVat: true,
useReceiptDeduction: false,
useInlineVat: true,
discounPercentage: 0,
discountAmount: quotation.totalDiscount,
subTotal: summary.subTotal,
totalAfterDiscount: summary.subTotal - summary.discountAmount,
vatableAmount: summary.vatableAmount,
exemptAmount: summary.exemptAmount,
vatAmount: summary.vatAmount,
grandTotal: summary.grandTotal,
subTotal:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.totalPrice,
totalAfterDiscount:
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(
convertTemplate(quotation.remark ?? "", {
"quotation-payment": {
paymentType: quotation?.payCondition || "Full",
amount: quotation.finalPrice,
installments: quotation?.paySplit,
},
"quotation-labor": {
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,
items: product.map((v) => ({
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
pricePerUnit: v.pricePerUnit,
quantity: v.amount,
discountAmount: v.discount,
total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
})),
};
return await flowAccountAPI.createReceipt(payload, false);
@ -411,219 +347,6 @@ const flowAccount = {
}
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;

View file

@ -346,64 +346,6 @@ export async function removeUserRoles(userId: string, roles: { id: string; name:
return true;
}
export async function getGroup(query: string) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/groups?${query}`, {
headers: {
authorization: `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "GET",
});
const dataMainGroup = await res.json();
const fetchSubGroups = async (group: any) => {
let fullSubGroup = await Promise.all(
group.subGroups.map((subGroupsData: any) => {
if (group.subGroupCount > 0) {
return fetchSubGroups(subGroupsData);
} else {
return {
id: subGroupsData.id,
name: subGroupsData.name,
path: subGroupsData.path,
subGroupCount: subGroupsData.subGroupCount,
subGroups: [],
};
}
}),
);
return {
id: group.id,
name: group.name,
path: group.path,
subGroupCount: group.subGroupCount,
subGroups: fullSubGroup,
};
};
const fullMainGroup = await Promise.all(dataMainGroup.map(fetchSubGroups));
return fullMainGroup;
}
export async function getGroupUser(userId: string) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/groups`, {
headers: {
authorization: `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "GET",
});
const data = await res.json();
return data.map((item: any) => {
return {
id: item.id,
name: item.name,
path: item.path,
};
});
}
export default {
createUser,
listRole,

View file

@ -4,8 +4,6 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
import { isSystem } from "../utils/keycloak";
import { ExpressionBuilder } from "kysely";
import { DB } from "../generated/kysely/types";
export function branchRelationPermInclude(user: RequestWithUser["user"]) {
return {
@ -135,47 +133,3 @@ export function createPermCheck(globalAllow: (user: RequestWithUser["user"]) =>
return branch;
};
}
export function createQueryPermissionCondition(
globalAllow: (user: RequestWithUser["user"]) => boolean,
opts?: { alwaysIncludeHead?: boolean },
) {
return (user: RequestWithUser["user"]) =>
({ eb, exists }: ExpressionBuilder<DB, keyof DB>) =>
exists(
eb
.selectFrom("Branch")
.leftJoin("BranchUser", "BranchUser.branchId", "Branch.id")
.leftJoin("Branch as SubBranch", "SubBranch.headOfficeId", "Branch.id")
.leftJoin("BranchUser as SubBranchUser", "SubBranchUser.branchId", "SubBranch.id")
.leftJoin("Branch as HeadBranch", "HeadBranch.id", "Branch.id")
.leftJoin("BranchUser as HeadBranchUser", "HeadBranchUser.branchId", "HeadBranch.id")
.leftJoin("Branch as SubHeadBranch", "SubHeadBranch.headOfficeId", "HeadBranch.id")
.leftJoin(
"BranchUser as SubHeadBranchUser",
"SubHeadBranchUser.branchId",
"SubHeadBranch.id",
)
.where((eb) => {
const cond = [
eb("BranchUser.userId", "=", user.sub), // NOTE: if user belong to current branch.
];
if (globalAllow?.(user) || opts?.alwaysIncludeHead) {
cond.push(
eb("SubBranchUser.userId", "=", user.sub), // NOTE: if user belong to branch under current branch.
);
}
if (globalAllow(user)) {
cond.push(
eb("HeadBranchUser.userId", "=", user.sub), // NOTE: if the current branch is under head branch user belong to.
eb("SubHeadBranchUser.userId", "=", user.sub), // NOTE: if the current branch is under the same head branch user belong to.
);
}
return eb.or(cond);
})
.select("Branch.id"),
);
}

View file

@ -1,8 +1,6 @@
import dayjs from "dayjs";
import { CronJob } from "cron";
import prisma from "../db";
import { Prisma } from "@prisma/client";
const jobs = [
CronJob.from({
@ -27,174 +25,6 @@ const jobs = [
.catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e));
},
}),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
await prisma.notification
.deleteMany({
where: { createdAt: { lte: dayjs().subtract(1, "month").toDate() } },
})
.then(() => console.log("[INFO]: Delete expired notification, OK."))
.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() {

View file

@ -35,12 +35,6 @@ export async function setFile(path: string, exp = 6 * 60 * 60) {
return await minio.presignedPutObject(MINIO_BUCKET, path, exp);
}
export async function uploadFile(path: string, buffer: Buffer, contentType?: string) {
await minio.putObject(MINIO_BUCKET, path, buffer, Buffer.byteLength(buffer), {
["Content-Type"]: contentType,
});
}
export async function deleteFile(path: string) {
await minio.removeObject(MINIO_BUCKET, path, { forceDelete: true });
}
@ -61,83 +55,71 @@ export async function deleteFolder(path: string) {
});
}
const ROOT = ".system";
export const fileLocation = {
branch: {
line: (branchId: string) => `${ROOT}/branch/line-qr-${branchId}`,
bank: (branchId: string, bankId: string) => `${ROOT}/branch/bank-qr-${branchId}-${bankId}`,
img: (branchId: string, name?: string) => `${ROOT}/branch/img-${branchId}/${name || ""}`,
attachment: (branchId: string, name?: string) =>
`${ROOT}/branch/attachment-${branchId}/${name || ""}`,
line: (branchId: string) => `branch/line-qr-${branchId}`,
bank: (branchId: string, bankId: string) => `branch/bank-qr-${branchId}-${bankId}`,
img: (branchId: string, name?: string) => `branch/img-${branchId}/${name || ""}`,
attachment: (branchId: string, name?: string) => `branch/attachment-${branchId}/${name || ""}`,
},
user: {
profile: (userId: string, name?: string) =>
`${ROOT}/user/profile-image-${userId}/${name || ""}`,
attachment: (userId: string, name?: string) =>
`${ROOT}/user/attachment-${userId}/${name || ""}`,
signature: (userId: string) => `${ROOT}/user/signature-${userId}`,
profile: (userId: string, name?: string) => `user/profile-image-${userId}/${name || ""}`,
attachment: (userId: string, name?: string) => `user/attachment-${userId}/${name || ""}`,
},
customer: {
img: (customerId: string, name?: string) => `${ROOT}/customer/img-${customerId}/${name || ""}`,
img: (customerId: string, name?: string) => `customer/img-${customerId}/${name || ""}`,
},
customerBranch: {
attachment: (customerBranchId: string, name?: string) =>
`${ROOT}/customer-branch/attachment-${customerBranchId}/${name || ""}`,
`customer-branch/attachment-${customerBranchId}/${name || ""}`,
citizen: (customerBranchId: string, citizenId?: string) =>
`${ROOT}/customer-branch/citizen-${customerBranchId}/${citizenId || ""}`,
`customer-branch/citizen-${customerBranchId}/${citizenId || ""}`,
houseRegistration: (customerBranchId: string, houseRegistrationId?: string) =>
`${ROOT}/customer-branch/house-registration-${customerBranchId}/${houseRegistrationId || ""}`,
`customer-branch/house-registration-${customerBranchId}/${houseRegistrationId || ""}`,
commercialRegistration: (customerBranchId: string, commercialRegistrationId?: string) =>
`${ROOT}/customer-branch/commercial-registration-${customerBranchId}/${commercialRegistrationId || ""}`,
`customer-branch/commercial-registration-${customerBranchId}/${commercialRegistrationId || ""}`,
vatRegistration: (customerBranchId: string, vatRegistrationId?: string) =>
`${ROOT}/customer-branch/vat-registration-${customerBranchId}/${vatRegistrationId || ""}`,
`customer-branch/vat-registration-${customerBranchId}/${vatRegistrationId || ""}`,
powerOfAttorney: (customerBranchId: string, powerOfAttorneyId?: string) =>
`${ROOT}/customer-branch/power-of-attorney-${customerBranchId}/${powerOfAttorneyId || ""}`,
`customer-branch/power-of-attorney-${customerBranchId}/${powerOfAttorneyId || ""}`,
},
employee: {
img: (employeeId: string, name?: string) => `${ROOT}/employee/img-${employeeId}/${name || ""}`,
img: (employeeId: string, name?: string) => `employee/img-${employeeId}/${name || ""}`,
attachment: (employeeId: string, name?: string) =>
`${ROOT}/employee/attachment-${employeeId}/${name || ""}`,
visa: (employeeId: string, visaId?: string) =>
`${ROOT}/employee/visa-${employeeId}/${visaId || ""}`,
`employee/attachment-${employeeId}/${name || ""}`,
visa: (employeeId: string, visaId?: string) => `employee/visa-${employeeId}/${visaId || ""}`,
passport: (employeeId: string, passportId?: string) =>
`${ROOT}/employee/passport-${employeeId}/${passportId || ""}`,
`employee/passport-${employeeId}/${passportId || ""}`,
inCountryNotice: (employeeId: string, noticeId?: string) =>
`${ROOT}/employee/in-country-notice-${employeeId}/${noticeId || ""}`,
`employee/in-country-notice-${employeeId}/${noticeId || ""}`,
},
product: {
img: (productId: string, name?: string) => `${ROOT}/product/img-${productId}/${name || ""}`,
img: (productId: string, name?: string) => `product/img-${productId}/${name || ""}`,
},
service: {
img: (serviceId: string, name?: string) => `${ROOT}/service/img-${serviceId}/${name || ""}`,
img: (serviceId: string, name?: string) => `service/img-${serviceId}/${name || ""}`,
},
quotation: {
attachment: (quotationId: string, name?: string) =>
`${ROOT}/quotation/attachment-${quotationId}/${name || ""}`,
`quotation/attachment-${quotationId}/${name || ""}`,
payment: (quotationId: string, paymentId: string, name?: string) =>
`${ROOT}/quotation/payment-${quotationId}/${paymentId}/${name || ""}`,
`quotation/payment-${quotationId}/${paymentId}/${name || ""}`,
},
request: {
attachment: (requestId: string, step: number, name?: string) =>
`${ROOT}/request/attachment-${requestId}-${step}/${name || ""}`,
`request/attachment-${requestId}-${step}/${name || ""}`,
},
institution: {
attachment: (institutionId: string, name?: string) =>
`${ROOT}/institution/attachment-${institutionId}/${name || ""}`,
img: (institutionId: string, name?: string) =>
`${ROOT}/institution/img-${institutionId}/${name || ""}`,
bank: (institutionId: string, bankId: string) =>
`${ROOT}/institution/bank-qr-${institutionId}-${bankId}`,
`institution/attachment-${institutionId}/${name || ""}`,
img: (institutionId: string, name?: string) => `institution/img-${institutionId}/${name || ""}`,
},
task: {
attachment: (taskId: string, name?: string) =>
`${ROOT}/task/attachment-${taskId}/${name || ""}`,
attachment: (taskId: string, name?: string) => `task/attachment-${taskId}/${name || ""}`,
},
creditNote: {
slip: (creditNoteId: string, name?: string) =>
`${ROOT}/credit-note/slip-${creditNoteId}/${name || ""}`,
slip: (creditNoteId: string, name?: string) => `credit-note/slip-${creditNoteId}/${name || ""}`,
attachment: (creditNoteId: string, name?: string) =>
`${ROOT}/credit-note/attachment-${creditNoteId}/${name || ""}`,
`credit-note/attachment-${creditNoteId}/${name || ""}`,
},
};

View file

@ -10,35 +10,26 @@ export function connectOrDisconnect(id?: string | null) {
export function whereAddressQuery(query: string) {
return [
{ address: { contains: query, mode: "insensitive" } },
{ addressEN: { contains: query, mode: "insensitive" } },
{ soi: { contains: query, mode: "insensitive" } },
{ soiEN: { contains: query, mode: "insensitive" } },
{ moo: { contains: query, mode: "insensitive" } },
{ mooEN: { contains: query, mode: "insensitive" } },
{ street: { contains: query, mode: "insensitive" } },
{ streetEN: { contains: query, mode: "insensitive" } },
{ province: { name: { contains: query, mode: "insensitive" } } },
{ province: { nameEN: { contains: query, mode: "insensitive" } } },
{ district: { name: { contains: query, mode: "insensitive" } } },
{ district: { nameEN: { contains: query, mode: "insensitive" } } },
{ subDistrict: { name: { contains: query, mode: "insensitive" } } },
{ subDistrict: { nameEN: { contains: query, mode: "insensitive" } } },
{ subDistrict: { zipCode: { contains: query, mode: "insensitive" } } },
] as const;
{ address: { contains: query } },
{ addressEN: { contains: query } },
{ soi: { contains: query } },
{ soiEN: { contains: query } },
{ moo: { contains: query } },
{ mooEN: { contains: query } },
{ street: { contains: query } },
{ streetEN: { contains: query } },
{ province: { name: { contains: query } } },
{ province: { nameEN: { contains: query } } },
{ district: { name: { contains: query } } },
{ district: { nameEN: { contains: query } } },
{ subDistrict: { name: { contains: query } } },
{ subDistrict: { nameEN: { contains: query } } },
{ subDistrict: { zipCode: { contains: query } } },
];
}
export function queryOrNot<T>(query: any, where: T): T | undefined;
export function queryOrNot<T, U>(query: any, where: T, fallback: U): T | U;
export function queryOrNot<T, U>(query: any, where: T, fallback?: U) {
export function queryOrNot<T>(query: string | boolean, where: T): T | undefined;
export function queryOrNot<T, U>(query: string | boolean, where: T, fallback: U): T | U;
export function queryOrNot<T, U>(query: string | boolean, where: T, fallback?: U) {
return !!query ? where : fallback;
}
export function whereDateQuery(startDate: Date | undefined, endDate: Date | undefined) {
return {
createdAt: {
gte: startDate,
lte: endDate,
},
};
}

View file

@ -1,105 +0,0 @@
import Excel from "exceljs";
export default class spreadsheet {
static async readCsv() {
// TODO: read csv
}
/**
* This function read data from excel file.
*
* @param buffer - Excel file.
* @param opts.header - Interprets the first row as the names of the fields.
* @param opts.worksheet - Specifies the worksheet to read. Can be the worksheet's name or its 1-based index.
*
* @returns
*/
static async readExcel<T extends unknown>(
buffer: Excel.Buffer,
opts?: { header?: boolean; worksheet?: number | string },
): Promise<T[]> {
const workbook = new Excel.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.getWorksheet(opts?.worksheet ?? 1);
if (!worksheet) return [];
const header: Record<number, string | number> = {};
const values: any[] = [];
worksheet.eachRow((row, rowId) => {
if (rowId === 1 && opts?.header !== false) {
row.eachCell((cell, cellId) => {
if (typeof cell.value === "string") {
header[cellId] = nameValue(cell.value);
} else {
header[cellId] = cellId.toString();
}
});
} else {
const data: Record<string | number, Excel.CellValue> = {};
row.eachCell((cell, cellId) => {
data[opts?.header !== false ? header[cellId] : cellId - 1] = cell.value;
});
values.push(opts?.header !== false ? data : Object.values(data));
}
});
return values;
}
}
function nameValue(value: string) {
let code: string;
switch (value) {
case "ชื่อสินค้าและบริการ":
code = "name";
break;
case "ระยะเวลาดำเนินการ":
code = "process";
break;
case "ประเภทค่าใช้จ่าย":
code = "expenseType";
break;
case "รายละเอียด":
code = "detail";
break;
case "หมายเหตุ":
code = "remark";
break;
case "ใช้งานร่วมกัน":
code = "shared";
break;
case "คำนวณภาษีราคาขาย":
code = "calcVat";
break;
case "รวม VAT ราคาขาย":
code = "vatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาขาย":
code = "price";
break;
case "คำนวณภาษีราคาตัวแทน":
code = "agentPriceCalcVat";
break;
case "รวม VAT ราคาตัวแทน":
code = "agentPriceVatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาตัวแทน":
code = "agentPrice";
break;
case "คำนวณภาษีราคาดำเนินการ":
code = "serviceChargeCalcVat";
break;
case "รวม VAT ราคาดำเนินการ":
code = "serviceChargeVatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาดำเนินการ":
code = "serviceCharge";
break;
default:
code = "code";
break;
}
return code;
}

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;
}
await prisma.$transaction(
async (tx) => {
const meta = {
createdBy: null,
createdAt: new Date(),
updatedBy: null,
updatedAt: new Date(),
};
await prisma.$transaction(async (tx) => {
const meta = {
createdBy: null,
createdAt: new Date(),
updatedBy: null,
updatedAt: new Date(),
};
await Promise.all(
splitChunk(province, 1000, async (r) => {
return await tx.$kysely
.insertInto("Province")
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(province, 1000, async (r) => {
return await tx.$kysely
.insertInto("Province")
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(district, 2000, async (r) => {
return await tx.$kysely
.insertInto("District")
.columns([
"id",
"name",
"nameEN",
"provinceId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
provinceId: (eb) => eb.ref("excluded.provinceId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(district, 2000, async (r) => {
return await tx.$kysely
.insertInto("District")
.columns([
"id",
"name",
"nameEN",
"provinceId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
provinceId: (eb) => eb.ref("excluded.provinceId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(subDistrict, 1000, async (r) => {
return await tx.$kysely
.insertInto("SubDistrict")
.columns([
"id",
"name",
"nameEN",
"districtId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
districtId: (eb) => eb.ref("excluded.districtId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
},
{
timeout: 15_000,
},
);
await Promise.all(
splitChunk(subDistrict, 1000, async (r) => {
return await tx.$kysely
.insertInto("SubDistrict")
.columns([
"id",
"name",
"nameEN",
"districtId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
districtId: (eb) => eb.ref("excluded.districtId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
});
console.log("[INFO]: Sync thailand province, district and subdistrict, OK.");
}
@ -175,72 +170,67 @@ export async function initEmploymentOffice() {
const list = await prisma.province.findMany();
await prisma.$transaction(
async (tx) => {
await Promise.all(
list
.map(async (province) => {
if (special[province.id]) {
await tx.employmentOffice.deleteMany({
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,
},
await prisma.$transaction(async (tx) => {
await Promise.all(
list
.map(async (province) => {
if (special[province.id]) {
await tx.employmentOffice.deleteMany({
where: { provinceId: province.id, district: { none: {} } },
});
})
.flat(),
);
},
{
timeout: 15_000,
},
);
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,
},
});
})
.flat(),
);
});
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

@ -41,7 +41,6 @@
{ "name": "Employee Other Info" },
{ "name": "Institution" },
{ "name": "Workflow" },
{ "name": "Property" },
{ "name": "Product Group" },
{ "name": "Product" },
{ "name": "Work" },
@ -54,9 +53,7 @@
{ "name": "Task Order" },
{ "name": "User Task Order" },
{ "name": "Credit Note" },
{ "name": "Debit Note" },
{ "name": "Report" },
{ "name": "Document Template" }
{ "name": "Debit Note" }
]
}
},

View file

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