Compare commits

...

208 commits

Author SHA1 Message Date
HAM
426ffb27a7 fix: update Dockerfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 15:05:21 +07:00
HAM
5d3997343f fix: Dockerfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 14:52:16 +07:00
HAM
a10626e756 update: entrypoint.sh
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 14:49:14 +07:00
HAM
c11fed9832 fix: lock prisma to v6 and fix lockfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 14:10:11 +07:00
HAM
8909a763c9 chore: remove unuse file
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 13:51:54 +07:00
HAM
f0a106e5fe fix: use prisma v6 config
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 13:48:10 +07:00
HAM
84b9ddcd2b fix: prisma v7 config in schema.prisma
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 13:41:00 +07:00
HAM
e54f62a5b3 fix: prisma v7 config
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 13:39:04 +07:00
HAM
cef26278ba fix: missing prisma query take and skip
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 15s
2026-01-12 13:15:31 +07:00
net
16c4c64c89 refactor: handle code error
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-10-14 17:12:00 +07:00
HAM
78669ed7ae feat: check invalid data for create taskOrder
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-10-14 16:19:37 +07:00
HAM
5dc88c22dc feat: delete created data when flow account create fail one
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-09-29 09:44:41 +07:00
Methapon2001
3454e46212 feat: flowaccount handle installments
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-19 16:17:56 +07:00
HAM
334fb57b46 refactor: flowaccount move product code to front for product name
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-19 16:09:21 +07:00
HAM
a9201f715a fix: missing product code, buyPrice when create and edit and add (ราคาตัวแทน) for agentPrice product name
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-19 15:35:19 +07:00
Methapon2001
95ee32fc57 feat: response branch api
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-19 11:13:42 +07:00
Methapon2001
a33983c530 fix: transaction ended before create
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-19 09:11:21 +07:00
Methapon2001
e8278b6af3 Merge branch 'develop' 2025-09-18 17:12:16 +07:00
Methapon2001
9ef006c860 refactor: handle flow account installments
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-18 17:12:02 +07:00
Methapon2001
8d52a5e726 Merge branch 'develop' 2025-09-18 17:08:13 +07:00
Methapon2001
7858291ae5 fix: receipt flow account
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-18 17:04:40 +07:00
Methapon2001
6ea672a2cb Merge branch 'develop' 2025-09-18 16:56:18 +07:00
Methapon2001
a426e18025 fix: wrong number
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-18 16:55:34 +07:00
Methapon2001
0930c3c833 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-18 10:15:36 +07:00
Methapon2001
be3c6405c6 feat: export product
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-09-18 10:15:32 +07:00
HAM
f50285161b fix: variable name for filter
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-18 10:13:39 +07:00
Methapon2001
4691d559f5 Merge branch 'develop' 2025-09-18 09:50:52 +07:00
HAM
7e7b8025c9 refactor: change to id
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-17 12:58:14 +07:00
HAM
e5a3d948a5 fix: missing query
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-17 12:39:10 +07:00
HAM
0d78ce4db3 feat: filter businessType, province, district, subDistrict for customer and customer-export endpoint 2025-09-17 12:37:41 +07:00
Methapon2001
9e74fb0fe6 Merge branch 'develop' 2025-09-17 12:01:55 +07:00
Methapon2001
ab4ea4ba4b fix: type error
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-17 11:55:06 +07:00
Methapon2001
068ba2d293 fix: wrong tax
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-17 11:53:15 +07:00
Methapon2001
beb7f4bcfe fix: flowaccount contact name
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-17 11:34:54 +07:00
Methapon2001
b6b35509e3 Merge branch 'develop' 2025-09-17 10:42:40 +07:00
Methapon2001
6598cd3bdf fix: error field ambigous 2025-09-17 10:42:35 +07:00
Methapon2001
2c790de606 fix: customer status not change after used 2025-09-17 09:51:35 +07:00
Methapon2001
6f1bca5234 Merge branch 'develop' 2025-09-16 13:20:30 +07:00
Methapon2001
158a6ff163 fix: timeout in some case
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 11:52:13 +07:00
Methapon2001
0772e4710a fix: cannot update payment data after set payment completed
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 11:44:13 +07:00
Methapon2001
25a4b50f8e fix: wrong condition
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 10:11:02 +07:00
Methapon2001
de33d03631 fix: calculate price
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 10:08:01 +07:00
HAM
4e71343af7 refactor: use function for correct decimal number
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-16 09:53:03 +07:00
HAM
6776188f7b fix: decimal number
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 09:50:23 +07:00
Methapon2001
892d76583f chore: flowaccount enable inline vat
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 09:48:59 +07:00
Methapon2001
f2def1b962 fix: wrong calc vat condition
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-16 09:40:31 +07:00
Methapon2001
1486ce79ab fix: calculate credit note price 2025-09-16 09:40:31 +07:00
HAM
d51531cd4d chore: clean
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-12 15:29:52 +07:00
HAM
61825309d1 feat: sync data from data to flowAccount
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-12 15:20:20 +07:00
HAM
250bbca226 feat: create product for flowAccount service 2025-09-12 15:19:51 +07:00
HAM
c774e9f44c chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-12 11:56:58 +07:00
HAM
8f2810ea29 feat: add flowAccountProductId field 2025-09-12 11:56:40 +07:00
Methapon2001
eda0edbd29 feat: send remark to flowaccount
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-11 17:23:21 +07:00
Methapon2001
86db927efe fix: price calc
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-11 13:57:23 +07:00
Methapon2001
5674a18cc3 fix: price calc match with flow account
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-09-11 11:48:17 +07:00
Methapon2001
d3c5b49649 chore: update prisma
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-11 09:32:38 +07:00
Methapon2001
c47ffb5435 chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-09-11 09:17:52 +07:00
Methapon2001
710382d544 feat: add more metadata for payment 2025-09-11 09:17:23 +07:00
Methapon2001
4042cbcea4 chore: add html to text dep
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-09-11 09:07:30 +07:00
Methapon2001
f487a9169c fix: prevent line user id and otp exposes 2025-09-10 11:44:22 +07:00
Methapon2001
ab8fd2ca43 feat: export customer and employee as csv 2025-09-10 11:44:22 +07:00
Methapon2001
d95eb349ec fix: update messenger also update work step messenger 2025-09-10 11:44:22 +07:00
Methapon2001
893eb4cca5 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-08-22 09:42:03 +07:00
Methapon2001
2c9fae400c fix: permission employee
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-08-22 09:41:43 +07:00
Methapon2001
df38eebbcc fix: permission
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-08-22 09:33:49 +07:00
Methapon2001
c2eaa5fba8 chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-08-14 13:10:49 +07:00
Methapon2001
1789fe1de0 feat: add updated at to step status 2025-08-14 13:10:44 +07:00
Methapon2001
a0bb23e1e8 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-08-04 09:02:06 +07:00
Methapon2001
c9939bf8bb fix: stats will only take non canceled status
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-08-04 09:01:56 +07:00
Methapon2001
f162081370 fix: dashboard payment now taken cancled into account
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-08-01 10:58:04 +07:00
Methapon2001
dfe7bd16d8 fix: error trying to create relation in create many
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-29 10:18:33 +07:00
Methapon2001
bfc2608af4 fix: notification not show because of missing registered branch
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-07-29 09:44:25 +07:00
Methapon2001
23334a4388 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-07-25 09:46:27 +07:00
Methapon2001
9e208dee89 feat: notify document_checker when task is canceled
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-25 09:42:22 +07:00
Methapon2001
a30bc33b81 fix: create response
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-07-22 14:38:29 +07:00
Methapon2001
c7183887c9 fix: error create many with relation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-22 10:35:48 +07:00
Methapon2001
c34af75bad Merge branch 'fix/line-alert' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-21 17:59:21 +07:00
Kanjana
5de1f27fca feat: add alert RequestData and CreditNote
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-21 17:57:00 +07:00
Methapon2001
8a87e37097 fix: search and pendingOnly get mixed
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-07-21 13:36:40 +07:00
Methapon2001
92762c512d Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-21 11:17:32 +07:00
Methapon2001
3455ae604a feat: more notification related to task and request
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-21 11:17:23 +07:00
Methapon2001
afb89ef949 feat: notify document check when status changed
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-07-21 10:38:31 +07:00
Methapon2001
fa8aa8c9b6 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-17 08:58:02 +07:00
Methapon2001
5c824a738a feat: notify more group when create new quotation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 3s
2025-07-16 12:59:15 +07:00
Methapon2001
e4caeaa780 fix: new request does not trigger noti
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-16 12:54:52 +07:00
Methapon2001
46ba857e28 fix: missing type
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-16 11:38:11 +07:00
Methapon2001
b7a13b2d7a feat: handle foreign address
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-16 09:51:27 +07:00
Methapon2001
5aa8b06cf2 chore: migration 2025-07-16 09:44:50 +07:00
Methapon2001
f90ee41a56 feat: add address text en 2025-07-16 09:44:46 +07:00
Methapon2001
2ee0e97953 chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-16 09:38:34 +07:00
Methapon2001
236ee48eab feat: foreign address 2025-07-16 09:38:30 +07:00
Methapon2001
6c350b12ce fix: prisma client error
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-15 15:17:53 +07:00
Methapon2001
0c53ac69b0 fix: auth error
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-14 11:13:33 +07:00
Methapon2001
0032ff4658 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-11 13:36:52 +07:00
Methapon2001
1f34ea7ecb feat: include business relation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-11 11:13:18 +07:00
Methapon2001
ccee309268 feat: deprecated business type helper
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-09 17:46:27 +07:00
Methapon2001
1cf53c91aa feat: doc template now include business type relation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 17:45:14 +07:00
Kanjana
36c9c25f61 Merge remote-tracking branch 'chamomind/feat/20250709' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 17:41:31 +07:00
Kanjana
1f63089363 fest: add page
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 17:40:24 +07:00
Methapon2001
c2195d4448 Merge branch 'feat/20250709' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 17:35:59 +07:00
Kanjana
7112a545b1 feat: add crud businessType
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 17:31:46 +07:00
Kanjana
15e0e34a47 feat: change listAllowed
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 17:26:49 +07:00
Methapon2001
126e00baf1 fix: stats does not account for permission
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 16:34:01 +07:00
Kanjana
163f07758e Merge branch 'feat/20250709' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 16:12:58 +07:00
Kanjana
73fea9d9ed feat: add businessType
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 16:12:13 +07:00
Kanjana
50fca4d540 feat: remove customerName add reportDate
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 16:02:45 +07:00
Kanjana
a61bd8c83e chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 15:21:00 +07:00
Methapon2001
8fb28ec3ab fix: perm
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 11:40:39 +07:00
Methapon2001
ab1d5f1326 Merge branch 'develop' 2025-07-09 10:15:04 +07:00
Methapon2001
e74516ce3b fix: allow anyone in company to see template
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-09 09:59:13 +07:00
Methapon2001
f9c4d579c4 feat: allow manage role to update any product under same head
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-08 17:17:07 +07:00
Methapon2001
842d81026e feat: allow anybody to edit customer data if can manage 2025-07-08 17:04:35 +07:00
Methapon2001
86085a74ba Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-08 10:00:03 +07:00
Kanjana
3d98f9d0ad feat: add null in update dateOfBirth
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-07 17:55:32 +07:00
Kanjana
8d25dda326 feat: change where sellerId
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-07-07 13:50:28 +07:00
Methapon2001
138031f662 fix: error not found
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-07-04 15:02:44 +07:00
Kanjana
e0be1f6ab5 feat: add sellerId in quotation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-04 14:40:46 +07:00
Kanjana
859a1e3def chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-04 14:08:54 +07:00
Methapon2001
2b255ff355 feat: do not allow sale to delete data
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-04 13:31:04 +07:00
Methapon2001
ced55b9518 feat: allow sale to manage 2025-07-04 13:31:04 +07:00
Methapon2001
1e0f97cdef feat(perm): allow anyone to edit owned data
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-03 16:20:23 +07:00
Methapon2001
5c7db2afc6 feat(perm): update api quotation perm
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-07-03 14:41:38 +07:00
Methapon2001
d6212e9ba4 Merge branch 'feat/permission' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-03 14:33:21 +07:00
Methapon2001
68025aad08 feat(perm): update api account related permission
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-03 14:33:17 +07:00
Methapon2001
d08327afb6 feat(perm): update api task permission 2025-07-03 14:33:17 +07:00
Methapon2001
afb725fceb feat(perm): update api product/service permission 2025-07-03 14:33:17 +07:00
Methapon2001
b0e941085e feat(perm): update api institue permission 2025-07-03 14:33:17 +07:00
Methapon2001
6d44d2979b feat(perm): update api flow template permission 2025-07-03 14:33:17 +07:00
Methapon2001
fa95fe46a5 feat(perm): update api customer/employee perm 2025-07-03 14:33:17 +07:00
Methapon2001
41f5de7fd0 feat(perm): update api user perm 2025-07-03 14:33:17 +07:00
Methapon2001
425e99bfde feat(perm): update api branch perm 2025-07-03 14:33:17 +07:00
Methapon2001
15381c089c fix: employee stats not working as expected
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-07-03 14:33:10 +07:00
Methapon2001
f7ec18fd7e Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-06-30 16:14:38 +07:00
Methapon2001
acd6bb35e9 chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-06-30 16:14:36 +07:00
Methapon2001
1a33080ac8 fix: relation constraint
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-06-30 16:13:57 +07:00
Methapon2001
b4470d9a0a Merge branch 'develop' 2025-06-30 15:38:22 +07:00
Methapon2001
a69962db48 fix: error validation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-06-30 15:38:18 +07:00
Methapon2001
96bdb86c73 Merge branch 'develop' 2025-06-30 14:05:46 +07:00
Methapon2001
7bd685ea96 fix: relation order get by id
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-06-30 14:05:41 +07:00
Methapon2001
5262ad2a63 fix: branch order
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-06-30 13:53:07 +07:00
Methapon2001
c430fc3c7a fix: missing zip code when issue doc
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-06-11 10:25:19 +07:00
Methapon2001
47907f61ab Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-30 13:07:33 +07:00
Kanjana
e02a29f053 fix : add type null in otherNationality
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-30 13:05:33 +07:00
Kanjana
1fe013d69d Merge commit '1896e2385d' into develop 2025-05-30 13:04:38 +07:00
Methapon2001
1896e2385d feat(test): add api branch test 2025-05-23 17:12:06 +07:00
Methapon2001
f7c81641b2 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-05-22 08:52:41 +07:00
Kanjana
791e8b4977 feat : add receiverId in notification Task Failed
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-15 13:29:12 +07:00
Methapon2001
125f708ac6 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-15 09:12:27 +07:00
Kanjana
0aa20d3728 change detail notification task
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-14 14:18:08 +07:00
Kanjana
897ef335b4 add notification expireDate passport, visa
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-05-14 09:28:57 +07:00
Methapon2001
0affb5337f fix: barcode font no display correctly
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-13 14:31:37 +07:00
Methapon2001
b276ccddd1 Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-05-13 10:06:51 +07:00
Methapon2001
2d0d977617 fix: wrong path
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 8s
2025-05-13 10:03:41 +07:00
Methapon2001
feda84de1c Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-05-07 10:54:41 +07:00
Methapon2001
106343d33d feat: generate barcode
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-05-07 10:54:22 +07:00
Kanjana
2fa50bd7de add value : null in user
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-30 15:36:13 +07:00
Methapon2001
07e5f53be2 fix: request data notification when new 2025-04-30 15:00:43 +07:00
Methapon2001
8a4317c94e Merge branch 'develop' 2025-04-28 16:07:47 +07:00
Methapon2001
f1a774f3bc fix: nullable
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-04-28 16:07:13 +07:00
Kanjana
ce42a6dca6 add null otherNationality
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-28 13:43:20 +07:00
Methapon2001
8a3a9e7eb3 Merge branch 'develop' 2025-04-25 17:30:16 +07:00
Kanjana
7fe0512a2f add troubleshooting controller and field otherNationality
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-25 11:59:46 +07:00
Kanjana
5c75c27470 Merge remote-tracking branch 'origin/develop' into develop 2025-04-25 10:52:29 +07:00
Methapon2001
7bc12f00b0 chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-24 17:55:21 +07:00
Methapon2001
ffb1ce2d40 feat: allow multiple import nationality for user 2025-04-24 17:55:18 +07:00
Kanjana
5536331984 Merge remote-tracking branch 'chamomind/develop' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-24 15:11:20 +07:00
Kanjana
a57e8d939f async await in getGroup 2025-04-24 15:10:42 +07:00
Methapon2001
92104c05cb feat: filter responsible only
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-04-24 14:53:04 +07:00
Methapon2001
4dbe89f290 feat: response more relation
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-24 14:09:57 +07:00
Methapon2001
08b9ddd2e1 feat: reponse relation responsible gropu in request list
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-24 14:07:30 +07:00
Methapon2001
d92e3bc57d feat: add relation to response
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-24 13:55:22 +07:00
Kanjana
5594fabb6a Merge remote-tracking branch 'origin/develop' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-04-24 11:40:36 +07:00
Kanjana
109494c6d7 add responsibleGroup in step 2025-04-24 11:40:02 +07:00
Methapon2001
afadea2d64 fix: search not work as expected
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-24 10:50:01 +07:00
Kanjana
1d6224da73 add query in keycloak
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-24 10:41:22 +07:00
Methapon2001
d15aa488c1 feat: fallback to 0 if empty
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-04-23 09:03:03 +07:00
Methapon2001
601deffce4 feat: check for type before try parse field 2025-04-23 09:01:38 +07:00
Kanjana
3193403f90 check price where null
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2025-04-22 16:27:13 +07:00
Kanjana
8b26f91dba getGroup
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-22 15:12:24 +07:00
Kanjana
94c7de89eb add group from keycloak
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-04-22 14:02:36 +07:00
Kanjana
3bf2446611 Merge remote-tracking branch 'origin/develop' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-22 11:23:36 +07:00
Kanjana
027326a9e4 add check point price 2025-04-22 11:23:12 +07:00
Methapon2001
40e5f495e5 fix: do not check global name conflict
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 12s
2025-04-22 11:20:11 +07:00
Methapon2001
35ec6cc061 chore: migration
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-22 09:47:20 +07:00
Methapon2001
7bd1f57c32 feat: change employee name requirement 2025-04-22 09:47:17 +07:00
Methapon2001
209ef05d3d chore: change endpoint
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-22 09:37:10 +07:00
Kanjana
b90547c622 Merge remote-tracking branch 'chamomind/develop' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-21 11:08:54 +07:00
Kanjana
f98371132a add startDate endDate in instition and receipt , codeProductRecieve in taskOrder 2025-04-21 11:07:06 +07:00
Methapon2001
a25968786d chore: update lock file
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-18 16:32:05 +07:00
Kanjana
e42b772dcf use await Promise.all in uploadedFile Product
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-18 16:23:02 +07:00
Kanjana
05d16f22de add import file product
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-04-18 15:39:02 +07:00
Kanjana
fd7833a592 add endDate,startDate
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-17 17:56:55 +07:00
Kanjana
d52680c23f add startDate, endDate
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-04-17 17:05:19 +07:00
Kanjana
27d3ce6573 change whereDateQuery in branch
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-04-17 16:21:05 +07:00
Kanjana
5147eed15b Merge remote-tracking branch 'origin/develop' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-17 13:43:59 +07:00
Kanjana
0aba9f9865 search startDate and endDate 2025-04-17 13:41:22 +07:00
Methapon2001
f2d0c20ece feat: add endpoint for get same office district
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 8s
2025-04-17 12:56:31 +07:00
Kanjana
ee610c5686 change position
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 11s
2025-04-11 12:50:44 +07:00
Kanjana
62def572de Pull data with CustomerBranch
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s
2025-04-11 12:06:11 +07:00
Kanjana
a06d5514fc updata user add employmentOffice
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-11 11:28:24 +07:00
Methapon2001
70245a2b4f Merge remote-tracking branch 'github/10042025-01' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-10 15:52:21 +07:00
Kanjana
7b28ddd2b2 change update
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-10 15:38:14 +07:00
Methapon Metanipat
1cd292cdf7
Merge pull request #25 from Frappet/10042025-01
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
add incomplete in RequestData
2025-04-10 15:37:36 +07:00
Kanjana
2e71c86b36 add incomplete in RequestData
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s
2025-04-10 15:01:18 +07:00
Methapon2001
08483c162a Merge branch 'patch-08-04-2025' into develop
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2025-04-10 08:41:50 +07:00
Kanjana
afe54b1a4e add insensitive search 2025-04-09 14:05:05 +07:00
Kanjana
743fde5493 add mode: "insensitive" 2025-04-09 11:54:52 +07:00
Kanjana
7e937333dc add payment attachment
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 11s
2025-04-09 09:51:52 +07:00
68 changed files with 5444 additions and 952 deletions

View file

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

View file

@ -7,6 +7,7 @@
"start": "node ./dist/app.js",
"dev": "nodemon",
"check": "tsc --noEmit",
"test": "vitest",
"format": "prettier --write .",
"debug": "nodemon",
"build": "tsoa spec-and-routes && tsc",
@ -24,35 +25,45 @@
"@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",
"barcode": "^0.1.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",

1782
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Employee" ADD COLUMN "otherNationality" TEXT;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EmployeePassport" ADD COLUMN "otherNationality" TEXT;

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "districtTextEN" TEXT,
ADD COLUMN "provinceTextEN" TEXT,
ADD COLUMN "subDistrictTextEN" TEXT;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "RequestWorkStepStatus" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "Payment" ADD COLUMN "account" TEXT,
ADD COLUMN "channel" TEXT,
ADD COLUMN "reference" TEXT;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "public"."Product" ADD COLUMN "flowAccountProductIdAgentPrice" TEXT,
ADD COLUMN "flowAccountProductIdSellPrice" TEXT;

View file

@ -366,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
@ -390,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
@ -424,7 +442,7 @@ model User {
licenseExpireDate DateTime? @db.Date
sourceNationality String?
importNationality String?
importNationality UserImportNationality[]
trainingPlace String?
responsibleArea UserResponsibleArea[]
@ -484,12 +502,17 @@ 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[] @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[]
@ -497,6 +520,10 @@ model User {
remark String?
agencyStatus String?
contactName String?
contactTel String?
quotation Quotation[]
}
model UserResponsibleArea {
@ -535,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
@ -600,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
@ -759,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())
@ -771,11 +813,12 @@ model Employee {
middleName String?
middleNameEN String?
lastName String?
lastNameEN 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?
@ -850,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
@ -878,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?
@ -1012,6 +1057,13 @@ model Institution {
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[]
}
@ -1076,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())
@ -1086,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?
@ -1181,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[]
@ -1353,6 +1418,9 @@ model Quotation {
invoice Invoice[]
creditNote CreditNote[]
seller User? @relation(fields: [sellerId], references: [id], onDelete: Cascade)
sellerId String?
}
model QuotationPaySplit {
@ -1377,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 {
@ -1456,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 {
@ -1540,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
@ -1614,7 +1693,8 @@ model TaskProduct {
model TaskOrder {
id String @id @default(cuid())
code String
code String
codeProductReceived String?
taskName String
taskOrderStatus TaskOrderStatus @default(Pending)

View file

@ -1,4 +1,5 @@
import barcode from "barcode";
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";
@ -33,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,
@ -111,12 +118,12 @@ export class DocTemplateController extends Controller {
) {
const ret = await edmList(
"file",
templateGroup ? [templateGroup, ...DOCUMENT_PATH] : DOCUMENT_PATH,
templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH,
);
if (ret) return ret.map((v) => v.fileName);
}
return await listFile(
(templateGroup ? [templateGroup, ...DOCUMENT_PATH] : DOCUMENT_PATH).join("/") + "/",
(templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH).join("/") + "/",
);
}
@ -253,13 +260,23 @@ export class DocTemplateController extends Controller {
thaiBahtText: (input: string | number) => {
ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input);
},
barcode: async (data: string) =>
new Promise<string>((resolve, reject) =>
barcode("code39", { data, width: 400, height: 100 }).getBase64((err, data) => {
if (!err) return resolve(data);
return reject(err);
}),
),
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);
@ -276,6 +293,7 @@ function replaceEmptyField<T>(data: T): T {
}
type FullAddress = {
addressForeign?: boolean;
address: string;
addressEN: string;
moo?: string;
@ -284,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;
};
@ -319,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:
@ -334,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(" ");
}
@ -354,6 +404,9 @@ function gender(text: string, lang: "th" | "en" = "en") {
}
}
/**
* @deprecated
*/
function businessType(text: string, lang: "th" | "en" = "en") {
switch (lang) {
case "th":

View file

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

View file

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

View file

@ -36,8 +36,8 @@ export class NotificationController extends Controller {
AND: [
{
OR: queryOrNot<(typeof where)[]>(query, [
{ title: { contains: query } },
{ detail: { contains: query } },
{ title: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
]),
},
{

View file

@ -618,9 +618,22 @@ export class StatsController extends Controller {
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"),
@ -629,11 +642,7 @@ export class StatsController extends Controller {
_sum: { amount: true },
where: {
createdAt: { gte: v, lte: date.endOf("month").toDate() },
invoice: {
quotation: {
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
invoiceId: { in: invoices.map((v) => v.id) },
},
by: "paymentStatus",
})

View file

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

View file

@ -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([

View file

@ -27,6 +27,7 @@ import {
listRole,
getUserRoles,
removeUserRoles,
getGroupUser,
} from "../services/keycloak";
import { isSystem } from "../utils/keycloak";
import {
@ -51,6 +52,7 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { retry } from "../utils/func";
@ -59,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;
}
@ -79,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;
@ -97,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;
@ -113,9 +123,16 @@ 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;
@ -123,6 +140,9 @@ type UserCreate = {
remark?: string;
agencyStatus?: string;
contactName?: string | null;
contactTel?: string | null;
};
type UserUpdate = {
@ -156,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;
@ -174,14 +195,24 @@ 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);
@ -273,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,
@ -284,6 +317,8 @@ export class UserController extends Controller {
status,
responsibleDistrictId,
activeBranchOnly,
startDate,
endDate,
);
}
@ -299,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[];
@ -324,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: {
@ -362,12 +399,14 @@ export class UserController extends Controller {
},
},
},
...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,
@ -386,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,
})),
@ -400,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,
@ -411,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()
@ -477,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",
@ -513,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,
@ -668,6 +716,7 @@ export class UserController extends Controller {
const record = await prisma.user.update({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -682,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),
@ -933,3 +986,17 @@ export class UserSignatureController extends Controller {
await deleteFile(fileLocation.user.signature(userId));
}
}
@Route("api/v1/user/{userId}/group")
@Tags("User")
@Security("keycloak")
export class UserGroupController extends Controller {
@Get()
async getUserGroup(@Path() userId: string) {
const groupUser = await getGroupUser(userId);
if (!Array.isArray(groupUser))
throw new Error("Failed. Cannot get user group(s) data from the server.");
return groupUser;
}
}

View file

@ -23,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 = {

View file

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

View file

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

View file

@ -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 = {

View file

@ -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 = {
@ -73,6 +81,7 @@ type EmployeeCreate = {
dateOfBirth?: Date | null;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName?: string;
@ -106,9 +115,10 @@ type EmployeeUpdate = {
nrcNo?: string | null;
dateOfBirth?: Date;
dateOfBirth?: Date | null;
gender?: string;
nationality?: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName?: string;
@ -116,7 +126,7 @@ type EmployeeUpdate = {
middleName?: string | null;
middleNameEN?: string | null;
lastName?: string;
lastNameEN: string;
lastNameEN?: string;
addressEN?: string;
address?: 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([
@ -643,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")
@ -899,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 },
);
}
}

View file

@ -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 = {

View file

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

View file

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

View file

@ -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 = {

View file

@ -24,7 +24,7 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
import { filterStatus } from "../services/prisma";
import { queryOrNot } 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 },

View file

@ -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";
@ -95,6 +95,17 @@ type InstitutionUpdatePayload = {
}[];
};
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 {
@ -108,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")
@ -122,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[];
@ -131,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([
@ -171,13 +196,14 @@ export class InstitutionController extends Controller {
}
@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({
@ -194,6 +220,8 @@ export class InstitutionController extends Controller {
return await tx.institution.create({
include: {
bank: true,
createdBy: true,
updatedBy: true,
},
data: {
...body,
@ -204,13 +232,15 @@ export class InstitutionController extends Controller {
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,
@ -259,7 +289,7 @@ export class InstitutionController extends Controller {
}
@Delete("{institutionId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
@OperationId("deleteInstitution")
async deleteInstitution(@Path() institutionId: string) {
return await prisma.$transaction(async (tx) => {
@ -331,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,
@ -345,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,
@ -375,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,
@ -386,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,
@ -417,7 +447,7 @@ export class InstitutionFileController extends Controller {
}
@Put("bank-qr/{bankId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
async putBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -431,7 +461,7 @@ export class InstitutionFileController extends Controller {
}
@Delete("bank-qr/{bankId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
async delBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,

View file

@ -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({
@ -225,7 +237,7 @@ export class InvoiceController extends Controller {
title: "ใบแจ้งหนี้ใหม่ / New Invoice",
detail: "รหัส / code : " + record.code,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "accountant" } },
groupReceiver: { create: { name: "branch_accountant" } },
},
});

View file

@ -11,6 +11,7 @@ import {
Security,
Tags,
Query,
UploadedFile,
} from "tsoa";
import { Prisma, Product, Status } from "@prisma/client";
@ -27,20 +28,25 @@ import { isSystem } from "../utils/keycloak";
import { filterStatus } from "../services/prisma";
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,6 +390,30 @@ 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,
@ -434,6 +476,18 @@ 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({
@ -444,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}")
@ -495,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 });
}
}

View file

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

View file

@ -24,7 +24,7 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
import { filterStatus } from "../services/prisma";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
type PropertyPayload = {
name: string;
@ -49,15 +49,21 @@ export class PropertiesController extends Controller {
@Query() status?: Status,
@Query() query = "",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot(query, [{ name: { contains: query } }, { nameEN: { contains: query } }]),
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({

View file

@ -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([

View file

@ -36,20 +36,22 @@ 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",
"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);
@ -164,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 =
@ -179,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: {
@ -219,6 +223,7 @@ export class ServiceController extends Controller {
: []),
],
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ServiceWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -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([

View file

@ -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,
},
});
@ -179,6 +209,7 @@ export class QuotationPayment extends Controller {
await tx.quotation
.update({
include: { requestData: true },
where: { id: quotation.id },
data: {
quotationStatus:
@ -236,6 +267,17 @@ export class QuotationPayment extends Controller {
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;

View file

@ -25,7 +25,7 @@ import {
import { isSystem } from "../utils/keycloak";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { precisionRound } from "../utils/arithmetic";
import { queryOrNot } 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);
@ -206,20 +213,22 @@ export class QuotationController extends Controller {
@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" } },
],
},
},
@ -253,6 +262,8 @@ export class QuotationController extends Controller {
},
}
: undefined,
...whereDateQuery(startDate, endDate),
sellerId: sellerId,
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -410,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"),
@ -516,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,
@ -546,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),
);
@ -653,7 +663,14 @@ export class QuotationController extends Controller {
title: "ใบเสนอราคาใหม่ / New Quotation",
detail: "รหัส / code : " + ret.code,
registeredBranchId: ret.registeredBranchId,
groupReceiver: { create: [{ name: "sale" }, { name: "head_of_sale" }] },
groupReceiver: {
create: [
{ name: "sale" },
{ name: "head_of_sale" },
{ name: "accountant" },
{ name: "branch_accountant" },
],
},
},
});
@ -661,7 +678,7 @@ export class QuotationController extends Controller {
}
@Put("{quotationId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
async editQuotation(
@Request() req: RequestWithUser,
@Path() quotationId: string,
@ -797,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 {
@ -827,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),
);
@ -851,6 +866,7 @@ export class QuotationController extends Controller {
finalPrice: 0,
},
);
const changed = list?.some((lhs) => {
const found = record.productServiceList.find((rhs) => {
return (
@ -884,6 +900,20 @@ export class QuotationController extends Controller {
}),
]);
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: {
@ -1005,6 +1035,7 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -1027,6 +1058,7 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;

View file

@ -27,11 +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);
@ -80,6 +81,9 @@ 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, [
@ -91,34 +95,39 @@ export class RequestDataController extends Controller {
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: {
@ -127,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),
),
},
},
},
},
],
},
},
},
@ -142,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([
@ -164,6 +189,7 @@ export class RequestDataController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -182,6 +208,20 @@ export class RequestDataController extends Controller {
employeePassport: {
orderBy: { expireDate: "desc" },
},
customerBranch: {
include: {
province: {
include: {
employmentOffice: true,
},
},
district: {
include: {
employmentOffice: true,
},
},
},
},
},
},
},
@ -192,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}")
@ -231,39 +288,67 @@ export class RequestDataController extends Controller {
return record;
}
@Post("updata-messenger")
@Post("update-messenger")
@Security("keycloak")
async updateRequestData(
@Request() req: RequestWithUser,
@Body()
boby: {
body: {
defaultMessengerId: string;
requestDataId: string[];
},
) {
const record = await prisma.requestData.updateManyAndReturn({
where: {
id: { in: boby.requestDataId },
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
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: boby.defaultMessengerId,
},
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];
});
if (record.length <= 0) throw notFoundError("Request Data");
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(
@ -338,6 +423,17 @@ export class RequestDataActionController extends Controller {
},
},
},
include: {
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
},
},
});
if (!result) throw notFoundError("Request Data");
@ -380,23 +476,88 @@ export class RequestDataActionController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
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 },
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),
});
});
}
@ -528,13 +689,19 @@ export class RequestDataActionController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
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: {
@ -617,14 +784,83 @@ export class RequestDataActionController extends Controller {
},
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
},
},
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
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);
@ -748,6 +984,7 @@ export class RequestListController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -808,6 +1045,7 @@ export class RequestListController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -917,7 +1155,7 @@ export class RequestListController extends Controller {
});
if (record.responsibleUserId === null) {
await prisma.requestWorkStepStatus.update({
await tx.requestWorkStepStatus.update({
where: {
step_requestWorkId: {
step: step,
@ -979,13 +1217,19 @@ export class RequestListController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
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: {
@ -1093,13 +1337,19 @@ export class RequestListController extends Controller {
},
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
});
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;

View file

@ -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;
});
});
}
@ -523,6 +562,8 @@ export class TaskController extends Controller {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}
@ -598,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,
@ -616,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: {
@ -677,6 +758,8 @@ export class TaskActionController extends Controller {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
]);
@ -690,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: {
@ -809,10 +923,34 @@ 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.requestData
.updateManyAndReturn({
where: { id: { in: completed } },
include: {
quotation: {
select: {
registeredBranchId: true,
createdByUserId: true,
},
},
},
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: {
@ -852,13 +990,19 @@ export class TaskActionController extends Controller {
},
})
.then(async (res) => {
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
});
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();
@ -979,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: {
@ -1021,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([
@ -1094,19 +1241,23 @@ export class UserTaskController extends Controller {
},
})
.then(async (v) => {
await tx.notification.createMany({
data: [
{
title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed",
detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress",
receiverId: v.createdByUserId,
},
{
title: "มีการรับงาน / Task Accepted",
detail: "รหัสใบสั่งงาน / Order : " + v.code,
receiverId: v.createdByUserId,
},
],
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({

View file

@ -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,7 +154,8 @@ 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, [
@ -153,17 +166,16 @@ export class CreditNoteController extends Controller {
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,
@ -470,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;
@ -569,6 +640,14 @@ 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) {
@ -587,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;
});
}
}

View file

@ -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),
);
@ -572,7 +582,7 @@ export class DebitNoteController extends Controller {
}
@Put("{debitNoteId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async updateDebitNote(
@Request() req: RequestWithUser,
@Path() debitNoteId: string,
@ -596,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 = {
@ -667,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 {
@ -695,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),
);

View file

@ -25,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";
@ -51,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
@ -58,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),
]) ?? []),
]
@ -87,6 +89,7 @@ export class LineController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
@ -173,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" } },
],
},
},
@ -198,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" } },
],
},
},
@ -247,6 +251,7 @@ export class LineController extends Controller {
],
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.RequestDataWhereInput;
const [result, total] = await prisma.$transaction([
@ -604,41 +609,26 @@ export class LineController extends Controller {
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() query = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR:
query || pendingOnly
? [
...(queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
],
},
},
]) || []),
...(queryOrNot<Prisma.QuotationWhereInput[]>(!!pendingOnly, [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
]) || []),
]
: undefined,
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" } },
],
},
},
]),
isDebitNote: false,
code,
payCondition,
@ -660,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([
@ -1368,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)),
);
}
}

View file

@ -90,7 +90,7 @@ export class WebHookController extends Controller {
firstNameEN: true,
lastName: true,
lastNameEN: true,
customerName: true,
registerName: true,
customer: {
select: {
customerType: true,
@ -133,13 +133,13 @@ export class WebHookController extends Controller {
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 =

View 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 },
});
});
}
}

View 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`));
}
}

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import dayjs from "dayjs";
import { CronJob } from "cron";
import prisma from "../db";
import { Prisma } from "@prisma/client";
const jobs = [
CronJob.from({
@ -38,6 +39,162 @@ const jobs = [
.catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e));
},
}),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
const employeeExpireData = await prisma.employee.findMany({
include: {
employeePassport: {
orderBy: {
expireDate: "desc",
},
take: 1,
},
customerBranch: {
include: {
customer: true,
},
},
quotationWorker: {
include: {
quotation: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
where: {
employeePassport: {
some: {
expireDate: dayjs().add(90, "day").toDate(),
},
},
},
});
await Promise.all(
employeeExpireData.map(async (record) => {
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
const expireDate = `${dayjs(record.employeePassport[0].expireDate).format("DD/MM")}/${dayjs(record.employeePassport[0].expireDate).year() + 543}`;
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} หนังสือเดินทางจะหมดอายุในวันที่ ${expireDate}`;
const duplicateText = await prisma.notification.findFirst({
where: {
detail: textDetail,
},
});
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
title: "หนังสือเดินทางลูกจ้างหมดอายุ / Employee Passport Expire",
detail: textDetail,
};
if (record.quotationWorker && record.quotationWorker.length > 0) {
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
dataNotification.registeredBranchId =
record.quotationWorker[0].quotation.registeredBranchId;
} else {
(dataNotification.groupReceiver = {
create: [{ name: "sale" }, { name: "head_of_sale" }],
}),
(dataNotification.registeredBranchId =
record.customerBranch.customer.registeredBranchId);
}
if (!duplicateText) {
await prisma.notification
.create({
data: dataNotification,
})
.then(() => console.log("[INFO]: Create notification employee passport expired, OK."))
.catch((e) =>
console.error("[ERR]: Create notification employee passport expired, FAILED.", e),
);
}
}),
);
},
}),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
const employeeVisaData = await prisma.employee.findMany({
include: {
employeeVisa: {
orderBy: {
expireDate: "desc",
},
take: 1,
},
customerBranch: {
include: {
customer: true,
},
},
quotationWorker: {
include: {
quotation: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
where: {
employeeVisa: {
some: {
expireDate: dayjs().add(90, "day").toDate(),
},
},
},
});
await Promise.all(
employeeVisaData.map(async (record) => {
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
const expireDate = `${dayjs(record.employeeVisa[0].expireDate).format("DD/MM")}/${dayjs(record.employeeVisa[0].expireDate).year() + 543}`;
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} ข้อมูลการตรวจลงตราจะหมดอายุในวันที่ ${expireDate}`;
const duplicateText = await prisma.notification.findFirst({
where: {
detail: textDetail,
},
});
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
title: "ข้อมูลการตรวจลงตราลูกจ้างหมดอายุ / Employee Visa Expire",
detail: textDetail,
};
if (record.quotationWorker && record.quotationWorker.length > 0) {
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
dataNotification.registeredBranchId =
record.quotationWorker[0].quotation.registeredBranchId;
} else {
(dataNotification.groupReceiver = {
create: [{ name: "sale" }, { name: "head_of_sale" }],
}),
(dataNotification.registeredBranchId =
record.customerBranch.customer.registeredBranchId);
}
if (!duplicateText) {
await prisma.notification
.create({
data: dataNotification,
})
.then(() => console.log("[INFO]: Create notification employee visa expired, OK."))
.catch((e) =>
console.error("[ERR]: Create notification employee visa expired, FAILED.", e),
);
}
}),
);
},
}),
];
export function initSchedule() {

View file

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

View 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 [
"**** เงื่อนไขเพิ่มเติม",
"- เงื่อนไขการชำระเงิน แบบเต็มจำนวน",
`&nbsp; จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
].join("<br/>");
} else {
return [
"**** เงื่อนไขเพิ่มเติม",
`- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === "SplitCustom" ? " กำหนดเอง " : " "}${context?.installments?.length} งวด`,
...(context?.installments?.map(
(v) => `&nbsp; งวดที่ ${v.no} จำนวน ${formatNumberDecimal(v.amount, 2)}`,
) || []),
].join("<br />");
}
},
},
} as const;
type Template = typeof templates;
type TemplateName = keyof Template;
type TemplateContext = {
[key in TemplateName]?: Parameters<Template[key]["converter"]>[0];
};
export function convertTemplate(
text: string,
context?: TemplateContext,
templateUse?: TemplateName[],
) {
let ret = text;
for (const [name, template] of Object.entries(templates)) {
if (templateUse && !templateUse.includes(name as TemplateName)) continue;
ret = ret.replace(
new RegExp("\\#\\[" + name.replaceAll("-", "\\-") + "\\]", "g"),
typeof template.converter === "function"
? template.converter(context?.[name as TemplateName] as any)
: template.converter,
);
}
return ret;
}

View file

@ -62,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
View 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
View 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;
}
};
}

5
vite.config.ts Normal file
View file

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