From 60be5f5103eed40a3f3f5b9754ee307406c32a38 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:32:25 +0700 Subject: [PATCH 01/54] chore(ci): update spellcheck event --- .forgejo/workflows/spellcheck.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.forgejo/workflows/spellcheck.yaml b/.forgejo/workflows/spellcheck.yaml index 468d970..b6a397f 100644 --- a/.forgejo/workflows/spellcheck.yaml +++ b/.forgejo/workflows/spellcheck.yaml @@ -3,8 +3,7 @@ name: Spell Check permissions: contents: read -on: - push: +on: [push, pull_request] env: CLICOLOR: 1 From feb792def95a827ac906292b2f9674395f14e692 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:36:52 +0700 Subject: [PATCH 02/54] refactor(ci): use same var instead ssh host can be multiple by using comma (,) --- .forgejo/workflows/deploy.yaml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 1aebe39..69642da 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -36,19 +36,11 @@ jobs: - name: Remote Deploy Development uses: appleboy/ssh-action@v1.2.1 with: - host: ${{ vars.SSH_DEVELOPMENT_HOST }} - port: ${{ vars.SSH_DEVELOPMENT_PORT }} - username: ${{ secrets.SSH_DEVELOPMENT_USER }} - password: ${{ secrets.SSH_DEVELOPMENT_PASSWORD }} - script: eval "${{ secrets.SSH_DEVELOPMENT_DEPLOY_CMD }}" - - name: Remote Deploy Test - uses: appleboy/ssh-action@v1.2.1 - with: - host: ${{ vars.SSH_TEST_HOST }} - port: ${{ vars.SSH_TEST_PORT }} - username: ${{ secrets.SSH_TEST_USER }} - password: ${{ secrets.SSH_TEST_PASSWORD }} - script: eval "${{ secrets.SSH_TEST_DEPLOY_CMD }}" + 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: | From ba697d006a2655c548253584690333d22407c697 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:42:21 +0700 Subject: [PATCH 03/54] fix(ci): step name --- .forgejo/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 69642da..86b83f0 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -33,7 +33,7 @@ jobs: platforms: linux/amd64 tags: ${{ env.CONTAINER_IMAGE_NAME }} push: true - - name: Remote Deploy Development + - name: Remote Deploy uses: appleboy/ssh-action@v1.2.1 with: host: ${{ vars.SSH_DEPLOY_HOST }} From c004c516c6590b7e71a94e896d3e58ce5b91a7e3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:27:15 +0700 Subject: [PATCH 04/54] feat: stats endpoints --- src/controllers/00-stats-controller.ts | 187 +++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/controllers/00-stats-controller.ts diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts new file mode 100644 index 0000000..a12d7a1 --- /dev/null +++ b/src/controllers/00-stats-controller.ts @@ -0,0 +1,187 @@ +import { QuotationStatus, RequestWorkStatus } from "@prisma/client"; +import { Controller, Get, Query, Request, Route, Security } from "tsoa"; +import prisma from "../db"; +import { createPermCondition } from "../services/permission"; +import { RequestWithUser } from "../interfaces/user"; +import { PaymentStatus } from "../generated/kysely/types"; + +const permissionCondCompany = createPermCondition((_) => true); + +@Route("/api/v1/report") +@Security("keycloak") +export class StatsController extends Controller { + @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, + createdAt: true, + updatedAt: true, + }, + where: { + registeredBranch: { OR: permissionCondCompany(req.user) }, + createdAt: { gte: startDate, lte: endDate }, + }, + take: limit, + }); + + return record.map((v) => ({ + document: "quotation", + code: v.code, + status: v.quotationStatus, + createdAt: v.createdAt, + updatedAt: v.updatedAt, + })); + } + + @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, + payment: { + select: { + paymentStatus: true, + }, + }, + createdAt: true, + }, + where: { + quotation: { + isDebitNote: false, + registeredBranch: { OR: permissionCondCompany(req.user) }, + }, + createdAt: { gte: startDate, lte: endDate }, + }, + take: limit, + }); + + return record.map((v) => ({ + document: "invoice", + code: v.code, + status: v.payment?.paymentStatus, + createdAt: v.createdAt, + })); + } + + @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, + paymentStatus: true, + createdAt: true, + }, + where: { + paymentStatus: PaymentStatus.PaymentSuccess, + invoice: { + quotation: { + isDebitNote: false, + registeredBranch: { OR: permissionCondCompany(req.user) }, + }, + }, + createdAt: { gte: startDate, lte: endDate }, + }, + take: limit, + }); + + return record.map((v) => ({ + document: "receipt", + code: v.code, + status: v.paymentStatus, + createdAt: v.createdAt, + })); + } + + @Get("product") + async productReport( + @Request() req: RequestWithUser, + @Query() limit?: number, + @Query() startDate?: Date, + @Query() endDate?: Date, + ) { + const record = await prisma.product.findMany({ + select: { + id: true, + code: true, + name: true, + createdAt: true, + updatedAt: true, + _count: { + select: { + quotationProductServiceList: { + where: { + quotation: { + quotationStatus: { + in: [ + QuotationStatus.PaymentInProcess, + QuotationStatus.PaymentSuccess, + QuotationStatus.ProcessComplete, + ], + }, + }, + }, + }, + }, + }, + }, + where: { + quotationProductServiceList: { some: {} }, + productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } }, + createdAt: { gte: startDate, lte: endDate }, + }, + take: limit, + }); + + const doing = await prisma.quotationProductServiceList.groupBy({ + _count: true, + by: "productId", + where: { + productId: { in: record.map((v) => v.id) }, + requestWork: { + some: { + stepStatus: { + some: { + workStatus: { + in: [ + RequestWorkStatus.Pending, + RequestWorkStatus.InProgress, + RequestWorkStatus.Validate, + RequestWorkStatus.Completed, + RequestWorkStatus.Ended, + ], + }, + }, + }, + }, + }, + }, + }); + + 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, + createdAt: v.createdAt, + updatedAt: v.updatedAt, + })); + } +} From ffa8095dcc26509a4584aab60798df8ed1acaddd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:43:28 +0700 Subject: [PATCH 05/54] fix: report not scope registered branch --- src/controllers/00-stats-controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index a12d7a1..eb3988f 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -153,6 +153,9 @@ export class StatsController extends Controller { _count: true, by: "productId", where: { + quotation: { + registeredBranch: { OR: permissionCondCompany(req.user) }, + }, productId: { in: record.map((v) => v.id) }, requestWork: { some: { From b36b6f9f075a6dd409808bd0a6a84530e43318f8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:46:11 +0700 Subject: [PATCH 06/54] feat: filter date --- src/controllers/00-stats-controller.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index eb3988f..a9060a1 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -154,6 +154,7 @@ export class StatsController extends Controller { by: "productId", where: { quotation: { + createdAt: { gte: startDate, lte: endDate }, registeredBranch: { OR: permissionCondCompany(req.user) }, }, productId: { in: record.map((v) => v.id) }, @@ -187,4 +188,12 @@ export class StatsController extends Controller { updatedAt: v.updatedAt, })); } + + @Get("sale") + async saleReport( + @Request() req: RequestWithUser, + @Query() limit?: number, + @Query() startDate?: Date, + @Query() endDate?: Date, + ) {} } From e9889a1682b9f55f07c72da054cd3f029b42f7e3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:42:01 +0700 Subject: [PATCH 07/54] fix: order --- src/controllers/00-stats-controller.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index a9060a1..923696f 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -28,6 +28,7 @@ export class StatsController extends Controller { registeredBranch: { OR: permissionCondCompany(req.user) }, createdAt: { gte: startDate, lte: endDate }, }, + orderBy: { createdAt: 'desc' }, take: limit, }); @@ -64,6 +65,7 @@ export class StatsController extends Controller { }, createdAt: { gte: startDate, lte: endDate }, }, + orderBy: { createdAt: 'desc' }, take: limit, }); @@ -98,6 +100,7 @@ export class StatsController extends Controller { }, createdAt: { gte: startDate, lte: endDate }, }, + orderBy: { createdAt: 'desc' }, take: limit, }); @@ -117,6 +120,13 @@ export class StatsController extends Controller { @Query() endDate?: Date, ) { const record = await prisma.product.findMany({ + include: { + quotationProductServiceList: { + include: { + quotation: true, + }, + }, + }, select: { id: true, code: true, @@ -142,9 +152,15 @@ export class StatsController extends Controller { }, }, where: { - quotationProductServiceList: { some: {} }, + quotationProductServiceList: { + some: { + quotation: { createdAt: { gte: startDate, lte: endDate } }, + }, + }, productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } }, - createdAt: { gte: startDate, lte: endDate }, + }, + orderBy: { + quotationProductServiceList: { _count: "desc" }, }, take: limit, }); From 9fb4a7a88c17282f9c49ac8353893fe66bee04d3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:42:10 +0700 Subject: [PATCH 08/54] feat: sale stats --- src/controllers/00-stats-controller.ts | 90 ++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 923696f..2059941 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -1,4 +1,11 @@ -import { QuotationStatus, RequestWorkStatus } from "@prisma/client"; +import { + Customer, + CustomerBranch, + ProductGroup, + QuotationStatus, + RequestWorkStatus, + User, +} from "@prisma/client"; import { Controller, Get, Query, Request, Route, Security } from "tsoa"; import prisma from "../db"; import { createPermCondition } from "../services/permission"; @@ -28,7 +35,6 @@ export class StatsController extends Controller { registeredBranch: { OR: permissionCondCompany(req.user) }, createdAt: { gte: startDate, lte: endDate }, }, - orderBy: { createdAt: 'desc' }, take: limit, }); @@ -65,7 +71,6 @@ export class StatsController extends Controller { }, createdAt: { gte: startDate, lte: endDate }, }, - orderBy: { createdAt: 'desc' }, take: limit, }); @@ -100,7 +105,9 @@ export class StatsController extends Controller { }, createdAt: { gte: startDate, lte: endDate }, }, - orderBy: { createdAt: 'desc' }, + orderBy: { + createdAt: 'desc' + } take: limit, }); @@ -211,5 +218,78 @@ export class StatsController extends Controller { @Query() limit?: number, @Query() startDate?: Date, @Query() endDate?: Date, - ) {} + ) { + const list = await prisma.quotationProductServiceList.findMany({ + include: { + quotation: { + include: { + createdBy: true, + customerBranch: { + 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: (ProductGroup & { _count: number })[]; + bySale: (User & { _count: number })[]; + byCustomer: ((CustomerBranch & { customer: Customer }) & { _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: [] }, + ); + } } From 3b29f5610033e8158bd34e2abe8bafd80947ab1a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:50:23 +0700 Subject: [PATCH 09/54] fix: order --- src/controllers/00-stats-controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 2059941..5a0c871 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -35,6 +35,7 @@ export class StatsController extends Controller { registeredBranch: { OR: permissionCondCompany(req.user) }, createdAt: { gte: startDate, lte: endDate }, }, + orderBy: { createdAt: "desc" }, take: limit, }); @@ -71,6 +72,7 @@ export class StatsController extends Controller { }, createdAt: { gte: startDate, lte: endDate }, }, + orderBy: { createdAt: "desc" }, take: limit, }); @@ -105,9 +107,7 @@ export class StatsController extends Controller { }, createdAt: { gte: startDate, lte: endDate }, }, - orderBy: { - createdAt: 'desc' - } + orderBy: { createdAt: "desc" }, take: limit, }); From e92870602d259d9d6dfa1b5e81c0c8a3f0747c03 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:15:26 +0700 Subject: [PATCH 10/54] feat: add total order of product to report --- src/controllers/00-stats-controller.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 5a0c871..330759c 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -201,12 +201,25 @@ export class StatsController extends Controller { }, }); + const order = await prisma.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, })); From 54dba11dc4aead19391f2c0ad11635d969716603 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:37:13 +0700 Subject: [PATCH 11/54] chore: add tsoa tag --- src/controllers/00-doc-template-controller.ts | 3 ++- src/controllers/00-stats-controller.ts | 3 ++- tsoa.json | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/00-doc-template-controller.ts b/src/controllers/00-doc-template-controller.ts index e375c55..c0e6fff 100644 --- a/src/controllers/00-doc-template-controller.ts +++ b/src/controllers/00-doc-template-controller.ts @@ -2,7 +2,7 @@ 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 } from "tsoa"; +import { Controller, Get, Path, Query, Route, Tags } from "tsoa"; import prisma from "../db"; import { notFoundError } from "../utils/error"; import HttpError from "../interfaces/http-error"; @@ -62,6 +62,7 @@ const quotationData = (id: string) => }); @Route("api/v1/doc-template") +@Tags("Document Template") export class DocTemplateController extends Controller { @Get() async getTemplate() { diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 330759c..0271acb 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -6,7 +6,7 @@ import { RequestWorkStatus, User, } from "@prisma/client"; -import { Controller, Get, Query, Request, Route, Security } from "tsoa"; +import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa"; import prisma from "../db"; import { createPermCondition } from "../services/permission"; import { RequestWithUser } from "../interfaces/user"; @@ -16,6 +16,7 @@ const permissionCondCompany = createPermCondition((_) => true); @Route("/api/v1/report") @Security("keycloak") +@Tags("Report") export class StatsController extends Controller { @Get("quotation") async quotationReport( diff --git a/tsoa.json b/tsoa.json index 09db573..f9a463c 100644 --- a/tsoa.json +++ b/tsoa.json @@ -53,7 +53,9 @@ { "name": "Task Order" }, { "name": "User Task Order" }, { "name": "Credit Note" }, - { "name": "Debit Note" } + { "name": "Debit Note" }, + { "name": "Report" }, + { "name": "Document Template" } ] } }, From b4df1a5d4e3418054fcfc233fa0a0c51958b9e6b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:19:44 +0700 Subject: [PATCH 12/54] feat: profit report --- src/controllers/00-stats-controller.ts | 240 ++++++++++++++++--------- 1 file changed, 157 insertions(+), 83 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 0271acb..e739d44 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -1,3 +1,4 @@ +import config from "../config.json"; import { Customer, CustomerBranch, @@ -11,9 +12,12 @@ import prisma from "../db"; import { createPermCondition } from "../services/permission"; import { RequestWithUser } from "../interfaces/user"; import { PaymentStatus } from "../generated/kysely/types"; +import { precisionRound } from "../utils/arithmetic"; const permissionCondCompany = createPermCondition((_) => true); +const VAT_DEFAULT = config.vat; + @Route("/api/v1/report") @Security("keycloak") @Tags("Report") @@ -127,30 +131,73 @@ export class StatsController extends Controller { @Query() startDate?: Date, @Query() endDate?: Date, ) { - const record = await prisma.product.findMany({ - include: { - quotationProductServiceList: { - include: { - quotation: true, + await prisma.$transaction(async (tx) => { + const record = await tx.product.findMany({ + include: { + quotationProductServiceList: { + include: { + quotation: true, + }, }, }, - }, - select: { - id: true, - code: true, - name: true, - createdAt: true, - updatedAt: true, - _count: { - select: { - quotationProductServiceList: { - where: { - quotation: { - quotationStatus: { + select: { + id: true, + code: true, + name: true, + createdAt: true, + updatedAt: 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: [ - QuotationStatus.PaymentInProcess, - QuotationStatus.PaymentSuccess, - QuotationStatus.ProcessComplete, + RequestWorkStatus.Pending, + RequestWorkStatus.InProgress, + RequestWorkStatus.Validate, + RequestWorkStatus.Completed, + RequestWorkStatus.Ended, ], }, }, @@ -158,72 +205,31 @@ export class StatsController extends Controller { }, }, }, - }, - where: { - quotationProductServiceList: { - some: { - quotation: { createdAt: { gte: startDate, lte: endDate } }, + }); + + 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) }, }, - productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } }, - }, - orderBy: { - quotationProductServiceList: { _count: "desc" }, - }, - take: limit, - }); + }); - const doing = await prisma.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, - ], - }, - }, - }, - }, - }, - }, + 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, + })); }); - - const order = await prisma.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") @@ -306,4 +312,72 @@ export class StatsController extends Controller { { byProductGroup: [], bySale: [], byCustomer: [] }, ); } + + @Get("profit") + async profit( + @Request() req: RequestWithUser, + @Query() limit?: number, + @Query() startDate?: Date, + @Query() endDate?: Date, + ) { + const record = await prisma.quotationProductServiceList.findMany({ + include: { + product: { + select: { + agentPrice: true, + agentPriceCalcVat: true, + agentPriceVatIncluded: true, + serviceCharge: true, + serviceChargeCalcVat: true, + serviceChargeVatIncluded: true, + price: true, + calcVat: true, + vatIncluded: true, + }, + }, + quotation: { + select: { + agentPrice: true, + }, + }, + }, + where: { + quotation: { + quotationStatus: { + in: [ + QuotationStatus.PaymentInProcess, + QuotationStatus.PaymentSuccess, + QuotationStatus.ProcessComplete, + ], + }, + registeredBranch: { + OR: permissionCondCompany(req.user), + }, + }, + }, + take: limit, + }); + + let income = 0; + let expenses = 0; + let netProfit = 0; + + record.forEach((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); + + income += finalPrice; + expenses += productExpenses; + netProfit += finalPrice - productExpenses; + }); + + return { + income: precisionRound(income), + expenses: precisionRound(expenses), + netProfit: precisionRound(netProfit), + }; + } } From 9bd24b5a8362359d16c9df8d3d3ff12dc2951b21 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:10:06 +0700 Subject: [PATCH 13/54] feat: calc profit endpoint --- src/controllers/00-stats-controller.ts | 58 ++++++++++++++++++++------ 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index e739d44..03a9ba2 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -322,6 +322,13 @@ export class StatsController extends Controller { ) { const record = await prisma.quotationProductServiceList.findMany({ include: { + work: { + include: { + productOnWork: { + select: { stepCount: true, productId: true }, + }, + }, + }, product: { select: { agentPrice: true, @@ -335,9 +342,16 @@ export class StatsController extends Controller { vatIncluded: true, }, }, + requestWork: { + include: { + stepStatus: true, + creditNote: true, + }, + }, quotation: { select: { agentPrice: true, + creditNote: true, }, }, }, @@ -358,26 +372,44 @@ export class StatsController extends Controller { take: limit, }); - let income = 0; - let expenses = 0; - let netProfit = 0; - - record.forEach((v) => { + 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); - income += finalPrice; - expenses += productExpenses; - netProfit += finalPrice - productExpenses; + 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 { + income, + expenses, + netProfit, + }; + }); }); - return { - income: precisionRound(income), - expenses: precisionRound(expenses), - netProfit: precisionRound(netProfit), - }; + return data.flat().reduce( + (a, c) => { + a.income += c.income; + a.expenses += c.expenses; + a.netProfit += c.netProfit; + return a; + }, + { income: 0, expenses: 0, netProfit: 0 }, + ); } } From 5d7816604775c11f3e9c7121ba066913091dab83 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:34:15 +0700 Subject: [PATCH 14/54] feat: allow filter profit range --- src/controllers/00-stats-controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 03a9ba2..92193fe 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -316,7 +316,6 @@ export class StatsController extends Controller { @Get("profit") async profit( @Request() req: RequestWithUser, - @Query() limit?: number, @Query() startDate?: Date, @Query() endDate?: Date, ) { @@ -367,9 +366,9 @@ export class StatsController extends Controller { registeredBranch: { OR: permissionCondCompany(req.user), }, + createdAt: { gte: startDate, lte: endDate }, }, }, - take: limit, }); const data = record.map((v) => { From 5c58953820513ed832012c24a3125353c9b3c9e3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:12:07 +0700 Subject: [PATCH 15/54] feat: add payment stats by month --- src/controllers/00-stats-controller.ts | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 92193fe..6db8ca4 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -13,6 +13,7 @@ import { createPermCondition } from "../services/permission"; import { RequestWithUser } from "../interfaces/user"; import { PaymentStatus } from "../generated/kysely/types"; import { precisionRound } from "../utils/arithmetic"; +import dayjs from "dayjs"; const permissionCondCompany = createPermCondition((_) => true); @@ -411,4 +412,56 @@ export class StatsController extends Controller { { income: 0, expenses: 0, netProfit: 0 }, ); } + + @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(); + } + + 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() } }, + by: "paymentStatus", + }) + .then((v) => + v.reduce>>((a, c) => { + a[c.paymentStatus] = c._sum.amount || 0; + return a; + }, {}), + ), + }; + }), + ); + }); + return data; + } } From 2502b7c68f887b96ac456019f6c2a0771e3a38b5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:49:27 +0700 Subject: [PATCH 16/54] fix: scope permission --- src/controllers/00-stats-controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 6db8ca4..8963099 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -449,7 +449,14 @@ export class StatsController extends Controller { data: await tx.payment .groupBy({ _sum: { amount: true }, - where: { createdAt: { gte: v, lte: date.endOf("month").toDate() } }, + where: { + createdAt: { gte: v, lte: date.endOf("month").toDate() }, + invoice: { + quotation: { + registeredBranch: { OR: permissionCondCompany(req.user) }, + }, + }, + }, by: "paymentStatus", }) .then((v) => From b4b7d633d17e09c59353bf193f76d2d8838cda89 Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Wed, 30 Oct 2024 13:48:18 +0700 Subject: [PATCH 17/54] feat: add query notification --- src/controllers/00-notification-controller.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 1718e04..4fdfce8 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -14,6 +14,9 @@ import { } from "tsoa"; import { RequestWithUser } from "../interfaces/user"; import HttpStatus from "../interfaces/http-status"; +import prisma from "../db"; +import { Prisma } from "@prisma/client"; +import { queryOrNot } from "../utils/relation"; type NotificationCreate = {}; type NotificationUpdate = {}; @@ -29,12 +32,31 @@ export class NotificationController extends Controller { @Query() pageSize: number = 30, @Query() query = "", ) { - const total = 0; - - // TODO: implement + const where: Prisma.NotificationWhereInput = { + AND: [ + { + OR: queryOrNot<(typeof where)[]>(query, [ + { title: { contains: query } }, + { detail: { contains: query } }, + ]), + }, + { + OR: [ + { receiverId: req.user.sub }, + req.user.roles.length > 0 + ? { groupReceiver: { some: { name: { in: req.user.roles } } } } + : {}, + ], + }, + ], + }; + const [result, total] = await prisma.$transaction([ + prisma.notification.findMany({ where }), + prisma.notification.count({ where }), + ]); return { - result: [], + result, page, pageSize, total, From 549410e9e371cc2b463d51fc33e81229f267023f Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Wed, 30 Oct 2024 14:08:39 +0700 Subject: [PATCH 18/54] feat: add delete notification --- src/controllers/00-notification-controller.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 4fdfce8..a006858 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -17,6 +17,7 @@ import HttpStatus from "../interfaces/http-status"; import prisma from "../db"; import { Prisma } from "@prisma/client"; import { queryOrNot } from "../utils/relation"; +import { notFoundError } from "../utils/error"; type NotificationCreate = {}; type NotificationUpdate = {}; @@ -95,8 +96,8 @@ export class NotificationController extends Controller { @Delete("{notificationId}") @Security("keycloak") async deleteNotification(@Request() req: RequestWithUser, @Path() notificationId: string) { - // TODO: implement - - return {}; + const record = await prisma.notification.deleteMany({ where: { id: notificationId } }); + if (record.count === 0) throw notFoundError("Notification"); + return record; } } From ae252acbb8acdee55e2e9f0973d1049b77e0f564 Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Wed, 30 Oct 2024 14:33:34 +0700 Subject: [PATCH 19/54] feat: add get notification single notification --- src/controllers/00-notification-controller.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index a006858..c0c463c 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -67,9 +67,11 @@ export class NotificationController extends Controller { @Get("{notificationId}") @Security("keycloak") async getNotification(@Request() req: RequestWithUser, @Path() notificationId: string) { - // TODO: implement + const record = await prisma.notification.findFirst({ where: { id: notificationId } }); - return {}; + if (!record) throw notFoundError("Notification"); + + return record; } @Post() From 34af1f9dcd8febef6a9d7654a7f8e8ffb5a3b603 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:49:08 +0700 Subject: [PATCH 20/54] feat: read by user --- src/controllers/00-notification-controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index c0c463c..0e27847 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -67,7 +67,14 @@ export class NotificationController extends Controller { @Get("{notificationId}") @Security("keycloak") async getNotification(@Request() req: RequestWithUser, @Path() notificationId: string) { - const record = await prisma.notification.findFirst({ where: { id: notificationId } }); + const record = await prisma.notification.update({ + where: { id: notificationId }, + data: { + readByUser: { + connect: { id: req.user.sub }, + }, + }, + }); if (!record) throw notFoundError("Notification"); From 53c0c0fce9cdc6f67f1632fa5b27f5dc03e4656f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:04:21 +0700 Subject: [PATCH 21/54] refactor: response result --- src/controllers/00-notification-controller.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 0e27847..ee20e50 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -18,6 +18,7 @@ import prisma from "../db"; import { Prisma } from "@prisma/client"; import { queryOrNot } from "../utils/relation"; import { notFoundError } from "../utils/error"; +import dayjs from "dayjs"; type NotificationCreate = {}; type NotificationUpdate = {}; @@ -50,14 +51,24 @@ export class NotificationController extends Controller { ], }, ], + NOT: { + readByUser: { some: { id: req.user.sub } }, + createdAt: dayjs().subtract(7, "days").toDate(), + }, }; const [result, total] = await prisma.$transaction([ - prisma.notification.findMany({ where }), + prisma.notification.findMany({ where, include: { readByUser: true } }), prisma.notification.count({ where }), ]); return { - result, + 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), + })), page, pageSize, total, From f0db968b20b453ea9a1335b6d263a9dae8d706bf Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:16:18 +0700 Subject: [PATCH 22/54] feat: add permission query to noti --- prisma/schema.prisma | 4 ++++ src/controllers/00-notification-controller.ts | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5cfd276..671b21d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,9 @@ 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? @@ -313,6 +316,7 @@ model Branch { quotation Quotation[] workflowTemplate WorkflowTemplate[] taskOrder TaskOrder[] + notification Notification[] } model BranchBank { diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index ee20e50..00baff7 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -19,10 +19,14 @@ import { Prisma } from "@prisma/client"; import { queryOrNot } from "../utils/relation"; import { notFoundError } from "../utils/error"; import dayjs from "dayjs"; +import HttpError from "../interfaces/http-error"; +import { createPermCondition } from "../services/permission"; type NotificationCreate = {}; type NotificationUpdate = {}; +const permissionCondCompany = createPermCondition((_) => true); + @Route("/api/v1/notification") @Tags("Notification") export class NotificationController extends Controller { @@ -46,7 +50,10 @@ export class NotificationController extends Controller { OR: [ { receiverId: req.user.sub }, req.user.roles.length > 0 - ? { groupReceiver: { some: { name: { in: req.user.roles } } } } + ? { + groupReceiver: { some: { name: { in: req.user.roles } } }, + registeredBranch: { OR: permissionCondCompany(req.user) }, + } : {}, ], }, @@ -97,8 +104,9 @@ export class NotificationController extends Controller { async createNotification(@Request() req: RequestWithUser, @Body() body: NotificationCreate) { // TODO: implement - this.setStatus(HttpStatus.CREATED); - return {}; + // this.setStatus(HttpStatus.CREATED); + + throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "Not implemented.", "notImplemented"); } @Put("{notificationId}") @@ -110,7 +118,7 @@ export class NotificationController extends Controller { ) { // TODO: implement - return {}; + throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "Not implemented.", "notImplemented"); } @Delete("{notificationId}") From c21ef1448b05b3b82317ba12506d60521ac97c0d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:16:54 +0700 Subject: [PATCH 23/54] chore: migration --- .../20250305041645_add_noti_perm_related_field/migration.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 prisma/migrations/20250305041645_add_noti_perm_related_field/migration.sql diff --git a/prisma/migrations/20250305041645_add_noti_perm_related_field/migration.sql b/prisma/migrations/20250305041645_add_noti_perm_related_field/migration.sql new file mode 100644 index 0000000..95388f6 --- /dev/null +++ b/prisma/migrations/20250305041645_add_noti_perm_related_field/migration.sql @@ -0,0 +1,5 @@ +-- 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; From 1f2a0639748765d811844ab620ad5355334e46ee Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:33:06 +0700 Subject: [PATCH 24/54] feat: add request cancel field --- prisma/schema.prisma | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 671b21d..520410c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1442,6 +1442,9 @@ model RequestData { requestDataStatus RequestDataStatus @default(Pending) + customerRequestCancel Boolean? + customerRequestCancelReason String? + flow Json? requestWork RequestWork[] @@ -1477,6 +1480,9 @@ model RequestWork { stepStatus RequestWorkStepStatus[] + customerRequestCancel Boolean? + customerRequestCancelReason String? + creditNote CreditNote? @relation(fields: [creditNoteId], references: [id], onDelete: SetNull) creditNoteId String? } From 57641681ea8982a38f917a8cc5cb296294eb1b4c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:37:25 +0700 Subject: [PATCH 25/54] feat: add customer request cancel detail --- src/controllers/09-line-controller.ts | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/controllers/09-line-controller.ts b/src/controllers/09-line-controller.ts index 7704c4d..4e4910d 100644 --- a/src/controllers/09-line-controller.ts +++ b/src/controllers/09-line-controller.ts @@ -1,9 +1,11 @@ import { + Body, Controller, Delete, Get, Head, Path, + Post, Put, Query, Request, @@ -776,6 +778,70 @@ export class LineController extends Controller { return record; } + + @Post("request/{requestDataId}/request-cancel") + @Security("line") + async customerRequestCancel( + @Path() requestDataId: string, + @Request() req: RequestWithLineUser, + @Body() body: { 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: true, + customerRequestCancelReason: body.reason, + }, + }); + 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: { 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: true, + customerRequestCancelReason: body.reason, + }, + }); + if (result.count <= 0) throw notFoundError("Request Data"); + } } @Route("api/v1/line/customer-branch/{branchId}") From 3a437d78d4aea9f74c3a5057588f78767f07a740 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:55:54 +0700 Subject: [PATCH 26/54] fix: wrong notification query condition --- src/controllers/00-notification-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 00baff7..a2034de 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -60,7 +60,7 @@ export class NotificationController extends Controller { ], NOT: { readByUser: { some: { id: req.user.sub } }, - createdAt: dayjs().subtract(7, "days").toDate(), + createdAt: { lte: dayjs().subtract(7, "days").toDate() }, }, }; const [result, total] = await prisma.$transaction([ From 8abca9b137e4d10ade8dc70d25df86b7998614c1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 13:54:32 +0700 Subject: [PATCH 27/54] fix: error use select and include together --- src/controllers/00-stats-controller.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 8963099..849ca2c 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -134,19 +134,15 @@ export class StatsController extends Controller { ) { await prisma.$transaction(async (tx) => { const record = await tx.product.findMany({ - include: { - quotationProductServiceList: { - include: { - quotation: true, - }, - }, - }, select: { id: true, code: true, name: true, createdAt: true, updatedAt: true, + quotationProductServiceList: { + include: { quotation: true }, + }, _count: { select: { quotationProductServiceList: { From b190f93ec351dadde6120254eec60c954ccd661c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:09:41 +0700 Subject: [PATCH 28/54] chore: migration --- .../migrations/20250305070931_update_field/migration.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 prisma/migrations/20250305070931_update_field/migration.sql diff --git a/prisma/migrations/20250305070931_update_field/migration.sql b/prisma/migrations/20250305070931_update_field/migration.sql new file mode 100644 index 0000000..7d3ee1a --- /dev/null +++ b/prisma/migrations/20250305070931_update_field/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "RequestData" ADD COLUMN "customerRequestCancel" BOOLEAN, +ADD COLUMN "customerRequestCancelReason" TEXT; + +-- AlterTable +ALTER TABLE "RequestWork" ADD COLUMN "customerRequestCancel" BOOLEAN, +ADD COLUMN "customerRequestCancelReason" TEXT; From 0eed3c55f6f695518420f6c4be2c4b650b093e27 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:57:09 +0700 Subject: [PATCH 29/54] feat: response more field --- src/controllers/00-stats-controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 849ca2c..965b394 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -69,6 +69,7 @@ export class StatsController extends Controller { paymentStatus: true, }, }, + amount: true, createdAt: true, }, where: { @@ -86,6 +87,7 @@ export class StatsController extends Controller { document: "invoice", code: v.code, status: v.payment?.paymentStatus, + amount: v.amount, createdAt: v.createdAt, })); } From c0bc31714e884d2826b6810d9457d1008a051181 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:04:17 +0700 Subject: [PATCH 30/54] fix: empty response --- src/controllers/00-stats-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 965b394..54edbce 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -134,7 +134,7 @@ export class StatsController extends Controller { @Query() startDate?: Date, @Query() endDate?: Date, ) { - await prisma.$transaction(async (tx) => { + return await prisma.$transaction(async (tx) => { const record = await tx.product.findMany({ select: { id: true, From 071262a85a03e071f2d2e70eebfa9484d7d701b1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:17:23 +0700 Subject: [PATCH 31/54] feat: mark read endpoint --- src/controllers/00-notification-controller.ts | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index a2034de..d8285f4 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -99,26 +99,36 @@ export class NotificationController extends Controller { return record; } - @Post() + @Post("notification/mark-read") @Security("keycloak") - async createNotification(@Request() req: RequestWithUser, @Body() body: NotificationCreate) { - // TODO: implement + 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, + }, + }); - // this.setStatus(HttpStatus.CREATED); - - throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "Not implemented.", "notImplemented"); - } - - @Put("{notificationId}") - @Security("keycloak") - async updateNotification( - @Request() req: RequestWithUser, - @Path() notificationId: string, - @Body() body: NotificationUpdate, - ) { - // TODO: implement - - throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "Not implemented.", "notImplemented"); + await prisma.$transaction( + record.map((v) => + prisma.notification.update({ + where: { id: v.id }, + data: { + readByUser: { connect: { id: req.user.sub } }, + }, + }), + ), + ); } @Delete("{notificationId}") From 2697b4f6e08a33539f811defe0d4ff65b46bf0b6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 13:00:10 +0700 Subject: [PATCH 32/54] feat: notify quotation status change --- src/controllers/05-payment-controller.ts | 105 +++++++------ src/controllers/05-quotation-controller.ts | 13 +- src/controllers/06-request-list-controller.ts | 143 ++++++++++++------ src/controllers/07-task-controller.ts | 34 +++-- 4 files changed, 192 insertions(+), 103 deletions(-) diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts index 1177dba..e0bbd20 100644 --- a/src/controllers/05-payment-controller.ts +++ b/src/controllers/05-payment-controller.ts @@ -177,55 +177,66 @@ export class QuotationPayment extends Controller { }, }); - 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 } - : [], - ); + 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 } - : [], - ), - }, - }; - }), - }; - } - })(), - }, - }); + 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: res.code + res.quotationStatus, + receiverId: res.createdByUserId, + }, + }); + }); return payment; }); diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index 9ae2ae1..de771fd 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -454,7 +454,7 @@ export class QuotationController extends Controller { const { productServiceList: _productServiceList, worker: _worker, ...rest } = body; - return await prisma.$transaction(async (tx) => { + const ret = await prisma.$transaction(async (tx) => { const nonExistEmployee = body.worker.filter((v) => typeof v !== "string"); const lastEmployee = await tx.runningNo.upsert({ where: { @@ -639,6 +639,17 @@ export class QuotationController extends Controller { }, }); }); + + await prisma.notification.create({ + data: { + title: "New Quotation", + detail: "New quotation: " + ret.code, + registeredBranchId: ret.registeredBranchId, + groupReceiver: { create: [{ name: "accountant" }, { name: "head_of_accountant" }] }, + }, + }); + + return ret; } @Put("{quotationId}") diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts index bbb2e39..47d6039 100644 --- a/src/controllers/06-request-list-controller.ts +++ b/src/controllers/06-request-list-controller.ts @@ -268,14 +268,24 @@ export class RequestDataActionController extends Controller { }), ]); await Promise.all([ - tx.quotation.updateMany({ - where: { - requestData: { - every: { requestDataStatus: RequestDataStatus.Canceled }, + tx.quotation + .updateManyAndReturn({ + where: { + requestData: { + every: { requestDataStatus: RequestDataStatus.Canceled }, + }, }, - }, - data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, - }), + data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, + }) + .then(async (res) => { + await tx.notification.createMany({ + data: res.map((v) => ({ + title: "Quotation Status Updated", + detail: v.code + "Canceled", + receiverId: v.createdByUserId, + })), + }); + }), tx.taskOrder.updateMany({ where: { taskList: { @@ -405,14 +415,25 @@ export class RequestDataActionController extends Controller { data: { taskStatus: TaskStatus.Canceled }, }); await Promise.all([ - tx.quotation.updateMany({ - where: { - requestData: { - every: { requestDataStatus: RequestDataStatus.Canceled }, + tx.quotation + .updateManyAndReturn({ + where: { + quotationStatus: { not: QuotationStatus.Canceled }, + requestData: { + every: { requestDataStatus: RequestDataStatus.Canceled }, + }, }, - }, - data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, - }), + data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, + }) + .then(async (res) => { + await tx.notification.createMany({ + data: res.map((v) => ({ + title: "Quotation Status Updated", + detail: v.code + "Canceled", + receiverId: v.createdByUserId, + })), + }); + }), tx.taskOrder.updateMany({ where: { taskList: { @@ -479,21 +500,31 @@ export class RequestDataActionController extends Controller { 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], + await tx.quotation + .updateManyAndReturn({ + where: { + quotationStatus: { + notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], + }, + requestData: { + every: { + requestDataStatus: { + in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], + }, }, }, }, - }, - data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, - }); + data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, + }) + .then(async (res) => { + await tx.notification.createMany({ + data: res.map((v) => ({ + title: "Quotation Status Updated", + detail: v.code + "Completed", + receiverId: v.createdByUserId, + })), + }); + }); // dataRecord.push(record); return data; }); @@ -812,14 +843,25 @@ export class RequestListController extends Controller { data: { taskStatus: TaskStatus.Canceled }, }); await Promise.all([ - tx.quotation.updateMany({ - where: { - requestData: { - every: { requestDataStatus: RequestDataStatus.Canceled }, + tx.quotation + .updateManyAndReturn({ + where: { + quotationStatus: { not: QuotationStatus.Canceled }, + requestData: { + every: { requestDataStatus: RequestDataStatus.Canceled }, + }, }, - }, - data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, - }), + data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, + }) + .then(async (res) => { + await tx.notification.createMany({ + data: res.map((v) => ({ + title: "Quotation Status Updated", + detail: v.code + "Canceled", + receiverId: v.createdByUserId, + })), + }); + }), tx.taskOrder.updateMany({ where: { taskList: { @@ -887,19 +929,32 @@ export class RequestListController extends Controller { 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] }, + await tx.quotation + .updateManyAndReturn({ + where: { + quotationStatus: { + notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], + }, + requestData: { + every: { + requestDataStatus: { + in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], + }, + }, }, }, - }, - data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, - }); + data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, + }) + .then(async (res) => { + await tx.notification.createMany({ + data: res.map((v) => ({ + title: "Quotation Status Updated", + detail: v.code + "Completed", + receiverId: v.createdByUserId, + })), + }); + }); + return record; }); } diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index a572962..c782294 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -785,19 +785,31 @@ export class TaskActionController extends Controller { 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] }, + await tx.quotation + .updateManyAndReturn({ + where: { + quotationStatus: { + notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], + }, + requestData: { + every: { + requestDataStatus: { + in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], + }, + }, }, }, - }, - data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, - }); + data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, + }) + .then(async (res) => { + await tx.notification.createMany({ + data: res.map((v) => ({ + title: "Quotation Status Updated", + detail: v.code + "Completed", + receiverId: v.createdByUserId, + })), + }); + }); }); } } From 9ffbed7cb83d112d5b5db588262a27bf7a750469 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 13:54:04 +0700 Subject: [PATCH 33/54] fix: text spacing --- src/controllers/05-payment-controller.ts | 2 +- src/controllers/06-request-list-controller.ts | 10 +++++----- src/controllers/07-task-controller.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts index e0bbd20..52ead42 100644 --- a/src/controllers/05-payment-controller.ts +++ b/src/controllers/05-payment-controller.ts @@ -232,7 +232,7 @@ export class QuotationPayment extends Controller { await tx.notification.create({ data: { title: "Quotation Status Updated", - detail: res.code + res.quotationStatus, + detail: res.code + " " + res.quotationStatus, receiverId: res.createdByUserId, }, }); diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts index 47d6039..841df32 100644 --- a/src/controllers/06-request-list-controller.ts +++ b/src/controllers/06-request-list-controller.ts @@ -281,7 +281,7 @@ export class RequestDataActionController extends Controller { await tx.notification.createMany({ data: res.map((v) => ({ title: "Quotation Status Updated", - detail: v.code + "Canceled", + detail: v.code + " Canceled", receiverId: v.createdByUserId, })), }); @@ -429,7 +429,7 @@ export class RequestDataActionController extends Controller { await tx.notification.createMany({ data: res.map((v) => ({ title: "Quotation Status Updated", - detail: v.code + "Canceled", + detail: v.code + " Canceled", receiverId: v.createdByUserId, })), }); @@ -520,7 +520,7 @@ export class RequestDataActionController extends Controller { await tx.notification.createMany({ data: res.map((v) => ({ title: "Quotation Status Updated", - detail: v.code + "Completed", + detail: v.code + " Completed", receiverId: v.createdByUserId, })), }); @@ -857,7 +857,7 @@ export class RequestListController extends Controller { await tx.notification.createMany({ data: res.map((v) => ({ title: "Quotation Status Updated", - detail: v.code + "Canceled", + detail: v.code + " Canceled", receiverId: v.createdByUserId, })), }); @@ -949,7 +949,7 @@ export class RequestListController extends Controller { await tx.notification.createMany({ data: res.map((v) => ({ title: "Quotation Status Updated", - detail: v.code + "Completed", + detail: v.code + " Completed", receiverId: v.createdByUserId, })), }); diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index c782294..31f44fc 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -805,7 +805,7 @@ export class TaskActionController extends Controller { await tx.notification.createMany({ data: res.map((v) => ({ title: "Quotation Status Updated", - detail: v.code + "Completed", + detail: v.code + " Completed", receiverId: v.createdByUserId, })), }); From afb4a83efa18acf011ffd67ad5aec129d14028aa Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 13:58:58 +0700 Subject: [PATCH 34/54] feat: notify product change --- src/controllers/04-product-controller.ts | 12 ++++++++++++ src/controllers/04-service-controller.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts index b73bd3a..6e07f98 100644 --- a/src/controllers/04-product-controller.ts +++ b/src/controllers/04-product-controller.ts @@ -374,6 +374,7 @@ export class ProductController extends Controller { const record = await prisma.product.update({ include: { + productGroup: true, createdBy: true, updatedBy: true, }, @@ -398,6 +399,17 @@ 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; } diff --git a/src/controllers/04-service-controller.ts b/src/controllers/04-service-controller.ts index c48d948..0670295 100644 --- a/src/controllers/04-service-controller.ts +++ b/src/controllers/04-service-controller.ts @@ -473,6 +473,7 @@ export class ServiceController extends Controller { return await tx.service.update({ include: { + productGroup: true, createdBy: true, updatedBy: true, }, @@ -523,6 +524,17 @@ 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; } From 17b92b4012744db45798d1c1bd11e6e4524a33e9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:41:55 +0700 Subject: [PATCH 35/54] feat: notify request, task update --- src/controllers/05-quotation-controller.ts | 78 +++++----- src/controllers/07-task-controller.ts | 164 ++++++++++++--------- 2 files changed, 137 insertions(+), 105 deletions(-) diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index de771fd..86a53f6 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -1136,41 +1136,53 @@ export class QuotationActionController extends Controller { }, update: { value: { increment: quotation.worker.length } }, }); - 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, - })), + 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, + })), + }, }, + 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, }, - 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: "New request: " + ret.requestData.map((v) => v.code).join(", "), + registeredBranchId: ret.registeredBranchId, + groupReceiver: { create: { name: "document_checker" } }, + }, + }); + }); }); } } diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index 31f44fc..59d26d7 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -433,88 +433,101 @@ 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" } }, - }, - }), - ), - ); + 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 }, - }, - }, + await tx.requestWorkStepStatus.updateMany({ + where: { + OR: body.taskList, + workStatus: RequestWorkStatus.Ready, }, - }, - where: { OR: body.taskList }, - }); + data: { workStatus: RequestWorkStatus.InProgress }, + }); - return await tx.taskOrder.update({ - where: { id: taskOrderId }, - include: { - taskList: { - include: { - requestWorkStep: { - include: { - requestWork: true, + const work = await tx.requestWorkStepStatus.findMany({ + include: { + requestWork: { + include: { + request: { + include: { quotation: 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, + 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, }, - taskProduct: { deleteMany: {}, create: body.taskProduct }, - }, + 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: "Task submitted in order: " + record.code, + receiverId: record.createdByUserId, + }, + }); + } + return ret; }); - }); } @Delete("{taskOrderId}") @@ -651,6 +664,13 @@ export class TaskActionController extends Controller { }, data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() }, }), + prisma.notification.create({ + data: { + title: "Task Submitted", + detail: "Task submitted in order: " + record.code, + receiverId: record.createdByUserId, + }, + }), ]); } From f583448e5f45d37cce71265ac5e331ed7a8f3de6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:50:08 +0700 Subject: [PATCH 36/54] feat: add thai text to notification --- src/controllers/05-payment-controller.ts | 4 ++-- src/controllers/05-quotation-controller.ts | 8 ++++---- src/controllers/06-request-list-controller.ts | 20 +++++++++---------- src/controllers/07-task-controller.ts | 12 +++++------ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts index 52ead42..a4b7339 100644 --- a/src/controllers/05-payment-controller.ts +++ b/src/controllers/05-payment-controller.ts @@ -231,8 +231,8 @@ export class QuotationPayment extends Controller { if (quotation.quotationStatus !== res.quotationStatus) await tx.notification.create({ data: { - title: "Quotation Status Updated", - detail: res.code + " " + res.quotationStatus, + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + res.code + " " + res.quotationStatus, receiverId: res.createdByUserId, }, }); diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index 86a53f6..bf0f0e5 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -642,8 +642,8 @@ export class QuotationController extends Controller { await prisma.notification.create({ data: { - title: "New Quotation", - detail: "New quotation: " + ret.code, + title: "ใบเสนอราคาใหม่ / New Quotation", + detail: "รหัส / code : " + ret.code, registeredBranchId: ret.registeredBranchId, groupReceiver: { create: [{ name: "accountant" }, { name: "head_of_accountant" }] }, }, @@ -1176,8 +1176,8 @@ export class QuotationActionController extends Controller { .then(async (ret) => { await prisma.notification.create({ data: { - title: "New Request", - detail: "New request: " + ret.requestData.map((v) => v.code).join(", "), + title: "รายการคำขอใหม่ / New Request", + detail: "รหัส / code : " + ret.requestData.map((v) => v.code).join(", "), registeredBranchId: ret.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, }, diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts index 841df32..8464c03 100644 --- a/src/controllers/06-request-list-controller.ts +++ b/src/controllers/06-request-list-controller.ts @@ -280,8 +280,8 @@ export class RequestDataActionController extends Controller { .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ - title: "Quotation Status Updated", - detail: v.code + " Canceled", + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, })), }); @@ -428,8 +428,8 @@ export class RequestDataActionController extends Controller { .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ - title: "Quotation Status Updated", - detail: v.code + " Canceled", + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, })), }); @@ -519,8 +519,8 @@ export class RequestDataActionController extends Controller { .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ - title: "Quotation Status Updated", - detail: v.code + " Completed", + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + v.code + " Completed", receiverId: v.createdByUserId, })), }); @@ -856,8 +856,8 @@ export class RequestListController extends Controller { .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ - title: "Quotation Status Updated", - detail: v.code + " Canceled", + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, })), }); @@ -948,8 +948,8 @@ export class RequestListController extends Controller { .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ - title: "Quotation Status Updated", - detail: v.code + " Completed", + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + v.code + " Completed", receiverId: v.createdByUserId, })), }); diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index 59d26d7..aa28b0c 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -520,8 +520,8 @@ export class TaskController extends Controller { if (body.taskOrderStatus && record.taskOrderStatus !== body.taskOrderStatus) { await prisma.notification.create({ data: { - title: "Task Submitted", - detail: "Task submitted in order: " + record.code, + title: "มีการส่งงาน / Task Submitted", + detail: "รหัสใบสั่งงาน / Order : " + record.code, receiverId: record.createdByUserId, }, }); @@ -666,8 +666,8 @@ export class TaskActionController extends Controller { }), prisma.notification.create({ data: { - title: "Task Submitted", - detail: "Task submitted in order: " + record.code, + title: "มีการส่งงาน / Task Submitted", + detail: "รหัสใบสั่งงาน / Order : " + record.code, receiverId: record.createdByUserId, }, }), @@ -824,8 +824,8 @@ export class TaskActionController extends Controller { .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ - title: "Quotation Status Updated", - detail: v.code + " Completed", + title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", + detail: "รหัส / code : " + v.code + " Completed", receiverId: v.createdByUserId, })), }); From b518488112aa0dfab5cba29f4f85b8e4c2792b53 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:23:37 +0700 Subject: [PATCH 37/54] feat: accept task notification --- src/controllers/07-task-controller.ts | 41 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index aa28b0c..2722c04 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -997,20 +997,37 @@ 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.createMany({ + data: [ + { + title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed", + detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress", + receiverId: v.createdByUserId, + }, + { + title: "มีการรับงาน / Task Accepted", + detail: "รหัสใบสั่งงาน / Order : " + v.code, + receiverId: v.createdByUserId, + }, + ], + }); + }), tx.task.updateMany({ where: { taskOrderId: taskOrderId, From c6a56df94af3936eb27487990da52504732c4913 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:25:52 +0700 Subject: [PATCH 38/54] fix: notification order --- src/controllers/00-notification-controller.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index d8285f4..d9b90c0 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -64,7 +64,11 @@ export class NotificationController extends Controller { }, }; const [result, total] = await prisma.$transaction([ - prisma.notification.findMany({ where, include: { readByUser: true } }), + prisma.notification.findMany({ + where, + include: { readByUser: true }, + orderBy: { createdAt: "desc" }, + }), prisma.notification.count({ where }), ]); From 9bf534adce211dedf914507d1fdd48fbf424111a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:18:25 +0700 Subject: [PATCH 39/54] feat: add delete notification --- prisma/schema.prisma | 6 ++-- src/controllers/00-notification-controller.ts | 34 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 520410c..71d3b60 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,7 +29,8 @@ model Notification { createdAt DateTime @default(now()) - readByUser User[] + readByUser User[] @relation(name: "NotificationRead") + deleteByUser User[] @relation(name: "NotificationDelete") } model NotificationGroup { @@ -484,7 +485,8 @@ model User { invoiceCreated Invoice[] paymentCreated Payment[] notificationReceive Notification[] @relation("NotificationReceiver") - notificationRead Notification[] + notificationRead Notification[] @relation("NotificationRead") + notificationDelete Notification[] @relation("NotificationDelete") taskOrderCreated TaskOrder[] @relation("TaskOrderCreatedByUser") creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser") diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index d9b90c0..6b49a76 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -135,11 +135,39 @@ export class NotificationController extends Controller { ); } + @Delete() + @Security("keycloak") + async deleteNotificationMany(@Request() req: RequestWithUser, @Body() notificationId: string[]) { + if (!notificationId.length) return; + + 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 } }, + }, + }), + ), + ); + }); + } + @Delete("{notificationId}") @Security("keycloak") async deleteNotification(@Request() req: RequestWithUser, @Path() notificationId: string) { - const record = await prisma.notification.deleteMany({ where: { id: notificationId } }); - if (record.count === 0) throw notFoundError("Notification"); - return record; + const record = await prisma.notification.findFirst({ where: { id: notificationId } }); + if (!record) throw notFoundError("Notification"); + return await prisma.notification.update({ + where: { id: notificationId }, + data: { + deleteByUser: { + disconnect: { id: req.user.sub }, + }, + }, + }); } } From 3803c3378ab86b9241ba7001e073b0aa21bc4822 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:19:06 +0700 Subject: [PATCH 40/54] chore: migration --- .../migration.sql | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 prisma/migrations/20250305101414_add_mark_delete_noti/migration.sql diff --git a/prisma/migrations/20250305101414_add_mark_delete_noti/migration.sql b/prisma/migrations/20250305101414_add_mark_delete_noti/migration.sql new file mode 100644 index 0000000..5be0ce5 --- /dev/null +++ b/prisma/migrations/20250305101414_add_mark_delete_noti/migration.sql @@ -0,0 +1,48 @@ +/* + 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; From c7fae98516cda62e00900904da46136cd2495d55 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:22:38 +0700 Subject: [PATCH 41/54] feat: clear expired notification date --- src/services/schedule.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/schedule.ts b/src/services/schedule.ts index df15f1f..bd12bf8 100644 --- a/src/services/schedule.ts +++ b/src/services/schedule.ts @@ -1,3 +1,4 @@ +import dayjs from "dayjs"; import { CronJob } from "cron"; import prisma from "../db"; @@ -25,6 +26,18 @@ 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)); + }, + }), ]; export function initSchedule() { From 615ba23e476ecabcf8c673ce88c537fd42394fd2 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:31:26 +0700 Subject: [PATCH 42/54] fix: list notificatio not filtered deleted --- src/controllers/00-notification-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 6b49a76..361c5de 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -60,6 +60,7 @@ export class NotificationController extends Controller { ], NOT: { readByUser: { some: { id: req.user.sub } }, + deleteByUser: { some: { id: req.user.sub } }, createdAt: { lte: dayjs().subtract(7, "days").toDate() }, }, }; From 90246bb3a8a1e4f0aafc108216b70d7ca8b7cf3c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:42:10 +0700 Subject: [PATCH 43/54] fix: wrong endpoint path --- src/controllers/00-notification-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 361c5de..8a953af 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -104,7 +104,7 @@ export class NotificationController extends Controller { return record; } - @Post("notification/mark-read") + @Post("mark-read") @Security("keycloak") async markRead(@Request() req: RequestWithUser, @Body() body?: { id: string[] }) { const record = await prisma.notification.findMany({ From 35fe9c69d1b2735797d4358dcde519cea0ee5748 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:49:47 +0700 Subject: [PATCH 44/54] feat: export report csv endpoint --- package.json | 1 + pnpm-lock.yaml | 24 ++++++++++++++++++ src/controllers/00-stats-controller.ts | 34 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/package.json b/package.json index ced7595..eafc24f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dotenv": "^16.4.7", "express": "^4.21.2", "fast-jwt": "^5.0.5", + "json-2-csv": "^5.5.8", "kysely": "^0.27.5", "minio": "^8.0.2", "morgan": "^1.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7495ebe..85397d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: fast-jwt: specifier: ^5.0.5 version: 5.0.5 + json-2-csv: + specifier: ^5.5.8 + version: 5.5.8 kysely: specifier: ^0.27.5 version: 0.27.5 @@ -904,6 +907,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + deeks@3.1.0: + resolution: {integrity: sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==} + engines: {node: '>= 16'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -932,6 +939,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + doc-path@4.1.1: + resolution: {integrity: sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==} + engines: {node: '>=16'} + docx-templates@4.13.0: resolution: {integrity: sha512-tTmR3WhROYctuyVReQ+PfCU3zprmC45/VuSVzn8EjovzpRkXYUdXiDatB9M8pasj0V+wuuOyY8bcSHvlQ2GNag==} engines: {node: '>=6'} @@ -1535,6 +1546,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + json-2-csv@5.5.8: + resolution: {integrity: sha512-eMQHOwV+av8Sgo+fkbEbQWOw/kwh89AZ5fNA8TYfcooG6TG1ZOL2WcPUrngIMIK8dBJitQ8QEU0zbncQ0CX4CQ==} + engines: {node: '>= 16'} + json-bignum@0.0.3: resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} engines: {node: '>=0.8'} @@ -3778,6 +3793,8 @@ snapshots: decode-uri-component@0.2.2: {} + deeks@3.1.0: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -3811,6 +3828,8 @@ snapshots: dependencies: path-type: 4.0.0 + doc-path@4.1.1: {} + docx-templates@4.13.0: dependencies: jszip: 3.10.1 @@ -4568,6 +4587,11 @@ snapshots: js-tokens@4.0.0: {} + json-2-csv@5.5.8: + dependencies: + deeks: 3.1.0 + doc-path: 4.1.1 + json-bignum@0.0.3: {} json-parse-even-better-errors@2.3.1: {} diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 54edbce..65e057c 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -14,6 +14,7 @@ import { RequestWithUser } from "../interfaces/user"; import { PaymentStatus } from "../generated/kysely/types"; import { precisionRound } from "../utils/arithmetic"; import dayjs from "dayjs"; +import { json2csv } from "json-2-csv"; const permissionCondCompany = createPermCondition((_) => true); @@ -23,6 +24,17 @@ const VAT_DEFAULT = config.vat; @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)); + } + @Get("quotation") async quotationReport( @Request() req: RequestWithUser, @@ -54,6 +66,17 @@ export class StatsController extends Controller { })); } + @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)); + } + @Get("invoice") async invoiceReport( @Request() req: RequestWithUser, @@ -92,6 +115,17 @@ export class StatsController extends Controller { })); } + @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)); + } + @Get("receipt") async receiptReport( @Request() req: RequestWithUser, From ba3ab9f7e42ff64dff15f72f710bde8d9d7acb14 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:51:48 +0700 Subject: [PATCH 45/54] chore: format csv date --- src/controllers/00-stats-controller.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 65e057c..63dbc5c 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -32,7 +32,9 @@ export class StatsController extends Controller { @Query() endDate?: Date, ) { this.setHeader("Content-Type", "text/csv"); - return json2csv(await this.quotationReport(req, limit, startDate, endDate)); + return json2csv(await this.quotationReport(req, limit, startDate, endDate), { + useDateIso8601Format: true, + }); } @Get("quotation") @@ -74,7 +76,9 @@ export class StatsController extends Controller { @Query() endDate?: Date, ) { this.setHeader("Content-Type", "text/csv"); - return json2csv(await this.invoiceReport(req, limit, startDate, endDate)); + return json2csv(await this.invoiceReport(req, limit, startDate, endDate), { + useDateIso8601Format: true, + }); } @Get("invoice") @@ -123,7 +127,9 @@ export class StatsController extends Controller { @Query() endDate?: Date, ) { this.setHeader("Content-Type", "text/csv"); - return json2csv(await this.receiptReport(req, limit, startDate, endDate)); + return json2csv(await this.receiptReport(req, limit, startDate, endDate), { + useDateIso8601Format: true, + }); } @Get("receipt") From 0bd717e8bdf892ce74818e730bcbdf5f913db769 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:41:33 +0700 Subject: [PATCH 46/54] fix: delete still appear in result --- src/controllers/00-notification-controller.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 8a953af..4f00bcf 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -59,9 +59,13 @@ export class NotificationController extends Controller { }, ], NOT: { - readByUser: { some: { id: req.user.sub } }, - deleteByUser: { some: { id: req.user.sub } }, - createdAt: { lte: dayjs().subtract(7, "days").toDate() }, + 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([ @@ -166,7 +170,7 @@ export class NotificationController extends Controller { where: { id: notificationId }, data: { deleteByUser: { - disconnect: { id: req.user.sub }, + connect: { id: req.user.sub }, }, }, }); From 64884875b3f9cdc215b9d89b4068629babb7f3d8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:42:15 +0700 Subject: [PATCH 47/54] chore: cleanup --- src/controllers/00-notification-controller.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 4f00bcf..2bc4bf0 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -5,7 +5,6 @@ import { Get, Path, Post, - Put, Query, Request, Route, @@ -13,18 +12,13 @@ import { Tags, } from "tsoa"; import { RequestWithUser } from "../interfaces/user"; -import HttpStatus from "../interfaces/http-status"; import prisma from "../db"; import { Prisma } from "@prisma/client"; import { queryOrNot } from "../utils/relation"; import { notFoundError } from "../utils/error"; import dayjs from "dayjs"; -import HttpError from "../interfaces/http-error"; import { createPermCondition } from "../services/permission"; -type NotificationCreate = {}; -type NotificationUpdate = {}; - const permissionCondCompany = createPermCondition((_) => true); @Route("/api/v1/notification") From 196c1a80dd8e8414071088391211c25098c77871 Mon Sep 17 00:00:00 2001 From: Kanjana Date: Thu, 6 Mar 2025 10:00:16 +0700 Subject: [PATCH 48/54] add env line liff --- src/controllers/09-web-hook-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/09-web-hook-controller.ts b/src/controllers/09-web-hook-controller.ts index 10587ef..d55126a 100644 --- a/src/controllers/09-web-hook-controller.ts +++ b/src/controllers/09-web-hook-controller.ts @@ -147,7 +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"); - return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n https://taii-cmm.case-collection.com/api/v1/line/employee/${item.employeeId}`; + return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n ${process.env.LINE_LIFF_URL}/${item.employeeId}`; }) .join("\n"); From d6f7c34331d9251a639c6a436ac6ebf555469cd0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:09:35 +0700 Subject: [PATCH 49/54] feat: download product report --- src/controllers/00-stats-controller.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index 63dbc5c..da27349 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -167,6 +167,19 @@ export class StatsController extends Controller { })); } + @Get("receipt/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, From 2b17a7ee3492ee3a0a09421f3550977a02864a53 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:43:04 +0700 Subject: [PATCH 50/54] fix: duplicate endpoint --- src/controllers/00-stats-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index da27349..cf047d4 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -167,7 +167,7 @@ export class StatsController extends Controller { })); } - @Get("receipt/download") + @Get("product/download") async downloadProductReport( @Request() req: RequestWithUser, @Query() limit?: number, From 2db28b14dc887e9d29c962fdbd8e5c1faff864a5 Mon Sep 17 00:00:00 2001 From: Kanjana Date: Thu, 6 Mar 2025 11:20:23 +0700 Subject: [PATCH 51/54] feat: notify complete quotation or work done (#20) * add message * do not throw error but skip notify * change conditions pull userId * do not throw error but skip --------- Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> --- src/controllers/06-request-list-controller.ts | 72 +++++++++++++++++++ src/controllers/07-task-controller.ts | 72 +++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts index 8464c03..b0d78e9 100644 --- a/src/controllers/06-request-list-controller.ts +++ b/src/controllers/06-request-list-controller.ts @@ -30,6 +30,8 @@ import { 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"; // User in company can edit. const permissionCheck = createPermCheck((_) => true); @@ -534,6 +536,14 @@ 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( @@ -944,6 +954,19 @@ export class RequestListController extends Controller { }, }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, + include: { + customerBranch: { + include: { + customer: { + include: { + branch: { + where: { userId: { not: null } }, + }, + }, + }, + }, + }, + }, }) .then(async (res) => { await tx.notification.createMany({ @@ -953,6 +976,55 @@ export class RequestListController extends Controller { receiverId: v.createdByUserId, })), }); + const token = await this.#getLineToken(); + if (!token) return; + + const textHead = "JWS ALERT:"; + + const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา"; + const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว"; + const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏"; + let finalTextWork = ""; + let textData = ""; + + let dataCustomerId: string[] = []; + let textWorkList: string[] = []; + let dataUserId: string[] = []; + + if (res) { + res.forEach((data, index) => { + data.customerBranch.customer.branch.forEach((item) => { + if (!dataCustomerId?.includes(item.id) && item.userId) { + dataCustomerId.push(item.id); + dataUserId.push(item.userId); + } + }); + textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`); + }); + + finalTextWork = textWorkList.join("\n"); + } + + textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`; + + const data = { + to: dataUserId, + messages: [ + { + type: "text", + text: textData, + }, + ], + }; + + await fetch("https://api.line.me/v2/bot/message/multicast", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); }); return record; diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index 2722c04..d4bcfb8 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -573,6 +573,14 @@ 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( @@ -820,6 +828,19 @@ export class TaskActionController extends Controller { }, }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, + include: { + customerBranch: { + include: { + customer: { + include: { + branch: { + where: { userId: { not: null } }, + }, + }, + }, + }, + }, + }, }) .then(async (res) => { await tx.notification.createMany({ @@ -829,6 +850,57 @@ export class TaskActionController extends Controller { receiverId: v.createdByUserId, })), }); + + const token = await this.#getLineToken(); + + if (!token) return; + + const textHead = "JWS ALERT:"; + + const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา"; + const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว"; + const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏"; + let finalTextWork = ""; + let textData = ""; + + let dataCustomerId: string[] = []; + let textWorkList: string[] = []; + let dataUserId: string[] = []; + + if (res) { + res.forEach((data, index) => { + data.customerBranch.customer.branch.forEach((item) => { + if (!dataCustomerId?.includes(item.id) && item.userId) { + dataCustomerId.push(item.id); + dataUserId.push(item.userId); + } + }); + textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`); + }); + + finalTextWork = textWorkList.join("\n"); + } + + textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`; + + const data = { + to: dataUserId, + messages: [ + { + type: "text", + text: textData, + }, + ], + }; + + await fetch("https://api.line.me/v2/bot/message/multicast", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); }); }); } From db4d21bb62379e7a13f9ca2060f77ccf32f20b46 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:38:53 +0700 Subject: [PATCH 52/54] refactor: filter to registered user --- src/controllers/09-web-hook-controller.ts | 80 ++++++++++++----------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/controllers/09-web-hook-controller.ts b/src/controllers/09-web-hook-controller.ts index d55126a..14b7015 100644 --- a/src/controllers/09-web-hook-controller.ts +++ b/src/controllers/09-web-hook-controller.ts @@ -68,37 +68,37 @@ export class WebHookController extends Controller { const userIdLine = payload.events[0]?.source?.userId; const dataNow = dayjs().tz("Asia/Bangkok").startOf("day"); - // const dataUser = await prisma.customerBranch.findFirst({ - // where:{ - // userId:userIdLine - // } - // }) + if (payload?.events[0]?.message) { + const message = payload.events[0].message.text; - const dataEmployee = await prisma.employeePassport.findMany({ - select: { - firstName: true, - firstNameEN: true, - lastName: true, - lastNameEN: true, - employeeId: true, - expireDate: true, - employee: { + if (message === "เมนูหลัก > ข้อความ") { + const dataEmployee = await prisma.employeePassport.findMany({ select: { firstName: true, + firstNameEN: true, lastName: true, - customerBranch: { + lastNameEN: true, + employeeId: true, + expireDate: true, + employee: { select: { firstName: true, - firstNameEN: true, lastName: true, - lastNameEN: true, - customerName: true, - customer: { + customerBranch: { select: { - customerType: true, - registeredBranch: { + firstName: true, + firstNameEN: true, + lastName: true, + lastNameEN: true, + customerName: true, + customer: { select: { - telephoneNo: true, + customerType: true, + registeredBranch: { + select: { + telephoneNo: true, + }, + }, }, }, }, @@ -106,22 +106,28 @@ export class WebHookController extends Controller { }, }, }, - }, - }, - where: { - expireDate: { - lt: dataNow.add(30, "day").toDate(), - }, - }, - orderBy: { - expireDate: "asc", - }, - }); + where: { + employee: { + customerBranch: { + OR: [ + { userId: userIdLine }, + { + customer: { + branch: { some: { userId: userIdLine } }, + }, + }, + ], + }, + }, + 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 = ""; From a979c601438c1096af64ccd7f66cd07932948f71 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:42:49 +0700 Subject: [PATCH 53/54] feat: csv sale export --- src/controllers/00-stats-controller.ts | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts index cf047d4..668f1e7 100644 --- a/src/controllers/00-stats-controller.ts +++ b/src/controllers/00-stats-controller.ts @@ -284,6 +284,47 @@ export class StatsController extends Controller { }); } + @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, From 2beaf6d26c1ff137d59fa193062f76cbf94e2771 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:47:51 +0700 Subject: [PATCH 54/54] feat: filter date --- src/controllers/05-quotation-controller.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index bf0f0e5..d60d6a5 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -168,13 +168,21 @@ const permissionCond = createPermCondition(globalAllow); export class QuotationController extends Controller { @Get("stats") @Security("keycloak") - async getProductStats(@Request() req: RequestWithUser) { + async getQuotationStats( + @Request() req: RequestWithUser, + @Query() startDate?: Date, + @Query() endDate?: Date, + ) { 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, + }, }, });