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

This commit is contained in:
Methapon2001 2025-03-06 11:49:18 +07:00
commit 749d25b1cf
21 changed files with 1453 additions and 305 deletions

View file

@ -33,22 +33,14 @@ 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_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: |

View file

@ -3,8 +3,7 @@ name: Spell Check
permissions:
contents: read
on:
push:
on: [push, pull_request]
env:
CLICOLOR: 1

View file

@ -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",

24
pnpm-lock.yaml generated
View file

@ -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: {}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -21,12 +21,16 @@ model Notification {
groupReceiver NotificationGroup[]
registeredBranchId String?
registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id])
receiver User? @relation(name: "NotificationReceiver", fields: [receiverId], references: [id], onDelete: Cascade)
receiverId String?
createdAt DateTime @default(now())
readByUser User[]
readByUser User[] @relation(name: "NotificationRead")
deleteByUser User[] @relation(name: "NotificationDelete")
}
model NotificationGroup {
@ -313,6 +317,7 @@ model Branch {
quotation Quotation[]
workflowTemplate WorkflowTemplate[]
taskOrder TaskOrder[]
notification Notification[]
}
model BranchBank {
@ -480,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")
@ -1438,6 +1444,9 @@ model RequestData {
requestDataStatus RequestDataStatus @default(Pending)
customerRequestCancel Boolean?
customerRequestCancelReason String?
flow Json?
requestWork RequestWork[]
@ -1473,6 +1482,9 @@ model RequestWork {
stepStatus RequestWorkStepStatus[]
customerRequestCancel Boolean?
customerRequestCancelReason String?
creditNote CreditNote? @relation(fields: [creditNoteId], references: [id], onDelete: SetNull)
creditNoteId String?
}

View file

@ -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() {

View file

@ -5,7 +5,6 @@ import {
Get,
Path,
Post,
Put,
Query,
Request,
Route,
@ -13,10 +12,14 @@ 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 { createPermCondition } from "../services/permission";
type NotificationCreate = {};
type NotificationUpdate = {};
const permissionCondCompany = createPermCondition((_) => true);
@Route("/api/v1/notification")
@Tags("Notification")
@ -29,12 +32,53 @@ 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 } } },
registeredBranch: { OR: permissionCondCompany(req.user) },
}
: {},
],
},
],
NOT: {
OR: [
{
readByUser: { some: { id: req.user.sub } },
createdAt: { lte: dayjs().subtract(7, "days").toDate() },
},
{ deleteByUser: { some: { id: req.user.sub } } },
],
},
};
const [result, total] = await prisma.$transaction([
prisma.notification.findMany({
where,
include: { readByUser: true },
orderBy: { createdAt: "desc" },
}),
prisma.notification.count({ where }),
]);
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,
@ -44,37 +88,85 @@ export class NotificationController extends Controller {
@Get("{notificationId}")
@Security("keycloak")
async getNotification(@Request() req: RequestWithUser, @Path() notificationId: string) {
// TODO: implement
const record = await prisma.notification.update({
where: { id: notificationId },
data: {
readByUser: {
connect: { id: req.user.sub },
},
},
});
return {};
if (!record) throw notFoundError("Notification");
return record;
}
@Post()
@Post("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);
return {};
await prisma.$transaction(
record.map((v) =>
prisma.notification.update({
where: { id: v.id },
data: {
readByUser: { connect: { id: req.user.sub } },
},
}),
),
);
}
@Put("{notificationId}")
@Delete()
@Security("keycloak")
async updateNotification(
@Request() req: RequestWithUser,
@Path() notificationId: string,
@Body() body: NotificationUpdate,
) {
// TODO: implement
async deleteNotificationMany(@Request() req: RequestWithUser, @Body() notificationId: string[]) {
if (!notificationId.length) return;
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) {
// TODO: implement
return {};
const record = await prisma.notification.findFirst({ where: { id: notificationId } });
if (!record) throw notFoundError("Notification");
return await prisma.notification.update({
where: { id: notificationId },
data: {
deleteByUser: {
connect: { id: req.user.sub },
},
},
});
}
}

View file

@ -0,0 +1,566 @@
import config from "../config.json";
import {
Customer,
CustomerBranch,
ProductGroup,
QuotationStatus,
RequestWorkStatus,
User,
} from "@prisma/client";
import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa";
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";
import dayjs from "dayjs";
import { json2csv } from "json-2-csv";
const permissionCondCompany = createPermCondition((_) => true);
const VAT_DEFAULT = config.vat;
@Route("/api/v1/report")
@Security("keycloak")
@Tags("Report")
export class StatsController extends Controller {
@Get("quotation/download")
async downloadQuotationReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(await this.quotationReport(req, limit, startDate, endDate), {
useDateIso8601Format: true,
});
}
@Get("quotation")
async quotationReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.quotation.findMany({
select: {
code: true,
quotationStatus: true,
createdAt: true,
updatedAt: true,
},
where: {
registeredBranch: { OR: permissionCondCompany(req.user) },
createdAt: { gte: startDate, lte: endDate },
},
orderBy: { createdAt: "desc" },
take: limit,
});
return record.map((v) => ({
document: "quotation",
code: v.code,
status: v.quotationStatus,
createdAt: v.createdAt,
updatedAt: v.updatedAt,
}));
}
@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), {
useDateIso8601Format: true,
});
}
@Get("invoice")
async invoiceReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.invoice.findMany({
select: {
code: true,
payment: {
select: {
paymentStatus: true,
},
},
amount: true,
createdAt: true,
},
where: {
quotation: {
isDebitNote: false,
registeredBranch: { OR: permissionCondCompany(req.user) },
},
createdAt: { gte: startDate, lte: endDate },
},
orderBy: { createdAt: "desc" },
take: limit,
});
return record.map((v) => ({
document: "invoice",
code: v.code,
status: v.payment?.paymentStatus,
amount: v.amount,
createdAt: v.createdAt,
}));
}
@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), {
useDateIso8601Format: true,
});
}
@Get("receipt")
async receiptReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.payment.findMany({
select: {
code: true,
paymentStatus: true,
createdAt: true,
},
where: {
paymentStatus: PaymentStatus.PaymentSuccess,
invoice: {
quotation: {
isDebitNote: false,
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
createdAt: { gte: startDate, lte: endDate },
},
orderBy: { createdAt: "desc" },
take: limit,
});
return record.map((v) => ({
document: "receipt",
code: v.code,
status: v.paymentStatus,
createdAt: v.createdAt,
}));
}
@Get("product/download")
async downloadProductReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(await this.productReport(req, limit, startDate, endDate), {
useDateIso8601Format: true,
});
}
@Get("product")
async productReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await prisma.$transaction(async (tx) => {
const record = await tx.product.findMany({
select: {
id: true,
code: true,
name: true,
createdAt: true,
updatedAt: true,
quotationProductServiceList: {
include: { quotation: true },
},
_count: {
select: {
quotationProductServiceList: {
where: {
quotation: {
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
},
},
},
},
},
},
where: {
quotationProductServiceList: {
some: {
quotation: { createdAt: { gte: startDate, lte: endDate } },
},
},
productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } },
},
orderBy: {
quotationProductServiceList: { _count: "desc" },
},
take: limit,
});
const doing = await tx.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
requestWork: {
some: {
stepStatus: {
some: {
workStatus: {
in: [
RequestWorkStatus.Pending,
RequestWorkStatus.InProgress,
RequestWorkStatus.Validate,
RequestWorkStatus.Completed,
RequestWorkStatus.Ended,
],
},
},
},
},
},
},
});
const order = await tx.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
},
});
return record.map((v) => ({
document: "product",
code: v.code,
name: v.name,
sale: v._count.quotationProductServiceList,
did: doing.find((item) => item.productId === v.id)?._count || 0,
order: order.find((item) => item.productId === v.id)?._count || 0,
createdAt: v.createdAt,
updatedAt: v.updatedAt,
}));
});
}
@Get("sale/by-product-group/download")
async downloadSaleByProductGroupReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.saleReport(req, limit, startDate, endDate).then((v) => v.byProductGroup),
{ useDateIso8601Format: true },
);
}
@Get("sale/by-sale/download")
async downloadSaleBySaleReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(await this.saleReport(req, limit, startDate, endDate).then((v) => v.bySale), {
useDateIso8601Format: true,
});
}
@Get("sale/by-customer/download")
async downloadSaleByCustomerReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
this.setHeader("Content-Type", "text/csv");
return json2csv(
await this.saleReport(req, limit, startDate, endDate).then((v) => v.byCustomer),
{ useDateIso8601Format: true },
);
}
@Get("sale")
async saleReport(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const list = await prisma.quotationProductServiceList.findMany({
include: {
quotation: {
include: {
createdBy: true,
customerBranch: {
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: [] },
);
}
@Get("profit")
async profit(
@Request() req: RequestWithUser,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.quotationProductServiceList.findMany({
include: {
work: {
include: {
productOnWork: {
select: { stepCount: true, productId: true },
},
},
},
product: {
select: {
agentPrice: true,
agentPriceCalcVat: true,
agentPriceVatIncluded: true,
serviceCharge: true,
serviceChargeCalcVat: true,
serviceChargeVatIncluded: true,
price: true,
calcVat: true,
vatIncluded: true,
},
},
requestWork: {
include: {
stepStatus: true,
creditNote: true,
},
},
quotation: {
select: {
agentPrice: true,
creditNote: true,
},
},
},
where: {
quotation: {
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
registeredBranch: {
OR: permissionCondCompany(req.user),
},
createdAt: { gte: startDate, lte: endDate },
},
},
});
const data = record.map((v) => {
const originalPrice = v.product.serviceCharge;
const productExpenses = precisionRound(
originalPrice + (v.product.serviceChargeVatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const finalPrice = v.pricePerUnit * v.amount * (1 + config.vat);
return v.requestWork.map((w) => {
const creditNote = w.creditNote;
const roundCount = v.work?.productOnWork.find((p) => p.productId)?.stepCount || 1;
const successCount = w.stepStatus.filter(
(s) => s.workStatus !== RequestWorkStatus.Canceled,
).length;
const income = creditNote
? precisionRound(productExpenses * successCount)
: precisionRound(finalPrice);
const expenses = creditNote
? precisionRound(productExpenses * successCount)
: precisionRound(productExpenses * roundCount);
const netProfit = creditNote ? 0 : precisionRound(finalPrice - expenses);
return {
income,
expenses,
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 },
);
}
@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() },
invoice: {
quotation: {
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
by: "paymentStatus",
})
.then((v) =>
v.reduce<Partial<Record<(typeof v)[number]["paymentStatus"], number>>>((a, c) => {
a[c.paymentStatus] = c._sum.amount || 0;
return a;
}, {}),
),
};
}),
);
});
return data;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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: "รหัส / code : " + res.code + " " + res.quotationStatus,
receiverId: res.createdByUserId,
},
});
});
return payment;
});

View file

@ -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,
},
},
});
@ -454,7 +462,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 +647,17 @@ export class QuotationController extends Controller {
},
});
});
await prisma.notification.create({
data: {
title: "ใบเสนอราคาใหม่ / New Quotation",
detail: "รหัส / code : " + ret.code,
registeredBranchId: ret.registeredBranchId,
groupReceiver: { create: [{ name: "accountant" }, { name: "head_of_accountant" }] },
},
});
return ret;
}
@Put("{quotationId}")
@ -1125,41 +1144,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: "รหัส / code : " + ret.requestData.map((v) => v.code).join(", "),
registeredBranchId: ret.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
});
});
}
}

View file

@ -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);
@ -268,14 +270,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: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
}),
tx.taskOrder.updateMany({
where: {
taskList: {
@ -405,14 +417,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: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
}),
tx.taskOrder.updateMany({
where: {
taskList: {
@ -479,21 +502,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: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
});
});
// dataRecord.push(record);
return data;
});
@ -503,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(
@ -812,14 +853,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: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
}),
tx.taskOrder.updateMany({
where: {
taskList: {
@ -887,19 +939,94 @@ 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 },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
},
},
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
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;
});
}

View file

@ -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: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
},
});
}
return ret;
});
});
}
@Delete("{taskOrderId}")
@ -560,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(
@ -651,6 +672,13 @@ export class TaskActionController extends Controller {
},
data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() },
}),
prisma.notification.create({
data: {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
},
}),
]);
}
@ -785,19 +813,95 @@ 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 },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
},
},
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
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),
});
});
});
}
}
@ -965,20 +1069,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,

View file

@ -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}")

View file

@ -68,37 +68,37 @@ export class WebHookController extends Controller {
const userIdLine = payload.events[0]?.source?.userId;
const dataNow = dayjs().tz("Asia/Bangkok").startOf("day");
// 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 = "";
@ -147,7 +153,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");

View file

@ -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() {

View file

@ -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" }
]
}
},