Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
This commit is contained in:
commit
749d25b1cf
21 changed files with 1453 additions and 305 deletions
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ name: Spell Check
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CLICOLOR: 1
|
||||
|
|
|
|||
|
|
@ -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
24
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
566
src/controllers/00-stats-controller.ts
Normal file
566
src/controllers/00-stats-controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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" } },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue