Compare commits
364 commits
version-0.
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
426ffb27a7 | ||
|
|
5d3997343f | ||
|
|
a10626e756 | ||
|
|
c11fed9832 | ||
|
|
8909a763c9 | ||
|
|
f0a106e5fe | ||
|
|
84b9ddcd2b | ||
|
|
e54f62a5b3 | ||
|
|
cef26278ba | ||
| 16c4c64c89 | |||
|
|
78669ed7ae | ||
|
|
5dc88c22dc | ||
|
|
3454e46212 | ||
|
|
334fb57b46 | ||
|
|
a9201f715a | ||
|
|
95ee32fc57 | ||
|
|
a33983c530 | ||
|
|
e8278b6af3 | ||
|
|
9ef006c860 | ||
|
|
8d52a5e726 | ||
|
|
7858291ae5 | ||
|
|
6ea672a2cb | ||
|
|
a426e18025 | ||
|
|
0930c3c833 | ||
|
|
be3c6405c6 | ||
|
|
f50285161b | ||
|
|
4691d559f5 | ||
|
|
7e7b8025c9 | ||
|
|
e5a3d948a5 | ||
|
|
0d78ce4db3 | ||
|
|
9e74fb0fe6 | ||
|
|
ab4ea4ba4b | ||
|
|
068ba2d293 | ||
|
|
beb7f4bcfe | ||
|
|
b6b35509e3 | ||
|
|
6598cd3bdf | ||
|
|
2c790de606 | ||
|
|
6f1bca5234 | ||
|
|
158a6ff163 | ||
|
|
0772e4710a | ||
|
|
25a4b50f8e | ||
|
|
de33d03631 | ||
|
|
4e71343af7 | ||
|
|
6776188f7b | ||
|
|
892d76583f | ||
|
|
f2def1b962 | ||
|
|
1486ce79ab | ||
|
|
d51531cd4d | ||
|
|
61825309d1 | ||
|
|
250bbca226 | ||
|
|
c774e9f44c | ||
|
|
8f2810ea29 | ||
|
|
eda0edbd29 | ||
|
|
86db927efe | ||
|
|
5674a18cc3 | ||
|
|
d3c5b49649 | ||
|
|
c47ffb5435 | ||
|
|
710382d544 | ||
|
|
4042cbcea4 | ||
|
|
f487a9169c | ||
|
|
ab8fd2ca43 | ||
|
|
d95eb349ec | ||
|
|
893eb4cca5 | ||
|
|
2c9fae400c | ||
|
|
df38eebbcc | ||
|
|
c2eaa5fba8 | ||
|
|
1789fe1de0 | ||
|
|
a0bb23e1e8 | ||
|
|
c9939bf8bb | ||
|
|
f162081370 | ||
|
|
dfe7bd16d8 | ||
|
|
bfc2608af4 | ||
|
|
23334a4388 | ||
|
|
9e208dee89 | ||
|
|
a30bc33b81 | ||
|
|
c7183887c9 | ||
|
|
c34af75bad | ||
|
|
5de1f27fca | ||
|
|
8a87e37097 | ||
|
|
92762c512d | ||
|
|
3455ae604a | ||
|
|
afb89ef949 | ||
|
|
fa8aa8c9b6 | ||
|
|
5c824a738a | ||
|
|
e4caeaa780 | ||
|
|
46ba857e28 | ||
|
|
b7a13b2d7a | ||
|
|
5aa8b06cf2 | ||
|
|
f90ee41a56 | ||
|
|
2ee0e97953 | ||
|
|
236ee48eab | ||
|
|
6c350b12ce | ||
|
|
0c53ac69b0 | ||
|
|
0032ff4658 | ||
|
|
1f34ea7ecb | ||
|
|
ccee309268 | ||
|
|
1cf53c91aa | ||
|
|
36c9c25f61 | ||
|
|
1f63089363 | ||
|
|
c2195d4448 | ||
|
|
7112a545b1 | ||
|
|
15e0e34a47 | ||
|
|
126e00baf1 | ||
|
|
163f07758e | ||
|
|
73fea9d9ed | ||
|
|
50fca4d540 | ||
|
|
a61bd8c83e | ||
|
|
8fb28ec3ab | ||
|
|
ab1d5f1326 | ||
|
|
e74516ce3b | ||
|
|
f9c4d579c4 | ||
|
|
842d81026e | ||
|
|
86085a74ba | ||
|
|
3d98f9d0ad | ||
|
|
8d25dda326 | ||
|
|
138031f662 | ||
|
|
e0be1f6ab5 | ||
|
|
859a1e3def | ||
|
|
2b255ff355 | ||
|
|
ced55b9518 | ||
|
|
1e0f97cdef | ||
|
|
5c7db2afc6 | ||
|
|
d6212e9ba4 | ||
|
|
68025aad08 | ||
|
|
d08327afb6 | ||
|
|
afb725fceb | ||
|
|
b0e941085e | ||
|
|
6d44d2979b | ||
|
|
fa95fe46a5 | ||
|
|
41f5de7fd0 | ||
|
|
425e99bfde | ||
|
|
15381c089c | ||
|
|
f7ec18fd7e | ||
|
|
acd6bb35e9 | ||
|
|
1a33080ac8 | ||
|
|
b4470d9a0a | ||
|
|
a69962db48 | ||
|
|
96bdb86c73 | ||
|
|
7bd685ea96 | ||
|
|
5262ad2a63 | ||
|
|
c430fc3c7a | ||
|
|
47907f61ab | ||
|
|
e02a29f053 | ||
|
|
1fe013d69d | ||
|
|
1896e2385d | ||
|
|
f7c81641b2 | ||
|
|
791e8b4977 | ||
|
|
125f708ac6 | ||
|
|
0aa20d3728 | ||
|
|
897ef335b4 | ||
|
|
0affb5337f | ||
|
|
b276ccddd1 | ||
|
|
2d0d977617 | ||
|
|
feda84de1c | ||
|
|
106343d33d | ||
|
|
2fa50bd7de | ||
|
|
07e5f53be2 | ||
|
|
8a4317c94e | ||
|
|
f1a774f3bc | ||
|
|
ce42a6dca6 | ||
|
|
8a3a9e7eb3 | ||
|
|
7fe0512a2f | ||
|
|
5c75c27470 | ||
|
|
7bc12f00b0 | ||
|
|
ffb1ce2d40 | ||
|
|
5536331984 | ||
|
|
a57e8d939f | ||
|
|
92104c05cb | ||
|
|
4dbe89f290 | ||
|
|
08b9ddd2e1 | ||
|
|
d92e3bc57d | ||
|
|
5594fabb6a | ||
|
|
109494c6d7 | ||
|
|
afadea2d64 | ||
|
|
1d6224da73 | ||
|
|
d15aa488c1 | ||
|
|
601deffce4 | ||
|
|
3193403f90 | ||
|
|
8b26f91dba | ||
|
|
94c7de89eb | ||
|
|
3bf2446611 | ||
|
|
027326a9e4 | ||
|
|
40e5f495e5 | ||
|
|
35ec6cc061 | ||
|
|
7bd1f57c32 | ||
|
|
209ef05d3d | ||
|
|
b90547c622 | ||
|
|
f98371132a | ||
|
|
a25968786d | ||
|
|
e42b772dcf | ||
|
|
05d16f22de | ||
|
|
fd7833a592 | ||
|
|
d52680c23f | ||
|
|
27d3ce6573 | ||
|
|
5147eed15b | ||
|
|
0aba9f9865 | ||
|
|
f2d0c20ece | ||
|
|
ee610c5686 | ||
|
|
62def572de | ||
|
|
a06d5514fc | ||
|
|
70245a2b4f | ||
|
|
7b28ddd2b2 | ||
|
|
1cd292cdf7 | ||
|
|
2e71c86b36 | ||
|
|
08483c162a | ||
|
|
afe54b1a4e | ||
|
|
743fde5493 | ||
|
|
7e937333dc | ||
|
|
b55998da4a | ||
|
|
dab554e401 | ||
|
|
2307a275f5 | ||
|
|
709c21082a | ||
|
|
116fa02bd9 | ||
|
|
d961ab086b | ||
|
|
afca10983d | ||
|
|
af4093d439 | ||
|
|
c1cd2b9518 | ||
|
|
0f648ab8a3 | ||
|
|
f98028fe6e | ||
|
|
9c7921fa7c | ||
|
|
002c2861f3 | ||
|
|
30815a462c | ||
|
|
df8ef4db33 | ||
|
|
4c47b0b7d1 | ||
|
|
1091b93805 | ||
|
|
5fc4bbab6e | ||
|
|
f88b25a52a | ||
|
|
c2b4602290 | ||
|
|
c6c187b8d3 | ||
|
|
5ce356648d | ||
|
|
c5d250ab0c | ||
|
|
46dea5514e | ||
|
|
11c3ce4272 | ||
|
|
0d002e59bd | ||
|
|
2f148b6f13 | ||
|
|
8dcec9ac96 | ||
|
|
65b1b516c2 | ||
|
|
d2c2d94a2d | ||
|
|
676bd1d16d | ||
|
|
1a6a19574d | ||
|
|
b64cd5d1a6 | ||
|
|
e76abd787c | ||
|
|
cfe820b89f | ||
|
|
536e812394 | ||
|
|
910dc196c5 | ||
|
|
422183b1c1 | ||
|
|
ae4488b76a | ||
|
|
0020eb6528 | ||
|
|
56318f581c | ||
|
|
542c260aba | ||
|
|
6263406945 | ||
|
|
958cac0f3b | ||
|
|
b32bfee49e | ||
|
|
140cb59ebb | ||
|
|
e6903af8ce | ||
|
|
a9de9a6469 | ||
|
|
ffabcd6ea0 | ||
|
|
19502acd8a | ||
|
|
2c6205d01a | ||
|
|
250d69d122 | ||
|
|
5f499eeda5 | ||
|
|
c6e4959e84 | ||
|
|
041e219059 | ||
|
|
78007f1cba | ||
|
|
329c69ec3d | ||
|
|
fbd423f84d | ||
|
|
26c671b032 | ||
|
|
749d25b1cf | ||
|
|
2beaf6d26c | ||
|
|
a979c60143 | ||
|
|
db4d21bb62 | ||
|
|
95fc06ce5f | ||
|
|
2db28b14dc | ||
|
|
2b17a7ee34 | ||
|
|
d6f7c34331 | ||
|
|
196c1a80dd | ||
|
|
64884875b3 | ||
|
|
0bd717e8bd | ||
|
|
ba3ab9f7e4 | ||
|
|
35fe9c69d1 | ||
|
|
90246bb3a8 | ||
|
|
615ba23e47 | ||
|
|
c7fae98516 | ||
|
|
3803c3378a | ||
|
|
9bf534adce | ||
|
|
6265ac8a65 | ||
|
|
c6a56df94a | ||
|
|
b518488112 | ||
|
|
f583448e5f | ||
|
|
17b92b4012 | ||
|
|
afb4a83efa | ||
|
|
9ffbed7cb8 | ||
|
|
2697b4f6e0 | ||
|
|
071262a85a | ||
|
|
c0bc31714e | ||
|
|
0eed3c55f6 | ||
|
|
b190f93ec3 | ||
|
|
8abca9b137 | ||
|
|
3a437d78d4 | ||
|
|
1add3f3ba0 | ||
|
|
57641681ea | ||
|
|
1f2a063974 | ||
|
|
5842935a54 | ||
|
|
c21ef1448b | ||
|
|
f0db968b20 | ||
|
|
53c0c0fce9 | ||
|
|
34af1f9dcd | ||
|
|
ae252acbb8 | ||
|
|
549410e9e3 | ||
|
|
b4b7d633d1 | ||
|
|
2502b7c68f | ||
|
|
5c58953820 | ||
|
|
5d78166047 | ||
|
|
9bd24b5a83 | ||
|
|
b4df1a5d4e | ||
|
|
54dba11dc4 | ||
|
|
e92870602d | ||
|
|
3b29f56100 | ||
|
|
9fb4a7a88c | ||
|
|
e9889a1682 | ||
|
|
b36b6f9f07 | ||
|
|
ffa8095dcc | ||
|
|
c004c516c6 | ||
|
|
ba697d006a | ||
|
|
feb792def9 | ||
|
|
60be5f5103 | ||
|
|
28c3930f80 | ||
|
|
d40bd5eb8c | ||
|
|
3b59aa1e14 | ||
|
|
f56088d101 | ||
|
|
aac73f334c | ||
|
|
bf9dac8d46 | ||
|
|
e2023ab0ed | ||
|
|
afa2808a0c | ||
|
|
d373c8dbf9 | ||
|
|
6f1969c829 | ||
|
|
ccbf21f71d | ||
|
|
9c55a37991 | ||
|
|
45482c3a1b | ||
|
|
9590803e40 | ||
|
|
ae37e7c955 | ||
|
|
b8ccfbbc02 | ||
|
|
8c936a830a | ||
|
|
c36d22356d | ||
|
|
a4850df120 | ||
|
|
64e53fcdaa | ||
|
|
18021193a6 | ||
|
|
55cb0d157c | ||
|
|
042f538a5a | ||
|
|
e1c2768167 | ||
|
|
473a13e5dc | ||
|
|
6d4f598fe0 | ||
|
|
f5537b590b | ||
|
|
87d9f39290 | ||
|
|
2e38ea22d5 | ||
|
|
b45753da2a | ||
|
|
feffe6d52f | ||
|
|
da8cca2ee4 | ||
|
|
979af5b672 | ||
|
|
470921c0df | ||
|
|
806a905d40 | ||
|
|
230eae5c89 | ||
|
|
c4d45a137d | ||
|
|
9a4c97c109 |
95 changed files with 8620 additions and 1268 deletions
77
.forgejo/workflows/deploy.yaml
Normal file
77
.forgejo/workflows/deploy.yaml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
name: Deploy Local
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
|
||||
REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
|
||||
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.REGISTRY_USERNAME }}
|
||||
password: ${{ env.REGISTRY_PASSWORD }}
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
config-inline: |
|
||||
[registry."${{ env.REGISTRY }}"]
|
||||
ca=["/etc/ssl/certs/ca-certificates.crt"]
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
tags: ${{ env.CONTAINER_IMAGE_NAME }}
|
||||
push: true
|
||||
- name: Remote Deploy
|
||||
uses: appleboy/ssh-action@v1.2.1
|
||||
with:
|
||||
host: ${{ vars.SSH_DEPLOY_HOST }}
|
||||
port: ${{ vars.SSH_DEPLOY_PORT }}
|
||||
username: ${{ secrets.SSH_DEPLOY_USER }}
|
||||
password: ${{ secrets.SSH_DEPLOY_PASSWORD }}
|
||||
script: eval "${{ secrets.SSH_DEPLOY_CMD }}"
|
||||
- name: Notify Discord Success
|
||||
if: success()
|
||||
run: |
|
||||
curl -H "Content-Type: application/json" -X POST \
|
||||
-d '{
|
||||
"embeds": [{
|
||||
"title": "✅ Gitea Local Deployment Success!",
|
||||
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Deployed by: `${{ github.actor }}`",
|
||||
"color": 3066993,
|
||||
"footer": {
|
||||
"text": "Local Release Notification",
|
||||
"icon_url": "https://example.com/success-icon.png"
|
||||
},
|
||||
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
|
||||
}]
|
||||
}' \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
- name: Notify Discord Failure
|
||||
if: failure()
|
||||
run: |
|
||||
curl -H "Content-Type: application/json" -X POST \
|
||||
-d '{
|
||||
"embeds": [{
|
||||
"title": "❌ Gitea Local Deployment Failed!",
|
||||
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Attempted by: `${{ github.actor }}`",
|
||||
"color": 15158332,
|
||||
"footer": {
|
||||
"text": "Local Release Notification",
|
||||
"icon_url": "https://example.com/failure-icon.png"
|
||||
},
|
||||
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
|
||||
}]
|
||||
}' \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
21
.forgejo/workflows/spellcheck.yaml
Normal file
21
.forgejo/workflows/spellcheck.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: Spell Check
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CLICOLOR: 1
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Spell Check Repo
|
||||
uses: crate-ci/typos@v1.29.9
|
||||
with:
|
||||
files: ./src
|
||||
20
.github/ISSUE_TEMPLATE/✨-feature-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/✨-feature-request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: "✨ Feature Request"
|
||||
about: Suggest an idea for this project
|
||||
title: "✨ Feature: "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
38
.github/ISSUE_TEMPLATE/🐞-bug-report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/🐞-bug-report.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
name: "\U0001F41E Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "\U0001F41E Bug: "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
31
.github/workflows/local-build-dev.yaml
vendored
31
.github/workflows/local-build-dev.yaml
vendored
|
|
@ -1,31 +0,0 @@
|
|||
name: local-build-dev
|
||||
|
||||
# Intended for local network use.
|
||||
# Remote access is possible if the host has a public IP address.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
|
||||
|
||||
jobs:
|
||||
local-build-dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
config-inline: |
|
||||
[registry."${{ env.REGISTRY }}"]
|
||||
http = true
|
||||
insecure = true
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/jws/jws-backend:dev
|
||||
allow: security.insecure
|
||||
31
.github/workflows/local-build-release.yaml
vendored
31
.github/workflows/local-build-release.yaml
vendored
|
|
@ -1,31 +0,0 @@
|
|||
name: local-build-release
|
||||
|
||||
# Intended for local network use.
|
||||
# Remote access is possible if the host has a public IP address.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
|
||||
|
||||
jobs:
|
||||
local-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
config-inline: |
|
||||
[registry."${{ env.REGISTRY }}"]
|
||||
http = true
|
||||
insecure = true
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/jws/jws-backend:latest
|
||||
allow: security.insecure
|
||||
24
.github/workflows/local-release-demo.yaml
vendored
24
.github/workflows/local-release-demo.yaml
vendored
|
|
@ -1,24 +0,0 @@
|
|||
name: local-release-demo
|
||||
|
||||
# Intended for local network use.
|
||||
# Remote access is possible if the host has a public IP address.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
local-release-demo:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remote deploy internal chamomind server
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
script: |
|
||||
cd ~/repositories/jws-backend
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
sleep 1
|
||||
docker compose logs -n 100
|
||||
24
.github/workflows/local-release-dev.yaml
vendored
24
.github/workflows/local-release-dev.yaml
vendored
|
|
@ -1,24 +0,0 @@
|
|||
name: local-release-dev
|
||||
|
||||
# Intended for local network use.
|
||||
# Remote access is possible if the host has a public IP address.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
local-release-dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remote deploy internal chamomind server
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
script: |
|
||||
cd ~/repositories/jws-backend
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
sleep 1
|
||||
docker compose logs -n 100
|
||||
2
.typos.toml
Normal file
2
.typos.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[default]
|
||||
extend-ignore-re = ["(?Rm)^.*(#|//)\\s*spellchecker:disable-line$"]
|
||||
35
Dockerfile
35
Dockerfile
|
|
@ -1,33 +1,22 @@
|
|||
FROM node:23-slim AS base
|
||||
FROM node:20-slim
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable
|
||||
RUN apt-get update && apt-get install -y openssl
|
||||
RUN pnpm i -g prisma prisma-kysely
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y openssl \
|
||||
&& npm install -g pnpm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
FROM base AS deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
RUN pnpm prisma generate
|
||||
|
||||
FROM base AS build
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm prisma generate
|
||||
RUN pnpm run build
|
||||
|
||||
FROM base AS prod
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/dist /app/dist
|
||||
COPY --from=base /app/static /app/static
|
||||
|
||||
RUN chmod u+x ./entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
|
|
|||
21
package.json
21
package.json
|
|
@ -7,7 +7,9 @@
|
|||
"start": "node ./dist/app.js",
|
||||
"dev": "nodemon",
|
||||
"check": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"format": "prettier --write .",
|
||||
"debug": "nodemon",
|
||||
"build": "tsoa spec-and-routes && tsc",
|
||||
"changelog:generate": "git-cliff -o CHANGELOG.md && git add CHANGELOG.md && git commit -m 'Update CHANGELOG.md'",
|
||||
"db:generate": "prisma generate",
|
||||
|
|
@ -19,36 +21,49 @@
|
|||
"author": "Frappe'T",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/barcode": "^0.0.33",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.17.10",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@vitest/ui": "^3.1.4",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^6.3.0",
|
||||
"prisma": "6.16.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.17.0",
|
||||
"@fast-csv/parse": "^5.0.2",
|
||||
"@prisma/client": "^6.3.0",
|
||||
"@prisma/client": "6.16.2",
|
||||
"@scalar/express-api-reference": "^0.4.182",
|
||||
"@tsoa/runtime": "^6.6.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"canvas": "^3.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^3.3.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs-plugin-utc": "^0.1.2",
|
||||
"docx-templates": "^4.13.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.21.2",
|
||||
"fast-jwt": "^5.0.5",
|
||||
"html-to-text": "^9.0.5",
|
||||
"jsbarcode": "^3.11.6",
|
||||
"json-2-csv": "^5.5.8",
|
||||
"kysely": "^0.27.5",
|
||||
"minio": "^8.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"pnpm": "^10.18.3",
|
||||
"prisma-extension-kysely": "^3.0.0",
|
||||
"promise.any": "^2.0.6",
|
||||
"thai-baht-text": "^2.0.5",
|
||||
|
|
|
|||
1766
pnpm-lock.yaml
generated
1766
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "CreditNoteStatus" ADD VALUE 'Waiting';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ALTER COLUMN "creditNoteStatus" SET DEFAULT 'Waiting';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Property" (
|
||||
"id" TEXT NOT NULL,
|
||||
"registeredBranchId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameEN" TEXT NOT NULL,
|
||||
"type" JSONB NOT NULL,
|
||||
"status" "Status" NOT NULL DEFAULT 'CREATED',
|
||||
"statusOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Property_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Property" ADD CONSTRAINT "Property_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "RequestData" ADD COLUMN "rejectRequestCancel" BOOLEAN,
|
||||
ADD COLUMN "rejectRequestCancelReason" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "RequestWork" ADD COLUMN "rejectRequestCancel" BOOLEAN,
|
||||
ADD COLUMN "rejectRequestCancelReason" TEXT;
|
||||
5
prisma/migrations/20250403074007_add/migration.sql
Normal file
5
prisma/migrations/20250403074007_add/migration.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "RequestData" ADD COLUMN "defaultMessengerId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RequestData" ADD CONSTRAINT "RequestData_defaultMessengerId_fkey" FOREIGN KEY ("defaultMessengerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
29
prisma/migrations/20250404040202_add/migration.sql
Normal file
29
prisma/migrations/20250404040202_add/migration.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Employee" ALTER COLUMN "firstName" DROP NOT NULL,
|
||||
ALTER COLUMN "lastName" DROP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Institution" ADD COLUMN "contactEmail" TEXT,
|
||||
ADD COLUMN "contactName" TEXT,
|
||||
ADD COLUMN "contactTel" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "agencyStatus" TEXT,
|
||||
ADD COLUMN "remark" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InstitutionBank" (
|
||||
"id" TEXT NOT NULL,
|
||||
"bankName" TEXT NOT NULL,
|
||||
"bankBranch" TEXT NOT NULL,
|
||||
"accountName" TEXT NOT NULL,
|
||||
"accountNumber" TEXT NOT NULL,
|
||||
"accountType" TEXT NOT NULL,
|
||||
"currentlyUse" BOOLEAN NOT NULL,
|
||||
"institutionId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "InstitutionBank_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InstitutionBank" ADD CONSTRAINT "InstitutionBank_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "Institution"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
2
prisma/migrations/20250404040846_add/migration.sql
Normal file
2
prisma/migrations/20250404040846_add/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Employee" ALTER COLUMN "dateOfBirth" DROP NOT NULL;
|
||||
5
prisma/migrations/20250404071034_add/migration.sql
Normal file
5
prisma/migrations/20250404071034_add/migration.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- DropForeignKey
|
||||
ALTER TABLE "InstitutionBank" DROP CONSTRAINT "InstitutionBank_institutionId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InstitutionBank" ADD CONSTRAINT "InstitutionBank_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "Institution"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
3
prisma/migrations/20250410102415_add/migration.sql
Normal file
3
prisma/migrations/20250410102415_add/migration.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "contactName" TEXT,
|
||||
ADD COLUMN "contactTel" TEXT;
|
||||
3
prisma/migrations/20250410104307_change/migration.sql
Normal file
3
prisma/migrations/20250410104307_change/migration.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "firstName" DROP NOT NULL,
|
||||
ALTER COLUMN "lastName" DROP NOT NULL;
|
||||
2
prisma/migrations/20250418095201_add/migration.sql
Normal file
2
prisma/migrations/20250418095201_add/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "TaskOrder" ADD COLUMN "codeProductReceived" TEXT;
|
||||
18
prisma/migrations/20250418103300_add/migration.sql
Normal file
18
prisma/migrations/20250418103300_add/migration.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Institution" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "createdByUserId" TEXT,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedByUserId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Payment" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedByUserId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Institution" ADD CONSTRAINT "Institution_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Institution" ADD CONSTRAINT "Institution_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Employee" ALTER COLUMN "lastNameEN" DROP NOT NULL;
|
||||
11
prisma/migrations/20250424042834_add/migration.sql
Normal file
11
prisma/migrations/20250424042834_add/migration.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "WorkflowTemplateStepGroup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"group" TEXT NOT NULL,
|
||||
"workflowTemplateStepId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "WorkflowTemplateStepGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkflowTemplateStepGroup" ADD CONSTRAINT "WorkflowTemplateStepGroup_workflowTemplateStepId_fkey" FOREIGN KEY ("workflowTemplateStepId") REFERENCES "WorkflowTemplateStep"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `importNationality` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "importNationality";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserImportNationality" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserImportNationality_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
2
prisma/migrations/20250425040315_add/migration.sql
Normal file
2
prisma/migrations/20250425040315_add/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Employee" ADD COLUMN "otherNationality" TEXT;
|
||||
2
prisma/migrations/20250425041426_add/migration.sql
Normal file
2
prisma/migrations/20250425041426_add/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EmployeePassport" ADD COLUMN "otherNationality" TEXT;
|
||||
3
prisma/migrations/20250513084929_add/migration.sql
Normal file
3
prisma/migrations/20250513084929_add/migration.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "QuotationWorker" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- DropForeignKey
|
||||
ALTER TABLE "UserImportNationality" DROP CONSTRAINT "UserImportNationality_userId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Quotation" ADD COLUMN "sellerId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `businessType` on the `CustomerBranch` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `customerName` on the `CustomerBranch` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "CustomerBranch" DROP COLUMN "businessType",
|
||||
DROP COLUMN "customerName",
|
||||
ADD COLUMN "businessTypeId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "EmployeeVisa" ADD COLUMN "reportDate" DATE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BusinessType" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameEN" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdByUserId" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"updatedByUserId" TEXT,
|
||||
|
||||
CONSTRAINT "BusinessType_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_businessTypeId_fkey" FOREIGN KEY ("businessTypeId") REFERENCES "BusinessType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BusinessType" ADD CONSTRAINT "BusinessType_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BusinessType" ADD CONSTRAINT "BusinessType_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "addressForeign" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "districtText" TEXT,
|
||||
ADD COLUMN "provinceText" TEXT,
|
||||
ADD COLUMN "subDistrictText" TEXT,
|
||||
ADD COLUMN "zipCodeText" TEXT;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "districtTextEN" TEXT,
|
||||
ADD COLUMN "provinceTextEN" TEXT,
|
||||
ADD COLUMN "subDistrictTextEN" TEXT;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "RequestWorkStepStatus" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Payment" ADD COLUMN "account" TEXT,
|
||||
ADD COLUMN "channel" TEXT,
|
||||
ADD COLUMN "reference" TEXT;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "public"."Product" ADD COLUMN "flowAccountProductIdAgentPrice" TEXT,
|
||||
ADD COLUMN "flowAccountProductIdSellPrice" TEXT;
|
||||
|
|
@ -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,8 @@ model Branch {
|
|||
quotation Quotation[]
|
||||
workflowTemplate WorkflowTemplate[]
|
||||
taskOrder TaskOrder[]
|
||||
notification Notification[]
|
||||
property Property[]
|
||||
}
|
||||
|
||||
model BranchBank {
|
||||
|
|
@ -360,16 +366,24 @@ enum UserType {
|
|||
AGENCY
|
||||
}
|
||||
|
||||
model UserImportNationality {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
|
||||
code String?
|
||||
namePrefix String?
|
||||
firstName String
|
||||
firstName String?
|
||||
firstNameEN String
|
||||
middleName String?
|
||||
middleNameEN String?
|
||||
lastName String
|
||||
lastName String?
|
||||
lastNameEN String
|
||||
username String
|
||||
gender String
|
||||
|
|
@ -384,14 +398,24 @@ model User {
|
|||
street String?
|
||||
streetEN String?
|
||||
|
||||
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
|
||||
provinceId String?
|
||||
addressForeign Boolean @default(false)
|
||||
|
||||
district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
|
||||
districtId String?
|
||||
provinceText String?
|
||||
provinceTextEN String?
|
||||
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
|
||||
provinceId String?
|
||||
|
||||
subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
|
||||
subDistrictId String?
|
||||
districtText String?
|
||||
districtTextEN String?
|
||||
district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
|
||||
districtId String?
|
||||
|
||||
subDistrictText String?
|
||||
subDistrictTextEN String?
|
||||
subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
|
||||
subDistrictId String?
|
||||
|
||||
zipCodeText String?
|
||||
|
||||
email String
|
||||
telephoneNo String
|
||||
|
|
@ -418,7 +442,7 @@ model User {
|
|||
licenseExpireDate DateTime? @db.Date
|
||||
|
||||
sourceNationality String?
|
||||
importNationality String?
|
||||
importNationality UserImportNationality[]
|
||||
|
||||
trainingPlace String?
|
||||
responsibleArea UserResponsibleArea[]
|
||||
|
|
@ -478,14 +502,28 @@ model User {
|
|||
flowCreated WorkflowTemplate[] @relation("FlowCreatedByUser")
|
||||
flowUpdated WorkflowTemplate[] @relation("FlowUpdatedByUser")
|
||||
invoiceCreated Invoice[]
|
||||
paymentCreated Payment[]
|
||||
paymentCreated Payment[] @relation("PaymentCreatedByUser")
|
||||
paymentUpdated Payment[] @relation("PaymentUpdatedByUser")
|
||||
notificationReceive Notification[] @relation("NotificationReceiver")
|
||||
notificationRead Notification[]
|
||||
notificationRead Notification[] @relation("NotificationRead")
|
||||
notificationDelete Notification[] @relation("NotificationDelete")
|
||||
taskOrderCreated TaskOrder[] @relation("TaskOrderCreatedByUser")
|
||||
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
|
||||
institutionCreated Institution[] @relation("InstitutionCreatedByUser")
|
||||
institutionUpdated Institution[] @relation("InstitutionUpdatedByUser")
|
||||
businessTypeCreated BusinessType[] @relation("BusinessTypeCreatedByUser")
|
||||
businessTypeUpdated BusinessType[] @relation("BusinessTypeUpdatedByUser")
|
||||
|
||||
requestWorkStepStatus RequestWorkStepStatus[]
|
||||
userTask UserTask[]
|
||||
requestData RequestData[]
|
||||
|
||||
remark String?
|
||||
agencyStatus String?
|
||||
|
||||
contactName String?
|
||||
contactTel String?
|
||||
quotation Quotation[]
|
||||
}
|
||||
|
||||
model UserResponsibleArea {
|
||||
|
|
@ -524,10 +562,9 @@ model Customer {
|
|||
}
|
||||
|
||||
model CustomerBranch {
|
||||
id String @id @default(cuid())
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
customerId String
|
||||
customerName String?
|
||||
id String @id @default(cuid())
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
customerId String
|
||||
|
||||
code String
|
||||
codeCustomer String
|
||||
|
|
@ -589,7 +626,8 @@ model CustomerBranch {
|
|||
agentUser User? @relation(fields: [agentUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
// NOTE: Business
|
||||
businessType String
|
||||
businessTypeId String?
|
||||
businessType BusinessType? @relation(fields: [businessTypeId], references: [id], onDelete: SetNull)
|
||||
jobPosition String
|
||||
jobDescription String
|
||||
payDate String
|
||||
|
|
@ -748,6 +786,21 @@ model CustomerBranchVatRegis {
|
|||
customerBranch CustomerBranch @relation(fields: [customerBranchId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model BusinessType {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEN String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "BusinessTypeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation(name: "BusinessTypeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
customerBranch CustomerBranch[]
|
||||
}
|
||||
|
||||
model Employee {
|
||||
id String @id @default(cuid())
|
||||
|
||||
|
|
@ -755,16 +808,17 @@ model Employee {
|
|||
nrcNo String?
|
||||
|
||||
namePrefix String?
|
||||
firstName String
|
||||
firstName String?
|
||||
firstNameEN String
|
||||
middleName String?
|
||||
middleNameEN String?
|
||||
lastName String
|
||||
lastNameEN String
|
||||
lastName String?
|
||||
lastNameEN String?
|
||||
|
||||
dateOfBirth DateTime @db.Date
|
||||
gender String
|
||||
nationality String
|
||||
dateOfBirth DateTime? @db.Date
|
||||
gender String
|
||||
nationality String
|
||||
otherNationality String?
|
||||
|
||||
address String?
|
||||
addressEN String?
|
||||
|
|
@ -839,18 +893,19 @@ model EmployeePassport {
|
|||
issuePlace String
|
||||
previousPassportRef String?
|
||||
|
||||
workerStatus String?
|
||||
nationality String?
|
||||
namePrefix String?
|
||||
firstName String?
|
||||
firstNameEN String?
|
||||
middleName String?
|
||||
middleNameEN String?
|
||||
lastName String?
|
||||
lastNameEN String?
|
||||
gender String?
|
||||
birthDate String?
|
||||
birthCountry String?
|
||||
workerStatus String?
|
||||
nationality String?
|
||||
otherNationality String?
|
||||
namePrefix String?
|
||||
firstName String?
|
||||
firstNameEN String?
|
||||
middleName String?
|
||||
middleNameEN String?
|
||||
lastName String?
|
||||
lastNameEN String?
|
||||
gender String?
|
||||
birthDate String?
|
||||
birthCountry String?
|
||||
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
|
|
@ -867,8 +922,9 @@ model EmployeeVisa {
|
|||
entryCount Int
|
||||
issueCountry String
|
||||
issuePlace String
|
||||
issueDate DateTime @db.Date
|
||||
expireDate DateTime @db.Date
|
||||
issueDate DateTime @db.Date
|
||||
expireDate DateTime @db.Date
|
||||
reportDate DateTime? @db.Date
|
||||
mrz String?
|
||||
remark String?
|
||||
|
||||
|
|
@ -996,6 +1052,49 @@ model Institution {
|
|||
selectedImage String?
|
||||
|
||||
taskOrder TaskOrder[]
|
||||
|
||||
contactName String?
|
||||
contactEmail String?
|
||||
contactTel String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "InstitutionCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
updatedBy User? @relation(name: "InstitutionUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
bank InstitutionBank[]
|
||||
}
|
||||
|
||||
model InstitutionBank {
|
||||
id String @id @default(cuid())
|
||||
bankName String
|
||||
bankBranch String
|
||||
accountName String
|
||||
accountNumber String
|
||||
accountType String
|
||||
currentlyUse Boolean
|
||||
|
||||
institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade)
|
||||
institutionId String
|
||||
}
|
||||
|
||||
model Property {
|
||||
id String @id @default(cuid())
|
||||
|
||||
registeredBranch Branch @relation(fields: [registeredBranchId], references: [id])
|
||||
registeredBranchId String
|
||||
|
||||
name String
|
||||
nameEN String
|
||||
|
||||
type Json
|
||||
|
||||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model WorkflowTemplate {
|
||||
|
|
@ -1029,6 +1128,15 @@ model WorkflowTemplateStepInstitution {
|
|||
workflowTemplateStepId String
|
||||
}
|
||||
|
||||
model WorkflowTemplateStepGroup {
|
||||
id String @id @default(cuid())
|
||||
|
||||
group String
|
||||
|
||||
workflowTemplateStep WorkflowTemplateStep @relation(fields: [workflowTemplateStepId], references: [id], onDelete: Cascade)
|
||||
workflowTemplateStepId String
|
||||
}
|
||||
|
||||
model WorkflowTemplateStep {
|
||||
id String @id @default(cuid())
|
||||
|
||||
|
|
@ -1039,6 +1147,7 @@ model WorkflowTemplateStep {
|
|||
value WorkflowTemplateStepValue[] // NOTE: For enum or options type
|
||||
responsiblePerson WorkflowTemplateStepUser[]
|
||||
responsibleInstitution WorkflowTemplateStepInstitution[]
|
||||
responsibleGroup WorkflowTemplateStepGroup[]
|
||||
messengerByArea Boolean @default(false)
|
||||
|
||||
attributes Json?
|
||||
|
|
@ -1134,6 +1243,9 @@ model Product {
|
|||
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
|
||||
productGroupId String
|
||||
|
||||
flowAccountProductIdSellPrice String?
|
||||
flowAccountProductIdAgentPrice String?
|
||||
|
||||
workProduct WorkProduct[]
|
||||
quotationProductServiceList QuotationProductServiceList[]
|
||||
taskProduct TaskProduct[]
|
||||
|
|
@ -1306,6 +1418,9 @@ model Quotation {
|
|||
|
||||
invoice Invoice[]
|
||||
creditNote CreditNote[]
|
||||
|
||||
seller User? @relation(fields: [sellerId], references: [id], onDelete: Cascade)
|
||||
sellerId String?
|
||||
}
|
||||
|
||||
model QuotationPaySplit {
|
||||
|
|
@ -1330,6 +1445,9 @@ model QuotationWorker {
|
|||
employeeId String
|
||||
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
|
||||
quotationId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
}
|
||||
|
||||
model QuotationProductServiceList {
|
||||
|
|
@ -1409,12 +1527,19 @@ model Payment {
|
|||
|
||||
paymentStatus PaymentStatus
|
||||
|
||||
amount Float
|
||||
date DateTime?
|
||||
amount Float
|
||||
date DateTime?
|
||||
channel String?
|
||||
account String?
|
||||
reference String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdByUserId String?
|
||||
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
updatedBy User? @relation(name: "PaymentUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
}
|
||||
|
||||
enum RequestDataStatus {
|
||||
|
|
@ -1438,8 +1563,16 @@ model RequestData {
|
|||
|
||||
requestDataStatus RequestDataStatus @default(Pending)
|
||||
|
||||
customerRequestCancel Boolean?
|
||||
customerRequestCancelReason String?
|
||||
rejectRequestCancel Boolean?
|
||||
rejectRequestCancelReason String?
|
||||
|
||||
flow Json?
|
||||
|
||||
defaultMessenger User? @relation(fields: [defaultMessengerId], references: [id])
|
||||
defaultMessengerId String?
|
||||
|
||||
requestWork RequestWork[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
|
@ -1473,6 +1606,11 @@ model RequestWork {
|
|||
|
||||
stepStatus RequestWorkStepStatus[]
|
||||
|
||||
customerRequestCancel Boolean?
|
||||
customerRequestCancelReason String?
|
||||
rejectRequestCancel Boolean?
|
||||
rejectRequestCancelReason String?
|
||||
|
||||
creditNote CreditNote? @relation(fields: [creditNoteId], references: [id], onDelete: SetNull)
|
||||
creditNoteId String?
|
||||
}
|
||||
|
|
@ -1480,6 +1618,7 @@ model RequestWork {
|
|||
model RequestWorkStepStatus {
|
||||
step Int
|
||||
workStatus RequestWorkStatus @default(Pending)
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
requestWork RequestWork @relation(fields: [requestWorkId], references: [id], onDelete: Cascade)
|
||||
requestWorkId String
|
||||
|
|
@ -1554,7 +1693,8 @@ model TaskProduct {
|
|||
model TaskOrder {
|
||||
id String @id @default(cuid())
|
||||
|
||||
code String
|
||||
code String
|
||||
codeProductReceived String?
|
||||
|
||||
taskName String
|
||||
taskOrderStatus TaskOrderStatus @default(Pending)
|
||||
|
|
@ -1603,6 +1743,7 @@ model UserTask {
|
|||
}
|
||||
|
||||
enum CreditNoteStatus {
|
||||
Waiting
|
||||
Pending
|
||||
Success
|
||||
}
|
||||
|
|
@ -1623,7 +1764,7 @@ model CreditNote {
|
|||
|
||||
code String
|
||||
|
||||
creditNoteStatus CreditNoteStatus @default(Pending)
|
||||
creditNoteStatus CreditNoteStatus @default(Waiting)
|
||||
|
||||
value Float @default(0)
|
||||
reason String?
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { createCanvas } from "canvas";
|
||||
import JsBarcode from "jsbarcode";
|
||||
import createReport from "docx-templates";
|
||||
import ThaiBahtText from "thai-baht-text";
|
||||
import { District, Province, SubDistrict } from "@prisma/client";
|
||||
import { Readable } from "node:stream";
|
||||
import { Controller, Get, Path, Query, Route } 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";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { getFileBuffer, listFile } from "../utils/minio";
|
||||
import { dateFormat } from "../utils/datetime";
|
||||
import { downloadFile as edmDownloadFile, list as edmList } from "../services/edm/edm-api";
|
||||
|
||||
const DOCUMENT_PATH = process.env.DOCUMENT_TEMPLATE_LOCATION?.split("/").filter(Boolean) || [];
|
||||
|
||||
const quotationData = (id: string) =>
|
||||
prisma.quotation.findFirst({
|
||||
|
|
@ -29,8 +34,14 @@ const quotationData = (id: string) =>
|
|||
},
|
||||
},
|
||||
customerBranch: {
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
include: {
|
||||
customer: true,
|
||||
businessType: true,
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
|
|
@ -58,14 +69,62 @@ const quotationData = (id: string) =>
|
|||
service: true,
|
||||
},
|
||||
},
|
||||
createdBy: {
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const requestWorkData = (id: string, step?: number) =>
|
||||
prisma.requestWork.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
processByUser: true,
|
||||
productService: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
request: {
|
||||
include: {
|
||||
employee: {
|
||||
include: {
|
||||
subDistrict: true,
|
||||
district: true,
|
||||
province: true,
|
||||
},
|
||||
},
|
||||
quotation: true,
|
||||
},
|
||||
},
|
||||
stepStatus: {
|
||||
where: { step },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@Route("api/v1/doc-template")
|
||||
@Tags("Document Template")
|
||||
export class DocTemplateController extends Controller {
|
||||
@Get()
|
||||
async getTemplate() {
|
||||
return await listFile(`doc-template/`);
|
||||
async getTemplate(@Query() templateGroup?: string) {
|
||||
if (
|
||||
process.env.DOCUMENT_TEMPLATE_PROVIDER &&
|
||||
process.env.DOCUMENT_TEMPLATE_PROVIDER === "edm-api"
|
||||
) {
|
||||
const ret = await edmList(
|
||||
"file",
|
||||
templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH,
|
||||
);
|
||||
if (ret) return ret.map((v) => v.fileName);
|
||||
}
|
||||
return await listFile(
|
||||
(templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH).join("/") + "/",
|
||||
);
|
||||
}
|
||||
|
||||
@Get("{documentTemplate}")
|
||||
|
|
@ -74,6 +133,7 @@ export class DocTemplateController extends Controller {
|
|||
@Query() data: string,
|
||||
@Query() dataId: string,
|
||||
@Query() dataOnly?: boolean,
|
||||
@Query() templateGroup?: string,
|
||||
): Promise<Readable | Record<string, any>> {
|
||||
let record: Record<string, any>;
|
||||
|
||||
|
|
@ -114,14 +174,44 @@ export class DocTemplateController extends Controller {
|
|||
}),
|
||||
);
|
||||
break;
|
||||
case "request-work":
|
||||
record = await requestWorkData(dataId).then((requestWork) => ({
|
||||
request: replaceEmptyField(requestWork?.request),
|
||||
requestWork: replaceEmptyField(requestWork),
|
||||
employee: requestWork?.request.employee,
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "No data for template", "noDataTemplate");
|
||||
}
|
||||
|
||||
if (!data) throw notFoundError("Data");
|
||||
if (dataOnly) return record;
|
||||
if (templateGroup) documentTemplate = templateGroup + "/" + documentTemplate;
|
||||
|
||||
const template = await getFileBuffer(`doc-template/${documentTemplate}`);
|
||||
let template: Buffer<ArrayBufferLike> | null = null;
|
||||
|
||||
switch (process.env.DOCUMENT_TEMPLATE_PROVIDER) {
|
||||
case "edm-api":
|
||||
await edmDownloadFile(DOCUMENT_PATH, documentTemplate).then(async (payload) => {
|
||||
if (!payload) return;
|
||||
const res = await fetch(payload.downloadUrl);
|
||||
if (!res.ok) return;
|
||||
template = Buffer.from(await res.arrayBuffer());
|
||||
});
|
||||
break;
|
||||
case "local":
|
||||
default:
|
||||
template = await getFileBuffer(`${DOCUMENT_PATH.join("/")}/${documentTemplate}`);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
throw new HttpError(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Failed to get template file",
|
||||
"templateGetFailed",
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) Readable.from(template);
|
||||
|
||||
|
|
@ -170,6 +260,23 @@ export class DocTemplateController extends Controller {
|
|||
thaiBahtText: (input: string | number) => {
|
||||
ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input);
|
||||
},
|
||||
barcode: async (data: string, width?: number, height?: number) =>
|
||||
new Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
data: string;
|
||||
extension: string;
|
||||
}>((resolve) => {
|
||||
const canvas = createCanvas(400, 100);
|
||||
JsBarcode(canvas, data);
|
||||
|
||||
resolve({
|
||||
width: width ?? 8,
|
||||
height: height ?? 3,
|
||||
data: canvas.toDataURL("image/jpeg").slice("data:image/jpeg;base64".length),
|
||||
extension: ".jpeg",
|
||||
});
|
||||
}),
|
||||
},
|
||||
}).then(Buffer.from);
|
||||
|
||||
|
|
@ -178,10 +285,15 @@ export class DocTemplateController extends Controller {
|
|||
}
|
||||
|
||||
function replaceEmptyField<T>(data: T): T {
|
||||
return JSON.parse(JSON.stringify(data).replace(/null|\"\"/g, '"\-"'));
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(data).replace(/null|\"\"/g, '"\-"'));
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
type FullAddress = {
|
||||
addressForeign?: boolean;
|
||||
address: string;
|
||||
addressEN: string;
|
||||
moo?: string;
|
||||
|
|
@ -190,8 +302,14 @@ type FullAddress = {
|
|||
soiEN?: string;
|
||||
street?: string;
|
||||
streetEN?: string;
|
||||
provinceText?: string | null;
|
||||
provinceTextEN?: string | null;
|
||||
province?: Province | null;
|
||||
districtText?: string | null;
|
||||
districtTextEN?: string | null;
|
||||
district?: District | null;
|
||||
subDistrictText?: string | null;
|
||||
subDistrictTextEN?: string | null;
|
||||
subDistrict?: SubDistrict | null;
|
||||
en?: boolean;
|
||||
};
|
||||
|
|
@ -225,13 +343,22 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
|
|||
if (addr.soi) fragments.push(`ซอย ${addr.soi},`);
|
||||
if (addr.street) fragments.push(`ถนน${addr.street},`);
|
||||
|
||||
if (addr.subDistrict) {
|
||||
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name},`);
|
||||
if (!addr.addressForeign && addr.subDistrict) {
|
||||
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name}`);
|
||||
}
|
||||
if (addr.district) {
|
||||
fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name},`);
|
||||
if (addr.addressForeign && addr.subDistrictText) {
|
||||
fragments.push(`ตำบล${addr.subDistrictText}`);
|
||||
}
|
||||
if (addr.province) fragments.push(`จังหวัด${addr.province.name},`);
|
||||
|
||||
if (!addr.addressForeign && addr.district) {
|
||||
fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name}`);
|
||||
}
|
||||
if (addr.addressForeign && addr.districtText) {
|
||||
fragments.push(`อำเภอ${addr.districtText}`);
|
||||
}
|
||||
|
||||
if (!addr.addressForeign && addr.province) fragments.push(`จังหวัด${addr.province.name}`);
|
||||
if (addr.addressForeign && addr.provinceText) fragments.push(`จังหวัด${addr.provinceText}`);
|
||||
|
||||
break;
|
||||
default:
|
||||
|
|
@ -240,14 +367,31 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
|
|||
if (addr.soiEN) fragments.push(`Soi ${addr.soiEN},`);
|
||||
if (addr.streetEN) fragments.push(`${addr.streetEN} Rd.`);
|
||||
|
||||
if (addr.subDistrict) {
|
||||
if (!addr.addressForeign && addr.subDistrict) {
|
||||
fragments.push(`${addr.subDistrict.nameEN} sub-district,`);
|
||||
}
|
||||
if (addr.district) fragments.push(`${addr.district.nameEN} district,`);
|
||||
if (addr.province) fragments.push(`${addr.province.nameEN},`);
|
||||
if (addr.addressForeign && addr.subDistrictTextEN) {
|
||||
fragments.push(`${addr.subDistrictTextEN} sub-district,`);
|
||||
}
|
||||
|
||||
if (!addr.addressForeign && addr.district) {
|
||||
fragments.push(`${addr.district.nameEN} district,`);
|
||||
}
|
||||
if (addr.addressForeign && addr.districtTextEN) {
|
||||
fragments.push(`${addr.districtTextEN} district,`);
|
||||
}
|
||||
|
||||
if (!addr.addressForeign && addr.province) {
|
||||
fragments.push(`${addr.province.nameEN},`);
|
||||
}
|
||||
if (addr.addressForeign && addr.provinceTextEN) {
|
||||
fragments.push(`${addr.provinceTextEN} district,`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (addr.subDistrict) fragments.push(addr.subDistrict.zipCode);
|
||||
|
||||
return fragments.join(" ");
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +404,9 @@ function gender(text: string, lang: "th" | "en" = "en") {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
function businessType(text: string, lang: "th" | "en" = "en") {
|
||||
switch (lang) {
|
||||
case "th":
|
||||
|
|
@ -341,7 +488,7 @@ function nationality(text: string, lang: "th" | "en" = "en") {
|
|||
case "th":
|
||||
return (
|
||||
{
|
||||
["THA"]: "ไทย",
|
||||
["THA"]: "ไทย", // spellchecker:disable-line
|
||||
["MMR"]: "เมียนมา",
|
||||
["LAO"]: "ลาว",
|
||||
["KHM"]: "กัมพูชา",
|
||||
|
|
@ -353,7 +500,7 @@ function nationality(text: string, lang: "th" | "en" = "en") {
|
|||
default:
|
||||
return (
|
||||
{
|
||||
["THA"]: "Thai",
|
||||
["THA"]: "Thai", // spellchecker:disable-line
|
||||
["MMR"]: "Myanmar",
|
||||
["LAO"]: "Laos",
|
||||
["KHM"]: "Khmer",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Body, Controller, Get, Path, Post, Query, Route, Tags } from "tsoa";
|
|||
import prisma from "../db";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
@Route("/api/v1/employment-office")
|
||||
@Tags("Employment Office")
|
||||
|
|
@ -11,6 +12,39 @@ export class EmploymentOfficeController extends Controller {
|
|||
return this.getEmploymentOfficeListByCriteria(districtId, query);
|
||||
}
|
||||
|
||||
@Post("list-same-office-area")
|
||||
async getSameOfficeArea(@Body() body: { districtId: string }) {
|
||||
const office = await prisma.employmentOffice.findFirst({
|
||||
include: {
|
||||
province: {
|
||||
include: {
|
||||
district: true,
|
||||
},
|
||||
},
|
||||
district: true,
|
||||
},
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
province: { district: { some: { id: body.districtId } } },
|
||||
district: { none: {} },
|
||||
},
|
||||
{
|
||||
district: {
|
||||
some: { districtId: body.districtId },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!office) return [];
|
||||
|
||||
return [
|
||||
...office.district.map((v) => v.districtId),
|
||||
...office.province.district.map((v) => v.id),
|
||||
];
|
||||
}
|
||||
|
||||
@Post("list")
|
||||
async getEmploymentOfficeListByCriteria(
|
||||
@Query() districtId?: string,
|
||||
|
|
@ -40,11 +74,14 @@ export class EmploymentOfficeController extends Controller {
|
|||
],
|
||||
[],
|
||||
),
|
||||
...queryOrNot(
|
||||
...(queryOrNot(
|
||||
query,
|
||||
[{ name: { contains: query } }, { nameEN: { contains: query } }],
|
||||
[
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ nameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
[],
|
||||
),
|
||||
) satisfies Prisma.EmploymentOfficeWhereInput["OR"]),
|
||||
...queryOrNot(!!body?.id, [{ id: { in: body?.id } }], []),
|
||||
]
|
||||
: undefined,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Body, Controller, Delete, Get, Path, Post, Route, Security, Tags } from "tsoa";
|
||||
import { addUserRoles, listRole, removeUserRoles } from "../services/keycloak";
|
||||
import { Body, Controller, Delete, Get, Path, Post, Query, Route, Security, Tags } from "tsoa";
|
||||
import { addUserRoles, getGroup, listRole, removeUserRoles } from "../services/keycloak";
|
||||
|
||||
@Route("api/v1/keycloak")
|
||||
@Tags("Single-Sign On")
|
||||
|
|
@ -44,4 +44,13 @@ export class KeycloakController extends Controller {
|
|||
);
|
||||
if (!result) throw new Error("Failed. Cannot remove user's role.");
|
||||
}
|
||||
|
||||
@Get("group")
|
||||
async getGroup(@Query() query: string = "") {
|
||||
const querySearch = query === "" ? "q" : `search=${query}`;
|
||||
const group = await getGroup(querySearch);
|
||||
if (!Array.isArray(group)) throw new Error("Failed. Cannot get group(s) data from the server.");
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, mode: "insensitive" } },
|
||||
{ detail: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{ receiverId: req.user.sub },
|
||||
req.user.roles.length > 0
|
||||
? {
|
||||
groupReceiver: { some: { name: { in: req.user.roles } } },
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
}
|
||||
: {},
|
||||
],
|
||||
},
|
||||
],
|
||||
NOT: {
|
||||
OR: [
|
||||
{
|
||||
readByUser: { some: { id: req.user.sub } },
|
||||
createdAt: { lte: dayjs().subtract(7, "days").toDate() },
|
||||
},
|
||||
{ deleteByUser: { some: { id: req.user.sub } } },
|
||||
],
|
||||
},
|
||||
};
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.notification.findMany({
|
||||
where,
|
||||
include: { readByUser: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.notification.count({ where }),
|
||||
]);
|
||||
|
||||
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 },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
733
src/controllers/00-stats-controller.ts
Normal file
733
src/controllers/00-stats-controller.ts
Normal file
|
|
@ -0,0 +1,733 @@
|
|||
import config from "../config.json";
|
||||
import {
|
||||
Customer,
|
||||
CustomerBranch,
|
||||
QuotationStatus,
|
||||
RequestWorkStatus,
|
||||
PaymentStatus,
|
||||
} from "@prisma/client";
|
||||
import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa";
|
||||
import prisma from "../db";
|
||||
import { createPermCondition, createQueryPermissionCondition } from "../services/permission";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { precisionRound } from "../utils/arithmetic";
|
||||
import dayjs from "dayjs";
|
||||
import { json2csv } from "json-2-csv";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import { jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionQueryCondCompany = createQueryPermissionCondition((_) => true);
|
||||
|
||||
const VAT_DEFAULT = config.vat;
|
||||
|
||||
@Route("/api/v1/report")
|
||||
@Security("keycloak")
|
||||
@Tags("Report")
|
||||
export class StatsController extends Controller {
|
||||
@Get("quotation/download")
|
||||
async downloadQuotationReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(
|
||||
await this.quotationReport(req, limit, startDate, endDate).then((v) =>
|
||||
v.map((v) => ({
|
||||
...v,
|
||||
customerBranch: {
|
||||
...v.customerBranch,
|
||||
customerType: v.customerBranch.customer.customerType,
|
||||
customer: undefined,
|
||||
},
|
||||
})),
|
||||
),
|
||||
{ useDateIso8601Format: true },
|
||||
);
|
||||
}
|
||||
|
||||
@Get("quotation")
|
||||
async quotationReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const record = await prisma.quotation.findMany({
|
||||
select: {
|
||||
code: true,
|
||||
quotationStatus: true,
|
||||
customerBranch: {
|
||||
omit: { otpCode: true, otpExpires: true, userId: true },
|
||||
include: { customer: true },
|
||||
},
|
||||
finalPrice: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
where: {
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return record.map((v) => ({
|
||||
document: "quotation",
|
||||
code: v.code,
|
||||
status: v.quotationStatus,
|
||||
amount: v.finalPrice,
|
||||
createdAt: v.createdAt,
|
||||
updatedAt: v.updatedAt,
|
||||
|
||||
customerBranch: v.customerBranch,
|
||||
}));
|
||||
}
|
||||
|
||||
@Get("invoice/download")
|
||||
async downloadInvoiceReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(
|
||||
await this.invoiceReport(req, limit, startDate, endDate).then((v) =>
|
||||
v.map((v) => ({
|
||||
...v,
|
||||
customerBranch: {
|
||||
...v.customerBranch,
|
||||
customerType: v.customerBranch.customer.customerType,
|
||||
customer: undefined,
|
||||
},
|
||||
})),
|
||||
),
|
||||
{
|
||||
useDateIso8601Format: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get("invoice")
|
||||
async invoiceReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const record = await prisma.invoice.findMany({
|
||||
select: {
|
||||
code: true,
|
||||
quotation: {
|
||||
select: {
|
||||
customerBranch: {
|
||||
omit: { otpCode: true, otpExpires: true, userId: true },
|
||||
include: { customer: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
select: {
|
||||
paymentStatus: true,
|
||||
},
|
||||
},
|
||||
amount: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: {
|
||||
quotation: {
|
||||
isDebitNote: false,
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
},
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return record.map((v) => ({
|
||||
document: "invoice",
|
||||
code: v.code,
|
||||
status: v.payment?.paymentStatus,
|
||||
amount: v.amount,
|
||||
createdAt: v.createdAt,
|
||||
|
||||
customerBranch: v.quotation.customerBranch,
|
||||
}));
|
||||
}
|
||||
|
||||
@Get("receipt/download")
|
||||
async downloadReceiptReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(
|
||||
await this.receiptReport(req, limit, startDate, endDate).then((v) =>
|
||||
v.map((v) => ({
|
||||
...v,
|
||||
customerBranch: {
|
||||
...v.customerBranch,
|
||||
customerType: v.customerBranch.customer.customerType,
|
||||
customer: undefined,
|
||||
},
|
||||
})),
|
||||
),
|
||||
{
|
||||
useDateIso8601Format: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get("receipt")
|
||||
async receiptReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const record = await prisma.payment.findMany({
|
||||
select: {
|
||||
code: true,
|
||||
invoice: {
|
||||
select: {
|
||||
quotation: {
|
||||
select: {
|
||||
customerBranch: {
|
||||
omit: { otpCode: true, otpExpires: true, userId: true },
|
||||
include: { customer: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
amount: true,
|
||||
paymentStatus: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: {
|
||||
paymentStatus: PaymentStatus.PaymentSuccess,
|
||||
invoice: {
|
||||
quotation: {
|
||||
isDebitNote: false,
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
},
|
||||
},
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return record.map((v) => ({
|
||||
document: "receipt",
|
||||
code: v.code,
|
||||
amount: v.amount,
|
||||
status: v.paymentStatus,
|
||||
createdAt: v.createdAt,
|
||||
|
||||
customerBranch: v.invoice.quotation.customerBranch,
|
||||
}));
|
||||
}
|
||||
|
||||
@Get("product/download")
|
||||
async downloadProductReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(await this.productReport(req, limit, startDate, endDate), {
|
||||
useDateIso8601Format: true,
|
||||
});
|
||||
}
|
||||
|
||||
@Get("product")
|
||||
async productReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const record = await tx.product.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
quotationProductServiceList: {
|
||||
include: { quotation: true },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
quotationProductServiceList: {
|
||||
where: {
|
||||
quotation: {
|
||||
quotationStatus: {
|
||||
in: [
|
||||
QuotationStatus.PaymentInProcess,
|
||||
QuotationStatus.PaymentSuccess,
|
||||
QuotationStatus.ProcessComplete,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
quotationProductServiceList: {
|
||||
some: {
|
||||
quotation: { createdAt: { gte: startDate, lte: endDate } },
|
||||
},
|
||||
},
|
||||
productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } },
|
||||
},
|
||||
orderBy: {
|
||||
quotationProductServiceList: { _count: "desc" },
|
||||
},
|
||||
take: limit,
|
||||
});
|
||||
|
||||
const doing = await tx.quotationProductServiceList.groupBy({
|
||||
_count: true,
|
||||
by: "productId",
|
||||
where: {
|
||||
quotation: {
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
},
|
||||
productId: { in: record.map((v) => v.id) },
|
||||
requestWork: {
|
||||
some: {
|
||||
stepStatus: {
|
||||
some: {
|
||||
workStatus: {
|
||||
in: [
|
||||
RequestWorkStatus.Pending,
|
||||
RequestWorkStatus.InProgress,
|
||||
RequestWorkStatus.Validate,
|
||||
RequestWorkStatus.Completed,
|
||||
RequestWorkStatus.Ended,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const order = await tx.quotationProductServiceList.groupBy({
|
||||
_count: true,
|
||||
by: "productId",
|
||||
where: {
|
||||
quotation: {
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
},
|
||||
productId: { in: record.map((v) => v.id) },
|
||||
},
|
||||
});
|
||||
|
||||
return record.map((v) => ({
|
||||
document: "product",
|
||||
code: v.code,
|
||||
name: v.name,
|
||||
sale: v._count.quotationProductServiceList,
|
||||
did: doing.find((item) => item.productId === v.id)?._count || 0,
|
||||
order: order.find((item) => item.productId === v.id)?._count || 0,
|
||||
createdAt: v.createdAt,
|
||||
updatedAt: v.updatedAt,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Get("sale/by-product-group/download")
|
||||
async downloadSaleByProductGroupReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(
|
||||
await this.saleReport(req, limit, startDate, endDate).then((v) => v.byProductGroup),
|
||||
{ useDateIso8601Format: true },
|
||||
);
|
||||
}
|
||||
|
||||
@Get("sale/by-sale/download")
|
||||
async downloadSaleBySaleReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(await this.saleReport(req, limit, startDate, endDate).then((v) => v.bySale), {
|
||||
useDateIso8601Format: true,
|
||||
});
|
||||
}
|
||||
|
||||
@Get("sale/by-customer/download")
|
||||
async downloadSaleByCustomerReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
return json2csv(
|
||||
await this.saleReport(req, limit, startDate, endDate).then((v) => v.byCustomer),
|
||||
{ useDateIso8601Format: true },
|
||||
);
|
||||
}
|
||||
|
||||
@Get("sale")
|
||||
async saleReport(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() limit?: number,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const list = await prisma.quotationProductServiceList.findMany({
|
||||
include: {
|
||||
quotation: {
|
||||
include: {
|
||||
createdBy: true,
|
||||
customerBranch: {
|
||||
omit: { otpCode: true, otpExpires: true, userId: true },
|
||||
include: { customer: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
product: {
|
||||
include: {
|
||||
productGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
quotation: {
|
||||
isDebitNote: false,
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
quotationStatus: {
|
||||
in: [
|
||||
QuotationStatus.PaymentInProcess,
|
||||
QuotationStatus.PaymentSuccess,
|
||||
QuotationStatus.ProcessComplete,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return list.reduce<{
|
||||
byProductGroup: ((typeof list)[number]["product"]["productGroup"] & { _count: number })[];
|
||||
bySale: ((typeof list)[number]["quotation"]["createdBy"] & { _count: number })[];
|
||||
byCustomer: ((typeof list)[number]["quotation"]["customerBranch"] & { _count: number })[];
|
||||
}>(
|
||||
(a, c) => {
|
||||
{
|
||||
const found = a.byProductGroup.find((v) => v.id === c.product.productGroupId);
|
||||
if (found) {
|
||||
found._count++;
|
||||
} else {
|
||||
a.byProductGroup.push({ ...c.product.productGroup, _count: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const found = a.bySale.find((v) => v.id === c.quotation.createdByUserId);
|
||||
if (found) {
|
||||
found._count++;
|
||||
} else {
|
||||
if (c.quotation.createdBy) {
|
||||
a.bySale.push({ ...c.quotation.createdBy, _count: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const found = a.byCustomer.find((v) => v.id === c.quotation.customerBranchId);
|
||||
if (found) {
|
||||
found._count++;
|
||||
} else {
|
||||
a.byCustomer.push({ ...c.quotation.customerBranch, _count: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
},
|
||||
{ byProductGroup: [], bySale: [], byCustomer: [] },
|
||||
);
|
||||
}
|
||||
|
||||
@Get("profit")
|
||||
async profit(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const record = await prisma.quotationProductServiceList.findMany({
|
||||
include: {
|
||||
work: {
|
||||
include: {
|
||||
productOnWork: {
|
||||
select: { stepCount: true, productId: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
product: {
|
||||
select: {
|
||||
agentPrice: true,
|
||||
agentPriceCalcVat: true,
|
||||
agentPriceVatIncluded: true,
|
||||
serviceCharge: true,
|
||||
serviceChargeCalcVat: true,
|
||||
serviceChargeVatIncluded: true,
|
||||
price: true,
|
||||
calcVat: true,
|
||||
vatIncluded: true,
|
||||
},
|
||||
},
|
||||
requestWork: {
|
||||
include: {
|
||||
stepStatus: true,
|
||||
creditNote: true,
|
||||
},
|
||||
},
|
||||
quotation: {
|
||||
select: {
|
||||
agentPrice: true,
|
||||
creditNote: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
quotation: {
|
||||
quotationStatus: {
|
||||
in: [
|
||||
QuotationStatus.PaymentInProcess,
|
||||
QuotationStatus.PaymentSuccess,
|
||||
QuotationStatus.ProcessComplete,
|
||||
],
|
||||
},
|
||||
registeredBranch: {
|
||||
OR: permissionCondCompany(req.user),
|
||||
},
|
||||
createdAt: { gte: startDate, lte: endDate },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const data = record.map((v) => {
|
||||
const originalPrice = v.product.serviceCharge;
|
||||
const productExpenses = precisionRound(
|
||||
originalPrice + (v.product.serviceChargeVatIncluded ? 0 : originalPrice * VAT_DEFAULT),
|
||||
);
|
||||
const finalPrice = v.pricePerUnit * v.amount * (1 + config.vat);
|
||||
|
||||
return v.requestWork.map((w) => {
|
||||
const creditNote = w.creditNote;
|
||||
const roundCount = v.work?.productOnWork.find((p) => p.productId)?.stepCount || 1;
|
||||
const successCount = w.stepStatus.filter(
|
||||
(s) => s.workStatus !== RequestWorkStatus.Canceled,
|
||||
).length;
|
||||
|
||||
const income = creditNote
|
||||
? precisionRound(productExpenses * successCount)
|
||||
: precisionRound(finalPrice);
|
||||
const expenses = creditNote
|
||||
? precisionRound(productExpenses * successCount)
|
||||
: precisionRound(productExpenses * roundCount);
|
||||
const netProfit = creditNote ? 0 : precisionRound(finalPrice - expenses);
|
||||
|
||||
return {
|
||||
month: v.quotation.createdAt.getMonth() + 1,
|
||||
year: v.quotation.createdAt.getFullYear(),
|
||||
income,
|
||||
expenses,
|
||||
netProfit,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return data
|
||||
.flat()
|
||||
.reduce<{ income: number; expenses: 0; netProfit: 0; dataset: (typeof data)[number] }>(
|
||||
(a, c) => {
|
||||
const current = a.dataset.find((v) => v.month === c.month && v.year === c.year);
|
||||
|
||||
if (current) {
|
||||
current.income += c.income;
|
||||
current.expenses += c.expenses;
|
||||
current.netProfit += c.netProfit;
|
||||
} else {
|
||||
a.dataset.push(c);
|
||||
}
|
||||
|
||||
a.income += c.income;
|
||||
a.expenses += c.expenses;
|
||||
a.netProfit += c.netProfit;
|
||||
|
||||
return a;
|
||||
},
|
||||
{ income: 0, expenses: 0, netProfit: 0, dataset: [] },
|
||||
);
|
||||
}
|
||||
|
||||
@Get("payment")
|
||||
async invoice(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
if (!startDate && !endDate) {
|
||||
startDate = dayjs(new Date()).subtract(12, "months").startOf("month").toDate();
|
||||
endDate = dayjs(new Date()).endOf("months").toDate();
|
||||
}
|
||||
|
||||
if (!startDate && endDate) {
|
||||
startDate = dayjs(endDate).subtract(12, "months").startOf("month").toDate();
|
||||
}
|
||||
|
||||
if (startDate && !endDate) {
|
||||
endDate = dayjs(new Date()).endOf("month").toDate();
|
||||
}
|
||||
|
||||
const data = await prisma.$transaction(async (tx) => {
|
||||
const months: Date[] = [];
|
||||
|
||||
while (startDate! < endDate!) {
|
||||
months.push(startDate!);
|
||||
startDate = dayjs(startDate).startOf("month").add(1, "month").toDate();
|
||||
}
|
||||
|
||||
const invoices = await tx.invoice.findMany({
|
||||
select: { id: true },
|
||||
where: {
|
||||
quotation: {
|
||||
quotationStatus: { notIn: [QuotationStatus.Canceled] },
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (invoices.length === 0) return [];
|
||||
|
||||
return await Promise.all(
|
||||
months.map(async (v) => {
|
||||
const date = dayjs(v);
|
||||
|
||||
return {
|
||||
month: date.format("MM"),
|
||||
year: date.format("YYYY"),
|
||||
data: await tx.payment
|
||||
.groupBy({
|
||||
_sum: { amount: true },
|
||||
where: {
|
||||
createdAt: { gte: v, lte: date.endOf("month").toDate() },
|
||||
invoiceId: { in: invoices.map((v) => v.id) },
|
||||
},
|
||||
by: "paymentStatus",
|
||||
})
|
||||
.then((v) =>
|
||||
v.reduce<Partial<Record<(typeof v)[number]["paymentStatus"], number>>>((a, c) => {
|
||||
a[c.paymentStatus] = c._sum.amount || 0;
|
||||
return a;
|
||||
}, {}),
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@Get("customer-dept")
|
||||
async reportCustomerDept(@Request() req: RequestWithUser) {
|
||||
let query = prisma.$kysely
|
||||
.selectFrom("Invoice")
|
||||
.leftJoin("Quotation", "Quotation.id", "Invoice.quotationId")
|
||||
.leftJoin("Payment", "Invoice.id", "Payment.invoiceId")
|
||||
.leftJoin("CustomerBranch", "CustomerBranch.id", "Quotation.customerBranchId")
|
||||
.leftJoin("Customer", "Customer.id", "CustomerBranch.customerId")
|
||||
.select((eb) => [
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("CustomerBranch")
|
||||
.select((eb) => [
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("Customer")
|
||||
.selectAll("Customer")
|
||||
.whereRef("Customer.id", "=", "CustomerBranch.customerId"),
|
||||
).as("customer"),
|
||||
])
|
||||
.selectAll("CustomerBranch")
|
||||
.whereRef("CustomerBranch.id", "=", "Quotation.customerBranchId"),
|
||||
).as("customerBranch"),
|
||||
])
|
||||
.select(["Payment.paymentStatus"])
|
||||
.selectAll(["Invoice"])
|
||||
.distinctOn("Invoice.id");
|
||||
|
||||
if (!isSystem(req.user)) {
|
||||
query = query.where(permissionQueryCondCompany(req.user));
|
||||
}
|
||||
|
||||
const ret = await query.execute();
|
||||
|
||||
return ret
|
||||
.reduce<
|
||||
{
|
||||
paid: number;
|
||||
unpaid: number;
|
||||
customerBranch: CustomerBranch & {
|
||||
customer: Customer;
|
||||
};
|
||||
}[]
|
||||
>((acc, item) => {
|
||||
const exists = acc.find((v) => v.customerBranch.id === item.customerBranch!.id);
|
||||
|
||||
if (!item.amount) return acc;
|
||||
|
||||
if (!exists) {
|
||||
return acc.concat({
|
||||
customerBranch: item.customerBranch as CustomerBranch & { customer: Customer },
|
||||
paid: item.paymentStatus === "PaymentSuccess" ? item.amount : 0,
|
||||
unpaid: item.paymentStatus !== "PaymentSuccess" ? item.amount : 0,
|
||||
});
|
||||
} else {
|
||||
exists[item.paymentStatus === "PaymentSuccess" ? "paid" : "unpaid"] += item.amount;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.map((v) => ({
|
||||
...v,
|
||||
customerBranch: {
|
||||
...v.customerBranch,
|
||||
userId: undefined,
|
||||
otpCode: undefined,
|
||||
otpExpires: undefined,
|
||||
},
|
||||
_quotation: undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ import {
|
|||
connectOrNot,
|
||||
queryOrNot,
|
||||
whereAddressQuery,
|
||||
whereDateQuery,
|
||||
} from "../utils/relation";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
|
||||
|
|
@ -46,16 +47,20 @@ if (!process.env.MINIO_BUCKET) {
|
|||
throw Error("Require MinIO bucket.");
|
||||
}
|
||||
|
||||
const MANAGE_ROLES = ["system", "head_of_admin"];
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
return MANAGE_ROLES.some((v) => user.roles?.includes(v));
|
||||
}
|
||||
|
||||
function globalAllowView(user: RequestWithUser["user"]) {
|
||||
return MANAGE_ROLES.concat("head_of_accountant", "head_of_sale").some((v) =>
|
||||
user.roles?.includes(v),
|
||||
);
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type BranchCreate = {
|
||||
|
|
@ -146,7 +151,7 @@ type BranchUpdate = {
|
|||
}[];
|
||||
};
|
||||
|
||||
const permissionCond = createPermCondition(globalAllowView);
|
||||
const permissionCond = createPermCondition(globalAllow);
|
||||
const permissionCheck = createPermCheck(globalAllow);
|
||||
|
||||
@Route("api/v1/branch")
|
||||
|
|
@ -250,6 +255,8 @@ export class BranchController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
AND: {
|
||||
|
|
@ -265,26 +272,27 @@ export class BranchController extends Controller {
|
|||
},
|
||||
OR: queryOrNot<Prisma.BranchWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ nameEN: { contains: query } },
|
||||
{ name: { contains: query } },
|
||||
{ email: { contains: query } },
|
||||
{ telephoneNo: { contains: query } },
|
||||
{ nameEN: { contains: query, mode: "insensitive" } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ email: { contains: query, mode: "insensitive" } },
|
||||
{ telephoneNo: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
{
|
||||
branch: {
|
||||
some: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ nameEN: { contains: query } },
|
||||
{ name: { contains: query } },
|
||||
{ email: { contains: query } },
|
||||
{ telephoneNo: { contains: query } },
|
||||
{ nameEN: { contains: query, mode: "insensitive" } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ email: { contains: query, mode: "insensitive" } },
|
||||
{ telephoneNo: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.BranchWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -309,19 +317,20 @@ export class BranchController extends Controller {
|
|||
where: {
|
||||
AND: { OR: permissionCond(req.user) },
|
||||
OR: [
|
||||
{ nameEN: { contains: query } },
|
||||
{ name: { contains: query } },
|
||||
{ email: { contains: query } },
|
||||
{ telephoneNo: { contains: query } },
|
||||
{ nameEN: { contains: query, mode: "insensitive" } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ email: { contains: query, mode: "insensitive" } },
|
||||
{ telephoneNo: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
],
|
||||
...whereDateQuery(startDate, endDate),
|
||||
},
|
||||
include: {
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
orderBy: { code: "asc" },
|
||||
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
|
||||
}
|
||||
: false,
|
||||
bank: true,
|
||||
|
|
@ -365,7 +374,7 @@ export class BranchController extends Controller {
|
|||
bank: true,
|
||||
contact: includeContact,
|
||||
},
|
||||
orderBy: { code: "asc" },
|
||||
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
|
||||
},
|
||||
bank: true,
|
||||
contact: includeContact,
|
||||
|
|
@ -378,6 +387,14 @@ export class BranchController extends Controller {
|
|||
return record;
|
||||
}
|
||||
|
||||
@Get("{branchId}/bank")
|
||||
@Security("keycloak")
|
||||
async getBranchBankById(@Path() branchId: string) {
|
||||
return await prisma.branchBank.findMany({
|
||||
where: { branchId },
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,21 @@ import HttpError from "../interfaces/http-error";
|
|||
import HttpStatus from "../interfaces/http-status";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { branchRelationPermInclude, createPermCheck } from "../services/permission";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
|
||||
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const listAllowed = ["system", "head_of_admin", "admin"];
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +106,8 @@ export class UserBranchController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
AND: {
|
||||
|
|
@ -104,9 +115,10 @@ export class UserBranchController extends Controller {
|
|||
userId,
|
||||
},
|
||||
OR: queryOrNot<Prisma.BranchUserWhereInput[]>(query, [
|
||||
{ branch: { name: { contains: query } } },
|
||||
{ branch: { nameEN: { contains: query } } },
|
||||
{ branch: { name: { contains: query, mode: "insensitive" } } },
|
||||
{ branch: { nameEN: { contains: query, mode: "insensitive" } } },
|
||||
]),
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.BranchUserWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -150,6 +162,8 @@ export class BranchUserController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
AND: {
|
||||
|
|
@ -157,13 +171,14 @@ export class BranchUserController extends Controller {
|
|||
branchId,
|
||||
},
|
||||
OR: [
|
||||
{ user: { firstName: { contains: query } } },
|
||||
{ user: { firstNameEN: { contains: query } } },
|
||||
{ user: { lastName: { contains: query } } },
|
||||
{ user: { lastNameEN: { contains: query } } },
|
||||
{ user: { email: { contains: query } } },
|
||||
{ user: { telephoneNo: { contains: query } } },
|
||||
{ user: { firstName: { contains: query, mode: "insensitive" } } },
|
||||
{ user: { firstNameEN: { contains: query, mode: "insensitive" } } },
|
||||
{ user: { lastName: { contains: query, mode: "insensitive" } } },
|
||||
{ user: { lastNameEN: { contains: query, mode: "insensitive" } } },
|
||||
{ user: { email: { contains: query, mode: "insensitive" } } },
|
||||
{ user: { telephoneNo: { contains: query, mode: "insensitive" } } },
|
||||
],
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.BranchUserWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
listRole,
|
||||
getUserRoles,
|
||||
removeUserRoles,
|
||||
getGroupUser,
|
||||
} from "../services/keycloak";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import {
|
||||
|
|
@ -37,6 +38,7 @@ import {
|
|||
getPresigned,
|
||||
listFile,
|
||||
setFile,
|
||||
uploadFile,
|
||||
} from "../utils/minio";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import {
|
||||
|
|
@ -50,6 +52,7 @@ import {
|
|||
connectOrNot,
|
||||
queryOrNot,
|
||||
whereAddressQuery,
|
||||
whereDateQuery,
|
||||
} from "../utils/relation";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { retry } from "../utils/func";
|
||||
|
|
@ -58,10 +61,17 @@ if (!process.env.MINIO_BUCKET) {
|
|||
throw Error("Require MinIO bucket.");
|
||||
}
|
||||
|
||||
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const listAllowed = ["system", "head_of_admin"];
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +88,11 @@ type UserCreate = {
|
|||
citizenExpire?: Date | null;
|
||||
|
||||
namePrefix?: string | null;
|
||||
firstName: string;
|
||||
firstName?: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string | null;
|
||||
middleNameEN?: string | null;
|
||||
lastName: string;
|
||||
lastName?: string;
|
||||
lastNameEN: string;
|
||||
gender: string;
|
||||
|
||||
|
|
@ -96,11 +106,12 @@ type UserCreate = {
|
|||
licenseIssueDate?: Date | null;
|
||||
licenseExpireDate?: Date | null;
|
||||
sourceNationality?: string | null;
|
||||
importNationality?: string | null;
|
||||
importNationality?: string[] | null;
|
||||
trainingPlace?: string | null;
|
||||
responsibleArea?: string[] | null;
|
||||
birthDate?: Date | null;
|
||||
|
||||
addressForeign?: boolean;
|
||||
address: string;
|
||||
addressEN: string;
|
||||
soi?: string | null;
|
||||
|
|
@ -112,13 +123,26 @@ type UserCreate = {
|
|||
email: string;
|
||||
telephoneNo: string;
|
||||
|
||||
subDistrictText?: string | null;
|
||||
subDistrictTextEN?: string | null;
|
||||
subDistrictId?: string | null;
|
||||
districtText?: string | null;
|
||||
districtTextEN?: string | null;
|
||||
districtId?: string | null;
|
||||
provinceText?: string | null;
|
||||
provinceTextEN?: string | null;
|
||||
provinceId?: string | null;
|
||||
zipCodeText?: string | null;
|
||||
|
||||
selectedImage?: string;
|
||||
|
||||
branchId: string | string[];
|
||||
|
||||
remark?: string;
|
||||
agencyStatus?: string;
|
||||
|
||||
contactName?: string | null;
|
||||
contactTel?: string | null;
|
||||
};
|
||||
|
||||
type UserUpdate = {
|
||||
|
|
@ -152,11 +176,12 @@ type UserUpdate = {
|
|||
licenseIssueDate?: Date | null;
|
||||
licenseExpireDate?: Date | null;
|
||||
sourceNationality?: string | null;
|
||||
importNationality?: string | null;
|
||||
importNationality?: string[] | null;
|
||||
trainingPlace?: string | null;
|
||||
responsibleArea?: string[] | null;
|
||||
birthDate?: Date | null;
|
||||
|
||||
addressForeign?: boolean;
|
||||
address?: string;
|
||||
addressEN?: string;
|
||||
soi?: string | null;
|
||||
|
|
@ -170,13 +195,27 @@ type UserUpdate = {
|
|||
|
||||
selectedImage?: string;
|
||||
|
||||
subDistrictText?: string | null;
|
||||
subDistrictTextEN?: string | null;
|
||||
subDistrictId?: string | null;
|
||||
districtText?: string | null;
|
||||
districtTextEN?: string | null;
|
||||
districtId?: string | null;
|
||||
provinceText?: string | null;
|
||||
provinceTextEN?: string | null;
|
||||
provinceId?: string | null;
|
||||
zipCodeText?: string | null;
|
||||
|
||||
branchId?: string | string[];
|
||||
|
||||
remark?: string;
|
||||
agencyStatus?: string;
|
||||
|
||||
contactName?: string | null;
|
||||
contactTel?: string | null;
|
||||
};
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCond = createPermCondition(globalAllow);
|
||||
const permissionCheck = createPermCheck(globalAllow);
|
||||
|
||||
|
|
@ -265,6 +304,8 @@ export class UserController extends Controller {
|
|||
@Query() status?: Status,
|
||||
@Query() responsibleDistrictId?: string,
|
||||
@Query() activeBranchOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return this.getUserByCriteria(
|
||||
req,
|
||||
|
|
@ -276,6 +317,8 @@ export class UserController extends Controller {
|
|||
status,
|
||||
responsibleDistrictId,
|
||||
activeBranchOnly,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -291,6 +334,8 @@ export class UserController extends Controller {
|
|||
@Query() status?: Status,
|
||||
@Query() responsibleDistrictId?: string,
|
||||
@Query() activeBranchOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Body()
|
||||
body?: {
|
||||
userId?: string[];
|
||||
|
|
@ -316,12 +361,12 @@ export class UserController extends Controller {
|
|||
const where = {
|
||||
OR: queryOrNot<Prisma.UserWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ email: { contains: query } },
|
||||
{ telephoneNo: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ email: { contains: query, mode: "insensitive" } },
|
||||
{ telephoneNo: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
]),
|
||||
AND: {
|
||||
|
|
@ -347,17 +392,21 @@ export class UserController extends Controller {
|
|||
: {
|
||||
some: {
|
||||
branch: {
|
||||
OR: permissionCond(req.user, { activeOnly: activeBranchOnly }),
|
||||
OR: responsibleDistrictId
|
||||
? permissionCondCompany(req.user, { activeOnly: activeBranchOnly }) // NOTE: when pass responsibleDistrictId should see all user not only to current branch
|
||||
: permissionCond(req.user, { activeOnly: activeBranchOnly }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.UserWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.user.findMany({
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
include: {
|
||||
importNationality: true,
|
||||
responsibleArea: true,
|
||||
province: true,
|
||||
district: true,
|
||||
|
|
@ -376,6 +425,7 @@ export class UserController extends Controller {
|
|||
return {
|
||||
result: result.map((v) => ({
|
||||
...v,
|
||||
importNationality: v.importNationality.map((v) => v.name),
|
||||
responsibleArea: v.responsibleArea.map((v) => v.area),
|
||||
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
|
||||
})),
|
||||
|
|
@ -390,6 +440,7 @@ export class UserController extends Controller {
|
|||
async getUserById(@Path() userId: string) {
|
||||
const record = await prisma.user.findFirst({
|
||||
include: {
|
||||
importNationality: true,
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
|
|
@ -401,7 +452,11 @@ export class UserController extends Controller {
|
|||
|
||||
if (!record) throw notFoundError("User");
|
||||
|
||||
return record;
|
||||
const { importNationality, ...rest } = record;
|
||||
|
||||
return Object.assign(rest, {
|
||||
importNationality: importNationality.map((v) => v.name),
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
|
@ -467,8 +522,8 @@ export class UserController extends Controller {
|
|||
}
|
||||
|
||||
const userId = await createUser(username, username, {
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
firstName: body.firstNameEN,
|
||||
lastName: body.lastNameEN,
|
||||
email: body.email,
|
||||
requiredActions: ["UPDATE_PASSWORD"],
|
||||
enabled: rest.status !== "INACTIVE",
|
||||
|
|
@ -503,6 +558,9 @@ export class UserController extends Controller {
|
|||
create: rest.responsibleArea.map((v) => ({ area: v })),
|
||||
}
|
||||
: undefined,
|
||||
importNationality: {
|
||||
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
|
||||
},
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
username,
|
||||
userRole: role.name,
|
||||
|
|
@ -658,6 +716,7 @@ export class UserController extends Controller {
|
|||
|
||||
const record = await prisma.user.update({
|
||||
include: {
|
||||
importNationality: true,
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
|
|
@ -672,6 +731,10 @@ export class UserController extends Controller {
|
|||
create: rest.responsibleArea.map((v) => ({ area: v })),
|
||||
}
|
||||
: undefined,
|
||||
importNationality: {
|
||||
deleteMany: {},
|
||||
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
|
||||
},
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
userRole,
|
||||
province: connectOrDisconnect(provinceId),
|
||||
|
|
@ -878,3 +941,62 @@ export class UserAttachmentController extends Controller {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/user/{userId}/signature")
|
||||
@Security("keycloak")
|
||||
export class UserSignatureController extends Controller {
|
||||
#checkPermission(req: RequestWithUser, userId: string) {
|
||||
if (req.user.sub !== userId) {
|
||||
throw new HttpError(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"You do not have permission to perform this action.",
|
||||
"noPermission",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
async getSignature(@Request() req: RequestWithUser, @Path() userId: string) {
|
||||
this.#checkPermission(req, userId);
|
||||
return await getFile(fileLocation.user.signature(userId));
|
||||
}
|
||||
|
||||
@Put()
|
||||
async setSignature(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() userId: string,
|
||||
@Body() signature?: { data: string },
|
||||
) {
|
||||
this.#checkPermission(req, userId);
|
||||
|
||||
const base64 = signature?.data;
|
||||
|
||||
if (base64) {
|
||||
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ""), "base64");
|
||||
const mime = "image/" + base64.split(";")[0].split("/")[1];
|
||||
await uploadFile(fileLocation.user.signature(userId), buffer, mime);
|
||||
} else {
|
||||
return await setFile(fileLocation.user.signature(userId));
|
||||
}
|
||||
}
|
||||
|
||||
@Delete()
|
||||
async deleteSignature(@Request() req: RequestWithUser, @Path() userId: string) {
|
||||
this.#checkPermission(req, userId);
|
||||
await deleteFile(fileLocation.user.signature(userId));
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/user/{userId}/group")
|
||||
@Tags("User")
|
||||
@Security("keycloak")
|
||||
export class UserGroupController extends Controller {
|
||||
@Get()
|
||||
async getUserGroup(@Path() userId: string) {
|
||||
const groupUser = await getGroupUser(userId);
|
||||
if (!Array.isArray(groupUser))
|
||||
throw new Error("Failed. Cannot get user group(s) data from the server.");
|
||||
|
||||
return groupUser;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,16 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type CustomerBranchCitizenPayload = {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
connectOrNot,
|
||||
queryOrNot,
|
||||
whereAddressQuery,
|
||||
whereDateQuery,
|
||||
} from "../utils/relation";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import {
|
||||
|
|
@ -46,15 +47,18 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
|
@ -83,7 +87,6 @@ export type CustomerBranchCreate = {
|
|||
authorizedCapital?: string;
|
||||
authorizedName?: string;
|
||||
authorizedNameEN?: string;
|
||||
customerName?: string;
|
||||
|
||||
telephoneNo: string;
|
||||
|
||||
|
|
@ -107,7 +110,7 @@ export type CustomerBranchCreate = {
|
|||
contactName: string;
|
||||
agentUserId?: string;
|
||||
|
||||
businessType: string;
|
||||
businessTypeId?: string;
|
||||
jobPosition: string;
|
||||
jobDescription: string;
|
||||
payDate: string;
|
||||
|
|
@ -141,7 +144,6 @@ export type CustomerBranchUpdate = {
|
|||
authorizedCapital?: string;
|
||||
authorizedName?: string;
|
||||
authorizedNameEN?: string;
|
||||
customerName?: string;
|
||||
|
||||
telephoneNo: string;
|
||||
|
||||
|
|
@ -165,7 +167,7 @@ export type CustomerBranchUpdate = {
|
|||
contactName?: string;
|
||||
agentUserId?: string;
|
||||
|
||||
businessType?: string;
|
||||
businessTypeId?: string;
|
||||
jobPosition?: string;
|
||||
jobDescription?: string;
|
||||
payDate?: string;
|
||||
|
|
@ -195,18 +197,19 @@ export class CustomerBranchController extends Controller {
|
|||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() activeRegisBranchOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.CustomerBranchWhereInput[]>(query, [
|
||||
{ customerName: { contains: query } },
|
||||
{ registerName: { contains: query } },
|
||||
{ registerNameEN: { contains: query } },
|
||||
{ email: { contains: query } },
|
||||
{ code: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ registerName: { contains: query, mode: "insensitive" } },
|
||||
{ registerNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ email: { contains: query, mode: "insensitive" } },
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
]),
|
||||
AND: {
|
||||
|
|
@ -229,11 +232,17 @@ export class CustomerBranchController extends Controller {
|
|||
subDistrict: zipCode ? { zipCode } : undefined,
|
||||
...filterStatus(activeRegisBranchOnly ? Status.ACTIVE : status),
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.CustomerBranchWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.customerBranch.findMany({
|
||||
orderBy: [{ code: "asc" }, { statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
include: {
|
||||
customer: includeCustomer,
|
||||
province: true,
|
||||
|
|
@ -242,6 +251,7 @@ export class CustomerBranchController extends Controller {
|
|||
createdBy: true,
|
||||
updatedBy: true,
|
||||
_count: true,
|
||||
businessType: true,
|
||||
},
|
||||
where,
|
||||
take: pageSize,
|
||||
|
|
@ -257,6 +267,11 @@ export class CustomerBranchController extends Controller {
|
|||
@Security("keycloak")
|
||||
async getById(@Path() branchId: string) {
|
||||
const record = await prisma.customerBranch.findFirst({
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
include: {
|
||||
customer: true,
|
||||
province: true,
|
||||
|
|
@ -264,6 +279,7 @@ export class CustomerBranchController extends Controller {
|
|||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
businessType: true,
|
||||
},
|
||||
where: { id: branchId },
|
||||
});
|
||||
|
|
@ -285,13 +301,15 @@ export class CustomerBranchController extends Controller {
|
|||
@Query() visa?: boolean,
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
]),
|
||||
AND: {
|
||||
|
|
@ -300,6 +318,7 @@ export class CustomerBranchController extends Controller {
|
|||
subDistrict: zipCode ? { zipCode } : undefined,
|
||||
gender,
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.EmployeeWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -343,6 +362,11 @@ export class CustomerBranchController extends Controller {
|
|||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
branch: {
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
take: 1,
|
||||
orderBy: { createdAt: "asc" },
|
||||
},
|
||||
|
|
@ -371,7 +395,15 @@ export class CustomerBranchController extends Controller {
|
|||
(v) => (v.headOffice || v).code,
|
||||
);
|
||||
|
||||
const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
|
||||
const {
|
||||
provinceId,
|
||||
districtId,
|
||||
subDistrictId,
|
||||
customerId,
|
||||
agentUserId,
|
||||
businessTypeId,
|
||||
...rest
|
||||
} = body;
|
||||
|
||||
const record = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
|
|
@ -414,6 +446,7 @@ export class CustomerBranchController extends Controller {
|
|||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
businessType: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
|
|
@ -425,6 +458,7 @@ export class CustomerBranchController extends Controller {
|
|||
province: connectOrNot(provinceId),
|
||||
district: connectOrNot(districtId),
|
||||
subDistrict: connectOrNot(subDistrictId),
|
||||
businessType: connectOrNot(businessTypeId),
|
||||
createdBy: { connect: { id: req.user.sub } },
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
|
|
@ -455,6 +489,7 @@ export class CustomerBranchController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
businessType: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -499,7 +534,15 @@ export class CustomerBranchController extends Controller {
|
|||
await permissionCheck(req.user, customer.registeredBranch);
|
||||
}
|
||||
|
||||
const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
|
||||
const {
|
||||
provinceId,
|
||||
districtId,
|
||||
subDistrictId,
|
||||
customerId,
|
||||
agentUserId,
|
||||
businessTypeId,
|
||||
...rest
|
||||
} = body;
|
||||
|
||||
return await prisma.customerBranch.update({
|
||||
where: { id: branchId },
|
||||
|
|
@ -509,6 +552,7 @@ export class CustomerBranchController extends Controller {
|
|||
subDistrict: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
businessType: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
|
|
@ -518,6 +562,7 @@ export class CustomerBranchController extends Controller {
|
|||
province: connectOrDisconnect(provinceId),
|
||||
district: connectOrDisconnect(districtId),
|
||||
subDistrict: connectOrDisconnect(subDistrictId),
|
||||
businessType: connectOrNot(businessTypeId),
|
||||
updatedBy: { connect: { id: req.user.sub } },
|
||||
},
|
||||
});
|
||||
|
|
@ -536,6 +581,7 @@ export class CustomerBranchController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
businessType: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -588,10 +634,11 @@ export class CustomerBranchFileController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
businessType: true,
|
||||
},
|
||||
});
|
||||
if (!data) throw notFoundError("Customer Branch");
|
||||
await permissionCheck(user, data.customer.registeredBranch);
|
||||
await permissionCheckCompany(user, data.customer.registeredBranch);
|
||||
}
|
||||
|
||||
@Get("attachment")
|
||||
|
|
|
|||
|
|
@ -36,21 +36,25 @@ import {
|
|||
setFile,
|
||||
} from "../utils/minio";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { connectOrNot, queryOrNot } from "../utils/relation";
|
||||
import { connectOrNot, queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import { json2csv } from "json-2-csv";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
|
@ -82,7 +86,6 @@ export type CustomerCreate = {
|
|||
authorizedCapital?: string;
|
||||
authorizedName?: string;
|
||||
authorizedNameEN?: string;
|
||||
customerName?: string;
|
||||
|
||||
telephoneNo: string;
|
||||
|
||||
|
|
@ -106,7 +109,7 @@ export type CustomerCreate = {
|
|||
contactName: string;
|
||||
agentUserId?: string;
|
||||
|
||||
businessType: string;
|
||||
businessTypeId?: string | null;
|
||||
jobPosition: string;
|
||||
jobDescription: string;
|
||||
payDate: string;
|
||||
|
|
@ -165,17 +168,22 @@ export class CustomerController extends Controller {
|
|||
@Query() includeBranch: boolean = false,
|
||||
@Query() company: boolean = false,
|
||||
@Query() activeBranchOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Query() businessTypeId?: string,
|
||||
@Query() provinceId?: string,
|
||||
@Query() districtId?: string,
|
||||
@Query() subDistrictId?: string,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.CustomerWhereInput[]>(query, [
|
||||
{ branch: { some: { namePrefix: { contains: query } } } },
|
||||
{ branch: { some: { customerName: { contains: query } } } },
|
||||
{ branch: { some: { registerName: { contains: query } } } },
|
||||
{ branch: { some: { registerNameEN: { contains: query } } } },
|
||||
{ branch: { some: { firstName: { contains: query } } } },
|
||||
{ branch: { some: { firstNameEN: { contains: query } } } },
|
||||
{ branch: { some: { lastName: { contains: query } } } },
|
||||
{ branch: { some: { lastNameEN: { contains: query } } } },
|
||||
{ branch: { some: { namePrefix: { contains: query, mode: "insensitive" } } } },
|
||||
{ branch: { some: { registerName: { contains: query, mode: "insensitive" } } } },
|
||||
{ branch: { some: { registerNameEN: { contains: query, mode: "insensitive" } } } },
|
||||
{ branch: { some: { firstName: { contains: query, mode: "insensitive" } } } },
|
||||
{ branch: { some: { firstNameEN: { contains: query, mode: "insensitive" } } } },
|
||||
{ branch: { some: { lastName: { contains: query, mode: "insensitive" } } } },
|
||||
{ branch: { some: { lastNameEN: { contains: query, mode: "insensitive" } } } },
|
||||
]),
|
||||
AND: {
|
||||
customerType,
|
||||
|
|
@ -188,6 +196,36 @@ export class CustomerController extends Controller {
|
|||
: permissionCond(req.user, { activeOnly: activeBranchOnly }),
|
||||
},
|
||||
},
|
||||
branch: {
|
||||
some: {
|
||||
AND: [
|
||||
businessTypeId
|
||||
? {
|
||||
OR: [{ businessType: { id: businessTypeId } }],
|
||||
}
|
||||
: {},
|
||||
|
||||
provinceId
|
||||
? {
|
||||
OR: [{ province: { id: provinceId } }],
|
||||
}
|
||||
: {},
|
||||
|
||||
districtId
|
||||
? {
|
||||
OR: [{ district: { id: districtId } }],
|
||||
}
|
||||
: {},
|
||||
|
||||
subDistrictId
|
||||
? {
|
||||
OR: [{ subDistrict: { id: subDistrictId } }],
|
||||
}
|
||||
: {},
|
||||
],
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.CustomerWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -197,10 +235,16 @@ export class CustomerController extends Controller {
|
|||
branch: includeBranch
|
||||
? {
|
||||
include: {
|
||||
businessType: true,
|
||||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
}
|
||||
: {
|
||||
|
|
@ -209,11 +253,17 @@ export class CustomerController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
take: 1,
|
||||
orderBy: { createdAt: "asc" },
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
// businessType:true
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
where,
|
||||
|
|
@ -238,6 +288,11 @@ export class CustomerController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
},
|
||||
createdBy: true,
|
||||
|
|
@ -309,6 +364,11 @@ export class CustomerController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
|
|
@ -320,6 +380,8 @@ export class CustomerController extends Controller {
|
|||
...v,
|
||||
code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - branch.length + i}`.padStart(2, "0")}`,
|
||||
codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""),
|
||||
businessType: connectOrNot(v.businessTypeId),
|
||||
businessTypeId: undefined,
|
||||
agentUser: connectOrNot(v.agentUserId),
|
||||
agentUserId: undefined,
|
||||
province: connectOrNot(v.provinceId),
|
||||
|
|
@ -406,6 +468,11 @@ export class CustomerController extends Controller {
|
|||
district: true,
|
||||
subDistrict: true,
|
||||
},
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
|
|
@ -444,7 +511,13 @@ export class CustomerController extends Controller {
|
|||
await deleteFolder(`customer/${customerId}`);
|
||||
const data = await tx.customer.delete({
|
||||
include: {
|
||||
branch: true,
|
||||
branch: {
|
||||
omit: {
|
||||
otpCode: true,
|
||||
otpExpires: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
registeredBranch: {
|
||||
include: {
|
||||
headOffice: true,
|
||||
|
|
@ -539,3 +612,52 @@ export class CustomerImageController extends Controller {
|
|||
await deleteFile(fileLocation.customer.img(customerId, name));
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/customer-export")
|
||||
@Tags("Customer")
|
||||
export class CustomerExportController extends CustomerController {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async exportCustomer(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() customerType?: CustomerType,
|
||||
@Query() query: string = "",
|
||||
@Query() status?: Status,
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() includeBranch: boolean = false,
|
||||
@Query() company: boolean = false,
|
||||
@Query() activeBranchOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Query() businessTypeId?: string,
|
||||
@Query() provinceId?: string,
|
||||
@Query() districtId?: string,
|
||||
@Query() subDistrictId?: string,
|
||||
) {
|
||||
const ret = await this.list(
|
||||
req,
|
||||
customerType,
|
||||
query,
|
||||
status,
|
||||
page,
|
||||
pageSize,
|
||||
includeBranch,
|
||||
company,
|
||||
activeBranchOnly,
|
||||
startDate,
|
||||
endDate,
|
||||
businessTypeId,
|
||||
provinceId,
|
||||
districtId,
|
||||
subDistrictId,
|
||||
);
|
||||
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
|
||||
return json2csv(
|
||||
ret.result.map((v) => Object.assign(v, { branch: v.branch.at(0) ?? null })),
|
||||
{ useDateIso8601Format: true, expandNestedObjects: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,18 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type EmployeeCheckupPayload = {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
connectOrNot,
|
||||
queryOrNot,
|
||||
whereAddressQuery,
|
||||
whereDateQuery,
|
||||
} from "../utils/relation";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import {
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
listFile,
|
||||
setFile,
|
||||
} from "../utils/minio";
|
||||
import { json2csv } from "json-2-csv";
|
||||
|
||||
if (!process.env.MINIO_BUCKET) {
|
||||
throw Error("Require MinIO bucket.");
|
||||
|
|
@ -50,17 +52,23 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCond = createPermCondition(globalAllow);
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
const permissionCheck = createPermCheck(globalAllow);
|
||||
|
||||
type EmployeeCreate = {
|
||||
|
|
@ -70,16 +78,17 @@ type EmployeeCreate = {
|
|||
|
||||
nrcNo?: string | null;
|
||||
|
||||
dateOfBirth: Date;
|
||||
dateOfBirth?: Date | null;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
|
||||
namePrefix?: string | null;
|
||||
firstName: string;
|
||||
firstName?: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string | null;
|
||||
middleNameEN?: string | null;
|
||||
lastName: string;
|
||||
lastName?: string;
|
||||
lastNameEN: string;
|
||||
|
||||
addressEN: string;
|
||||
|
|
@ -106,13 +115,14 @@ type EmployeeUpdate = {
|
|||
|
||||
nrcNo?: string | null;
|
||||
|
||||
dateOfBirth?: Date;
|
||||
dateOfBirth?: Date | null;
|
||||
gender?: string;
|
||||
nationality?: string;
|
||||
otherNationality?: string | null;
|
||||
|
||||
namePrefix?: string | null;
|
||||
firstName?: string;
|
||||
firstNameEN?: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string | null;
|
||||
middleNameEN?: string | null;
|
||||
lastName?: string;
|
||||
|
|
@ -141,9 +151,18 @@ type EmployeeUpdate = {
|
|||
export class EmployeeController extends Controller {
|
||||
@Get("stats")
|
||||
@Security("keycloak")
|
||||
async getEmployeeStats(@Query() customerBranchId?: string) {
|
||||
async getEmployeeStats(@Request() req: RequestWithUser, @Query() customerBranchId?: string) {
|
||||
return await prisma.employee.count({
|
||||
where: { customerBranchId },
|
||||
where: {
|
||||
customerBranchId,
|
||||
customerBranch: {
|
||||
customer: isSystem(req.user)
|
||||
? undefined
|
||||
: {
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +173,8 @@ export class EmployeeController extends Controller {
|
|||
@Query() customerBranchId?: string,
|
||||
@Query() status?: Status,
|
||||
@Query() query: string = "",
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return await prisma.employee
|
||||
.groupBy({
|
||||
|
|
@ -163,13 +184,13 @@ export class EmployeeController extends Controller {
|
|||
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
|
||||
{
|
||||
employeePassport: {
|
||||
some: { number: { contains: query } },
|
||||
some: { number: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
]),
|
||||
AND: {
|
||||
|
|
@ -183,6 +204,7 @@ export class EmployeeController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
},
|
||||
})
|
||||
.then((res) =>
|
||||
|
|
@ -208,6 +230,8 @@ export class EmployeeController extends Controller {
|
|||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return this.listByCriteria(
|
||||
req,
|
||||
|
|
@ -222,9 +246,10 @@ export class EmployeeController extends Controller {
|
|||
page,
|
||||
pageSize,
|
||||
activeOnly,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("list")
|
||||
@Security("keycloak")
|
||||
async listByCriteria(
|
||||
|
|
@ -240,6 +265,8 @@ export class EmployeeController extends Controller {
|
|||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Body()
|
||||
body?: {
|
||||
passport?: string[];
|
||||
|
|
@ -252,13 +279,13 @@ export class EmployeeController extends Controller {
|
|||
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
|
||||
{
|
||||
employeePassport: {
|
||||
some: { number: { contains: query } },
|
||||
some: { number: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
]) ?? []),
|
||||
...(queryOrNot<Prisma.EmployeeWhereInput[]>(!!body, [
|
||||
|
|
@ -288,6 +315,7 @@ export class EmployeeController extends Controller {
|
|||
subDistrict: zipCode ? { zipCode } : undefined,
|
||||
gender,
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.EmployeeWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -364,9 +392,10 @@ export class EmployeeController extends Controller {
|
|||
},
|
||||
}),
|
||||
]);
|
||||
if (body.provinceId !== province?.id) throw relationError("Province");
|
||||
if (body.districtId !== district?.id) throw relationError("District");
|
||||
if (body.subDistrictId !== subDistrict?.id) throw relationError("SubDistrict");
|
||||
if (!!body.provinceId && body.provinceId !== province?.id) throw relationError("Province");
|
||||
if (!!body.districtId && body.districtId !== district?.id) throw relationError("District");
|
||||
if (!!body.subDistrictId && body.subDistrictId !== subDistrict?.id)
|
||||
throw relationError("SubDistrict");
|
||||
if (!customerBranch) throw relationError("Customer Branch");
|
||||
|
||||
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
|
||||
|
|
@ -642,7 +671,7 @@ export class EmployeeFileController extends Controller {
|
|||
},
|
||||
});
|
||||
if (!data) throw notFoundError("Employee");
|
||||
await permissionCheck(user, data.customerBranch.customer.registeredBranch);
|
||||
await permissionCheckCompany(user, data.customerBranch.customer.registeredBranch);
|
||||
}
|
||||
|
||||
@Get("image")
|
||||
|
|
@ -898,3 +927,55 @@ export class EmployeeFileController extends Controller {
|
|||
return await deleteFile(fileLocation.employee.inCountryNotice(employeeId, noticeId));
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/employee-export")
|
||||
@Tags("Employee")
|
||||
export class EmployeeExportController extends EmployeeController {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async exportEmployee(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() zipCode?: string,
|
||||
@Query() gender?: string,
|
||||
@Query() status?: Status,
|
||||
@Query() visa?: boolean,
|
||||
@Query() passport?: boolean,
|
||||
@Query() customerId?: string,
|
||||
@Query() customerBranchId?: string,
|
||||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const ret = await this.listByCriteria(
|
||||
req,
|
||||
zipCode,
|
||||
gender,
|
||||
status,
|
||||
visa,
|
||||
passport,
|
||||
customerId,
|
||||
customerBranchId,
|
||||
query,
|
||||
page,
|
||||
pageSize,
|
||||
activeOnly,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
|
||||
return json2csv(
|
||||
ret.result.map((v) =>
|
||||
Object.assign(v, {
|
||||
employeePassport: v.employeePassport?.at(0) ?? null,
|
||||
employeeVisa: v.employeeVisa?.at(0) ?? null,
|
||||
}),
|
||||
),
|
||||
{ useDateIso8601Format: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,18 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type EmployeeOtherInfoPayload = {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,18 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type EmployeePassportPayload = {
|
||||
|
|
@ -43,6 +47,7 @@ type EmployeePassportPayload = {
|
|||
|
||||
workerStatus: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
namePrefix?: string | null;
|
||||
firstName: string;
|
||||
firstNameEN: string;
|
||||
|
|
|
|||
|
|
@ -22,14 +22,18 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type EmployeeVisaPayload = {
|
||||
|
|
@ -40,6 +44,7 @@ type EmployeeVisaPayload = {
|
|||
issuePlace: string;
|
||||
issueDate: Date;
|
||||
expireDate: Date;
|
||||
reportDate?: Date | null;
|
||||
mrz?: string | null;
|
||||
remark?: string | null;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,14 +22,18 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
type EmployeeWorkPayload = {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import HttpError from "../interfaces/http-error";
|
|||
import HttpStatus from "../interfaces/http-status";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
|
||||
type WorkflowPayload = {
|
||||
name: string;
|
||||
|
|
@ -37,20 +37,37 @@ type WorkflowPayload = {
|
|||
attributes?: { [key: string]: any };
|
||||
responsiblePersonId?: string[];
|
||||
responsibleInstitution?: string[];
|
||||
responsibleGroup?: string[];
|
||||
messengerByArea?: boolean;
|
||||
}[];
|
||||
registeredBranchId?: string;
|
||||
status?: Status;
|
||||
};
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition(globalAllow);
|
||||
const permissionCheckCompany = createPermCheck(globalAllow);
|
||||
|
||||
@Route("api/v1/workflow-template")
|
||||
@Tags("Workflow")
|
||||
@Security("keycloak")
|
||||
export class FlowTemplateController extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async getFlowTemplate(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() page: number = 1,
|
||||
|
|
@ -58,13 +75,15 @@ export class FlowTemplateController extends Controller {
|
|||
@Query() status?: Status,
|
||||
@Query() query = "",
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot(query, [
|
||||
{ name: { contains: query } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{
|
||||
step: {
|
||||
some: { name: { contains: query } },
|
||||
some: { name: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
|
@ -74,6 +93,7 @@ export class FlowTemplateController extends Controller {
|
|||
OR: permissionCondCompany(req.user, { activeOnly: true }),
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.WorkflowTemplateWhereInput;
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.workflowTemplate.findMany({
|
||||
|
|
@ -86,6 +106,7 @@ export class FlowTemplateController extends Controller {
|
|||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
responsibleGroup: true,
|
||||
},
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
|
|
@ -103,6 +124,7 @@ export class FlowTemplateController extends Controller {
|
|||
step: r.step.map((v) => ({
|
||||
...v,
|
||||
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
|
||||
responsibleGroup: v.responsibleGroup.map((group) => group.group),
|
||||
})),
|
||||
})),
|
||||
page,
|
||||
|
|
@ -112,6 +134,7 @@ export class FlowTemplateController extends Controller {
|
|||
}
|
||||
|
||||
@Get("{templateId}")
|
||||
@Security("keycloak")
|
||||
async getFlowTemplateById(@Request() _req: RequestWithUser, @Path() templateId: string) {
|
||||
const record = await prisma.workflowTemplate.findFirst({
|
||||
include: {
|
||||
|
|
@ -123,6 +146,7 @@ export class FlowTemplateController extends Controller {
|
|||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
responsibleGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -137,11 +161,13 @@ export class FlowTemplateController extends Controller {
|
|||
step: record.step.map((v) => ({
|
||||
...v,
|
||||
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
|
||||
responsibleGroup: v.responsibleGroup.map((group) => group.group),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async createFlowTemplate(@Request() req: RequestWithUser, @Body() body: WorkflowPayload) {
|
||||
const where = {
|
||||
OR: [
|
||||
|
|
@ -212,6 +238,9 @@ export class FlowTemplateController extends Controller {
|
|||
responsibleInstitution: {
|
||||
create: v.responsibleInstitution?.map((group) => ({ group })),
|
||||
},
|
||||
responsibleGroup: {
|
||||
create: v.responsibleGroup?.map((group) => ({ group })),
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
|
@ -219,6 +248,7 @@ export class FlowTemplateController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{templateId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async updateFlowTemplate(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() templateId: string,
|
||||
|
|
@ -292,6 +322,10 @@ export class FlowTemplateController extends Controller {
|
|||
deleteMany: {},
|
||||
create: v.responsibleInstitution?.map((group) => ({ group })),
|
||||
},
|
||||
responsibleGroup: {
|
||||
deleteMany: {},
|
||||
create: v.responsibleGroup?.map((group) => ({ group })),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
|
@ -300,6 +334,7 @@ export class FlowTemplateController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("{templateId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async deleteFlowTemplateById(@Request() req: RequestWithUser, @Path() templateId: string) {
|
||||
const record = await prisma.workflowTemplate.findUnique({
|
||||
where: { id: templateId },
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "tsoa";
|
||||
import prisma from "../db";
|
||||
import { isUsedError, notFoundError } from "../utils/error";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
|
|
@ -44,8 +44,68 @@ type InstitutionPayload = {
|
|||
provinceId: string;
|
||||
|
||||
selectedImage?: string | null;
|
||||
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactTel?: string;
|
||||
|
||||
bank?: {
|
||||
bankName: string;
|
||||
bankBranch: string;
|
||||
accountName: string;
|
||||
accountNumber: string;
|
||||
accountType: string;
|
||||
currentlyUse: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
type InstitutionUpdatePayload = {
|
||||
name: string;
|
||||
nameEN: string;
|
||||
|
||||
code: string;
|
||||
|
||||
addressEN: string;
|
||||
address: string;
|
||||
soi?: string | null;
|
||||
soiEN?: string | null;
|
||||
moo?: string | null;
|
||||
mooEN?: string | null;
|
||||
street?: string | null;
|
||||
streetEN?: string | null;
|
||||
|
||||
subDistrictId: string;
|
||||
districtId: string;
|
||||
provinceId: string;
|
||||
|
||||
selectedImage?: string | null;
|
||||
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactTel?: string;
|
||||
|
||||
bank?: {
|
||||
id?: string;
|
||||
bankName: string;
|
||||
bankBranch: string;
|
||||
accountName: string;
|
||||
accountNumber: string;
|
||||
accountType: string;
|
||||
currentlyUse: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
@Route("api/v1/institution")
|
||||
@Tags("Institution")
|
||||
export class InstitutionController extends Controller {
|
||||
|
|
@ -59,8 +119,19 @@ export class InstitutionController extends Controller {
|
|||
@Query() status?: Status,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() group?: string,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return this.getInstitutionListByCriteria(query, page, pageSize, status, activeOnly, group);
|
||||
return this.getInstitutionListByCriteria(
|
||||
query,
|
||||
page,
|
||||
pageSize,
|
||||
status,
|
||||
activeOnly,
|
||||
group,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("list")
|
||||
|
|
@ -73,6 +144,8 @@ export class InstitutionController extends Controller {
|
|||
@Query() status?: Status,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() group?: string,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Body()
|
||||
body?: {
|
||||
group?: string[];
|
||||
|
|
@ -82,9 +155,10 @@ export class InstitutionController extends Controller {
|
|||
...filterStatus(activeOnly ? Status.ACTIVE : status),
|
||||
group: body?.group ? { in: body.group } : group,
|
||||
OR: queryOrNot<Prisma.InstitutionWhereInput[]>(query, [
|
||||
{ name: { contains: query } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.InstitutionWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -94,6 +168,7 @@ export class InstitutionController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
bank: true,
|
||||
},
|
||||
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
|
||||
take: pageSize,
|
||||
|
|
@ -114,19 +189,21 @@ export class InstitutionController extends Controller {
|
|||
province: true,
|
||||
district: true,
|
||||
subDistrict: true,
|
||||
bank: true,
|
||||
},
|
||||
where: { id: institutionId, group },
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@OperationId("createInstitution")
|
||||
async createInstitution(
|
||||
@Body()
|
||||
body: InstitutionPayload & {
|
||||
status?: Status;
|
||||
},
|
||||
@Request() req: RequestWithUser,
|
||||
) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
|
|
@ -141,33 +218,78 @@ export class InstitutionController extends Controller {
|
|||
});
|
||||
|
||||
return await tx.institution.create({
|
||||
include: {
|
||||
bank: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
code: `${body.code}${last.value.toString().padStart(5, "0")}`,
|
||||
group: body.code,
|
||||
bank: {
|
||||
createMany: {
|
||||
data: body.bank ?? [],
|
||||
},
|
||||
},
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Put("{institutionId}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@OperationId("updateInstitution")
|
||||
async updateInstitution(
|
||||
@Path() institutionId: string,
|
||||
@Body()
|
||||
body: InstitutionPayload & {
|
||||
body: InstitutionUpdatePayload & {
|
||||
status?: "ACTIVE" | "INACTIVE";
|
||||
},
|
||||
) {
|
||||
return await prisma.institution.update({
|
||||
where: { id: institutionId },
|
||||
data: { ...body, statusOrder: +(body.status === "INACTIVE") },
|
||||
const { bank } = body;
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const listDeleted = bank
|
||||
? await tx.institutionBank.findMany({
|
||||
where: {
|
||||
id: { not: { in: bank.flatMap((v) => (!!v.id ? v.id : [])) } },
|
||||
institutionId,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
await Promise.all(
|
||||
listDeleted.map((v) => deleteFile(fileLocation.institution.bank(v.institutionId, v.id))),
|
||||
);
|
||||
|
||||
return await prisma.institution.update({
|
||||
include: {
|
||||
bank: true,
|
||||
},
|
||||
where: { id: institutionId },
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
bank: bank
|
||||
? {
|
||||
deleteMany:
|
||||
listDeleted.length > 0 ? { id: { in: listDeleted.map((v) => v.id) } } : undefined,
|
||||
upsert: bank.map((v) => ({
|
||||
where: { id: v.id || "" },
|
||||
create: { ...v, id: undefined },
|
||||
update: v,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Delete("{institutionId}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@OperationId("deleteInstitution")
|
||||
async deleteInstitution(@Path() institutionId: string) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
|
|
@ -185,9 +307,18 @@ export class InstitutionController extends Controller {
|
|||
throw isUsedError("Institution");
|
||||
}
|
||||
|
||||
return await tx.institution.delete({
|
||||
const data = await tx.institution.delete({
|
||||
include: {
|
||||
bank: true,
|
||||
},
|
||||
where: { id: institutionId },
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
...data.bank.map((v) => deleteFile(fileLocation.institution.bank(institutionId, v.id))),
|
||||
]);
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -230,7 +361,7 @@ export class InstitutionFileController extends Controller {
|
|||
}
|
||||
|
||||
@Put("image/{name}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async putImage(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
|
|
@ -244,7 +375,7 @@ export class InstitutionFileController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("image/{name}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async delImage(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
|
|
@ -274,7 +405,7 @@ export class InstitutionFileController extends Controller {
|
|||
}
|
||||
|
||||
@Put("attachment/{name}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async putAttachment(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
|
|
@ -285,7 +416,7 @@ export class InstitutionFileController extends Controller {
|
|||
}
|
||||
|
||||
@Delete("attachment/{name}")
|
||||
@Security("keycloak")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async delAttachment(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
|
|
@ -294,4 +425,49 @@ export class InstitutionFileController extends Controller {
|
|||
await this.checkPermission(req.user, institutionId);
|
||||
return await deleteFile(fileLocation.institution.attachment(institutionId, name));
|
||||
}
|
||||
|
||||
@Get("bank-qr/{bankId}")
|
||||
async getBankImage(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
@Path() bankId: string,
|
||||
) {
|
||||
return req.res?.redirect(await getFile(fileLocation.institution.bank(institutionId, bankId)));
|
||||
}
|
||||
|
||||
@Head("bank-qr/{bankId}")
|
||||
async headBankImage(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
@Path() bankId: string,
|
||||
) {
|
||||
return req.res?.redirect(
|
||||
await getPresigned("head", fileLocation.institution.bank(institutionId, bankId)),
|
||||
);
|
||||
}
|
||||
|
||||
@Put("bank-qr/{bankId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async putBankImage(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
@Path() bankId: string,
|
||||
) {
|
||||
if (!req.headers["content-type"]?.startsWith("image/")) {
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Not a valid image.", "notValidImage");
|
||||
}
|
||||
await this.checkPermission(req.user, institutionId);
|
||||
return req.res?.redirect(await setFile(fileLocation.institution.bank(institutionId, bankId)));
|
||||
}
|
||||
|
||||
@Delete("bank-qr/{bankId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async delBankImage(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() institutionId: string,
|
||||
@Path() bankId: string,
|
||||
) {
|
||||
await this.checkPermission(req.user, institutionId);
|
||||
return await deleteFile(fileLocation.institution.bank(institutionId, bankId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
createPermCondition,
|
||||
} from "../services/permission";
|
||||
import { PaymentStatus } from "../generated/kysely/types";
|
||||
import { whereDateQuery } from "../utils/relation";
|
||||
|
||||
type InvoicePayload = {
|
||||
quotationId: string;
|
||||
|
|
@ -28,14 +29,23 @@ type InvoicePayload = {
|
|||
installmentNo: number[];
|
||||
};
|
||||
|
||||
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCondCompany = createPermCondition(globalAllow);
|
||||
const permissionCheck = createPermCheck(globalAllow);
|
||||
|
||||
@Route("/api/v1/invoice")
|
||||
|
|
@ -95,23 +105,24 @@ export class InvoiceController extends Controller {
|
|||
@Query() quotationId?: string,
|
||||
@Query() debitNoteId?: string,
|
||||
@Query() pay?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where: Prisma.InvoiceWhereInput = {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ quotation: { workName: { contains: query } } },
|
||||
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
|
||||
{
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ registerName: { contains: query } },
|
||||
{ registerNameEN: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ registerName: { contains: query, mode: "insensitive" } },
|
||||
{ registerNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -132,6 +143,7 @@ export class InvoiceController extends Controller {
|
|||
OR: permissionCondCompany(req.user),
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
};
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -180,7 +192,7 @@ export class InvoiceController extends Controller {
|
|||
|
||||
@Post()
|
||||
@OperationId("createInvoice")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
|
||||
async createInvoice(@Request() req: RequestWithUser, @Body() body: InvoicePayload) {
|
||||
const [quotation] = await prisma.$transaction([
|
||||
prisma.quotation.findUnique({
|
||||
|
|
@ -219,6 +231,16 @@ export class InvoiceController extends Controller {
|
|||
quotationStatus: "PaymentInProcess",
|
||||
},
|
||||
});
|
||||
|
||||
await tx.notification.create({
|
||||
data: {
|
||||
title: "ใบแจ้งหนี้ใหม่ / New Invoice",
|
||||
detail: "รหัส / code : " + record.code,
|
||||
registeredBranchId: record.registeredBranchId,
|
||||
groupReceiver: { create: { name: "branch_accountant" } },
|
||||
},
|
||||
});
|
||||
|
||||
return await tx.invoice.create({
|
||||
data: {
|
||||
quotationId: body.quotationId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Security,
|
||||
Tags,
|
||||
Query,
|
||||
UploadedFile,
|
||||
} from "tsoa";
|
||||
import { Prisma, Product, Status } from "@prisma/client";
|
||||
|
||||
|
|
@ -25,22 +26,27 @@ import {
|
|||
} from "../services/permission";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import { deleteFile, fileLocation, getFile, listFile, setFile } from "../utils/minio";
|
||||
import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } from "../utils/minio";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import spreadsheet from "../utils/spreadsheet";
|
||||
import flowAccount from "../services/flowaccount";
|
||||
import { json2csv } from "json-2-csv";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
|
@ -72,6 +78,7 @@ type ProductCreate = {
|
|||
|
||||
type ProductUpdate = {
|
||||
status?: "ACTIVE" | "INACTIVE";
|
||||
code?: string;
|
||||
name?: string;
|
||||
detail?: string;
|
||||
process?: number;
|
||||
|
|
@ -139,6 +146,8 @@ export class ProductController extends Controller {
|
|||
@Query() orderField?: keyof Product,
|
||||
@Query() orderBy?: "asc" | "desc",
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
// NOTE: will be used to scope product within product group that is shared between branch but not company when select shared product if user is system
|
||||
const targetGroup =
|
||||
|
|
@ -154,8 +163,8 @@ export class ProductController extends Controller {
|
|||
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.ProductWhereInput[]>(query, [
|
||||
{ name: { contains: query } },
|
||||
{ detail: { contains: query } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ detail: { contains: query, mode: "insensitive" } },
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
AND: {
|
||||
|
|
@ -194,6 +203,7 @@ export class ProductController extends Controller {
|
|||
: []),
|
||||
],
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.ProductWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -292,13 +302,21 @@ export class ProductController extends Controller {
|
|||
},
|
||||
update: { value: { increment: 1 } },
|
||||
});
|
||||
return await prisma.product.create({
|
||||
|
||||
const listId = await flowAccount.createProducts(
|
||||
`${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
||||
body,
|
||||
);
|
||||
|
||||
return await tx.product.create({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
data: {
|
||||
...body,
|
||||
flowAccountProductIdAgentPrice: `${listId.data.productIdAgentPrice}`,
|
||||
flowAccountProductIdSellPrice: `${listId.data.productIdSellPrice}`,
|
||||
document: body.document
|
||||
? {
|
||||
createMany: { data: body.document.map((v) => ({ name: v })) },
|
||||
|
|
@ -372,8 +390,33 @@ export class ProductController extends Controller {
|
|||
await permissionCheck(req.user, productGroup.registeredBranch);
|
||||
}
|
||||
|
||||
if (
|
||||
product.flowAccountProductIdSellPrice !== null &&
|
||||
product.flowAccountProductIdAgentPrice !== null
|
||||
) {
|
||||
const mergedBody = {
|
||||
...body,
|
||||
code: body.code ?? product.code,
|
||||
price: body.price ?? product.price,
|
||||
agentPrice: body.agentPrice ?? product.agentPrice,
|
||||
serviceCharge: body.serviceCharge ?? product.serviceCharge,
|
||||
vatIncluded: body.vatIncluded ?? product.vatIncluded,
|
||||
agentPriceVatIncluded: body.agentPriceVatIncluded ?? product.agentPriceVatIncluded,
|
||||
serviceChargeVatIncluded: body.serviceChargeVatIncluded ?? product.serviceChargeVatIncluded,
|
||||
};
|
||||
|
||||
await flowAccount.editProducts(
|
||||
product.flowAccountProductIdSellPrice,
|
||||
product.flowAccountProductIdAgentPrice,
|
||||
mergedBody,
|
||||
);
|
||||
} else {
|
||||
throw notFoundError("FlowAccountProductId");
|
||||
}
|
||||
|
||||
const record = await prisma.product.update({
|
||||
include: {
|
||||
productGroup: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
|
|
@ -398,6 +441,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;
|
||||
}
|
||||
|
||||
|
|
@ -422,6 +476,20 @@ export class ProductController extends Controller {
|
|||
|
||||
if (record.status !== Status.CREATED) throw isUsedError("Product");
|
||||
|
||||
if (
|
||||
record.flowAccountProductIdSellPrice !== null &&
|
||||
record.flowAccountProductIdAgentPrice !== null
|
||||
) {
|
||||
await Promise.all([
|
||||
flowAccount.deleteProduct(record.flowAccountProductIdSellPrice),
|
||||
flowAccount.deleteProduct(record.flowAccountProductIdAgentPrice),
|
||||
]);
|
||||
} else {
|
||||
throw notFoundError("FlowAccountProductId");
|
||||
}
|
||||
|
||||
await deleteFolder(fileLocation.product.img(productId));
|
||||
|
||||
return await prisma.product.delete({
|
||||
include: {
|
||||
createdBy: true,
|
||||
|
|
@ -430,6 +498,146 @@ export class ProductController extends Controller {
|
|||
where: { id: productId },
|
||||
});
|
||||
}
|
||||
|
||||
@Post("import-product")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async importProduct(
|
||||
@Request() req: RequestWithUser,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query() productGroupId: string,
|
||||
) {
|
||||
if (!file?.buffer) throw notFoundError("File");
|
||||
|
||||
const buffer = new Uint8Array(file.buffer).buffer;
|
||||
const dataFile = await spreadsheet.readExcel(buffer, {
|
||||
header: true,
|
||||
worksheet: "Sheet1",
|
||||
});
|
||||
|
||||
let dataName: string[] = [];
|
||||
const data = dataFile.map((item: any) => {
|
||||
dataName.push(item.name);
|
||||
return {
|
||||
...item,
|
||||
expenseType:
|
||||
item.expenseType === "ค่าธรรมเนียม"
|
||||
? "fee"
|
||||
: item.expenseType === "ค่าบริการ"
|
||||
? "serviceFee"
|
||||
: "processingFee",
|
||||
shared: item.shared === "ใช่" ? true : false,
|
||||
price:
|
||||
typeof item.price === "number"
|
||||
? item.price
|
||||
: +parseFloat(item.price?.replace(",", "") || "0").toFixed(6),
|
||||
calcVat: item.calcVat === "ใช่" ? true : false,
|
||||
vatIncluded: item.vatIncluded === "รวม" ? true : false,
|
||||
agentPrice:
|
||||
typeof item.agentPrice === "number"
|
||||
? item.agentPrice
|
||||
: +parseFloat(item.agentPrice?.replace(",", "") || "0").toFixed(6),
|
||||
agentPriceCalcVat: item.agentPriceCalcVat === "ใช่" ? true : false,
|
||||
agentPriceVatIncluded: item.agentPriceVatIncluded === "รวม" ? true : false,
|
||||
serviceCharge:
|
||||
typeof item.serviceCharge === "number"
|
||||
? item.serviceCharge
|
||||
: +parseFloat(item.serviceCharge?.replace(",", "") || "0").toFixed(6),
|
||||
serviceChargeCalcVat: item.serviceChargeCalcVat === "ใช่" ? true : false,
|
||||
serviceChargeVatIncluded: item.serviceChargeVatIncluded === "รวม" ? true : false,
|
||||
};
|
||||
});
|
||||
|
||||
const [productGroup, productSameName] = await prisma.$transaction([
|
||||
prisma.productGroup.findFirst({
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: productGroupId },
|
||||
}),
|
||||
prisma.product.findMany({
|
||||
where: {
|
||||
productGroup: {
|
||||
id: productGroupId,
|
||||
registeredBranch: {
|
||||
OR: permissionCondCompany(req.user),
|
||||
},
|
||||
},
|
||||
name: { in: dataName },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!productGroup) throw relationError("Product Group");
|
||||
|
||||
await permissionCheck(req.user, productGroup.registeredBranch);
|
||||
let dataProduct: ProductCreate[] = [];
|
||||
|
||||
const record = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const branch = productGroup.registeredBranch;
|
||||
const company = (branch.headOffice || branch).code;
|
||||
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const dataDuplicate = productSameName.some(
|
||||
(v) => v.code.slice(0, -3) === item.code.toUpperCase() && v.name === item.name,
|
||||
);
|
||||
|
||||
if (!dataDuplicate) {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
|
||||
},
|
||||
create: {
|
||||
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
|
||||
value: 1,
|
||||
},
|
||||
update: { value: { increment: 1 } },
|
||||
});
|
||||
|
||||
dataProduct.push({
|
||||
...item,
|
||||
code: `${item.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
productGroupId: productGroupId,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return await prisma.product.createManyAndReturn({
|
||||
data: dataProduct,
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
||||
},
|
||||
);
|
||||
|
||||
if (productGroup.status === "CREATED") {
|
||||
await prisma.productGroup.update({
|
||||
include: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
where: { id: productGroupId },
|
||||
data: { status: Status.ACTIVE },
|
||||
});
|
||||
}
|
||||
|
||||
this.setStatus(HttpStatus.CREATED);
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/product/{productId}")
|
||||
|
|
@ -481,3 +689,43 @@ export class ProductFileController extends Controller {
|
|||
return await deleteFile(fileLocation.product.img(productId, name));
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/product-export")
|
||||
@Tags("Product")
|
||||
export class ProductExportController extends ProductController {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async exportCustomer(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() status?: Status,
|
||||
@Query() shared?: boolean,
|
||||
@Query() productGroupId?: string,
|
||||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() orderField?: keyof Product,
|
||||
@Query() orderBy?: "asc" | "desc",
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const ret = await this.getProduct(
|
||||
req,
|
||||
status,
|
||||
shared,
|
||||
productGroupId,
|
||||
query,
|
||||
page,
|
||||
pageSize,
|
||||
orderField,
|
||||
orderBy,
|
||||
activeOnly,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
|
||||
this.setHeader("Content-Type", "text/csv");
|
||||
|
||||
return json2csv(ret.result, { useDateIso8601Format: true, expandNestedObjects: true });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from "../services/permission";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
|
||||
type ProductGroupCreate = {
|
||||
name: string;
|
||||
|
|
@ -35,7 +35,7 @@ type ProductGroupCreate = {
|
|||
remark: string;
|
||||
status?: Status;
|
||||
shared?: boolean;
|
||||
registeredBranchId: string;
|
||||
registeredBranchId?: string;
|
||||
};
|
||||
|
||||
type ProductGroupUpdate = {
|
||||
|
|
@ -51,14 +51,16 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCond = createPermCondition((_) => true);
|
||||
|
|
@ -90,11 +92,13 @@ export class ProductGroup extends Controller {
|
|||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.ProductGroupWhereInput[]>(query, [
|
||||
{ name: { contains: query } },
|
||||
{ detail: { contains: query } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ detail: { contains: query, mode: "insensitive" } },
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
AND: [
|
||||
|
|
@ -105,6 +109,7 @@ export class ProductGroup extends Controller {
|
|||
: { OR: permissionCond(req.user, { activeOnly }) },
|
||||
},
|
||||
],
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.ProductGroupWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -154,7 +159,23 @@ export class ProductGroup extends Controller {
|
|||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
|
||||
let company = await permissionCheck(req.user, body.registeredBranchId).then(
|
||||
const userAffiliatedBranch = await prisma.branch.findFirst({
|
||||
include: branchRelationPermInclude(req.user),
|
||||
where: body.registeredBranchId
|
||||
? { id: body.registeredBranchId }
|
||||
: {
|
||||
user: { some: { userId: req.user.sub } },
|
||||
},
|
||||
});
|
||||
if (!userAffiliatedBranch) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
|
||||
"reqMinAffilatedBranch",
|
||||
);
|
||||
}
|
||||
|
||||
let company = await permissionCheck(req.user, userAffiliatedBranch).then(
|
||||
(v) => (v.headOffice || v).code,
|
||||
);
|
||||
|
||||
|
|
@ -178,6 +199,7 @@ export class ProductGroup extends Controller {
|
|||
},
|
||||
data: {
|
||||
...body,
|
||||
registeredBranchId: userAffiliatedBranch.id,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
code: `G${last.value.toString().padStart(2, "0")}`,
|
||||
createdByUserId: req.user.sub,
|
||||
|
|
|
|||
193
src/controllers/04-properties-controller.ts
Normal file
193
src/controllers/04-properties-controller.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
Route,
|
||||
Security,
|
||||
Tags,
|
||||
} from "tsoa";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import prisma from "../db";
|
||||
import { Prisma, Status } from "@prisma/client";
|
||||
import {
|
||||
branchRelationPermInclude,
|
||||
createPermCheck,
|
||||
createPermCondition,
|
||||
} from "../services/permission";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
|
||||
type PropertyPayload = {
|
||||
name: string;
|
||||
nameEN: string;
|
||||
type: Record<string, any>;
|
||||
registeredBranchId?: string;
|
||||
status?: Status;
|
||||
};
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
|
||||
@Route("api/v1/property")
|
||||
@Tags("Property")
|
||||
@Security("keycloak")
|
||||
export class PropertiesController extends Controller {
|
||||
@Get()
|
||||
async getProperties(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() status?: Status,
|
||||
@Query() query = "",
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot(query, [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ nameEN: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
AND: {
|
||||
...filterStatus(activeOnly ? Status.ACTIVE : status),
|
||||
registeredBranch: {
|
||||
OR: permissionCondCompany(req.user, { activeOnly: true }),
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.PropertyWhereInput;
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.property.findMany({
|
||||
where,
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
}),
|
||||
prisma.property.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
result,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("{propertyId}")
|
||||
async getPropertyById(@Request() _req: RequestWithUser, @Path() propertyId: string) {
|
||||
const record = await prisma.property.findFirst({
|
||||
where: { id: propertyId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("Property");
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createProperty(@Request() req: RequestWithUser, @Body() body: PropertyPayload) {
|
||||
const where = {
|
||||
OR: [{ name: { contains: body.name } }, { nameEN: { contains: body.nameEN } }],
|
||||
AND: {
|
||||
registeredBranch: {
|
||||
OR: permissionCondCompany(req.user),
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.PropertyWhereInput;
|
||||
|
||||
const exists = await prisma.property.findFirst({ where });
|
||||
|
||||
if (exists) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Property with this name already exists",
|
||||
"samePropertyNameExists",
|
||||
);
|
||||
}
|
||||
|
||||
const userAffiliatedBranch = await prisma.branch.findFirst({
|
||||
include: branchRelationPermInclude(req.user),
|
||||
where: body.registeredBranchId
|
||||
? { id: body.registeredBranchId }
|
||||
: {
|
||||
user: { some: { userId: req.user.sub } },
|
||||
},
|
||||
});
|
||||
if (!userAffiliatedBranch) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
|
||||
"reqMinAffilatedBranch",
|
||||
);
|
||||
}
|
||||
|
||||
await permissionCheckCompany(req.user, userAffiliatedBranch);
|
||||
|
||||
return await prisma.property.create({
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
registeredBranchId: userAffiliatedBranch.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Put("{propertyId}")
|
||||
async updatePropertyById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() propertyId: string,
|
||||
@Body() body: PropertyPayload,
|
||||
) {
|
||||
const record = await prisma.property.findUnique({
|
||||
where: { id: propertyId },
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("Property");
|
||||
|
||||
await permissionCheckCompany(req.user, record.registeredBranch);
|
||||
|
||||
return await prisma.property.update({
|
||||
where: { id: propertyId },
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Delete("{propertyId}")
|
||||
async deletePropertyById(@Request() req: RequestWithUser, @Path() propertyId: string) {
|
||||
const record = await prisma.property.findUnique({
|
||||
where: { id: propertyId },
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("Property");
|
||||
|
||||
await permissionCheckCompany(req.user, record.registeredBranch);
|
||||
|
||||
return await prisma.property.delete({
|
||||
where: { id: propertyId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { Prisma } from "@prisma/client";
|
|||
import { notFoundError } from "../utils/error";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { createPermCondition } from "../services/permission";
|
||||
import { whereDateQuery } from "../utils/relation";
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
||||
|
|
@ -21,6 +22,8 @@ export class ReceiptController extends Controller {
|
|||
@Query() quotationId?: string,
|
||||
@Query() debitNoteId?: string,
|
||||
@Query() debitNoteOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where: Prisma.PaymentWhereInput = {
|
||||
paymentStatus: "PaymentSuccess",
|
||||
|
|
@ -33,6 +36,7 @@ export class ReceiptController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
};
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
|
|||
|
|
@ -27,21 +27,31 @@ import {
|
|||
} from "../services/permission";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import {
|
||||
deleteFile,
|
||||
deleteFolder,
|
||||
fileLocation,
|
||||
getFile,
|
||||
getPresigned,
|
||||
listFile,
|
||||
setFile,
|
||||
} from "../utils/minio";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = MANAGE_ROLES;
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
|
@ -156,6 +166,8 @@ export class ServiceController extends Controller {
|
|||
@Query() fullDetail?: boolean,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() shared?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
// NOTE: will be used to scope product within product group that is shared between branch but not company when select shared product if user is system
|
||||
const targetGroup =
|
||||
|
|
@ -171,8 +183,8 @@ export class ServiceController extends Controller {
|
|||
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.ServiceWhereInput[]>(query, [
|
||||
{ name: { contains: query } },
|
||||
{ detail: { contains: query } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ detail: { contains: query, mode: "insensitive" } },
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
AND: {
|
||||
|
|
@ -211,6 +223,7 @@ export class ServiceController extends Controller {
|
|||
: []),
|
||||
],
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.ServiceWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -473,6 +486,7 @@ export class ServiceController extends Controller {
|
|||
|
||||
return await tx.service.update({
|
||||
include: {
|
||||
productGroup: true,
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
},
|
||||
|
|
@ -523,6 +537,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;
|
||||
}
|
||||
|
||||
|
|
@ -548,6 +573,8 @@ export class ServiceController extends Controller {
|
|||
|
||||
if (record.status !== Status.CREATED) throw isUsedError("Service");
|
||||
|
||||
await deleteFolder(fileLocation.service.img(serviceId));
|
||||
|
||||
return await prisma.service.delete({
|
||||
include: {
|
||||
createdBy: true,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import prisma from "../db";
|
|||
import { RequestWithUser } from "../interfaces/user";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { isUsedError, notFoundError } from "../utils/error";
|
||||
import { whereDateQuery } from "../utils/relation";
|
||||
|
||||
type WorkCreate = {
|
||||
order: number;
|
||||
|
|
@ -45,9 +46,12 @@ export class WorkController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: [{ name: { contains: query }, serviceId: baseOnly ? null : undefined }],
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.WorkWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
|
|||
|
|
@ -26,11 +26,20 @@ import flowAccount from "../services/flowaccount";
|
|||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
|
||||
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
|
@ -101,10 +110,19 @@ export class QuotationPayment extends Controller {
|
|||
}
|
||||
|
||||
@Put("{paymentId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
|
||||
async updatePayment(
|
||||
@Path() paymentId: string,
|
||||
@Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus },
|
||||
@Body()
|
||||
body: {
|
||||
amount?: number;
|
||||
date?: Date;
|
||||
paymentStatus?: PaymentStatus;
|
||||
channel?: string | null;
|
||||
account?: string | null;
|
||||
reference?: string | null;
|
||||
},
|
||||
@Request() req: RequestWithUser,
|
||||
) {
|
||||
const record = await prisma.payment.findUnique({
|
||||
where: { id: paymentId },
|
||||
|
|
@ -134,7 +152,18 @@ export class QuotationPayment extends Controller {
|
|||
|
||||
if (!record) throw notFoundError("Payment");
|
||||
|
||||
if (record.paymentStatus === "PaymentSuccess") return record;
|
||||
if (record.paymentStatus === "PaymentSuccess") {
|
||||
const { channel, account, reference } = body;
|
||||
return await prisma.payment.update({
|
||||
where: { id: paymentId, invoice: { quotationId: record.invoice.quotationId } },
|
||||
data: {
|
||||
channel,
|
||||
account,
|
||||
reference,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const current = new Date();
|
||||
|
|
@ -164,6 +193,7 @@ export class QuotationPayment extends Controller {
|
|||
code: lastReceipt
|
||||
? `RE${year}${month}${lastReceipt.value.toString().padStart(6, "0")}`
|
||||
: undefined,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -177,55 +207,78 @@ 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({
|
||||
include: { requestData: true },
|
||||
where: { id: quotation.id },
|
||||
data: {
|
||||
quotationStatus:
|
||||
(paymentSum._sum.amount || 0) >= quotation.finalPrice
|
||||
? "PaymentSuccess"
|
||||
: "PaymentInProcess",
|
||||
requestData: await (async () => {
|
||||
if (
|
||||
body.paymentStatus === "PaymentSuccess" &&
|
||||
(paymentSum._sum.amount || 0) - payment.amount <= 0
|
||||
) {
|
||||
const lastRequest = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `REQUEST_${year}${month}`,
|
||||
},
|
||||
create: {
|
||||
key: `REQUEST_${year}${month}`,
|
||||
value: quotation.worker.length,
|
||||
},
|
||||
update: { value: { increment: quotation.worker.length } },
|
||||
});
|
||||
return {
|
||||
create: quotation.worker.flatMap((v, i) => {
|
||||
const productEmployee = quotation.productServiceList.flatMap((item) =>
|
||||
item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1
|
||||
? { productServiceId: item.id }
|
||||
: [],
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
if (quotation.quotationStatus === "PaymentInProcess") {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
title: "รายการคำขอใหม่ / New Request",
|
||||
detail: "รหัส / code : " + res.requestData.map((v) => v.code).join(", "),
|
||||
registeredBranchId: res.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return payment;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
import { isSystem } from "../utils/keycloak";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { precisionRound } from "../utils/arithmetic";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
|
|
@ -55,13 +55,14 @@ type QuotationCreate = {
|
|||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
namePrefix?: string;
|
||||
firstName: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string;
|
||||
middleNameEN?: string;
|
||||
lastName: string;
|
||||
lastNameEN: string;
|
||||
lastNameEN?: string;
|
||||
}
|
||||
)[];
|
||||
|
||||
|
|
@ -83,6 +84,8 @@ type QuotationCreate = {
|
|||
installmentNo?: number;
|
||||
workerIndex?: number[];
|
||||
}[];
|
||||
|
||||
sellerId?: string;
|
||||
};
|
||||
|
||||
type QuotationUpdate = {
|
||||
|
|
@ -112,14 +115,15 @@ type QuotationUpdate = {
|
|||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
|
||||
namePrefix?: string;
|
||||
firstName: string;
|
||||
firstName?: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string;
|
||||
middleNameEN?: string;
|
||||
lastName: string;
|
||||
lastNameEN: string;
|
||||
lastName?: string;
|
||||
lastNameEN?: string;
|
||||
}
|
||||
)[];
|
||||
|
||||
|
|
@ -140,6 +144,8 @@ type QuotationUpdate = {
|
|||
installmentNo?: number;
|
||||
workerIndex?: number[];
|
||||
}[];
|
||||
|
||||
sellerId?: string;
|
||||
};
|
||||
|
||||
const VAT_DEFAULT = config.vat;
|
||||
|
|
@ -148,15 +154,16 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
|
|
@ -168,13 +175,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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -194,28 +209,31 @@ export class QuotationController extends Controller {
|
|||
@Query() urgentFirst?: boolean,
|
||||
@Query() includeRegisteredBranch?: boolean,
|
||||
@Query() hasCancel?: boolean,
|
||||
@Query() cancelIncludeDebitNote?: boolean,
|
||||
@Query() forDebitNote?: boolean,
|
||||
@Query() code?: string,
|
||||
@Query() query = "",
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Query() sellerId?: string,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ workName: { contains: query } },
|
||||
{ workName: { contains: query, mode: "insensitive" } },
|
||||
{
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
isDebitNote: false,
|
||||
isDebitNote: hasCancel && cancelIncludeDebitNote ? undefined : false,
|
||||
code,
|
||||
payCondition,
|
||||
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
|
||||
|
|
@ -244,6 +262,8 @@ export class QuotationController extends Controller {
|
|||
},
|
||||
}
|
||||
: undefined,
|
||||
...whereDateQuery(startDate, endDate),
|
||||
sellerId: sellerId,
|
||||
} satisfies Prisma.QuotationWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -401,7 +421,7 @@ export class QuotationController extends Controller {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
|
||||
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
|
||||
const ids = {
|
||||
employee: body.worker.filter((v) => typeof v === "string"),
|
||||
|
|
@ -453,7 +473,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: {
|
||||
|
|
@ -507,16 +527,15 @@ export class QuotationController extends Controller {
|
|||
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
|
||||
|
||||
const originalPrice = body.agentPrice ? p.agentPrice : p.price;
|
||||
const finalPriceWithVat = precisionRound(
|
||||
const finalPrice = precisionRound(
|
||||
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
|
||||
);
|
||||
|
||||
const price = finalPriceWithVat;
|
||||
const pricePerUnit = price / (1 + VAT_DEFAULT);
|
||||
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
|
||||
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
|
||||
? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
|
||||
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
|
||||
(1 + VAT_DEFAULT)) *
|
||||
VAT_DEFAULT
|
||||
: 0;
|
||||
|
||||
return {
|
||||
order: i + 1,
|
||||
productId: v.productId,
|
||||
|
|
@ -537,13 +556,13 @@ export class QuotationController extends Controller {
|
|||
|
||||
const price = list.reduce(
|
||||
(a, c) => {
|
||||
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
|
||||
const vat = c.vat ? VAT_DEFAULT : 0;
|
||||
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
|
||||
a.vat = precisionRound(a.vat + c.vat);
|
||||
a.vatExcluded =
|
||||
c.vat === 0
|
||||
? precisionRound(a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)))
|
||||
: a.vatExcluded;
|
||||
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
|
||||
a.finalPrice = precisionRound(
|
||||
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
|
||||
);
|
||||
|
|
@ -638,10 +657,28 @@ export class QuotationController extends Controller {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
title: "ใบเสนอราคาใหม่ / New Quotation",
|
||||
detail: "รหัส / code : " + ret.code,
|
||||
registeredBranchId: ret.registeredBranchId,
|
||||
groupReceiver: {
|
||||
create: [
|
||||
{ name: "sale" },
|
||||
{ name: "head_of_sale" },
|
||||
{ name: "accountant" },
|
||||
{ name: "branch_accountant" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Put("{quotationId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
|
||||
async editQuotation(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() quotationId: string,
|
||||
|
|
@ -659,6 +696,7 @@ export class QuotationController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
productServiceList: true,
|
||||
},
|
||||
where: { id: quotationId, isDebitNote: false },
|
||||
});
|
||||
|
|
@ -776,14 +814,14 @@ export class QuotationController extends Controller {
|
|||
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
|
||||
|
||||
const originalPrice = record.agentPrice ? p.agentPrice : p.price;
|
||||
const finalPriceWithVat = precisionRound(
|
||||
const finalPrice = precisionRound(
|
||||
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
|
||||
);
|
||||
|
||||
const price = finalPriceWithVat;
|
||||
const pricePerUnit = price / (1 + VAT_DEFAULT);
|
||||
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
|
||||
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
|
||||
? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
|
||||
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
|
||||
(1 + VAT_DEFAULT)) *
|
||||
VAT_DEFAULT
|
||||
: 0;
|
||||
|
||||
return {
|
||||
|
|
@ -806,15 +844,13 @@ export class QuotationController extends Controller {
|
|||
|
||||
const price = list?.reduce(
|
||||
(a, c) => {
|
||||
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
|
||||
const vat = c.vat ? VAT_DEFAULT : 0;
|
||||
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
|
||||
a.vat = precisionRound(a.vat + c.vat);
|
||||
a.vatExcluded =
|
||||
c.vat === 0
|
||||
? precisionRound(
|
||||
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
|
||||
)
|
||||
: a.vatExcluded;
|
||||
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
|
||||
a.finalPrice = precisionRound(
|
||||
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
|
||||
);
|
||||
|
|
@ -831,6 +867,18 @@ export class QuotationController extends Controller {
|
|||
},
|
||||
);
|
||||
|
||||
const changed = list?.some((lhs) => {
|
||||
const found = record.productServiceList.find((rhs) => {
|
||||
return (
|
||||
lhs.serviceId === rhs.serviceId &&
|
||||
lhs.workId === rhs.workId &&
|
||||
lhs.productId === rhs.productId &&
|
||||
lhs.amount === rhs.amount &&
|
||||
precisionRound(lhs.pricePerUnit, 6) === precisionRound(rhs.pricePerUnit, 6)
|
||||
);
|
||||
});
|
||||
return !found;
|
||||
});
|
||||
await Promise.all([
|
||||
tx.service.updateMany({
|
||||
where: { id: { in: ids.service }, status: Status.CREATED },
|
||||
|
|
@ -840,8 +888,32 @@ export class QuotationController extends Controller {
|
|||
where: { id: { in: ids.product }, status: Status.CREATED },
|
||||
data: { status: Status.ACTIVE },
|
||||
}),
|
||||
changed &&
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "ใบเสนอราคามีการเปลี่ยนแปลง / Quotation Detail Changes",
|
||||
detail:
|
||||
"รหัส / code : " + record.code + " มีการเปลี่ยนแปลงของสินค้า / Product Updated",
|
||||
registeredBranchId: record.registeredBranchId,
|
||||
groupReceiver: { create: [{ name: "sale" }, { name: "head_of_sale" }] },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (customerBranch) {
|
||||
await tx.customerBranch.update({
|
||||
where: { id: customerBranch.id },
|
||||
data: {
|
||||
customer: {
|
||||
update: {
|
||||
status: Status.ACTIVE,
|
||||
},
|
||||
},
|
||||
status: Status.ACTIVE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.quotation.update({
|
||||
include: {
|
||||
productServiceList: {
|
||||
|
|
@ -963,6 +1035,7 @@ export class QuotationActionController extends Controller {
|
|||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
namePrefix?: string;
|
||||
firstName: string;
|
||||
firstNameEN: string;
|
||||
|
|
@ -985,6 +1058,7 @@ export class QuotationActionController extends Controller {
|
|||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
namePrefix?: string;
|
||||
firstName: string;
|
||||
firstNameEN: string;
|
||||
|
|
@ -1124,41 +1198,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" } },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ import {
|
|||
createPermCheck,
|
||||
createPermCondition,
|
||||
} from "../services/permission";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { getGroupUser } from "../services/keycloak";
|
||||
|
||||
// User in company can edit.
|
||||
const permissionCheck = createPermCheck((_) => true);
|
||||
|
|
@ -45,11 +48,7 @@ export class RequestDataController extends Controller {
|
|||
async getRequestDataStats(@Request() req: RequestWithUser) {
|
||||
const where = {
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
customer: {
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
},
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
} satisfies Prisma.RequestDataWhereInput;
|
||||
|
||||
|
|
@ -82,45 +81,53 @@ export class RequestDataController extends Controller {
|
|||
@Query() requestDataStatus?: RequestDataStatus,
|
||||
@Query() quotationId?: string,
|
||||
@Query() code?: string,
|
||||
@Query() incomplete?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ quotation: { code: { contains: query, mode: "insensitive" } } },
|
||||
{ quotation: { workName: { contains: query } } },
|
||||
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
|
||||
{
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ registerName: { contains: query } },
|
||||
{ registerNameEN: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ registerName: { contains: query, mode: "insensitive" } },
|
||||
{ registerNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
employee: {
|
||||
OR: [
|
||||
{
|
||||
employeePassport: {
|
||||
some: { number: { contains: query } },
|
||||
some: { number: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
code,
|
||||
requestDataStatus,
|
||||
requestDataStatus: incomplete
|
||||
? {
|
||||
notIn: [RequestDataStatus.Completed, RequestDataStatus.Canceled],
|
||||
}
|
||||
: requestDataStatus,
|
||||
requestWork: responsibleOnly
|
||||
? {
|
||||
some: {
|
||||
|
|
@ -129,9 +136,24 @@ export class RequestDataController extends Controller {
|
|||
workflow: {
|
||||
step: {
|
||||
some: {
|
||||
responsiblePerson: {
|
||||
some: { userId: req.user.sub },
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
responsiblePerson: {
|
||||
some: { userId: req.user.sub },
|
||||
},
|
||||
},
|
||||
{
|
||||
responsibleGroup: {
|
||||
some: {
|
||||
group: {
|
||||
in: await getGroupUser(req.user.sub).then((r) =>
|
||||
r.map(({ name }: { name: string }) => name),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -144,6 +166,7 @@ export class RequestDataController extends Controller {
|
|||
id: quotationId,
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.RequestDataWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -166,6 +189,7 @@ export class RequestDataController extends Controller {
|
|||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
responsibleGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -184,6 +208,20 @@ export class RequestDataController extends Controller {
|
|||
employeePassport: {
|
||||
orderBy: { expireDate: "desc" },
|
||||
},
|
||||
customerBranch: {
|
||||
include: {
|
||||
province: {
|
||||
include: {
|
||||
employmentOffice: true,
|
||||
},
|
||||
},
|
||||
district: {
|
||||
include: {
|
||||
employmentOffice: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -194,7 +232,24 @@ export class RequestDataController extends Controller {
|
|||
prisma.requestData.count({ where }),
|
||||
]);
|
||||
|
||||
return { result, page, pageSize, total };
|
||||
const dataRequestData = result.map((item) => {
|
||||
const employee = item.employee;
|
||||
const dataOffice =
|
||||
employee.customerBranch.district?.employmentOffice.at(0) ??
|
||||
employee.customerBranch.province?.employmentOffice.at(0);
|
||||
|
||||
return {
|
||||
...item,
|
||||
dataOffice,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
result: dataRequestData,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("{requestDataId}")
|
||||
|
|
@ -206,6 +261,9 @@ export class RequestDataController extends Controller {
|
|||
quotation: {
|
||||
include: {
|
||||
customerBranch: { include: { customer: true } },
|
||||
debitNoteQuotation: {
|
||||
select: { code: true },
|
||||
},
|
||||
invoice: {
|
||||
include: {
|
||||
installments: true,
|
||||
|
|
@ -229,14 +287,157 @@ export class RequestDataController extends Controller {
|
|||
|
||||
return record;
|
||||
}
|
||||
|
||||
@Post("update-messenger")
|
||||
@Security("keycloak")
|
||||
async updateRequestData(
|
||||
@Request() req: RequestWithUser,
|
||||
@Body()
|
||||
body: {
|
||||
defaultMessengerId: string;
|
||||
requestDataId: string[];
|
||||
},
|
||||
) {
|
||||
if (body.requestDataId.length === 0) return;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const record = await tx.requestData.updateManyAndReturn({
|
||||
where: {
|
||||
id: { in: body.requestDataId },
|
||||
quotation: {
|
||||
registeredBranch: {
|
||||
OR: permissionCond(req.user),
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
defaultMessengerId: body.defaultMessengerId,
|
||||
},
|
||||
});
|
||||
|
||||
if (record.length <= 0) throw notFoundError("Request Data");
|
||||
|
||||
await tx.requestWorkStepStatus.updateMany({
|
||||
where: {
|
||||
workStatus: {
|
||||
in: [
|
||||
RequestWorkStatus.Pending,
|
||||
RequestWorkStatus.Waiting,
|
||||
RequestWorkStatus.InProgress,
|
||||
],
|
||||
},
|
||||
requestWork: {
|
||||
requestDataId: { in: body.requestDataId },
|
||||
},
|
||||
},
|
||||
data: { responsibleUserId: body.defaultMessengerId },
|
||||
});
|
||||
|
||||
return record[0];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Route("/api/v1/request-data/{requestDataId}")
|
||||
@Tags("Request List")
|
||||
export class RequestDataActionController extends Controller {
|
||||
async #getLineToken() {
|
||||
if (!process.env.LINE_MESSAGING_API_TOKEN) {
|
||||
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
|
||||
}
|
||||
|
||||
return process.env.LINE_MESSAGING_API_TOKEN;
|
||||
}
|
||||
|
||||
@Post("reject-request-cancel")
|
||||
@Security("keycloak")
|
||||
async rejectRequestCancel(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() requestDataId: string,
|
||||
@Body()
|
||||
body: {
|
||||
reason?: string;
|
||||
},
|
||||
) {
|
||||
const result = await prisma.requestData.updateManyAndReturn({
|
||||
where: {
|
||||
id: requestDataId,
|
||||
quotation: {
|
||||
registeredBranch: {
|
||||
OR: permissionCond(req.user),
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
rejectRequestCancel: true,
|
||||
rejectRequestCancelReason: body.reason || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.length <= 0) throw notFoundError("Request Data");
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
@Post("request-work/{requestWorkId}/reject-request-cancel")
|
||||
@Security("keycloak")
|
||||
async rejectWorkRequestCancel(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() requestWorkId: string,
|
||||
@Body()
|
||||
body: {
|
||||
reason?: string;
|
||||
},
|
||||
) {
|
||||
const result = await prisma.requestWork.updateManyAndReturn({
|
||||
where: {
|
||||
id: requestWorkId,
|
||||
request: {
|
||||
quotation: {
|
||||
registeredBranch: {
|
||||
OR: permissionCond(req.user),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
rejectRequestCancel: true,
|
||||
rejectRequestCancelReason: body.reason || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.length <= 0) throw notFoundError("Request Data");
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
@Post("cancel")
|
||||
@Security("keycloak")
|
||||
async cancelRequestData(@Path() requestDataId: string) {
|
||||
async cancelRequestData(@Request() req: RequestWithUser, @Path() requestDataId: string) {
|
||||
const result = await prisma.requestData.findFirst({
|
||||
where: {
|
||||
id: requestDataId,
|
||||
quotation: {
|
||||
registeredBranch: {
|
||||
OR: permissionCond(req.user),
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
quotation: {
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: { include: { branch: { where: { userId: { not: null } } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) throw notFoundError("Request Data");
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const workStepCondition = {
|
||||
requestWork: { requestDataId },
|
||||
|
|
@ -265,23 +466,405 @@ 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 },
|
||||
}),
|
||||
tx.taskOrder.updateMany({
|
||||
where: {
|
||||
taskList: {
|
||||
every: { taskStatus: TaskStatus.Canceled },
|
||||
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Canceled",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
tx.taskOrder
|
||||
.updateManyAndReturn({
|
||||
where: {
|
||||
taskList: {
|
||||
every: { taskStatus: TaskStatus.Canceled },
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { taskOrderStatus: TaskStatus.Canceled },
|
||||
}),
|
||||
data: { taskOrderStatus: TaskStatus.Canceled },
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Canceled",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
const token = await this.#getLineToken();
|
||||
if (!token) return;
|
||||
|
||||
const textHead = "JWS ALERT:";
|
||||
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
|
||||
const textAlert2 = "ได้ดำเนินการยกเลิกเรียบร้อยแล้ว";
|
||||
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
|
||||
let finalTextWork = "";
|
||||
let textData = "";
|
||||
|
||||
let dataCustomerId: string[] = [];
|
||||
let dataUserId: string[] = [];
|
||||
|
||||
result.quotation.customerBranch.customer.branch.forEach((item) => {
|
||||
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
||||
dataCustomerId.push(item.id);
|
||||
dataUserId.push(item.userId);
|
||||
}
|
||||
});
|
||||
finalTextWork = `เลขที่ใบเสนอราคา: ${result.code} ${result.quotation.workName}`;
|
||||
|
||||
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
|
||||
|
||||
const data = {
|
||||
to: dataUserId,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch("https://api.line.me/v2/bot/message/multicast", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Put("request-work/step-status/{step}")
|
||||
@Security("keycloak")
|
||||
async updateRequestWorkDataStepStatus(
|
||||
@Path() requestDataId: string,
|
||||
@Path() step: number,
|
||||
@Body()
|
||||
payload: {
|
||||
workStatus?: RequestWorkStatus;
|
||||
requestWorkId: string;
|
||||
attributes?: Record<string, any>;
|
||||
customerDuty?: boolean | null;
|
||||
customerDutyCost?: number | null;
|
||||
companyDuty?: boolean | null;
|
||||
companyDutyCost?: number | null;
|
||||
individualDuty?: boolean | null;
|
||||
individualDutyCost?: number | null;
|
||||
responsibleUserLocal?: boolean | null;
|
||||
responsibleUserId?: string | null;
|
||||
}[],
|
||||
) {
|
||||
payload.forEach((item) => {
|
||||
if (!item.responsibleUserId) item.responsibleUserId = undefined;
|
||||
});
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const workStepCondition = await tx.requestData.findFirst({
|
||||
where: {
|
||||
id: requestDataId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!workStepCondition) {
|
||||
throw new Error("RequestWork not found requestDataId");
|
||||
}
|
||||
|
||||
const data = await Promise.all(
|
||||
payload.map(async (item) => {
|
||||
return await tx.requestWorkStepStatus.upsert({
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
step_requestWorkId: {
|
||||
step: step,
|
||||
requestWorkId: item.requestWorkId,
|
||||
},
|
||||
requestWork: {
|
||||
request: { id: requestDataId },
|
||||
},
|
||||
},
|
||||
create: {
|
||||
...item,
|
||||
step: step,
|
||||
requestWorkId: item.requestWorkId,
|
||||
},
|
||||
update: item,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
data.some((item) => {
|
||||
return (
|
||||
item.workStatus === "Ready" && item.requestWork.request.requestDataStatus === "Pending"
|
||||
);
|
||||
})
|
||||
) {
|
||||
await tx.requestData.updateMany({
|
||||
where: {
|
||||
id: requestDataId,
|
||||
requestDataStatus: "Pending",
|
||||
},
|
||||
data: { requestDataStatus: "Ready" },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
data.some((item) => {
|
||||
return (
|
||||
item.workStatus === "InProgress" ||
|
||||
item.workStatus === "Waiting" ||
|
||||
item.workStatus === "Validate" ||
|
||||
item.workStatus === "Completed" ||
|
||||
item.workStatus === "Ended"
|
||||
);
|
||||
})
|
||||
) {
|
||||
await tx.requestData.update({
|
||||
where: {
|
||||
id: requestDataId,
|
||||
},
|
||||
data: { requestDataStatus: "InProgress" },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
data.some((item) => {
|
||||
return item.workStatus === "Canceled";
|
||||
})
|
||||
) {
|
||||
const dataId = data.map((itemId) => itemId.requestWork.id);
|
||||
await tx.task.updateMany({
|
||||
where: {
|
||||
taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] },
|
||||
requestWorkStep: {
|
||||
step: step,
|
||||
requestWorkId: { in: dataId },
|
||||
workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] },
|
||||
},
|
||||
},
|
||||
data: { taskStatus: TaskStatus.Canceled },
|
||||
});
|
||||
await Promise.all([
|
||||
tx.quotation
|
||||
.updateManyAndReturn({
|
||||
where: {
|
||||
quotationStatus: { not: QuotationStatus.Canceled },
|
||||
requestData: {
|
||||
every: { requestDataStatus: RequestDataStatus.Canceled },
|
||||
},
|
||||
},
|
||||
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Canceled",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
tx.taskOrder.updateMany({
|
||||
where: {
|
||||
taskList: {
|
||||
every: { taskStatus: TaskStatus.Canceled },
|
||||
},
|
||||
},
|
||||
data: { taskOrderStatus: TaskStatus.Canceled },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
const requestList = await tx.requestData.findMany({
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
productService: {
|
||||
include: {
|
||||
product: true,
|
||||
service: true,
|
||||
work: {
|
||||
include: { productOnWork: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
stepStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
requestWork: {
|
||||
some: {
|
||||
requestDataId: requestDataId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const completed: string[] = [];
|
||||
|
||||
requestList.forEach((item) => {
|
||||
const completeCheck = item.requestWork.every((work) => {
|
||||
const stepCount =
|
||||
work.productService.work?.productOnWork.find(
|
||||
(v) => v.productId === work.productService.productId,
|
||||
)?.stepCount || 0;
|
||||
|
||||
const completeCount = work.stepStatus.filter(
|
||||
(v) =>
|
||||
v.workStatus === RequestWorkStatus.Completed ||
|
||||
v.workStatus === RequestWorkStatus.Ended ||
|
||||
v.workStatus === RequestWorkStatus.Canceled,
|
||||
).length;
|
||||
|
||||
// NOTE: step found then check if complete count equals step count
|
||||
if (stepCount === completeCount && completeCount > 0) return true;
|
||||
// NOTE: likely no step found and completed at least one
|
||||
if (stepCount === 0 && completeCount > 0) return true;
|
||||
});
|
||||
|
||||
if (completeCheck) completed.push(item.id);
|
||||
});
|
||||
|
||||
await tx.requestData.updateMany({
|
||||
where: { id: { in: completed } },
|
||||
data: { requestDataStatus: RequestDataStatus.Completed },
|
||||
});
|
||||
await tx.quotation
|
||||
.updateManyAndReturn({
|
||||
where: {
|
||||
quotationStatus: {
|
||||
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
||||
},
|
||||
requestData: {
|
||||
every: {
|
||||
requestDataStatus: {
|
||||
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: {
|
||||
include: {
|
||||
branch: {
|
||||
where: { userId: { not: null } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Completed",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const token = await this.#getLineToken();
|
||||
if (!token) return;
|
||||
|
||||
const textHead = "JWS ALERT:";
|
||||
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
|
||||
const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว";
|
||||
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
|
||||
let finalTextWork = "";
|
||||
let textData = "";
|
||||
|
||||
let dataCustomerId: string[] = [];
|
||||
let textWorkList: string[] = [];
|
||||
let dataUserId: string[] = [];
|
||||
|
||||
if (res) {
|
||||
res.forEach((data, index) => {
|
||||
data.customerBranch.customer.branch.forEach((item) => {
|
||||
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
||||
dataCustomerId.push(item.id);
|
||||
dataUserId.push(item.userId);
|
||||
}
|
||||
});
|
||||
textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`);
|
||||
});
|
||||
|
||||
finalTextWork = textWorkList.join("\n");
|
||||
}
|
||||
|
||||
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
|
||||
|
||||
const data = {
|
||||
to: dataUserId,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch("https://api.line.me/v2/bot/message/multicast", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
});
|
||||
// dataRecord.push(record);
|
||||
return data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -289,6 +872,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(
|
||||
|
|
@ -393,6 +984,7 @@ export class RequestListController extends Controller {
|
|||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
responsibleGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -453,6 +1045,7 @@ export class RequestListController extends Controller {
|
|||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
responsibleGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -561,6 +1154,21 @@ export class RequestListController extends Controller {
|
|||
update: payload,
|
||||
});
|
||||
|
||||
if (record.responsibleUserId === null) {
|
||||
await tx.requestWorkStepStatus.update({
|
||||
where: {
|
||||
step_requestWorkId: {
|
||||
step: step,
|
||||
requestWorkId,
|
||||
},
|
||||
responsibleUserId: null,
|
||||
},
|
||||
data: {
|
||||
responsibleUserId: record.requestWork?.request.defaultMessengerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
switch (payload.workStatus) {
|
||||
case "Ready":
|
||||
if (record.requestWork.request.requestDataStatus === "Pending") {
|
||||
|
|
@ -598,14 +1206,31 @@ 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 Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Canceled",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
tx.taskOrder.updateMany({
|
||||
where: {
|
||||
taskList: {
|
||||
|
|
@ -673,19 +1298,109 @@ export class RequestListController extends Controller {
|
|||
where: { id: { in: completed } },
|
||||
data: { requestDataStatus: RequestDataStatus.Completed },
|
||||
});
|
||||
await tx.quotation.updateMany({
|
||||
where: {
|
||||
quotationStatus: {
|
||||
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
||||
await tx.quotation
|
||||
.updateManyAndReturn({
|
||||
where: {
|
||||
quotationStatus: {
|
||||
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
||||
},
|
||||
AND: [
|
||||
{
|
||||
requestData: {
|
||||
every: {
|
||||
requestDataStatus: {
|
||||
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
requestData: {
|
||||
some: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
requestData: {
|
||||
every: {
|
||||
requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed] },
|
||||
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: {
|
||||
include: {
|
||||
branch: {
|
||||
where: { userId: { not: null } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
||||
});
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Completed",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
const token = await this.#getLineToken();
|
||||
if (!token) return;
|
||||
|
||||
const textHead = "JWS ALERT:";
|
||||
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
|
||||
const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว";
|
||||
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
|
||||
let finalTextWork = "";
|
||||
let textData = "";
|
||||
|
||||
let dataCustomerId: string[] = [];
|
||||
let textWorkList: string[] = [];
|
||||
let dataUserId: string[] = [];
|
||||
|
||||
if (res) {
|
||||
res.forEach((data, index) => {
|
||||
data.customerBranch.customer.branch.forEach((item) => {
|
||||
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
||||
dataCustomerId.push(item.id);
|
||||
dataUserId.push(item.userId);
|
||||
}
|
||||
});
|
||||
textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`);
|
||||
});
|
||||
|
||||
finalTextWork = textWorkList.join("\n");
|
||||
}
|
||||
|
||||
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
|
||||
|
||||
const data = {
|
||||
to: dataUserId,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch("https://api.line.me/v2/bot/message/multicast", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
});
|
||||
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,13 +42,23 @@ import {
|
|||
listFile,
|
||||
setFile,
|
||||
} from "../utils/minio";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
|
||||
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"];
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"executive",
|
||||
"accountant",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
"data_entry",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
|
|
@ -60,11 +70,14 @@ const permissionCheckCompany = createPermCheck((_) => true);
|
|||
@Tags("Task Order")
|
||||
export class TaskController extends Controller {
|
||||
@Get("stats")
|
||||
async getTaskOrderStats() {
|
||||
@Security("keycloak")
|
||||
async getTaskOrderStats(@Request() req: RequestWithUser) {
|
||||
const task = await prisma.taskOrder.groupBy({
|
||||
where: { registeredBranch: { OR: permissionCondCompany(req.user) } },
|
||||
by: ["taskOrderStatus"],
|
||||
_count: true,
|
||||
});
|
||||
|
||||
return task.reduce<Record<TaskOrderStatus, number>>(
|
||||
(a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }),
|
||||
{
|
||||
|
|
@ -86,6 +99,8 @@ export class TaskController extends Controller {
|
|||
@Query() pageSize = 30,
|
||||
@Query() assignedByUserId?: string,
|
||||
@Query() taskOrderStatus?: TaskOrderStatus,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return this.getTaskOrderListByCriteria(
|
||||
req,
|
||||
|
|
@ -94,6 +109,8 @@ export class TaskController extends Controller {
|
|||
pageSize,
|
||||
assignedByUserId,
|
||||
taskOrderStatus,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +123,8 @@ export class TaskController extends Controller {
|
|||
@Query() pageSize = 30,
|
||||
@Query() assignedUserId?: string,
|
||||
@Query() taskOrderStatus?: TaskOrderStatus,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Body() body?: { code?: string[] },
|
||||
) {
|
||||
const where = {
|
||||
|
|
@ -121,10 +140,11 @@ export class TaskController extends Controller {
|
|||
code: body?.code ? { in: body.code } : undefined,
|
||||
OR: queryOrNot(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ taskName: { contains: query } },
|
||||
{ contactName: { contains: query } },
|
||||
{ contactTel: { contains: query } },
|
||||
{ taskName: { contains: query, mode: "insensitive" } },
|
||||
{ contactName: { contains: query, mode: "insensitive" } },
|
||||
{ contactTel: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.TaskOrderWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -193,6 +213,7 @@ export class TaskController extends Controller {
|
|||
step: {
|
||||
include: {
|
||||
value: true,
|
||||
responsibleGroup: true,
|
||||
responsiblePerson: {
|
||||
include: { user: true },
|
||||
},
|
||||
|
|
@ -244,6 +265,12 @@ export class TaskController extends Controller {
|
|||
taskProduct?: { productId: string; discount?: number }[];
|
||||
},
|
||||
) {
|
||||
if (body.taskList.length < 1 || !body.registeredBranchId)
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Your created invalid task order",
|
||||
"taskOrderInvalid",
|
||||
);
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
|
|
@ -293,8 +320,8 @@ export class TaskController extends Controller {
|
|||
if (updated.count !== taskList.length) {
|
||||
throw new HttpError(
|
||||
HttpStatus.PRECONDITION_FAILED,
|
||||
"All request work to issue task order must be in ready state.",
|
||||
"requestWorkMustReady",
|
||||
"all request work to issue task order must be in ready state.",
|
||||
"requestworkmustready",
|
||||
);
|
||||
}
|
||||
await tx.institution.updateMany({
|
||||
|
|
@ -317,49 +344,51 @@ export class TaskController extends Controller {
|
|||
where: { OR: taskList },
|
||||
});
|
||||
|
||||
return await tx.taskOrder.create({
|
||||
include: {
|
||||
taskList: {
|
||||
include: {
|
||||
requestWorkStep: {
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: {
|
||||
include: {
|
||||
employee: true,
|
||||
quotation: {
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
productService: {
|
||||
include: {
|
||||
service: {
|
||||
include: {
|
||||
workflow: {
|
||||
include: {
|
||||
step: {
|
||||
include: {
|
||||
value: true,
|
||||
responsiblePerson: {
|
||||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
},
|
||||
return await tx.taskOrder
|
||||
.create({
|
||||
include: {
|
||||
taskList: {
|
||||
include: {
|
||||
requestWorkStep: {
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: {
|
||||
include: {
|
||||
employee: true,
|
||||
quotation: {
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
work: true,
|
||||
product: true,
|
||||
},
|
||||
productService: {
|
||||
include: {
|
||||
service: {
|
||||
include: {
|
||||
workflow: {
|
||||
include: {
|
||||
step: {
|
||||
include: {
|
||||
value: true,
|
||||
responsiblePerson: {
|
||||
include: { user: true },
|
||||
},
|
||||
responsibleInstitution: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
work: true,
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -367,20 +396,30 @@ export class TaskController extends Controller {
|
|||
},
|
||||
},
|
||||
},
|
||||
institution: true,
|
||||
createdBy: true,
|
||||
},
|
||||
institution: true,
|
||||
createdBy: true,
|
||||
},
|
||||
data: {
|
||||
...rest,
|
||||
code,
|
||||
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
|
||||
registeredBranchId: userAffiliatedBranch.id,
|
||||
createdByUserId: req.user.sub,
|
||||
taskList: { create: taskList },
|
||||
taskProduct: { create: taskProduct },
|
||||
},
|
||||
});
|
||||
data: {
|
||||
...rest,
|
||||
code,
|
||||
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
|
||||
registeredBranchId: userAffiliatedBranch.id,
|
||||
createdByUserId: req.user.sub,
|
||||
taskList: { create: taskList },
|
||||
taskProduct: { create: taskProduct },
|
||||
},
|
||||
})
|
||||
.then(async (v) => {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
title: "ใบสั่งงานใหม่ / New Task Order",
|
||||
detail: "รหัส / code : " + v.code,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
});
|
||||
return v;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -433,88 +472,103 @@ 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,
|
||||
registeredBranchId: record.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Delete("{taskOrderId}")
|
||||
|
|
@ -560,6 +614,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(
|
||||
|
|
@ -577,7 +639,28 @@ export class TaskActionController extends Controller {
|
|||
return await prisma.$transaction(async (tx) => {
|
||||
const promises = body.map(async (v) => {
|
||||
const record = await tx.task.findFirst({
|
||||
include: { requestWorkStep: true },
|
||||
include: {
|
||||
requestWorkStep: {
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: {
|
||||
include: {
|
||||
quotation: true,
|
||||
employee: true,
|
||||
},
|
||||
},
|
||||
productService: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
taskOrder: true,
|
||||
},
|
||||
where: {
|
||||
step: v.step,
|
||||
requestWorkId: v.requestWorkId,
|
||||
|
|
@ -595,6 +678,25 @@ export class TaskActionController extends Controller {
|
|||
data: { userTaskStatus: UserTaskStatus.Restart },
|
||||
});
|
||||
}
|
||||
|
||||
if (v.taskStatus === TaskStatus.Failed) {
|
||||
const taskCode = record.taskOrder.code;
|
||||
const taskName = record.taskOrder.taskName;
|
||||
const productCode = record.requestWorkStep.requestWork.productService.product.code;
|
||||
const productName = record.requestWorkStep.requestWork.productService.product.name;
|
||||
const employeeName = `${record.requestWorkStep.requestWork.request.employee.namePrefix}.${record.requestWorkStep.requestWork.request.employee.firstNameEN} ${record.requestWorkStep.requestWork.request.employee.lastNameEN}`;
|
||||
|
||||
await tx.notification.create({
|
||||
data: {
|
||||
title: "ใบรายการคำขอที่จัดการเกิดปัญหา / Task Failed",
|
||||
detail: `ใบรายการคำขอรหัส ${taskCode}: ${taskName} รหัสสินค้า ${productCode}: ${productName} ของลูกจ้าง ${employeeName} เกิดข้อผิดพลาด`,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
receiverId: record.requestWorkStep.requestWork.request.quotation.createdByUserId,
|
||||
registeredBranchId: record.taskOrder.registeredBranchId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.task.update({
|
||||
where: { id: record.id },
|
||||
data: {
|
||||
|
|
@ -651,6 +753,15 @@ export class TaskActionController extends Controller {
|
|||
},
|
||||
data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() },
|
||||
}),
|
||||
prisma.notification.create({
|
||||
data: {
|
||||
title: "มีการส่งงาน / Task Submitted",
|
||||
detail: "รหัสใบสั่งงาน / Order : " + record.code,
|
||||
receiverId: record.createdByUserId,
|
||||
registeredBranchId: record.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -662,22 +773,53 @@ export class TaskActionController extends Controller {
|
|||
if (!record) throw notFoundError("Task Order");
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: "TASK_RI",
|
||||
},
|
||||
create: {
|
||||
key: "TASK_RI",
|
||||
value: 1,
|
||||
},
|
||||
update: {
|
||||
value: { increment: 1 },
|
||||
},
|
||||
});
|
||||
const current = new Date();
|
||||
const year = `${current.getFullYear()}`.padStart(2, "0");
|
||||
const month = `${current.getMonth() + 1}`.padStart(2, "0");
|
||||
|
||||
const code = `RI${year}${month}${last.value.toString().padStart(6, "0")}`;
|
||||
|
||||
await Promise.all([
|
||||
tx.taskOrder.update({
|
||||
where: { id: taskOrderId },
|
||||
data: {
|
||||
urgent: false,
|
||||
taskOrderStatus: TaskOrderStatus.Complete,
|
||||
userTask: {
|
||||
updateMany: {
|
||||
where: { taskOrderId },
|
||||
data: {
|
||||
userTaskStatus: UserTaskStatus.Submit,
|
||||
tx.taskOrder
|
||||
.update({
|
||||
where: { id: taskOrderId },
|
||||
data: {
|
||||
urgent: false,
|
||||
taskOrderStatus: TaskOrderStatus.Complete,
|
||||
codeProductReceived: code,
|
||||
userTask: {
|
||||
updateMany: {
|
||||
where: { taskOrderId },
|
||||
data: {
|
||||
userTaskStatus: UserTaskStatus.Submit,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(async (record) => {
|
||||
await tx.notification.create({
|
||||
data: {
|
||||
title: "ใบงานเสร็จสิ้น / Task Complete",
|
||||
detail: "รหัสใบสั่งงาน / Order : " + record.code,
|
||||
receiverId: record.createdByUserId,
|
||||
registeredBranchId: record.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
});
|
||||
}),
|
||||
tx.requestWorkStepStatus.updateMany({
|
||||
where: {
|
||||
task: {
|
||||
|
|
@ -781,23 +923,138 @@ export class TaskActionController extends Controller {
|
|||
if (completeCheck) completed.push(item.id);
|
||||
});
|
||||
|
||||
await tx.requestData.updateMany({
|
||||
where: { id: { in: completed } },
|
||||
data: { requestDataStatus: RequestDataStatus.Completed },
|
||||
});
|
||||
await tx.quotation.updateMany({
|
||||
where: {
|
||||
quotationStatus: {
|
||||
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
||||
},
|
||||
requestData: {
|
||||
every: {
|
||||
requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed] },
|
||||
await tx.requestData
|
||||
.updateManyAndReturn({
|
||||
where: { id: { in: completed } },
|
||||
include: {
|
||||
quotation: {
|
||||
select: {
|
||||
registeredBranchId: true,
|
||||
createdByUserId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
||||
});
|
||||
data: { requestDataStatus: RequestDataStatus.Completed },
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Completed",
|
||||
receiverId: v.quotation.createdByUserId,
|
||||
registeredBranchId: v.quotation.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
await tx.quotation
|
||||
.updateManyAndReturn({
|
||||
where: {
|
||||
quotationStatus: {
|
||||
notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete],
|
||||
},
|
||||
AND: [
|
||||
{
|
||||
requestData: {
|
||||
every: {
|
||||
requestDataStatus: {
|
||||
in: [RequestDataStatus.Canceled, RequestDataStatus.Completed],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
requestData: {
|
||||
some: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: {
|
||||
include: {
|
||||
branch: {
|
||||
where: { userId: { not: null } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
await Promise.all(
|
||||
res.map((v) =>
|
||||
tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
|
||||
detail: "รหัส / code : " + v.code + " Completed",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const token = await this.#getLineToken();
|
||||
|
||||
if (!token) return;
|
||||
|
||||
const textHead = "JWS ALERT:";
|
||||
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
|
||||
const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว";
|
||||
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
|
||||
let finalTextWork = "";
|
||||
let textData = "";
|
||||
|
||||
let dataCustomerId: string[] = [];
|
||||
let textWorkList: string[] = [];
|
||||
let dataUserId: string[] = [];
|
||||
|
||||
if (res) {
|
||||
res.forEach((data, index) => {
|
||||
data.customerBranch.customer.branch.forEach((item) => {
|
||||
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
||||
dataCustomerId.push(item.id);
|
||||
dataUserId.push(item.userId);
|
||||
}
|
||||
});
|
||||
textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`);
|
||||
});
|
||||
|
||||
finalTextWork = textWorkList.join("\n");
|
||||
}
|
||||
|
||||
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
|
||||
|
||||
const data = {
|
||||
to: dataUserId,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch("https://api.line.me/v2/bot/message/multicast", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -866,6 +1123,8 @@ export class UserTaskController extends Controller {
|
|||
@Query() page = 1,
|
||||
@Query() pageSize = 30,
|
||||
@Query() userTaskStatus?: UserTaskStatus,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
taskList: {
|
||||
|
|
@ -908,10 +1167,11 @@ export class UserTaskController extends Controller {
|
|||
: undefined,
|
||||
OR: queryOrNot(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ taskName: { contains: query } },
|
||||
{ contactName: { contains: query } },
|
||||
{ contactTel: { contains: query } },
|
||||
{ taskName: { contains: query, mode: "insensitive" } },
|
||||
{ contactName: { contains: query, mode: "insensitive" } },
|
||||
{ contactTel: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.TaskOrderWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -965,20 +1225,41 @@ export class UserTaskController extends Controller {
|
|||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const promises = body.taskOrderId.flatMap((taskOrderId) => [
|
||||
tx.taskOrder.update({
|
||||
where: { id: taskOrderId },
|
||||
data: {
|
||||
taskOrderStatus: TaskOrderStatus.InProgress,
|
||||
userTask: {
|
||||
deleteMany: { userId: req.user.sub },
|
||||
create: {
|
||||
userId: req.user.sub,
|
||||
userTaskStatus: UserTaskStatus.Accept,
|
||||
acceptedAt: new Date(),
|
||||
tx.taskOrder
|
||||
.update({
|
||||
where: { id: taskOrderId },
|
||||
data: {
|
||||
taskOrderStatus: TaskOrderStatus.InProgress,
|
||||
userTask: {
|
||||
deleteMany: { userId: req.user.sub },
|
||||
create: {
|
||||
userId: req.user.sub,
|
||||
userTaskStatus: UserTaskStatus.Accept,
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(async (v) => {
|
||||
await tx.notification.create({
|
||||
data: {
|
||||
title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed",
|
||||
detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress",
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
});
|
||||
await tx.notification.create({
|
||||
data: {
|
||||
title: "มีการรับงาน / Task Accepted",
|
||||
detail: "รหัสใบสั่งงาน / Order : " + v.code,
|
||||
receiverId: v.createdByUserId,
|
||||
registeredBranchId: v.registeredBranchId,
|
||||
groupReceiver: { create: { name: "document_checker" } },
|
||||
},
|
||||
});
|
||||
}),
|
||||
tx.task.updateMany({
|
||||
where: {
|
||||
taskOrderId: taskOrderId,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Security,
|
||||
Tags,
|
||||
} from "tsoa";
|
||||
import config from "../config.json";
|
||||
|
||||
import prisma from "../db";
|
||||
|
||||
|
|
@ -35,29 +36,28 @@ import {
|
|||
} from "../utils/minio";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { CreditNotePaybackType, CreditNoteStatus, Prisma, RequestDataStatus } from "@prisma/client";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import { PaybackStatus, RequestWorkStatus } from "../generated/kysely/types";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
const VAT_DEFAULT = config.vat;
|
||||
|
||||
// NOTE: permission condition/check in requestWork -> requestData -> quotation -> registeredBranch
|
||||
const permissionCond = createPermCondition(globalAllow);
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCheck = createPermCheck(globalAllow);
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
|
||||
type CreditNoteCreate = {
|
||||
requestWorkId: string[];
|
||||
|
|
@ -85,6 +85,14 @@ type CreditNoteUpdate = {
|
|||
@Route("api/v1/credit-note")
|
||||
@Tags("Credit Note")
|
||||
export class CreditNoteController extends Controller {
|
||||
async #getLineToken() {
|
||||
if (!process.env.LINE_MESSAGING_API_TOKEN) {
|
||||
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
|
||||
}
|
||||
|
||||
return process.env.LINE_MESSAGING_API_TOKEN;
|
||||
}
|
||||
|
||||
@Get("stats")
|
||||
@Security("keycloak")
|
||||
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) {
|
||||
|
|
@ -94,7 +102,7 @@ export class CreditNoteController extends Controller {
|
|||
request: {
|
||||
quotationId,
|
||||
quotation: {
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -121,6 +129,8 @@ export class CreditNoteController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() quotationId?: string,
|
||||
@Query() creditNoteStatus?: CreditNoteStatus,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return await this.getCreditNoteListByCriteria(
|
||||
req,
|
||||
|
|
@ -129,6 +139,8 @@ export class CreditNoteController extends Controller {
|
|||
query,
|
||||
quotationId,
|
||||
creditNoteStatus,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -142,28 +154,28 @@ export class CreditNoteController extends Controller {
|
|||
@Query() query: string = "",
|
||||
@Query() quotationId?: string,
|
||||
@Query() creditNoteStatus?: CreditNoteStatus,
|
||||
@Body() body?: {},
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{
|
||||
code: { contains: query, mode: "insensitive" },
|
||||
requestWork: {
|
||||
some: {
|
||||
request: {
|
||||
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
|
||||
{ quotation: { code: { contains: query, mode: "insensitive" } } },
|
||||
{ quotation: { workName: { contains: query } } },
|
||||
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
|
||||
{
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -171,14 +183,14 @@ export class CreditNoteController extends Controller {
|
|||
OR: [
|
||||
{
|
||||
employeePassport: {
|
||||
some: { number: { contains: query } },
|
||||
some: { number: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -194,16 +206,19 @@ export class CreditNoteController extends Controller {
|
|||
request: {
|
||||
quotationId,
|
||||
quotation: {
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.CreditNoteWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.creditNote.findMany({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
include: {
|
||||
quotation: {
|
||||
include: {
|
||||
|
|
@ -236,7 +251,7 @@ export class CreditNoteController extends Controller {
|
|||
some: {
|
||||
request: {
|
||||
quotation: {
|
||||
registeredBranch: { OR: permissionCondCompany(req.user) },
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -334,9 +349,8 @@ export class CreditNoteController extends Controller {
|
|||
).length;
|
||||
|
||||
const price =
|
||||
c.productService.pricePerUnit -
|
||||
c.productService.discount / c.productService.amount +
|
||||
c.productService.vat / c.productService.amount;
|
||||
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
|
||||
c.productService.discount;
|
||||
|
||||
if (serviceChargeStepCount && successCount) {
|
||||
return a + price - c.productService.product.serviceCharge * successCount;
|
||||
|
|
@ -362,40 +376,98 @@ export class CreditNoteController extends Controller {
|
|||
update: { value: { increment: 1 } },
|
||||
});
|
||||
|
||||
return await prisma.creditNote.create({
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
return await prisma.creditNote
|
||||
.create({
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
},
|
||||
},
|
||||
quotation: {
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: { include: { branch: { where: { userId: { not: null } } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
quotation: true,
|
||||
},
|
||||
data: {
|
||||
reason: body.reason,
|
||||
detail: body.detail,
|
||||
remark: body.remark,
|
||||
paybackType: body.paybackType,
|
||||
paybackBank: body.paybackBank,
|
||||
paybackAccount: body.paybackAccount,
|
||||
paybackAccountName: body.paybackAccountName,
|
||||
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
|
||||
value,
|
||||
requestWork: {
|
||||
connect: body.requestWorkId.map((v) => ({
|
||||
id: v,
|
||||
})),
|
||||
data: {
|
||||
reason: body.reason,
|
||||
detail: body.detail,
|
||||
remark: body.remark,
|
||||
paybackType: body.paybackType,
|
||||
paybackBank: body.paybackBank,
|
||||
paybackAccount: body.paybackAccount,
|
||||
paybackAccountName: body.paybackAccountName,
|
||||
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
|
||||
value,
|
||||
requestWork: {
|
||||
connect: body.requestWorkId.map((v) => ({
|
||||
id: v,
|
||||
})),
|
||||
},
|
||||
quotationId: body.quotationId,
|
||||
},
|
||||
quotationId: body.quotationId,
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(async (res) => {
|
||||
const token = await this.#getLineToken();
|
||||
if (!token) return;
|
||||
|
||||
const textHead = "JWS ALERT:";
|
||||
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าใบลดหนี้";
|
||||
const textAlert2 = "ได้ถูกสร้างขึ้นเรียบร้อยแล้ว";
|
||||
const textAlert3 =
|
||||
"หากท่านต้องการข้อมูลเพิ่มเติมหรือมีข้อสงสัยประการใด โปรดแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ ทางเรายินดีให้ความช่วยเหลืออย่างเต็มที่ 🙏";
|
||||
let finalTextWork = "";
|
||||
let textData = "";
|
||||
|
||||
let dataCustomerId: string[] = [];
|
||||
let dataUserId: string[] = [];
|
||||
|
||||
if (res) {
|
||||
res.quotation.customerBranch.customer.branch.forEach((item) => {
|
||||
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
||||
dataCustomerId.push(item.id);
|
||||
dataUserId.push(item.userId);
|
||||
}
|
||||
});
|
||||
finalTextWork = `จำนวนเงิน ${res.value.toFixed(2)} บาท `;
|
||||
}
|
||||
|
||||
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}${textAlert2}\n\n${textAlert3}`;
|
||||
|
||||
const data = {
|
||||
to: dataUserId,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch("https://api.line.me/v2/bot/message/multicast", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return res;
|
||||
});
|
||||
},
|
||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||
);
|
||||
}
|
||||
|
||||
@Put("{creditNoteId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@Security("keycloak")
|
||||
async updateCreditNote(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() creditNoteId: string,
|
||||
|
|
@ -418,17 +490,20 @@ export class CreditNoteController extends Controller {
|
|||
|
||||
const requestWork = await prisma.requestWork.findMany({
|
||||
where: {
|
||||
OR: [{ creditNote: null }, { creditNoteId }],
|
||||
request: {
|
||||
quotation: {
|
||||
id: body.quotationId,
|
||||
},
|
||||
quotation: { id: body.quotationId },
|
||||
},
|
||||
stepStatus: {
|
||||
some: {
|
||||
workStatus: RequestWorkStatus.Canceled,
|
||||
AND: [
|
||||
{
|
||||
OR: [{ creditNote: null }, { creditNoteId }],
|
||||
},
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{ request: { requestDataStatus: RequestDataStatus.Canceled } },
|
||||
{ stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
id: { in: body.requestWorkId },
|
||||
},
|
||||
include: {
|
||||
|
|
@ -467,9 +542,8 @@ export class CreditNoteController extends Controller {
|
|||
).length;
|
||||
|
||||
const price =
|
||||
c.productService.pricePerUnit -
|
||||
c.productService.discount / c.productService.amount +
|
||||
c.productService.vat / c.productService.amount;
|
||||
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
|
||||
c.productService.discount;
|
||||
|
||||
if (serviceChargeStepCount && successCount) {
|
||||
return a + price - c.productService.product.serviceCharge * successCount;
|
||||
|
|
@ -498,10 +572,8 @@ export class CreditNoteController extends Controller {
|
|||
value,
|
||||
requestWork: {
|
||||
disconnect: creditNoteData.requestWork
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
}))
|
||||
.filter((data) => !body.requestWorkId.find((item) => (item = data.id))),
|
||||
.map((item) => ({ id: item.id }))
|
||||
.filter((data) => !body.requestWorkId.find((item) => item === data.id)),
|
||||
connect: body.requestWorkId.map((v) => ({
|
||||
id: v,
|
||||
})),
|
||||
|
|
@ -532,6 +604,14 @@ export class CreditNoteController extends Controller {
|
|||
if (!record) throw notFoundError("Credit Note");
|
||||
await permissionCheck(req.user, record.quotation.registeredBranch);
|
||||
|
||||
if (record.creditNoteStatus !== CreditNoteStatus.Waiting) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Accepted credit note cannot be deleted",
|
||||
"creditNoteAcceptedNoDelete",
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
deleteFolder(fileLocation.creditNote.slip(creditNoteId)),
|
||||
deleteFolder(fileLocation.creditNote.attachment(creditNoteId)),
|
||||
|
|
@ -560,6 +640,24 @@ export class CreditNoteActionController extends Controller {
|
|||
return creditNoteData;
|
||||
}
|
||||
|
||||
async #getLineToken() {
|
||||
if (!process.env.LINE_MESSAGING_API_TOKEN) {
|
||||
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
|
||||
}
|
||||
|
||||
return process.env.LINE_MESSAGING_API_TOKEN;
|
||||
}
|
||||
|
||||
@Post("accept")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async acceptCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) {
|
||||
await this.#checkPermission(req.user, creditNoteId);
|
||||
return await prisma.creditNote.update({
|
||||
where: { id: creditNoteId },
|
||||
data: { creditNoteStatus: CreditNoteStatus.Pending },
|
||||
});
|
||||
}
|
||||
|
||||
@Post("payback-status")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async updateStatus(
|
||||
|
|
@ -568,23 +666,81 @@ export class CreditNoteActionController extends Controller {
|
|||
@Body() body: { paybackStatus: PaybackStatus },
|
||||
) {
|
||||
await this.#checkPermission(req.user, creditNoteId);
|
||||
return await prisma.creditNote.update({
|
||||
where: { id: creditNoteId },
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
return await prisma.creditNote
|
||||
.update({
|
||||
where: { id: creditNoteId },
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
},
|
||||
},
|
||||
quotation: {
|
||||
include: {
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: { include: { branch: { where: { userId: { not: null } } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
quotation: true,
|
||||
},
|
||||
data: {
|
||||
creditNoteStatus:
|
||||
body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
|
||||
paybackStatus: body.paybackStatus,
|
||||
paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined,
|
||||
},
|
||||
});
|
||||
data: {
|
||||
creditNoteStatus:
|
||||
body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
|
||||
paybackStatus: body.paybackStatus,
|
||||
paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined,
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
const token = await this.#getLineToken();
|
||||
if (!token) return;
|
||||
|
||||
const textHead = "JWS ALERT:";
|
||||
|
||||
const textAlert = "ทางเราขอแจ้งให้ทราบว่าการดำเนินการคืนเงินสำหรับใบลดหนี้";
|
||||
const textAlert2 = "ได้รับการอนุมัติและเสร็จสมบูรณ์เรียบร้อยแล้ว";
|
||||
const textAlert3 =
|
||||
"หากท่านต้องการข้อมูลเพิ่มเติมหรือมีข้อสงสัยประการใด โปรดแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ ทางเรายินดีให้ความช่วยเหลืออย่างเต็มที่ 🙏";
|
||||
let finalTextWork = "";
|
||||
let textData = "";
|
||||
|
||||
let dataCustomerId: string[] = [];
|
||||
let textWorkList: string[] = [];
|
||||
let dataUserId: string[] = [];
|
||||
|
||||
if (res) {
|
||||
res.quotation.customerBranch.customer.branch.forEach((item) => {
|
||||
if (!dataCustomerId?.includes(item.id) && item.userId) {
|
||||
dataCustomerId.push(item.id);
|
||||
dataUserId.push(item.userId);
|
||||
}
|
||||
});
|
||||
finalTextWork = `จำนวนเงิน ${res.value.toFixed(2)} บาท `;
|
||||
}
|
||||
|
||||
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}${textAlert2}\n\n${textAlert3}`;
|
||||
|
||||
const data = {
|
||||
to: dataUserId,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
body.paybackStatus === PaybackStatus.Done
|
||||
? await fetch("https://api.line.me/v2/bot/message/multicast", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
: undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import {
|
|||
setFile,
|
||||
} from "../utils/minio";
|
||||
import { isUsedError, notFoundError, relationError } from "../utils/error";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { queryOrNot, whereDateQuery } from "../utils/relation";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import { precisionRound } from "../utils/arithmetic";
|
||||
|
||||
|
|
@ -44,22 +44,20 @@ const MANAGE_ROLES = [
|
|||
"system",
|
||||
"head_of_admin",
|
||||
"admin",
|
||||
"head_of_accountant",
|
||||
"executive",
|
||||
"accountant",
|
||||
"head_of_sale",
|
||||
"sale",
|
||||
"branch_admin",
|
||||
"branch_manager",
|
||||
"branch_accountant",
|
||||
];
|
||||
|
||||
function globalAllow(user: RequestWithUser["user"]) {
|
||||
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
|
||||
return allowList.some((v) => user.roles?.includes(v));
|
||||
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
|
||||
return user.roles?.some((v) => listAllowed.includes(v)) || false;
|
||||
}
|
||||
|
||||
// NOTE: permission condition/check in registeredBranch
|
||||
const permissionCond = createPermCondition(globalAllow);
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCheck = createPermCheck(globalAllow);
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
|
||||
type DebitNoteCreate = {
|
||||
quotationId: string;
|
||||
|
|
@ -76,6 +74,7 @@ type DebitNoteCreate = {
|
|||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
namePrefix?: string;
|
||||
firstName: string;
|
||||
firstNameEN: string;
|
||||
|
|
@ -111,13 +110,14 @@ type DebitNoteUpdate = {
|
|||
dateOfBirth: Date;
|
||||
gender: string;
|
||||
nationality: string;
|
||||
otherNationality?: string | null;
|
||||
namePrefix?: string;
|
||||
firstName: string;
|
||||
firstName?: string;
|
||||
firstNameEN: string;
|
||||
middleName?: string;
|
||||
middleNameEN?: string;
|
||||
lastName: string;
|
||||
lastNameEN: string;
|
||||
lastName?: string;
|
||||
lastNameEN?: string;
|
||||
}
|
||||
)[];
|
||||
|
||||
|
|
@ -168,6 +168,8 @@ export class DebitNoteController extends Controller {
|
|||
@Query() payCondition?: PayCondition,
|
||||
@Query() includeRegisteredBranch?: boolean,
|
||||
@Query() code?: string,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
return await this.getDebitNoteListByCriteria(
|
||||
req,
|
||||
|
|
@ -179,6 +181,8 @@ export class DebitNoteController extends Controller {
|
|||
payCondition,
|
||||
includeRegisteredBranch,
|
||||
code,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -195,21 +199,22 @@ export class DebitNoteController extends Controller {
|
|||
@Query() payCondition?: PayCondition,
|
||||
@Query() includeRegisteredBranch?: boolean,
|
||||
@Query() code?: string,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
@Body() body?: {},
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ workName: { contains: query } },
|
||||
{ workName: { contains: query, mode: "insensitive" } },
|
||||
{
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -220,6 +225,7 @@ export class DebitNoteController extends Controller {
|
|||
debitNoteQuotationId: quotationId,
|
||||
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
|
||||
quotationStatus: status,
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.QuotationWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -424,12 +430,18 @@ export class DebitNoteController extends Controller {
|
|||
|
||||
const list = body.productServiceList.map((v, i) => {
|
||||
const p = product.find((p) => p.id === v.productId)!;
|
||||
const price = body.agentPrice ? p.agentPrice : p.price;
|
||||
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
|
||||
const vat = p.calcVat
|
||||
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
|
||||
VAT_DEFAULT *
|
||||
(!v.discount ? v.amount : 1)
|
||||
|
||||
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
|
||||
|
||||
const originalPrice = body.agentPrice ? p.agentPrice : p.price;
|
||||
const finalPrice = precisionRound(
|
||||
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
|
||||
);
|
||||
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
|
||||
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
|
||||
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
|
||||
(1 + VAT_DEFAULT)) *
|
||||
VAT_DEFAULT
|
||||
: 0;
|
||||
|
||||
return {
|
||||
|
|
@ -452,15 +464,13 @@ export class DebitNoteController extends Controller {
|
|||
|
||||
const price = list.reduce(
|
||||
(a, c) => {
|
||||
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
|
||||
const vat = c.vat ? VAT_DEFAULT : 0;
|
||||
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
|
||||
a.vat = precisionRound(a.vat + c.vat);
|
||||
a.vatExcluded =
|
||||
c.vat === 0
|
||||
? precisionRound(
|
||||
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
|
||||
)
|
||||
: a.vatExcluded;
|
||||
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
|
||||
a.finalPrice = precisionRound(
|
||||
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
|
||||
);
|
||||
|
|
@ -531,7 +541,6 @@ export class DebitNoteController extends Controller {
|
|||
...price,
|
||||
isDebitNote: true,
|
||||
debitNoteQuotationId: quotationId,
|
||||
quotationStatus: QuotationStatus.PaymentPending,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
code: `DN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(6, "0")}`,
|
||||
contactName: master?.contactName ?? "",
|
||||
|
|
@ -573,7 +582,7 @@ export class DebitNoteController extends Controller {
|
|||
}
|
||||
|
||||
@Put("{debitNoteId}")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
@Security("keycloak")
|
||||
async updateDebitNote(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() debitNoteId: string,
|
||||
|
|
@ -597,7 +606,7 @@ export class DebitNoteController extends Controller {
|
|||
|
||||
if (!record) throw notFoundError("Debit Note");
|
||||
|
||||
await permissionCheckCompany(req.user, record.registeredBranch);
|
||||
await permissionCheck(req.user, record.registeredBranch);
|
||||
|
||||
const { productServiceList: _productServiceList, ...rest } = body;
|
||||
const ids = {
|
||||
|
|
@ -668,12 +677,18 @@ export class DebitNoteController extends Controller {
|
|||
}
|
||||
const list = body.productServiceList.map((v, i) => {
|
||||
const p = product.find((p) => p.id === v.productId)!;
|
||||
const price = body.agentPrice ? p.agentPrice : p.price;
|
||||
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
|
||||
const vat = p.calcVat
|
||||
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
|
||||
VAT_DEFAULT *
|
||||
(!v.discount ? v.amount : 1)
|
||||
|
||||
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
|
||||
|
||||
const originalPrice = record.agentPrice ? p.agentPrice : p.price;
|
||||
const finalPrice = precisionRound(
|
||||
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
|
||||
);
|
||||
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
|
||||
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
|
||||
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
|
||||
(1 + VAT_DEFAULT)) *
|
||||
VAT_DEFAULT
|
||||
: 0;
|
||||
|
||||
return {
|
||||
|
|
@ -696,15 +711,13 @@ export class DebitNoteController extends Controller {
|
|||
|
||||
const price = list.reduce(
|
||||
(a, c) => {
|
||||
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
|
||||
const vat = c.vat ? VAT_DEFAULT : 0;
|
||||
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
|
||||
a.vat = precisionRound(a.vat + c.vat);
|
||||
a.vatExcluded =
|
||||
c.vat === 0
|
||||
? precisionRound(
|
||||
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
|
||||
)
|
||||
: a.vatExcluded;
|
||||
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
|
||||
a.finalPrice = precisionRound(
|
||||
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
|
||||
);
|
||||
|
|
@ -824,6 +837,38 @@ export class DebitNoteController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/debit-note/{debitNoteId}")
|
||||
@Tags("Debit Note")
|
||||
export class DebitNoteActionController extends Controller {
|
||||
async #checkPermission(user: RequestWithUser["user"], id: string) {
|
||||
const data = await prisma.quotation.findUnique({
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(user),
|
||||
},
|
||||
},
|
||||
where: { id, isDebitNote: true },
|
||||
});
|
||||
if (!data) throw notFoundError("Debit Note");
|
||||
await permissionCheck(user, data.registeredBranch);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Post("accept")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async acceptDebitNote(@Request() req: RequestWithUser, @Path() debitNoteId: string) {
|
||||
const record = await this.#checkPermission(req.user, debitNoteId);
|
||||
|
||||
if (record.quotationStatus !== QuotationStatus.Issued) {
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Already Accepted", "debitNoteAlreadyAccept");
|
||||
}
|
||||
return await prisma.quotation.update({
|
||||
where: { id: debitNoteId },
|
||||
data: { quotationStatus: QuotationStatus.PaymentPending },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/debit-note/{debitNoteId}")
|
||||
@Tags("Debit Note")
|
||||
export class DebitNoteFileController extends Controller {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Head,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
|
|
@ -23,7 +25,7 @@ import {
|
|||
TaskStatus,
|
||||
RequestWorkStatus,
|
||||
} from "@prisma/client";
|
||||
import { queryOrNot, whereAddressQuery } from "../utils/relation";
|
||||
import { queryOrNot, whereAddressQuery, whereDateQuery } from "../utils/relation";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
// import { RequestWorkStatus } from "../generated/kysely/types";
|
||||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
|
|
@ -49,6 +51,8 @@ export class LineController extends Controller {
|
|||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() activeOnly?: boolean,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: !!query
|
||||
|
|
@ -56,13 +60,13 @@ export class LineController extends Controller {
|
|||
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
|
||||
{
|
||||
employeePassport: {
|
||||
some: { number: { contains: query } },
|
||||
some: { number: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
...whereAddressQuery(query),
|
||||
]) ?? []),
|
||||
]
|
||||
|
|
@ -73,11 +77,19 @@ export class LineController extends Controller {
|
|||
status: activeOnly ? { not: Status.INACTIVE } : undefined,
|
||||
id: customerBranchId,
|
||||
customerId,
|
||||
userId: line.user.sub,
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
subDistrict: zipCode ? { zipCode } : undefined,
|
||||
gender,
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.EmployeeWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -135,7 +147,14 @@ export class LineController extends Controller {
|
|||
where: {
|
||||
id: employeeId,
|
||||
customerBranch: {
|
||||
userId: line.user.sub,
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -157,24 +176,25 @@ export class LineController extends Controller {
|
|||
@Query() requestDataStatus?: RequestDataStatus,
|
||||
@Query() quotationId?: string,
|
||||
@Query() code?: string,
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ quotation: { code: { contains: query, mode: "insensitive" } } },
|
||||
{ quotation: { workName: { contains: query } } },
|
||||
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
|
||||
{
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ registerName: { contains: query } },
|
||||
{ registerNameEN: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ registerName: { contains: query, mode: "insensitive" } },
|
||||
{ registerNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -182,14 +202,14 @@ export class LineController extends Controller {
|
|||
OR: [
|
||||
{
|
||||
employeePassport: {
|
||||
some: { number: { contains: query } },
|
||||
some: { number: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -220,8 +240,18 @@ export class LineController extends Controller {
|
|||
// registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
employee: {
|
||||
customerBranch: { userId: line.user.sub },
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.RequestDataWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -282,7 +312,16 @@ export class LineController extends Controller {
|
|||
where: {
|
||||
id: requestDataId,
|
||||
employee: {
|
||||
customerBranch: { userId: line.user.sub },
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
|
|
@ -399,7 +438,16 @@ export class LineController extends Controller {
|
|||
: undefined,
|
||||
quotationId,
|
||||
employee: {
|
||||
customerBranch: { userId: line.user.sub },
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.RequestWorkWhereInput;
|
||||
|
|
@ -519,7 +567,16 @@ export class LineController extends Controller {
|
|||
id: requestWorkId,
|
||||
request: {
|
||||
employee: {
|
||||
customerBranch: { userId: line.user.sub },
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -546,49 +603,45 @@ export class LineController extends Controller {
|
|||
@Query() status?: QuotationStatus,
|
||||
@Query() pendingOnly?: boolean,
|
||||
@Query() inProgressOnly?: boolean,
|
||||
@Query() historyOnly?: boolean,
|
||||
@Query() successOnly?: boolean,
|
||||
@Query() canceledOnly?: boolean,
|
||||
@Query() urgentFirst?: boolean,
|
||||
@Query() includeRegisteredBranch?: boolean,
|
||||
@Query() code?: string,
|
||||
@Query() query = "",
|
||||
@Query() startDate?: Date,
|
||||
@Query() endDate?: Date,
|
||||
) {
|
||||
const where = {
|
||||
OR: [
|
||||
...(queryOrNot<Prisma.QuotationWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ workName: { contains: query } },
|
||||
{
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ customerName: { contains: query } },
|
||||
{ firstName: { contains: query } },
|
||||
{ firstNameEN: { contains: query } },
|
||||
{ lastName: { contains: query } },
|
||||
{ lastNameEN: { contains: query } },
|
||||
],
|
||||
},
|
||||
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ workName: { contains: query, mode: "insensitive" } },
|
||||
{
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
{ registerName: { contains: query, mode: "insensitive" } },
|
||||
{ firstName: { contains: query, mode: "insensitive" } },
|
||||
{ firstNameEN: { contains: query, mode: "insensitive" } },
|
||||
{ lastName: { contains: query, mode: "insensitive" } },
|
||||
{ lastNameEN: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
]) || []),
|
||||
...(queryOrNot<Prisma.QuotationWhereInput[]>(!!pendingOnly, [
|
||||
{
|
||||
requestData: {
|
||||
some: {
|
||||
requestDataStatus: "Pending",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
requestData: { none: {} },
|
||||
},
|
||||
]) || []),
|
||||
],
|
||||
},
|
||||
]),
|
||||
isDebitNote: false,
|
||||
code,
|
||||
payCondition,
|
||||
quotationStatus: historyOnly ? { in: ["ProcessComplete", "Canceled"] } : status,
|
||||
quotationStatus: successOnly ? "ProcessComplete" : canceledOnly ? "Canceled" : status,
|
||||
customerBranch: {
|
||||
userId: line.user.sub,
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
requestData: inProgressOnly
|
||||
? {
|
||||
|
|
@ -597,6 +650,23 @@ export class LineController extends Controller {
|
|||
},
|
||||
}
|
||||
: undefined,
|
||||
AND: pendingOnly
|
||||
? {
|
||||
OR: [
|
||||
{
|
||||
requestData: {
|
||||
some: {
|
||||
requestDataStatus: "Pending",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
requestData: { none: {} },
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
...whereDateQuery(startDate, endDate),
|
||||
} satisfies Prisma.QuotationWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -698,7 +768,16 @@ export class LineController extends Controller {
|
|||
where: {
|
||||
id: quotationId,
|
||||
isDebitNote: false,
|
||||
customerBranch: { userId: line.user.sub },
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: line.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: line.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -706,6 +785,74 @@ export class LineController extends Controller {
|
|||
|
||||
return record;
|
||||
}
|
||||
|
||||
@Post("request/{requestDataId}/request-cancel")
|
||||
@Security("line")
|
||||
async customerRequestCancel(
|
||||
@Path() requestDataId: string,
|
||||
@Request() req: RequestWithLineUser,
|
||||
@Body() body: { cancel: boolean; reason?: string },
|
||||
) {
|
||||
const result = await prisma.requestData.updateMany({
|
||||
where: {
|
||||
id: requestDataId,
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: req.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: req.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
customerRequestCancel: body.cancel,
|
||||
customerRequestCancelReason: body.reason || null,
|
||||
rejectRequestCancel: false,
|
||||
rejectRequestCancelReason: null,
|
||||
},
|
||||
});
|
||||
if (result.count <= 0) throw notFoundError("Request Data");
|
||||
}
|
||||
|
||||
@Post("request-work/{requestWorkId}/request-cancel")
|
||||
@Security("line")
|
||||
async customerRequestCancelWork(
|
||||
@Path() requestWorkId: string,
|
||||
@Request() req: RequestWithLineUser,
|
||||
@Body() body: { cancel: boolean; reason?: string },
|
||||
) {
|
||||
const result = await prisma.requestWork.updateMany({
|
||||
where: {
|
||||
id: requestWorkId,
|
||||
request: {
|
||||
quotation: {
|
||||
customerBranch: {
|
||||
OR: [
|
||||
{ userId: req.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: req.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
customerRequestCancel: body.cancel,
|
||||
customerRequestCancelReason: body.reason || null,
|
||||
rejectRequestCancel: false,
|
||||
rejectRequestCancelReason: null,
|
||||
},
|
||||
});
|
||||
if (result.count <= 0) throw notFoundError("Request Data");
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/line/customer-branch/{branchId}")
|
||||
|
|
@ -1228,3 +1375,65 @@ export class LineQuotationFileController extends Controller {
|
|||
return await deleteFile(fileLocation.quotation.attachment(quotationId, name));
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/line/payment/{paymentId}/attachment")
|
||||
@Tags("Line")
|
||||
export class PaymentFileLineController extends Controller {
|
||||
private async checkPermission(_user: RequestWithUser["user"], id: string) {
|
||||
const data = await prisma.payment.findUnique({
|
||||
include: {
|
||||
invoice: {
|
||||
include: {
|
||||
quotation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) throw notFoundError("Payment");
|
||||
return { paymentId: id, quotationId: data.invoice.quotationId };
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Security("line")
|
||||
async listAttachment(@Request() req: RequestWithUser, @Path() paymentId: string) {
|
||||
const { quotationId } = await this.checkPermission(req.user, paymentId);
|
||||
return await listFile(fileLocation.quotation.payment(quotationId, paymentId));
|
||||
}
|
||||
|
||||
@Head("{name}")
|
||||
async headAttachment(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() paymentId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
const data = await prisma.payment.findUnique({
|
||||
where: { id: paymentId },
|
||||
include: { invoice: true },
|
||||
});
|
||||
if (!data) throw notFoundError("Payment");
|
||||
return req.res?.redirect(
|
||||
await getPresigned(
|
||||
"head",
|
||||
fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Get("{name}")
|
||||
async getAttachment(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() paymentId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
const data = await prisma.payment.findUnique({
|
||||
where: { id: paymentId },
|
||||
include: { invoice: true },
|
||||
});
|
||||
if (!data) throw notFoundError("Payment");
|
||||
return req.res?.redirect(
|
||||
await getFile(fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { RequestWithLineUser } from "../interfaces/user";
|
|||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
|
||||
type SendEmail = {
|
||||
type SendOTP = {
|
||||
identityNumber: string;
|
||||
email: string;
|
||||
};
|
||||
|
|
@ -25,11 +25,22 @@ export class verificationController extends Controller {
|
|||
@Get()
|
||||
@Security("line")
|
||||
async isRegistered(@Request() req: RequestWithLineUser) {
|
||||
return !!(await prisma.customerBranch.findFirst({ where: { userId: req.user.sub } }));
|
||||
return !!(await prisma.customerBranch.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: req.user.sub },
|
||||
{
|
||||
customer: {
|
||||
branch: { some: { userId: req.user.sub } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
@Post("/send-otp")
|
||||
public async sendOTP(@Body() body: SendEmail) {
|
||||
public async sendOTP(@Body() body: SendOTP) {
|
||||
if (
|
||||
![
|
||||
process.env.SMTP_HOST,
|
||||
|
|
@ -133,13 +144,11 @@ export class verificationController extends Controller {
|
|||
customerBranch.otpExpires &&
|
||||
customerBranch.otpExpires >= new Date()
|
||||
) {
|
||||
const dataCustomer = await prisma.customerBranch.update({
|
||||
const dataCustomer = await prisma.customerBranch.updateMany({
|
||||
where: {
|
||||
id: customerBranch.id,
|
||||
},
|
||||
data: {
|
||||
userId: req.user.sub,
|
||||
customerId: customerBranch.customerId,
|
||||
},
|
||||
data: { userId: req.user.sub },
|
||||
});
|
||||
|
||||
return dataCustomer;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
registerName: true,
|
||||
customer: {
|
||||
select: {
|
||||
telephoneNo: true,
|
||||
customerType: true,
|
||||
registeredBranch: {
|
||||
select: {
|
||||
telephoneNo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -106,34 +106,40 @@ 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 = "";
|
||||
|
||||
if (dataEmployee.length > 0) {
|
||||
const customerName =
|
||||
dataEmployee[0]?.employee?.customerBranch?.customerName ?? "ไม่ระบุ";
|
||||
const registerName =
|
||||
dataEmployee[0]?.employee?.customerBranch?.registerName ?? "ไม่ระบุ";
|
||||
const telephoneNo =
|
||||
dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ??
|
||||
"ไม่ระบุ";
|
||||
|
||||
const textEmployer = `เรียน คุณ${customerName}`;
|
||||
const textEmployer = `เรียน คุณ${registerName}`;
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง";
|
||||
const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้";
|
||||
const textExpDate =
|
||||
|
|
@ -147,7 +153,10 @@ 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}`;
|
||||
if (diffDate > 0) {
|
||||
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n ${process.env.LINE_LIFF_URL}/${item.employeeId}`;
|
||||
}
|
||||
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} หมดอายุไปแล้ว ${Math.abs(diffDate)} วัน \n ${process.env.LINE_LIFF_URL}/${item.employeeId}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
|
|
|
|||
113
src/controllers/10-business-type-controller.ts
Normal file
113
src/controllers/10-business-type-controller.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
Route,
|
||||
Security,
|
||||
Tags,
|
||||
} from "tsoa";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import prisma from "../db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { notFoundError } from "../utils/error";
|
||||
|
||||
type BusinessTypePayload = {
|
||||
name: string;
|
||||
nameEN: string;
|
||||
};
|
||||
|
||||
@Route("api/v1/business-type")
|
||||
@Tags("Business Type")
|
||||
export class businessTypeController extends Controller {
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async getList(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() query: string = "",
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot<Prisma.BusinessTypeWhereInput[]>(query, [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ nameEN: { contains: query, mode: "insensitive" } },
|
||||
]),
|
||||
} satisfies Prisma.BusinessTypeWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.businessType.findMany({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
}),
|
||||
prisma.businessType.count({ where }),
|
||||
]);
|
||||
|
||||
return { result, page, pageSize, total };
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Security("keycloak")
|
||||
async createBusinessType(@Request() req: RequestWithUser, @Body() body: BusinessTypePayload) {
|
||||
return await prisma.businessType.create({
|
||||
data: {
|
||||
...body,
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Get(":businessTypeId")
|
||||
@Security("keycloak")
|
||||
async getBusinessTypeById(@Path() businessTypeId: string) {
|
||||
return await prisma.businessType.findUnique({
|
||||
where: { id: businessTypeId },
|
||||
});
|
||||
}
|
||||
|
||||
@Put(":businessTypeId")
|
||||
@Security("keycloak")
|
||||
async updateBusinessType(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() businessTypeId: string,
|
||||
@Body() body: BusinessTypePayload,
|
||||
) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const record = await tx.businessType.findUnique({
|
||||
where: { id: businessTypeId },
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("BusinessType");
|
||||
return await tx.businessType.update({
|
||||
where: { id: businessTypeId },
|
||||
data: {
|
||||
...body,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(":businessTypeId")
|
||||
@Security("keycloak")
|
||||
async deleteBusinessType(@Path() businessTypeId: string) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const record = await tx.businessType.findUnique({
|
||||
where: { id: businessTypeId },
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("BusinessType");
|
||||
return await tx.businessType.delete({
|
||||
where: { id: businessTypeId },
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
25
src/controllers/10-manual-controller.ts
Normal file
25
src/controllers/10-manual-controller.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import express from "express";
|
||||
import { Controller, Get, Path, Request, Route } from "tsoa";
|
||||
import { getFile } from "../utils/minio";
|
||||
|
||||
@Route("api/v1/manual")
|
||||
export class ManualController extends Controller {
|
||||
@Get()
|
||||
async get(@Request() req: express.Request) {
|
||||
return req.res?.redirect(await getFile(".manual/toc.json"));
|
||||
}
|
||||
|
||||
@Get("{category}/assets/{name}")
|
||||
async getAsset(@Request() req: express.Request, @Path() category: string, @Path() name: string) {
|
||||
return req.res?.redirect(await getFile(`.manual/${category}/assets/${name}`));
|
||||
}
|
||||
|
||||
@Get("{category}/page/{page}")
|
||||
async getContent(
|
||||
@Request() req: express.Request,
|
||||
@Path() category: string,
|
||||
@Path() page: string,
|
||||
) {
|
||||
return req.res?.redirect(await getFile(`.manual/${category}/${page}.md`));
|
||||
}
|
||||
}
|
||||
25
src/controllers/10-troubleshooting-controller.ts
Normal file
25
src/controllers/10-troubleshooting-controller.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import express from "express";
|
||||
import { Controller, Get, Path, Request, Route } from "tsoa";
|
||||
import { getFile } from "../utils/minio";
|
||||
|
||||
@Route("api/v1/troubleshooting")
|
||||
export class TroubleshootingController extends Controller {
|
||||
@Get()
|
||||
async get(@Request() req: express.Request) {
|
||||
return req.res?.redirect(await getFile(".troubleshooting/toc.json"));
|
||||
}
|
||||
|
||||
@Get("{category}/assets/{name}")
|
||||
async getAsset(@Request() req: express.Request, @Path() category: string, @Path() name: string) {
|
||||
return req.res?.redirect(await getFile(`.troubleshooting/${category}/assets/${name}`));
|
||||
}
|
||||
|
||||
@Get("{category}/page/{page}")
|
||||
async getContent(
|
||||
@Request() req: express.Request,
|
||||
@Path() category: string,
|
||||
@Path() page: string,
|
||||
) {
|
||||
return req.res?.redirect(await getFile(`.troubleshooting/${category}/${page}.md`));
|
||||
}
|
||||
}
|
||||
39
src/interfaces/edm.ts
Normal file
39
src/interfaces/edm.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
export interface StorageFolder {
|
||||
/**
|
||||
* @prop Full path to this folder. It is used as key as there are no files or directories at the same location.
|
||||
*/
|
||||
pathname: string;
|
||||
/**
|
||||
* @prop Directory / Folder name.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
createdAt: string | Date;
|
||||
createdBy: string | Date;
|
||||
}
|
||||
|
||||
export interface StorageFile {
|
||||
/**
|
||||
* @prop Full path to this folder. It is used as key as there are no files or directories at the same location.
|
||||
*/
|
||||
pathname: string;
|
||||
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
category: string[];
|
||||
keyword: string[];
|
||||
metadata: Record<string, unknown>;
|
||||
|
||||
path: string;
|
||||
upload: boolean;
|
||||
|
||||
updatedAt: string | Date;
|
||||
updatedBy: string;
|
||||
createdAt: string | Date;
|
||||
createdBy: string;
|
||||
}
|
||||
170
src/services/edm/edm-api.ts
Normal file
170
src/services/edm/edm-api.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { DecodedJwt, createDecoder } from "fast-jwt";
|
||||
import HttpError from "../../interfaces/http-error";
|
||||
import HttpStatus from "../../interfaces/http-status";
|
||||
import { StorageFile, StorageFolder } from "../../interfaces/edm";
|
||||
|
||||
const jwtDecode = createDecoder({ complete: true });
|
||||
|
||||
export type FileProps = Partial<
|
||||
Pick<StorageFile, "title" | "description" | "author" | "keyword" | "category">
|
||||
> & {
|
||||
metadata?: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
const STORAGE_KEYCLOAK = process.env.EDM_KEYCLOAK!;
|
||||
const STORAGE_KEYCLOAK_CLIENT = process.env.EDM_KEYCLOAK_CLIENT!;
|
||||
const STORAGE_REALM = process.env.EDM_REALM!;
|
||||
const STORAGE_URL = process.env.EDM_URL!;
|
||||
const STORAGE_USER = process.env.EDM_ADMIN_USER!;
|
||||
const STORAGE_PASSWORD = process.env.EDM_ADMIN_PASSWORD!;
|
||||
|
||||
let token: string | null = null;
|
||||
let decoded: DecodedJwt | null = null;
|
||||
|
||||
/**
|
||||
* Check if token is expired or will expire in 30 seconds
|
||||
* @returns true if expire or can't get exp, false otherwise
|
||||
*/
|
||||
export function expireCheck(token: string, beforeExpire: number = 30) {
|
||||
decoded = jwtDecode(token);
|
||||
|
||||
if (decoded && decoded.payload.exp) {
|
||||
return Date.now() / 1000 >= decoded.payload.exp - beforeExpire;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from id service if needed
|
||||
*/
|
||||
export async function getToken() {
|
||||
if (!token || expireCheck(token)) {
|
||||
const body = new URLSearchParams();
|
||||
|
||||
body.append("scope", "openid");
|
||||
body.append("grant_type", "password");
|
||||
body.append("client_id", STORAGE_KEYCLOAK_CLIENT || "edm");
|
||||
body.append("username", STORAGE_USER);
|
||||
body.append("password", STORAGE_PASSWORD);
|
||||
|
||||
const res = await fetch(
|
||||
`${STORAGE_KEYCLOAK}/realms/${STORAGE_REALM}/protocol/openid-connect/token`,
|
||||
{
|
||||
method: "POST",
|
||||
body: body,
|
||||
},
|
||||
).catch((e) => console.error(e));
|
||||
|
||||
if (!res) return;
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data && data.access_token) {
|
||||
token = data.access_token;
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - Path that new folder will live
|
||||
* @param name - Name of the folder to create
|
||||
* @param recursive - Will create parent automatically
|
||||
*/
|
||||
export async function createFolder(path: string[], name: string, recursive: boolean = false) {
|
||||
if (recursive && path.length > 0) {
|
||||
await createFolder(path.slice(0, -1), path[path.length - 1], true);
|
||||
}
|
||||
|
||||
const res = await fetch(`${STORAGE_URL}/storage/folder`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getToken()}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ path, name }),
|
||||
}).catch((e) => console.error(e));
|
||||
|
||||
if (!res || !res.ok) {
|
||||
return Boolean(console.error(res ? await res.json() : res));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - Path that new file will live
|
||||
* @param file - Name of the file to create
|
||||
*/
|
||||
export async function createFile(path: string[], file: string, props?: FileProps) {
|
||||
const res = await fetch(`${STORAGE_URL}/storage/file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getToken()}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...props,
|
||||
path,
|
||||
file,
|
||||
hidden: path.some((v) => v.startsWith(".")),
|
||||
}),
|
||||
}).catch((e) => console.error(e));
|
||||
|
||||
if (!res || !res.ok) {
|
||||
return Boolean(console.error(res ? await res.json() : res));
|
||||
}
|
||||
return (await res.json()) as StorageFile & { uploadUrl: string };
|
||||
}
|
||||
|
||||
export async function list(
|
||||
operation: "file" | "folder",
|
||||
path: string[],
|
||||
): Promise<false | (StorageFile & { uploadUrl: string })[]> {
|
||||
const res = await fetch(`${STORAGE_URL}/storage/list`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getToken()}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ operation, path, hidden: true }),
|
||||
}).catch((e) => console.error(e));
|
||||
|
||||
if (!res || !res.ok) {
|
||||
if (res && res.status === HttpStatus.NOT_FOUND) {
|
||||
return [];
|
||||
}
|
||||
return Boolean(console.error(res ? await res.json() : res)) as false;
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function listFolder(path: string[]) {
|
||||
return (await list("folder", path)) as StorageFolder[] | boolean;
|
||||
}
|
||||
|
||||
export async function listFile(path: string[]) {
|
||||
return (await list("file", path)) as StorageFile[] | boolean;
|
||||
}
|
||||
|
||||
export async function downloadFile(
|
||||
path: string[],
|
||||
file: string,
|
||||
): Promise<false | (StorageFile & { downloadUrl: string })> {
|
||||
const res = await fetch(`${STORAGE_URL}/storage/file/download`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getToken()}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ path, file }),
|
||||
}).catch((e) => console.error(e));
|
||||
|
||||
if (!res || !res.ok) {
|
||||
if (res && res.status === HttpStatus.NOT_FOUND) {
|
||||
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบไฟล์ในระบบ");
|
||||
}
|
||||
console.error(res ? await res.json() : res);
|
||||
return false;
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import prisma from "../db";
|
||||
import config from "../config.json";
|
||||
import { CustomerType, PayCondition } from "@prisma/client";
|
||||
import { convertTemplate } from "../utils/string-template";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { JsonObject } from "@prisma/client/runtime/library";
|
||||
import { precisionRound } from "../utils/arithmetic";
|
||||
|
||||
if (!process.env.FLOW_ACCOUNT_URL) throw new Error("Require FLOW_ACCOUNT_URL");
|
||||
if (!process.env.FLOW_ACCOUNT_CLIENT_ID) throw new Error("Require FLOW_ACCOUNT_CLIENT_ID");
|
||||
|
|
@ -232,6 +236,29 @@ const flowAccount = {
|
|||
installments: true,
|
||||
quotation: {
|
||||
include: {
|
||||
paySplit: true,
|
||||
worker: {
|
||||
select: {
|
||||
employee: {
|
||||
select: {
|
||||
employeePassport: {
|
||||
select: {
|
||||
number: true,
|
||||
},
|
||||
orderBy: {
|
||||
expireDate: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
namePrefix: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
firstNameEN: true,
|
||||
lastNameEN: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
registeredBranch: {
|
||||
include: {
|
||||
province: true,
|
||||
|
|
@ -262,19 +289,58 @@ const flowAccount = {
|
|||
|
||||
const quotation = data.quotation;
|
||||
const customer = quotation.customerBranch;
|
||||
const product =
|
||||
|
||||
const summary = {
|
||||
subTotal: 0,
|
||||
discountAmount: 0,
|
||||
vatableAmount: 0,
|
||||
exemptAmount: 0,
|
||||
vatAmount: 0,
|
||||
grandTotal: 0,
|
||||
};
|
||||
|
||||
const products = (
|
||||
quotation.payCondition === PayCondition.BillFull ||
|
||||
quotation.payCondition === PayCondition.Full
|
||||
? quotation.productServiceList
|
||||
: quotation.productServiceList.filter((lhs) =>
|
||||
data.installments.some((rhs) => rhs.no === lhs.installmentNo),
|
||||
);
|
||||
)
|
||||
).map((v) => {
|
||||
// TODO: Use product's VAT field (not implemented) instead.
|
||||
const VAT_RATE = VAT_DEFAULT;
|
||||
|
||||
summary.subTotal +=
|
||||
precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_RATE : 0))) * v.amount;
|
||||
summary.discountAmount += v.discount;
|
||||
|
||||
const total =
|
||||
precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_RATE : 0))) * v.amount -
|
||||
(v.discount ?? 0);
|
||||
|
||||
if (v.vat > 0) {
|
||||
summary.vatableAmount += precisionRound(total / (1 + VAT_RATE));
|
||||
summary.vatAmount += v.vat;
|
||||
} else {
|
||||
summary.exemptAmount += total;
|
||||
}
|
||||
|
||||
summary.grandTotal += total;
|
||||
|
||||
return {
|
||||
type: ProductAndServiceType.ProductNonInv,
|
||||
name: v.product.name,
|
||||
pricePerUnit: precisionRound(v.pricePerUnit),
|
||||
quantity: v.amount,
|
||||
discountAmount: v.discount,
|
||||
vatRate: v.vat === 0 ? 0 : Math.round(VAT_RATE * 100),
|
||||
total,
|
||||
};
|
||||
});
|
||||
|
||||
const payload = {
|
||||
contactCode: customer.code,
|
||||
contactName:
|
||||
(customer.customer.customerType === CustomerType.PERS
|
||||
? [customer.firstName, customer.lastName].join(" ").trim()
|
||||
: customer.registerName) || "-",
|
||||
contactName: customer.contactName || "-",
|
||||
contactAddress: [
|
||||
customer.address,
|
||||
!!customer.moo ? "หมู่ " + customer.moo : null,
|
||||
|
|
@ -283,11 +349,10 @@ const flowAccount = {
|
|||
(customer.province?.id === "10" ? "แขวง" : "อำเภอ") + customer.subDistrict?.name,
|
||||
(customer.province?.id === "10" ? "เขต" : "ตำบล") + customer.district?.name,
|
||||
"จังหวัด" + customer.province?.name,
|
||||
customer.subDistrict?.zipCode,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
contactTaxId: customer.citizenId || customer.code,
|
||||
contactTaxId: customer.citizenId || customer.legalPersonNo || "-",
|
||||
contactBranch:
|
||||
(customer.customer.customerType === CustomerType.PERS
|
||||
? [customer.firstName, customer.lastName].join(" ").trim()
|
||||
|
|
@ -305,36 +370,35 @@ const flowAccount = {
|
|||
isVat: true,
|
||||
|
||||
useReceiptDeduction: false,
|
||||
useInlineVat: true,
|
||||
|
||||
discounPercentage: 0,
|
||||
discountAmount: quotation.totalDiscount,
|
||||
|
||||
subTotal:
|
||||
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
|
||||
? 0
|
||||
: quotation.totalPrice,
|
||||
totalAfterDiscount:
|
||||
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
|
||||
? 0
|
||||
: quotation.finalPrice,
|
||||
vatAmount:
|
||||
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
|
||||
? 0
|
||||
: quotation.vat,
|
||||
grandTotal:
|
||||
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
|
||||
? data.installments.reduce((a, c) => a + c.amount, 0)
|
||||
: quotation.finalPrice,
|
||||
subTotal: summary.subTotal,
|
||||
totalAfterDiscount: summary.subTotal - summary.discountAmount,
|
||||
vatableAmount: summary.vatableAmount,
|
||||
exemptAmount: summary.exemptAmount,
|
||||
vatAmount: summary.vatAmount,
|
||||
grandTotal: summary.grandTotal,
|
||||
|
||||
items: product.map((v) => ({
|
||||
type: ProductAndServiceType.ProductNonInv,
|
||||
name: v.product.name,
|
||||
pricePerUnit: v.pricePerUnit,
|
||||
quantity: v.amount,
|
||||
discountAmount: v.discount,
|
||||
total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
|
||||
vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
|
||||
})),
|
||||
remarks: htmlToText(
|
||||
convertTemplate(quotation.remark ?? "", {
|
||||
"quotation-payment": {
|
||||
paymentType: quotation?.payCondition || "Full",
|
||||
amount: quotation.finalPrice,
|
||||
installments: quotation?.paySplit,
|
||||
},
|
||||
"quotation-labor": {
|
||||
name: quotation.worker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.employee.employeePassport.length !== 0 ? v.employee.employeePassport[0].number + "_" : ""}${v.employee.namePrefix}. ${v.employee.firstNameEN ? `${v.employee.firstNameEN} ${v.employee.lastNameEN}` : `${v.employee.firstName} ${v.employee.lastName}`} `.toUpperCase(),
|
||||
),
|
||||
},
|
||||
}),
|
||||
),
|
||||
items: products,
|
||||
};
|
||||
|
||||
return await flowAccountAPI.createReceipt(payload, false);
|
||||
|
|
@ -347,6 +411,219 @@ const flowAccount = {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// flowAccount GET Product list
|
||||
async getProducts() {
|
||||
const { token } = await flowAccountAPI.auth();
|
||||
|
||||
const res = await fetch(api + "/products", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
["Content-Type"]: `application/json`,
|
||||
["Authorization"]: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
body: await res.json(),
|
||||
};
|
||||
},
|
||||
|
||||
// flowAccount GET Product by id
|
||||
async getProductsById(recordId: string) {
|
||||
const { token } = await flowAccountAPI.auth();
|
||||
|
||||
const res = await fetch(api + `/products/${recordId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
["Content-Type"]: `application/json`,
|
||||
["Authorization"]: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
list: data.data.list,
|
||||
total: data.data.total,
|
||||
};
|
||||
},
|
||||
|
||||
// flowAccount POST create Product
|
||||
async createProducts(code: string, body: JsonObject) {
|
||||
const { token } = await flowAccountAPI.auth();
|
||||
|
||||
const commonBody = {
|
||||
productStructureType: null,
|
||||
type: 3,
|
||||
name: body.name,
|
||||
sellDescription: body.detail,
|
||||
sellVatType: 3,
|
||||
buyPrice: body.serviceCharge,
|
||||
buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
|
||||
buyDescription: body.detail,
|
||||
};
|
||||
|
||||
const createProduct = async (name: string, price: any, vatIncluded: boolean) => {
|
||||
try {
|
||||
const res = await fetch(`${api}/products`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...commonBody,
|
||||
name,
|
||||
sellPrice: price,
|
||||
sellVatType: vatIncluded ? 1 : 3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: Failed to create product`);
|
||||
|
||||
const json = await res.json().catch(() => {
|
||||
throw new Error("Invalid JSON response from FlowAccount API");
|
||||
});
|
||||
|
||||
return json?.data?.list?.[0]?.id ?? null;
|
||||
} catch (err) {
|
||||
console.error("createProduct error:", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProduct = async (id: string) => {
|
||||
try {
|
||||
await fetch(`${api}/products/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Rollback delete failed:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const [sellResult, agentResult] = await Promise.allSettled([
|
||||
createProduct(`${code} ${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
|
||||
createProduct(
|
||||
`${code} ${body.name} (ราคาตัวแทน)`,
|
||||
body.agentPrice,
|
||||
/true/.test(`${body.agentPriceVatIncluded}`),
|
||||
),
|
||||
]);
|
||||
|
||||
const sellId = sellResult.status === "fulfilled" ? sellResult.value : null;
|
||||
const agentId = agentResult.status === "fulfilled" ? agentResult.value : null;
|
||||
|
||||
// --- validation ---
|
||||
if (!sellId && !agentId) {
|
||||
throw new Error("FlowAccountProductError.BOTH_CREATION_FAILED");
|
||||
}
|
||||
if (!sellId && agentId) {
|
||||
await deleteProduct(agentId);
|
||||
throw new Error("FlowAccountProductError.SELL_PRICE_CREATION_FAILED");
|
||||
}
|
||||
if (sellId && !agentId) {
|
||||
await deleteProduct(sellId);
|
||||
throw new Error("FlowAccountProductError.AGENT_PRICE_CREATION_FAILED");
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
data: {
|
||||
productIdSellPrice: sellId,
|
||||
productIdAgentPrice: agentId,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// flowAccount PUT edit Product
|
||||
async editProducts(sellPriceId: String, agentPriceId: String, body: JsonObject) {
|
||||
const { token } = await flowAccountAPI.auth();
|
||||
|
||||
const commonBody = {
|
||||
productStructureType: null,
|
||||
type: 3,
|
||||
name: body.name,
|
||||
sellDescription: body.detail,
|
||||
sellVatType: 3,
|
||||
buyPrice: body.serviceCharge,
|
||||
buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
|
||||
buyDescription: body.detail,
|
||||
};
|
||||
|
||||
const editProduct = async (id: String, name: String, price: any, vatIncluded: boolean) => {
|
||||
try {
|
||||
const res = await fetch(api + `/products/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...commonBody,
|
||||
name: name,
|
||||
sellPrice: price,
|
||||
sellVatType: vatIncluded ? 1 : 3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status} ${res}`);
|
||||
}
|
||||
|
||||
let json: any = null;
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {
|
||||
throw new Error("Response is not valid JSON");
|
||||
}
|
||||
|
||||
return json?.data?.list?.[0]?.id ?? null;
|
||||
} catch (err) {
|
||||
console.error("createProduct error:", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
editProduct(
|
||||
sellPriceId,
|
||||
`${body.code} ${body.name}`,
|
||||
body.price,
|
||||
/true/.test(`${body.vatIncluded}`),
|
||||
),
|
||||
editProduct(
|
||||
agentPriceId,
|
||||
`${body.code} ${body.name} (ราคาตัวแทน)`,
|
||||
body.agentPrice,
|
||||
/true/.test(`${body.agentPriceVatIncluded}`),
|
||||
),
|
||||
]);
|
||||
},
|
||||
|
||||
// flowAccount DELETE Product
|
||||
async deleteProduct(recordId: string) {
|
||||
const { token } = await flowAccountAPI.auth();
|
||||
|
||||
const res = await fetch(api + `/products/${recordId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
["Authorization"]: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default flowAccount;
|
||||
|
|
|
|||
|
|
@ -346,6 +346,64 @@ export async function removeUserRoles(userId: string, roles: { id: string; name:
|
|||
return true;
|
||||
}
|
||||
|
||||
export async function getGroup(query: string) {
|
||||
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/groups?${query}`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${await getToken()}`,
|
||||
"content-type": `application/json`,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const dataMainGroup = await res.json();
|
||||
const fetchSubGroups = async (group: any) => {
|
||||
let fullSubGroup = await Promise.all(
|
||||
group.subGroups.map((subGroupsData: any) => {
|
||||
if (group.subGroupCount > 0) {
|
||||
return fetchSubGroups(subGroupsData);
|
||||
} else {
|
||||
return {
|
||||
id: subGroupsData.id,
|
||||
name: subGroupsData.name,
|
||||
path: subGroupsData.path,
|
||||
subGroupCount: subGroupsData.subGroupCount,
|
||||
subGroups: [],
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
path: group.path,
|
||||
subGroupCount: group.subGroupCount,
|
||||
subGroups: fullSubGroup,
|
||||
};
|
||||
};
|
||||
|
||||
const fullMainGroup = await Promise.all(dataMainGroup.map(fetchSubGroups));
|
||||
return fullMainGroup;
|
||||
}
|
||||
|
||||
export async function getGroupUser(userId: string) {
|
||||
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/groups`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${await getToken()}`,
|
||||
"content-type": `application/json`,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
return data.map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
createUser,
|
||||
listRole,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import HttpError from "../interfaces/http-error";
|
|||
import HttpStatus from "../interfaces/http-status";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import { ExpressionBuilder } from "kysely";
|
||||
import { DB } from "../generated/kysely/types";
|
||||
|
||||
export function branchRelationPermInclude(user: RequestWithUser["user"]) {
|
||||
return {
|
||||
|
|
@ -133,3 +135,47 @@ export function createPermCheck(globalAllow: (user: RequestWithUser["user"]) =>
|
|||
return branch;
|
||||
};
|
||||
}
|
||||
|
||||
export function createQueryPermissionCondition(
|
||||
globalAllow: (user: RequestWithUser["user"]) => boolean,
|
||||
opts?: { alwaysIncludeHead?: boolean },
|
||||
) {
|
||||
return (user: RequestWithUser["user"]) =>
|
||||
({ eb, exists }: ExpressionBuilder<DB, keyof DB>) =>
|
||||
exists(
|
||||
eb
|
||||
.selectFrom("Branch")
|
||||
.leftJoin("BranchUser", "BranchUser.branchId", "Branch.id")
|
||||
.leftJoin("Branch as SubBranch", "SubBranch.headOfficeId", "Branch.id")
|
||||
.leftJoin("BranchUser as SubBranchUser", "SubBranchUser.branchId", "SubBranch.id")
|
||||
.leftJoin("Branch as HeadBranch", "HeadBranch.id", "Branch.id")
|
||||
.leftJoin("BranchUser as HeadBranchUser", "HeadBranchUser.branchId", "HeadBranch.id")
|
||||
.leftJoin("Branch as SubHeadBranch", "SubHeadBranch.headOfficeId", "HeadBranch.id")
|
||||
.leftJoin(
|
||||
"BranchUser as SubHeadBranchUser",
|
||||
"SubHeadBranchUser.branchId",
|
||||
"SubHeadBranch.id",
|
||||
)
|
||||
.where((eb) => {
|
||||
const cond = [
|
||||
eb("BranchUser.userId", "=", user.sub), // NOTE: if user belong to current branch.
|
||||
];
|
||||
|
||||
if (globalAllow?.(user) || opts?.alwaysIncludeHead) {
|
||||
cond.push(
|
||||
eb("SubBranchUser.userId", "=", user.sub), // NOTE: if user belong to branch under current branch.
|
||||
);
|
||||
}
|
||||
|
||||
if (globalAllow(user)) {
|
||||
cond.push(
|
||||
eb("HeadBranchUser.userId", "=", user.sub), // NOTE: if the current branch is under head branch user belong to.
|
||||
eb("SubHeadBranchUser.userId", "=", user.sub), // NOTE: if the current branch is under the same head branch user belong to.
|
||||
);
|
||||
}
|
||||
|
||||
return eb.or(cond);
|
||||
})
|
||||
.select("Branch.id"),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import dayjs from "dayjs";
|
||||
import { CronJob } from "cron";
|
||||
|
||||
import prisma from "../db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const jobs = [
|
||||
CronJob.from({
|
||||
|
|
@ -25,6 +27,174 @@ const jobs = [
|
|||
.catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e));
|
||||
},
|
||||
}),
|
||||
CronJob.from({
|
||||
cronTime: "0 0 0 * * *",
|
||||
runOnInit: true,
|
||||
onTick: async () => {
|
||||
await prisma.notification
|
||||
.deleteMany({
|
||||
where: { createdAt: { lte: dayjs().subtract(1, "month").toDate() } },
|
||||
})
|
||||
.then(() => console.log("[INFO]: Delete expired notification, OK."))
|
||||
.catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e));
|
||||
},
|
||||
}),
|
||||
CronJob.from({
|
||||
cronTime: "0 0 0 * * *",
|
||||
runOnInit: true,
|
||||
onTick: async () => {
|
||||
const employeeExpireData = await prisma.employee.findMany({
|
||||
include: {
|
||||
employeePassport: {
|
||||
orderBy: {
|
||||
expireDate: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
},
|
||||
quotationWorker: {
|
||||
include: {
|
||||
quotation: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
employeePassport: {
|
||||
some: {
|
||||
expireDate: dayjs().add(90, "day").toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
employeeExpireData.map(async (record) => {
|
||||
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
|
||||
const expireDate = `${dayjs(record.employeePassport[0].expireDate).format("DD/MM")}/${dayjs(record.employeePassport[0].expireDate).year() + 543}`;
|
||||
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} หนังสือเดินทางจะหมดอายุในวันที่ ${expireDate}`;
|
||||
const duplicateText = await prisma.notification.findFirst({
|
||||
where: {
|
||||
detail: textDetail,
|
||||
},
|
||||
});
|
||||
|
||||
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
|
||||
title: "หนังสือเดินทางลูกจ้างหมดอายุ / Employee Passport Expire",
|
||||
detail: textDetail,
|
||||
};
|
||||
|
||||
if (record.quotationWorker && record.quotationWorker.length > 0) {
|
||||
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
|
||||
dataNotification.registeredBranchId =
|
||||
record.quotationWorker[0].quotation.registeredBranchId;
|
||||
} else {
|
||||
(dataNotification.groupReceiver = {
|
||||
create: [{ name: "sale" }, { name: "head_of_sale" }],
|
||||
}),
|
||||
(dataNotification.registeredBranchId =
|
||||
record.customerBranch.customer.registeredBranchId);
|
||||
}
|
||||
|
||||
if (!duplicateText) {
|
||||
await prisma.notification
|
||||
.create({
|
||||
data: dataNotification,
|
||||
})
|
||||
.then(() => console.log("[INFO]: Create notification employee passport expired, OK."))
|
||||
.catch((e) =>
|
||||
console.error("[ERR]: Create notification employee passport expired, FAILED.", e),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
CronJob.from({
|
||||
cronTime: "0 0 0 * * *",
|
||||
runOnInit: true,
|
||||
onTick: async () => {
|
||||
const employeeVisaData = await prisma.employee.findMany({
|
||||
include: {
|
||||
employeeVisa: {
|
||||
orderBy: {
|
||||
expireDate: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
},
|
||||
quotationWorker: {
|
||||
include: {
|
||||
quotation: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
employeeVisa: {
|
||||
some: {
|
||||
expireDate: dayjs().add(90, "day").toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
employeeVisaData.map(async (record) => {
|
||||
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
|
||||
const expireDate = `${dayjs(record.employeeVisa[0].expireDate).format("DD/MM")}/${dayjs(record.employeeVisa[0].expireDate).year() + 543}`;
|
||||
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} ข้อมูลการตรวจลงตราจะหมดอายุในวันที่ ${expireDate}`;
|
||||
const duplicateText = await prisma.notification.findFirst({
|
||||
where: {
|
||||
detail: textDetail,
|
||||
},
|
||||
});
|
||||
|
||||
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
|
||||
title: "ข้อมูลการตรวจลงตราลูกจ้างหมดอายุ / Employee Visa Expire",
|
||||
detail: textDetail,
|
||||
};
|
||||
|
||||
if (record.quotationWorker && record.quotationWorker.length > 0) {
|
||||
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
|
||||
dataNotification.registeredBranchId =
|
||||
record.quotationWorker[0].quotation.registeredBranchId;
|
||||
} else {
|
||||
(dataNotification.groupReceiver = {
|
||||
create: [{ name: "sale" }, { name: "head_of_sale" }],
|
||||
}),
|
||||
(dataNotification.registeredBranchId =
|
||||
record.customerBranch.customer.registeredBranchId);
|
||||
}
|
||||
|
||||
if (!duplicateText) {
|
||||
await prisma.notification
|
||||
.create({
|
||||
data: dataNotification,
|
||||
})
|
||||
.then(() => console.log("[INFO]: Create notification employee visa expired, OK."))
|
||||
.catch((e) =>
|
||||
console.error("[ERR]: Create notification employee visa expired, FAILED.", e),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export function initSchedule() {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ export async function setFile(path: string, exp = 6 * 60 * 60) {
|
|||
return await minio.presignedPutObject(MINIO_BUCKET, path, exp);
|
||||
}
|
||||
|
||||
export async function uploadFile(path: string, buffer: Buffer, contentType?: string) {
|
||||
await minio.putObject(MINIO_BUCKET, path, buffer, Buffer.byteLength(buffer), {
|
||||
["Content-Type"]: contentType,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteFile(path: string) {
|
||||
await minio.removeObject(MINIO_BUCKET, path, { forceDelete: true });
|
||||
}
|
||||
|
|
@ -55,71 +61,83 @@ export async function deleteFolder(path: string) {
|
|||
});
|
||||
}
|
||||
|
||||
const ROOT = ".system";
|
||||
|
||||
export const fileLocation = {
|
||||
branch: {
|
||||
line: (branchId: string) => `branch/line-qr-${branchId}`,
|
||||
bank: (branchId: string, bankId: string) => `branch/bank-qr-${branchId}-${bankId}`,
|
||||
img: (branchId: string, name?: string) => `branch/img-${branchId}/${name || ""}`,
|
||||
attachment: (branchId: string, name?: string) => `branch/attachment-${branchId}/${name || ""}`,
|
||||
line: (branchId: string) => `${ROOT}/branch/line-qr-${branchId}`,
|
||||
bank: (branchId: string, bankId: string) => `${ROOT}/branch/bank-qr-${branchId}-${bankId}`,
|
||||
img: (branchId: string, name?: string) => `${ROOT}/branch/img-${branchId}/${name || ""}`,
|
||||
attachment: (branchId: string, name?: string) =>
|
||||
`${ROOT}/branch/attachment-${branchId}/${name || ""}`,
|
||||
},
|
||||
user: {
|
||||
profile: (userId: string, name?: string) => `user/profile-image-${userId}/${name || ""}`,
|
||||
attachment: (userId: string, name?: string) => `user/attachment-${userId}/${name || ""}`,
|
||||
profile: (userId: string, name?: string) =>
|
||||
`${ROOT}/user/profile-image-${userId}/${name || ""}`,
|
||||
attachment: (userId: string, name?: string) =>
|
||||
`${ROOT}/user/attachment-${userId}/${name || ""}`,
|
||||
signature: (userId: string) => `${ROOT}/user/signature-${userId}`,
|
||||
},
|
||||
customer: {
|
||||
img: (customerId: string, name?: string) => `customer/img-${customerId}/${name || ""}`,
|
||||
img: (customerId: string, name?: string) => `${ROOT}/customer/img-${customerId}/${name || ""}`,
|
||||
},
|
||||
customerBranch: {
|
||||
attachment: (customerBranchId: string, name?: string) =>
|
||||
`customer-branch/attachment-${customerBranchId}/${name || ""}`,
|
||||
`${ROOT}/customer-branch/attachment-${customerBranchId}/${name || ""}`,
|
||||
citizen: (customerBranchId: string, citizenId?: string) =>
|
||||
`customer-branch/citizen-${customerBranchId}/${citizenId || ""}`,
|
||||
`${ROOT}/customer-branch/citizen-${customerBranchId}/${citizenId || ""}`,
|
||||
houseRegistration: (customerBranchId: string, houseRegistrationId?: string) =>
|
||||
`customer-branch/house-registration-${customerBranchId}/${houseRegistrationId || ""}`,
|
||||
`${ROOT}/customer-branch/house-registration-${customerBranchId}/${houseRegistrationId || ""}`,
|
||||
commercialRegistration: (customerBranchId: string, commercialRegistrationId?: string) =>
|
||||
`customer-branch/commercial-registration-${customerBranchId}/${commercialRegistrationId || ""}`,
|
||||
`${ROOT}/customer-branch/commercial-registration-${customerBranchId}/${commercialRegistrationId || ""}`,
|
||||
vatRegistration: (customerBranchId: string, vatRegistrationId?: string) =>
|
||||
`customer-branch/vat-registration-${customerBranchId}/${vatRegistrationId || ""}`,
|
||||
`${ROOT}/customer-branch/vat-registration-${customerBranchId}/${vatRegistrationId || ""}`,
|
||||
powerOfAttorney: (customerBranchId: string, powerOfAttorneyId?: string) =>
|
||||
`customer-branch/power-of-attorney-${customerBranchId}/${powerOfAttorneyId || ""}`,
|
||||
`${ROOT}/customer-branch/power-of-attorney-${customerBranchId}/${powerOfAttorneyId || ""}`,
|
||||
},
|
||||
employee: {
|
||||
img: (employeeId: string, name?: string) => `employee/img-${employeeId}/${name || ""}`,
|
||||
img: (employeeId: string, name?: string) => `${ROOT}/employee/img-${employeeId}/${name || ""}`,
|
||||
attachment: (employeeId: string, name?: string) =>
|
||||
`employee/attachment-${employeeId}/${name || ""}`,
|
||||
visa: (employeeId: string, visaId?: string) => `employee/visa-${employeeId}/${visaId || ""}`,
|
||||
`${ROOT}/employee/attachment-${employeeId}/${name || ""}`,
|
||||
visa: (employeeId: string, visaId?: string) =>
|
||||
`${ROOT}/employee/visa-${employeeId}/${visaId || ""}`,
|
||||
passport: (employeeId: string, passportId?: string) =>
|
||||
`employee/passport-${employeeId}/${passportId || ""}`,
|
||||
`${ROOT}/employee/passport-${employeeId}/${passportId || ""}`,
|
||||
inCountryNotice: (employeeId: string, noticeId?: string) =>
|
||||
`employee/in-country-notice-${employeeId}/${noticeId || ""}`,
|
||||
`${ROOT}/employee/in-country-notice-${employeeId}/${noticeId || ""}`,
|
||||
},
|
||||
product: {
|
||||
img: (productId: string, name?: string) => `product/img-${productId}/${name || ""}`,
|
||||
img: (productId: string, name?: string) => `${ROOT}/product/img-${productId}/${name || ""}`,
|
||||
},
|
||||
service: {
|
||||
img: (serviceId: string, name?: string) => `service/img-${serviceId}/${name || ""}`,
|
||||
img: (serviceId: string, name?: string) => `${ROOT}/service/img-${serviceId}/${name || ""}`,
|
||||
},
|
||||
quotation: {
|
||||
attachment: (quotationId: string, name?: string) =>
|
||||
`quotation/attachment-${quotationId}/${name || ""}`,
|
||||
`${ROOT}/quotation/attachment-${quotationId}/${name || ""}`,
|
||||
payment: (quotationId: string, paymentId: string, name?: string) =>
|
||||
`quotation/payment-${quotationId}/${paymentId}/${name || ""}`,
|
||||
`${ROOT}/quotation/payment-${quotationId}/${paymentId}/${name || ""}`,
|
||||
},
|
||||
request: {
|
||||
attachment: (requestId: string, step: number, name?: string) =>
|
||||
`request/attachment-${requestId}-${step}/${name || ""}`,
|
||||
`${ROOT}/request/attachment-${requestId}-${step}/${name || ""}`,
|
||||
},
|
||||
institution: {
|
||||
attachment: (institutionId: string, name?: string) =>
|
||||
`institution/attachment-${institutionId}/${name || ""}`,
|
||||
img: (institutionId: string, name?: string) => `institution/img-${institutionId}/${name || ""}`,
|
||||
`${ROOT}/institution/attachment-${institutionId}/${name || ""}`,
|
||||
img: (institutionId: string, name?: string) =>
|
||||
`${ROOT}/institution/img-${institutionId}/${name || ""}`,
|
||||
bank: (institutionId: string, bankId: string) =>
|
||||
`${ROOT}/institution/bank-qr-${institutionId}-${bankId}`,
|
||||
},
|
||||
task: {
|
||||
attachment: (taskId: string, name?: string) => `task/attachment-${taskId}/${name || ""}`,
|
||||
attachment: (taskId: string, name?: string) =>
|
||||
`${ROOT}/task/attachment-${taskId}/${name || ""}`,
|
||||
},
|
||||
creditNote: {
|
||||
slip: (creditNoteId: string, name?: string) => `credit-note/slip-${creditNoteId}/${name || ""}`,
|
||||
slip: (creditNoteId: string, name?: string) =>
|
||||
`${ROOT}/credit-note/slip-${creditNoteId}/${name || ""}`,
|
||||
attachment: (creditNoteId: string, name?: string) =>
|
||||
`credit-note/attachment-${creditNoteId}/${name || ""}`,
|
||||
`${ROOT}/credit-note/attachment-${creditNoteId}/${name || ""}`,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,26 +10,35 @@ export function connectOrDisconnect(id?: string | null) {
|
|||
|
||||
export function whereAddressQuery(query: string) {
|
||||
return [
|
||||
{ address: { contains: query } },
|
||||
{ addressEN: { contains: query } },
|
||||
{ soi: { contains: query } },
|
||||
{ soiEN: { contains: query } },
|
||||
{ moo: { contains: query } },
|
||||
{ mooEN: { contains: query } },
|
||||
{ street: { contains: query } },
|
||||
{ streetEN: { contains: query } },
|
||||
{ province: { name: { contains: query } } },
|
||||
{ province: { nameEN: { contains: query } } },
|
||||
{ district: { name: { contains: query } } },
|
||||
{ district: { nameEN: { contains: query } } },
|
||||
{ subDistrict: { name: { contains: query } } },
|
||||
{ subDistrict: { nameEN: { contains: query } } },
|
||||
{ subDistrict: { zipCode: { contains: query } } },
|
||||
];
|
||||
{ address: { contains: query, mode: "insensitive" } },
|
||||
{ addressEN: { contains: query, mode: "insensitive" } },
|
||||
{ soi: { contains: query, mode: "insensitive" } },
|
||||
{ soiEN: { contains: query, mode: "insensitive" } },
|
||||
{ moo: { contains: query, mode: "insensitive" } },
|
||||
{ mooEN: { contains: query, mode: "insensitive" } },
|
||||
{ street: { contains: query, mode: "insensitive" } },
|
||||
{ streetEN: { contains: query, mode: "insensitive" } },
|
||||
{ province: { name: { contains: query, mode: "insensitive" } } },
|
||||
{ province: { nameEN: { contains: query, mode: "insensitive" } } },
|
||||
{ district: { name: { contains: query, mode: "insensitive" } } },
|
||||
{ district: { nameEN: { contains: query, mode: "insensitive" } } },
|
||||
{ subDistrict: { name: { contains: query, mode: "insensitive" } } },
|
||||
{ subDistrict: { nameEN: { contains: query, mode: "insensitive" } } },
|
||||
{ subDistrict: { zipCode: { contains: query, mode: "insensitive" } } },
|
||||
] as const;
|
||||
}
|
||||
|
||||
export function queryOrNot<T>(query: string | boolean, where: T): T | undefined;
|
||||
export function queryOrNot<T, U>(query: string | boolean, where: T, fallback: U): T | U;
|
||||
export function queryOrNot<T, U>(query: string | boolean, where: T, fallback?: U) {
|
||||
export function queryOrNot<T>(query: any, where: T): T | undefined;
|
||||
export function queryOrNot<T, U>(query: any, where: T, fallback: U): T | U;
|
||||
export function queryOrNot<T, U>(query: any, where: T, fallback?: U) {
|
||||
return !!query ? where : fallback;
|
||||
}
|
||||
|
||||
export function whereDateQuery(startDate: Date | undefined, endDate: Date | undefined) {
|
||||
return {
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
105
src/utils/spreadsheet.ts
Normal file
105
src/utils/spreadsheet.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import Excel from "exceljs";
|
||||
|
||||
export default class spreadsheet {
|
||||
static async readCsv() {
|
||||
// TODO: read csv
|
||||
}
|
||||
|
||||
/**
|
||||
* This function read data from excel file.
|
||||
*
|
||||
* @param buffer - Excel file.
|
||||
* @param opts.header - Interprets the first row as the names of the fields.
|
||||
* @param opts.worksheet - Specifies the worksheet to read. Can be the worksheet's name or its 1-based index.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
static async readExcel<T extends unknown>(
|
||||
buffer: Excel.Buffer,
|
||||
opts?: { header?: boolean; worksheet?: number | string },
|
||||
): Promise<T[]> {
|
||||
const workbook = new Excel.Workbook();
|
||||
await workbook.xlsx.load(buffer);
|
||||
const worksheet = workbook.getWorksheet(opts?.worksheet ?? 1);
|
||||
|
||||
if (!worksheet) return [];
|
||||
|
||||
const header: Record<number, string | number> = {};
|
||||
const values: any[] = [];
|
||||
|
||||
worksheet.eachRow((row, rowId) => {
|
||||
if (rowId === 1 && opts?.header !== false) {
|
||||
row.eachCell((cell, cellId) => {
|
||||
if (typeof cell.value === "string") {
|
||||
header[cellId] = nameValue(cell.value);
|
||||
} else {
|
||||
header[cellId] = cellId.toString();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const data: Record<string | number, Excel.CellValue> = {};
|
||||
row.eachCell((cell, cellId) => {
|
||||
data[opts?.header !== false ? header[cellId] : cellId - 1] = cell.value;
|
||||
});
|
||||
values.push(opts?.header !== false ? data : Object.values(data));
|
||||
}
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
function nameValue(value: string) {
|
||||
let code: string;
|
||||
switch (value) {
|
||||
case "ชื่อสินค้าและบริการ":
|
||||
code = "name";
|
||||
break;
|
||||
case "ระยะเวลาดำเนินการ":
|
||||
code = "process";
|
||||
break;
|
||||
case "ประเภทค่าใช้จ่าย":
|
||||
code = "expenseType";
|
||||
break;
|
||||
case "รายละเอียด":
|
||||
code = "detail";
|
||||
break;
|
||||
case "หมายเหตุ":
|
||||
code = "remark";
|
||||
break;
|
||||
case "ใช้งานร่วมกัน":
|
||||
code = "shared";
|
||||
break;
|
||||
case "คำนวณภาษีราคาขาย":
|
||||
code = "calcVat";
|
||||
break;
|
||||
case "รวม VAT ราคาขาย":
|
||||
code = "vatIncluded";
|
||||
break;
|
||||
case "ราคาต่อหน่วย (บาท) ราคาขาย":
|
||||
code = "price";
|
||||
break;
|
||||
case "คำนวณภาษีราคาตัวแทน":
|
||||
code = "agentPriceCalcVat";
|
||||
break;
|
||||
case "รวม VAT ราคาตัวแทน":
|
||||
code = "agentPriceVatIncluded";
|
||||
break;
|
||||
case "ราคาต่อหน่วย (บาท) ราคาตัวแทน":
|
||||
code = "agentPrice";
|
||||
break;
|
||||
case "คำนวณภาษีราคาดำเนินการ":
|
||||
code = "serviceChargeCalcVat";
|
||||
break;
|
||||
case "รวม VAT ราคาดำเนินการ":
|
||||
code = "serviceChargeVatIncluded";
|
||||
break;
|
||||
case "ราคาต่อหน่วย (บาท) ราคาดำเนินการ":
|
||||
code = "serviceCharge";
|
||||
break;
|
||||
default:
|
||||
code = "code";
|
||||
break;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
67
src/utils/string-template.ts
Normal file
67
src/utils/string-template.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
export function formatNumberDecimal(num: number, point: number = 2): string {
|
||||
return (num || 0).toLocaleString("eng", {
|
||||
minimumFractionDigits: point,
|
||||
maximumFractionDigits: point,
|
||||
});
|
||||
}
|
||||
|
||||
const templates = {
|
||||
"quotation-labor": {
|
||||
converter: (context?: { name: string[] }) => {
|
||||
return context?.name.join("<br />") || "";
|
||||
},
|
||||
},
|
||||
"quotation-payment": {
|
||||
converter: (context?: {
|
||||
paymentType: "Full" | "Split" | "SplitCustom" | "BillFull" | "BillSplit" | "BillSplitCustom";
|
||||
|
||||
amount?: number;
|
||||
installments?: {
|
||||
no: number;
|
||||
amount: number;
|
||||
}[];
|
||||
}) => {
|
||||
if (context?.paymentType === "Full") {
|
||||
return [
|
||||
"**** เงื่อนไขเพิ่มเติม",
|
||||
"- เงื่อนไขการชำระเงิน แบบเต็มจำนวน",
|
||||
` จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
|
||||
].join("<br/>");
|
||||
} else {
|
||||
return [
|
||||
"**** เงื่อนไขเพิ่มเติม",
|
||||
`- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === "SplitCustom" ? " กำหนดเอง " : " "}${context?.installments?.length} งวด`,
|
||||
...(context?.installments?.map(
|
||||
(v) => ` งวดที่ ${v.no} จำนวน ${formatNumberDecimal(v.amount, 2)}`,
|
||||
) || []),
|
||||
].join("<br />");
|
||||
}
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
type Template = typeof templates;
|
||||
type TemplateName = keyof Template;
|
||||
type TemplateContext = {
|
||||
[key in TemplateName]?: Parameters<Template[key]["converter"]>[0];
|
||||
};
|
||||
|
||||
export function convertTemplate(
|
||||
text: string,
|
||||
context?: TemplateContext,
|
||||
templateUse?: TemplateName[],
|
||||
) {
|
||||
let ret = text;
|
||||
|
||||
for (const [name, template] of Object.entries(templates)) {
|
||||
if (templateUse && !templateUse.includes(name as TemplateName)) continue;
|
||||
ret = ret.replace(
|
||||
new RegExp("\\#\\[" + name.replaceAll("-", "\\-") + "\\]", "g"),
|
||||
typeof template.converter === "function"
|
||||
? template.converter(context?.[name as TemplateName] as any)
|
||||
: template.converter,
|
||||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -62,85 +62,90 @@ export async function initThailandAreaDatabase() {
|
|||
return result;
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const meta = {
|
||||
createdBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedBy: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const meta = {
|
||||
createdBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedBy: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
splitChunk(province, 1000, async (r) => {
|
||||
return await tx.$kysely
|
||||
.insertInto("Province")
|
||||
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
|
||||
.values(r.map((v) => ({ ...v, ...meta })))
|
||||
.onConflict((oc) =>
|
||||
oc.column("id").doUpdateSet({
|
||||
name: (eb) => eb.ref("excluded.name"),
|
||||
nameEN: (eb) => eb.ref("excluded.nameEN"),
|
||||
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}),
|
||||
);
|
||||
await Promise.all(
|
||||
splitChunk(province, 1000, async (r) => {
|
||||
return await tx.$kysely
|
||||
.insertInto("Province")
|
||||
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
|
||||
.values(r.map((v) => ({ ...v, ...meta })))
|
||||
.onConflict((oc) =>
|
||||
oc.column("id").doUpdateSet({
|
||||
name: (eb) => eb.ref("excluded.name"),
|
||||
nameEN: (eb) => eb.ref("excluded.nameEN"),
|
||||
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
splitChunk(district, 2000, async (r) => {
|
||||
return await tx.$kysely
|
||||
.insertInto("District")
|
||||
.columns([
|
||||
"id",
|
||||
"name",
|
||||
"nameEN",
|
||||
"provinceId",
|
||||
"createdBy",
|
||||
"createdAt",
|
||||
"updatedBy",
|
||||
"updatedAt",
|
||||
])
|
||||
.values(r.map((v) => ({ ...v, ...meta })))
|
||||
.onConflict((oc) =>
|
||||
oc.column("id").doUpdateSet({
|
||||
name: (eb) => eb.ref("excluded.name"),
|
||||
nameEN: (eb) => eb.ref("excluded.nameEN"),
|
||||
provinceId: (eb) => eb.ref("excluded.provinceId"),
|
||||
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}),
|
||||
);
|
||||
await Promise.all(
|
||||
splitChunk(district, 2000, async (r) => {
|
||||
return await tx.$kysely
|
||||
.insertInto("District")
|
||||
.columns([
|
||||
"id",
|
||||
"name",
|
||||
"nameEN",
|
||||
"provinceId",
|
||||
"createdBy",
|
||||
"createdAt",
|
||||
"updatedBy",
|
||||
"updatedAt",
|
||||
])
|
||||
.values(r.map((v) => ({ ...v, ...meta })))
|
||||
.onConflict((oc) =>
|
||||
oc.column("id").doUpdateSet({
|
||||
name: (eb) => eb.ref("excluded.name"),
|
||||
nameEN: (eb) => eb.ref("excluded.nameEN"),
|
||||
provinceId: (eb) => eb.ref("excluded.provinceId"),
|
||||
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
splitChunk(subDistrict, 1000, async (r) => {
|
||||
return await tx.$kysely
|
||||
.insertInto("SubDistrict")
|
||||
.columns([
|
||||
"id",
|
||||
"name",
|
||||
"nameEN",
|
||||
"districtId",
|
||||
"createdBy",
|
||||
"createdAt",
|
||||
"updatedBy",
|
||||
"updatedAt",
|
||||
])
|
||||
.values(r.map((v) => ({ ...v, ...meta })))
|
||||
.onConflict((oc) =>
|
||||
oc.column("id").doUpdateSet({
|
||||
name: (eb) => eb.ref("excluded.name"),
|
||||
nameEN: (eb) => eb.ref("excluded.nameEN"),
|
||||
districtId: (eb) => eb.ref("excluded.districtId"),
|
||||
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}),
|
||||
);
|
||||
});
|
||||
await Promise.all(
|
||||
splitChunk(subDistrict, 1000, async (r) => {
|
||||
return await tx.$kysely
|
||||
.insertInto("SubDistrict")
|
||||
.columns([
|
||||
"id",
|
||||
"name",
|
||||
"nameEN",
|
||||
"districtId",
|
||||
"createdBy",
|
||||
"createdAt",
|
||||
"updatedBy",
|
||||
"updatedAt",
|
||||
])
|
||||
.values(r.map((v) => ({ ...v, ...meta })))
|
||||
.onConflict((oc) =>
|
||||
oc.column("id").doUpdateSet({
|
||||
name: (eb) => eb.ref("excluded.name"),
|
||||
nameEN: (eb) => eb.ref("excluded.nameEN"),
|
||||
districtId: (eb) => eb.ref("excluded.districtId"),
|
||||
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
timeout: 15_000,
|
||||
},
|
||||
);
|
||||
|
||||
console.log("[INFO]: Sync thailand province, district and subdistrict, OK.");
|
||||
}
|
||||
|
|
@ -170,67 +175,72 @@ export async function initEmploymentOffice() {
|
|||
|
||||
const list = await prisma.province.findMany();
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await Promise.all(
|
||||
list
|
||||
.map(async (province) => {
|
||||
if (special[province.id]) {
|
||||
await tx.employmentOffice.deleteMany({
|
||||
where: { provinceId: province.id, district: { none: {} } },
|
||||
});
|
||||
return await Promise.all(
|
||||
Object.entries(special[province.id]).map(async ([key, val]) => {
|
||||
const id = province.id + "-" + key.padStart(2, "0");
|
||||
return tx.employmentOffice.upsert({
|
||||
where: { id },
|
||||
create: {
|
||||
id,
|
||||
name: nameSpecial(province.name, +key),
|
||||
nameEN: nameSpecialEN(province.nameEN, +key),
|
||||
provinceId: province.id,
|
||||
district: {
|
||||
createMany: {
|
||||
data: val.map((districtId) => ({ districtId })),
|
||||
skipDuplicates: true,
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await Promise.all(
|
||||
list
|
||||
.map(async (province) => {
|
||||
if (special[province.id]) {
|
||||
await tx.employmentOffice.deleteMany({
|
||||
where: { provinceId: province.id, district: { none: {} } },
|
||||
});
|
||||
return await Promise.all(
|
||||
Object.entries(special[province.id]).map(async ([key, val]) => {
|
||||
const id = province.id + "-" + key.padStart(2, "0");
|
||||
return tx.employmentOffice.upsert({
|
||||
where: { id },
|
||||
create: {
|
||||
id,
|
||||
name: nameSpecial(province.name, +key),
|
||||
nameEN: nameSpecialEN(province.nameEN, +key),
|
||||
provinceId: province.id,
|
||||
district: {
|
||||
createMany: {
|
||||
data: val.map((districtId) => ({ districtId })),
|
||||
skipDuplicates: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id,
|
||||
name: nameSpecial(province.name, +key),
|
||||
nameEN: nameSpecialEN(province.nameEN, +key),
|
||||
provinceId: province.id,
|
||||
district: {
|
||||
deleteMany: { districtId: { notIn: val } },
|
||||
createMany: {
|
||||
data: val.map((districtId) => ({ districtId })),
|
||||
skipDuplicates: true,
|
||||
update: {
|
||||
id,
|
||||
name: nameSpecial(province.name, +key),
|
||||
nameEN: nameSpecialEN(province.nameEN, +key),
|
||||
provinceId: province.id,
|
||||
district: {
|
||||
deleteMany: { districtId: { notIn: val } },
|
||||
createMany: {
|
||||
data: val.map((districtId) => ({ districtId })),
|
||||
skipDuplicates: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return tx.employmentOffice.upsert({
|
||||
where: { id: province.id },
|
||||
create: {
|
||||
id: province.id,
|
||||
name: name(province.name),
|
||||
nameEN: nameEN(province.nameEN),
|
||||
provinceId: province.id,
|
||||
},
|
||||
update: {
|
||||
name: name(province.name),
|
||||
nameEN: nameEN(province.nameEN),
|
||||
provinceId: province.id,
|
||||
},
|
||||
});
|
||||
})
|
||||
.flat(),
|
||||
);
|
||||
});
|
||||
return tx.employmentOffice.upsert({
|
||||
where: { id: province.id },
|
||||
create: {
|
||||
id: province.id,
|
||||
name: name(province.name),
|
||||
nameEN: nameEN(province.nameEN),
|
||||
provinceId: province.id,
|
||||
},
|
||||
update: {
|
||||
name: name(province.name),
|
||||
nameEN: nameEN(province.nameEN),
|
||||
provinceId: province.id,
|
||||
},
|
||||
});
|
||||
})
|
||||
.flat(),
|
||||
);
|
||||
},
|
||||
{
|
||||
timeout: 15_000,
|
||||
},
|
||||
);
|
||||
|
||||
console.log("[INFO]: Sync employment office, OK.");
|
||||
}
|
||||
|
|
|
|||
323
test/branch.test.ts
Normal file
323
test/branch.test.ts
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from "vitest";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { isDateString } from "./lib";
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
datasourceUrl: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL,
|
||||
});
|
||||
const baseUrl = process.env.TEST_BASE_URL || "http://localhost";
|
||||
const record: Record<string, any> = {
|
||||
code: "CMT",
|
||||
taxNo: "1052299402851",
|
||||
name: "Chamomind",
|
||||
nameEN: "Chamomind",
|
||||
email: "contact@chamomind.com",
|
||||
lineId: "@chamomind",
|
||||
telephoneNo: "0988929248",
|
||||
contactName: "John",
|
||||
webUrl: "https://chamomind.com",
|
||||
latitude: "",
|
||||
longitude: "",
|
||||
virtual: false,
|
||||
permitNo: "1135182804792",
|
||||
permitIssueDate: "2025-01-01T00:00:00.000Z",
|
||||
permitExpireDate: "2030-01-01T00:00:00.000Z",
|
||||
address: "11/3",
|
||||
addressEN: "11/3",
|
||||
soi: "1",
|
||||
soiEN: "1",
|
||||
moo: "2",
|
||||
mooEN: "2",
|
||||
street: "Straight",
|
||||
streetEN: "Straight",
|
||||
provinceId: "50",
|
||||
districtId: "5001",
|
||||
subDistrictId: "500107",
|
||||
};
|
||||
const recordList: Record<string, any>[] = [];
|
||||
|
||||
let token: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const body = new URLSearchParams();
|
||||
|
||||
body.append("grant_type", "password");
|
||||
body.append("client_id", "app");
|
||||
body.append("username", process.env.TEST_USERNAME || "");
|
||||
body.append("password", process.env.TEST_PASSWORD || "");
|
||||
body.append("scope", "openid");
|
||||
|
||||
const res = await fetch(
|
||||
process.env.KC_URL + "/realms/" + process.env.KC_REALM + "/protocol/openid-connect/token",
|
||||
{
|
||||
method: "POST",
|
||||
body: body,
|
||||
},
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
await res.json().then((data) => {
|
||||
token = data["access_token"];
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!record["id"]) return;
|
||||
|
||||
await prisma.branch.deleteMany({
|
||||
where: { id: { in: [record, ...recordList].map((v) => v["id"]) } },
|
||||
});
|
||||
await prisma.runningNo.deleteMany({
|
||||
where: {
|
||||
key: { in: [record, ...recordList].map((v) => `MAIN_BRANCH_${v["code"].slice(0, -5)}`) },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch management", () => {
|
||||
it("create branch without required fields", async () => {
|
||||
const requiredFields = [
|
||||
"taxNo",
|
||||
"name",
|
||||
"nameEN",
|
||||
"permitNo",
|
||||
"telephoneNo",
|
||||
"address",
|
||||
"addressEN",
|
||||
"email",
|
||||
];
|
||||
onTestFailed(() => console.log("Field:", requiredFields, "is required."));
|
||||
|
||||
for await (const field of requiredFields) {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
["Content-Type"]: "application/json",
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
body: JSON.stringify({ ...record, [field]: undefined }),
|
||||
});
|
||||
|
||||
if (res.ok) recordList.push(await res.json());
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.status).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
it("create branch", async () => {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
["Content-Type"]: "application/json",
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
body: JSON.stringify(record),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
console.log(JSON.parse(text));
|
||||
} catch (e) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
record["id"] = data["id"]; // This field is auto generated
|
||||
record["code"] = data["code"]; // This field is auto generated
|
||||
|
||||
recordList.push(data);
|
||||
|
||||
expect(data).toMatchObject(record);
|
||||
});
|
||||
|
||||
it("get branch list", async () => {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
console.log(JSON.parse(text));
|
||||
} catch (e) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
expect(data).toHaveProperty("result");
|
||||
expect(data).toHaveProperty("total");
|
||||
expect(data).toHaveProperty("page");
|
||||
expect(data).toHaveProperty("pageSize");
|
||||
expect(data.result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
code: expect.any(String),
|
||||
virtual: expect.any(Boolean),
|
||||
name: expect.any(String),
|
||||
nameEN: expect.any(String),
|
||||
email: expect.any(String),
|
||||
taxNo: expect.any(String),
|
||||
telephoneNo: expect.any(String),
|
||||
latitude: expect.any(String),
|
||||
longitude: expect.any(String),
|
||||
contactName: expect.toBeOneOf([expect.any(String), null]),
|
||||
lineId: expect.toBeOneOf([expect.any(String), null]),
|
||||
webUrl: expect.toBeOneOf([expect.any(String), null]),
|
||||
remark: expect.toBeOneOf([expect.any(String), null]),
|
||||
selectedImage: expect.toBeOneOf([expect.any(String), null]),
|
||||
|
||||
isHeadOffice: expect.any(Boolean),
|
||||
|
||||
permitNo: expect.any(String),
|
||||
permitIssueDate: expect.toSatisfy(isDateString(true)),
|
||||
permitExpireDate: expect.toSatisfy(isDateString(true)),
|
||||
|
||||
address: expect.any(String),
|
||||
addressEN: expect.any(String),
|
||||
moo: expect.toBeOneOf([expect.any(String), null]),
|
||||
mooEN: expect.toBeOneOf([expect.any(String), null]),
|
||||
street: expect.toBeOneOf([expect.any(String), null]),
|
||||
streetEN: expect.toBeOneOf([expect.any(String), null]),
|
||||
provinceId: expect.any(String),
|
||||
province: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
nameEN: expect.any(String),
|
||||
}),
|
||||
districtId: expect.any(String),
|
||||
district: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
nameEN: expect.any(String),
|
||||
}),
|
||||
subDistrictId: expect.any(String),
|
||||
subDistrict: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
nameEN: expect.any(String),
|
||||
zipCode: expect.any(String),
|
||||
}),
|
||||
|
||||
status: expect.toBeOneOf(["CREATED", "ACTIVE", "INACTIVE"]),
|
||||
statusOrder: expect.toBeOneOf([1, 0]),
|
||||
|
||||
createdAt: expect.toSatisfy(isDateString()),
|
||||
createdByUserId: expect.toBeOneOf([expect.any(String), null]),
|
||||
createdBy: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
username: expect.any(String),
|
||||
firstName: expect.any(String),
|
||||
lastName: expect.any(String),
|
||||
firstNameEN: expect.any(String),
|
||||
lastNameEN: expect.any(String),
|
||||
}),
|
||||
updatedAt: expect.toSatisfy(isDateString()),
|
||||
updatedByUserId: expect.toBeOneOf([expect.any(String), null]),
|
||||
updatedBy: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
username: expect.any(String),
|
||||
firstName: expect.any(String),
|
||||
lastName: expect.any(String),
|
||||
firstNameEN: expect.any(String),
|
||||
lastNameEN: expect.any(String),
|
||||
}),
|
||||
|
||||
_count: expect.objectContaining({
|
||||
branch: expect.any(Number),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("get branch by id", async () => {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
|
||||
method: "GET",
|
||||
headers: {
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
console.log(JSON.parse(text));
|
||||
} catch (e) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
expect(data).toMatchObject(record);
|
||||
});
|
||||
|
||||
it("update branch by id", async () => {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
["Content-Type"]: "application/json",
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
body: JSON.stringify({ name: "Chamomind Intl.", nameEN: "Chamomind Intl." }),
|
||||
});
|
||||
|
||||
record["name"] = "Chamomind Intl.";
|
||||
record["nameEN"] = "Chamomind Intl.";
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
expect(data).toMatchObject(record);
|
||||
});
|
||||
|
||||
it("delete branch by id", async () => {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
["Content-Type"]: "application/json",
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
console.log(JSON.parse(text));
|
||||
} catch (e) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
expect(data).toMatchObject(record);
|
||||
});
|
||||
|
||||
it("get deleted branch by id", async () => {
|
||||
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
|
||||
method: "GET",
|
||||
headers: {
|
||||
["Authorization"]: "Bearer " + token,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
10
test/lib/index.ts
Normal file
10
test/lib/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export function isDateString(nullable: boolean = false): (val: any) => boolean {
|
||||
return (value: any) => {
|
||||
try {
|
||||
if (value) return !!new Date(value);
|
||||
return nullable;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@
|
|||
{ "name": "Employee Other Info" },
|
||||
{ "name": "Institution" },
|
||||
{ "name": "Workflow" },
|
||||
{ "name": "Property" },
|
||||
{ "name": "Product Group" },
|
||||
{ "name": "Product" },
|
||||
{ "name": "Work" },
|
||||
|
|
@ -53,7 +54,9 @@
|
|||
{ "name": "Task Order" },
|
||||
{ "name": "User Task Order" },
|
||||
{ "name": "Credit Note" },
|
||||
{ "name": "Debit Note" }
|
||||
{ "name": "Debit Note" },
|
||||
{ "name": "Report" },
|
||||
{ "name": "Document Template" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
5
vite.config.ts
Normal file
5
vite.config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue